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.
- package/CHANGELOG.md +43 -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 +6 -6
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +1053 -513
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -365
- package/dist/ai.js +801 -80
- 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 +287 -65
- package/docs/reference/01-api.mdx +122 -27
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +118 -21
- package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
- package/examples/playground/app/agent/compaction-settings.test.ts +22 -6
- package/examples/playground/app/agent/model.ts +11 -2
- package/examples/playground/app/agent/server.ts +8 -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 +179 -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-progress-batches.ts +68 -0
- package/src/ai-projector.ts +76 -15
- package/src/ai-sdk-step.ts +0 -1
- package/src/ai-server.ts +429 -619
- package/src/ai.ts +553 -108
- 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
|
@@ -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 {
|
|
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: ({
|
|
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(
|
|
80
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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: ({
|
|
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 (!
|
|
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.
|
|
555
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
122
|
-
event.
|
|
123
|
-
event.
|
|
124
|
-
event.
|
|
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
|
|
131
|
-
if (
|
|
132
|
-
throw new Error('A2 AI handlers must include ai.tool.
|
|
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
|
|
135
|
-
typeof
|
|
136
|
-
? { handler:
|
|
137
|
-
: { ...
|
|
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.
|
|
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;
|