experimental-a2 0.12.0 → 0.14.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 +47 -0
- package/dist/{actor-DJi3RsNu.d.ts → actor-BfQSE0KC.d.ts} +4 -4
- package/dist/{actor-DJi3RsNu.d.ts.map → actor-BfQSE0KC.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +2 -2
- package/dist/{actor-shared-DI7J5upy.js → actor-shared-B5tJfzt-.js} +2 -2
- package/dist/{actor-shared-DI7J5upy.js.map → actor-shared-B5tJfzt-.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +3 -3
- package/dist/ai-Cai-lCbj.d.ts +580 -0
- package/dist/ai-Cai-lCbj.d.ts.map +1 -0
- package/dist/ai-control-CcD4hh3y.js +119 -0
- package/dist/ai-control-CcD4hh3y.js.map +1 -0
- package/dist/ai-server.d.ts +16 -8
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +1322 -475
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -334
- package/dist/ai.js +838 -85
- package/dist/ai.js.map +1 -1
- package/dist/{client-P_NNNRM-.d.ts → client-BAEABRZB.d.ts} +2 -2
- package/dist/{client-P_NNNRM-.d.ts.map → client-BAEABRZB.d.ts.map} +1 -1
- package/dist/{client-Bf6uSEAk.js → client-BYzHjkwU.js} +21 -6
- package/dist/client-BYzHjkwU.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/{contract-48bUMgcL.js → contract-CKRg_E4q.js} +3 -26
- package/dist/contract-CKRg_E4q.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{reducer-DJKWm3cp.d.ts → reducer-BcS9VDKC.d.ts} +4 -1
- package/dist/{reducer-DJKWm3cp.d.ts.map → reducer-BcS9VDKC.d.ts.map} +1 -1
- package/dist/reducer-DEMjEY_O.js +29 -0
- package/dist/reducer-DEMjEY_O.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-DjZZa1wr.d.ts → server-Bp5Nd1pF.d.ts} +3 -3
- package/dist/{server-DjZZa1wr.d.ts.map → server-Bp5Nd1pF.d.ts.map} +1 -1
- package/dist/{server-BeNADlCI.js → server-CjJSGcF7.js} +4 -3
- package/dist/server-CjJSGcF7.js.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{store-DtDOWLSn.d.ts → store-D_yhNdPz.d.ts} +7 -2
- package/dist/{store-DtDOWLSn.d.ts.map → store-D_yhNdPz.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +19 -0
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +1 -1
- package/dist/{store-redis-notify-BUCyXOn0.js → store-redis-notify-D2EI6gwX.js} +27 -2
- package/dist/store-redis-notify-D2EI6gwX.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/docs/guides/06-ai-agents.mdx +361 -76
- package/docs/reference/01-api.mdx +159 -27
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +145 -33
- package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +14 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +38 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +294 -0
- package/examples/playground/app/agent/compaction-settings.test.ts +144 -0
- package/examples/playground/app/agent/compaction-settings.ts +49 -0
- package/examples/playground/app/agent/compaction-timeline.test.ts +337 -0
- package/examples/playground/app/agent/compaction-timeline.ts +198 -0
- package/examples/playground/app/agent/model.ts +56 -1
- package/examples/playground/app/agent/server.ts +9 -2
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
- package/examples/playground/app/chat/model.ts +2 -2
- package/examples/playground/app/chat/server.ts +24 -17
- package/examples/playground/app/globals.css +333 -0
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/ai-client-state.ts +185 -0
- package/src/ai-control-server.ts +829 -0
- package/src/ai-control-state.ts +152 -0
- package/src/ai-control.ts +139 -0
- package/src/ai-coordinator.ts +99 -32
- package/src/ai-model-metadata.ts +108 -0
- package/src/ai-progress-batches.ts +68 -0
- package/src/ai-projector.ts +76 -15
- package/src/ai-sdk-step.ts +5 -2
- package/src/ai-server.ts +920 -638
- package/src/ai.ts +650 -110
- package/src/client.ts +31 -9
- package/src/licenses/Apache-2.0.txt +55 -0
- package/src/parse-partial-json.ts +441 -0
- package/src/reducer.ts +6 -0
- package/src/server.ts +8 -4
- package/src/store-postgres.ts +27 -0
- package/src/store-redis-core.ts +53 -1
- package/src/store-redis-notify.ts +1 -0
- package/src/store.ts +6 -0
- package/dist/ai.d.ts.map +0 -1
- package/dist/client-Bf6uSEAk.js.map +0 -1
- package/dist/contract-48bUMgcL.js.map +0 -1
- package/dist/server-BeNADlCI.js.map +0 -1
- package/dist/store-redis-notify-BUCyXOn0.js.map +0 -1
|
@@ -697,6 +697,13 @@ Raw events from the session, oldest first. `gte` and `lte` are inclusive event
|
|
|
697
697
|
indexes; omit a bound to leave that end of the log open.
|
|
698
698
|
Bounds are non-negative safe integers. `lte: 0` returns `[]`; `gte > lte` throws.
|
|
699
699
|
|
|
700
|
+
Redis reads a fixed committed range in pages of at most 256 events, with up to
|
|
701
|
+
sixteen page reads in flight. Public history omits dispatch metadata. PostgreSQL
|
|
702
|
+
uses one indexed range query selecting only public event columns. Custom store
|
|
703
|
+
adapters can implement `readHistory(sessionId, options)` with the same exclusive
|
|
704
|
+
`afterIndex` and inclusive `throughIndex` bounds as `read()`. A2 falls back to
|
|
705
|
+
`read()` when that capability is absent.
|
|
706
|
+
|
|
700
707
|
### `session.state(reducer, options?)`
|
|
701
708
|
|
|
702
709
|
```ts
|
|
@@ -1033,14 +1040,12 @@ result and is verified against the executing server's contract: a
|
|
|
1033
1040
|
mismatched agent, or a call outside any tool execution, throws a
|
|
1034
1041
|
`TypeError`.
|
|
1035
1042
|
|
|
1036
|
-
The returned `AgentToolContext<D>`
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
`
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
hold a scheduler. Calls such as `ctx.session.schedule()` use the
|
|
1043
|
-
scheduler of the server executing the tool.
|
|
1043
|
+
The returned `AgentToolContext<D>` supplies the current A2 handler's `event`,
|
|
1044
|
+
`attempt`, `session`, and `signal`. Local tools execute the explicit
|
|
1045
|
+
`ai.tool.execution.requested` work event. Its payload contains the admitted
|
|
1046
|
+
`call`, `generation`, `turnId`, and execution `version`. The session has the
|
|
1047
|
+
supplied agent's complete event vocabulary. Calls such as
|
|
1048
|
+
`ctx.session.schedule()` use the scheduler of the server executing the tool.
|
|
1044
1049
|
|
|
1045
1050
|
The context is runtime capability, not event data. A2 creates it for the
|
|
1046
1051
|
current attempt, never persists it, and never sends it to the model or a
|
|
@@ -1060,29 +1065,72 @@ Pure typed inputs for `append()` and `push()`:
|
|
|
1060
1065
|
|
|
1061
1066
|
| Input | Events |
|
|
1062
1067
|
| --- | --- |
|
|
1063
|
-
| `inputs.message(message, { generate? })` |
|
|
1064
|
-
| `inputs.seed(message)` |
|
|
1065
|
-
| `inputs.approval(response)` |
|
|
1066
|
-
| `inputs.input(response)` |
|
|
1067
|
-
| `inputs.requestInput(request)` |
|
|
1068
|
-
| `inputs.retry(options)` |
|
|
1069
|
-
| `inputs.
|
|
1068
|
+
| `inputs.message(message, { generate? })` | queues input; `generate: false` records passive context at admission |
|
|
1069
|
+
| `inputs.seed(message)` | queues passive context; non-user roles require a trusted append |
|
|
1070
|
+
| `inputs.approval(response)` | answers a pending tool approval |
|
|
1071
|
+
| `inputs.input(response)` | answers a pending application input |
|
|
1072
|
+
| `inputs.requestInput(request)` | requests application input from trusted server code |
|
|
1073
|
+
| `inputs.retry(options)` | retries a failed response |
|
|
1074
|
+
| `inputs.stop({ turnId, lastSeenIndex? })` | ends the observed turn and advances when admission is open |
|
|
1075
|
+
| `inputs.steer({ turnId, message, lastSeenIndex? })` | puts the new input first for a handoff after the observed turn's current step |
|
|
1076
|
+
| `inputs.queue.edit({ message, expectedRevision })` | changes a pending input; its ID stays fixed |
|
|
1077
|
+
| `inputs.queue.move({ inputId, beforeId })` | moves pending input before an ID; `null` means the end |
|
|
1078
|
+
| `inputs.queue.remove({ inputId })` | removes pending input; its identity stays reserved |
|
|
1079
|
+
| `inputs.queue.sendNow({ inputId, turnId, lastSeenIndex? })` | promotes an existing queued input for a handoff after the current step; preserves pause |
|
|
1080
|
+
| `inputs.pause({ when: 'now' \| 'after-turn' })` | pauses execution now or inbox admission after the turn |
|
|
1081
|
+
| `inputs.resume()` | resumes retained work, then inbox admission |
|
|
1082
|
+
| `inputs.toolResult(result)` | records a deferred provider-executed tool result from trusted server code |
|
|
1083
|
+
| `inputs.interrupt(options)` | interrupts a specific generation/request owner |
|
|
1084
|
+
|
|
1085
|
+
All builders emit `ai.control.requested`. Persistence and application are
|
|
1086
|
+
separate: `ai.control.decided` returns `{ commandId, outcome, reason? }` through
|
|
1087
|
+
the existing stream. Outcomes are `applied` or `rejected`. Rejection reasons
|
|
1088
|
+
include `already-active`, `revision-conflict`, `stale-turn`, `not-found`,
|
|
1089
|
+
`duplicate-input`, `not-user-input`, and `closed`. `state.receipt` holds the latest user receipt.
|
|
1090
|
+
|
|
1091
|
+
Steering validates and prioritizes new input even if the observed turn has
|
|
1092
|
+
finished. A matching active turn finishes its current model response and tool
|
|
1093
|
+
batch, then yields before another model step. Running local workers and deferred
|
|
1094
|
+
provider results settle first; unanswered human approval/input waits can be
|
|
1095
|
+
closed at that point. Pause stays closed. Otherwise the input starts when idle
|
|
1096
|
+
or waits first behind a different active turn. The priority change is atomic;
|
|
1097
|
+
the later handoff and new work are committed atomically at the step boundary.
|
|
1098
|
+
Steer and Send now do not use `lastSeenIndex` to cut output.
|
|
1099
|
+
|
|
1100
|
+
Queue removal leaves the log intact and retains the removed identity's reservation.
|
|
1101
|
+
Send now preserves the queued input ID and latest accepted revision. It requires
|
|
1102
|
+
an observed `turnId` (or null for idle), preserves pause, and only accepts
|
|
1103
|
+
user-role input. It promotes passive user input to generation after reserving its
|
|
1104
|
+
response ID. Missing targets, stale turns, and identity collisions reject before
|
|
1105
|
+
any queue change.
|
|
1106
|
+
|
|
1107
|
+
`state.inbox` contains `paused` and ordered `items`. Each item contains
|
|
1108
|
+
`{ id, revision, message, generate, afterStepOf? }`. The optional turn ID
|
|
1109
|
+
`afterStepOf` records the observed steering turn while that item remains queued;
|
|
1110
|
+
a matching active turn hands off after its current step.
|
|
1111
|
+
Removal cancels it and admission consumes it. `state.active` is null or contains
|
|
1112
|
+
`{ turnId, input, phase }`; admission freezes `input`. On the server, pending
|
|
1113
|
+
inputs are absent from `messages` and model context. The `paused` status and the `pausing`/`paused`
|
|
1114
|
+
active phases represent retained work. Resume uses a fresh model request and
|
|
1115
|
+
execution version. Tools without terminal outcomes can retry; their stable
|
|
1116
|
+
`toolCallId` supports external idempotency.
|
|
1070
1117
|
|
|
1071
1118
|
`inputs` deliberately has no session lifecycle methods. Append explicit
|
|
1072
1119
|
`ai.session.created` and `ai.session.closed` events from trusted server code
|
|
1073
|
-
when an application uses them.
|
|
1074
|
-
|
|
1120
|
+
when an application uses them. Message, approval, input, and retry builders use stable interaction IDs.
|
|
1121
|
+
Other commands use IDs assigned by `push()` or `append()`; transport retries
|
|
1122
|
+
preserve those IDs. Browser
|
|
1075
1123
|
ingress accepts user messages, approval and input responses, interruptions,
|
|
1076
1124
|
and explicit retries. Only built-in server handlers append generation requests
|
|
1077
|
-
and AI lifecycle events. `inputs.requestInput()` and non-user messages built
|
|
1125
|
+
and AI lifecycle events. `inputs.requestInput()`, `inputs.toolResult()`, and non-user messages built
|
|
1078
1126
|
with `inputs.seed()` are for trusted server appends.
|
|
1079
1127
|
|
|
1080
|
-
`inputs.message(message, { generate: false })` is valid browser input. It
|
|
1081
|
-
the user message
|
|
1128
|
+
`inputs.message(message, { generate: false })` is valid browser input. It admits
|
|
1129
|
+
the user message to `AIState.messages` without starting a model turn. The next
|
|
1082
1130
|
user message that allows generation includes passive user messages before it
|
|
1083
1131
|
in model context. Passive user messages after that trigger wait for a later
|
|
1084
1132
|
generating message. Omitting the option preserves the default scheduling
|
|
1085
|
-
behavior
|
|
1133
|
+
behavior. `inputs.seed()` writes
|
|
1086
1134
|
the same passive flag. Trusted assistant and system seeds are context, not user
|
|
1087
1135
|
queue cutpoints. The server never schedules them, regardless of the `generate`
|
|
1088
1136
|
field. Model output is generation progress rather than another
|
|
@@ -1117,12 +1165,44 @@ Completed tool results at or before `lastSeenIndex` remain completed. A final
|
|
|
1117
1165
|
tool result that races an ordinary generation failure remains authoritative in
|
|
1118
1166
|
either commit order, while the generation stays failed.
|
|
1119
1167
|
|
|
1168
|
+
Interruption and failure projections discard tool parts still in
|
|
1169
|
+
`input-streaming`. These argument drafts are not completed tool calls and do not
|
|
1170
|
+
enter later model prompts. Raw progress remains in the log. Available inputs,
|
|
1171
|
+
approval-waiting calls, and completed results retain their existing terminal
|
|
1172
|
+
handling.
|
|
1173
|
+
|
|
1120
1174
|
### `events` and `createEvents(options?)`
|
|
1121
1175
|
|
|
1122
1176
|
The built-in Standard Schema definitions. Use `events` for the default AI SDK
|
|
1123
1177
|
`UIMessage`; use `createEvents({ messageSchema })` to validate a more specific
|
|
1124
1178
|
message type. Both are isomorphic.
|
|
1125
1179
|
|
|
1180
|
+
### Optimistic AI client state
|
|
1181
|
+
|
|
1182
|
+
Clients using `agent().reducer` or `createReducer()` expose `state.messages`,
|
|
1183
|
+
`state.inbox`, and `state.starting` with pending send, edit, move, remove,
|
|
1184
|
+
Send now, steer, pause, and resume commands applied for display. Idle, unpaused
|
|
1185
|
+
sends appear in the conversation immediately. A generating input occupies the
|
|
1186
|
+
optimistic turn, so subsequent sends queue. Busy or paused sessions queue new
|
|
1187
|
+
sends. `starting` indicates a speculative turn awaiting acceptance; it does not
|
|
1188
|
+
indicate model execution. Confirmed turns take precedence during reconciliation.
|
|
1189
|
+
Steering inputs, and Send now inputs targeting an active turn, appear in
|
|
1190
|
+
`state.messages` after the current response and are omitted from the client
|
|
1191
|
+
inbox. This display survives receipt and hydration, including while paused.
|
|
1192
|
+
They remain pending server inputs until admission and do not enter the model
|
|
1193
|
+
prompt early. Multiple steering inputs follow inbox admission order.
|
|
1194
|
+
|
|
1195
|
+
`state.active`, model execution, and revision numbers stay confirmed.
|
|
1196
|
+
Server reads and reducer folds retain confirmed messages and inbox entries;
|
|
1197
|
+
`state.starting` is false there. Custom reducers that call `reduceAIState()` or
|
|
1198
|
+
nest the AI fold do not inherit the client display behavior. Disable repeat editing and Send now while an
|
|
1199
|
+
edit for that input is present in `state.pendingQueueCommands`.
|
|
1200
|
+
|
|
1201
|
+
The pending-command ledger survives request echo and retires on the matching
|
|
1202
|
+
`ai.control.decided` receipt or session close. Failed pushes roll back through
|
|
1203
|
+
the client's existing optimistic overlay. This display adds no I/O and does not
|
|
1204
|
+
read raw history.
|
|
1205
|
+
|
|
1126
1206
|
### `createReducer(options)`
|
|
1127
1207
|
|
|
1128
1208
|
```ts
|
|
@@ -1130,7 +1210,8 @@ createReducer({ contract, name? }): Reducer<AIState>
|
|
|
1130
1210
|
```
|
|
1131
1211
|
|
|
1132
1212
|
Builds the standard AI projection for a compatible contract. `AIState`
|
|
1133
|
-
contains
|
|
1213
|
+
contains `inbox`, `active`, `starting`, the latest command `receipt`, session lifecycle,
|
|
1214
|
+
messages, generation status, pending approvals
|
|
1134
1215
|
and input, tool activity, compaction, usage, the last error, and
|
|
1135
1216
|
`activeRequestId`, `activeResponseMessageId`,
|
|
1136
1217
|
`responseGenerationIds: Record<string, string>`, and terminal request and
|
|
@@ -1140,7 +1221,7 @@ delayed requests before their generation starts. `activeResponseMessageId`
|
|
|
1140
1221
|
identifies the requested response until `activeGeneration` exists.
|
|
1141
1222
|
`activeProjection` holds the indexed generation frontier while a generation is
|
|
1142
1223
|
active, after a generation step completes while its response waits on tool,
|
|
1143
|
-
approval, or input barriers, and after a generation fails while it awaits retry
|
|
1224
|
+
approval, or input barriers, while paused, and after a generation fails while it awaits retry
|
|
1144
1225
|
or interruption. Response completion, interruption, supersession, retry, or a
|
|
1145
1226
|
later generation clears or replaces it. `responseGenerationIds` keeps the
|
|
1146
1227
|
latest generation owner for each response message, so late events from a
|
|
@@ -1149,7 +1230,19 @@ superseded owner cannot alter the projection. `terminalRequestIds` and
|
|
|
1149
1230
|
and supersession fences across snapshots and recovery. They are optional
|
|
1150
1231
|
snapshot-compatible fields with the shapes `Record<string, true>` and
|
|
1151
1232
|
`Record<string, 'completed' | 'failed' | 'interrupted' | 'superseded'>`.
|
|
1152
|
-
|
|
1233
|
+
The active projection also stores its prompt boundary and a serializable stream
|
|
1234
|
+
cursor for open text, reasoning, and tool-input parts. Ordered progress applies
|
|
1235
|
+
only its new chunks. `activeProjection.batches` stores indexed batches as
|
|
1236
|
+
`{ length, tail, blocks }`: a bounded tail and shared completed blocks. Appending
|
|
1237
|
+
ordered progress does not copy the full retained batch list. The blocks are
|
|
1238
|
+
ordinary JSON with logarithmic nesting depth, so checkpoints can be serialized
|
|
1239
|
+
and cloned. Retained batches support exact interruption cutoffs and out-of-order
|
|
1240
|
+
reconstruction. The cursor and blocks share the active projection's lifetime.
|
|
1241
|
+
Checkpoint size and serialization cost still grow with retained progress.
|
|
1242
|
+
Tool-input deltas update `input-streaming` parts with best-effort partial JSON
|
|
1243
|
+
for display. The state stays `input-streaming` until a completed input arrives;
|
|
1244
|
+
partial parsing does not authorize execution or change interruption handling.
|
|
1245
|
+
Extension events are ignored. The default reducer name is `a2.ai.state.v17`.
|
|
1153
1246
|
|
|
1154
1247
|
### `deriveUIMessages(history)` and `reduceAIState(state, event)`
|
|
1155
1248
|
|
|
@@ -1243,9 +1336,48 @@ support wait for their provider result instead of a local executor. An
|
|
|
1243
1336
|
authenticated provider callback appends the terminal `ai.tool.result` through
|
|
1244
1337
|
the trusted server session API; the browser push allowlist rejects it.
|
|
1245
1338
|
|
|
1246
|
-
`compaction`
|
|
1247
|
-
|
|
1248
|
-
|
|
1339
|
+
`compaction` defaults to automatic for supported Gateway requests. It accepts
|
|
1340
|
+
`false` to disable compaction and discovery, `{ thresholdTokens?, instructions? }`
|
|
1341
|
+
to override automatic behavior, or the existing custom
|
|
1342
|
+
`{ shouldCompact(context), compact(context) }` policy. It also accepts a
|
|
1343
|
+
resolver `(context: AgentResolverContext) => CompactionOptions | Promise<CompactionOptions>`.
|
|
1344
|
+
The resolver runs once per generation and returns a valid policy or `false`.
|
|
1345
|
+
It can await application reducers through `context.session.state(reducer)`, whose
|
|
1346
|
+
default boundary is the captured prompt frontier. Model, instruction, compaction,
|
|
1347
|
+
metadata, and custom-generation contexts expose state access and omit raw history.
|
|
1348
|
+
Static options are validated at construction; resolved options are validated
|
|
1349
|
+
before that generation starts. Each session uses its own resolved value. `thresholdTokens` is an
|
|
1350
|
+
optional positive safe integer. `instructions` adds to A2's internal summary
|
|
1351
|
+
prompt without replacing the agent's instructions.
|
|
1352
|
+
|
|
1353
|
+
Without an explicit threshold, the first generation batches a metadata request
|
|
1354
|
+
into its existing start write. An independent handler reads the public Gateway
|
|
1355
|
+
catalog and records that session/model's limits. It does not delay model dispatch.
|
|
1356
|
+
The default threshold is 75% of the context window, lowered for a larger configured
|
|
1357
|
+
output allowance. A pending or unavailable lookup leaves the threshold unknown;
|
|
1358
|
+
the first request proceeds, and provider context-limit errors fail normally.
|
|
1359
|
+
`state.modelMetadata` retains each model's pending, resolved, or unavailable state.
|
|
1360
|
+
Pending also covers exhausted handler retries; no new request is added per turn.
|
|
1361
|
+
The catalog has a shared one-hour process cache and a five-second fetch timeout.
|
|
1362
|
+
|
|
1363
|
+
Direct/custom providers, fallback model lists, and custom generators need explicit
|
|
1364
|
+
compaction configuration. Provider-executed tools, structured output, and forced
|
|
1365
|
+
tool choices skip implicit compaction and reject explicit automatic policies.
|
|
1366
|
+
Custom policies remain available for these configurations.
|
|
1367
|
+
|
|
1368
|
+
The input estimate includes messages, tools, and instructions, and uses prior
|
|
1369
|
+
reported input usage to anchor later growth where applicable. It is not a provider
|
|
1370
|
+
tokenizer. Completed generations record `inputTokenEstimate` beside usage. The
|
|
1371
|
+
built-in summary adds one model request and preserves the request prefix for
|
|
1372
|
+
caching. It does not invoke application generation callbacks, transforms, or tool
|
|
1373
|
+
hooks. Both custom policy callbacks and `generate` receive `modelMessages`, the
|
|
1374
|
+
model-ready prompt including summaries, alongside typed UI `messages`.
|
|
1375
|
+
|
|
1376
|
+
Built-in summaries remain separate from application-typed messages. Render
|
|
1377
|
+
`state.compaction?.status === 'running'` as current activity, and the durable
|
|
1378
|
+
`ai.compaction.*` events as historical timeline markers. The conversation in
|
|
1379
|
+
`state.messages` remains intact. `progress` controls durable batching with
|
|
1380
|
+
`maxChunks` and `maxDelayMs`.
|
|
1249
1381
|
|
|
1250
1382
|
This entry point is server-only and resolves to a throwing browser stub.
|
|
1251
1383
|
|
|
@@ -6,10 +6,14 @@ import type {
|
|
|
6
6
|
} from 'react'
|
|
7
7
|
import { useEffect, useRef, useState } from 'react'
|
|
8
8
|
import type { UIMessage } from 'ai'
|
|
9
|
-
import { inputs } from 'experimental-a2/ai'
|
|
9
|
+
import { inputs, type ControlCommand } from 'experimental-a2/ai'
|
|
10
10
|
import { ActivityFeed } from '@/app/components/activity-feed'
|
|
11
11
|
import { ConnectionPill } from '@/app/components/connection-pill'
|
|
12
12
|
import { useSession } from '../session'
|
|
13
|
+
import { compactionTimeline } from '../compaction-timeline'
|
|
14
|
+
import { CompactionEvent } from './compaction-event'
|
|
15
|
+
import { CompactionPanel } from './compaction-panel'
|
|
16
|
+
import { AgentQueue } from './agent-queue'
|
|
13
17
|
|
|
14
18
|
type MessagePart = UIMessage['parts'][number]
|
|
15
19
|
type ToolPartView = {
|
|
@@ -26,6 +30,7 @@ const STATUS_LABEL = {
|
|
|
26
30
|
waiting: 'approval needed',
|
|
27
31
|
failed: 'failed',
|
|
28
32
|
closed: 'closed',
|
|
33
|
+
paused: 'paused',
|
|
29
34
|
} as const
|
|
30
35
|
|
|
31
36
|
const toolTitle = (name: string): string =>
|
|
@@ -113,11 +118,11 @@ const hasVisibleParts = (message: UIMessage): boolean =>
|
|
|
113
118
|
toolPartView(part) !== null,
|
|
114
119
|
)
|
|
115
120
|
|
|
116
|
-
function Thinking(): ReactNode {
|
|
121
|
+
function Thinking({ starting = false }: { starting?: boolean }): ReactNode {
|
|
117
122
|
return (
|
|
118
123
|
<p className="agent-thinking" role="status">
|
|
119
124
|
<span aria-hidden="true" />
|
|
120
|
-
Thinking…
|
|
125
|
+
{starting ? 'Starting…' : 'Thinking…'}
|
|
121
126
|
</p>
|
|
122
127
|
)
|
|
123
128
|
}
|
|
@@ -163,24 +168,32 @@ function submitOnEnter(event: ReactKeyboardEvent<HTMLTextAreaElement>): void {
|
|
|
163
168
|
|
|
164
169
|
export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
165
170
|
const { state, push, events, index, connection } = useSession()
|
|
171
|
+
const { inbox } = state
|
|
166
172
|
const promptRef = useRef<HTMLTextAreaElement>(null)
|
|
167
173
|
const [prompt, setPrompt] = useState('')
|
|
168
174
|
const [error, setError] = useState<string | null>(null)
|
|
169
175
|
const [responding, setResponding] = useState(false)
|
|
170
176
|
|
|
171
177
|
const generating = state.status === 'generating'
|
|
172
|
-
const
|
|
178
|
+
const compacting = state.compaction?.status === 'running'
|
|
179
|
+
const canSend = state.status !== 'closed'
|
|
180
|
+
const responseMessageId =
|
|
181
|
+
state.activeGeneration?.responseMessageId ??
|
|
182
|
+
state.activeResponseMessageId ??
|
|
183
|
+
state.activeProjection?.responseMessageId
|
|
184
|
+
const timeline = compactionTimeline({ state, events })
|
|
173
185
|
|
|
174
186
|
useEffect(() => {
|
|
175
|
-
if (canSend) promptRef.current?.focus()
|
|
187
|
+
if (canSend) promptRef.current?.focus({ preventScroll: true })
|
|
176
188
|
}, [canSend])
|
|
177
189
|
|
|
178
|
-
const send = async (event
|
|
179
|
-
event
|
|
190
|
+
const send = async (event?: FormEvent, steer = false): Promise<void> => {
|
|
191
|
+
event?.preventDefault()
|
|
180
192
|
const submittedPrompt = prompt
|
|
181
193
|
const text = prompt.trim()
|
|
182
194
|
if (!text || !canSend) return
|
|
183
195
|
setPrompt('')
|
|
196
|
+
promptRef.current?.focus({ preventScroll: true })
|
|
184
197
|
setError(null)
|
|
185
198
|
try {
|
|
186
199
|
const message: UIMessage = {
|
|
@@ -188,7 +201,14 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
188
201
|
role: 'user',
|
|
189
202
|
parts: [{ type: 'text', text }],
|
|
190
203
|
}
|
|
191
|
-
await push(
|
|
204
|
+
await push(
|
|
205
|
+
...(steer && state.active
|
|
206
|
+
? inputs.steer({
|
|
207
|
+
turnId: state.active.turnId,
|
|
208
|
+
message,
|
|
209
|
+
})
|
|
210
|
+
: inputs.message(message)),
|
|
211
|
+
)
|
|
192
212
|
} catch (err) {
|
|
193
213
|
setPrompt((current) => (current === '' ? submittedPrompt : current))
|
|
194
214
|
setError(err instanceof Error ? err.message : String(err))
|
|
@@ -220,18 +240,27 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
220
240
|
}
|
|
221
241
|
}
|
|
222
242
|
|
|
243
|
+
const command = async (
|
|
244
|
+
commands: {
|
|
245
|
+
type: 'ai.control.requested'
|
|
246
|
+
payload: ControlCommand
|
|
247
|
+
id?: string
|
|
248
|
+
}[],
|
|
249
|
+
): Promise<void> => {
|
|
250
|
+
setError(null)
|
|
251
|
+
try {
|
|
252
|
+
await push(...commands)
|
|
253
|
+
} catch (err) {
|
|
254
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
223
258
|
const stop = async (): Promise<void> => {
|
|
224
|
-
|
|
225
|
-
if (!active) return
|
|
259
|
+
if (!state.active) return
|
|
226
260
|
setError(null)
|
|
227
261
|
try {
|
|
228
262
|
await push(
|
|
229
|
-
...inputs.
|
|
230
|
-
messageId: active.responseMessageId,
|
|
231
|
-
generationId: active.generationId,
|
|
232
|
-
reason: 'operator stopped the agent',
|
|
233
|
-
lastSeenIndex: index,
|
|
234
|
-
}),
|
|
263
|
+
...inputs.stop({ turnId: state.active.turnId, lastSeenIndex: index }),
|
|
235
264
|
)
|
|
236
265
|
} catch (err) {
|
|
237
266
|
setError(err instanceof Error ? err.message : String(err))
|
|
@@ -250,36 +279,56 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
250
279
|
state.status === 'waiting' ? ' waiting' : ''
|
|
251
280
|
}${state.status === 'failed' ? ' cut' : ''}`}
|
|
252
281
|
>
|
|
253
|
-
{
|
|
282
|
+
{state.starting
|
|
283
|
+
? 'starting…'
|
|
284
|
+
: compacting
|
|
285
|
+
? 'compacting…'
|
|
286
|
+
: STATUS_LABEL[state.status]}
|
|
254
287
|
</span>
|
|
255
288
|
</header>
|
|
256
289
|
|
|
290
|
+
<CompactionPanel agentId={agentId} state={state} events={events} />
|
|
291
|
+
|
|
257
292
|
<section className="agent-conversation" aria-label="Conversation">
|
|
258
293
|
<div className="agent-transcript" aria-live="polite">
|
|
259
|
-
{
|
|
294
|
+
{timeline.length === 0 ? (
|
|
260
295
|
<p className="agent-empty">
|
|
261
296
|
Ask the agent to investigate an incident, run a Bash command, or
|
|
262
297
|
set a reminder for itself. Its tools and decisions will appear
|
|
263
298
|
here as they enter the log.
|
|
264
299
|
</p>
|
|
265
300
|
) : (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
key={
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
message.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
301
|
+
timeline.map((entry) =>
|
|
302
|
+
entry.type === 'compaction' ? (
|
|
303
|
+
<CompactionEvent key={entry.id} entry={entry} />
|
|
304
|
+
) : (
|
|
305
|
+
<AgentMessage
|
|
306
|
+
key={entry.id}
|
|
307
|
+
message={entry.message}
|
|
308
|
+
thinking={
|
|
309
|
+
generating &&
|
|
310
|
+
!state.starting &&
|
|
311
|
+
!compacting &&
|
|
312
|
+
entry.lastSegment &&
|
|
313
|
+
entry.message.id ===
|
|
314
|
+
state.activeGeneration?.responseMessageId &&
|
|
315
|
+
!hasVisibleParts(entry.message)
|
|
316
|
+
}
|
|
317
|
+
/>
|
|
318
|
+
),
|
|
319
|
+
)
|
|
277
320
|
)}
|
|
278
|
-
{
|
|
321
|
+
{state.starting ||
|
|
322
|
+
(generating &&
|
|
323
|
+
!compacting &&
|
|
324
|
+
state.messages.at(-1)?.role === 'user' &&
|
|
325
|
+
!state.messages.some(
|
|
326
|
+
(message) => message.id === responseMessageId,
|
|
327
|
+
)) ? (
|
|
279
328
|
<div className="agent-message assistant">
|
|
280
329
|
<span className="agent-role">Agent</span>
|
|
281
330
|
<div>
|
|
282
|
-
<Thinking />
|
|
331
|
+
<Thinking starting={state.starting} />
|
|
283
332
|
</div>
|
|
284
333
|
</div>
|
|
285
334
|
) : null}
|
|
@@ -335,12 +384,43 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
335
384
|
</p>
|
|
336
385
|
) : null}
|
|
337
386
|
|
|
387
|
+
<AgentQueue
|
|
388
|
+
items={inbox.items}
|
|
389
|
+
paused={inbox.paused}
|
|
390
|
+
pendingEdits={(state.pendingQueueCommands ?? []).flatMap(
|
|
391
|
+
({ command: pending }) =>
|
|
392
|
+
pending.action === 'edit' ? [pending.message.id] : [],
|
|
393
|
+
)}
|
|
394
|
+
remove={async (inputId) => {
|
|
395
|
+
await command(inputs.queue.remove({ inputId }))
|
|
396
|
+
}}
|
|
397
|
+
sendNow={async (inputId) => {
|
|
398
|
+
await command(
|
|
399
|
+
inputs.queue.sendNow({
|
|
400
|
+
inputId,
|
|
401
|
+
turnId: state.active?.turnId ?? null,
|
|
402
|
+
}),
|
|
403
|
+
)
|
|
404
|
+
}}
|
|
405
|
+
edit={async (message, expectedRevision) => {
|
|
406
|
+
await push(...inputs.queue.edit({ message, expectedRevision }))
|
|
407
|
+
}}
|
|
408
|
+
move={(inputId, beforeId) =>
|
|
409
|
+
void command(inputs.queue.move({ inputId, beforeId }))
|
|
410
|
+
}
|
|
411
|
+
/>
|
|
412
|
+
{state.receipt?.outcome === 'rejected' ? (
|
|
413
|
+
<p className="error" role="alert">
|
|
414
|
+
Command rejected: {state.receipt.reason}. Refresh the input or turn
|
|
415
|
+
you want to change.
|
|
416
|
+
</p>
|
|
417
|
+
) : null}
|
|
418
|
+
|
|
338
419
|
<form className="agent-form" onSubmit={(event) => void send(event)}>
|
|
339
420
|
<label>
|
|
340
421
|
Message
|
|
341
422
|
<textarea
|
|
342
423
|
ref={promptRef}
|
|
343
|
-
autoFocus
|
|
344
424
|
rows={3}
|
|
345
425
|
value={prompt}
|
|
346
426
|
onChange={(event) => setPrompt(event.target.value)}
|
|
@@ -355,11 +435,43 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
355
435
|
<button
|
|
356
436
|
type="button"
|
|
357
437
|
className="danger"
|
|
358
|
-
disabled={!
|
|
438
|
+
disabled={!state.active}
|
|
359
439
|
onClick={() => void stop()}
|
|
360
440
|
>
|
|
361
441
|
Stop
|
|
362
442
|
</button>
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
disabled={!canSend || !prompt.trim()}
|
|
446
|
+
onClick={() => void send(undefined, true)}
|
|
447
|
+
>
|
|
448
|
+
Steer
|
|
449
|
+
</button>
|
|
450
|
+
{state.inbox.paused ? (
|
|
451
|
+
<button
|
|
452
|
+
type="button"
|
|
453
|
+
onClick={() => void command(inputs.resume())}
|
|
454
|
+
>
|
|
455
|
+
Resume
|
|
456
|
+
</button>
|
|
457
|
+
) : (
|
|
458
|
+
<>
|
|
459
|
+
<button
|
|
460
|
+
type="button"
|
|
461
|
+
onClick={() =>
|
|
462
|
+
void command(inputs.pause({ when: 'after-turn' }))
|
|
463
|
+
}
|
|
464
|
+
>
|
|
465
|
+
Pause after turn
|
|
466
|
+
</button>
|
|
467
|
+
<button
|
|
468
|
+
type="button"
|
|
469
|
+
onClick={() => void command(inputs.pause({ when: 'now' }))}
|
|
470
|
+
>
|
|
471
|
+
Pause now
|
|
472
|
+
</button>
|
|
473
|
+
</>
|
|
474
|
+
)}
|
|
363
475
|
</div>
|
|
364
476
|
</form>
|
|
365
477
|
</section>
|