experimental-a2 0.17.0 → 0.18.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/CHANGELOG.md +21 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-Bjce9_5U.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-Bjce9_5U.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-UrWfKeuv.js → ai-BpOGm-zB.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-BpOGm-zB.js.map} +1 -1
- package/dist/{ai-vpGtcaCB.d.ts → ai-DrUaMyRt.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-DrUaMyRt.d.ts.map} +1 -1
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +634 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
- package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +904 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +47 -25
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +106 -37
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-COaiaDub.js → client-BReKqwZC.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BReKqwZC.js.map} +1 -1
- package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
- package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
- package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
- package/dist/{internal-CbVvEwad.js → internal-B2T3U4Rq.js} +8 -2
- package/dist/internal-B2T3U4Rq.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
- package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/reducer-BAXc8JyF.js.map +1 -1
- package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
- package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-Rj64fNvX.js → server-Dw_k_7kO.js} +93 -60
- package/dist/server-Dw_k_7kO.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-En7cbi3v.d.ts} +36 -7
- package/dist/server-En7cbi3v.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{session-clear-Dshpuhff.d.ts → session-clear-Bo-XTFEy.d.ts} +2 -2
- package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-Djdv0bnZ.d.ts} +31 -2
- package/dist/store-Djdv0bnZ.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +32 -6
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-B5FcwJfo.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-B5FcwJfo.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +54 -3
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-BAwS8RWn.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-BRITti_o.js} +187 -68
- package/dist/store-redis-notify-BRITti_o.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +32 -2
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-DjfHOvdW.js → wire-rSUglYo8.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-rSUglYo8.js.map} +1 -1
- package/docs/01-quickstart.mdx +49 -83
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -86
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -150
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +108 -144
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +855 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +722 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +141 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +322 -0
- package/docs/server/06-guarantees.mdx +123 -0
- package/examples/playground/DEPLOY.md +1 -1
- package/examples/playground/app/a0/[appId]/page.tsx +11 -6
- package/examples/playground/app/a0/app-list.tsx +9 -2
- package/examples/playground/app/a0/app-view.tsx +9 -1
- package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
- package/examples/playground/app/a0/apps.test.ts +12 -0
- package/examples/playground/app/a0/apps.ts +22 -7
- package/examples/playground/app/a0/chat.tsx +47 -1
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/model.test.ts +18 -1
- package/examples/playground/app/a0/model.ts +47 -10
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/sandbox-log.ts +10 -4
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/scheduler/route.ts +2 -2
- package/examples/playground/app/a0/server.ts +100 -45
- package/examples/playground/app/a0/session.ts +17 -5
- package/examples/playground/app/a0/shell.tsx +11 -3
- package/examples/playground/app/agent/ingress.test.ts +6 -3
- package/examples/playground/app/agent/server.ts +9 -6
- package/examples/playground/app/agent/telemetry.test.ts +71 -50
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +30 -26
- package/examples/playground/app/chat/server.ts +46 -46
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +4 -1
- package/examples/playground/vercel.json +18 -0
- package/package.json +27 -3
- package/src/ai-agent.ts +261 -87
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +833 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1263 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/internal.ts +7 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/server.ts +177 -19
- package/src/store-memory.ts +42 -7
- package/src/store-postgres.ts +80 -0
- package/src/store-redis-core.ts +210 -79
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +60 -0
- package/src/store.ts +33 -0
- package/src/telemetry.ts +1 -0
- package/dist/internal-CbVvEwad.js.map +0 -1
- package/dist/server-CGM8WzRG.d.ts.map +0 -1
- package/dist/server-Rj64fNvX.js.map +0 -1
- package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
- package/dist/store-Gpkq7WBo.d.ts.map +0 -1
- package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
- package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -237
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -193
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -324
- package/docs/guides/04-local-first.mdx +0 -47
- package/docs/guides/05-production.mdx +0 -534
- package/docs/guides/06-ai-agents.mdx +0 -1408
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -262
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -2358
- package/docs/reference/02-errors.mdx +0 -150
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Introduction
|
|
3
|
+
description: Build an AI SDK agent whose messages, streaming, tools, and recovery live in an A2 event log.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add experimental-a2 ai zod
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Define the agent
|
|
13
|
+
|
|
14
|
+
```ts server/assistant.ts
|
|
15
|
+
import { agent } from 'experimental-a2/ai/server'
|
|
16
|
+
|
|
17
|
+
export const assistant = agent({
|
|
18
|
+
name: 'support-agent',
|
|
19
|
+
model: 'openai/gpt-5.6-terra',
|
|
20
|
+
instructions: 'Help the customer. Be concise.',
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`agent()` is the whole server: the AI protocol, the agent's loop, and A2's
|
|
25
|
+
store, scheduler, and handlers. `name` is its identity. The loop is the
|
|
26
|
+
agent's `harness`. By default it's `{ type: 'ai-sdk' }`: each response runs as
|
|
27
|
+
durable one-step `streamText()` generations, tuned with `harness.generation`
|
|
28
|
+
and `maxSteps`, listed under [`agent()`](/reference/ai#agentoptions). The same
|
|
29
|
+
agent can run [FX's or Pi's loop](/agents/harnesses) instead.
|
|
30
|
+
|
|
31
|
+
## Expose the route
|
|
32
|
+
|
|
33
|
+
```ts app/api/agent/route.ts
|
|
34
|
+
import { assistant } from '@/server/assistant'
|
|
35
|
+
|
|
36
|
+
export const GET = assistant.fetch
|
|
37
|
+
export const POST = assistant.fetch
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`GET` streams events and `POST` accepts the browser's pushes. The route never
|
|
41
|
+
calls the model: `fetch` accepts user messages and controls, and rejects every
|
|
42
|
+
server-authored event with `400`. The route is where you'd do auth, or any
|
|
43
|
+
other checks, before `assistant.fetch(request, { authorize })` applies
|
|
44
|
+
per-operation policy.
|
|
45
|
+
|
|
46
|
+
## Render the conversation
|
|
47
|
+
|
|
48
|
+
Fold the initial state on the server and hand it to a client component:
|
|
49
|
+
|
|
50
|
+
```tsx app/agent/[sessionId]/page.tsx
|
|
51
|
+
import { assistant } from '@/server/assistant'
|
|
52
|
+
import { Chat } from './chat'
|
|
53
|
+
|
|
54
|
+
export default async function AgentPage({
|
|
55
|
+
params,
|
|
56
|
+
}: {
|
|
57
|
+
params: Promise<{ sessionId: string }>
|
|
58
|
+
}) {
|
|
59
|
+
const { sessionId } = await params
|
|
60
|
+
const initial = await assistant.session(sessionId).state()
|
|
61
|
+
return <Chat sessionId={sessionId} initial={initial} />
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
An empty session folds to the initial `AIState`; its log is created when the
|
|
66
|
+
first message is pushed. `useAgent()` keeps that state live and returns the
|
|
67
|
+
actions a chat needs:
|
|
68
|
+
|
|
69
|
+
```tsx app/agent/[sessionId]/chat.tsx
|
|
70
|
+
'use client'
|
|
71
|
+
import type { FormEvent } from 'react'
|
|
72
|
+
import { useState } from 'react'
|
|
73
|
+
import type { StateSnapshot } from 'experimental-a2'
|
|
74
|
+
import type { AIState } from 'experimental-a2/ai'
|
|
75
|
+
import { useAgent } from 'experimental-a2/ai/react'
|
|
76
|
+
import type { assistant } from '@/server/assistant'
|
|
77
|
+
|
|
78
|
+
const indicatorLabel = {
|
|
79
|
+
starting: 'Starting…',
|
|
80
|
+
thinking: 'Thinking…',
|
|
81
|
+
compacting: 'Compacting…',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function Chat({
|
|
85
|
+
sessionId,
|
|
86
|
+
initial,
|
|
87
|
+
}: {
|
|
88
|
+
sessionId: string
|
|
89
|
+
initial: StateSnapshot<AIState>
|
|
90
|
+
}) {
|
|
91
|
+
const { messages, status, indicator, sendMessage } =
|
|
92
|
+
useAgent<typeof assistant>({ api: '/api/agent', id: sessionId, initial })
|
|
93
|
+
const [draft, setDraft] = useState('')
|
|
94
|
+
const [error, setError] = useState<string | null>(null)
|
|
95
|
+
|
|
96
|
+
async function send(event: FormEvent<HTMLFormElement>): Promise<void> {
|
|
97
|
+
event.preventDefault()
|
|
98
|
+
const text = draft.trim()
|
|
99
|
+
if (!text) return
|
|
100
|
+
setDraft('')
|
|
101
|
+
setError(null)
|
|
102
|
+
try {
|
|
103
|
+
await sendMessage(text)
|
|
104
|
+
} catch (cause) {
|
|
105
|
+
setDraft(text)
|
|
106
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<main>
|
|
112
|
+
<section aria-label="Conversation">
|
|
113
|
+
{messages.map((message) => (
|
|
114
|
+
<article key={message.id}>
|
|
115
|
+
<strong>{message.role === 'user' ? 'You' : 'Agent'}</strong>
|
|
116
|
+
{message.parts.map((part, index) =>
|
|
117
|
+
part.type === 'text' ? <p key={index}>{part.text}</p> : null,
|
|
118
|
+
)}
|
|
119
|
+
</article>
|
|
120
|
+
))}
|
|
121
|
+
{indicator ? (
|
|
122
|
+
<p role="status">{indicatorLabel[indicator]}</p>
|
|
123
|
+
) : null}
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
<form onSubmit={send}>
|
|
127
|
+
<input
|
|
128
|
+
aria-label="Message"
|
|
129
|
+
value={draft}
|
|
130
|
+
onChange={(event) => setDraft(event.currentTarget.value)}
|
|
131
|
+
/>
|
|
132
|
+
<button disabled={!draft.trim() || status === 'closed'}>Send</button>
|
|
133
|
+
</form>
|
|
134
|
+
|
|
135
|
+
{error ? <p role="alert">{error}</p> : null}
|
|
136
|
+
</main>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The `import type` keeps server code out of the bundle. The hook returns the
|
|
142
|
+
session's state plus `messages`, `status`, `indicator`, and actions; see
|
|
143
|
+
[`useAgent()`](/reference/ai#useagentoptions).
|
|
144
|
+
|
|
145
|
+
`sendMessage()` takes a string, sent as one text part with a fresh message ID,
|
|
146
|
+
or a complete `UIMessage`. Every action returns the same result as `push()`: it
|
|
147
|
+
resolves once the command events are stored, and `.confirmed` resolves when the
|
|
148
|
+
stream delivers them back.
|
|
149
|
+
|
|
150
|
+
Pass `{ generate: false }` to add a user message without starting a turn, as in
|
|
151
|
+
`sendMessage('Order #4411 shipped this morning.', { generate: false })`. The
|
|
152
|
+
next generating user message includes earlier passive messages in its model
|
|
153
|
+
context; passive messages sent after it wait for the next one.
|
|
154
|
+
|
|
155
|
+
## Start a session from another page
|
|
156
|
+
|
|
157
|
+
Wait for the durable append before navigating, so the destination's server
|
|
158
|
+
render sees the message. `createAgentClient()` is the same client outside React:
|
|
159
|
+
|
|
160
|
+
```ts app/agent/client.ts
|
|
161
|
+
'use client'
|
|
162
|
+
import { createAgentClient } from 'experimental-a2/ai/client'
|
|
163
|
+
import type { assistant } from '@/server/assistant'
|
|
164
|
+
|
|
165
|
+
export const assistantClient = createAgentClient<typeof assistant>({
|
|
166
|
+
api: '/api/agent',
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```ts app/agent/open-agent.ts
|
|
171
|
+
'use client'
|
|
172
|
+
import { inputs } from 'experimental-a2/ai'
|
|
173
|
+
import { assistantClient } from './client'
|
|
174
|
+
|
|
175
|
+
export async function openAgent({
|
|
176
|
+
sessionId,
|
|
177
|
+
text,
|
|
178
|
+
router,
|
|
179
|
+
}: {
|
|
180
|
+
sessionId: string
|
|
181
|
+
text: string
|
|
182
|
+
router: { push(href: string): void }
|
|
183
|
+
}): Promise<void> {
|
|
184
|
+
await assistantClient.session(sessionId).push(
|
|
185
|
+
...inputs.message({
|
|
186
|
+
id: crypto.randomUUID(),
|
|
187
|
+
role: 'user',
|
|
188
|
+
parts: [{ type: 'text', text }],
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
router.push(`/agent/${sessionId}`)
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Pass the same client to the destination's hook,
|
|
196
|
+
`useAgent<typeof assistant>({ client: assistantClient, id: sessionId, initial })`,
|
|
197
|
+
so it picks up the live session the push created and reconciles the server
|
|
198
|
+
render without replacing it. Idle client session objects expire after five
|
|
199
|
+
minutes by default; set `gcTime` on `createAgentClient()` to change that.
|
|
200
|
+
|
|
201
|
+
## Choose the model
|
|
202
|
+
|
|
203
|
+
`model` is an AI Gateway model ID, under every harness, or `ModelSettings` to
|
|
204
|
+
choose its reasoning level and speed with it:
|
|
205
|
+
|
|
206
|
+
```ts server/fast-assistant.ts
|
|
207
|
+
import { agent } from 'experimental-a2/ai/server'
|
|
208
|
+
|
|
209
|
+
export const fastAssistant = agent({
|
|
210
|
+
name: 'fast-support',
|
|
211
|
+
model: { model: 'openai/gpt-5.6-terra', reasoning: 'low', speed: 'fast' },
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`speed: 'fast'` prefers AI Gateway's faster tier when the model offers one.
|
|
216
|
+
`reasoningLevels` from `experimental-a2/ai` lists the levels for a picker. See
|
|
217
|
+
[`ModelSettings`](/reference/ai#modelsettings). A resolver can also choose the
|
|
218
|
+
model for each generation from your app's state, such as a plan; see
|
|
219
|
+
[Add your own events and state](/agents/conversations#add-your-own-events-and-state).
|
|
220
|
+
|
|
221
|
+
## What happens after a send
|
|
222
|
+
|
|
223
|
+
One generating user message becomes a durable sequence:
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
browser ai.control.requested durable input command
|
|
227
|
+
server ai.message.created input admitted
|
|
228
|
+
server ai.control.committed accepted state changes
|
|
229
|
+
server ai.control.decided command receipt
|
|
230
|
+
server ai.generation.requested scheduled from durable facts
|
|
231
|
+
server ai.generation.started
|
|
232
|
+
server ai.generation.progress batched UIMessageChunk[]
|
|
233
|
+
server ai.tool.called when present
|
|
234
|
+
server ai.approval.requested when present
|
|
235
|
+
server ai.tool.result after concurrent execution or denial
|
|
236
|
+
server ai.generation.completed closes this model step
|
|
237
|
+
server ai.generation.requested after all sibling tools are terminal
|
|
238
|
+
... another one-step generation
|
|
239
|
+
server ai.message.completed
|
|
240
|
+
server ai.generation.requested next queued user turn, when present
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Tool results and generation completion can arrive in either order; the next
|
|
244
|
+
step waits for both. The SSE stream delivers these events to the same reducer,
|
|
245
|
+
which applies the streamed chunks to the current `UIMessage`; the exported
|
|
246
|
+
`deriveUIMessages()` does the same over a raw history.
|
|
247
|
+
|
|
248
|
+
Model calls for a session never overlap. A message sent while a response is
|
|
249
|
+
active enters the durable log immediately, but its turn waits until that
|
|
250
|
+
response, including every tool step and approval, finishes. Inbox order is
|
|
251
|
+
arrival order and can be edited; see
|
|
252
|
+
[Inbox and turn controls](/agents/conversations#inbox-and-turn-controls).
|
|
253
|
+
|
|
254
|
+
The hook's actions read [`AIState`](/reference/ai#aistate)'s ownership
|
|
255
|
+
bookkeeping, such as `activeGeneration` and `activeRequestId`, for you; the
|
|
256
|
+
lower-level `inputs` builders need it explicitly. A2-generated IDs use the
|
|
257
|
+
reserved `a2.ai:` namespace; treat them as opaque.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tools and approvals
|
|
3
|
+
description: "Give an agent tools, ask for approval before they run, and interrupt or retry a turn."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Add tools and approval
|
|
7
|
+
|
|
8
|
+
```ts server/with-tools.ts
|
|
9
|
+
import { tool } from 'ai'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
import { agent } from 'experimental-a2/ai/server'
|
|
12
|
+
|
|
13
|
+
const tools = {
|
|
14
|
+
closeTicket: tool({
|
|
15
|
+
description: 'Close a resolved support ticket',
|
|
16
|
+
inputSchema: z.object({ ticketId: z.string() }),
|
|
17
|
+
execute: async ({ ticketId }, { toolCallId }) => {
|
|
18
|
+
// your side effect; toolCallId makes a stable idempotency key:
|
|
19
|
+
// await closeTicket(ticketId, { idempotencyKey: toolCallId })
|
|
20
|
+
return { ticketId, closed: true }
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const toolsAssistant = agent({
|
|
26
|
+
name: 'support-tools',
|
|
27
|
+
model: 'openai/gpt-5.6-terra',
|
|
28
|
+
tools,
|
|
29
|
+
harness: {
|
|
30
|
+
type: 'ai-sdk',
|
|
31
|
+
generation: {
|
|
32
|
+
toolApproval: {
|
|
33
|
+
closeTicket: 'user-approval',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Tools are ordinary AI SDK `tool()` definitions, passed through untouched.
|
|
41
|
+
Approval policy lives in `harness.generation.toolApproval`, beside the other
|
|
42
|
+
per-step settings. Later examples that import `assistant` use the agent from
|
|
43
|
+
[Agents](/agents/introduction#define-the-agent).
|
|
44
|
+
|
|
45
|
+
### Read the handler context
|
|
46
|
+
|
|
47
|
+
```ts server/set-reminder.ts
|
|
48
|
+
import { tool } from 'ai'
|
|
49
|
+
import { handlerContext, inputs } from 'experimental-a2/ai'
|
|
50
|
+
import { z } from 'zod'
|
|
51
|
+
|
|
52
|
+
export const setReminder = tool({
|
|
53
|
+
description: 'Set a reminder for yourself',
|
|
54
|
+
inputSchema: z.object({
|
|
55
|
+
delaySeconds: z.number().int().min(1).max(6 * 24 * 60 * 60),
|
|
56
|
+
reminder: z.string(),
|
|
57
|
+
}),
|
|
58
|
+
execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
|
|
59
|
+
const ctx = handlerContext()
|
|
60
|
+
await ctx.session.schedule(
|
|
61
|
+
`reminder:${toolCallId}`,
|
|
62
|
+
{ delay: `${delaySeconds}s` },
|
|
63
|
+
...inputs.message({
|
|
64
|
+
id: `reminder:${ctx.event.id}`,
|
|
65
|
+
role: 'user',
|
|
66
|
+
parts: [{ type: 'text', text: `Reminder: ${reminder}` }],
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return { scheduled: true, delaySeconds }
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`handlerContext()` returns the running tool's durable handler context: `event`,
|
|
76
|
+
`attempt`, `session`, and `signal`. It works anywhere below `execute`, including
|
|
77
|
+
awaited helpers, and throws outside a tool execution. Pass a shared
|
|
78
|
+
`agentContract()` to type application events; A2 checks it against the
|
|
79
|
+
executing agent. See [`handlerContext()`](/reference/ai#handlercontextcontract)
|
|
80
|
+
for the event payload.
|
|
81
|
+
|
|
82
|
+
This tool schedules a reminder as a new user message. `ctx.session` belongs to
|
|
83
|
+
the executing agent, so `schedule()` uses that agent's scheduler: add
|
|
84
|
+
`setReminder` to its `tools` and configure a [scheduler](/server/production).
|
|
85
|
+
Naming the timer after `toolCallId` and the message after the triggering
|
|
86
|
+
event's ID makes a retry converge on the same timer and message, even if a
|
|
87
|
+
provider reuses tool-call IDs. The six-day maximum is Vercel Queues'
|
|
88
|
+
[delay limit](/server/timers#provider-limits); adjust it for your scheduler. A
|
|
89
|
+
transient or ambiguous scheduler failure retries the handler with the same
|
|
90
|
+
identity and target time; a deterministic rejection or a missing scheduler is
|
|
91
|
+
an ordinary tool error.
|
|
92
|
+
|
|
93
|
+
To test a context-reading tool, call it inside `runWithHandlerContext(ctx, fn)`
|
|
94
|
+
from `experimental-a2/ai/server`; within `fn`, `handlerContext()` returns `ctx`.
|
|
95
|
+
|
|
96
|
+
### Answer approvals
|
|
97
|
+
|
|
98
|
+
```tsx app/tools/[sessionId]/approval-controls.tsx
|
|
99
|
+
'use client'
|
|
100
|
+
import { useAgent } from 'experimental-a2/ai/react'
|
|
101
|
+
import type { toolsAssistant } from '@/server/with-tools'
|
|
102
|
+
|
|
103
|
+
export function ApprovalControls({ sessionId }: { sessionId: string }) {
|
|
104
|
+
const { state, approve, deny } = useAgent<typeof toolsAssistant>({
|
|
105
|
+
api: '/api/tools',
|
|
106
|
+
id: sessionId,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return state.pendingApprovals.map((approval) => (
|
|
110
|
+
<div key={approval.approvalId}>
|
|
111
|
+
<span>Allow tool call {approval.toolCallId}?</span>
|
|
112
|
+
<button onClick={() => void approve(approval)}>Allow</button>
|
|
113
|
+
<button
|
|
114
|
+
onClick={() => void deny(approval, { reason: 'Denied by the user' })}
|
|
115
|
+
>
|
|
116
|
+
Deny
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
))
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Instead of running a tool that needs approval, A2 records the AI SDK's approval
|
|
124
|
+
request as `ai.approval.requested`, and the reducer adds it to
|
|
125
|
+
`state.pendingApprovals`. `approve()` and `deny()` copy
|
|
126
|
+
the request, message, generation, and approval IDs from it, so a delayed click
|
|
127
|
+
cannot authorize a later model step. The decision updates the approval part in
|
|
128
|
+
the message, and a built-in handler then runs the approved tool or records the
|
|
129
|
+
denied result.
|
|
130
|
+
|
|
131
|
+
A policy can also be a function that returns `'user-approval'` or
|
|
132
|
+
`'approved'` from the validated input. It runs during the model step and again
|
|
133
|
+
on recovery for the same call, so keep it deterministic over the input; an
|
|
134
|
+
impure predicate can fork one call's flow.
|
|
135
|
+
|
|
136
|
+
`state.pendingInputs` works the same way for application-defined input: answer
|
|
137
|
+
one with `respond(input, value)`. The value stays in raw history and
|
|
138
|
+
`state.pendingInputs`; to make it model context, resolve `instructions`
|
|
139
|
+
dynamically or replace `generate`.
|
|
140
|
+
|
|
141
|
+
### How tools run
|
|
142
|
+
|
|
143
|
+
Validation errors, execution errors, denials, and preliminary and final outputs
|
|
144
|
+
all become durable `ai.tool.result` events; a preliminary output keeps the tool
|
|
145
|
+
`running` until the final one. Dynamic-tool, provider-executed, provider
|
|
146
|
+
metadata, and tool metadata fields carry through to the message. Independent
|
|
147
|
+
tools run concurrently as soon as their inputs and authorization are ready,
|
|
148
|
+
with no fixed limit. Set `toolLane: 'tools'` when local tools must run one at a
|
|
149
|
+
time, for example because they share a sandbox. The next model step starts
|
|
150
|
+
once the current one closes and every sibling tool has a success, error, or
|
|
151
|
+
denied result.
|
|
152
|
+
|
|
153
|
+
An approved provider-executed call can satisfy that join through its response. A
|
|
154
|
+
provider tool with deferred-result support waits for its provider result: your
|
|
155
|
+
authenticated callback submits it from trusted server code with
|
|
156
|
+
`inputs.toolResult(result)`, and the controller checks it against the admitted
|
|
157
|
+
call. Browser pushes reject it.
|
|
158
|
+
|
|
159
|
+
## Interrupt and retry
|
|
160
|
+
|
|
161
|
+
```tsx app/agent/[sessionId]/turn-controls.tsx
|
|
162
|
+
'use client'
|
|
163
|
+
import { useAgent } from 'experimental-a2/ai/react'
|
|
164
|
+
import type { assistant } from '@/server/assistant'
|
|
165
|
+
|
|
166
|
+
export function TurnControls({ sessionId }: { sessionId: string }) {
|
|
167
|
+
const { state, status, stop, retry } = useAgent<typeof assistant>({
|
|
168
|
+
api: '/api/agent',
|
|
169
|
+
id: sessionId,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (status === 'failed')
|
|
173
|
+
return <button onClick={() => void retry()}>Retry</button>
|
|
174
|
+
return state.active ? (
|
|
175
|
+
<button onClick={() => void stop()}>Stop</button>
|
|
176
|
+
) : null
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`stop()` sends the log frontier the rendered state showed, not whatever arrived
|
|
181
|
+
before the click was handled. The client previews Stop at once (a rejection
|
|
182
|
+
restores the turn), and the response rewinds to that frontier, keeping the
|
|
183
|
+
partial text the user saw. Tools completed by then stay completed, and accepted
|
|
184
|
+
tools still awaiting completion become `output-error`. Tool calls whose
|
|
185
|
+
arguments were still streaming are drafts: they leave the conversation and later
|
|
186
|
+
prompts, while their progress stays in the raw log. Later events cannot
|
|
187
|
+
reactivate a stopped response.
|
|
188
|
+
|
|
189
|
+
Once Stop is persisted, A2 aborts the turn's running model and tool work.
|
|
190
|
+
Cancellation is cooperative: tools must honor their abort signal. A Stop for an
|
|
191
|
+
older turn cannot cancel its successor.
|
|
192
|
+
|
|
193
|
+
`retry()` reruns the failed model call with a fresh `retryId` for each click:
|
|
194
|
+
resending one retry is safe, and a later retry stays distinct. Earlier
|
|
195
|
+
completed steps, their tool results, and the committed compaction summary
|
|
196
|
+
remain; the failed call's uncommitted step leaves the prompt and response.
|
|
197
|
+
Later user messages stay queued until the failed response is retried or
|
|
198
|
+
interrupted. Conversation forks and edit-and-resend of admitted messages are
|
|
199
|
+
yours to build.
|
|
200
|
+
|
|
201
|
+
The lower-level `inputs.interrupt()` takes exactly one owner, so a delayed
|
|
202
|
+
interruption cannot stop later work. Before the generation starts, pass
|
|
203
|
+
`activeRequestId` as `requestId` and `activeResponseMessageId` as `messageId`.
|
|
204
|
+
Once `activeGeneration` exists, pass its `generationId` and `responseMessageId`
|
|
205
|
+
instead. After the generation completes, copy the owner from a pending approval,
|
|
206
|
+
pending input, or running tool while the response waits.
|
|
207
|
+
|
|
208
|
+
## Append from the server
|
|
209
|
+
|
|
210
|
+
Webhooks, jobs, and other backends start turns with the same inputs and
|
|
211
|
+
`append()`:
|
|
212
|
+
|
|
213
|
+
```ts server/append-message.ts
|
|
214
|
+
import { inputs } from 'experimental-a2/ai'
|
|
215
|
+
import { assistant } from './assistant'
|
|
216
|
+
|
|
217
|
+
export async function appendServerMessage(sessionId: string, text: string) {
|
|
218
|
+
await assistant.session(sessionId).append(
|
|
219
|
+
...inputs.message({
|
|
220
|
+
id: crypto.randomUUID(),
|
|
221
|
+
role: 'user',
|
|
222
|
+
parts: [{ type: 'text', text }],
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
A connected browser still receives the events over SSE, without an optimistic
|
|
229
|
+
overlay for an interaction that began elsewhere.
|
|
230
|
+
|
|
231
|
+
## The built-in inputs
|
|
232
|
+
|
|
233
|
+
The hook's actions build their commands with `inputs`, the same pure,
|
|
234
|
+
isomorphic builders you pass to raw `push()` and server `append()`:
|
|
235
|
+
`message()` and the passive `seed()` for messages; `approval()`,
|
|
236
|
+
`input()`, and `requestInput()` for pending requests; `retry()`, `stop()`,
|
|
237
|
+
`steer()`, `interrupt()`, `pause()`, and `resume()` for turns; `queue.edit()`,
|
|
238
|
+
`queue.move()`, `queue.remove()`, and `queue.sendNow()` for the inbox; and
|
|
239
|
+
`toolResult()` for deferred provider results. See
|
|
240
|
+
[`inputs`](/reference/ai#inputs) for each signature, ID rules, and payloads.
|
|
241
|
+
|
|
242
|
+
Browser pushes accept user messages, approval and input responses,
|
|
243
|
+
interruptions, and retries. `inputs.requestInput()`, `inputs.toolResult()`, and
|
|
244
|
+
non-user `inputs.seed()` messages are for trusted server appends. `inputs` has
|
|
245
|
+
no session lifecycle methods: the log begins with the first append, and apps
|
|
246
|
+
that use `ai.session.created` or `ai.session.closed` append them from trusted
|
|
247
|
+
server code.
|