experimental-a2 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
  4. package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.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-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
  11. package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
  12. package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
  13. package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.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 +692 -0
  22. package/dist/ai-fx.js.map +1 -0
  23. package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
  24. package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
  25. package/dist/ai-harness-BuLh_EYi.js +273 -0
  26. package/dist/ai-harness-BuLh_EYi.js.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 +950 -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 +113 -40
  39. package/dist/ai-server.js.map +1 -1
  40. package/dist/ai-tool-input-DmSqMUQi.js +40 -0
  41. package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
  42. package/dist/ai.d.ts +1 -1
  43. package/dist/ai.js +1 -1
  44. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  45. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  46. package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
  47. package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
  48. package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
  49. package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
  50. package/dist/client.d.ts +3 -3
  51. package/dist/client.js +1 -1
  52. package/dist/index.d.ts +5 -5
  53. package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
  54. package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
  55. package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
  56. package/dist/internal-CscSY7K_.js.map +1 -0
  57. package/dist/otel.d.ts +1 -1
  58. package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
  59. package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
  60. package/dist/react.d.ts +1 -1
  61. package/dist/react.js +1 -1
  62. package/dist/reducer-BAXc8JyF.js.map +1 -1
  63. package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
  64. package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
  65. package/dist/scheduler-qstash.d.ts +2 -2
  66. package/dist/scheduler-qstash.js +3 -3
  67. package/dist/scheduler-qstash.js.map +1 -1
  68. package/dist/scheduler-vercel.d.ts +7 -7
  69. package/dist/scheduler-vercel.d.ts.map +1 -1
  70. package/dist/scheduler-vercel.js +9 -10
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
  73. package/dist/server-D-Y4pqwu.js.map +1 -0
  74. package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
  75. package/dist/server-DYAATFuT.d.ts.map +1 -0
  76. package/dist/server.d.ts +4 -4
  77. package/dist/server.js +1 -1
  78. package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
  79. package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
  80. package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
  81. package/dist/store-CyLUy86v.d.ts.map +1 -0
  82. package/dist/store-N8PXxDAS.js.map +1 -1
  83. package/dist/store-memory.d.ts +1 -1
  84. package/dist/store-memory.js +56 -11
  85. package/dist/store-memory.js.map +1 -1
  86. package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
  87. package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.js.map} +1 -1
  88. package/dist/store-postgres.d.ts +1 -1
  89. package/dist/store-postgres.d.ts.map +1 -1
  90. package/dist/store-postgres.js +106 -25
  91. package/dist/store-postgres.js.map +1 -1
  92. package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
  93. package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
  94. package/dist/store-redis-http.d.ts +1 -1
  95. package/dist/store-redis-http.js +3 -3
  96. package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
  97. package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
  98. package/dist/store-redis.d.ts +1 -1
  99. package/dist/store-redis.js +1 -1
  100. package/dist/store-sqlite.d.ts +1 -1
  101. package/dist/store-sqlite.js +66 -21
  102. package/dist/store-sqlite.js.map +1 -1
  103. package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  104. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  105. package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
  106. package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
  107. package/docs/01-quickstart.mdx +49 -83
  108. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  109. package/docs/actors/01-introduction.mdx +48 -86
  110. package/docs/actors/02-concurrency.mdx +43 -70
  111. package/docs/actors/03-timers.mdx +28 -42
  112. package/docs/actors/04-routes.mdx +110 -150
  113. package/docs/actors/meta.ts +1 -1
  114. package/docs/agents/01-introduction.mdx +257 -0
  115. package/docs/agents/02-tools.mdx +247 -0
  116. package/docs/agents/03-conversations.mdx +538 -0
  117. package/docs/agents/04-harnesses.mdx +136 -0
  118. package/docs/agents/meta.ts +1 -0
  119. package/docs/client/01-react.mdx +294 -0
  120. package/docs/client/02-transports.mdx +86 -0
  121. package/docs/client/03-presence.mdx +224 -0
  122. package/docs/client/meta.ts +1 -0
  123. package/docs/contract/01-events.mdx +57 -0
  124. package/docs/contract/02-state.mdx +142 -0
  125. package/docs/contract/03-sessions.mdx +175 -0
  126. package/docs/index.mdx +109 -145
  127. package/docs/reference/01-core.mdx +139 -0
  128. package/docs/reference/02-server.mdx +860 -0
  129. package/docs/reference/03-client.mdx +301 -0
  130. package/docs/reference/04-ai.mdx +731 -0
  131. package/docs/reference/05-adapters.mdx +351 -0
  132. package/docs/reference/06-errors.mdx +138 -0
  133. package/docs/reference/meta.ts +1 -1
  134. package/docs/server/01-handlers.mdx +200 -0
  135. package/docs/server/02-timers.mdx +130 -0
  136. package/docs/server/03-cancellation.mdx +142 -0
  137. package/docs/server/04-routes.mdx +243 -0
  138. package/docs/server/05-production.mdx +323 -0
  139. package/docs/server/06-guarantees.mdx +132 -0
  140. package/examples/playground/DEPLOY.md +3 -5
  141. package/examples/playground/app/a0/[appId]/page.tsx +12 -13
  142. package/examples/playground/app/a0/app-list.tsx +9 -2
  143. package/examples/playground/app/a0/app-view.tsx +9 -1
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
  145. package/examples/playground/app/a0/apps.test.ts +12 -0
  146. package/examples/playground/app/a0/apps.ts +22 -7
  147. package/examples/playground/app/a0/chat.tsx +47 -1
  148. package/examples/playground/app/a0/devtools.tsx +2 -8
  149. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  150. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  151. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  152. package/examples/playground/app/a0/model.test.ts +18 -1
  153. package/examples/playground/app/a0/model.ts +47 -10
  154. package/examples/playground/app/a0/no-photon.cjs +4 -0
  155. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  156. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  157. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  158. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  159. package/examples/playground/app/a0/sandbox-log.ts +10 -4
  160. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  161. package/examples/playground/app/a0/scheduler/route.ts +2 -2
  162. package/examples/playground/app/a0/server.ts +98 -54
  163. package/examples/playground/app/a0/session.ts +17 -5
  164. package/examples/playground/app/a0/shell.tsx +11 -3
  165. package/examples/playground/app/agent/ingress.test.ts +6 -3
  166. package/examples/playground/app/agent/server.ts +9 -6
  167. package/examples/playground/app/agent/telemetry.test.ts +71 -50
  168. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
  169. package/examples/playground/app/chat/server.test.ts +30 -26
  170. package/examples/playground/app/chat/server.ts +46 -46
  171. package/examples/playground/next.config.ts +17 -5
  172. package/examples/playground/package.json +4 -1
  173. package/examples/playground/vercel.json +18 -0
  174. package/package.json +27 -3
  175. package/src/ai-agent.ts +271 -90
  176. package/src/ai-fx.browser.ts +5 -0
  177. package/src/ai-fx.ts +912 -0
  178. package/src/ai-harness.ts +518 -0
  179. package/src/ai-model-metadata.ts +11 -6
  180. package/src/ai-pi.browser.ts +5 -0
  181. package/src/ai-pi.ts +1322 -0
  182. package/src/ai-sdk-step.ts +2 -2
  183. package/src/ai-server.ts +3 -1
  184. package/src/ai-tool-input.ts +45 -0
  185. package/src/internal.ts +13 -0
  186. package/src/libfx.d.ts +34 -0
  187. package/src/reducer.ts +1 -1
  188. package/src/scheduler-qstash.ts +5 -5
  189. package/src/scheduler-vercel.ts +14 -15
  190. package/src/server.ts +233 -29
  191. package/src/store-memory.ts +92 -16
  192. package/src/store-postgres.ts +156 -25
  193. package/src/store-redis-core.ts +322 -110
  194. package/src/store-redis-notify.ts +2 -0
  195. package/src/store-sqlite.ts +121 -28
  196. package/src/store.ts +55 -3
  197. package/src/telemetry.ts +1 -0
  198. package/dist/internal-CbVvEwad.js.map +0 -1
  199. package/dist/server-CGM8WzRG.d.ts.map +0 -1
  200. package/dist/server-Rj64fNvX.js.map +0 -1
  201. package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
  202. package/dist/store-Gpkq7WBo.d.ts.map +0 -1
  203. package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
  204. package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
  205. package/docs/concepts/01-contracts.mdx +0 -237
  206. package/docs/concepts/02-handlers.mdx +0 -284
  207. package/docs/concepts/03-durability.mdx +0 -314
  208. package/docs/concepts/04-state.mdx +0 -209
  209. package/docs/guides/01-timers.mdx +0 -193
  210. package/docs/guides/02-cancellation.mdx +0 -161
  211. package/docs/guides/03-react.mdx +0 -324
  212. package/docs/guides/04-local-first.mdx +0 -47
  213. package/docs/guides/05-production.mdx +0 -534
  214. package/docs/guides/06-ai-agents.mdx +0 -1408
  215. package/docs/guides/08-application-data.mdx +0 -113
  216. package/docs/guides/09-presence.mdx +0 -262
  217. package/docs/guides/10-transports.mdx +0 -221
  218. package/docs/reference/01-api.mdx +0 -2358
  219. package/docs/reference/02-errors.mdx +0 -150
  220. /package/docs/{concepts → contract}/meta.ts +0 -0
  221. /package/docs/{guides → server}/meta.ts +0 -0
@@ -0,0 +1,152 @@
1
+ import 'server-only'
2
+ import type { Sandbox } from '@vercel/sandbox'
3
+ import type { Experimental_SandboxSession } from 'ai'
4
+ import { appDir } from './sandbox'
5
+
6
+ // A job the command backgrounds, like `next dev &`, keeps whatever output it
7
+ // started with, and a read of a pipe would wait on it forever. Output goes to
8
+ // a file instead; background jobs live on.
9
+ const CAPTURE = `out=$(mktemp); bash -lc "$0" > "$out" 2>&1 < /dev/null; code=$?; cat "$out"; rm -f "$out"; exit $code`
10
+
11
+ type Run = Parameters<Experimental_SandboxSession['run']>[0]
12
+
13
+ /**
14
+ * The app's Vercel Sandbox as an AI SDK sandbox session, for harnesses whose
15
+ * own tools need one. Commands run in the app's directory.
16
+ */
17
+ export function sandboxSession(sandbox: Sandbox): Experimental_SandboxSession {
18
+ const params = ({ command, workingDirectory, env, abortSignal }: Run) => ({
19
+ cmd: 'bash',
20
+ args: ['-c', CAPTURE, command],
21
+ cwd: workingDirectory ?? appDir,
22
+ ...(env ? { env } : {}),
23
+ ...(abortSignal ? { signal: abortSignal } : {}),
24
+ })
25
+ const readBinaryFile = async ({
26
+ path,
27
+ abortSignal,
28
+ }: {
29
+ path: string
30
+ abortSignal?: AbortSignal
31
+ }): Promise<Uint8Array | null> => {
32
+ const buffer = await sandbox.readFileToBuffer(
33
+ { path, cwd: appDir },
34
+ abortSignal ? { signal: abortSignal } : {},
35
+ )
36
+ return buffer && new Uint8Array(buffer)
37
+ }
38
+ // The sandbox extracts written files at its root, creating their directories.
39
+ const write = (path: string, content: Uint8Array, signal?: AbortSignal) =>
40
+ sandbox.writeFiles(
41
+ [{ path: path.startsWith('/') ? path : `${appDir}/${path}`, content }],
42
+ signal ? { signal } : {},
43
+ )
44
+
45
+ return {
46
+ description: `A Vercel Sandbox. The app is in ${appDir}.`,
47
+ // Streams, so a reader that stops early stops the download.
48
+ readFile: async ({ path, abortSignal }) => {
49
+ const stream = await sandbox.readFile(
50
+ { path, cwd: appDir },
51
+ abortSignal ? { signal: abortSignal } : {},
52
+ )
53
+ if (stream === null) return null
54
+ const chunks = stream[Symbol.asyncIterator]()
55
+ return new ReadableStream<Uint8Array>({
56
+ pull: async (controller) => {
57
+ const { done, value } = await chunks.next()
58
+ if (done) controller.close()
59
+ else
60
+ controller.enqueue(
61
+ typeof value === 'string'
62
+ ? new TextEncoder().encode(value)
63
+ : value,
64
+ )
65
+ },
66
+ cancel: async () => {
67
+ await chunks.return?.()
68
+ },
69
+ })
70
+ },
71
+ readBinaryFile,
72
+ readTextFile: async ({ encoding, startLine, endLine, ...options }) => {
73
+ const bytes = await readBinaryFile(options)
74
+ if (bytes === null) return null
75
+ const text = new TextDecoder(encoding).decode(bytes)
76
+ if (startLine === undefined && endLine === undefined) return text
77
+ return text
78
+ .split('\n')
79
+ .slice((startLine ?? 1) - 1, endLine)
80
+ .join('\n')
81
+ },
82
+ writeFile: async ({ path, content, abortSignal }) =>
83
+ write(
84
+ path,
85
+ new Uint8Array(await new Response(content).arrayBuffer()),
86
+ abortSignal,
87
+ ),
88
+ writeBinaryFile: ({ path, content, abortSignal }) =>
89
+ write(path, content, abortSignal),
90
+ writeTextFile: ({ path, content, abortSignal }) =>
91
+ write(path, new TextEncoder().encode(content), abortSignal),
92
+ run: async (options) => {
93
+ const finished = await sandbox.runCommand(params(options))
94
+ return {
95
+ exitCode: finished.exitCode,
96
+ stdout: await finished.stdout(),
97
+ stderr: await finished.stderr(),
98
+ }
99
+ },
100
+ spawn: async (options) => {
101
+ const { abortSignal } = options
102
+ const command = await sandbox.runCommand({
103
+ ...params(options),
104
+ detached: true,
105
+ })
106
+ // For a detached command the signal only cancels its start; aborting
107
+ // kills it, as the AI SDK's sandbox contract says.
108
+ const kill = () => void command.kill().catch(() => {})
109
+ if (abortSignal?.aborted) kill()
110
+ abortSignal?.addEventListener('abort', kill, { once: true })
111
+ const encoder = new TextEncoder()
112
+ let stdout!: ReadableStreamDefaultController<Uint8Array>
113
+ let stderr!: ReadableStreamDefaultController<Uint8Array>
114
+ const streams = {
115
+ stdout: new ReadableStream<Uint8Array>({
116
+ start: (c) => void (stdout = c),
117
+ }),
118
+ stderr: new ReadableStream<Uint8Array>({
119
+ start: (c) => void (stderr = c),
120
+ }),
121
+ }
122
+ // The command's log ends when it exits, and closes both streams.
123
+ void (async () => {
124
+ try {
125
+ for await (const line of command.logs())
126
+ (line.stream === 'stdout' ? stdout : stderr).enqueue(
127
+ encoder.encode(line.data),
128
+ )
129
+ stdout.close()
130
+ stderr.close()
131
+ } catch (error) {
132
+ stdout.error(error)
133
+ stderr.error(error)
134
+ }
135
+ })()
136
+ return {
137
+ ...streams,
138
+ wait: async () => {
139
+ try {
140
+ const finished = await command.wait(
141
+ abortSignal ? { signal: abortSignal } : {},
142
+ )
143
+ return { exitCode: finished.exitCode }
144
+ } finally {
145
+ abortSignal?.removeEventListener('abort', kill)
146
+ }
147
+ },
148
+ kill: () => command.kill(),
149
+ }
150
+ },
151
+ }
152
+ }
@@ -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,106 @@ 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
+ const appId = event.sessionId
110
+ waitUntil(
111
+ crashOnRequest({
112
+ appId,
113
+ app: agentServers[harness].session(appId),
114
+ log: crashServer.session(crashLogId(appId)),
115
+ }).catch(() => {}),
116
+ )
117
+ const { model, reasoning, fast } = state.modelChoice
118
+ return { model, reasoning, speed: fast ? 'fast' : 'standard' } as const
119
+ }
120
+
121
+ const shared = (harness: Harness) => ({
91
122
  ...(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' }
123
+ ...(appSchedulers[harness] ? { scheduler: appSchedulers[harness] } : {}),
124
+ contract: harnessAgents[harness],
125
+ maxSteps: 80,
126
+ })
127
+
128
+ export const agentServer = agent({
129
+ ...shared('ai-sdk'),
130
+ model: ({ event, state }) => chosenModel({ harness: 'ai-sdk', event, state }),
131
+ harness: {
132
+ type: 'ai-sdk',
133
+ generation: () => ({
134
+ // Claude leaves its thinking's text out unless asked for a summary.
135
+ providerOptions: {
136
+ anthropic: { thinking: { type: 'adaptive', display: 'summarized' } },
137
+ },
138
+ }),
109
139
  },
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,
140
+ instructions: instructions('through the bash tool'),
117
141
  tools,
118
- maxSteps: 80,
119
142
  })
120
143
 
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
- })
144
+ /** FX's own loop, with the same tools and prompt. */
145
+ const fxServer = agent({
146
+ ...shared('fx'),
147
+ harness: fx(),
148
+ model: ({ event, state }) => chosenModel({ harness: 'fx', event, state }),
149
+ instructions: instructions('through the bash tool'),
150
+ tools,
151
+ })
152
+
153
+ /** Pi's own loop and tools, working in the app's sandbox, plus the preview. */
154
+ const piServer = agent({
155
+ ...shared('pi'),
156
+ harness: pi({
157
+ sandbox: async ({ session }) =>
158
+ sandboxSession(await appSandbox({ appId: session.id })),
159
+ }),
160
+ model: ({ event, state }) => chosenModel({ harness: 'pi', event, state }),
161
+ instructions: instructions('with your tools'),
162
+ tools: { show_preview: tools.show_preview },
163
+ })
164
+
165
+ export const agentServers = {
166
+ 'ai-sdk': agentServer,
167
+ fx: fxServer,
168
+ pi: piServer,
127
169
  }
128
170
 
171
+ /** An app's event stream and pushes, for one harness's agent; only an app id names a session. */
172
+ export const harnessEvents =
173
+ (harness: Harness) =>
174
+ (request: Request): Promise<Response> =>
175
+ agentServers[harness].fetch(request, {
176
+ // The agent refuses the AI protocol's server-authored events itself.
177
+ authorize: (operation) => isAppId(operation.sessionId),
178
+ })
179
+
180
+ export const appEvents = harnessEvents('ai-sdk')
181
+
129
182
  /** The apps sidebar's lists, which only the browser writes. */
130
183
  export const listServer = createServer({
131
184
  ...(store ? { store } : {}),
@@ -138,20 +191,11 @@ export const crashServer = createServer({
138
191
  contract: crashLog,
139
192
  })
140
193
 
141
- /**
142
- * A crash ends the function instance, and anything else it's serving, so
143
- * only previews allow it, behind Deployment Protection, unless A0_CRASHES
144
- * is on. Under next dev it would take the dev server down.
145
- */
146
- export const crashesAllowed =
147
- process.env.VERCEL_ENV === 'preview' || process.env.A0_CRASHES === 'on'
148
-
149
194
  /** The browser can only ask for a crash; the process it ends records it. */
150
195
  export function crashEvents(request: Request): Promise<Response> {
151
196
  return crashServer.fetch(request, {
152
197
  authorize: (operation) =>
153
198
  isCrashLogId(operation.sessionId) &&
154
- (crashesAllowed || operation.type !== 'push') &&
155
199
  (operation.type !== 'push' ||
156
200
  operation.events.every((event) => event.type === 'crash.requested')),
157
201
  })
@@ -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 }