# What's new in our Agentic Framework

> Six new features in the Corti Agentic Framework: connectors and local tools, token streaming, inline citations, a feedback API, OpenInference traces, and open-source evals. What each one does and the API calls behind it.

*2026-11-06* — [https://corti.ai/guides/what-is-new-in-our-agentic-framework](https://corti.ai/guides/what-is-new-in-our-agentic-framework)

## Summary

- **Connectors set what an agent can do.** An agent now has one `connectors` array covering registry, MCP, agent, A2A, and schema connectors. You can browse the registry and attach or remove connectors one at a time. A schema connector with `transition: "input_required"` lets the agent call a tool that runs on your side.
- **Streaming, citations, feedback, and traces describe a single run.** The answer streams token by token. Each claim links back to the connector data it came from. Feedback attaches to the task that produced the answer, and the trace shows every LLM and tool call behind it.
- **Evals turn a run into a repeatable test.** A YAML suite describes the agent, the message, and the expected result, and the harness scores real runs against it.

## Connectors and local code connectors

An agent's capabilities come from its connectors. There are five types in a single `connectors` array:

- `registry`: Corti-hosted connectors, such as literature search, drug databases, and coding.
- `mcp`: any MCP server you run or subscribe to.
- `agent`: another Corti agent.
- `a2a`: any external A2A-compliant agent.
- `schema`: a tool defined by a JSON Schema and executed by you.

**The registry is browsable over the API.** You can list the available connectors and attach the ones you need. Connectors are a sub-resource of the agent at `/agents/{id}/connectors`, so adding or removing one doesn't mean rewriting the whole agent definition.

`const page = await client.agentic.registry.connectors.list({ pageSize: 100 });  
  
await client.agentic.agents.connectors.create(agentId, {  
  type: "registry",  
  name: page.data[0].id, // the registry entry id is the connector name  
});`

**Local code connectors keep execution on your side.** A local code connector is a `schema` connector with `transition: "input_required"`. The agent sees the tool's schema, decides when to call it, and fills in the arguments. When it calls the tool, the task pauses in `TASK_STATE_INPUT_REQUIRED` and the arguments are stored as a data part. Your code runs the tool and resumes the same task by sending the result with the task's `taskId`.

`await client.agentic.agents.connectors.create(agentId, {  
  type: "schema",  
  name: "lookup_local_medications",  
  description: "Fetch the patient's active medications from the caller's local EHR.",  
  transition: "input_required", // pause the task; the caller runs the tool  
  schema: {  
    type: "object",  
    properties: { reason: { type: "string" } },  
    required: ["reason"],  
  },  
});`

The tool's implementation never leaves your system. This suits data you can't expose to a hosted service, such as an on-premise EHR, an internal formulary, or a system behind your network boundary. You don't need to stand up and host an MCP server to get it.

## Streaming

`message:stream` returns server-sent events. Each event carries exactly one of `task`, `statusUpdate`, `artifactUpdate`, or `message`.

The answer arrives as `artifactUpdate` deltas:

- `append: true` extends the artifact with the next tokens.
- A closing event with `lastChunk: true` and `append: false` replaces the accumulated deltas with the canonical text. Citation metadata attaches to this final version.

If the agent writes text before a tool call, that text arrives as a separate artifact tagged `metadata.type: "intermediate-thinking"`. It isn't part of the answer, so don't render it as one.

A stream ends on a terminal status (`completed`, `failed`, `canceled`, `rejected`) or an interrupted one (`input-required`, `auth-required`). Interrupted tasks aren't finished. You resume them by sending another message with the same `taskId`. That is also how a local code connector call works.

`const stream = await client.agentic.agents.streamMessage(agentId, { message });  
for await (const event of stream) {  
  // fold task / statusUpdate / artifactUpdate / message events into UI state  
}  
  
// The task paused on the local tool: run it and resume the same task.  
await client.agentic.agents.streamMessage(agentId, {  
  message: {  
    role: "ROLE_USER",  
    contextId,  
    taskId, // same task, resumed  
    parts: [{ data: localToolResult }],  
  },  
});`

On A2A v1.0, a `completed` status carries no answer message. Read the answer from the completion artifact. See [Stream responses](https://docs.corti.ai/agentic/guides/stream-responses) for the full event reference.

## Citations

When the agent answers from connector data, the completion artifact's text part carries `metadata.citations`. Each citation has three fields:

- `data_part_id`: the source, a sibling data part in the same artifact identified by `metadata.dataPartId`.
- `offset`: where in the answer the citation belongs.
- `locator`: which part of the source is cited.

Offsets count characters (code points) across the joined text of all text parts, so index into `Array.from(text)` rather than the UTF-16 string. A locator starting with `/` is a JSON Pointer into the data part's value, and a locator like `L8-L13` is a line range. If a locator doesn't resolve, cite the whole part.

`const chars = Array.from(parts.map((p) => p.text ?? "").join(""));  
const sources = new Map(  
  parts.filter((p) => p.metadata?.dataPartId).map((p) => [p.metadata.dataPartId, p]),  
);  
  
for (const c of citations) {  
  const source = sources.get(c.data_part_id);  
  // insert a footnote marker at chars[c.offset] that reveals the cited source  
}`

This lets a clinician check any claim against the record or reference it came from, without leaving your interface. Citations only appear when the answer used connector data. When the model answers from its own knowledge, the text has no citations, and your UI can show that difference.

## Feedback API

Feedback is a resource on the task at `/contexts/{id}/tasks/{taskId}/feedback`. Each entry has:

- a binary rating;
- up to five structured labels, such as `missingInformation` or `unsupportedClaim`, where `other` requires a reason;
- an optional free-text reason;
- an optional target: the whole task, or a single message via `target.messageId`.

`metadata` records provenance: the collection method, a client reference, and an `actor.externalId`. The actor ID must be pseudonymous. Never put a name, email, or MRN in it.

`await client.agentic.contexts.tasks.feedback.create(contextId, taskId, {  
  rating: { scale: "binary", value: 0 },  
  labels: ["missingInformation", "unsupportedClaim"],  
  reason: "Didn't account for the anticoagulant.",  
  target: { messageId: agentMessageId },  
  metadata: { collectionMethod: "thumbs", actor: { externalId: "user-7f3a91c2" } },  
});`

Because feedback is stored against the task, every rating points to the exact run that produced the answer. You can review a negative rating next to its trace instead of trying to reconstruct what happened. Listing feedback returns only the caller's own entries, newest first.

## Traces

`GET /contexts/{id}/trace` exports a context as [OpenInference](https://github.com/Arize-ai/openinference) traces. Traces come back newest first, with spans inline and pagination via `pageSize` and `pageToken`.

Each span has a `parent_span_id`, timing, and flat attributes:

- `openinference.span.kind`: `CHAIN`, `LLM`, or `TOOL`
- `llm.token_count.*`
- `tool.name`
- `input.value` and `output.value`

`let page = await client.agentic.contexts.trace(contextId, { pageSize: 100 });  
const items = [...page.data];  
while (page.hasNextPage()) {  
  page = await page.getNextPage();  
  items.push(...page.data);  
}`

Rebuild the tree from `parent_span_id` to see which LLM calls and connector calls a run made, in what order, and how long each took. To get the token total, sum `llm.token_count.*` over `LLM` spans only, because `CHAIN` spans can carry aggregates of their children.

OpenInference is an open format, so you can load the export into any observability tool that reads it. Trace ingestion can lag the task by a few seconds, so retry if the result is empty.

## Open-source evals

A YAML suite describes each case: the agent and its connectors, the message or sequence of messages, and the expected result.

`evals:  
  - name: medication_safety_check  
    type: sequential  
    agent:  
      name: "Medication safety (eval)"  
      connectors:  
        - type: registry  
          name: drugbank  
        - type: schema  
          name: lookup_local_medications  
          transition: input_required  
    steps:  
      - name: ask  
        expectations:  
          expected_state: input-required  
      - name: return_local_tool_result  
        expectations:  
          expected_state: completed  
          must_include:  
            - warfarin  
          trace:  
            - kind: TOOL  
              name: lookup_local_medications`

`uv run agent-evals run <suite> --env eu` provisions the agent, sends the messages, and scores the result. You can test against three kinds of expectation:

- **The final task state.**
- **Required content.** `must_include` matches whole words, case-sensitively, against the task.
- **The trace.** For example, "the run called this tool exactly once".

A run that pauses on a local code connector becomes a `sequential` case. The first step expects `input-required` and passes its `taskId` to the next step, which sends the tool result.

Checking the trace means a passing eval proves the agent used the right tools to get the right answer, not just that the answer contained the right word. Run the suite in CI and you'll find out when a prompt change, a connector change, or a model update changes behavior, before your users do.

## How the features fit together

All six features describe the same run.

1. Connectors set which systems the agent can reach, including ones only your code can reach.
2. Streaming shows the answer as it's generated, and pauses for local tool calls.
3. Citations link each claim in the answer to the connector data behind it.
4. Feedback records whether the answer was good, against the exact task.
5. Traces record how the answer was produced: every LLM call, tool call, and token count.
6. Evals turn that run into a test you can repeat on every change.

## Getting started

- **API Docs:** [docs.corti.ai](https://docs.corti.ai)
- **SDK:** `npm install @corti/sdk`
