content-agent 0.8.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,9 +35,9 @@ const { text } = await agent.generate({
35
35
 
36
36
  > **Why a class instead of `streamText({ model, ... })`?**
37
37
  >
38
- > The AI SDK 6 explicitly invites SDK authors to ship their own `Agent` implementations: ["The `Agent` is now an interface instead of a class, which allows developers to implement their own agent abstractions for specific needs."](https://ai-sdk.dev/blog/ai-sdk-6) `ContentAgent` is that implementation for the Sanity Content Agent — it preconfigures `stopWhen`, `prepareStep`, and `toolChoice` for the agent's tool-response mode so you don't have to.
38
+ > The AI SDK 6 explicitly invites SDK authors to ship their own `Agent` implementations: ["The `Agent` is now an interface instead of a class, which allows developers to implement their own agent abstractions for specific needs."](https://ai-sdk.dev/blog/ai-sdk-6) `ContentAgent` is that implementation for the Sanity Content Agent — it preconfigures `stopWhen`, `prepareStep`, and `toolChoice` for the `final_answer` reply pipeline so you don't have to.
39
39
 
40
- > **Migrating from `streamText({ model: provider.agent(threadId) })`?** Nothing breaks — the lower-level API is still supported and works exactly as before. But if you're using tool-response mode (`config.responseMode: 'tool'`) you'll want to switch to `ContentAgent` to pick up the safe defaults that prevent empty responses when the model burns its step budget on client tool calls. See [Tool-response mode](#tool-response-mode) below.
40
+ > **Migrating from `streamText({ model: provider.agent(threadId) })`?** Nothing breaks — the lower-level API is still supported and works exactly as before. Switch to `ContentAgent` to pick up the safe defaults that prevent empty responses when the model burns its step budget on client tool calls. See [How replies work](#how-replies-work) below.
41
41
 
42
42
  ## Streaming
43
43
 
@@ -87,28 +87,17 @@ const result = await agent.stream({
87
87
  });
88
88
  ```
89
89
 
90
- ## Tool-response mode
90
+ ## How replies work
91
91
 
92
- Set `config.responseMode: 'tool'` to deliver responses through a server-side `final_answer` tool. This is what the Linear, Slack, and MS Teams integrations use — the agent suppresses raw model text on the wire and emits all output via `final_answer.message`, with structured suggestions and a guaranteed delivery contract.
92
+ Every reply is delivered through a server-side `final_answer` tool call. The agent streams `final_answer.message` to the wire as `text-delta` chunks (parsed incrementally from the tool-call args) and includes structured `suggestions` for clients that render reply buttons. There's no separate "streaming mode" — every conversation, every client follows the same path.
93
93
 
94
- ```typescript
95
- const agent = new ContentAgent({
96
- provider,
97
- threadId: "my-thread",
98
- tools: { update_issue_status, get_settings },
99
- config: {
100
- responseMode: "tool",
101
- enableReplySuggestions: true,
102
- },
103
- });
104
- ```
105
-
106
- In this mode `ContentAgent` adjusts the [loop control](https://ai-sdk.dev/docs/agents/loop-control) primitives to make the agent safe by default:
94
+ `ContentAgent` adjusts the [loop control](https://ai-sdk.dev/docs/agents/loop-control) primitives to make this safe by default:
107
95
 
108
- 1. **[`stopWhen`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent#stopwhen)** defaults to "stop only when the last step's tool calls are exclusively `final_answer`," plus a [`stepCountIs(20)`](https://ai-sdk.dev/docs/reference/ai-sdk-core/step-count-is) safety cap. A `final_answer` accompanied by other tool calls in the same step is treated as a preamble (the model is signalling more work to do) and the loop continues; a `final_answer` alone ends it.
109
- 2. **[`prepareStep`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent#preparestep)** is composed with a safety net that, on the cap step, forces [`toolChoice`](https://ai-sdk.dev/docs/agents/building-agents#tool-choice) `{ type: 'tool', toolName: 'final_answer' }` if the model has been calling tools without ever converging on `final_answer`. This guarantees text on the wire even in pathological cases where the model would otherwise burn its step budget on client tool calls.
96
+ 1. **[`stopWhen`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent#stopwhen)** defaults to "stop on the first step that contains a `final_answer` tool call," plus a [`stepCountIs(20)`](https://ai-sdk.dev/docs/reference/ai-sdk-core/step-count-is) safety cap. A `final_answer` accompanied by other tool calls in the same step is the canonical act-and-answer pattern — the loop terminates without an extra round-trip.
97
+ 2. **[`prepareStep`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent#preparestep)** is composed with a safety net that, on the cap step, forces [`toolChoice`](https://ai-sdk.dev/docs/agents/building-agents#tool-choice) `{ type: 'tool', toolName: 'final_answer' }` if the model has been calling tools without ever converging on `final_answer`. This guarantees a reply on the wire even in pathological cases where the model would otherwise burn its step budget on client-tool round-trips.
98
+ 3. **Server-side recovery (SAGE-248)** — if the model fails to call `final_answer` naturally on any single turn, the agent runner retries that turn with forced `tool_choice`. Anthropic's API enforces forced tool choice strictly; the recovery is guaranteed to produce a `final_answer` chunk.
110
99
 
111
- If you pass your own `stopWhen` or `prepareStep`, they take precedence — `ContentAgent` doesn't reach into them. Outside tool-response mode the agent is a plain `ToolLoopAgent` with no overrides.
100
+ If you pass your own `stopWhen` or `prepareStep`, they take precedence — `ContentAgent` doesn't reach into them.
112
101
 
113
102
  ## Applications
114
103
 
@@ -165,15 +154,13 @@ const agent = new ContentAgent({
165
154
 
166
155
  ### Config fields
167
156
 
168
- | Field | Type | Description |
169
- | ------------------------ | ------------------------ | ----------------------------------------------------------------------- |
170
- | `instruction` | `string` | Custom instruction included in the system prompt |
171
- | `userMessageContext` | `Record<string, string>` | Key-value pairs appended to each user message as XML tags |
172
- | `capabilities` | `object` | Controls what operations the agent can perform (`read`, `write`) |
173
- | `filter` | `object` | GROQ boolean expressions for document access control |
174
- | `perspectives` | `object` | Perspective locking for read and write operations |
175
- | `responseMode` | `'stream' \| 'tool'` | Set to `'tool'` to deliver responses through the `final_answer` tool |
176
- | `enableReplySuggestions` | `boolean` | When `responseMode: 'tool'`, include reply-suggestion buttons on output |
157
+ | Field | Type | Description |
158
+ | -------------------- | ------------------------ | ---------------------------------------------------------------- |
159
+ | `instruction` | `string` | Custom instruction included in the system prompt |
160
+ | `userMessageContext` | `Record<string, string>` | Key-value pairs appended to each user message as XML tags |
161
+ | `capabilities` | `object` | Controls what operations the agent can perform (`read`, `write`) |
162
+ | `filter` | `object` | GROQ boolean expressions for document access control |
163
+ | `perspectives` | `object` | Perspective locking for read and write operations |
177
164
 
178
165
  ### Capabilities
179
166
 
@@ -490,10 +490,7 @@ type ApplicationResource = NonNullable<Application['resource']>;
490
490
  * }
491
491
  * ```
492
492
  */
493
- type Config = components['schemas']['Config'] & {
494
- /** Controls how the agent delivers responses. 'tool' enables the final_answer tool with structured message + suggestions. Omit or use 'stream' for normal streaming. */responseMode?: 'stream' | 'tool'; /** When true and responseMode is 'tool', the final_answer tool includes a suggestions field for follow-up reply buttons. */
495
- enableReplySuggestions?: boolean;
496
- };
493
+ type Config = components['schemas']['Config'];
497
494
  /**
498
495
  * Controls what operations the agent can perform.
499
496
  *
@@ -1 +1 @@
1
- {"version":3,"file":"sanity-agent-provider.d.ts","names":[],"sources":["../../src/generated/openapi.ts","../../src/types.ts","../../src/from-client.ts","../../src/sanity-agent-provider.ts"],"mappings":"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"}
1
+ {"version":3,"file":"sanity-agent-provider.d.ts","names":[],"sources":["../../src/generated/openapi.ts","../../src/types.ts","../../src/from-client.ts","../../src/sanity-agent-provider.ts"],"mappings":"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"}
@@ -19,10 +19,10 @@ type ContentAgentSettings<CALL_OPTIONS = never, TOOLS extends ToolSet = ToolSet,
19
19
  * abstraction. Construction is the only thing that differs: pass a `provider`
20
20
  * and `threadId` instead of a `model`.
21
21
  *
22
- * In tool-response mode (`config.responseMode: 'tool'`, used by integrations
23
- * like Linear, Slack, and MS Teams) the agent suppresses raw model text on
24
- * the wire and delivers all output through a server-side `final_answer`
25
- * tool. To make that mode safe by default, `ContentAgent`:
22
+ * Every conversation runs in the unified tool-response flow: the model's
23
+ * user-visible reply is delivered through the server-side `final_answer`
24
+ * tool, and the wire streams its `message` field as text-deltas. To make
25
+ * that flow safe by default, `ContentAgent`:
26
26
  *
27
27
  * 1. Defaults `stopWhen` to "stop on the first step that contains a
28
28
  * `final_answer` tool call, or after 20 steps." This lets the model land
@@ -34,8 +34,7 @@ type ContentAgentSettings<CALL_OPTIONS = never, TOOLS extends ToolSet = ToolSet,
34
34
  * has been calling tools but hasn't yet converged.
35
35
  *
36
36
  * Both defaults are skipped when the consumer passes their own `stopWhen` —
37
- * they own the loop semantics. Outside tool-response mode the agent is a
38
- * plain `ToolLoopAgent` with no overrides.
37
+ * they own the loop semantics in that case.
39
38
  *
40
39
  * @example
41
40
  * ```ts
@@ -48,7 +47,6 @@ type ContentAgentSettings<CALL_OPTIONS = never, TOOLS extends ToolSet = ToolSet,
48
47
  * provider,
49
48
  * threadId: 'my-thread',
50
49
  * tools: { update_issue_status, get_settings },
51
- * config: { responseMode: 'tool' },
52
50
  * })
53
51
  *
54
52
  * const result = await agent.stream({ messages })
@@ -1 +1 @@
1
- {"version":3,"file":"content-agent.d.ts","names":[],"sources":["../src/content-agent.ts"],"mappings":";;AAY4C;AAAA,KAMvC,yBAAA,GAA4B,IAAA,CAC/B,aAAA;;;;AASF;;KAAY,oBAAA,qCAEI,OAAA,GAAU,OAAA,iBACT,MAAA,CAAO,MAAA,YACpB,IAAA,CAAK,qBAAA,CAAsB,YAAA,EAAc,KAAA,EAAO,MAAA,cAClD,yBAAA;EAHc,wEAKZ,QAAA,EAAU,oBAAA,EAJG;EAMb,QAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;AAwHJ;;;;;;;;;;;;;;;;;;;;;cAAa,YAAA,qCAEG,OAAA,GAAU,OAAA,iBACT,MAAA,CAAO,MAAA,kBACd,aAAA,CAAc,YAAA,EAAc,KAAA,EAAO,MAAA;cAC/B,QAAA,EAAU,oBAAA,CAAqB,YAAA,EAAc,KAAA,EAAO,MAAA;AAAA"}
1
+ {"version":3,"file":"content-agent.d.ts","names":[],"sources":["../src/content-agent.ts"],"mappings":";;AAY4C;AAAA,KAMvC,yBAAA,GAA4B,IAAA,CAC/B,aAAA;;;;AASF;;KAAY,oBAAA,qCAEI,OAAA,GAAU,OAAA,iBACT,MAAA,CAAO,MAAA,YACpB,IAAA,CAAK,qBAAA,CAAsB,YAAA,EAAc,KAAA,EAAO,MAAA,cAClD,yBAAA;EAHc,wEAKZ,QAAA,EAAU,oBAAA,EAJG;EAMb,QAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;AAkHJ;;;;;;;;;;;;;;;;;;;cAAa,YAAA,qCAEG,OAAA,GAAU,OAAA,iBACT,MAAA,CAAO,MAAA,kBACd,aAAA,CAAc,YAAA,EAAc,KAAA,EAAO,MAAA;cAC/B,QAAA,EAAU,oBAAA,CAAqB,YAAA,EAAc,KAAA,EAAO,MAAA;AAAA"}
@@ -1,16 +1,13 @@
1
1
  import { ToolLoopAgent, stepCountIs } from "ai";
2
2
  import { pickBy, isNotNil } from "es-toolkit";
3
3
  const DEFAULT_MAX_STEPS = 20, FINAL_ANSWER_TOOL_NAME = "final_answer";
4
- function isToolResponseMode(settings) {
5
- return settings.config?.responseMode === "tool";
6
- }
7
4
  function hasFinalAnswer() {
8
5
  return ({ steps }) => {
9
6
  const lastStep = steps.at(-1);
10
7
  return lastStep?.toolCalls?.length ? lastStep.toolCalls.some((call) => call.toolName === FINAL_ANSWER_TOOL_NAME) : !1;
11
8
  };
12
9
  }
13
- function defaultToolResponseStopWhen() {
10
+ function defaultStopWhen() {
14
11
  return [hasFinalAnswer(), stepCountIs(DEFAULT_MAX_STEPS)];
15
12
  }
16
13
  const FORCE_FINAL_ANSWER = {
@@ -41,7 +38,7 @@ class ContentAgent extends ToolLoopAgent {
41
38
  } = settings, modelSettings = pickBy(
42
39
  { application, config, format, clientId, onStreamData },
43
40
  isNotNil
44
- ), model = provider.agent(threadId, modelSettings), useDefaults = stopWhen === void 0 && isToolResponseMode(modelSettings), resolvedStopWhen = useDefaults ? defaultToolResponseStopWhen() : stopWhen, resolvedPrepareStep = useDefaults ? buildSafetyNetPrepareStep(DEFAULT_MAX_STEPS, prepareStep) : prepareStep;
41
+ ), model = provider.agent(threadId, modelSettings), useDefaults = stopWhen === void 0, resolvedStopWhen = useDefaults ? defaultStopWhen() : stopWhen, resolvedPrepareStep = useDefaults ? buildSafetyNetPrepareStep(DEFAULT_MAX_STEPS, prepareStep) : prepareStep;
45
42
  super({
46
43
  ...toolLoopSettings,
47
44
  model,
@@ -1 +1 @@
1
- {"version":3,"file":"content-agent.js","sources":["../src/content-agent.ts"],"sourcesContent":["import {\n type Output,\n type PrepareStepFunction,\n type StopCondition,\n stepCountIs,\n type ToolChoice,\n ToolLoopAgent,\n type ToolLoopAgentSettings,\n type ToolSet,\n} from 'ai'\nimport { isNotNil, pickBy } from 'es-toolkit'\nimport type { ContentAgentProvider } from './sanity-agent-provider'\nimport type { AgentSettings } from './types'\n\nconst DEFAULT_MAX_STEPS = 20\nconst FINAL_ANSWER_TOOL_NAME = 'final_answer'\n\n/** Sanity-specific settings forwarded to `provider.agent(threadId, ...)`. */\ntype ContentAgentModelSettings = Pick<\n AgentSettings,\n 'application' | 'config' | 'format' | 'clientId' | 'onStreamData'\n>\n\n/**\n * Settings accepted by the {@link ContentAgent} constructor — everything\n * `ToolLoopAgent` accepts, minus `model` (resolved from the provider) and\n * plus the Sanity-specific routing fields.\n */\nexport type ContentAgentSettings<\n CALL_OPTIONS = never,\n TOOLS extends ToolSet = ToolSet,\n OUTPUT extends Output.Output = never,\n> = Omit<ToolLoopAgentSettings<CALL_OPTIONS, TOOLS, OUTPUT>, 'model'> &\n ContentAgentModelSettings & {\n /** Provider returned by `createContentAgent` or `createStudioAgent`. */\n provider: ContentAgentProvider\n /** Conversation thread ID. Reusing it continues an existing conversation. */\n threadId: string\n }\n\nfunction isToolResponseMode(settings: ContentAgentModelSettings): boolean {\n return settings.config?.responseMode === 'tool'\n}\n\n/**\n * Stop when the model's last step contains `final_answer`, regardless of\n * what other tool calls are alongside it.\n *\n * Before SAGE-182's Phase 1 (which removed the SSE `providerExecuted`\n * filter on the consumer side) the consumer's `step.toolCalls` never\n * actually contained `final_answer` — it was stripped at the wire — so\n * a stricter \"final_answer alone\" condition was the only thing that\n * could ever fire, and only after the model emitted a solo `final_answer`\n * step *following* the act-and-answer step. That cost an extra LLM\n * round-trip per turn and was the root of the SAGE-180 latency bug.\n *\n * With the filter gone, the canonical act-and-answer step\n * (`[update_issue_status, final_answer]`) is the natural terminator.\n * Stopping on any presence of `final_answer` lets the loop end on the\n * step the model actually finishes, with no wasted round-trip.\n */\nfunction hasFinalAnswer<TOOLS extends ToolSet>(): StopCondition<TOOLS> {\n return ({ steps }) => {\n const lastStep = steps.at(-1)\n if (!lastStep?.toolCalls?.length) return false\n return lastStep.toolCalls.some((call) => call.toolName === FINAL_ANSWER_TOOL_NAME)\n }\n}\n\nfunction defaultToolResponseStopWhen<TOOLS extends ToolSet>(): Array<StopCondition<TOOLS>> {\n return [hasFinalAnswer<TOOLS>(), stepCountIs(DEFAULT_MAX_STEPS)]\n}\n\n/**\n * `final_answer` is a server-side tool — not in the consumer's typed `tools` map.\n * Hoisted as a constant so the cast lives in one place.\n */\nconst FORCE_FINAL_ANSWER = {\n type: 'tool',\n toolName: FINAL_ANSWER_TOOL_NAME,\n} satisfies ToolChoice<ToolSet>\n\n/**\n * Forces `final_answer` on the cap step when the model has been calling tools\n * without ever converging on `final_answer`. Composes with a user-provided\n * `prepareStep` — theirs runs first, the override is overlaid.\n */\nfunction buildSafetyNetPrepareStep<TOOLS extends ToolSet>(\n maxSteps: number,\n userPrepareStep: PrepareStepFunction<TOOLS> | undefined,\n): PrepareStepFunction<TOOLS> {\n return async (options) => {\n const userResult = await userPrepareStep?.(options)\n if (options.stepNumber !== maxSteps - 1) return userResult\n\n const toolCalls = options.steps.flatMap((step) => step.toolCalls ?? [])\n if (toolCalls.length === 0) return userResult\n if (toolCalls.some((call) => call.toolName === FINAL_ANSWER_TOOL_NAME)) return userResult\n\n // Anthropic rejects `tool_choice` when the conversation's last message is\n // an assistant turn (the \"no assistant message prefill\" rule). That can\n // happen when a prior server-side tool call hasn't been paired with its\n // result in the consumer's view yet. In that case, skip the override —\n // the model gets its normal final step. Worst case we're back to the\n // pre-fix behaviour (possibly empty response), but no crash.\n if (options.messages.at(-1)?.role === 'assistant') return userResult\n\n return { ...userResult, toolChoice: FORCE_FINAL_ANSWER as ToolChoice<TOOLS> }\n }\n}\n\n/**\n * Sanity Content Agent wrapped as an `ai` SDK agent.\n *\n * Extends {@link ToolLoopAgent} so it inherits `.generate()`, `.stream()`,\n * telemetry, and every other capability of the AI SDK's first-class agent\n * abstraction. Construction is the only thing that differs: pass a `provider`\n * and `threadId` instead of a `model`.\n *\n * In tool-response mode (`config.responseMode: 'tool'`, used by integrations\n * like Linear, Slack, and MS Teams) the agent suppresses raw model text on\n * the wire and delivers all output through a server-side `final_answer`\n * tool. To make that mode safe by default, `ContentAgent`:\n *\n * 1. Defaults `stopWhen` to \"stop on the first step that contains a\n * `final_answer` tool call, or after 20 steps.\" This lets the model land\n * its terminal `final_answer` together with client tools in the same\n * step (the act-and-answer pattern) without burning an extra LLM\n * round-trip on a solo `final_answer` follow-up.\n * 2. Composes a `prepareStep` safety net that, on the cap step, forces\n * `toolChoice: { type: 'tool', toolName: 'final_answer' }` when the model\n * has been calling tools but hasn't yet converged.\n *\n * Both defaults are skipped when the consumer passes their own `stopWhen` —\n * they own the loop semantics. Outside tool-response mode the agent is a\n * plain `ToolLoopAgent` with no overrides.\n *\n * @example\n * ```ts\n * import { createContentAgent } from 'content-agent'\n * import { ContentAgent } from 'content-agent/agent'\n *\n * const provider = createContentAgent({ organizationId, token })\n *\n * const agent = new ContentAgent({\n * provider,\n * threadId: 'my-thread',\n * tools: { update_issue_status, get_settings },\n * config: { responseMode: 'tool' },\n * })\n *\n * const result = await agent.stream({ messages })\n * for await (const chunk of result.textStream) process.stdout.write(chunk)\n * ```\n *\n * @category Agent\n */\nexport class ContentAgent<\n CALL_OPTIONS = never,\n TOOLS extends ToolSet = ToolSet,\n OUTPUT extends Output.Output = never,\n> extends ToolLoopAgent<CALL_OPTIONS, TOOLS, OUTPUT> {\n constructor(settings: ContentAgentSettings<CALL_OPTIONS, TOOLS, OUTPUT>) {\n const {\n provider,\n threadId,\n application,\n config,\n format,\n clientId,\n onStreamData,\n stopWhen,\n prepareStep,\n ...toolLoopSettings\n } = settings\n\n const modelSettings = pickBy(\n { application, config, format, clientId, onStreamData },\n isNotNil,\n ) as ContentAgentModelSettings\n const model = provider.agent(threadId, modelSettings)\n\n // Defaults are coupled: if the consumer brought their own `stopWhen` they\n // own the loop, so the safety-net `prepareStep` (which assumes a known\n // step cap) doesn't apply either.\n const useDefaults = stopWhen === undefined && isToolResponseMode(modelSettings)\n const resolvedStopWhen = useDefaults ? defaultToolResponseStopWhen<TOOLS>() : stopWhen\n const resolvedPrepareStep = useDefaults\n ? buildSafetyNetPrepareStep<TOOLS>(DEFAULT_MAX_STEPS, prepareStep)\n : prepareStep\n\n super({\n ...toolLoopSettings,\n model,\n ...(resolvedStopWhen !== undefined && { stopWhen: resolvedStopWhen }),\n ...(resolvedPrepareStep !== undefined && { prepareStep: resolvedPrepareStep }),\n } as ToolLoopAgentSettings<CALL_OPTIONS, TOOLS, OUTPUT>)\n }\n}\n"],"names":[],"mappings":";;AAcA,MAAM,oBAAoB,IACpB,yBAAyB;AAyB/B,SAAS,mBAAmB,UAA8C;AACxE,SAAO,SAAS,QAAQ,iBAAiB;AAC3C;AAmBA,SAAS,iBAA8D;AACrE,SAAO,CAAC,EAAE,YAAY;AACpB,UAAM,WAAW,MAAM,GAAG,EAAE;AAC5B,WAAK,UAAU,WAAW,SACnB,SAAS,UAAU,KAAK,CAAC,SAAS,KAAK,aAAa,sBAAsB,IADxC;AAAA,EAE3C;AACF;AAEA,SAAS,8BAAkF;AACzF,SAAO,CAAC,eAAA,GAAyB,YAAY,iBAAiB,CAAC;AACjE;AAMA,MAAM,qBAAqB;AAAA,EACzB,MAAM;AAAA,EACN,UAAU;AACZ;AAOA,SAAS,0BACP,UACA,iBAC4B;AAC5B,SAAO,OAAO,YAAY;AACxB,UAAM,aAAa,MAAM,kBAAkB,OAAO;AAClD,QAAI,QAAQ,eAAe,WAAW,EAAG,QAAO;AAEhD,UAAM,YAAY,QAAQ,MAAM,QAAQ,CAAC,SAAS,KAAK,aAAa,EAAE;AAUtE,WATI,UAAU,WAAW,KACrB,UAAU,KAAK,CAAC,SAAS,KAAK,aAAa,sBAAsB,KAQjE,QAAQ,SAAS,GAAG,EAAE,GAAG,SAAS,cAAoB,aAEnD,EAAE,GAAG,YAAY,YAAY,mBAAA;AAAA,EACtC;AACF;AAgDO,MAAM,qBAIH,cAA2C;AAAA,EACnD,YAAY,UAA6D;AACvE,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,IAAA,IACD,UAEE,gBAAgB;AAAA,MACpB,EAAE,aAAa,QAAQ,QAAQ,UAAU,aAAA;AAAA,MACzC;AAAA,IAAA,GAEI,QAAQ,SAAS,MAAM,UAAU,aAAa,GAK9C,cAAc,aAAa,UAAa,mBAAmB,aAAa,GACxE,mBAAmB,cAAc,4BAAA,IAAuC,UACxE,sBAAsB,cACxB,0BAAiC,mBAAmB,WAAW,IAC/D;AAEJ,UAAM;AAAA,MACJ,GAAG;AAAA,MACH;AAAA,MACA,GAAI,qBAAqB,UAAa,EAAE,UAAU,iBAAA;AAAA,MAClD,GAAI,wBAAwB,UAAa,EAAE,aAAa,oBAAA;AAAA,IAAoB,CACvB;AAAA,EACzD;AACF;"}
1
+ {"version":3,"file":"content-agent.js","sources":["../src/content-agent.ts"],"sourcesContent":["import {\n type Output,\n type PrepareStepFunction,\n type StopCondition,\n stepCountIs,\n type ToolChoice,\n ToolLoopAgent,\n type ToolLoopAgentSettings,\n type ToolSet,\n} from 'ai'\nimport { isNotNil, pickBy } from 'es-toolkit'\nimport type { ContentAgentProvider } from './sanity-agent-provider'\nimport type { AgentSettings } from './types'\n\nconst DEFAULT_MAX_STEPS = 20\nconst FINAL_ANSWER_TOOL_NAME = 'final_answer'\n\n/** Sanity-specific settings forwarded to `provider.agent(threadId, ...)`. */\ntype ContentAgentModelSettings = Pick<\n AgentSettings,\n 'application' | 'config' | 'format' | 'clientId' | 'onStreamData'\n>\n\n/**\n * Settings accepted by the {@link ContentAgent} constructor — everything\n * `ToolLoopAgent` accepts, minus `model` (resolved from the provider) and\n * plus the Sanity-specific routing fields.\n */\nexport type ContentAgentSettings<\n CALL_OPTIONS = never,\n TOOLS extends ToolSet = ToolSet,\n OUTPUT extends Output.Output = never,\n> = Omit<ToolLoopAgentSettings<CALL_OPTIONS, TOOLS, OUTPUT>, 'model'> &\n ContentAgentModelSettings & {\n /** Provider returned by `createContentAgent` or `createStudioAgent`. */\n provider: ContentAgentProvider\n /** Conversation thread ID. Reusing it continues an existing conversation. */\n threadId: string\n }\n\n/**\n * Stop when the model's last step contains `final_answer`, regardless of\n * what other tool calls are alongside it.\n *\n * Before SAGE-182's Phase 1 (which removed the SSE `providerExecuted`\n * filter on the consumer side) the consumer's `step.toolCalls` never\n * actually contained `final_answer` — it was stripped at the wire — so\n * a stricter \"final_answer alone\" condition was the only thing that\n * could ever fire, and only after the model emitted a solo `final_answer`\n * step *following* the act-and-answer step. That cost an extra LLM\n * round-trip per turn and was the root of the SAGE-180 latency bug.\n *\n * With the filter gone, the canonical act-and-answer step\n * (`[update_issue_status, final_answer]`) is the natural terminator.\n * Stopping on any presence of `final_answer` lets the loop end on the\n * step the model actually finishes, with no wasted round-trip.\n */\nfunction hasFinalAnswer<TOOLS extends ToolSet>(): StopCondition<TOOLS> {\n return ({ steps }) => {\n const lastStep = steps.at(-1)\n if (!lastStep?.toolCalls?.length) return false\n return lastStep.toolCalls.some((call) => call.toolName === FINAL_ANSWER_TOOL_NAME)\n }\n}\n\nfunction defaultStopWhen<TOOLS extends ToolSet>(): Array<StopCondition<TOOLS>> {\n return [hasFinalAnswer<TOOLS>(), stepCountIs(DEFAULT_MAX_STEPS)]\n}\n\n/**\n * `final_answer` is a server-side tool — not in the consumer's typed `tools` map.\n * Hoisted as a constant so the cast lives in one place.\n */\nconst FORCE_FINAL_ANSWER = {\n type: 'tool',\n toolName: FINAL_ANSWER_TOOL_NAME,\n} satisfies ToolChoice<ToolSet>\n\n/**\n * Forces `final_answer` on the cap step when the model has been calling tools\n * without ever converging on `final_answer`. Composes with a user-provided\n * `prepareStep` — theirs runs first, the override is overlaid.\n */\nfunction buildSafetyNetPrepareStep<TOOLS extends ToolSet>(\n maxSteps: number,\n userPrepareStep: PrepareStepFunction<TOOLS> | undefined,\n): PrepareStepFunction<TOOLS> {\n return async (options) => {\n const userResult = await userPrepareStep?.(options)\n if (options.stepNumber !== maxSteps - 1) return userResult\n\n const toolCalls = options.steps.flatMap((step) => step.toolCalls ?? [])\n if (toolCalls.length === 0) return userResult\n if (toolCalls.some((call) => call.toolName === FINAL_ANSWER_TOOL_NAME)) return userResult\n\n // Anthropic rejects `tool_choice` when the conversation's last message is\n // an assistant turn (the \"no assistant message prefill\" rule). That can\n // happen when a prior server-side tool call hasn't been paired with its\n // result in the consumer's view yet. In that case, skip the override —\n // the model gets its normal final step. Worst case we're back to the\n // pre-fix behaviour (possibly empty response), but no crash.\n if (options.messages.at(-1)?.role === 'assistant') return userResult\n\n return { ...userResult, toolChoice: FORCE_FINAL_ANSWER as ToolChoice<TOOLS> }\n }\n}\n\n/**\n * Sanity Content Agent wrapped as an `ai` SDK agent.\n *\n * Extends {@link ToolLoopAgent} so it inherits `.generate()`, `.stream()`,\n * telemetry, and every other capability of the AI SDK's first-class agent\n * abstraction. Construction is the only thing that differs: pass a `provider`\n * and `threadId` instead of a `model`.\n *\n * Every conversation runs in the unified tool-response flow: the model's\n * user-visible reply is delivered through the server-side `final_answer`\n * tool, and the wire streams its `message` field as text-deltas. To make\n * that flow safe by default, `ContentAgent`:\n *\n * 1. Defaults `stopWhen` to \"stop on the first step that contains a\n * `final_answer` tool call, or after 20 steps.\" This lets the model land\n * its terminal `final_answer` together with client tools in the same\n * step (the act-and-answer pattern) without burning an extra LLM\n * round-trip on a solo `final_answer` follow-up.\n * 2. Composes a `prepareStep` safety net that, on the cap step, forces\n * `toolChoice: { type: 'tool', toolName: 'final_answer' }` when the model\n * has been calling tools but hasn't yet converged.\n *\n * Both defaults are skipped when the consumer passes their own `stopWhen` —\n * they own the loop semantics in that case.\n *\n * @example\n * ```ts\n * import { createContentAgent } from 'content-agent'\n * import { ContentAgent } from 'content-agent/agent'\n *\n * const provider = createContentAgent({ organizationId, token })\n *\n * const agent = new ContentAgent({\n * provider,\n * threadId: 'my-thread',\n * tools: { update_issue_status, get_settings },\n * })\n *\n * const result = await agent.stream({ messages })\n * for await (const chunk of result.textStream) process.stdout.write(chunk)\n * ```\n *\n * @category Agent\n */\nexport class ContentAgent<\n CALL_OPTIONS = never,\n TOOLS extends ToolSet = ToolSet,\n OUTPUT extends Output.Output = never,\n> extends ToolLoopAgent<CALL_OPTIONS, TOOLS, OUTPUT> {\n constructor(settings: ContentAgentSettings<CALL_OPTIONS, TOOLS, OUTPUT>) {\n const {\n provider,\n threadId,\n application,\n config,\n format,\n clientId,\n onStreamData,\n stopWhen,\n prepareStep,\n ...toolLoopSettings\n } = settings\n\n const modelSettings = pickBy(\n { application, config, format, clientId, onStreamData },\n isNotNil,\n ) as ContentAgentModelSettings\n const model = provider.agent(threadId, modelSettings)\n\n // Defaults are coupled: if the consumer brought their own `stopWhen` they\n // own the loop, so the safety-net `prepareStep` (which assumes a known\n // step cap) doesn't apply either.\n const useDefaults = stopWhen === undefined\n const resolvedStopWhen = useDefaults ? defaultStopWhen<TOOLS>() : stopWhen\n const resolvedPrepareStep = useDefaults\n ? buildSafetyNetPrepareStep<TOOLS>(DEFAULT_MAX_STEPS, prepareStep)\n : prepareStep\n\n super({\n ...toolLoopSettings,\n model,\n ...(resolvedStopWhen !== undefined && { stopWhen: resolvedStopWhen }),\n ...(resolvedPrepareStep !== undefined && { prepareStep: resolvedPrepareStep }),\n } as ToolLoopAgentSettings<CALL_OPTIONS, TOOLS, OUTPUT>)\n }\n}\n"],"names":[],"mappings":";;AAcA,MAAM,oBAAoB,IACpB,yBAAyB;AA0C/B,SAAS,iBAA8D;AACrE,SAAO,CAAC,EAAE,YAAY;AACpB,UAAM,WAAW,MAAM,GAAG,EAAE;AAC5B,WAAK,UAAU,WAAW,SACnB,SAAS,UAAU,KAAK,CAAC,SAAS,KAAK,aAAa,sBAAsB,IADxC;AAAA,EAE3C;AACF;AAEA,SAAS,kBAAsE;AAC7E,SAAO,CAAC,eAAA,GAAyB,YAAY,iBAAiB,CAAC;AACjE;AAMA,MAAM,qBAAqB;AAAA,EACzB,MAAM;AAAA,EACN,UAAU;AACZ;AAOA,SAAS,0BACP,UACA,iBAC4B;AAC5B,SAAO,OAAO,YAAY;AACxB,UAAM,aAAa,MAAM,kBAAkB,OAAO;AAClD,QAAI,QAAQ,eAAe,WAAW,EAAG,QAAO;AAEhD,UAAM,YAAY,QAAQ,MAAM,QAAQ,CAAC,SAAS,KAAK,aAAa,EAAE;AAUtE,WATI,UAAU,WAAW,KACrB,UAAU,KAAK,CAAC,SAAS,KAAK,aAAa,sBAAsB,KAQjE,QAAQ,SAAS,GAAG,EAAE,GAAG,SAAS,cAAoB,aAEnD,EAAE,GAAG,YAAY,YAAY,mBAAA;AAAA,EACtC;AACF;AA8CO,MAAM,qBAIH,cAA2C;AAAA,EACnD,YAAY,UAA6D;AACvE,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,IAAA,IACD,UAEE,gBAAgB;AAAA,MACpB,EAAE,aAAa,QAAQ,QAAQ,UAAU,aAAA;AAAA,MACzC;AAAA,IAAA,GAEI,QAAQ,SAAS,MAAM,UAAU,aAAa,GAK9C,cAAc,aAAa,QAC3B,mBAAmB,cAAc,gBAAA,IAA2B,UAC5D,sBAAsB,cACxB,0BAAiC,mBAAmB,WAAW,IAC/D;AAEJ,UAAM;AAAA,MACJ,GAAG;AAAA,MACH;AAAA,MACA,GAAI,qBAAqB,UAAa,EAAE,UAAU,iBAAA;AAAA,MAClD,GAAI,wBAAwB,UAAa,EAAE,aAAa,oBAAA;AAAA,IAAoB,CACvB;AAAA,EACzD;AACF;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-agent",
3
- "version": "0.8.2",
3
+ "version": "1.0.0",
4
4
  "description": "Vercel AI SDK provider for Sanity Content Agent API",
5
5
  "keywords": [
6
6
  "sanity",
@@ -34,7 +34,7 @@
34
34
  "@ai-sdk/provider-utils": "^2.2.8",
35
35
  "es-toolkit": "^1.44.0",
36
36
  "zod": "^3.25.76",
37
- "@sanity/agent-types": "0.0.102"
37
+ "@sanity/agent-types": "0.1.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@sanity/pkg-utils": "^10.4.1",