experimental-a2 0.17.0 → 0.18.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 (211) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-DBbe5xeE.d.ts → actor-Bjce9_5U.d.ts} +5 -5
  4. package/dist/{actor-DBbe5xeE.d.ts.map → actor-Bjce9_5U.d.ts.map} +1 -1
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +1 -1
  8. package/dist/actor.d.ts +1 -1
  9. package/dist/actor.js +2 -2
  10. package/dist/{ai-UrWfKeuv.js → ai-BpOGm-zB.js} +2 -2
  11. package/dist/{ai-UrWfKeuv.js.map → ai-BpOGm-zB.js.map} +1 -1
  12. package/dist/{ai-vpGtcaCB.d.ts → ai-DrUaMyRt.d.ts} +3 -3
  13. package/dist/{ai-vpGtcaCB.d.ts.map → ai-DrUaMyRt.d.ts.map} +1 -1
  14. package/dist/ai-client.d.ts +2 -2
  15. package/dist/ai-client.js +2 -2
  16. package/dist/ai-fx.browser.d.ts +1 -0
  17. package/dist/ai-fx.browser.js +6 -0
  18. package/dist/ai-fx.browser.js.map +1 -0
  19. package/dist/ai-fx.d.ts +10 -0
  20. package/dist/ai-fx.d.ts.map +1 -0
  21. package/dist/ai-fx.js +634 -0
  22. package/dist/ai-fx.js.map +1 -0
  23. package/dist/ai-harness-BuLh_EYi.js +273 -0
  24. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  25. package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
  26. package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
  27. package/dist/ai-pi.browser.d.ts +1 -0
  28. package/dist/ai-pi.browser.js +6 -0
  29. package/dist/ai-pi.browser.js.map +1 -0
  30. package/dist/ai-pi.d.ts +17 -0
  31. package/dist/ai-pi.d.ts.map +1 -0
  32. package/dist/ai-pi.js +904 -0
  33. package/dist/ai-pi.js.map +1 -0
  34. package/dist/ai-react.d.ts +5 -5
  35. package/dist/ai-react.js +2 -2
  36. package/dist/ai-server.d.ts +47 -25
  37. package/dist/ai-server.d.ts.map +1 -1
  38. package/dist/ai-server.js +106 -37
  39. package/dist/ai-server.js.map +1 -1
  40. package/dist/ai.d.ts +1 -1
  41. package/dist/ai.js +1 -1
  42. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  43. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  44. package/dist/{client-COaiaDub.js → client-BReKqwZC.js} +3 -3
  45. package/dist/{client-COaiaDub.js.map → client-BReKqwZC.js.map} +1 -1
  46. package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
  47. package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
  48. package/dist/client.d.ts +3 -3
  49. package/dist/client.js +1 -1
  50. package/dist/index.d.ts +5 -5
  51. package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
  52. package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
  53. package/dist/{internal-CbVvEwad.js → internal-B2T3U4Rq.js} +8 -2
  54. package/dist/internal-B2T3U4Rq.js.map +1 -0
  55. package/dist/otel.d.ts +1 -1
  56. package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
  57. package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/reducer-BAXc8JyF.js.map +1 -1
  61. package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
  62. package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
  63. package/dist/scheduler-qstash.d.ts +2 -2
  64. package/dist/scheduler-qstash.js +2 -2
  65. package/dist/scheduler-vercel.d.ts +2 -2
  66. package/dist/scheduler-vercel.js +2 -2
  67. package/dist/{server-Rj64fNvX.js → server-Dw_k_7kO.js} +93 -60
  68. package/dist/server-Dw_k_7kO.js.map +1 -0
  69. package/dist/{server-CGM8WzRG.d.ts → server-En7cbi3v.d.ts} +36 -7
  70. package/dist/server-En7cbi3v.d.ts.map +1 -0
  71. package/dist/server.d.ts +4 -4
  72. package/dist/server.js +1 -1
  73. package/dist/{session-clear-Dshpuhff.d.ts → session-clear-Bo-XTFEy.d.ts} +2 -2
  74. package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
  75. package/dist/{store-Gpkq7WBo.d.ts → store-Djdv0bnZ.d.ts} +31 -2
  76. package/dist/store-Djdv0bnZ.d.ts.map +1 -0
  77. package/dist/store-N8PXxDAS.js.map +1 -1
  78. package/dist/store-memory.d.ts +1 -1
  79. package/dist/store-memory.js +32 -6
  80. package/dist/store-memory.js.map +1 -1
  81. package/dist/{store-polling-4_y8lETm.js → store-polling-B5FcwJfo.js} +2 -2
  82. package/dist/{store-polling-4_y8lETm.js.map → store-polling-B5FcwJfo.js.map} +1 -1
  83. package/dist/store-postgres.d.ts +1 -1
  84. package/dist/store-postgres.js +54 -3
  85. package/dist/store-postgres.js.map +1 -1
  86. package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-BAwS8RWn.js} +3 -3
  87. package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
  88. package/dist/store-redis-http.d.ts +1 -1
  89. package/dist/store-redis-http.js +3 -3
  90. package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-BRITti_o.js} +187 -68
  91. package/dist/store-redis-notify-BRITti_o.js.map +1 -0
  92. package/dist/store-redis.d.ts +1 -1
  93. package/dist/store-redis.js +1 -1
  94. package/dist/store-sqlite.d.ts +1 -1
  95. package/dist/store-sqlite.js +32 -2
  96. package/dist/store-sqlite.js.map +1 -1
  97. package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  98. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  99. package/dist/{wire-DjfHOvdW.js → wire-rSUglYo8.js} +2 -2
  100. package/dist/{wire-DjfHOvdW.js.map → wire-rSUglYo8.js.map} +1 -1
  101. package/docs/01-quickstart.mdx +49 -83
  102. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  103. package/docs/actors/01-introduction.mdx +48 -86
  104. package/docs/actors/02-concurrency.mdx +43 -70
  105. package/docs/actors/03-timers.mdx +28 -42
  106. package/docs/actors/04-routes.mdx +110 -150
  107. package/docs/actors/meta.ts +1 -1
  108. package/docs/agents/01-introduction.mdx +257 -0
  109. package/docs/agents/02-tools.mdx +247 -0
  110. package/docs/agents/03-conversations.mdx +538 -0
  111. package/docs/agents/04-harnesses.mdx +136 -0
  112. package/docs/agents/meta.ts +1 -0
  113. package/docs/client/01-react.mdx +294 -0
  114. package/docs/client/02-transports.mdx +86 -0
  115. package/docs/client/03-presence.mdx +224 -0
  116. package/docs/client/meta.ts +1 -0
  117. package/docs/contract/01-events.mdx +57 -0
  118. package/docs/contract/02-state.mdx +142 -0
  119. package/docs/contract/03-sessions.mdx +175 -0
  120. package/docs/index.mdx +108 -144
  121. package/docs/reference/01-core.mdx +139 -0
  122. package/docs/reference/02-server.mdx +855 -0
  123. package/docs/reference/03-client.mdx +301 -0
  124. package/docs/reference/04-ai.mdx +722 -0
  125. package/docs/reference/05-adapters.mdx +351 -0
  126. package/docs/reference/06-errors.mdx +138 -0
  127. package/docs/reference/meta.ts +1 -1
  128. package/docs/server/01-handlers.mdx +200 -0
  129. package/docs/server/02-timers.mdx +130 -0
  130. package/docs/server/03-cancellation.mdx +141 -0
  131. package/docs/server/04-routes.mdx +243 -0
  132. package/docs/server/05-production.mdx +322 -0
  133. package/docs/server/06-guarantees.mdx +123 -0
  134. package/examples/playground/DEPLOY.md +1 -1
  135. package/examples/playground/app/a0/[appId]/page.tsx +11 -6
  136. package/examples/playground/app/a0/app-list.tsx +9 -2
  137. package/examples/playground/app/a0/app-view.tsx +9 -1
  138. package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
  139. package/examples/playground/app/a0/apps.test.ts +12 -0
  140. package/examples/playground/app/a0/apps.ts +22 -7
  141. package/examples/playground/app/a0/chat.tsx +47 -1
  142. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  143. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  144. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  145. package/examples/playground/app/a0/model.test.ts +18 -1
  146. package/examples/playground/app/a0/model.ts +47 -10
  147. package/examples/playground/app/a0/no-photon.cjs +4 -0
  148. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  149. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  150. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  151. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  152. package/examples/playground/app/a0/sandbox-log.ts +10 -4
  153. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  154. package/examples/playground/app/a0/scheduler/route.ts +2 -2
  155. package/examples/playground/app/a0/server.ts +100 -45
  156. package/examples/playground/app/a0/session.ts +17 -5
  157. package/examples/playground/app/a0/shell.tsx +11 -3
  158. package/examples/playground/app/agent/ingress.test.ts +6 -3
  159. package/examples/playground/app/agent/server.ts +9 -6
  160. package/examples/playground/app/agent/telemetry.test.ts +71 -50
  161. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
  162. package/examples/playground/app/chat/server.test.ts +30 -26
  163. package/examples/playground/app/chat/server.ts +46 -46
  164. package/examples/playground/next.config.ts +17 -5
  165. package/examples/playground/package.json +4 -1
  166. package/examples/playground/vercel.json +18 -0
  167. package/package.json +27 -3
  168. package/src/ai-agent.ts +261 -87
  169. package/src/ai-fx.browser.ts +5 -0
  170. package/src/ai-fx.ts +833 -0
  171. package/src/ai-harness.ts +518 -0
  172. package/src/ai-model-metadata.ts +11 -6
  173. package/src/ai-pi.browser.ts +5 -0
  174. package/src/ai-pi.ts +1263 -0
  175. package/src/ai-sdk-step.ts +2 -2
  176. package/src/ai-server.ts +3 -1
  177. package/src/internal.ts +7 -0
  178. package/src/libfx.d.ts +34 -0
  179. package/src/reducer.ts +1 -1
  180. package/src/server.ts +177 -19
  181. package/src/store-memory.ts +42 -7
  182. package/src/store-postgres.ts +80 -0
  183. package/src/store-redis-core.ts +210 -79
  184. package/src/store-redis-notify.ts +2 -0
  185. package/src/store-sqlite.ts +60 -0
  186. package/src/store.ts +33 -0
  187. package/src/telemetry.ts +1 -0
  188. package/dist/internal-CbVvEwad.js.map +0 -1
  189. package/dist/server-CGM8WzRG.d.ts.map +0 -1
  190. package/dist/server-Rj64fNvX.js.map +0 -1
  191. package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
  192. package/dist/store-Gpkq7WBo.d.ts.map +0 -1
  193. package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
  194. package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
  195. package/docs/concepts/01-contracts.mdx +0 -237
  196. package/docs/concepts/02-handlers.mdx +0 -284
  197. package/docs/concepts/03-durability.mdx +0 -314
  198. package/docs/concepts/04-state.mdx +0 -209
  199. package/docs/guides/01-timers.mdx +0 -193
  200. package/docs/guides/02-cancellation.mdx +0 -161
  201. package/docs/guides/03-react.mdx +0 -324
  202. package/docs/guides/04-local-first.mdx +0 -47
  203. package/docs/guides/05-production.mdx +0 -534
  204. package/docs/guides/06-ai-agents.mdx +0 -1408
  205. package/docs/guides/08-application-data.mdx +0 -113
  206. package/docs/guides/09-presence.mdx +0 -262
  207. package/docs/guides/10-transports.mdx +0 -221
  208. package/docs/reference/01-api.mdx +0 -2358
  209. package/docs/reference/02-errors.mdx +0 -150
  210. /package/docs/{concepts → contract}/meta.ts +0 -0
  211. /package/docs/{guides → server}/meta.ts +0 -0
@@ -1,7 +1,7 @@
1
- import { agentServer, appScheduler } from '../server'
1
+ import { agentServer, appSchedulers } from '../server'
2
2
 
3
3
  export const POST =
4
- appScheduler?.handler(agentServer) ??
4
+ appSchedulers['ai-sdk']?.handler(agentServer) ??
5
5
  (() => new Response('a0 scheduler is not configured', { status: 503 }))
6
6
 
7
7
  export const maxDuration = 300
@@ -2,6 +2,8 @@ import 'server-only'
2
2
  import { waitUntil } from '@vercel/functions'
3
3
  import { tool } from 'ai'
4
4
  import { handlerContext } from 'experimental-a2/ai'
5
+ import { fx } from 'experimental-a2/ai/fx'
6
+ import { pi } from 'experimental-a2/ai/pi'
5
7
  import { agent } from 'experimental-a2/ai/server'
6
8
  import { createServer } from 'experimental-a2/server'
7
9
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
@@ -10,7 +12,7 @@ import { store } from '@/lib/store'
10
12
  import { appList, isAppId } from './apps'
11
13
  import { crashOnRequest } from './crash-on-request'
12
14
  import { crashLog, crashLogId, isCrashLogId } from './crashes'
13
- import { appAgent, appPort } from './model'
15
+ import { appPort, type Harness, harnessAgents, type ModelChoice } from './model'
14
16
  import {
15
17
  appDir,
16
18
  appSandbox,
@@ -19,8 +21,11 @@ import {
19
21
  START_DEV,
20
22
  workspace,
21
23
  } from './sandbox'
24
+ import { sandboxSession } from './sandbox-session'
22
25
 
23
- const INSTRUCTIONS = `You are a coding agent that builds a Next.js app in your own Linux sandbox, through the bash tool. The user watches the app in a live preview next to this chat.
26
+ const instructions = (
27
+ tools: string,
28
+ ): string => `You are a coding agent that builds a Next.js app in your own Linux sandbox, ${tools}. The user watches the app in a live preview next to this chat.
24
29
  - ${appDir} already holds a blank Next.js app (App Router, TypeScript, Tailwind) with its dependencies installed, and next dev serves it on port ${appPort} with hot reload. It may still be starting when you begin: don't wait for it, poll it, or start another one. Build what the user asks for there; don't create another app.
25
30
  - Until you change them, app/page.tsx renders an empty <main />, app/layout.tsx only sets the title, and app/globals.css only imports Tailwind. Write them directly; there's no need to look first.
26
31
  - Assume your edits work. Don't curl the dev server, sleep, or read the page back to check, and don't run builds, type checks or lint unless the user asks.
@@ -48,9 +53,7 @@ const tools = {
48
53
  execute: ({ path }) =>
49
54
  timed(async () =>
50
55
  previewUrl({
51
- sandbox: await appSandbox({
52
- appId: handlerContext(appAgent).session.id,
53
- }),
56
+ sandbox: await appSandbox({ appId: handlerContext().session.id }),
54
57
  path,
55
58
  }),
56
59
  ),
@@ -63,7 +66,7 @@ const tools = {
63
66
  timeoutSeconds: z.number().int().min(1).max(240).default(120),
64
67
  }),
65
68
  execute: ({ command, timeoutSeconds }) => {
66
- const context = handlerContext(appAgent)
69
+ const context = handlerContext()
67
70
  return timed(async () =>
68
71
  bash({
69
72
  sandbox: await appSandbox({ appId: context.session.id }),
@@ -76,56 +79,108 @@ const tools = {
76
79
  }),
77
80
  }
78
81
 
82
+ const queued = !!(process.env.VERCEL || process.env.A2_QUEUES)
83
+
79
84
  /**
80
85
  * Wakes agent work in a fresh invocation when the one that started it ends,
81
- * and recovers from crashed ones.
86
+ * and recovers from crashed ones. Each harness's agent has its own topic.
82
87
  */
83
- export const appScheduler =
84
- process.env.VERCEL || process.env.A2_QUEUES
85
- ? vercelQueues({ topic: 'a2-a0' })
86
- : undefined
88
+ export const appSchedulers = {
89
+ 'ai-sdk': queued ? vercelQueues({ topic: 'a2-a0' }) : undefined,
90
+ fx: queued ? vercelQueues({ topic: 'a2-a0-fx' }) : undefined,
91
+ pi: queued ? vercelQueues({ topic: 'a2-a0-pi' }) : undefined,
92
+ }
87
93
 
88
- // Each step runs with the app's model choice. The browser pushes it, and the
89
- // contract's schema checks it on the way in.
90
- export const agentServer = agent({
94
+ /**
95
+ * The app's model choice for a step. The browser pushes it, and the
96
+ * contract's schema checks it on the way in.
97
+ */
98
+ function chosenModel({
99
+ harness,
100
+ event,
101
+ state,
102
+ }: {
103
+ harness: Harness
104
+ event: { sessionId: string }
105
+ state: { modelChoice: ModelChoice }
106
+ }) {
107
+ // Every step starts here, in the process that runs it, which is what
108
+ // the dev tools' crash has to end.
109
+ if (crashesAllowed) {
110
+ const appId = event.sessionId
111
+ waitUntil(
112
+ crashOnRequest({
113
+ appId,
114
+ app: agentServers[harness].session(appId),
115
+ log: crashServer.session(crashLogId(appId)),
116
+ }).catch(() => {}),
117
+ )
118
+ }
119
+ const { model, reasoning, fast } = state.modelChoice
120
+ return { model, reasoning, speed: fast ? 'fast' : 'standard' } as const
121
+ }
122
+
123
+ const shared = (harness: Harness) => ({
91
124
  ...(store ? { store } : {}),
92
- ...(appScheduler ? { scheduler: appScheduler } : {}),
93
- contract: appAgent,
94
- model: ({ event, state }) => {
95
- // Every step starts here, in the process that runs it, which is what
96
- // the dev tools' crash has to end.
97
- if (crashesAllowed) {
98
- const appId = event.sessionId
99
- waitUntil(
100
- crashOnRequest({
101
- appId,
102
- app: agentServer.session(appId),
103
- log: crashServer.session(crashLogId(appId)),
104
- }).catch(() => {}),
105
- )
106
- }
107
- const { model, reasoning, fast } = state.modelChoice
108
- return { model, reasoning, speed: fast ? 'fast' : 'standard' }
125
+ ...(appSchedulers[harness] ? { scheduler: appSchedulers[harness] } : {}),
126
+ contract: harnessAgents[harness],
127
+ maxSteps: 80,
128
+ })
129
+
130
+ export const agentServer = agent({
131
+ ...shared('ai-sdk'),
132
+ model: ({ event, state }) => chosenModel({ harness: 'ai-sdk', event, state }),
133
+ harness: {
134
+ type: 'ai-sdk',
135
+ generation: () => ({
136
+ // Claude leaves its thinking's text out unless asked for a summary.
137
+ providerOptions: {
138
+ anthropic: { thinking: { type: 'adaptive', display: 'summarized' } },
139
+ },
140
+ }),
109
141
  },
110
- generation: () => ({
111
- // Claude leaves its thinking's text out unless asked for a summary.
112
- providerOptions: {
113
- anthropic: { thinking: { type: 'adaptive', display: 'summarized' } },
114
- },
115
- }),
116
- instructions: INSTRUCTIONS,
142
+ instructions: instructions('through the bash tool'),
117
143
  tools,
118
- maxSteps: 80,
119
144
  })
120
145
 
121
- /** The app's event stream and pushes; only an app id names a session. */
122
- export function appEvents(request: Request): Promise<Response> {
123
- return agentServer.fetch(request, {
124
- // The agent refuses the AI protocol's server-authored events itself.
125
- authorize: (operation) => isAppId(operation.sessionId),
126
- })
146
+ /** FX's own loop, with the same tools and prompt. */
147
+ const fxServer = agent({
148
+ ...shared('fx'),
149
+ harness: fx(),
150
+ model: ({ event, state }) => chosenModel({ harness: 'fx', event, state }),
151
+ instructions: instructions('through the bash tool'),
152
+ tools,
153
+ })
154
+
155
+ /** Pi's own loop and tools, working in the app's sandbox, plus the preview. */
156
+ const piServer = agent({
157
+ ...shared('pi'),
158
+ harness: pi({
159
+ sandbox: async ({ session }) =>
160
+ sandboxSession(await appSandbox({ appId: session.id })),
161
+ }),
162
+ model: ({ event, state }) => chosenModel({ harness: 'pi', event, state }),
163
+ instructions: instructions('with your tools'),
164
+ tools: { show_preview: tools.show_preview },
165
+ })
166
+
167
+ export const agentServers = {
168
+ 'ai-sdk': agentServer,
169
+ fx: fxServer,
170
+ pi: piServer,
127
171
  }
128
172
 
173
+ /** An app's event stream and pushes, for one harness's agent; only an app id names a session. */
174
+ export const harnessEvents =
175
+ (harness: Harness) =>
176
+ (request: Request): Promise<Response> =>
177
+ agentServers[harness].fetch(request, {
178
+ // The agent refuses the AI protocol's server-authored events itself.
179
+ authorize: (operation) => isAppId(operation.sessionId),
180
+ })
181
+
182
+ export const appEvents = harnessEvents('ai-sdk')
183
+
129
184
  /** The apps sidebar's lists, which only the browser writes. */
130
185
  export const listServer = createServer({
131
186
  ...(store ? { store } : {}),
@@ -3,7 +3,7 @@ import { createClient } from 'experimental-a2/client'
3
3
  import { createReact } from 'experimental-a2/react'
4
4
  import { appList } from './apps'
5
5
  import { crashLog } from './crashes'
6
- import { appAgent } from './model'
6
+ import { appAgent, type Harness, harnessFrom } from './model'
7
7
 
8
8
  /** The dev tools' switch, `?limit=5s`, for the app at this URL, while they're open. */
9
9
  export function shortFunctions(appId: string): boolean {
@@ -15,17 +15,29 @@ export function shortFunctions(appId: string): boolean {
15
15
  )
16
16
  }
17
17
 
18
+ /** The harness the app at this URL runs, from `?harness=`. */
19
+ export function appHarness(appId: string): Harness {
20
+ return location.pathname === `/a0/${appId}`
21
+ ? harnessFrom(new URLSearchParams(location.search).get('harness'))
22
+ : 'ai-sdk'
23
+ }
24
+
25
+ const routes = (appId: string): string => {
26
+ const harness = appHarness(appId)
27
+ return harness === 'ai-sdk' ? '/a0' : `/a0/${harness}`
28
+ }
29
+
18
30
  /**
19
- * Follows an app's conversation log over the events route. Pushes go to a
20
- * 5-second function instead while the dev tools' switch is on.
31
+ * Follows an app's conversation log over its harness's events route. Pushes
32
+ * go to a 5-second function instead while the dev tools' switch is on.
21
33
  */
22
34
  export const appClient = createClient({
23
35
  contract: appAgent,
24
36
  api: {
25
37
  type: 'http',
26
- stream: '/a0/events',
38
+ stream: (appId) => `${routes(appId)}/events`,
27
39
  push: (appId) =>
28
- shortFunctions(appId) ? '/a0/short/events' : '/a0/events',
40
+ `${routes(appId)}${shortFunctions(appId) ? '/short' : ''}/events`,
29
41
  },
30
42
  })
31
43
 
@@ -4,6 +4,7 @@ import { createContext, useContext, useEffect, useState } from 'react'
4
4
  import { usePathname, useRouter, useSearchParams } from 'next/navigation'
5
5
  import { SiteNav } from '../components/site-header'
6
6
  import { type App, byLatestPrompt } from './apps'
7
+ import type { Harness } from './model'
7
8
  import { AppList } from './app-list'
8
9
  import { ColumnEdge, Columns } from './columns'
9
10
  import { listCookie, readCookie, type Widths, writeCookie } from './cookies'
@@ -13,7 +14,7 @@ import { useList } from './session'
13
14
  type Apps = {
14
15
  listed: (id: string) => App | null
15
16
  /** Records a prompt to an app, listing it under `name` the first time. */
16
- prompted: (app: { id: string; name: string }) => void
17
+ prompted: (app: { id: string; name: string; harness: Harness }) => void
17
18
  /** Makes a new app on screen the one "+" opens until it's prompted. */
18
19
  opened: (appId: string) => void
19
20
  }
@@ -85,12 +86,19 @@ export function A0Shell({
85
86
  apps.find((app) => app.id === id) ?? null
86
87
  const context: Apps = {
87
88
  listed,
88
- prompted: ({ id, name }) => {
89
+ prompted: ({ id, name, harness }) => {
89
90
  if (listed(id)) {
90
91
  write({ type: 'app.prompted', payload: { appId: id } })
91
92
  return
92
93
  }
93
- write({ type: 'app.created', payload: { appId: id, name } })
94
+ write({
95
+ type: 'app.created',
96
+ payload: {
97
+ appId: id,
98
+ name,
99
+ ...(harness === 'ai-sdk' ? {} : { harness }),
100
+ },
101
+ })
94
102
  if (id === ready) setReady(crypto.randomUUID())
95
103
  },
96
104
  opened: setReady,
@@ -19,9 +19,12 @@ const server = () => {
19
19
  contract: demoAgent,
20
20
  store: memory(),
21
21
  model: 'test/model',
22
- generate: () => {
23
- generations += 1
24
- return new ReadableStream({ start: (controller) => controller.close() })
22
+ harness: {
23
+ type: 'ai-sdk',
24
+ generate: () => {
25
+ generations += 1
26
+ return new ReadableStream({ start: (controller) => controller.close() })
27
+ },
25
28
  },
26
29
  })
27
30
  return { agentServer, generations: () => generations }
@@ -137,8 +137,15 @@ export const agentServer = agent({
137
137
  ...(agentScheduler ? { scheduler: agentScheduler } : {}),
138
138
  telemetry,
139
139
  model: ({ state }) => modelSettings(state.modelChoice),
140
- messageMetadata: traceMetadata,
141
- compaction: ({ state }) => compactionOptions(state.compactionSettings),
140
+ harness: {
141
+ type: 'ai-sdk',
142
+ messageMetadata: traceMetadata,
143
+ compaction: ({ state }) => compactionOptions(state.compactionSettings),
144
+ generation: () => ({
145
+ telemetry: modelTelemetry(),
146
+ toolApproval: { restartService: 'user-approval' },
147
+ }),
148
+ },
142
149
  tools,
143
150
  instructions: `You are an incident-response agent for the playground.
144
151
  When responding to an incident, inspect checkout-api before taking action. Its
@@ -154,8 +161,4 @@ it again.
154
161
 
155
162
  When the operator asks you to run a shell command, call bash. Make one bash call
156
163
  at a time and wait for its result before calling it again. Be concise.`,
157
- generation: () => ({
158
- telemetry: modelTelemetry(),
159
- toolApproval: { restartService: 'user-approval' },
160
- }),
161
164
  })
@@ -11,7 +11,15 @@ import { inputs } from 'experimental-a2/ai'
11
11
  import { agent } from 'experimental-a2/ai/server'
12
12
  import { memory } from 'experimental-a2/store-memory'
13
13
  import { otel } from 'experimental-a2/otel'
14
- import { afterAll, beforeAll, beforeEach, expect, it, vi } from 'vitest'
14
+ import {
15
+ afterAll,
16
+ beforeAll,
17
+ beforeEach,
18
+ expect,
19
+ it,
20
+ onTestFinished,
21
+ vi,
22
+ } from 'vitest'
15
23
  import { demoAgent } from './model'
16
24
  import { modelTelemetry, traceMetadata } from './telemetry'
17
25
  import { traceUrl } from './trace'
@@ -51,60 +59,73 @@ afterAll(async () => {
51
59
  it.each(['complete', 'stop'] as const)(
52
60
  'correlates standard SDK traces and releases spans when generation ends with %s',
53
61
  async (outcome) => {
54
- const server = agent({
55
- contract: demoAgent,
56
- store: memory(),
57
- telemetry: otel(),
58
- generation: { telemetry: modelTelemetry() },
59
- messageMetadata: traceMetadata,
60
- model: new MockLanguageModelV4({
61
- modelId: 'test-model',
62
- doStream: async ({ abortSignal }) => ({
63
- stream:
64
- outcome === 'complete'
65
- ? simulateReadableStream({
66
- chunks: [
67
- { type: 'stream-start', warnings: [] },
68
- { type: 'text-start', id: 'text' },
69
- { type: 'text-delta', id: 'text', delta: 'PRIVATE_OUTPUT' },
70
- { type: 'text-end', id: 'text' },
71
- {
72
- type: 'finish',
73
- finishReason: { unified: 'stop', raw: 'stop' },
74
- usage: {
75
- inputTokens: {
76
- total: 5,
77
- noCache: 5,
78
- cacheRead: 0,
79
- cacheWrite: 0,
80
- },
81
- outputTokens: { total: 2, text: 2, reasoning: 0 },
82
- },
83
- providerMetadata: {
84
- test: { secret: 'PRIVATE_METADATA' },
62
+ const model = new MockLanguageModelV4({
63
+ modelId: 'test-model',
64
+ doStream: async ({ abortSignal }) => ({
65
+ stream:
66
+ outcome === 'complete'
67
+ ? simulateReadableStream({
68
+ chunks: [
69
+ { type: 'stream-start', warnings: [] },
70
+ { type: 'text-start', id: 'text' },
71
+ { type: 'text-delta', id: 'text', delta: 'PRIVATE_OUTPUT' },
72
+ { type: 'text-end', id: 'text' },
73
+ {
74
+ type: 'finish',
75
+ finishReason: { unified: 'stop', raw: 'stop' },
76
+ usage: {
77
+ inputTokens: {
78
+ total: 5,
79
+ noCache: 5,
80
+ cacheRead: 0,
81
+ cacheWrite: 0,
85
82
  },
83
+ outputTokens: { total: 2, text: 2, reasoning: 0 },
84
+ },
85
+ providerMetadata: {
86
+ test: { secret: 'PRIVATE_METADATA' },
86
87
  },
87
- ],
88
- })
89
- : new ReadableStream({
90
- start(controller) {
91
- controller.enqueue({ type: 'stream-start', warnings: [] })
92
- controller.enqueue({ type: 'text-start', id: 'text' })
93
- controller.enqueue({
94
- type: 'text-delta',
95
- id: 'text',
96
- delta: 'PRIVATE_PARTIAL',
97
- })
98
- abortSignal?.addEventListener(
99
- 'abort',
100
- () => controller.close(),
101
- { once: true },
102
- )
103
88
  },
104
- }),
105
- }),
89
+ ],
90
+ })
91
+ : new ReadableStream({
92
+ start(controller) {
93
+ controller.enqueue({ type: 'stream-start', warnings: [] })
94
+ controller.enqueue({ type: 'text-start', id: 'text' })
95
+ controller.enqueue({
96
+ type: 'text-delta',
97
+ id: 'text',
98
+ delta: 'PRIVATE_PARTIAL',
99
+ })
100
+ abortSignal?.addEventListener(
101
+ 'abort',
102
+ () => controller.close(),
103
+ { once: true },
104
+ )
105
+ },
106
+ }),
106
107
  }),
107
108
  })
109
+ // Agents take AI Gateway model IDs; the AI SDK's global provider serves
110
+ // this one from the mock.
111
+ vi.stubGlobal('AI_SDK_DEFAULT_PROVIDER', {
112
+ specificationVersion: 'v4',
113
+ languageModel: () => model,
114
+ })
115
+ onTestFinished(() => {
116
+ vi.unstubAllGlobals()
117
+ })
118
+ const server = agent({
119
+ contract: demoAgent,
120
+ store: memory(),
121
+ telemetry: otel(),
122
+ harness: {
123
+ type: 'ai-sdk',
124
+ generation: { telemetry: modelTelemetry() },
125
+ messageMetadata: traceMetadata,
126
+ },
127
+ model: 'test-model',
128
+ })
108
129
  const session = server.session(`trace-${outcome}`)
109
130
  await session.append(
110
131
  ...inputs.message({
@@ -330,7 +330,7 @@ function CanvasClient({
330
330
 
331
331
  // A ghost is dead once the log has moved past it: show `dragging`
332
332
  // only while its `seen` frontier reaches the shape's durable
333
- // `movedAt` (/guides/presence#reconciling-the-planes-seen).
333
+ // `movedAt` (/client/presence#reconciling-the-planes-seen).
334
334
  const ghosts = new Map<
335
335
  string,
336
336
  { x: number; y: number; color: PaletteColor }
@@ -26,32 +26,36 @@ vi.mock('experimental-a2/ai/server', async (importActual) => {
26
26
  agent: (options: Parameters<typeof actual.agent>[0]) =>
27
27
  actual.agent({
28
28
  ...options,
29
- generate: ({
30
- event,
31
- messages,
32
- }: {
33
- event: { payload: { reason: string } }
34
- messages: { role: string; id: string }[]
35
- }) => {
36
- prompts.push(
37
- messages.map((message) => `${message.role}:${message.id}`).join(),
38
- )
39
- return step(
40
- event.payload.reason === 'message'
41
- ? [
42
- { type: 'start' },
43
- ...['Hello', 'there'].map(
44
- (message, call): UIMessageChunk => ({
45
- type: 'tool-input-available',
46
- toolCallId: `call-${call}`,
47
- toolName: 'post',
48
- input: { message },
49
- }),
50
- ),
51
- { type: 'finish', finishReason: 'tool-calls' },
52
- ]
53
- : [{ type: 'start' }, { type: 'finish', finishReason: 'stop' }],
54
- )
29
+ harness: {
30
+ ...(options.harness?.type === 'ai-sdk' ? options.harness : {}),
31
+ type: 'ai-sdk',
32
+ generate: ({
33
+ event,
34
+ messages,
35
+ }: {
36
+ event: { payload: { reason: string } }
37
+ messages: { role: string; id: string }[]
38
+ }) => {
39
+ prompts.push(
40
+ messages.map((message) => `${message.role}:${message.id}`).join(),
41
+ )
42
+ return step(
43
+ event.payload.reason === 'message'
44
+ ? [
45
+ { type: 'start' },
46
+ ...['Hello', 'there'].map(
47
+ (message, call): UIMessageChunk => ({
48
+ type: 'tool-input-available',
49
+ toolCallId: `call-${call}`,
50
+ toolName: 'post',
51
+ input: { message },
52
+ }),
53
+ ),
54
+ { type: 'finish', finishReason: 'tool-calls' },
55
+ ]
56
+ : [{ type: 'start' }, { type: 'finish', finishReason: 'stop' }],
57
+ )
58
+ },
55
59
  },
56
60
  } as never),
57
61
  }
@@ -61,57 +61,57 @@ Sound like a sharp, relaxed person in the chat, not a customer-support bot. Matc
61
61
  Only contribute when you have something useful, relevant, or friendly to add. It is fine to stay silent or stop whenever you have nothing else to say.
62
62
  To speak, call post once for each visible chat bubble. Never communicate through ordinary assistant text because it is private and is not shown in the room.
63
63
  Use as many post calls as the moment naturally calls for. Several short messages are welcome when they create better rhythm, separate distinct thoughts, or fulfill a request for a sequence. A single message is fine too. Do not spam. After post results, continue only when another message genuinely improves the conversation.`,
64
- generation: {
65
- maxOutputTokens: 500,
66
- temperature: 0.4,
64
+ harness: {
65
+ type: 'ai-sdk',
66
+ generation: {
67
+ maxOutputTokens: 500,
68
+ temperature: 0.4,
69
+ },
70
+ messageMetadata: (context) => {
71
+ const previous = context.messages.find(
72
+ (message) => message.id === context.event.payload.responseMessageId,
73
+ )?.metadata
74
+ const trigger = context.messages.find(
75
+ (message) => message.id === context.event.payload.messageId,
76
+ )?.metadata
77
+ const steps = previous?.kind === 'agent' ? previous.steps : []
78
+ const mode: ChatAgentMode | undefined =
79
+ previous?.kind === 'agent'
80
+ ? previous.mode
81
+ : trigger?.kind === 'participant' && trigger.mode !== 'passive'
82
+ ? trigger.mode
83
+ : undefined
84
+ if (mode === undefined) return undefined
85
+ const metadata = {
86
+ kind: 'agent' as const,
87
+ author: 'agent' as const,
88
+ mode,
89
+ steps,
90
+ }
91
+ if (context.part.type === 'start') return metadata
92
+ if (context.part.type !== 'finish') return undefined
93
+ return {
94
+ ...metadata,
95
+ steps: [
96
+ ...steps,
97
+ {
98
+ generationId: context.generationId,
99
+ model: context.model,
100
+ finishReason: context.part.finishReason,
101
+ ...(context.part.rawFinishReason === undefined
102
+ ? {}
103
+ : { rawFinishReason: context.part.rawFinishReason }),
104
+ usage: context.part.totalUsage,
105
+ completedAt: new Date().toISOString(),
106
+ },
107
+ ],
108
+ }
109
+ },
67
110
  },
68
111
  progress: {
69
112
  maxChunks: 4,
70
113
  maxDelayMs: 30,
71
114
  },
72
- messageMetadata: (context) => {
73
- const previous = context.messages.find(
74
- (message) => message.id === context.event.payload.responseMessageId,
75
- )?.metadata
76
- const trigger = context.messages.find(
77
- (message) => message.id === context.event.payload.messageId,
78
- )?.metadata
79
- const steps = previous?.kind === 'agent' ? previous.steps : []
80
- const mode: ChatAgentMode | undefined =
81
- previous?.kind === 'agent'
82
- ? previous.mode
83
- : trigger?.kind === 'participant' && trigger.mode !== 'passive'
84
- ? trigger.mode
85
- : undefined
86
- if (mode === undefined) return undefined
87
- const metadata = {
88
- kind: 'agent' as const,
89
- author: 'agent' as const,
90
- mode,
91
- steps,
92
- }
93
- if (context.part.type === 'start') return metadata
94
- if (context.part.type !== 'finish') return undefined
95
- return {
96
- ...metadata,
97
- steps: [
98
- ...steps,
99
- {
100
- generationId: context.generationId,
101
- model:
102
- typeof context.model === 'string'
103
- ? context.model
104
- : context.model.modelId,
105
- finishReason: context.part.finishReason,
106
- ...(context.part.rawFinishReason === undefined
107
- ? {}
108
- : { rawFinishReason: context.part.rawFinishReason }),
109
- usage: context.part.totalUsage,
110
- completedAt: new Date().toISOString(),
111
- },
112
- ],
113
- }
114
- },
115
115
  })
116
116
 
117
117
  /** The room. Each message goes on to the agent's session, marked for whether it should answer. */