experimental-a2 0.13.0 → 0.14.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/{actor-DJi3RsNu.d.ts → actor-BfQSE0KC.d.ts} +4 -4
  3. package/dist/{actor-DJi3RsNu.d.ts.map → actor-BfQSE0KC.d.ts.map} +1 -1
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-client.js +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +2 -2
  8. package/dist/{actor-shared-DI7J5upy.js → actor-shared-B5tJfzt-.js} +2 -2
  9. package/dist/{actor-shared-DI7J5upy.js.map → actor-shared-B5tJfzt-.js.map} +1 -1
  10. package/dist/actor.d.ts +1 -1
  11. package/dist/actor.js +3 -3
  12. package/dist/ai-Cai-lCbj.d.ts +580 -0
  13. package/dist/ai-Cai-lCbj.d.ts.map +1 -0
  14. package/dist/ai-control-CcD4hh3y.js +119 -0
  15. package/dist/ai-control-CcD4hh3y.js.map +1 -0
  16. package/dist/ai-server.d.ts +6 -6
  17. package/dist/ai-server.d.ts.map +1 -1
  18. package/dist/ai-server.js +1053 -513
  19. package/dist/ai-server.js.map +1 -1
  20. package/dist/ai.d.ts +2 -365
  21. package/dist/ai.js +801 -80
  22. package/dist/ai.js.map +1 -1
  23. package/dist/{client-P_NNNRM-.d.ts → client-BAEABRZB.d.ts} +2 -2
  24. package/dist/{client-P_NNNRM-.d.ts.map → client-BAEABRZB.d.ts.map} +1 -1
  25. package/dist/{client-Bf6uSEAk.js → client-BYzHjkwU.js} +21 -6
  26. package/dist/client-BYzHjkwU.js.map +1 -0
  27. package/dist/client.d.ts +1 -1
  28. package/dist/client.js +1 -1
  29. package/dist/{contract-48bUMgcL.js → contract-CKRg_E4q.js} +3 -26
  30. package/dist/contract-CKRg_E4q.js.map +1 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +1 -1
  33. package/dist/react.d.ts +2 -2
  34. package/dist/react.js +1 -1
  35. package/dist/{reducer-DJKWm3cp.d.ts → reducer-BcS9VDKC.d.ts} +4 -1
  36. package/dist/{reducer-DJKWm3cp.d.ts.map → reducer-BcS9VDKC.d.ts.map} +1 -1
  37. package/dist/reducer-DEMjEY_O.js +29 -0
  38. package/dist/reducer-DEMjEY_O.js.map +1 -0
  39. package/dist/scheduler-qstash.d.ts +2 -2
  40. package/dist/scheduler-qstash.js +1 -1
  41. package/dist/scheduler-vercel.d.ts +2 -2
  42. package/dist/scheduler-vercel.js +1 -1
  43. package/dist/{server-DjZZa1wr.d.ts → server-Bp5Nd1pF.d.ts} +3 -3
  44. package/dist/{server-DjZZa1wr.d.ts.map → server-Bp5Nd1pF.d.ts.map} +1 -1
  45. package/dist/{server-BeNADlCI.js → server-CjJSGcF7.js} +4 -3
  46. package/dist/server-CjJSGcF7.js.map +1 -0
  47. package/dist/server.d.ts +3 -3
  48. package/dist/server.js +1 -1
  49. package/dist/{store-DtDOWLSn.d.ts → store-D_yhNdPz.d.ts} +7 -2
  50. package/dist/{store-DtDOWLSn.d.ts.map → store-D_yhNdPz.d.ts.map} +1 -1
  51. package/dist/store-N8PXxDAS.js.map +1 -1
  52. package/dist/store-memory.d.ts +1 -1
  53. package/dist/store-postgres.d.ts +1 -1
  54. package/dist/store-postgres.js +19 -0
  55. package/dist/store-postgres.js.map +1 -1
  56. package/dist/store-redis-http.d.ts +1 -1
  57. package/dist/store-redis-http.js +1 -1
  58. package/dist/{store-redis-notify-BUCyXOn0.js → store-redis-notify-D2EI6gwX.js} +27 -2
  59. package/dist/store-redis-notify-D2EI6gwX.js.map +1 -0
  60. package/dist/store-redis.d.ts +1 -1
  61. package/dist/store-redis.js +1 -1
  62. package/dist/store-sqlite.d.ts +1 -1
  63. package/docs/guides/06-ai-agents.mdx +287 -65
  64. package/docs/reference/01-api.mdx +122 -27
  65. package/examples/playground/app/agent/[agentId]/agent-client.tsx +118 -21
  66. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
  67. package/examples/playground/app/agent/compaction-settings.test.ts +22 -6
  68. package/examples/playground/app/agent/model.ts +11 -2
  69. package/examples/playground/app/agent/server.ts +8 -2
  70. package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
  71. package/examples/playground/app/chat/model.ts +2 -2
  72. package/examples/playground/app/chat/server.ts +24 -17
  73. package/examples/playground/app/globals.css +179 -0
  74. package/examples/playground/package.json +1 -1
  75. package/package.json +1 -1
  76. package/src/ai-client-state.ts +185 -0
  77. package/src/ai-control-server.ts +829 -0
  78. package/src/ai-control-state.ts +152 -0
  79. package/src/ai-control.ts +139 -0
  80. package/src/ai-coordinator.ts +99 -32
  81. package/src/ai-progress-batches.ts +68 -0
  82. package/src/ai-projector.ts +76 -15
  83. package/src/ai-sdk-step.ts +0 -1
  84. package/src/ai-server.ts +429 -619
  85. package/src/ai.ts +553 -108
  86. package/src/client.ts +31 -9
  87. package/src/licenses/Apache-2.0.txt +55 -0
  88. package/src/parse-partial-json.ts +441 -0
  89. package/src/reducer.ts +6 -0
  90. package/src/server.ts +8 -4
  91. package/src/store-postgres.ts +27 -0
  92. package/src/store-redis-core.ts +53 -1
  93. package/src/store-redis-notify.ts +1 -0
  94. package/src/store.ts +6 -0
  95. package/dist/ai.d.ts.map +0 -1
  96. package/dist/client-Bf6uSEAk.js.map +0 -1
  97. package/dist/contract-48bUMgcL.js.map +0 -1
  98. package/dist/server-BeNADlCI.js.map +0 -1
  99. package/dist/store-redis-notify-BUCyXOn0.js.map +0 -1
@@ -0,0 +1,289 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState, type ReactNode } from 'react'
4
+ import type { UIMessage } from 'ai'
5
+ import type { InboxItem } from 'experimental-a2/ai'
6
+
7
+ type QueueProps = {
8
+ items: InboxItem[]
9
+ paused: boolean
10
+ pendingEdits: readonly string[]
11
+ edit(message: UIMessage, revision: number): Promise<void>
12
+ move(inputId: string, beforeId: string | null): void
13
+ remove(inputId: string): Promise<void>
14
+ sendNow(inputId: string): Promise<void>
15
+ }
16
+
17
+ const queueIconPaths = {
18
+ edit: 'm15 5 4 4M4 20l4-1L20 7a2.8 2.8 0 0 0-4-4L4 15z',
19
+ up: 'm6 14 6-6 6 6',
20
+ down: 'm6 10 6 6 6-6',
21
+ remove: 'm6 6 12 12M6 18 18 6',
22
+ send: 'M4 12h15m-6-6 6 6-6 6',
23
+ }
24
+
25
+ function QueueIcon({ name }: { name: keyof typeof queueIconPaths }): ReactNode {
26
+ return (
27
+ <svg
28
+ width="14"
29
+ height="14"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ strokeWidth="1.7"
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ aria-hidden="true"
37
+ >
38
+ <path d={queueIconPaths[name]} />
39
+ </svg>
40
+ )
41
+ }
42
+
43
+ function QueuedInput({
44
+ item,
45
+ position,
46
+ count,
47
+ edit,
48
+ moveUp,
49
+ moveDown,
50
+ remove,
51
+ sendNow,
52
+ paused,
53
+ pendingEdit,
54
+ }: {
55
+ item: InboxItem
56
+ position: number
57
+ count: number
58
+ edit: QueueProps['edit']
59
+ moveUp(): void
60
+ moveDown(): void
61
+ remove: QueueProps['remove']
62
+ sendNow: QueueProps['sendNow']
63
+ paused: boolean
64
+ pendingEdit: boolean
65
+ }): ReactNode {
66
+ const [draft, setDraft] = useState<{ text: string; revision: number } | null>(
67
+ null,
68
+ )
69
+ const [pending, setPending] = useState(false)
70
+ const [error, setError] = useState<string | null>(null)
71
+ const editButton = useRef<HTMLButtonElement>(null)
72
+ const restoreFocus = useRef(false)
73
+ useEffect(() => {
74
+ if (draft === null && !pending && !pendingEdit && restoreFocus.current) {
75
+ if (document.activeElement === document.body) editButton.current?.focus()
76
+ restoreFocus.current = false
77
+ }
78
+ }, [draft, pending, pendingEdit])
79
+ const text = item.message.parts
80
+ .filter((part) => part.type === 'text')
81
+ .map((part) => part.text)
82
+ .join('\n')
83
+ const cancel = (): void => {
84
+ restoreFocus.current = true
85
+ setDraft(null)
86
+ setError(null)
87
+ }
88
+ const perform = async (action: () => Promise<void>): Promise<void> => {
89
+ if (pending) return
90
+ setPending(true)
91
+ setError(null)
92
+ try {
93
+ await action()
94
+ } catch (failure) {
95
+ setError(failure instanceof Error ? failure.message : String(failure))
96
+ } finally {
97
+ setPending(false)
98
+ }
99
+ }
100
+ const save = async (): Promise<void> => {
101
+ if (!draft || pendingEdit || !draft.text.trim()) return
102
+ await perform(async () => {
103
+ await edit(
104
+ { ...item.message, parts: [{ type: 'text', text: draft.text }] },
105
+ draft.revision,
106
+ )
107
+ cancel()
108
+ })
109
+ }
110
+ return (
111
+ <li className={`agent-queue-item${draft ? ' editing' : ''}`}>
112
+ <div className="agent-queue-row">
113
+ <span
114
+ className="agent-queue-number"
115
+ aria-label={`Queue position ${position + 1}`}
116
+ >
117
+ {position + 1}
118
+ </span>
119
+ <p className="agent-queue-text" title={text}>
120
+ {text || 'Message'}
121
+ </p>
122
+ <div className="agent-queue-actions">
123
+ <button
124
+ type="button"
125
+ className="agent-queue-send"
126
+ title={
127
+ paused
128
+ ? 'Send next · waits for Resume while paused'
129
+ : 'Send after current step'
130
+ }
131
+ aria-label={`Send queued message ${position + 1} after current step`}
132
+ disabled={
133
+ pending ||
134
+ pendingEdit ||
135
+ draft !== null ||
136
+ item.message.role !== 'user'
137
+ }
138
+ onClick={() => void perform(() => sendNow(item.id))}
139
+ >
140
+ <QueueIcon name="send" />
141
+ </button>
142
+ <button
143
+ ref={editButton}
144
+ type="button"
145
+ title={
146
+ pendingEdit ? 'Waiting for edit confirmation' : 'Edit message'
147
+ }
148
+ aria-label={`Edit queued message ${position + 1}`}
149
+ aria-expanded={draft !== null}
150
+ disabled={pending || pendingEdit}
151
+ onClick={() =>
152
+ draft ? cancel() : setDraft({ text, revision: item.revision })
153
+ }
154
+ >
155
+ <QueueIcon name="edit" />
156
+ </button>
157
+ <button
158
+ type="button"
159
+ title="Move up"
160
+ aria-label={`Move queued message ${position + 1} up`}
161
+ disabled={position === 0 || pending}
162
+ onClick={moveUp}
163
+ >
164
+ <QueueIcon name="up" />
165
+ </button>
166
+ <button
167
+ type="button"
168
+ title="Move down"
169
+ aria-label={`Move queued message ${position + 1} down`}
170
+ disabled={position === count - 1 || pending}
171
+ onClick={moveDown}
172
+ >
173
+ <QueueIcon name="down" />
174
+ </button>
175
+ <button
176
+ type="button"
177
+ className="agent-queue-remove"
178
+ title="Remove from queue"
179
+ aria-label={`Remove queued message ${position + 1}`}
180
+ disabled={pending || draft !== null}
181
+ onClick={() => void perform(() => remove(item.id))}
182
+ >
183
+ <QueueIcon name="remove" />
184
+ </button>
185
+ </div>
186
+ </div>
187
+ {error ? (
188
+ <p className="error agent-queue-error" role="alert">
189
+ {error}
190
+ </p>
191
+ ) : null}
192
+ {draft ? (
193
+ <form
194
+ className="agent-queue-editor"
195
+ onSubmit={(event) => {
196
+ event.preventDefault()
197
+ void save()
198
+ }}
199
+ >
200
+ <textarea
201
+ autoFocus
202
+ aria-label={`Edit text for queued message ${position + 1}`}
203
+ rows={2}
204
+ value={draft.text}
205
+ disabled={pending}
206
+ onChange={(event) =>
207
+ setDraft({ ...draft, text: event.target.value })
208
+ }
209
+ onKeyDown={(event) => {
210
+ if (event.nativeEvent.isComposing) return
211
+ if (event.key === 'Escape' && !pending) {
212
+ event.preventDefault()
213
+ cancel()
214
+ }
215
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
216
+ event.preventDefault()
217
+ void save()
218
+ }
219
+ }}
220
+ />
221
+ <div className="agent-queue-editor-actions">
222
+ <span>⌘ / Ctrl + Enter to save</span>
223
+ <button
224
+ type="button"
225
+ className="secondary"
226
+ disabled={pending}
227
+ onClick={cancel}
228
+ >
229
+ Cancel
230
+ </button>
231
+ <button
232
+ type="submit"
233
+ disabled={pending || pendingEdit || !draft.text.trim()}
234
+ >
235
+ {pending ? 'Saving…' : 'Save'}
236
+ </button>
237
+ </div>
238
+ </form>
239
+ ) : null}
240
+ </li>
241
+ )
242
+ }
243
+
244
+ export function AgentQueue({
245
+ items,
246
+ paused,
247
+ pendingEdits,
248
+ edit,
249
+ move,
250
+ remove,
251
+ sendNow,
252
+ }: QueueProps): ReactNode {
253
+ if (items.length === 0) return null
254
+ return (
255
+ <section className="agent-queue" aria-label="Queued messages">
256
+ <div className="agent-queue-header">
257
+ <h3>
258
+ Queued <span>{items.length}</span>
259
+ </h3>
260
+ <span className={`agent-queue-status${paused ? ' paused' : ''}`}>
261
+ {paused ? 'Paused' : 'Pending'}
262
+ </span>
263
+ </div>
264
+ <ol className="agent-queue-list">
265
+ {items.map((item, position) => (
266
+ <QueuedInput
267
+ key={item.id}
268
+ item={item}
269
+ position={position}
270
+ count={items.length}
271
+ edit={edit}
272
+ remove={remove}
273
+ sendNow={sendNow}
274
+ paused={paused}
275
+ pendingEdit={pendingEdits.includes(item.id)}
276
+ moveUp={() => {
277
+ const previous = items[position - 1]
278
+ if (previous) move(item.id, previous.id)
279
+ }}
280
+ moveDown={() => {
281
+ if (position < items.length - 1)
282
+ move(item.id, items[position + 2]?.id ?? null)
283
+ }}
284
+ />
285
+ ))}
286
+ </ol>
287
+ </section>
288
+ )
289
+ }
@@ -1,7 +1,12 @@
1
1
  import { expect, it } from 'vitest'
2
2
  import { createAgentServer } from 'experimental-a2/ai/server'
3
3
  import { memory } from 'experimental-a2/store-memory'
4
- import { compactionOptions, demoAgent, latestCompactionSettings } from './model'
4
+ import {
5
+ compactionOptions,
6
+ compactionSettings,
7
+ demoAgent,
8
+ latestCompactionSettings,
9
+ } from './model'
5
10
  import { updateCompactionSettings } from './compaction-settings'
6
11
 
7
12
  const request = (
@@ -47,7 +52,8 @@ it('saves session settings durably and idempotently without starting a turn', as
47
52
  agent: demoAgent,
48
53
  store: memory(),
49
54
  model: 'test/model',
50
- compaction: ({ history }) => compactionOptions(history),
55
+ compaction: async ({ session }) =>
56
+ compactionOptions((await session.state(compactionSettings)).state),
51
57
  generate: () => {
52
58
  generations += 1
53
59
  return new ReadableStream({
@@ -76,8 +82,14 @@ it('saves session settings durably and idempotently without starting a turn', as
76
82
  mode: 'manual',
77
83
  thresholdTokens: 1,
78
84
  })
79
- expect(compactionOptions(history)).toEqual({ thresholdTokens: 1 })
80
- expect(compactionOptions(await server.session('other').history())).toEqual({})
85
+ expect(
86
+ compactionOptions((await session.state(compactionSettings)).state),
87
+ ).toEqual({ thresholdTokens: 1 })
88
+ expect(
89
+ compactionOptions(
90
+ (await server.session('other').state(compactionSettings)).state,
91
+ ),
92
+ ).toEqual({})
81
93
  expect(generations).toBe(0)
82
94
 
83
95
  await updateCompactionSettings({
@@ -85,13 +97,17 @@ it('saves session settings durably and idempotently without starting a turn', as
85
97
  sessionId: session.id,
86
98
  append: (event) => session.append(event),
87
99
  })
88
- expect(compactionOptions(await session.history())).toBe(false)
100
+ expect(
101
+ compactionOptions((await session.state(compactionSettings)).state),
102
+ ).toBe(false)
89
103
  await updateCompactionSettings({
90
104
  request: request({ mode: 'automatic' }),
91
105
  sessionId: session.id,
92
106
  append: (event) => session.append(event),
93
107
  })
94
- expect(compactionOptions(await session.history())).toEqual({})
108
+ expect(
109
+ compactionOptions((await session.state(compactionSettings)).state),
110
+ ).toEqual({})
95
111
  })
96
112
 
97
113
  it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, '1', null])(
@@ -37,10 +37,19 @@ export function latestCompactionSettings(history: readonly AgentEvent[]): {
37
37
  : { settings: automatic, id: 'default' }
38
38
  }
39
39
 
40
+ export const compactionSettings = demoAgent.contract
41
+ .reducer({
42
+ name: 'agent.compaction-settings.v1',
43
+ initialState: automatic,
44
+ stateSchema: compactionSettingsSchema,
45
+ })
46
+ .fold((state, event): CompactionSettings =>
47
+ event.type === 'compaction.settings.updated' ? event.payload : state,
48
+ )
49
+
40
50
  export function compactionOptions(
41
- history: readonly AgentEvent[],
51
+ settings: CompactionSettings,
42
52
  ): AutomaticCompaction | false {
43
- const { settings } = latestCompactionSettings(history)
44
53
  return settings.mode === 'disabled'
45
54
  ? false
46
55
  : settings.mode === 'manual'
@@ -8,7 +8,12 @@ import { createAgentServer } from 'experimental-a2/ai/server'
8
8
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
9
9
  import { z } from 'zod'
10
10
  import { store } from '@/lib/store'
11
- import { agentModel, compactionOptions, demoAgent } from './model'
11
+ import {
12
+ agentModel,
13
+ compactionOptions,
14
+ compactionSettings,
15
+ demoAgent,
16
+ } from './model'
12
17
 
13
18
  const maxReminderDelaySeconds = 6 * 24 * 60 * 60
14
19
  const bashTimeoutMs = 120_000
@@ -132,7 +137,8 @@ export const agentServer = createAgentServer({
132
137
  ...(agentScheduler ? { scheduler: agentScheduler } : {}),
133
138
  agent: demoAgent,
134
139
  model: agentModel,
135
- compaction: ({ history }) => compactionOptions(history),
140
+ compaction: async ({ session }) =>
141
+ compactionOptions((await session.state(compactionSettings)).state),
136
142
  tools,
137
143
  instructions: `You are an incident-response agent for the playground.
138
144
  When responding to an incident, inspect checkout-api before taking action. Its
@@ -539,22 +539,12 @@ export function ChatClient({ chatId }: { chatId: string }): ReactNode {
539
539
  }
540
540
 
541
541
  const stopAgent = async (): Promise<void> => {
542
- if (!activeResponseId) return
543
- const target = state.ai.activeGeneration
544
- ? { generationId: state.ai.activeGeneration.generationId }
545
- : runningTool
546
- ? { generationId: runningTool.generationId }
547
- : state.ai.activeRequestId
548
- ? { requestId: state.ai.activeRequestId }
549
- : null
550
- if (target === null) return
542
+ if (!state.ai.active) return
551
543
  setError(null)
552
544
  try {
553
545
  await push(
554
- ...inputs.interrupt({
555
- messageId: activeResponseId,
556
- ...target,
557
- reason: 'a chat participant stopped the agent',
546
+ ...inputs.stop({
547
+ turnId: state.ai.active.turnId,
558
548
  lastSeenIndex: index,
559
549
  }),
560
550
  )
@@ -164,7 +164,7 @@ export const chatAgent = agent<ChatAgentMessage, typeof chatEvents>({
164
164
  name: 'chat',
165
165
  events: chatEvents,
166
166
  messageSchema: chatMessageSchema,
167
- reducerName: 'a2.playground.chat.ai.v1',
167
+ reducerName: 'a2.playground.chat.ai.v3',
168
168
  })
169
169
 
170
170
  export const chat = chatAgent.contract
@@ -208,7 +208,7 @@ const initialChatState: ChatState = {
208
208
 
209
209
  export const chatReducer = chat
210
210
  .reducer({
211
- name: 'a2.playground.chat.v1',
211
+ name: 'a2.playground.chat.v3',
212
212
  initialState: initialChatState,
213
213
  })
214
214
  .fold((state, event) => {
@@ -21,16 +21,13 @@ const tools = {
21
21
  inputSchema: chatPostInputSchema,
22
22
  execute: async ({ message }, { toolCallId }) => {
23
23
  const context = handlerContext(chatAgent)
24
- if (context.event.type !== 'ai.tool.called') {
25
- throw new Error('The chat post tool must execute automatically')
26
- }
27
24
  context.signal.throwIfAborted()
28
25
  await context.session.append('post', {
29
26
  type: 'agent.message.posted',
30
27
  payload: {
31
28
  message,
32
- generationId: context.event.payload.generationId,
33
- responseMessageId: context.event.payload.messageId,
29
+ generationId: context.event.payload.call.generationId,
30
+ responseMessageId: context.event.payload.call.messageId,
34
31
  toolCallId,
35
32
  },
36
33
  })
@@ -118,23 +115,33 @@ export const authorizeChatOperation = (
118
115
  return false
119
116
  }
120
117
  return !(
121
- event.type === 'ai.approval.responded' ||
122
- event.type === 'ai.input.responded' ||
123
- event.type === 'ai.message.interrupted' ||
124
- event.type === 'ai.retry.requested'
118
+ event.type === 'ai.control.requested' &&
119
+ typeof event.payload === 'object' &&
120
+ event.payload !== null &&
121
+ 'action' in event.payload &&
122
+ typeof event.payload.action === 'string' &&
123
+ [
124
+ 'approval',
125
+ 'input',
126
+ 'interrupt',
127
+ 'stop',
128
+ 'retry',
129
+ 'pause',
130
+ 'resume',
131
+ ].includes(event.payload.action)
125
132
  )
126
133
  })
127
134
  return rejected === undefined
128
135
  }
129
136
 
130
- const toolCalledHandler = aiHandlers['ai.tool.called']
131
- if (toolCalledHandler === undefined) {
132
- throw new Error('A2 AI handlers must include ai.tool.called')
137
+ const toolExecutionHandler = aiHandlers['ai.tool.execution.requested']
138
+ if (toolExecutionHandler === undefined) {
139
+ throw new Error('A2 AI handlers must include ai.tool.execution.requested')
133
140
  }
134
- const serializedToolCalledHandler =
135
- typeof toolCalledHandler === 'function'
136
- ? { handler: toolCalledHandler, lane: 'a2.chat.agent-tools' }
137
- : { ...toolCalledHandler, lane: 'a2.chat.agent-tools' }
141
+ const serializedToolExecutionHandler =
142
+ typeof toolExecutionHandler === 'function'
143
+ ? { handler: toolExecutionHandler, lane: 'a2.chat.agent-tools' }
144
+ : { ...toolExecutionHandler, lane: 'a2.chat.agent-tools' }
138
145
 
139
146
  export const chatScheduler =
140
147
  process.env.VERCEL || process.env.A2_QUEUES
@@ -147,7 +154,7 @@ export const chatServer = createServer({
147
154
  contract: chatAgent.contract,
148
155
  handlers: {
149
156
  ...aiHandlers,
150
- 'ai.tool.called': serializedToolCalledHandler,
157
+ 'ai.tool.execution.requested': serializedToolExecutionHandler,
151
158
  'message.sent': {
152
159
  lane: 'a2.chat.ingress',
153
160
  handler: async (context) => {
@@ -1799,6 +1799,185 @@ button.danger {
1799
1799
  flex: 0 0 auto;
1800
1800
  }
1801
1801
 
1802
+ .agent-queue {
1803
+ margin: 0 1rem 0.85rem;
1804
+ border: 1px solid var(--line);
1805
+ border-radius: 9px;
1806
+ background: var(--bg);
1807
+ overflow: hidden;
1808
+ }
1809
+
1810
+ .agent-queue-header {
1811
+ display: flex;
1812
+ align-items: center;
1813
+ justify-content: space-between;
1814
+ gap: 0.75rem;
1815
+ padding: 0.55rem 0.8rem;
1816
+ border-bottom: 1px solid var(--line);
1817
+ }
1818
+
1819
+ .agent-queue-header h3 {
1820
+ display: flex;
1821
+ align-items: center;
1822
+ gap: 0.45rem;
1823
+ margin: 0;
1824
+ color: var(--muted);
1825
+ font-size: 0.75rem;
1826
+ font-weight: 600;
1827
+ }
1828
+
1829
+ .agent-queue-header h3 span {
1830
+ min-width: 1.2rem;
1831
+ padding: 0 0.3rem;
1832
+ border-radius: 4px;
1833
+ background: var(--line);
1834
+ text-align: center;
1835
+ font-size: 0.68rem;
1836
+ font-variant-numeric: tabular-nums;
1837
+ }
1838
+
1839
+ .agent-queue-status {
1840
+ color: var(--muted);
1841
+ font-size: 0.7rem;
1842
+ }
1843
+
1844
+ .agent-queue-status.paused {
1845
+ color: #885b12;
1846
+ }
1847
+
1848
+ .agent-queue-list {
1849
+ max-height: 17rem;
1850
+ overflow-y: auto;
1851
+ overscroll-behavior: contain;
1852
+ list-style: none;
1853
+ margin: 0;
1854
+ padding: 0.15rem 0;
1855
+ }
1856
+
1857
+ .agent-queue-item + .agent-queue-item {
1858
+ border-top: 1px solid var(--line);
1859
+ }
1860
+
1861
+ .agent-queue-row {
1862
+ display: grid;
1863
+ grid-template-columns: 1.25rem minmax(0, 1fr) auto;
1864
+ align-items: center;
1865
+ gap: 0.5rem;
1866
+ min-height: 2.7rem;
1867
+ padding: 0.25rem 0.55rem 0.25rem 0.8rem;
1868
+ }
1869
+
1870
+ .agent-queue-number {
1871
+ color: var(--muted);
1872
+ font-size: 0.7rem;
1873
+ font-variant-numeric: tabular-nums;
1874
+ }
1875
+
1876
+ .agent-queue-text {
1877
+ margin: 0;
1878
+ overflow: hidden;
1879
+ white-space: nowrap;
1880
+ text-overflow: ellipsis;
1881
+ font-size: 0.83rem;
1882
+ }
1883
+
1884
+ .agent-queue-actions {
1885
+ display: flex;
1886
+ gap: 0.1rem;
1887
+ }
1888
+
1889
+ .agent-queue-actions button {
1890
+ display: grid;
1891
+ place-items: center;
1892
+ width: 1.9rem;
1893
+ height: 1.9rem;
1894
+ padding: 0;
1895
+ border: 0;
1896
+ border-radius: 5px;
1897
+ background: transparent;
1898
+ color: var(--muted);
1899
+ }
1900
+
1901
+ .agent-queue-actions button:hover:not(:disabled),
1902
+ .agent-queue-actions button[aria-expanded='true'] {
1903
+ background: var(--line);
1904
+ color: var(--ink);
1905
+ filter: none;
1906
+ }
1907
+
1908
+ .agent-queue-actions button:disabled {
1909
+ opacity: 0.22;
1910
+ }
1911
+
1912
+ .agent-queue-actions .agent-queue-send {
1913
+ color: var(--accent);
1914
+ }
1915
+
1916
+ .agent-queue-actions .agent-queue-remove:hover:not(:disabled) {
1917
+ background: #f8e8e5;
1918
+ color: var(--danger);
1919
+ }
1920
+
1921
+ .agent-queue-error {
1922
+ margin: 0;
1923
+ padding: 0 0.75rem 0.65rem 2.55rem;
1924
+ font-size: 0.75rem;
1925
+ }
1926
+
1927
+ .agent-queue-item.editing {
1928
+ background: var(--surface);
1929
+ }
1930
+
1931
+ .agent-queue-editor {
1932
+ gap: 0.45rem;
1933
+ margin: 0;
1934
+ padding: 0 0.75rem 0.65rem 2.55rem;
1935
+ border: 0;
1936
+ border-radius: 0;
1937
+ }
1938
+
1939
+ .agent-queue-editor textarea {
1940
+ width: 100%;
1941
+ min-width: 0;
1942
+ resize: vertical;
1943
+ font-size: 0.83rem;
1944
+ line-height: 1.5;
1945
+ }
1946
+
1947
+ .agent-queue-editor-actions {
1948
+ display: flex;
1949
+ align-items: center;
1950
+ justify-content: flex-end;
1951
+ gap: 0.35rem;
1952
+ }
1953
+
1954
+ .agent-queue-editor-actions span {
1955
+ margin-right: auto;
1956
+ color: var(--muted);
1957
+ font-size: 0.65rem;
1958
+ }
1959
+
1960
+ .agent-queue-editor-actions button {
1961
+ padding: 0.3rem 0.6rem;
1962
+ font-size: 0.72rem;
1963
+ }
1964
+
1965
+ @media (pointer: coarse) {
1966
+ .agent-queue-actions button {
1967
+ width: 2.25rem;
1968
+ height: 2.5rem;
1969
+ }
1970
+ }
1971
+
1972
+ @media (max-width: 420px) {
1973
+ .agent-queue-editor {
1974
+ padding-left: 0.75rem;
1975
+ }
1976
+ .agent-queue-editor-actions span {
1977
+ display: none;
1978
+ }
1979
+ }
1980
+
1802
1981
  .agent-form {
1803
1982
  margin: 0;
1804
1983
  padding: 1rem;