content-agent 0.7.5 → 0.8.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 +124 -66
- package/dist/_chunks-dts/sanity-agent-provider.d.ts +980 -0
- package/dist/_chunks-dts/sanity-agent-provider.d.ts.map +1 -0
- package/dist/content-agent.d.ts +64 -0
- package/dist/content-agent.d.ts.map +1 -0
- package/dist/content-agent.js +56 -0
- package/dist/content-agent.js.map +1 -0
- package/dist/index.d.ts +2 -978
- package/dist/index.d.ts.map +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -10,81 +10,59 @@ npm install content-agent ai
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
`ContentAgent` extends the AI SDK's [`ToolLoopAgent`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent) — it inherits `.generate()`, `.stream()`, telemetry, and every other capability of the AI SDK's first-class agent abstraction. Construction is the only thing that differs: pass a `provider` and `threadId` instead of a `model`, and `ContentAgent` resolves the model for you.
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
16
|
import { createContentAgent } from "content-agent";
|
|
17
|
-
import {
|
|
17
|
+
import { ContentAgent } from "content-agent/agent";
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const provider = createContentAgent({
|
|
20
20
|
organizationId: "your-org-id",
|
|
21
21
|
token: "your-sanity-token",
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const agent = new ContentAgent({
|
|
25
|
+
provider,
|
|
26
|
+
threadId: "my-thread",
|
|
27
|
+
});
|
|
25
28
|
|
|
26
|
-
const { text } = await
|
|
27
|
-
model,
|
|
29
|
+
const { text } = await agent.generate({
|
|
28
30
|
prompt: "What blog posts do I have?",
|
|
29
31
|
});
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
`provider` is reusable — create it once per organization and instantiate as many `ContentAgent`s as you need.
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
> **Why a class instead of `streamText({ model, ... })`?**
|
|
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.
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
41
|
+
|
|
42
|
+
## Streaming
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
```typescript
|
|
45
|
+
const result = await agent.stream({
|
|
41
46
|
prompt: "Summarize my latest content",
|
|
42
47
|
});
|
|
43
48
|
|
|
44
|
-
for await (const text of textStream) {
|
|
49
|
+
for await (const text of result.textStream) {
|
|
45
50
|
process.stdout.write(text);
|
|
46
51
|
}
|
|
47
52
|
```
|
|
48
53
|
|
|
49
|
-
## Applications
|
|
50
|
-
|
|
51
|
-
Each application key uniquely identifies a deployed Sanity Studio workspace. Since multiple studios can share the same project ID and dataset, the application key disambiguates which one to target.
|
|
52
|
-
|
|
53
|
-
List available applications for your organization, then target one by its key:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
const apps = await contentAgent.applications();
|
|
57
|
-
const app = apps.find((app) => app.title === "My Studio");
|
|
58
|
-
|
|
59
|
-
const model = contentAgent.agent("my-thread", {
|
|
60
|
-
application: { key: app.key },
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
You can also set a default application on the provider so every `.agent()` and `.prompt()` call inherits it:
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
const contentAgent = createContentAgent({
|
|
68
|
-
organizationId: "your-org-id",
|
|
69
|
-
token: "your-token",
|
|
70
|
-
application: resolvedApp,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// No need to pass application per-call
|
|
74
|
-
const model = contentAgent.agent("my-thread");
|
|
75
|
-
```
|
|
76
|
-
|
|
77
54
|
## Tools
|
|
78
55
|
|
|
79
|
-
Pass AI SDK tools to
|
|
56
|
+
Pass AI SDK tools to the constructor — schemas are forwarded to the agent, and execution happens locally on the client:
|
|
80
57
|
|
|
81
58
|
```typescript
|
|
82
|
-
import {
|
|
59
|
+
import { ContentAgent } from "content-agent/agent";
|
|
60
|
+
import { tool } from "ai";
|
|
83
61
|
import { z } from "zod";
|
|
84
62
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
63
|
+
const agent = new ContentAgent({
|
|
64
|
+
provider,
|
|
65
|
+
threadId: "my-thread",
|
|
88
66
|
tools: {
|
|
89
67
|
submit_for_review: tool({
|
|
90
68
|
description: "Submit a document for editorial review.",
|
|
@@ -103,14 +81,70 @@ const result = streamText({
|
|
|
103
81
|
}),
|
|
104
82
|
},
|
|
105
83
|
});
|
|
84
|
+
|
|
85
|
+
const result = await agent.stream({
|
|
86
|
+
prompt: "Find all draft blog posts and submit them for review.",
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Tool-response mode
|
|
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.
|
|
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:
|
|
107
|
+
|
|
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.
|
|
110
|
+
|
|
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.
|
|
112
|
+
|
|
113
|
+
## Applications
|
|
114
|
+
|
|
115
|
+
Each application key uniquely identifies a deployed Sanity Studio workspace. Since multiple studios can share the same project ID and dataset, the application key disambiguates which one to target.
|
|
116
|
+
|
|
117
|
+
List available applications for your organization, then target one by its key:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const apps = await provider.applications();
|
|
121
|
+
const app = apps.find((app) => app.title === "My Studio");
|
|
122
|
+
|
|
123
|
+
const agent = new ContentAgent({
|
|
124
|
+
provider,
|
|
125
|
+
threadId: "my-thread",
|
|
126
|
+
application: { key: app.key },
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
You can also set a default application on the provider so every agent and `.prompt()` call inherits it:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const provider = createContentAgent({
|
|
134
|
+
organizationId: "your-org-id",
|
|
135
|
+
token: "your-token",
|
|
136
|
+
application: resolvedApp,
|
|
137
|
+
});
|
|
106
138
|
```
|
|
107
139
|
|
|
108
140
|
## Configuration
|
|
109
141
|
|
|
110
|
-
Pass a `config` object to
|
|
142
|
+
Pass a `config` object to control agent behavior:
|
|
111
143
|
|
|
112
144
|
```typescript
|
|
113
|
-
const
|
|
145
|
+
const agent = new ContentAgent({
|
|
146
|
+
provider,
|
|
147
|
+
threadId: "my-thread",
|
|
114
148
|
config: {
|
|
115
149
|
instruction: "You are a helpful content assistant for our marketing team.",
|
|
116
150
|
userMessageContext: {
|
|
@@ -131,13 +165,15 @@ const model = contentAgent.agent("my-thread", {
|
|
|
131
165
|
|
|
132
166
|
### Config fields
|
|
133
167
|
|
|
134
|
-
| Field
|
|
135
|
-
|
|
|
136
|
-
| `instruction`
|
|
137
|
-
| `userMessageContext`
|
|
138
|
-
| `capabilities`
|
|
139
|
-
| `filter`
|
|
140
|
-
| `perspectives`
|
|
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 |
|
|
141
177
|
|
|
142
178
|
### Capabilities
|
|
143
179
|
|
|
@@ -151,6 +187,21 @@ config: { capabilities: { read: true, write: false } }
|
|
|
151
187
|
config: { capabilities: { read: true, write: { preset: "minimal" } } }
|
|
152
188
|
```
|
|
153
189
|
|
|
190
|
+
## One-shot prompts
|
|
191
|
+
|
|
192
|
+
For stateless single-turn requests, use the provider's `.prompt()` method directly — no thread, no agent class:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const { text } = await provider.prompt(
|
|
196
|
+
{
|
|
197
|
+
application: { key: "projectId.datasetName" },
|
|
198
|
+
config: { capabilities: { read: true, write: false } },
|
|
199
|
+
instructions: "Be concise",
|
|
200
|
+
},
|
|
201
|
+
{ message: "List my 5 most recent posts" },
|
|
202
|
+
);
|
|
203
|
+
```
|
|
204
|
+
|
|
154
205
|
## Studio usage
|
|
155
206
|
|
|
156
207
|
`createStudioAgent` makes the provider work inside Sanity Studio with no token or manual configuration. It derives the API host, organization ID, and application from the Studio context:
|
|
@@ -158,35 +209,42 @@ config: { capabilities: { read: true, write: { preset: "minimal" } } }
|
|
|
158
209
|
```typescript
|
|
159
210
|
import { useMemo } from "react";
|
|
160
211
|
import { createStudioAgent } from "content-agent";
|
|
212
|
+
import { ContentAgent } from "content-agent/agent";
|
|
161
213
|
import { useClient, useWorkspace } from "sanity";
|
|
162
214
|
|
|
163
215
|
function MyComponent() {
|
|
164
216
|
const client = useClient({ apiVersion: "2024-01-01" });
|
|
165
217
|
const { name: workspace } = useWorkspace();
|
|
166
218
|
|
|
167
|
-
const
|
|
219
|
+
const provider = useMemo(
|
|
168
220
|
() => createStudioAgent(client, workspace),
|
|
169
221
|
[client, workspace],
|
|
170
222
|
);
|
|
171
223
|
|
|
172
|
-
|
|
224
|
+
const agent = useMemo(
|
|
225
|
+
() => new ContentAgent({ provider, threadId: "my-thread" }),
|
|
226
|
+
[provider],
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Use agent.stream() / agent.generate() with useChat, etc.
|
|
173
230
|
}
|
|
174
231
|
```
|
|
175
232
|
|
|
176
|
-
The factory call is synchronous — async setup (org ID fetch, application resolution) runs lazily on the first API call and is cached at the module level. Wrap in `useMemo` so the provider
|
|
233
|
+
The factory call is synchronous — async setup (org ID fetch, application resolution) runs lazily on the first API call and is cached at the module level. Wrap in `useMemo` so the provider and agent keep stable identities across renders.
|
|
177
234
|
|
|
178
235
|
Since there is no token in a Studio context, `credentials: 'include'` is set on all fetch calls automatically so the browser sends session cookies. The base URL is derived from the client's project-scoped `getUrl` method.
|
|
179
236
|
|
|
180
237
|
### Using with `useChat`
|
|
181
238
|
|
|
182
|
-
For a streaming chat UI, pair with `DirectChatTransport` from the AI SDK:
|
|
239
|
+
For a streaming chat UI, pair with [`DirectChatTransport`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/direct-chat-transport) and [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) from the AI SDK:
|
|
183
240
|
|
|
184
241
|
```tsx
|
|
185
242
|
import { useMemo, useState } from "react";
|
|
186
243
|
import { useClient, useWorkspace } from "sanity";
|
|
187
244
|
import { useChat } from "@ai-sdk/react";
|
|
188
|
-
import { DirectChatTransport
|
|
245
|
+
import { DirectChatTransport } from "ai";
|
|
189
246
|
import { createStudioAgent } from "content-agent";
|
|
247
|
+
import { ContentAgent } from "content-agent/agent";
|
|
190
248
|
|
|
191
249
|
function ChatTool() {
|
|
192
250
|
const client = useClient({ apiVersion: "2024-01-01" });
|
|
@@ -194,10 +252,9 @@ function ChatTool() {
|
|
|
194
252
|
const [threadId] = useState(() => `thread-${crypto.randomUUID()}`);
|
|
195
253
|
|
|
196
254
|
const transport = useMemo(() => {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
});
|
|
255
|
+
const provider = createStudioAgent(client, workspace);
|
|
256
|
+
const agent = new ContentAgent({ provider, threadId });
|
|
257
|
+
return new DirectChatTransport({ agent });
|
|
201
258
|
}, [client, workspace, threadId]);
|
|
202
259
|
|
|
203
260
|
const { messages, sendMessage, status } = useChat({ transport });
|
|
@@ -208,10 +265,11 @@ function ChatTool() {
|
|
|
208
265
|
|
|
209
266
|
### Lower-level helpers
|
|
210
267
|
|
|
211
|
-
If you
|
|
268
|
+
If you need more control than `ContentAgent` provides, the underlying primitives are still exported:
|
|
212
269
|
|
|
270
|
+
- `provider.agent(threadId, settings)` — returns a raw `LanguageModelV3` you can pass to [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text)/[`generateText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text)/[`ToolLoopAgent`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent) directly
|
|
213
271
|
- `fromClient(client, workspace)` — derives `baseURL` and `application` from a Sanity client
|
|
214
|
-
- `
|
|
272
|
+
- `provider.resolveApplication(client, workspace)` — fetches applications and matches by resource ID + workspace name
|
|
215
273
|
|
|
216
274
|
## License
|
|
217
275
|
|