experimental-a2 0.13.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 (99) hide show
  1. package/CHANGELOG.md +34 -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 +1014 -501
  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 +265 -62
  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 +381 -608
  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
@@ -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;
@@ -22,7 +22,7 @@
22
22
  "@vercel/sandbox": "^3.0.0",
23
23
  "ai": "^7.0.58",
24
24
  "codemirror": "^6.0.2",
25
- "experimental-a2": "0.13.0",
25
+ "experimental-a2": "0.14.0",
26
26
  "ioredis": "^5.9.0",
27
27
  "next": "^16.3.0",
28
28
  "pg": "^8.16.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-a2",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,185 @@
1
+ import type { UIMessage } from 'ai'
2
+ import type { AIState } from './ai.ts'
3
+ import type { ControlCommand, InboxItem } from './ai-control.ts'
4
+
5
+ export type QueueCommand<M extends UIMessage> = Extract<
6
+ ControlCommand<M>,
7
+ {
8
+ action:
9
+ | 'send'
10
+ | 'edit'
11
+ | 'move'
12
+ | 'remove'
13
+ | 'send-now'
14
+ | 'steer'
15
+ | 'pause'
16
+ | 'resume'
17
+ }
18
+ >
19
+ export type PendingQueueCommand<M extends UIMessage> = {
20
+ commandId: string
21
+ command: QueueCommand<M>
22
+ }
23
+
24
+ export const isQueueCommand = <M extends UIMessage>(
25
+ command: ControlCommand<M>,
26
+ ): command is QueueCommand<M> =>
27
+ command.action === 'send' ||
28
+ command.action === 'edit' ||
29
+ command.action === 'move' ||
30
+ command.action === 'remove' ||
31
+ command.action === 'send-now' ||
32
+ command.action === 'steer' ||
33
+ command.action === 'pause' ||
34
+ command.action === 'resume'
35
+
36
+ const moveItem = <M extends UIMessage>({
37
+ items,
38
+ inputId,
39
+ beforeId,
40
+ }: {
41
+ items: InboxItem<M>[]
42
+ inputId: string
43
+ beforeId: string | null
44
+ }): InboxItem<M>[] => {
45
+ const from = items.findIndex((item) => item.id === inputId)
46
+ if (
47
+ from < 0 ||
48
+ inputId === beforeId ||
49
+ (beforeId !== null && !items.some((item) => item.id === beforeId))
50
+ )
51
+ return items
52
+ const next = [...items]
53
+ const [item] = next.splice(from, 1)
54
+ const to =
55
+ beforeId === null
56
+ ? next.length
57
+ : next.findIndex((entry) => entry.id === beforeId)
58
+ next.splice(to, 0, item!)
59
+ return next
60
+ }
61
+
62
+ export function projectAIClientState<M extends UIMessage>(
63
+ state: AIState<M>,
64
+ ): AIState<M> {
65
+ const pending = state.pendingQueueCommands ?? []
66
+ if (state.status === 'closed') return state
67
+ if (
68
+ pending.length === 0 &&
69
+ !state.inbox.items.some((item) => item.afterStepOf !== undefined)
70
+ )
71
+ return state
72
+ let messages = state.messages
73
+ let { items, paused } = state.inbox
74
+ let turnId = state.active?.turnId ?? null
75
+ let starting = false
76
+ let suspended =
77
+ state.active?.phase === 'paused' || state.active?.phase === 'pausing'
78
+ let messageIds: Set<string> | undefined
79
+ const show = (message: M): void => {
80
+ messageIds ??= new Set(messages.map((item) => item.id))
81
+ if (messageIds.has(message.id)) return
82
+ if (messages === state.messages) messages = state.messages.slice()
83
+ messages.push(message)
84
+ messageIds.add(message.id)
85
+ }
86
+ const admit = (): void => {
87
+ if (turnId !== null || paused || items.length === 0) return
88
+ let count = 0
89
+ for (const item of items) {
90
+ count += 1
91
+ show(item.message)
92
+ if (item.generate) {
93
+ turnId = item.id
94
+ starting = true
95
+ suspended = false
96
+ break
97
+ }
98
+ }
99
+ if (count > 0) items = items.slice(count)
100
+ }
101
+ for (const { command } of pending) {
102
+ switch (command.action) {
103
+ case 'send':
104
+ case 'steer': {
105
+ if (
106
+ items.some((item) => item.id === command.message.id) ||
107
+ turnId === command.message.id
108
+ )
109
+ break
110
+ const item = {
111
+ ...(command.action === 'steer'
112
+ ? { afterStepOf: command.turnId }
113
+ : {}),
114
+ id: command.message.id,
115
+ revision: 0,
116
+ message: command.message,
117
+ generate:
118
+ command.message.role === 'user' &&
119
+ (command.action === 'steer' || command.generate !== false),
120
+ }
121
+ items = command.action === 'steer' ? [item, ...items] : [...items, item]
122
+ break
123
+ }
124
+ case 'edit':
125
+ items = items.map((item) =>
126
+ item.id === command.message.id &&
127
+ item.revision === command.expectedRevision &&
128
+ item.message.role === command.message.role
129
+ ? { ...item, message: command.message }
130
+ : item,
131
+ )
132
+ break
133
+ case 'move':
134
+ items = moveItem({
135
+ items,
136
+ inputId: command.inputId,
137
+ beforeId: command.beforeId,
138
+ })
139
+ break
140
+ case 'remove':
141
+ items = items.filter((item) => item.id !== command.inputId)
142
+ break
143
+ case 'send-now': {
144
+ if (turnId !== command.turnId) break
145
+ const selected = items.find((item) => item.id === command.inputId)
146
+ if (!selected || selected.message.role !== 'user') break
147
+ items = items.map((item) =>
148
+ item.id === selected.id
149
+ ? {
150
+ ...item,
151
+ generate: true,
152
+ ...(turnId === null ? {} : { afterStepOf: turnId }),
153
+ }
154
+ : item,
155
+ )
156
+ items = moveItem({
157
+ items,
158
+ inputId: command.inputId,
159
+ beforeId: items[0]?.id ?? null,
160
+ })
161
+ break
162
+ }
163
+ case 'pause':
164
+ paused = true
165
+ if (command.when === 'now') suspended = true
166
+ break
167
+ case 'resume':
168
+ paused = false
169
+ suspended = false
170
+ break
171
+ }
172
+ admit()
173
+ }
174
+ items = items.filter((item) => {
175
+ if (item.afterStepOf === undefined) return true
176
+ show(item.message)
177
+ return false
178
+ })
179
+ return {
180
+ ...state,
181
+ messages,
182
+ inbox: { items, paused },
183
+ starting: starting && !suspended,
184
+ }
185
+ }