experimental-a2 0.18.0 → 0.18.1
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 +11 -0
- package/dist/{actor-Bjce9_5U.d.ts → actor-C7b-1arn.d.ts} +3 -3
- package/dist/{actor-Bjce9_5U.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-DrUaMyRt.d.ts → ai-BMLlI2Ff.d.ts} +2 -2
- package/dist/{ai-DrUaMyRt.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
- package/dist/{ai-BpOGm-zB.js → ai-D6mGlO8j.js} +2 -2
- package/dist/{ai-BpOGm-zB.js.map → ai-D6mGlO8j.js.map} +1 -1
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +92 -34
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-DvsPeJeB.d.ts → ai-harness-B2suP2Qh.d.ts} +2 -2
- package/dist/{ai-harness-DvsPeJeB.d.ts.map → ai-harness-B2suP2Qh.d.ts.map} +1 -1
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.d.ts.map +1 -1
- package/dist/ai-pi.js +59 -13
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +10 -6
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/{client-BReKqwZC.js → client-BhHLoNKF.js} +3 -3
- package/dist/{client-BReKqwZC.js.map → client-BhHLoNKF.js.map} +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{internal-B2T3U4Rq.js → internal-CscSY7K_.js} +4 -2
- package/dist/{internal-B2T3U4Rq.js.map → internal-CscSY7K_.js.map} +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +9 -10
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Dw_k_7kO.js → server-D-Y4pqwu.js} +37 -11
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-En7cbi3v.d.ts → server-DYAATFuT.d.ts} +11 -5
- package/dist/server-DYAATFuT.d.ts.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-Bo-XTFEy.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Djdv0bnZ.d.ts → store-CyLUy86v.d.ts} +23 -4
- package/dist/store-CyLUy86v.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 +25 -6
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-B5FcwJfo.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-B5FcwJfo.js.map → store-polling-7WJV0YWP.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +55 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-BAwS8RWn.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-BAwS8RWn.js.map → store-presence-polling-B1ixeqIS.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-BRITti_o.js → store-redis-notify-CM0AfLY0.js} +88 -37
- package/dist/store-redis-notify-CM0AfLY0.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 +35 -20
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-rSUglYo8.js → wire-BKxH1vbP.js} +2 -2
- package/dist/{wire-rSUglYo8.js.map → wire-BKxH1vbP.js.map} +1 -1
- package/docs/agents/03-conversations.mdx +2 -2
- package/docs/index.mdx +3 -3
- package/docs/reference/02-server.mdx +12 -7
- package/docs/reference/04-ai.mdx +16 -7
- package/docs/reference/05-adapters.mdx +4 -4
- package/docs/server/01-handlers.mdx +1 -1
- package/docs/server/03-cancellation.mdx +3 -2
- package/docs/server/05-production.mdx +2 -1
- package/docs/server/06-guarantees.mdx +20 -11
- package/examples/playground/DEPLOY.md +2 -4
- package/examples/playground/app/a0/[appId]/page.tsx +2 -8
- package/examples/playground/app/a0/devtools.tsx +2 -8
- package/examples/playground/app/a0/server.ts +8 -19
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/ai-agent.ts +10 -3
- package/src/ai-fx.ts +126 -47
- package/src/ai-pi.ts +74 -15
- package/src/ai-tool-input.ts +45 -0
- package/src/internal.ts +6 -0
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +14 -15
- package/src/server.ts +58 -12
- package/src/store-memory.ts +50 -9
- package/src/store-postgres.ts +79 -28
- package/src/store-redis-core.ts +112 -31
- package/src/store-sqlite.ts +61 -28
- package/src/store.ts +22 -3
- package/dist/server-Dw_k_7kO.js.map +0 -1
- package/dist/server-En7cbi3v.d.ts.map +0 -1
- package/dist/session-clear-Bo-XTFEy.d.ts.map +0 -1
- package/dist/store-Djdv0bnZ.d.ts.map +0 -1
- package/dist/store-redis-notify-BRITti_o.js.map +0 -1
package/docs/reference/04-ai.mdx
CHANGED
|
@@ -577,7 +577,10 @@ fx(): Harness<'fx'>
|
|
|
577
577
|
The [FX](https://fx.sh) harness for `agent({ harness })`, running FX's kernel
|
|
578
578
|
from `libfx` (an optional peer dependency) in the server process. FX adds no
|
|
579
579
|
system prompt and brings no tools: `instructions` is the whole system prompt
|
|
580
|
-
and the agent's `tools` are all FX can call.
|
|
580
|
+
and the agent's `tools` are all FX can call. A call's input goes through its
|
|
581
|
+
tool's schema, as under `{ type: 'ai-sdk' }`: `execute` receives the schema's
|
|
582
|
+
output, defaults included, and input the schema rejects goes back to the model
|
|
583
|
+
as an error, without running the tool. `reasoning` becomes FX's effort and
|
|
581
584
|
`speed: 'fast'` its fast tier. FX compacts its own context; a compaction
|
|
582
585
|
appears in the transcript as a `data-a2-compaction` part with its summary.
|
|
583
586
|
|
|
@@ -607,12 +610,18 @@ agent session from `@earendil-works/pi-coding-agent` (with
|
|
|
607
610
|
`@earendil-works/pi-ai`, both optional peer dependencies). Pi brings its system
|
|
608
611
|
prompt and its `read`, `write`, `edit`, and `bash` tools, which run in the
|
|
609
612
|
session's sandbox. `instructions` is added to Pi's prompt. The agent's `tools`
|
|
610
|
-
are added to Pi's; a tool with one of their names replaces Pi's.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
613
|
+
are added to Pi's; a tool with one of their names replaces Pi's. Every call
|
|
614
|
+
first passes Pi's own checks: Pi coerces its arguments to the tool's JSON Schema
|
|
615
|
+
(the string `"5"` becomes `5` for a number) and rejects what that schema
|
|
616
|
+
forbids, such as a property it does not list. A call to one of the agent's tools
|
|
617
|
+
then goes through the tool's schema, as under `{ type: 'ai-sdk' }`: `execute`
|
|
618
|
+
receives the schema's output, defaults included. Input either check rejects goes
|
|
619
|
+
back to the model as an error, without running the tool. `reasoning` becomes
|
|
620
|
+
Pi's thinking level, and `model` is one of the Gateway models Pi's catalog
|
|
621
|
+
lists. Pi works in the sandbox's working directory; its `bash` runs with the
|
|
622
|
+
sandbox's environment, never the server's. Pi compacts its own context; a
|
|
623
|
+
compaction appears in the transcript as a `data-a2-compaction` part with its
|
|
624
|
+
summary.
|
|
616
625
|
|
|
617
626
|
- `sandbox` (`HarnessSandbox`, given a `HarnessSandboxContext`) resolves the
|
|
618
627
|
session's AI SDK sandbox session (`Experimental_SandboxSession` from `ai`).
|
|
@@ -104,8 +104,8 @@ mid-span.
|
|
|
104
104
|
| `a2.ai.ack.report_id` | `a2.ai.ack` | start | work report event id, matching the control span's id |
|
|
105
105
|
|
|
106
106
|
- Drain outcomes: `settled` means nothing actionable is left; `busy` means live
|
|
107
|
-
per-event claims remain; `stalled` means a
|
|
108
|
-
retry.
|
|
107
|
+
per-event claims or a failed event's retry hold remain; `stalled` means a
|
|
108
|
+
caught failure in this drain needs a later retry.
|
|
109
109
|
- A failing handler marks its `a2.event` span with the exception and error
|
|
110
110
|
status. Alert on `a2.event.outcome = dead_lettered`. A dead-lettered event
|
|
111
111
|
blocks only later events in its lane, until `session.redrive()` resolves it;
|
|
@@ -294,8 +294,8 @@ export const POST = scheduler.handler(ordersServer, billingServer)
|
|
|
294
294
|
signatures return 401.
|
|
295
295
|
- A signed malformed envelope, or a declared or streamed body over
|
|
296
296
|
`maxBodyBytes`, returns 489 with `Upstash-NonRetryable-Error: true`.
|
|
297
|
-
- Returns 200 when settled; when busy, after a durable successor
|
|
298
|
-
exists
|
|
297
|
+
- Returns 200 when settled; when busy or stalled, after a durable successor
|
|
298
|
+
watchdog exists, so each caught failure gets a fresh message and a fresh
|
|
299
299
|
per-message retry budget. Append tasks return 200 after their fixed-id append
|
|
300
300
|
succeeds and any recovery watchdog for pending handler work is durable; clear
|
|
301
301
|
tasks after their conditional clear and its deletion finish.
|
|
@@ -192,7 +192,7 @@ records completion runs the handler again.
|
|
|
192
192
|
## When handlers throw
|
|
193
193
|
|
|
194
194
|
Throwing means "retry me." A2 records the failure and retries with backoff.
|
|
195
|
-
The failed event blocks only its lane. After ten
|
|
195
|
+
The failed event blocks only its lane. After ten failed attempts it
|
|
196
196
|
dead-letters and keeps blocking its lane until
|
|
197
197
|
[you retry or skip it](/server/production#3-when-an-event-dead-letters).
|
|
198
198
|
|
|
@@ -70,8 +70,9 @@ what stops it racing its successor outside A2.
|
|
|
70
70
|
Pass `ctx.signal` to your I/O and let the abort propagate. That handles every
|
|
71
71
|
case:
|
|
72
72
|
|
|
73
|
-
- **A propagated lease abort** surrenders the attempt: no completion,
|
|
74
|
-
|
|
73
|
+
- **A propagated lease abort** surrenders the attempt: no completion, and
|
|
74
|
+
normal recovery. Like a crash, it costs the event one attempt of its
|
|
75
|
+
failure budget.
|
|
75
76
|
- **A user cancellation** (`abortOn`) is the one to catch and return normally
|
|
76
77
|
from. The stop is the outcome, so the completion counts.
|
|
77
78
|
- **Return normally after a lease abort** only when the work is done. A lapsed
|
|
@@ -241,7 +241,8 @@ import { ordersServer } from '@/server'
|
|
|
241
241
|
const order = ordersServer.session(orderId)
|
|
242
242
|
const [dead] = await order.deadLetters()
|
|
243
243
|
if (dead) {
|
|
244
|
-
// dead.lastError says why
|
|
244
|
+
// dead.lastError says why (null if every attempt timed out or crashed);
|
|
245
|
+
// deploy the handler fix first, then:
|
|
245
246
|
await order.redrive(dead.index, { action: 'retry' })
|
|
246
247
|
}
|
|
247
248
|
```
|
|
@@ -22,8 +22,9 @@ and the log decides what remains. Reads never dispatch handlers.
|
|
|
22
22
|
Each claim expires. If its holder dies, the claim lapses and the next wakeup
|
|
23
23
|
claims the event again with a larger `ctx.attempt`. The retry gets the same
|
|
24
24
|
event id, index, type, and payload. A kill after the claim can use up an
|
|
25
|
-
attempt before your code runs,
|
|
26
|
-
older attempt can never
|
|
25
|
+
attempt before your code runs, and that attempt counts toward the event's
|
|
26
|
+
[failure budget](#when-a-handler-keeps-failing). An older attempt can never
|
|
27
|
+
overwrite a newer one's completion or failure.
|
|
27
28
|
|
|
28
29
|
:::note[Recovery is not an event]
|
|
29
30
|
A2 does not append `recovered` or `continued`. Recovery retries the same event.
|
|
@@ -86,20 +87,28 @@ keeps its original cause.
|
|
|
86
87
|
## A chain that crosses the function timeout
|
|
87
88
|
|
|
88
89
|
A function killed mid-handler restarts that handler from its entry point in a
|
|
89
|
-
fresh invocation: same event, new attempt
|
|
90
|
-
processed
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
fresh invocation: same event, new attempt. Events already processed stay
|
|
91
|
+
processed, so a chain of handlers can outlast one invocation, but each handler
|
|
92
|
+
must fit in a fresh one: an attempt that runs out of time is a failed attempt,
|
|
93
|
+
and a handler that never fits dead-letters after ten. Split longer work into
|
|
94
|
+
smaller events or increase the function duration. On Vercel, A2 caps the claim window
|
|
93
95
|
at the function deadline when the platform exposes it, so a retry starts
|
|
94
|
-
promptly after a timeout.
|
|
96
|
+
promptly after a timeout. Once the deadline passes, A2 claims nothing more in
|
|
97
|
+
that invocation, so a timeout costs the event one attempt.
|
|
95
98
|
|
|
96
99
|
## When a handler keeps failing
|
|
97
100
|
|
|
98
101
|
A caught failure counts against that event, releases its claim, and leaves
|
|
99
|
-
other eligible events running. A2
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
other eligible events running. A2 holds it back for a delay that starts at
|
|
103
|
+
one second and doubles with each failed attempt up to 30 seconds, then retries
|
|
104
|
+
it on the session's next wake. With a scheduler, that's a few seconds at first,
|
|
105
|
+
then the delay itself. An attempt that ends before it finishes, because its
|
|
106
|
+
process timed out, crashed, or lost its claim, counts too, once the next
|
|
107
|
+
attempt takes its claim over. Ten failed attempts dead-letter the event: later
|
|
108
|
+
events in its lane stay blocked, while unlaned events and other lanes continue.
|
|
109
|
+
A crash costs every event it interrupts one attempt, so a session whose work
|
|
110
|
+
keeps outrunning its function dead-letters: bound its fan-out, or put work that
|
|
111
|
+
waits on the same resource in one lane. The event waits until you
|
|
103
112
|
[retry or skip it](/server/production#3-when-an-event-dead-letters) with
|
|
104
113
|
`session.redrive()`.
|
|
105
114
|
|
|
@@ -118,10 +118,8 @@ cookie.
|
|
|
118
118
|
|
|
119
119
|
A0's dev tools have a Crash button, which ends the function instance running
|
|
120
120
|
an app's agent, mid-turn, so the scheduler resumes the turn in a fresh one.
|
|
121
|
-
Crashing ends everything else that instance is serving,
|
|
122
|
-
|
|
123
|
-
allow it anywhere else. Under `next dev` that takes the dev server down with
|
|
124
|
-
it, so it's for trying recovery, not for everyday local runs.
|
|
121
|
+
Crashing ends everything else that instance is serving, and under `next dev`
|
|
122
|
+
it takes the dev server down with it.
|
|
125
123
|
|
|
126
124
|
## 6. WebSockets for the canvas
|
|
127
125
|
|
|
@@ -4,7 +4,7 @@ import { AppView } from '../app-view'
|
|
|
4
4
|
import { isAppId } from '../apps'
|
|
5
5
|
import { DevTools } from '../devtools'
|
|
6
6
|
import { harnessFrom, logPage } from '../model'
|
|
7
|
-
import { agentServers
|
|
7
|
+
import { agentServers } from '../server'
|
|
8
8
|
import { SessionProvider } from '../session'
|
|
9
9
|
|
|
10
10
|
/** An app, from its current state; the provider follows its log from there. */
|
|
@@ -40,13 +40,7 @@ export default async function A0App({
|
|
|
40
40
|
<AppView
|
|
41
41
|
appId={appId}
|
|
42
42
|
harness={harness}
|
|
43
|
-
devtools={
|
|
44
|
-
<DevTools
|
|
45
|
-
appId={appId}
|
|
46
|
-
deployed={!!process.env.VERCEL}
|
|
47
|
-
crashesAllowed={crashesAllowed}
|
|
48
|
-
/>
|
|
49
|
-
}
|
|
43
|
+
devtools={<DevTools appId={appId} deployed={!!process.env.VERCEL} />}
|
|
50
44
|
/>
|
|
51
45
|
</SessionProvider>
|
|
52
46
|
)
|
|
@@ -20,12 +20,10 @@ const gap = (ms: number): string =>
|
|
|
20
20
|
export function DevTools({
|
|
21
21
|
appId,
|
|
22
22
|
deployed,
|
|
23
|
-
crashesAllowed,
|
|
24
23
|
}: {
|
|
25
24
|
appId: string
|
|
26
25
|
/** Function limits and crashes only mean something on Vercel. */
|
|
27
26
|
deployed: boolean
|
|
28
|
-
crashesAllowed: boolean
|
|
29
27
|
}): ReactNode {
|
|
30
28
|
const { state, events, index, history, hydrated, loadHistory } = useSession()
|
|
31
29
|
const short = useSearchParams().get('limit') === '5s'
|
|
@@ -83,12 +81,8 @@ export function DevTools({
|
|
|
83
81
|
<button
|
|
84
82
|
type="button"
|
|
85
83
|
className="a0-ghost a0-devtools-crash"
|
|
86
|
-
disabled={
|
|
87
|
-
title=
|
|
88
|
-
crashesAllowed
|
|
89
|
-
? 'Ends the process running the agent, right now'
|
|
90
|
-
: 'Crashes need a preview deployment, or A0_CRASHES=on'
|
|
91
|
-
}
|
|
84
|
+
disabled={crashes.state}
|
|
85
|
+
title="Ends the process running the agent, right now"
|
|
92
86
|
onClick={crash}
|
|
93
87
|
>
|
|
94
88
|
<Zap size={13} aria-hidden />
|
|
@@ -106,16 +106,14 @@ function chosenModel({
|
|
|
106
106
|
}) {
|
|
107
107
|
// Every step starts here, in the process that runs it, which is what
|
|
108
108
|
// the dev tools' crash has to end.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
}
|
|
109
|
+
const appId = event.sessionId
|
|
110
|
+
waitUntil(
|
|
111
|
+
crashOnRequest({
|
|
112
|
+
appId,
|
|
113
|
+
app: agentServers[harness].session(appId),
|
|
114
|
+
log: crashServer.session(crashLogId(appId)),
|
|
115
|
+
}).catch(() => {}),
|
|
116
|
+
)
|
|
119
117
|
const { model, reasoning, fast } = state.modelChoice
|
|
120
118
|
return { model, reasoning, speed: fast ? 'fast' : 'standard' } as const
|
|
121
119
|
}
|
|
@@ -193,20 +191,11 @@ export const crashServer = createServer({
|
|
|
193
191
|
contract: crashLog,
|
|
194
192
|
})
|
|
195
193
|
|
|
196
|
-
/**
|
|
197
|
-
* A crash ends the function instance, and anything else it's serving, so
|
|
198
|
-
* only previews allow it, behind Deployment Protection, unless A0_CRASHES
|
|
199
|
-
* is on. Under next dev it would take the dev server down.
|
|
200
|
-
*/
|
|
201
|
-
export const crashesAllowed =
|
|
202
|
-
process.env.VERCEL_ENV === 'preview' || process.env.A0_CRASHES === 'on'
|
|
203
|
-
|
|
204
194
|
/** The browser can only ask for a crash; the process it ends records it. */
|
|
205
195
|
export function crashEvents(request: Request): Promise<Response> {
|
|
206
196
|
return crashServer.fetch(request, {
|
|
207
197
|
authorize: (operation) =>
|
|
208
198
|
isCrashLogId(operation.sessionId) &&
|
|
209
|
-
(crashesAllowed || operation.type !== 'push') &&
|
|
210
199
|
(operation.type !== 'push' ||
|
|
211
200
|
operation.events.every((event) => event.type === 'crash.requested')),
|
|
212
201
|
})
|
package/package.json
CHANGED
package/src/ai-agent.ts
CHANGED
|
@@ -680,7 +680,15 @@ async function* consumeGeneration(options: {
|
|
|
680
680
|
chunks: mergeDeltas(pending.splice(0)),
|
|
681
681
|
})
|
|
682
682
|
const reader = options.source.stream.getReader()
|
|
683
|
-
|
|
683
|
+
// The next read starts before a batch is yielded, and the consumer may
|
|
684
|
+
// take a while to ask again: a stream that fails meanwhile must not leave
|
|
685
|
+
// that read rejected with no handler, which ends the process.
|
|
686
|
+
const readAhead = (): Promise<StreamRead<UIMessageChunk>> => {
|
|
687
|
+
const read = reader.read()
|
|
688
|
+
read.catch(() => {})
|
|
689
|
+
return read
|
|
690
|
+
}
|
|
691
|
+
let pendingChunk = readAhead()
|
|
684
692
|
try {
|
|
685
693
|
for (;;) {
|
|
686
694
|
let result: StreamRead<UIMessageChunk>
|
|
@@ -701,7 +709,7 @@ async function* consumeGeneration(options: {
|
|
|
701
709
|
pending.push(chunk)
|
|
702
710
|
if (chunk.type === 'finish') streamedFinishReason = chunk.finishReason
|
|
703
711
|
if (chunk.type === 'error') streamedError = chunk.errorText
|
|
704
|
-
pendingChunk =
|
|
712
|
+
pendingChunk = readAhead()
|
|
705
713
|
|
|
706
714
|
if (pending.length >= maxChunks || isBoundaryChunk(pending.at(-1))) {
|
|
707
715
|
yield batch()
|
|
@@ -712,7 +720,6 @@ async function* consumeGeneration(options: {
|
|
|
712
720
|
if (pending.length > 0) yield batch()
|
|
713
721
|
throw error
|
|
714
722
|
} finally {
|
|
715
|
-
void pendingChunk.catch(() => {})
|
|
716
723
|
try {
|
|
717
724
|
await reader.cancel()
|
|
718
725
|
} catch {
|
package/src/ai-fx.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
saveHarness,
|
|
31
31
|
} from './ai-harness.ts'
|
|
32
32
|
import type { CompactionPartData, GenerationCompletedPayload } from './ai.ts'
|
|
33
|
+
import { schemaInput } from './ai-tool-input.ts'
|
|
33
34
|
import { boundedCache } from './bounded-cache.ts'
|
|
34
35
|
|
|
35
36
|
export type { Harness } from './ai-harness.ts'
|
|
@@ -49,6 +50,7 @@ const SNAPSHOT_VERSION = 1
|
|
|
49
50
|
const CATALOG_TTL_MS = 60 * 60 * 1000
|
|
50
51
|
|
|
51
52
|
type StreamPart = { type: string } & Record<string, unknown>
|
|
53
|
+
/** A recorded tool call, with the input the kernel reads from its text. */
|
|
52
54
|
type ToolCall = { toolCallId: string; toolName: string; input: unknown }
|
|
53
55
|
|
|
54
56
|
/** A model response A2 recorded, in the Gateway's stream format. */
|
|
@@ -108,6 +110,10 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
108
110
|
|
|
109
111
|
let completion: Completion = {}
|
|
110
112
|
let stopped = false
|
|
113
|
+
// Live responses belong to the step: the kernel aborts its fetch once it
|
|
114
|
+
// has read a response's finish, while A2 may still be reading its copy.
|
|
115
|
+
const released = new AbortController()
|
|
116
|
+
const network = AbortSignal.any([step.signal, released.signal])
|
|
111
117
|
let agent: FxAgent | undefined
|
|
112
118
|
let turn: TurnState | undefined
|
|
113
119
|
|
|
@@ -245,9 +251,18 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
245
251
|
const response = await fetch(url, {
|
|
246
252
|
...init,
|
|
247
253
|
headers: withAuth(init.headers, credential),
|
|
254
|
+
signal: network,
|
|
248
255
|
})
|
|
256
|
+
// Abandoned before its headers came: the kernel has moved on without it.
|
|
257
|
+
if (init.signal?.aborted) {
|
|
258
|
+
void response.body?.cancel()
|
|
259
|
+
throw init.signal.reason
|
|
260
|
+
}
|
|
249
261
|
if (!response.ok || response.body === null) return response
|
|
250
|
-
const [
|
|
262
|
+
const [fromNetwork, forLog] = response.body.tee()
|
|
263
|
+
const forKernel = init.signal
|
|
264
|
+
? fromNetwork.pipeThrough(new TransformStream(), { signal: init.signal })
|
|
265
|
+
: fromNetwork
|
|
251
266
|
// Live responses reach the log in request order: a compaction's part
|
|
252
267
|
// precedes the step that follows it.
|
|
253
268
|
const previous = state.parsed
|
|
@@ -268,8 +283,13 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
268
283
|
state.parsed = (async () => {
|
|
269
284
|
await previous
|
|
270
285
|
emit({ type: 'start-step' })
|
|
271
|
-
await streamStep(
|
|
272
|
-
|
|
286
|
+
await streamStep({
|
|
287
|
+
body: forLog,
|
|
288
|
+
tools: step.tools,
|
|
289
|
+
emit,
|
|
290
|
+
finished: (finished) => {
|
|
291
|
+
completion = finished
|
|
292
|
+
},
|
|
273
293
|
})
|
|
274
294
|
})()
|
|
275
295
|
}
|
|
@@ -357,6 +377,7 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
357
377
|
step.signal.aborted ? step.signal.reason : error,
|
|
358
378
|
)
|
|
359
379
|
} finally {
|
|
380
|
+
released.abort()
|
|
360
381
|
step.signal.removeEventListener('abort', abort)
|
|
361
382
|
await agent?.close().catch(() => {})
|
|
362
383
|
}
|
|
@@ -365,6 +386,7 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
365
386
|
cancel() {
|
|
366
387
|
closed = true
|
|
367
388
|
stopped = true
|
|
389
|
+
released.abort()
|
|
368
390
|
turn?.fxTurn.cancel()
|
|
369
391
|
},
|
|
370
392
|
})
|
|
@@ -448,36 +470,35 @@ function recordedCalls(
|
|
|
448
470
|
})
|
|
449
471
|
break
|
|
450
472
|
case 'tool-input-error':
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
inputText.get(chunk.toolCallId) ||
|
|
460
|
-
(typeof chunk.input === 'string'
|
|
461
|
-
? chunk.input
|
|
462
|
-
: JSON.stringify(chunk.input)),
|
|
463
|
-
})
|
|
464
|
-
break
|
|
465
|
-
case 'tool-input-available':
|
|
473
|
+
case 'tool-input-available': {
|
|
474
|
+
// A call recorded without its text: input that failed to parse is
|
|
475
|
+
// that text.
|
|
476
|
+
const text =
|
|
477
|
+
inputText.get(chunk.toolCallId) ||
|
|
478
|
+
(chunk.type === 'tool-input-error' && typeof chunk.input === 'string'
|
|
479
|
+
? chunk.input
|
|
480
|
+
: JSON.stringify(chunk.input))
|
|
466
481
|
if (inputText.has(chunk.toolCallId))
|
|
467
482
|
parts.push({ type: 'tool-input-end', id: chunk.toolCallId })
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
483
|
+
// The kernel calls back with this text's JSON, not the input the
|
|
484
|
+
// tool's schema made of it. A call it answers itself, such as one
|
|
485
|
+
// whose text is not JSON, is never asked for.
|
|
486
|
+
const input = parseJson(text)
|
|
487
|
+
if (input !== undefined)
|
|
488
|
+
toolCalls.push({
|
|
489
|
+
toolCallId: chunk.toolCallId,
|
|
490
|
+
toolName: chunk.toolName,
|
|
491
|
+
input,
|
|
492
|
+
})
|
|
473
493
|
parts.push({
|
|
474
494
|
type: 'tool-call',
|
|
475
495
|
toolCallId: chunk.toolCallId,
|
|
476
496
|
toolName: chunk.toolName,
|
|
477
|
-
input:
|
|
497
|
+
input: text,
|
|
478
498
|
...metadata,
|
|
479
499
|
})
|
|
480
500
|
break
|
|
501
|
+
}
|
|
481
502
|
}
|
|
482
503
|
}
|
|
483
504
|
parts.push({
|
|
@@ -505,11 +526,19 @@ function compactionSse(data: CompactionPartData): string {
|
|
|
505
526
|
// ------------------------------------------------------- live responses
|
|
506
527
|
|
|
507
528
|
/** Streams one live step response as UI chunks and reports its finish. */
|
|
508
|
-
async function streamStep(
|
|
509
|
-
body
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
529
|
+
async function streamStep({
|
|
530
|
+
body,
|
|
531
|
+
tools,
|
|
532
|
+
emit,
|
|
533
|
+
finished,
|
|
534
|
+
}: {
|
|
535
|
+
body: ReadableStream<Uint8Array>
|
|
536
|
+
tools: ToolSet
|
|
537
|
+
emit: (chunk: UIMessageChunk) => void
|
|
538
|
+
finished: (completion: Completion) => void
|
|
539
|
+
}): Promise<void> {
|
|
540
|
+
// The tool calls whose input started streaming, and whether text arrived.
|
|
541
|
+
const streamed = new Map<string, boolean>()
|
|
513
542
|
for await (const part of sseParts(body)) {
|
|
514
543
|
const metadata =
|
|
515
544
|
part['providerMetadata'] === undefined
|
|
@@ -532,6 +561,7 @@ async function streamStep(
|
|
|
532
561
|
})
|
|
533
562
|
break
|
|
534
563
|
case 'tool-input-start':
|
|
564
|
+
streamed.set(part['id'] as string, false)
|
|
535
565
|
emit({
|
|
536
566
|
type: 'tool-input-start',
|
|
537
567
|
toolCallId: part['id'] as string,
|
|
@@ -540,6 +570,7 @@ async function streamStep(
|
|
|
540
570
|
})
|
|
541
571
|
break
|
|
542
572
|
case 'tool-input-delta':
|
|
573
|
+
if (part['delta'] !== '') streamed.set(part['id'] as string, true)
|
|
543
574
|
emit({
|
|
544
575
|
type: 'tool-input-delta',
|
|
545
576
|
toolCallId: part['id'] as string,
|
|
@@ -547,27 +578,39 @@ async function streamStep(
|
|
|
547
578
|
})
|
|
548
579
|
break
|
|
549
580
|
case 'tool-call': {
|
|
581
|
+
const toolCallId = part['toolCallId'] as string
|
|
582
|
+
const toolName = part['toolName'] as string
|
|
550
583
|
const text = part['input'] as string
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
toolCallId: part['toolCallId'] as string,
|
|
558
|
-
toolName: part['toolName'] as string,
|
|
559
|
-
input: text,
|
|
560
|
-
errorText: error instanceof Error ? error.message : String(error),
|
|
561
|
-
})
|
|
562
|
-
break
|
|
584
|
+
// Replay hands the kernel the text the log holds, so the log keeps
|
|
585
|
+
// the provider's text when it streamed none.
|
|
586
|
+
if (streamed.get(toolCallId) !== true && text !== '') {
|
|
587
|
+
if (!streamed.has(toolCallId))
|
|
588
|
+
emit({ type: 'tool-input-start', toolCallId, toolName })
|
|
589
|
+
emit({ type: 'tool-input-delta', toolCallId, inputTextDelta: text })
|
|
563
590
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
input,
|
|
569
|
-
...metadata,
|
|
591
|
+
const input = await parseToolInput({
|
|
592
|
+
tools,
|
|
593
|
+
toolName,
|
|
594
|
+
text,
|
|
570
595
|
})
|
|
596
|
+
emit(
|
|
597
|
+
input.success
|
|
598
|
+
? {
|
|
599
|
+
type: 'tool-input-available',
|
|
600
|
+
toolCallId,
|
|
601
|
+
toolName,
|
|
602
|
+
input: input.value,
|
|
603
|
+
...metadata,
|
|
604
|
+
}
|
|
605
|
+
: {
|
|
606
|
+
type: 'tool-input-error',
|
|
607
|
+
toolCallId,
|
|
608
|
+
toolName,
|
|
609
|
+
input: input.input,
|
|
610
|
+
errorText: input.errorText,
|
|
611
|
+
...metadata,
|
|
612
|
+
},
|
|
613
|
+
)
|
|
571
614
|
break
|
|
572
615
|
}
|
|
573
616
|
case 'finish': {
|
|
@@ -587,6 +630,34 @@ async function streamStep(
|
|
|
587
630
|
}
|
|
588
631
|
}
|
|
589
632
|
|
|
633
|
+
type ToolInput =
|
|
634
|
+
| { success: true; value: unknown }
|
|
635
|
+
| { success: false; input: unknown; errorText: string }
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* A live call's input as the AI SDK step reads it: JSON, then the tool's
|
|
639
|
+
* schema with its defaults. Input that fails either is an input error, which
|
|
640
|
+
* A2 answers without running the tool.
|
|
641
|
+
*/
|
|
642
|
+
async function parseToolInput({
|
|
643
|
+
tools,
|
|
644
|
+
toolName,
|
|
645
|
+
text,
|
|
646
|
+
}: {
|
|
647
|
+
tools: ToolSet
|
|
648
|
+
toolName: string
|
|
649
|
+
text: string
|
|
650
|
+
}): Promise<ToolInput> {
|
|
651
|
+
let value: unknown
|
|
652
|
+
try {
|
|
653
|
+
value = JSON.parse(text)
|
|
654
|
+
} catch (error) {
|
|
655
|
+
return { success: false, input: text, errorText: errorText(error) }
|
|
656
|
+
}
|
|
657
|
+
const input = await schemaInput({ tools, toolName, value })
|
|
658
|
+
return input.success ? input : { ...input, input: value }
|
|
659
|
+
}
|
|
660
|
+
|
|
590
661
|
async function readParts(
|
|
591
662
|
body: ReadableStream<Uint8Array>,
|
|
592
663
|
): Promise<StreamPart[]> {
|
|
@@ -760,6 +831,14 @@ function decodeBody(body: BodyInit | null | undefined): string {
|
|
|
760
831
|
throw new TypeError('FX sent an unexpected request body')
|
|
761
832
|
}
|
|
762
833
|
|
|
834
|
+
function parseJson(text: string): unknown {
|
|
835
|
+
try {
|
|
836
|
+
return JSON.parse(text)
|
|
837
|
+
} catch {
|
|
838
|
+
return undefined
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
763
842
|
function sameJson(a: unknown, b: unknown): boolean {
|
|
764
843
|
return JSON.stringify(a) === JSON.stringify(b)
|
|
765
844
|
}
|