experimental-a2 0.11.0 → 0.13.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 +19 -0
- package/dist/{actor-D_54lz_1.d.ts → actor-DJi3RsNu.d.ts} +2 -2
- package/dist/{actor-D_54lz_1.d.ts.map → actor-DJi3RsNu.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +1 -1
- package/dist/ai-server.d.ts +12 -4
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +477 -143
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +33 -2
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +39 -7
- package/dist/ai.js.map +1 -1
- package/dist/scheduler-qstash.d.ts +1 -1
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +1 -1
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-Dkz2a84E.js → server-BeNADlCI.js} +8 -5
- package/dist/server-BeNADlCI.js.map +1 -0
- package/dist/{server-DwPrMqHB.d.ts → server-DjZZa1wr.d.ts} +8 -2
- package/dist/{server-DwPrMqHB.d.ts.map → server-DjZZa1wr.d.ts.map} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/docs/guides/06-ai-agents.mdx +100 -18
- package/docs/reference/01-api.mdx +105 -5
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +31 -16
- 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 +128 -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 +47 -1
- package/examples/playground/app/agent/server.ts +3 -2
- package/examples/playground/app/globals.css +154 -0
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/ai-model-metadata.ts +108 -0
- package/src/ai-sdk-step.ts +5 -1
- package/src/ai-server.ts +712 -203
- package/src/ai.ts +99 -4
- package/src/server.ts +14 -3
- package/dist/server-Dkz2a84E.js.map +0 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import type { UIMessageChunk } from 'ai'
|
|
2
|
+
import { reduceAIState } from 'experimental-a2/ai'
|
|
3
|
+
import { expect, it } from 'vitest'
|
|
4
|
+
import { compactionTimeline, type TimelineEntry } from './compaction-timeline'
|
|
5
|
+
import { demoAgent, type AgentEvent } from './model'
|
|
6
|
+
|
|
7
|
+
type EventInput = {
|
|
8
|
+
[K in AgentEvent['type']]: Pick<
|
|
9
|
+
Extract<AgentEvent, { type: K }>,
|
|
10
|
+
'type' | 'payload'
|
|
11
|
+
> & { id?: string }
|
|
12
|
+
}[AgentEvent['type']]
|
|
13
|
+
type Generation = Extract<
|
|
14
|
+
AgentEvent,
|
|
15
|
+
{ type: 'ai.generation.started' }
|
|
16
|
+
>['payload']
|
|
17
|
+
|
|
18
|
+
function transcript() {
|
|
19
|
+
const events: AgentEvent[] = []
|
|
20
|
+
const append = (input: EventInput): void => {
|
|
21
|
+
events.push({
|
|
22
|
+
...input,
|
|
23
|
+
id: input.id ?? `event:${events.length + 1}`,
|
|
24
|
+
index: events.length + 1,
|
|
25
|
+
sessionId: 'timeline',
|
|
26
|
+
createdAt: new Date('2026-09-14T12:00:00Z'),
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
const user = (id: string): void =>
|
|
30
|
+
append({
|
|
31
|
+
type: 'ai.message.created',
|
|
32
|
+
payload: {
|
|
33
|
+
message: { id, role: 'user', parts: [{ type: 'text', text: id }] },
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
const start = ({
|
|
37
|
+
requestId = 'request',
|
|
38
|
+
reason = 'message',
|
|
39
|
+
attempt = 1,
|
|
40
|
+
}: {
|
|
41
|
+
requestId?: string
|
|
42
|
+
reason?: 'message' | 'tool' | 'retry'
|
|
43
|
+
attempt?: number
|
|
44
|
+
} = {}): Generation => {
|
|
45
|
+
const generation = {
|
|
46
|
+
requestId,
|
|
47
|
+
messageId: reason === 'tool' ? 'assistant' : 'user',
|
|
48
|
+
responseMessageId: 'assistant',
|
|
49
|
+
generationId: `${requestId}:generation:${attempt}`,
|
|
50
|
+
attempt,
|
|
51
|
+
model: 'test/model',
|
|
52
|
+
}
|
|
53
|
+
if (attempt === 1)
|
|
54
|
+
append({
|
|
55
|
+
id: requestId,
|
|
56
|
+
type: 'ai.generation.requested',
|
|
57
|
+
payload: {
|
|
58
|
+
messageId: generation.messageId,
|
|
59
|
+
responseMessageId: 'assistant',
|
|
60
|
+
reason,
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
append({ type: 'ai.generation.started', payload: generation })
|
|
64
|
+
return generation
|
|
65
|
+
}
|
|
66
|
+
const progress = (generation: Generation, chunks: UIMessageChunk[]): void =>
|
|
67
|
+
append({
|
|
68
|
+
type: 'ai.generation.progress',
|
|
69
|
+
payload: { ...generation, sequence: events.length, chunks },
|
|
70
|
+
})
|
|
71
|
+
const text = (generation: Generation, value: string, step = true): void =>
|
|
72
|
+
progress(generation, [
|
|
73
|
+
{ type: 'start', messageId: generation.responseMessageId },
|
|
74
|
+
...(step ? [{ type: 'start-step' as const }] : []),
|
|
75
|
+
{ type: 'text-start', id: generation.generationId },
|
|
76
|
+
{ type: 'text-delta', id: generation.generationId, delta: value },
|
|
77
|
+
{ type: 'text-end', id: generation.generationId },
|
|
78
|
+
])
|
|
79
|
+
const compact = (generation: Generation, completed = true): void => {
|
|
80
|
+
const payload = {
|
|
81
|
+
generationId: generation.generationId,
|
|
82
|
+
throughMessageId: generation.messageId,
|
|
83
|
+
throughIndex: events.length,
|
|
84
|
+
}
|
|
85
|
+
append({ type: 'ai.compaction.requested', payload })
|
|
86
|
+
if (completed)
|
|
87
|
+
append({
|
|
88
|
+
type: 'ai.compaction.completed',
|
|
89
|
+
payload: {
|
|
90
|
+
...payload,
|
|
91
|
+
messages: [],
|
|
92
|
+
summary: `Summary ${generation.generationId}`,
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
const state = () =>
|
|
97
|
+
events.reduce(reduceAIState, demoAgent.reducer.initialState)
|
|
98
|
+
const timeline = () => compactionTimeline({ state: state(), events })
|
|
99
|
+
return {
|
|
100
|
+
events,
|
|
101
|
+
append,
|
|
102
|
+
user,
|
|
103
|
+
start,
|
|
104
|
+
progress,
|
|
105
|
+
text,
|
|
106
|
+
compact,
|
|
107
|
+
state,
|
|
108
|
+
timeline,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const labels = (entries: TimelineEntry[]): string[] =>
|
|
113
|
+
entries.map((entry) =>
|
|
114
|
+
entry.type === 'compaction'
|
|
115
|
+
? entry.id
|
|
116
|
+
: entry.message.parts
|
|
117
|
+
.flatMap((part) => (part.type === 'text' ? [part.text] : []))
|
|
118
|
+
.join(''),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
it('keeps a stable marker from running to completed, including after replay', () => {
|
|
122
|
+
const log = transcript()
|
|
123
|
+
log.user('user')
|
|
124
|
+
const generation = log.start()
|
|
125
|
+
log.compact(generation, false)
|
|
126
|
+
const running = log.timeline().find((entry) => entry.type === 'compaction')
|
|
127
|
+
expect(running?.running).toBe(true)
|
|
128
|
+
expect(labels(log.timeline())).toEqual([
|
|
129
|
+
'user',
|
|
130
|
+
`compaction:${generation.generationId}`,
|
|
131
|
+
])
|
|
132
|
+
log.append({
|
|
133
|
+
type: 'ai.compaction.completed',
|
|
134
|
+
payload: {
|
|
135
|
+
generationId: generation.generationId,
|
|
136
|
+
throughMessageId: 'user',
|
|
137
|
+
messages: [],
|
|
138
|
+
summary: 'Summary',
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
log.text(generation, 'Answer')
|
|
142
|
+
log.append({ type: 'ai.generation.completed', payload: generation })
|
|
143
|
+
log.append({
|
|
144
|
+
type: 'ai.message.completed',
|
|
145
|
+
payload: { messageId: 'assistant' },
|
|
146
|
+
})
|
|
147
|
+
const completed = log.timeline().find((entry) => entry.type === 'compaction')
|
|
148
|
+
expect(completed).toMatchObject({
|
|
149
|
+
id: running?.id,
|
|
150
|
+
running: false,
|
|
151
|
+
event: { type: 'ai.compaction.completed' },
|
|
152
|
+
})
|
|
153
|
+
expect(labels(log.timeline())).toEqual([
|
|
154
|
+
'user',
|
|
155
|
+
`compaction:${generation.generationId}`,
|
|
156
|
+
'Answer',
|
|
157
|
+
])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('places compaction between tool steps without moving a queued user into the response', () => {
|
|
161
|
+
const log = transcript()
|
|
162
|
+
log.user('user')
|
|
163
|
+
const first = log.start()
|
|
164
|
+
log.text(first, 'Before tool')
|
|
165
|
+
log.append({
|
|
166
|
+
type: 'ai.generation.completed',
|
|
167
|
+
payload: { ...first, finishReason: 'tool-calls' },
|
|
168
|
+
})
|
|
169
|
+
log.user('queued')
|
|
170
|
+
const second = log.start({
|
|
171
|
+
requestId: `ai.generate:tools:${first.generationId}`,
|
|
172
|
+
reason: 'tool',
|
|
173
|
+
})
|
|
174
|
+
log.compact(second, false)
|
|
175
|
+
expect(log.state().compaction?.status).toBe('running')
|
|
176
|
+
expect(labels(log.timeline())).toEqual([
|
|
177
|
+
'user',
|
|
178
|
+
'Before tool',
|
|
179
|
+
`compaction:${second.generationId}`,
|
|
180
|
+
'queued',
|
|
181
|
+
])
|
|
182
|
+
log.append({
|
|
183
|
+
type: 'ai.compaction.completed',
|
|
184
|
+
payload: {
|
|
185
|
+
generationId: second.generationId,
|
|
186
|
+
throughMessageId: 'assistant',
|
|
187
|
+
messages: [],
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
log.text(second, 'After tool')
|
|
191
|
+
expect(labels(log.timeline())).toEqual([
|
|
192
|
+
'user',
|
|
193
|
+
'Before tool',
|
|
194
|
+
`compaction:${second.generationId}`,
|
|
195
|
+
'After tool',
|
|
196
|
+
'queued',
|
|
197
|
+
])
|
|
198
|
+
expect(
|
|
199
|
+
log
|
|
200
|
+
.timeline()
|
|
201
|
+
.flatMap((entry) =>
|
|
202
|
+
entry.type === 'message' && entry.message.id === 'assistant'
|
|
203
|
+
? entry.message.parts
|
|
204
|
+
: [],
|
|
205
|
+
),
|
|
206
|
+
).toEqual(
|
|
207
|
+
log.state().messages.find((message) => message.id === 'assistant')?.parts,
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('preserves compaction history but drops superseded attempt content', () => {
|
|
212
|
+
const log = transcript()
|
|
213
|
+
log.user('user')
|
|
214
|
+
const first = log.start()
|
|
215
|
+
log.compact(first)
|
|
216
|
+
log.text(first, 'Superseded')
|
|
217
|
+
const second = log.start({ attempt: 2 })
|
|
218
|
+
log.compact(second)
|
|
219
|
+
log.text(second, 'Replacement')
|
|
220
|
+
expect(log.state().terminalGenerations?.[first.generationId]).toBe(
|
|
221
|
+
'superseded',
|
|
222
|
+
)
|
|
223
|
+
expect(labels(log.timeline())).toEqual([
|
|
224
|
+
'user',
|
|
225
|
+
`compaction:${first.generationId}`,
|
|
226
|
+
`compaction:${second.generationId}`,
|
|
227
|
+
'Replacement',
|
|
228
|
+
])
|
|
229
|
+
expect(
|
|
230
|
+
log
|
|
231
|
+
.timeline()
|
|
232
|
+
.find((entry) => entry.id === `compaction:${first.generationId}`),
|
|
233
|
+
).toMatchObject({ outcome: 'superseded', running: false })
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('keeps old checkpoints before an explicit retry without restoring discarded parts', () => {
|
|
237
|
+
const log = transcript()
|
|
238
|
+
log.user('user')
|
|
239
|
+
const first = log.start()
|
|
240
|
+
log.compact(first)
|
|
241
|
+
log.text(first, 'Failed response')
|
|
242
|
+
log.append({
|
|
243
|
+
type: 'ai.generation.failed',
|
|
244
|
+
payload: { ...first, error: 'Test failure' },
|
|
245
|
+
})
|
|
246
|
+
const retry = log.start({ requestId: 'retry', reason: 'retry' })
|
|
247
|
+
log.compact(retry)
|
|
248
|
+
log.text(retry, 'Retried response')
|
|
249
|
+
expect(labels(log.timeline())).toEqual([
|
|
250
|
+
'user',
|
|
251
|
+
`compaction:${first.generationId}`,
|
|
252
|
+
`compaction:${retry.generationId}`,
|
|
253
|
+
'Retried response',
|
|
254
|
+
])
|
|
255
|
+
expect(
|
|
256
|
+
log
|
|
257
|
+
.timeline()
|
|
258
|
+
.find((entry) => entry.id === `compaction:${first.generationId}`),
|
|
259
|
+
).toMatchObject({ outcome: 'failed' })
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('does not leave failed compaction spinning', () => {
|
|
263
|
+
const log = transcript()
|
|
264
|
+
log.user('user')
|
|
265
|
+
const generation = log.start()
|
|
266
|
+
log.compact(generation, false)
|
|
267
|
+
log.append({
|
|
268
|
+
type: 'ai.generation.failed',
|
|
269
|
+
payload: { ...generation, error: 'Compaction failed' },
|
|
270
|
+
})
|
|
271
|
+
expect(
|
|
272
|
+
log.timeline().find((entry) => entry.type === 'compaction'),
|
|
273
|
+
).toMatchObject({
|
|
274
|
+
running: false,
|
|
275
|
+
outcome: 'failed',
|
|
276
|
+
event: { type: 'ai.compaction.requested' },
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it.each([false, true])(
|
|
281
|
+
'preserves complete messages without step boundaries, delta only=%s',
|
|
282
|
+
(deltaOnly) => {
|
|
283
|
+
const log = transcript()
|
|
284
|
+
log.user('user')
|
|
285
|
+
const first = log.start()
|
|
286
|
+
log.text(first, 'Before')
|
|
287
|
+
log.append({
|
|
288
|
+
type: 'ai.generation.completed',
|
|
289
|
+
payload: { ...first, finishReason: 'tool-calls' },
|
|
290
|
+
})
|
|
291
|
+
const second = log.start({
|
|
292
|
+
requestId: `ai.generate:tools:${first.generationId}`,
|
|
293
|
+
reason: 'tool',
|
|
294
|
+
})
|
|
295
|
+
log.compact(second)
|
|
296
|
+
if (deltaOnly)
|
|
297
|
+
log.progress(second, [
|
|
298
|
+
{
|
|
299
|
+
type: 'text-delta',
|
|
300
|
+
id: 'after',
|
|
301
|
+
delta: 'After without a step boundary',
|
|
302
|
+
},
|
|
303
|
+
])
|
|
304
|
+
else log.text(second, 'After without a step boundary', false)
|
|
305
|
+
const messages = log
|
|
306
|
+
.timeline()
|
|
307
|
+
.flatMap((entry) => (entry.type === 'message' ? [entry.message] : []))
|
|
308
|
+
expect(messages).toEqual(log.state().messages)
|
|
309
|
+
expect(
|
|
310
|
+
log.timeline().filter((entry) => entry.type === 'compaction'),
|
|
311
|
+
).toHaveLength(1)
|
|
312
|
+
expect(labels(log.timeline())[1]).toBe(`compaction:${second.generationId}`)
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
it('ignores a rejected later generation when mapping canonical parts', () => {
|
|
317
|
+
const log = transcript()
|
|
318
|
+
log.user('user')
|
|
319
|
+
const first = log.start()
|
|
320
|
+
log.compact(first)
|
|
321
|
+
log.text(first, 'Kept')
|
|
322
|
+
log.append({ type: 'ai.generation.completed', payload: first })
|
|
323
|
+
log.append({
|
|
324
|
+
type: 'ai.message.completed',
|
|
325
|
+
payload: { messageId: 'assistant' },
|
|
326
|
+
})
|
|
327
|
+
const rejected = log.start({ requestId: 'stale-retry', reason: 'retry' })
|
|
328
|
+
log.text(rejected, 'Ignored')
|
|
329
|
+
expect(log.state().responseGenerationIds['assistant']).toBe(
|
|
330
|
+
first.generationId,
|
|
331
|
+
)
|
|
332
|
+
expect(labels(log.timeline())).toEqual([
|
|
333
|
+
'user',
|
|
334
|
+
`compaction:${first.generationId}`,
|
|
335
|
+
'Kept',
|
|
336
|
+
])
|
|
337
|
+
})
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { UIMessage } from 'ai'
|
|
2
|
+
import type { AIState } from 'experimental-a2/ai'
|
|
3
|
+
import type { AgentEvent } from './model'
|
|
4
|
+
|
|
5
|
+
type Generation = Extract<AgentEvent, { type: 'ai.generation.started' }>
|
|
6
|
+
type CompactionEvent = Extract<
|
|
7
|
+
AgentEvent,
|
|
8
|
+
{ type: 'ai.compaction.requested' | 'ai.compaction.completed' }
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
export type CompactionEntry = {
|
|
12
|
+
type: 'compaction'
|
|
13
|
+
id: string
|
|
14
|
+
event: CompactionEvent
|
|
15
|
+
running: boolean
|
|
16
|
+
outcome: 'failed' | 'interrupted' | 'superseded' | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type TimelineEntry =
|
|
20
|
+
| { type: 'message'; id: string; message: UIMessage; lastSegment: boolean }
|
|
21
|
+
| CompactionEntry
|
|
22
|
+
|
|
23
|
+
export function compactionTimeline({
|
|
24
|
+
state,
|
|
25
|
+
events,
|
|
26
|
+
}: {
|
|
27
|
+
state: AIState
|
|
28
|
+
events: readonly AgentEvent[]
|
|
29
|
+
}): TimelineEntry[] {
|
|
30
|
+
const generations = new Map<string, Generation>()
|
|
31
|
+
const requests = new Map<string, string>()
|
|
32
|
+
const stepCounts = new Map<string, number>()
|
|
33
|
+
const contentGenerations = new Set<string>()
|
|
34
|
+
const compactions = new Map<string, CompactionEvent>()
|
|
35
|
+
const roots = new Map<string, string>()
|
|
36
|
+
for (const event of events) {
|
|
37
|
+
switch (event.type) {
|
|
38
|
+
case 'ai.generation.requested':
|
|
39
|
+
requests.set(event.id, event.payload.reason)
|
|
40
|
+
break
|
|
41
|
+
case 'ai.generation.started': {
|
|
42
|
+
const { generationId, messageId, responseMessageId } = event.payload
|
|
43
|
+
generations.set(generationId, event)
|
|
44
|
+
if (messageId !== responseMessageId)
|
|
45
|
+
roots.set(responseMessageId, messageId)
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
case 'ai.generation.progress': {
|
|
49
|
+
const id = event.payload.generationId
|
|
50
|
+
const count = event.payload.chunks.filter(
|
|
51
|
+
(chunk) => chunk.type === 'start-step',
|
|
52
|
+
).length
|
|
53
|
+
stepCounts.set(id, (stepCounts.get(id) ?? 0) + count)
|
|
54
|
+
if (
|
|
55
|
+
event.payload.chunks.some(
|
|
56
|
+
(chunk) =>
|
|
57
|
+
![
|
|
58
|
+
'start',
|
|
59
|
+
'finish',
|
|
60
|
+
'start-step',
|
|
61
|
+
'finish-step',
|
|
62
|
+
'message-metadata',
|
|
63
|
+
'abort',
|
|
64
|
+
'error',
|
|
65
|
+
].includes(chunk.type),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
contentGenerations.add(id)
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
case 'ai.compaction.requested':
|
|
72
|
+
case 'ai.compaction.completed':
|
|
73
|
+
compactions.set(event.payload.generationId, event)
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const contributions = new Map<string, Generation[]>()
|
|
79
|
+
for (const generation of generations.values()) {
|
|
80
|
+
const { generationId, responseMessageId, requestId } = generation.payload
|
|
81
|
+
const ownerId = state.responseGenerationIds[responseMessageId]
|
|
82
|
+
const owner = ownerId === undefined ? undefined : generations.get(ownerId)
|
|
83
|
+
if (
|
|
84
|
+
owner === undefined ||
|
|
85
|
+
generation.index > owner.index ||
|
|
86
|
+
state.terminalGenerations?.[generationId] === 'superseded'
|
|
87
|
+
)
|
|
88
|
+
continue
|
|
89
|
+
let steps = contributions.get(responseMessageId) ?? []
|
|
90
|
+
if (requests.get(requestId) === 'retry') steps = []
|
|
91
|
+
if (steps.at(-1)?.payload.requestId === requestId) steps.pop()
|
|
92
|
+
steps.push(generation)
|
|
93
|
+
contributions.set(responseMessageId, steps)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const included = new Set(
|
|
97
|
+
[...contributions.values()].flatMap((steps) =>
|
|
98
|
+
steps.map((step) => step.payload.generationId),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
const offsets = new Map<string, number>()
|
|
102
|
+
const counts = new Map<string, number>()
|
|
103
|
+
for (const { payload } of generations.values()) {
|
|
104
|
+
const count = counts.get(payload.responseMessageId) ?? 0
|
|
105
|
+
offsets.set(payload.generationId, count)
|
|
106
|
+
if (included.has(payload.generationId))
|
|
107
|
+
counts.set(
|
|
108
|
+
payload.responseMessageId,
|
|
109
|
+
count + (stepCounts.get(payload.generationId) ?? 0),
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const markers = new Map<string, CompactionEntry[]>()
|
|
114
|
+
const orphaned: CompactionEntry[] = []
|
|
115
|
+
const messageIds = new Set(state.messages.map((message) => message.id))
|
|
116
|
+
for (const [generationId, event] of compactions) {
|
|
117
|
+
const terminal = state.terminalGenerations?.[generationId]
|
|
118
|
+
const entry: CompactionEntry = {
|
|
119
|
+
type: 'compaction',
|
|
120
|
+
id: `compaction:${generationId}`,
|
|
121
|
+
event,
|
|
122
|
+
running:
|
|
123
|
+
state.compaction?.generationId === generationId &&
|
|
124
|
+
state.compaction.status === 'running',
|
|
125
|
+
outcome: terminal === 'completed' ? undefined : terminal,
|
|
126
|
+
}
|
|
127
|
+
const responseId = generations.get(generationId)?.payload.responseMessageId
|
|
128
|
+
const anchor =
|
|
129
|
+
responseId !== undefined && messageIds.has(responseId)
|
|
130
|
+
? responseId
|
|
131
|
+
: responseId === undefined
|
|
132
|
+
? undefined
|
|
133
|
+
: roots.get(responseId)
|
|
134
|
+
if (anchor === undefined || !messageIds.has(anchor)) orphaned.push(entry)
|
|
135
|
+
else {
|
|
136
|
+
const entries = markers.get(anchor) ?? []
|
|
137
|
+
entries.push(entry)
|
|
138
|
+
markers.set(anchor, entries)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const timeline: TimelineEntry[] = []
|
|
143
|
+
for (const message of state.messages) {
|
|
144
|
+
const entries = markers.get(message.id) ?? []
|
|
145
|
+
if (entries.length === 0) {
|
|
146
|
+
timeline.push({
|
|
147
|
+
type: 'message',
|
|
148
|
+
id: `${message.id}:0`,
|
|
149
|
+
message,
|
|
150
|
+
lastSegment: true,
|
|
151
|
+
})
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
const steps = contributions.get(message.id) ?? []
|
|
155
|
+
const boundaries = message.parts.flatMap((part, index) =>
|
|
156
|
+
part.type === 'step-start' ? [index] : [],
|
|
157
|
+
)
|
|
158
|
+
const mapped =
|
|
159
|
+
boundaries[0] === 0 &&
|
|
160
|
+
boundaries.length === counts.get(message.id) &&
|
|
161
|
+
steps.every(
|
|
162
|
+
({ payload }) =>
|
|
163
|
+
!contentGenerations.has(payload.generationId) ||
|
|
164
|
+
(stepCounts.get(payload.generationId) ?? 0) > 0,
|
|
165
|
+
)
|
|
166
|
+
let offset = 0
|
|
167
|
+
const appendMessage = (end: number): void => {
|
|
168
|
+
if (end <= offset) return
|
|
169
|
+
timeline.push({
|
|
170
|
+
type: 'message',
|
|
171
|
+
id: `${message.id}:${offset}`,
|
|
172
|
+
message: { ...message, parts: message.parts.slice(offset, end) },
|
|
173
|
+
lastSegment: end === message.parts.length,
|
|
174
|
+
})
|
|
175
|
+
offset = end
|
|
176
|
+
}
|
|
177
|
+
for (const entry of entries) {
|
|
178
|
+
if (message.role === 'user') {
|
|
179
|
+
appendMessage(message.parts.length)
|
|
180
|
+
} else if (mapped) {
|
|
181
|
+
const stepIndex = offsets.get(entry.event.payload.generationId) ?? 0
|
|
182
|
+
appendMessage(boundaries[stepIndex] ?? message.parts.length)
|
|
183
|
+
}
|
|
184
|
+
timeline.push(entry)
|
|
185
|
+
}
|
|
186
|
+
if (message.parts.length === 0) {
|
|
187
|
+
timeline.push({
|
|
188
|
+
type: 'message',
|
|
189
|
+
id: message.id,
|
|
190
|
+
message,
|
|
191
|
+
lastSegment: true,
|
|
192
|
+
})
|
|
193
|
+
} else {
|
|
194
|
+
appendMessage(message.parts.length)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return [...timeline, ...orphaned]
|
|
198
|
+
}
|
|
@@ -1,3 +1,49 @@
|
|
|
1
1
|
import { agent } from 'experimental-a2/ai'
|
|
2
|
+
import type { ContractEvent } from 'experimental-a2'
|
|
3
|
+
import type { AutomaticCompaction } from 'experimental-a2/ai/server'
|
|
4
|
+
import { z } from 'zod'
|
|
2
5
|
|
|
3
|
-
export const
|
|
6
|
+
export const agentModel = 'openai/gpt-5.6-luna'
|
|
7
|
+
|
|
8
|
+
export const compactionSettingsSchema = z.discriminatedUnion('mode', [
|
|
9
|
+
z.strictObject({ mode: z.literal('automatic') }),
|
|
10
|
+
z.strictObject({
|
|
11
|
+
mode: z.literal('manual'),
|
|
12
|
+
thresholdTokens: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
|
|
13
|
+
}),
|
|
14
|
+
z.strictObject({ mode: z.literal('disabled') }),
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
export type CompactionSettings = z.infer<typeof compactionSettingsSchema>
|
|
18
|
+
|
|
19
|
+
export const demoAgent = agent({
|
|
20
|
+
name: 'agent',
|
|
21
|
+
events: { 'compaction.settings.updated': compactionSettingsSchema },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export type AgentEvent = ContractEvent<typeof demoAgent.contract.events>
|
|
25
|
+
|
|
26
|
+
const automatic: CompactionSettings = { mode: 'automatic' }
|
|
27
|
+
|
|
28
|
+
export function latestCompactionSettings(history: readonly AgentEvent[]): {
|
|
29
|
+
settings: CompactionSettings
|
|
30
|
+
id: string
|
|
31
|
+
} {
|
|
32
|
+
const event = history.findLast(
|
|
33
|
+
(candidate) => candidate.type === 'compaction.settings.updated',
|
|
34
|
+
)
|
|
35
|
+
return event
|
|
36
|
+
? { settings: event.payload, id: event.id }
|
|
37
|
+
: { settings: automatic, id: 'default' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function compactionOptions(
|
|
41
|
+
history: readonly AgentEvent[],
|
|
42
|
+
): AutomaticCompaction | false {
|
|
43
|
+
const { settings } = latestCompactionSettings(history)
|
|
44
|
+
return settings.mode === 'disabled'
|
|
45
|
+
? false
|
|
46
|
+
: settings.mode === 'manual'
|
|
47
|
+
? { thresholdTokens: settings.thresholdTokens }
|
|
48
|
+
: {}
|
|
49
|
+
}
|
|
@@ -8,7 +8,7 @@ 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 { demoAgent } from './model'
|
|
11
|
+
import { agentModel, compactionOptions, demoAgent } from './model'
|
|
12
12
|
|
|
13
13
|
const maxReminderDelaySeconds = 6 * 24 * 60 * 60
|
|
14
14
|
const bashTimeoutMs = 120_000
|
|
@@ -131,7 +131,8 @@ export const agentServer = createAgentServer({
|
|
|
131
131
|
...(store ? { store } : {}),
|
|
132
132
|
...(agentScheduler ? { scheduler: agentScheduler } : {}),
|
|
133
133
|
agent: demoAgent,
|
|
134
|
-
model:
|
|
134
|
+
model: agentModel,
|
|
135
|
+
compaction: ({ history }) => compactionOptions(history),
|
|
135
136
|
tools,
|
|
136
137
|
instructions: `You are an incident-response agent for the playground.
|
|
137
138
|
When responding to an incident, inspect checkout-api before taking action. Its
|