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.
Files changed (106) hide show
  1. package/CHANGELOG.md +47 -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 +16 -8
  17. package/dist/ai-server.d.ts.map +1 -1
  18. package/dist/ai-server.js +1322 -475
  19. package/dist/ai-server.js.map +1 -1
  20. package/dist/ai.d.ts +2 -334
  21. package/dist/ai.js +838 -85
  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 +361 -76
  64. package/docs/reference/01-api.mdx +159 -27
  65. package/examples/playground/app/agent/[agentId]/agent-client.tsx +145 -33
  66. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
  67. package/examples/playground/app/agent/[agentId]/compaction/route.ts +14 -0
  68. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +38 -0
  69. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +294 -0
  70. package/examples/playground/app/agent/compaction-settings.test.ts +144 -0
  71. package/examples/playground/app/agent/compaction-settings.ts +49 -0
  72. package/examples/playground/app/agent/compaction-timeline.test.ts +337 -0
  73. package/examples/playground/app/agent/compaction-timeline.ts +198 -0
  74. package/examples/playground/app/agent/model.ts +56 -1
  75. package/examples/playground/app/agent/server.ts +9 -2
  76. package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
  77. package/examples/playground/app/chat/model.ts +2 -2
  78. package/examples/playground/app/chat/server.ts +24 -17
  79. package/examples/playground/app/globals.css +333 -0
  80. package/examples/playground/package.json +1 -1
  81. package/package.json +1 -1
  82. package/src/ai-client-state.ts +185 -0
  83. package/src/ai-control-server.ts +829 -0
  84. package/src/ai-control-state.ts +152 -0
  85. package/src/ai-control.ts +139 -0
  86. package/src/ai-coordinator.ts +99 -32
  87. package/src/ai-model-metadata.ts +108 -0
  88. package/src/ai-progress-batches.ts +68 -0
  89. package/src/ai-projector.ts +76 -15
  90. package/src/ai-sdk-step.ts +5 -2
  91. package/src/ai-server.ts +920 -638
  92. package/src/ai.ts +650 -110
  93. package/src/client.ts +31 -9
  94. package/src/licenses/Apache-2.0.txt +55 -0
  95. package/src/parse-partial-json.ts +441 -0
  96. package/src/reducer.ts +6 -0
  97. package/src/server.ts +8 -4
  98. package/src/store-postgres.ts +27 -0
  99. package/src/store-redis-core.ts +53 -1
  100. package/src/store-redis-notify.ts +1 -0
  101. package/src/store.ts +6 -0
  102. package/dist/ai.d.ts.map +0 -1
  103. package/dist/client-Bf6uSEAk.js.map +0 -1
  104. package/dist/contract-48bUMgcL.js.map +0 -1
  105. package/dist/server-BeNADlCI.js.map +0 -1
  106. package/dist/store-redis-notify-BUCyXOn0.js.map +0 -1
@@ -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,58 @@
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 demoAgent = agent({ name: 'agent' })
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 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
+
50
+ export function compactionOptions(
51
+ settings: CompactionSettings,
52
+ ): AutomaticCompaction | false {
53
+ return settings.mode === 'disabled'
54
+ ? false
55
+ : settings.mode === 'manual'
56
+ ? { thresholdTokens: settings.thresholdTokens }
57
+ : {}
58
+ }
@@ -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 { 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
@@ -131,7 +136,9 @@ export const agentServer = createAgentServer({
131
136
  ...(store ? { store } : {}),
132
137
  ...(agentScheduler ? { scheduler: agentScheduler } : {}),
133
138
  agent: demoAgent,
134
- model: 'openai/gpt-5.6-luna',
139
+ model: agentModel,
140
+ compaction: async ({ session }) =>
141
+ compactionOptions((await session.state(compactionSettings)).state),
135
142
  tools,
136
143
  instructions: `You are an incident-response agent for the playground.
137
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) => {