> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corti.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent SDK core concepts

> Learn the building blocks of the Corti Agent SDK: client, agents, contexts, connectors, message responses, streaming, and credentials.

<Warning>
  The Corti Agent SDK is in **alpha v2 private preview**. The API may change between releases. Contact [help@corti.ai](mailto:help@corti.ai) to request access.
</Warning>

This page covers the core building blocks you use in every Agent SDK application.

## CortiClient

The root client. You create one instance and pass it to every resource client. It handles authentication, base URL resolution, and the `Tenant-Name` and `A2A-Version` headers automatically.

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  import { CortiClient } from "@corti/agent-sdk";

  // Replace these with your values
  const TENANT = "<your-tenant-name>";
  const TOKEN = "<your-access-token>";

  const client = new CortiClient({ token: TOKEN, tenant: TENANT });
  ```

  ```python title="Python" theme={null}
  from corti_agent_sdk import CortiClient

  # Replace these with your values
  CLIENT_ID = "<your-client-id>"
  CLIENT_SECRET = "<your-client-secret>"
  ENVIRONMENT = "<eu-or-us>"
  TENANT = "<your-tenant-name>"

  client = CortiClient(
      tenant_name=TENANT,
      environment=ENVIRONMENT,
      auth={"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
  )
  ```
</CodeGroup>

<Tip>
  The TypeScript client accepts a `tokenProvider` function instead of a static `token` for OAuth flows where the token expires. The client calls it before every request and uses the return value.
</Tip>

The client exposes typed resource clients:

| Property           | Covers                                                                |
| ------------------ | --------------------------------------------------------------------- |
| `client.agents`    | Agent CRUD (`POST`, `GET`, `PATCH`, `DELETE` on `/v2/agentic/agents`) |
| `client.contexts`  | Contexts, tasks, and traces                                           |
| `client.registry`  | Browse pre-built registry connectors                                  |
| `client.usage`     | Agent usage metrics                                                   |
| `client.feedback`  | Submit task feedback                                                  |
| `client.agentCard` | A2A agent card                                                        |
| `client.models`    | List available models                                                 |

<Note>
  The resource clients above are available in the TypeScript SDK. The Python SDK currently provides `AgentsClient` only; additional resource clients will be added in the v2 rebuild.
</Note>

The TypeScript package also exposes a `client.raw` property: the underlying `openapi-fetch` client for direct API access when you need an endpoint the SDK does not wrap yet.

## Agents

An agent is a reusable unit of behaviour: a name, a description, a system prompt, and a set of tools (connectors). You create agents via the agents resource client, which returns typed `AgentHandle` objects.

### Create an agent

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  import { AgentHandle, connectors } from "@corti/agent-sdk";

  // Replace these with your values
  const REGISTRY_NAME = "@corti/medical-coding";
  const TENANT = "<your-tenant-name>";
  const TOKEN = "<your-access-token>";

  const client = new CortiClient({ token: TOKEN, tenant: TENANT });

  const agent = await client.agents.create({
    name: "my-agent",
    description: "Handles medical coding queries",
    systemPrompt: "You are a medical coding assistant.",
    lifecycle: "persistent",
    connectors: [connectors.registry(REGISTRY_NAME)],
  });

  const handle = new AgentHandle(agent, client);
  ```

  ```python title="Python" theme={null}
  from corti_agent_sdk import CortiClient, AgentsClient, connectors

  # Replace these with your values
  CLIENT_ID = "<your-client-id>"
  CLIENT_SECRET = "<your-client-secret>"
  ENVIRONMENT = "<eu-or-us>"
  REGISTRY_NAME = "@corti/medical-coding"
  TENANT = "<your-tenant-name>"

  async with CortiClient(
      tenant_name=TENANT,
      environment=ENVIRONMENT,
      auth={"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
  ) as client:
      agents = AgentsClient(client)

      agent = await agents.create(
          name="my-agent",
          description="Handles medical coding queries",
          system_prompt="You are a medical coding assistant.",
          lifecycle="persistent",
          connectors=[connectors.registry(name=REGISTRY_NAME)],
      )
  ```
</CodeGroup>

### Fetching and listing

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  const handle = new AgentHandle(await client.agents.get("<your-agent-id>"), client);
  const list = await client.agents.list();
  ```

  ```python title="Python" theme={null}
  agent = await agents.get("<your-agent-id>")
  all_agents = await agents.list()
  ```
</CodeGroup>

### Updating

Only the fields you pass are sent. Passing `connectors` **replaces** the full set.

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  const updated = await handle.update({ systemPrompt: "Be more concise." });
  ```

  ```python title="Python" theme={null}
  updated = await agent.update(system_prompt="Be more concise.")
  ```
</CodeGroup>

### AgentHandle

An `AgentHandle` wraps an agent response and provides conversation helpers. In TypeScript, `client.agents.create()` returns a raw `Agent` object; you wrap it explicitly. In Python, `AgentsClient.create()` returns an `AgentHandle` directly.

| Member                                                  | Description                                                  |
| ------------------------------------------------------- | ------------------------------------------------------------ |
| `id`                                                    | Server-assigned agent ID                                     |
| `name`, `description`, `systemPrompt` / `system_prompt` | Agent metadata                                               |
| `raw`                                                   | The underlying API response object                           |
| `createContext()` / `create_context()`                  | Open a new conversation thread                               |
| `getContext(id)` / `get_context(id)`                    | Resume an existing thread by ID                              |
| `run(input, opts?)`                                     | One-shot: create a context, send a message, return the reply |
| `stream(input, opts?)`                                  | One-shot streaming (TypeScript only)                         |
| `update(patch)`                                         | Partially update the agent, returns a new handle             |
| `refresh()`                                             | Re-fetch the agent from the API                              |
| `delete()`                                              | Delete the agent                                             |

### Lifecycle: ephemeral vs persistent

Agents default to `ephemeral`, which means the server cleans them up automatically. Use `persistent` only when the agent must survive process restarts.

|                            | `ephemeral` (default)                           | `persistent`                                                                          |
| -------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------- |
| Cleaned up by server       | Yes, automatically                              | No, you must call `delete()`                                                          |
| Visible in `agents.list()` | No                                              | Yes                                                                                   |
| Survives process restarts  | No                                              | Yes, store `agent.id`                                                                 |
| Best for                   | Scripts, request handlers, tests, one-off tasks | Long-lived bots or services where recreating the agent on every deploy is undesirable |

<Note>
  Lifecycle controls the agent definition (system prompt, connectors). Conversation threads (`AgentContext`) are always managed automatically regardless of lifecycle. You never need to create or delete threads manually.
</Note>

Use ephemeral unless you have a specific reason not to. Persistent agents accumulate in your tenant if you forget to delete them.

## Contexts and conversations

An `AgentContext` represents a single conversation thread. Contexts are lazy: no network call is made until the first message is sent, at which point the server creates the thread and returns a `contextId` that the SDK tracks automatically.

### Multi-turn conversation

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  const ctx = handle.createContext();

  const reply1 = await ctx.sendText("What is the ICD-10 code for hypertension?");
  console.log(reply1.text); // "The ICD-10 code is I10."

  // Follow-ups on the same context remember prior turns
  const reply2 = await ctx.sendText("What about type 2 diabetes?");
  console.log(reply2.text); // "The ICD-10 code is E11.9."
  ```

  ```python title="Python" theme={null}
  ctx = agent.create_context()

  reply1 = await ctx.send_text("What is the ICD-10 code for hypertension?")
  print(reply1.text)  # "The ICD-10 code is I10."

  # Follow-ups on the same context remember prior turns
  reply2 = await ctx.send_text("What about type 2 diabetes?")
  print(reply2.text)  # "The ICD-10 code is E11.9."
  ```
</CodeGroup>

<Note>
  You do not need to manage context IDs yourself. Keep the context object in memory across turns. Only persist `ctx.id` if you need to resume the exact same thread after a process restart, and use `handle.getContext(id)` to do so.
</Note>

### One-shot helper

No context object needed for single-shot invocations:

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  const reply = await handle.run("What is the ICD-10 code for asthma?");
  ```

  ```python title="Python" theme={null}
  reply = await agent.run("What is the ICD-10 code for asthma?")
  ```
</CodeGroup>

### Resuming a thread across sessions

If you need to continue a thread after a process restart, persist `ctx.id` (only available after the first turn) and use `handle.getContext(id)` / `agent.get_context(id)` next time.

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  // Session 1
  const ctx = handle.createContext();
  await ctx.sendText("Hello");
  const savedId = ctx.id!; // persist this

  // Session 2
  const ctx2 = handle.getContext(savedId);
  await ctx2.sendText("What did I say last time?");
  ```

  ```python title="Python" theme={null}
  # Session 1
  ctx = agent.create_context()
  await ctx.send_text("Hello")
  saved_id = ctx.id  # persist this

  # Session 2
  ctx2 = agent.get_context(saved_id)
  await ctx2.send_text("What did I say last time?")
  ```
</CodeGroup>

This is the only valid way to pass a context ID. Do not construct `AgentContext` yourself or pass IDs to `createContext()`.

### sendText vs sendMessage vs streamMessage

All three live on `AgentContext` and send to the same thread (the `contextId` is shared). The only differences are the input shape and whether the response is buffered or streamed.

| Method                                           | Input                       | Output                        | Use when                                          |
| ------------------------------------------------ | --------------------------- | ----------------------------- | ------------------------------------------------- |
| `sendText(text)` / `send_text(text)`             | `string`                    | `Promise<MessageResponse>`    | You only need to send plain text                  |
| `sendMessage(parts)` / `send_message(parts)`     | `Part[]` (text, data, file) | `Promise<MessageResponse>`    | You need to attach data, files, or mix part kinds |
| `streamMessage(parts)` / `stream_message(parts)` | `Part[]`                    | `AsyncGenerator<StreamEvent>` | You want incremental tokens as they arrive        |

`sendText` is a convenience wrapper for `sendMessage`:

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  ctx.sendText("hello");
  // is equivalent to
  ctx.sendMessage([{ text: "hello" }]);
  ```

  ```python title="Python" theme={null}
  await ctx.send_text("hello")
  # is equivalent to
  await ctx.send_message([{"text": "hello"}])
  ```
</CodeGroup>

When to reach for `sendMessage` over `sendText`:

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  // Mix text + structured data in one turn
  await ctx.sendMessage([
    { text: "Use the attached patient record:" },
    { data: { patientId: "abc", age: 62 } },
  ]);

  // Send a file
  await ctx.sendMessage([
    { file: { name: "scan.pdf", mimeType: "application/pdf", uri: "https://..." } },
  ]);
  ```

  ```python title="Python" theme={null}
  # Mix text + structured data in one turn
  await ctx.send_message([
      {"text": "Use the attached patient record:"},
      {"data": {"patientId": "abc", "age": 62}},
  ])

  # Send a file
  await ctx.send_message([
      {"file": {"name": "scan.pdf", "mimeType": "application/pdf", "uri": "https://..."}}
  ])
  ```
</CodeGroup>

`streamMessage` returns events incrementally; `sendMessage` waits for `completed` or `failed` and returns the final aggregate. Streaming has no buffered `MessageResponse`: assemble the text yourself by concatenating `event.message.parts`.

## Connectors

Connector factories build typed connector definitions. You declare connectors at agent creation time in the `connectors` array. The agent can call them autonomously when its prompt suggests doing so.

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  import { connectors } from "@corti/agent-sdk";

  connectors.registry("@corti/medical-coding");
  connectors.mcp({ name: "my-mcp", url: "https://mcp.example.com" });
  connectors.agent("<your-agent-id>");
  connectors.a2a("https://remote-agent.example.com");
  connectors.schema({ name: "structured-output", schema: { type: "object" } });
  ```

  ```python title="Python" theme={null}
  from corti_agent_sdk import connectors

  connectors.registry(name="@corti/medical-coding")
  connectors.mcp(mcp_url="https://mcp.example.com")
  connectors.from_agent(agent_id="<your-agent-id>")
  connectors.a2a(a2a_url="https://remote-agent.example.com")
  ```
</CodeGroup>

| Factory                                   | Type       | Required fields        | Description                                                       |
| ----------------------------------------- | ---------- | ---------------------- | ----------------------------------------------------------------- |
| `registry(name)`                          | `registry` | `name`                 | Use a published Corti registry connector                          |
| `mcp({ name, url })` / `mcp(mcp_url=...)` | `mcp`      | `url` / `mcp_url`      | Attach an MCP server                                              |
| `agent(agentId)` / `from_agent(agent_id)` | `agent`    | `agentId` / `agent_id` | Wire another Corti agent in as a sub-agent                        |
| `a2a(url)` / `a2a(a2a_url)`               | `a2a`      | `url` / `a2a_url`      | Connect to a remote A2A agent                                     |
| `schema({ name, schema })`                | `schema`   | `name`, `schema`       | Define a JSON Schema tool for structured output (TypeScript only) |

<Note>
  The `schema` connector type is new in v2 and currently available in the TypeScript package only. The Python package will add it in the v2 rebuild.
</Note>

### Registry connectors

Typical registry connectors include:

| Name                | What it does                                                   |
| ------------------- | -------------------------------------------------------------- |
| `web-search-expert` | Live web search, good for recent guidelines and drug approvals |
| `coding-expert`     | ICD-10 and medical coding, translates clinical terms to codes  |
| `pubmed-expert`     | PubMed literature search, finds relevant clinical studies      |

To see what connectors are available in your tenant, browse the [registry connector reference](/agentic/registry/overview).

### Building a clinical orchestrator

Use multiple connectors together to build a well-rounded clinical orchestrator:

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  import { CortiClient, AgentHandle, connectors } from "@corti/agent-sdk";

  // Replace these with your values
  const TENANT = "<your-tenant-name>";
  const TOKEN = "<your-access-token>";

  const client = new CortiClient({ token: TOKEN, tenant: TENANT });

  // A small worker agent
  const symptomExtractor = await client.agents.create({
    name: "symptom-extractor",
    description: "Extracts symptoms from a clinical note.",
    systemPrompt:
      "You are a symptom extractor. Reply with ONLY a comma-separated list of symptoms. Never ask for clarification.",
  });
  const extractorHandle = new AgentHandle(symptomExtractor, client);

  // An orchestrator that wires sub-agent + registry connectors
  const orchestrator = await client.agents.create({
    name: "triage-orchestrator",
    description: "Triages a clinical note.",
    systemPrompt:
      "Pass the note to symptom-extractor, then write a one-paragraph triage recommendation.",
    connectors: [
      connectors.agent(extractorHandle.id),
      connectors.registry("web-search-expert"),
      connectors.registry("coding-expert"),
    ],
  });
  const orchestratorHandle = new AgentHandle(orchestrator, client);

  // Orchestrators fan out: raise the timeout
  const reply = await orchestratorHandle.run(
    "62yo with sudden severe headache, photophobia, neck stiffness.",
    { timeoutInSeconds: 180 },
  );
  console.log(reply.text);
  ```

  ```python title="Python" theme={null}
  import asyncio
  from corti_agent_sdk import CortiClient, AgentsClient, connectors

  # Replace these with your values
  CLIENT_ID = "<your-client-id>"
  CLIENT_SECRET = "<your-client-secret>"
  ENVIRONMENT = "<eu-or-us>"
  TENANT = "<your-tenant-name>"


  async def main():
      async with CortiClient(
          tenant_name=TENANT,
          environment=ENVIRONMENT,
          auth={"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
      ) as client:
          agents = AgentsClient(client)

          symptom_extractor = await agents.create(
              name="symptom-extractor",
              description="Extracts symptoms from a clinical note.",
              system_prompt="You are a symptom extractor. Reply with ONLY a comma-separated list of symptoms.",
          )

          orchestrator = await agents.create(
              name="triage-orchestrator",
              description="Triages a clinical note.",
              system_prompt="Pass the note to symptom-extractor, then write a one-paragraph triage recommendation.",
              connectors=[
                  connectors.from_agent(agent_id=symptom_extractor.id),
                  connectors.registry(name="web-search-expert"),
                  connectors.registry(name="coding-expert"),
              ],
          )

          # Orchestrators fan out: raise the timeout
          reply = await orchestrator.run(
              "62yo with sudden severe headache, photophobia, neck stiffness.",
              timeout_in_seconds=180,
          )
          print(reply.text)


  asyncio.run(main())
  ```
</CodeGroup>

## MessageResponse

Every non-streaming call returns a `MessageResponse` that promotes the fields you most often need to the top level.

| Member                     | Type                  | Description                                                                             |
| -------------------------- | --------------------- | --------------------------------------------------------------------------------------- |
| `text`                     | `string \| null`      | All text parts from the agent reply, joined                                             |
| `status`                   | `string`              | Task state: `completed`, `failed`, `working`, `submitted`, `canceled`, `input-required` |
| `artifacts`                | `Artifact[]`          | Structured outputs the agent attached, deduplicated                                     |
| `contextId` / `context_id` | `string \| undefined` | The thread ID, set after the first turn                                                 |
| `taskId` / `task_id`       | `string \| undefined` | The task ID for this invocation                                                         |
| `raw`                      | `object`              | The full, unmodified API response                                                       |

## Streaming

Streaming responses arrive as an async generator of `StreamEvent` objects. Each event is one of `task`, `message`, `statusUpdate`, or `artifactUpdate`.

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  for await (const event of ctx.streamMessage([{ text: "Explain photosynthesis." }])) {
    // Status transitions: "submitted" -> "working" -> "completed" | "failed"
    if (event.statusUpdate) {
      console.log(`[${event.statusUpdate.status.state}]`);
    }
    // Incremental message parts
    if (event.message) {
      for (const part of event.message.parts) {
        if (part.text) process.stdout.write(part.text);
      }
    }
    // event.artifactUpdate: emitted when the agent attaches structured outputs
  }
  ```

  ```python title="Python" theme={null}
  async for event in ctx.stream_message([{"text": "Explain photosynthesis."}]):
      if "statusUpdate" in event:
          print(f"[{event['statusUpdate']['status']['state']}]")
      if "message" in event:
          for part in event["message"]["parts"]:
              if "text" in part:
                  print(part["text"], end="")
  ```
</CodeGroup>

`streamMessage` tracks the same `contextId` as `sendMessage`. You can freely mix streaming and non-streaming calls on a single context.

## Connector auth

In the TypeScript v2 SDK, connector auth is configured on the connector itself at agent creation time, not forwarded as credentials at call time. Use the `auth` factory to specify the auth type:

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  import { connectors, auth } from "@corti/agent-sdk";

  // Replace these with your values
  const MCP_URL = "https://mcp.example.com";

  connectors.mcp({
    name: "my-mcp",
    url: MCP_URL,
    auth: auth.bearer(),
  });

  connectors.mcp({
    name: "secure-mcp",
    url: MCP_URL,
    auth: auth.apiKey("<your-secret-ref>"),
  });

  connectors.mcp({
    name: "oauth-mcp",
    url: MCP_URL,
    auth: auth.oauth2({ scope: "read", redirectUrl: "https://app.example.com/callback" }),
  });
  ```
</CodeGroup>

The Python SDK (still on v1 architecture) uses a `CredentialStore` passed to `create_context()`. The credential key must match the connector's `name`:

<CodeGroup>
  ```python title="Python" theme={null}
  # Replace these with your values
  MCP_TOKEN = "<your-mcp-token>"
  MCP_URL = "https://mcp.example.com"

  agent = await agents.create(
      name="auth-demo",
      description="Calls an auth-protected MCP server.",
      connectors=[connectors.mcp(mcp_url=MCP_URL, name="my-mcp", auth_type="bearer")],
  )

  ctx = agent.create_context(credentials={
      "my-mcp": {"type": "token", "token": MCP_TOKEN},
  })

  reply = await ctx.send_text("List the tools you have access to.")
  print(reply.status)  # "completed"
  ```
</CodeGroup>

<Tip>
  When an MCP connector requires auth, the agent may reply with status `auth-required` until you provide credentials. The Python SDK forwards credentials transparently and re-sends them if the agent asks again.
</Tip>
