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
@@ -3,7 +3,7 @@
3
3
  The event log runs with zero setup in development (SQLite at `.a2/dev.db`,
4
4
  no scheduler). The Agent demo and A0 also need AI Gateway and Vercel Sandbox
5
5
  credentials. Production needs the deliberate pieces from
6
- `node_modules/experimental-a2/docs/guides/05-production.mdx`.
6
+ `node_modules/experimental-a2/docs/server/05-production.mdx`.
7
7
 
8
8
  ## 1. A store
9
9
 
@@ -118,10 +118,8 @@ cookie.
118
118
 
119
119
  A0's dev tools have a Crash button, which ends the function instance running
120
120
  an app's agent, mid-turn, so the scheduler resumes the turn in a fresh one.
121
- Crashing ends everything else that instance is serving, so only preview
122
- deployments allow it, behind Deployment Protection. Set `A0_CRASHES=on` to
123
- allow it anywhere else. Under `next dev` that takes the dev server down with
124
- it, so it's for trying recovery, not for everyday local runs.
121
+ Crashing ends everything else that instance is serving, and under `next dev`
122
+ it takes the dev server down with it.
125
123
 
126
124
  ## 6. WebSockets for the canvas
127
125
 
@@ -3,8 +3,8 @@ import { notFound } from 'next/navigation'
3
3
  import { AppView } from '../app-view'
4
4
  import { isAppId } from '../apps'
5
5
  import { DevTools } from '../devtools'
6
- import { logPage } from '../model'
7
- import { agentServer, crashesAllowed } from '../server'
6
+ import { harnessFrom, logPage } from '../model'
7
+ import { agentServers } from '../server'
8
8
  import { SessionProvider } from '../session'
9
9
 
10
10
  /** An app, from its current state; the provider follows its log from there. */
@@ -17,31 +17,30 @@ export default async function A0App({
17
17
  }): Promise<ReactNode> {
18
18
  const { appId } = await params
19
19
  if (!isAppId(appId)) notFound()
20
- const initial = await agentServer.session(appId).state()
20
+ const query = await searchParams
21
+ const harness = harnessFrom(query.harness)
22
+ const session = agentServers[harness].session(appId)
23
+ const initial = await session.state()
21
24
  // Open dev tools show the log itself, starting with its latest page.
22
25
  const events =
23
- (await searchParams).devtools !== undefined && initial.index > 0
24
- ? await agentServer.session(appId).history({
26
+ query.devtools !== undefined && initial.index > 0
27
+ ? await session.history({
25
28
  gte: Math.max(1, initial.index - logPage + 1),
26
29
  lte: initial.index,
27
30
  })
28
31
  : undefined
29
32
  return (
30
33
  <SessionProvider
31
- key={appId}
34
+ // Another harness is another log, followed over its own route.
35
+ key={`${harness}:${appId}`}
32
36
  sessionId={appId}
33
37
  initial={initial}
34
38
  initialEvents={events}
35
39
  >
36
40
  <AppView
37
41
  appId={appId}
38
- devtools={
39
- <DevTools
40
- appId={appId}
41
- deployed={!!process.env.VERCEL}
42
- crashesAllowed={crashesAllowed}
43
- />
44
- }
42
+ harness={harness}
43
+ devtools={<DevTools appId={appId} deployed={!!process.env.VERCEL} />}
45
44
  />
46
45
  </SessionProvider>
47
46
  )
@@ -35,9 +35,16 @@ export function AppList({
35
35
  onDelete: (app: App) => void
36
36
  }): ReactNode {
37
37
  const now = useNow(renderedAt)
38
- // Links keep the dev tools' state from app to app.
38
+ // Links keep the dev tools' state from app to app. A new app starts with
39
+ // the open app's harness; a listed one keeps its own.
39
40
  const params = useSearchParams()
40
41
  const query = queryString(new URLSearchParams(params))
42
+ const appQuery = (app: App): string => {
43
+ const own = new URLSearchParams(params)
44
+ own.delete('harness')
45
+ if (app.harness) own.set('harness', app.harness)
46
+ return queryString(own)
47
+ }
41
48
  // Open dev tools also read each page's log, so they don't prefetch every
42
49
  // app's in full.
43
50
  const prefetch = params.has('devtools') ? null : true
@@ -63,7 +70,7 @@ export function AppList({
63
70
  app={app}
64
71
  when={timeAgo({ at: app.promptedAt, now })}
65
72
  current={app.id === currentId}
66
- href={`/a0/${app.id}${query}`}
73
+ href={`/a0/${app.id}${appQuery(app)}`}
67
74
  prefetch={prefetch}
68
75
  onRename={(name) => onRename({ id: app.id, name })}
69
76
  onDelete={() => onDelete(app)}
@@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'
5
5
  import { titleFromPrompt } from './apps'
6
6
  import { Chat } from './chat'
7
7
  import { ColumnEdge } from './columns'
8
+ import type { Harness } from './model'
8
9
  import { checkApp, PreviewPane } from './preview-pane'
9
10
  import { useSession } from './session'
10
11
  import { useApps } from './shell'
@@ -12,9 +13,11 @@ import { useApps } from './shell'
12
13
  /** One app: its chat with the agent, and the app it builds. */
13
14
  export function AppView({
14
15
  appId,
16
+ harness,
15
17
  devtools,
16
18
  }: {
17
19
  appId: string
20
+ harness: Harness
18
21
  /** Shown while the query has `devtools`. */
19
22
  devtools: ReactNode
20
23
  }): ReactNode {
@@ -41,8 +44,13 @@ export function AppView({
41
44
  <section aria-label="Chat" className="a0-chat">
42
45
  <h1 className="a0-title">{name}</h1>
43
46
  <Chat
47
+ harness={harness}
44
48
  onPrompt={(text) =>
45
- prompted({ id: appId, name: titleFromPrompt(prompt ?? text) })
49
+ prompted({
50
+ id: appId,
51
+ name: titleFromPrompt(prompt ?? text),
52
+ harness,
53
+ })
46
54
  }
47
55
  autoFocus={!app}
48
56
  />
@@ -1,7 +1,7 @@
1
1
  import { inputs } from 'experimental-a2/ai'
2
2
  import { isAppId } from '../../apps'
3
3
  import { deleteAppSandbox } from '../../sandbox'
4
- import { agentServer } from '../../server'
4
+ import { agentServers } from '../../server'
5
5
 
6
6
  /**
7
7
  * Stops the agent's current turn, so no later tool call brings the sandbox
@@ -14,10 +14,15 @@ export async function DELETE(
14
14
  const { appId } = await context.params
15
15
  if (!isAppId(appId))
16
16
  return Response.json({ error: 'Unknown app.' }, { status: 404 })
17
- const session = agentServer.session(appId)
18
- const { state } = await session.state()
19
- if (state.active)
20
- await session.append(...inputs.stop({ turnId: state.active.turnId }))
17
+ // The app's agent is whichever harness's agent has its log.
18
+ await Promise.all(
19
+ Object.values(agentServers).map(async (server) => {
20
+ const session = server.session(appId)
21
+ const { state } = await session.state()
22
+ if (state.active)
23
+ await session.append(...inputs.stop({ turnId: state.active.turnId }))
24
+ }),
25
+ )
21
26
  await deleteAppSandbox({ appId })
22
27
  return Response.json({ ok: true })
23
28
  }
@@ -114,3 +114,15 @@ it('reads the widths someone dragged to, and ignores bad ones', () => {
114
114
  chat: 480,
115
115
  })
116
116
  })
117
+
118
+ it("keeps the harness an app's agent runs, for its link", () => {
119
+ expect(
120
+ fold(
121
+ ['app.created', { appId: first, name: 'todo app', harness: 'fx' }, 1_000],
122
+ ['app.created', { appId: second, name: 'counter' }, 2_000],
123
+ ),
124
+ ).toEqual([
125
+ { id: first, name: 'todo app', promptedAt: 1_000, harness: 'fx' },
126
+ { id: second, name: 'counter', promptedAt: 2_000 },
127
+ ])
128
+ })
@@ -1,14 +1,21 @@
1
1
  import { contract } from 'experimental-a2'
2
2
  import { z } from 'zod'
3
+ import { type Harness, harnessSchema } from './model'
3
4
 
4
5
  export const isAppId = (value: string): boolean =>
5
6
  z.uuid().safeParse(value).success
6
7
 
7
8
  /**
8
9
  * An app is one conversation with the coding agent and one sandbox. It's
9
- * listed with the time of its latest prompt, as the server recorded it.
10
+ * listed with the time of its latest prompt, as the server recorded it, and
11
+ * the harness its agent runs when that's not the AI SDK's.
10
12
  */
11
- export type App = { id: string; name: string; promptedAt: number }
13
+ export type App = {
14
+ id: string
15
+ name: string
16
+ promptedAt: number
17
+ harness?: Harness
18
+ }
12
19
 
13
20
  const appId = z.uuid()
14
21
  const name = z.string().trim().min(1).max(80)
@@ -21,7 +28,11 @@ const name = z.string().trim().min(1).max(80)
21
28
  export const appList = contract({
22
29
  name: 'a0-apps',
23
30
  events: {
24
- 'app.created': z.object({ appId, name }),
31
+ 'app.created': z.object({
32
+ appId,
33
+ name,
34
+ harness: harnessSchema.optional(),
35
+ }),
25
36
  'app.prompted': z.object({ appId }),
26
37
  'app.renamed': z.object({ appId, name }),
27
38
  'app.deleted': z.object({ appId }),
@@ -33,10 +44,14 @@ export const appList = contract({
33
44
  const { appId: id } = event.payload
34
45
  const promptedAt = event.createdAt.getTime()
35
46
  switch (event.type) {
36
- case 'app.created':
37
- return apps.some((app) => app.id === id)
38
- ? apps
39
- : [...apps, { id, name: event.payload.name, promptedAt }]
47
+ case 'app.created': {
48
+ if (apps.some((app) => app.id === id)) return apps
49
+ const { name: created, harness } = event.payload
50
+ return [
51
+ ...apps,
52
+ { id, name: created, promptedAt, ...(harness ? { harness } : {}) },
53
+ ]
54
+ }
40
55
  case 'app.prompted':
41
56
  return apps.map((app) =>
42
57
  app.id === id ? { ...app, promptedAt } : app,
@@ -1,10 +1,15 @@
1
1
  'use client'
2
2
  import type { ReactNode } from 'react'
3
3
  import { useEffect, useRef, useState } from 'react'
4
+ import { useRouter, useSearchParams } from 'next/navigation'
4
5
  import { Asterisk, LoaderCircle, Zap } from 'lucide-react'
5
6
  import { inputs } from 'experimental-a2/ai'
7
+ import { queryString } from './flags'
6
8
  import {
7
9
  type AppMessage,
10
+ type Harness,
11
+ harnessLabels,
12
+ harnessSchema,
8
13
  type ModelChoice,
9
14
  reasoningSchema,
10
15
  toolLabels,
@@ -22,13 +27,25 @@ const suggestions = [
22
27
 
23
28
  /** The coding agent's conversation: its words, and one line per tool step. */
24
29
  export function Chat({
30
+ harness,
25
31
  onPrompt,
26
32
  autoFocus,
27
33
  }: {
34
+ harness: Harness
28
35
  onPrompt: (prompt: string) => void
29
36
  autoFocus: boolean
30
37
  }): ReactNode {
31
38
  const { state, push } = useSession()
39
+ const router = useRouter()
40
+ const params = useSearchParams()
41
+ // Each harness's agent has its own log, so an app picks one before its
42
+ // first message: the URL names it, and the page follows that agent.
43
+ const pickHarness = (next: Harness): void => {
44
+ const query = new URLSearchParams(params)
45
+ if (next === 'ai-sdk') query.delete('harness')
46
+ else query.set('harness', next)
47
+ router.replace(`${location.pathname}${queryString(query)}`)
48
+ }
32
49
  const [error, setError] = useState<string | null>(null)
33
50
  // A change here applies when the next message goes out with it, so typing a
34
51
  // model id doesn't record a choice per keystroke. Until then, the composer
@@ -133,6 +150,8 @@ export function Chat({
133
150
  queued={state.inbox.items.length}
134
151
  settings={settings}
135
152
  onSettings={setChosen}
153
+ harness={harness}
154
+ onHarness={state.messages.length ? null : pickHarness}
136
155
  autoFocus={autoFocus}
137
156
  />
138
157
  </>
@@ -179,7 +198,10 @@ function MessagePart({
179
198
  const name = part.type.slice('tool-'.length)
180
199
  const done = part.state === 'output-available'
181
200
  const failed = part.state === 'output-error'
182
- const detail = (part as { input?: { command?: unknown } }).input?.command
201
+ // A command, or the file one of Pi's tools works on.
202
+ const input = (part as { input?: { command?: unknown; path?: unknown } })
203
+ .input
204
+ const detail = input?.command ?? input?.path
183
205
  const took = (part as { output?: { durationMs?: unknown } }).output
184
206
  ?.durationMs
185
207
  const label =
@@ -220,6 +242,8 @@ function Composer({
220
242
  queued,
221
243
  settings,
222
244
  onSettings,
245
+ harness,
246
+ onHarness,
223
247
  autoFocus,
224
248
  }: {
225
249
  onSend: (text: string) => Promise<void>
@@ -228,6 +252,9 @@ function Composer({
228
252
  queued: number
229
253
  settings: ModelChoice
230
254
  onSettings: (settings: ModelChoice) => void
255
+ harness: Harness
256
+ /** Until the app's first message; it keeps its harness after that. */
257
+ onHarness: ((harness: Harness) => void) | null
231
258
  autoFocus: boolean
232
259
  }): ReactNode {
233
260
  const [text, setText] = useState('')
@@ -297,6 +324,25 @@ function Composer({
297
324
  />
298
325
  <div className="a0-composer-bar">
299
326
  <div className="a0-composer-model">
327
+ <select
328
+ aria-label="Harness"
329
+ title={
330
+ onHarness
331
+ ? "The agent's loop: the AI SDK's, FX's, or Pi's"
332
+ : 'The agent loop this app runs'
333
+ }
334
+ value={harness}
335
+ disabled={!onHarness}
336
+ onChange={(event) =>
337
+ onHarness?.(harnessSchema.parse(event.target.value))
338
+ }
339
+ >
340
+ {harnessSchema.options.map((option) => (
341
+ <option key={option} value={option}>
342
+ {harnessLabels[option]}
343
+ </option>
344
+ ))}
345
+ </select>
300
346
  <input
301
347
  aria-label="Model"
302
348
  title="The Gateway model the next request runs with"
@@ -20,12 +20,10 @@ const gap = (ms: number): string =>
20
20
  export function DevTools({
21
21
  appId,
22
22
  deployed,
23
- crashesAllowed,
24
23
  }: {
25
24
  appId: string
26
25
  /** Function limits and crashes only mean something on Vercel. */
27
26
  deployed: boolean
28
- crashesAllowed: boolean
29
27
  }): ReactNode {
30
28
  const { state, events, index, history, hydrated, loadHistory } = useSession()
31
29
  const short = useSearchParams().get('limit') === '5s'
@@ -83,12 +81,8 @@ export function DevTools({
83
81
  <button
84
82
  type="button"
85
83
  className="a0-ghost a0-devtools-crash"
86
- disabled={!crashesAllowed || crashes.state}
87
- title={
88
- crashesAllowed
89
- ? 'Ends the process running the agent, right now'
90
- : 'Crashes need a preview deployment, or A0_CRASHES=on'
91
- }
84
+ disabled={crashes.state}
85
+ title="Ends the process running the agent, right now"
92
86
  onClick={crash}
93
87
  >
94
88
  <Zap size={13} aria-hidden />
@@ -0,0 +1,9 @@
1
+ import { harnessEvents } from '../../server'
2
+
3
+ /** The events of apps whose agent runs FX's loop. */
4
+ const events = harnessEvents('fx')
5
+
6
+ export { events as GET, events as POST }
7
+
8
+ // Agent work started by a push runs after the response, up to this long.
9
+ export const maxDuration = 300
@@ -0,0 +1,7 @@
1
+ import { agentServers, appSchedulers } from '../../server'
2
+
3
+ export const POST =
4
+ appSchedulers.fx?.handler(agentServers.fx) ??
5
+ (() => new Response('a0 fx scheduler is not configured', { status: 503 }))
6
+
7
+ export const maxDuration = 300
@@ -0,0 +1,11 @@
1
+ import { harnessEvents } from '../../../server'
2
+
3
+ /**
4
+ * Pushes from the dev tools' 5-second switch. Agent work they start stops
5
+ * with the function, and the scheduler resumes it in a full-length one.
6
+ */
7
+ const events = harnessEvents('fx')
8
+
9
+ export { events as POST }
10
+
11
+ export const maxDuration = 5
@@ -1,6 +1,11 @@
1
1
  import type { ContractEvent } from 'experimental-a2'
2
2
  import { expect, it } from 'vitest'
3
- import { appAgent, defaultModelChoice } from './model'
3
+ import {
4
+ appAgent,
5
+ defaultModelChoice,
6
+ harnessAgents,
7
+ harnessFrom,
8
+ } from './model'
4
9
 
5
10
  const chosen = (payload: unknown): ContractEvent<typeof appAgent.events> =>
6
11
  ({
@@ -54,3 +59,15 @@ it('fills in defaults and rejects choices a browser could write badly', () => {
54
59
  ])
55
60
  expect(validate(payload)).toHaveProperty('issues')
56
61
  })
62
+
63
+ it("reads the app's harness from its URL, the AI SDK's by default", () => {
64
+ expect(harnessFrom('fx')).toBe('fx')
65
+ expect(harnessFrom(null)).toBe('ai-sdk')
66
+ expect(harnessFrom('other')).toBe('ai-sdk')
67
+ // Each harness's agent keeps its own logs.
68
+ expect(Object.values(harnessAgents).map((agent) => agent.name)).toEqual([
69
+ 'a0',
70
+ 'a0-fx',
71
+ 'a0-pi',
72
+ ])
73
+ })
@@ -35,24 +35,61 @@ export const defaultModelChoice: ModelChoice = {
35
35
 
36
36
  export type AppMessage = UIMessage
37
37
 
38
+ /**
39
+ * The loop an app's agent runs: the AI SDK's, FX's, or Pi's. An
40
+ * app picks it before its first message and keeps it.
41
+ */
42
+ export const harnessSchema = z.enum(['ai-sdk', 'fx', 'pi'])
43
+
44
+ export type Harness = z.output<typeof harnessSchema>
45
+
46
+ export const harnessLabels: Record<Harness, string> = {
47
+ 'ai-sdk': 'AI SDK',
48
+ fx: 'FX',
49
+ pi: 'Pi',
50
+ }
51
+
52
+ /** The harness an app's URL names, `?harness=fx`; the AI SDK's without one. */
53
+ export const harnessFrom = (value: unknown): Harness =>
54
+ harnessSchema.safeParse(value).data ?? 'ai-sdk'
55
+
56
+ const appContract = (name: string) =>
57
+ agentContract({
58
+ name,
59
+ events: { 'model.chosen': modelChoiceSchema },
60
+ reducer: {
61
+ name: 'a0.model-choice.v1',
62
+ initialState: { modelChoice: defaultModelChoice },
63
+ fold: (state, event) =>
64
+ event.type === 'model.chosen' ? { modelChoice: event.payload } : state,
65
+ },
66
+ })
67
+
38
68
  /** The coding agent's contract, shared by the server and the browser. */
39
- export const appAgent = agentContract({
40
- name: 'a0',
41
- events: { 'model.chosen': modelChoiceSchema },
42
- reducer: {
43
- name: 'a0.model-choice.v1',
44
- initialState: { modelChoice: defaultModelChoice },
45
- fold: (state, event) =>
46
- event.type === 'model.chosen' ? { modelChoice: event.payload } : state,
47
- },
48
- })
69
+ export const appAgent = appContract('a0')
70
+
71
+ /**
72
+ * Each harness's agent keeps its own logs. They differ only in name, so the
73
+ * browser follows any of them with `appAgent`.
74
+ */
75
+ export const harnessAgents = {
76
+ 'ai-sdk': appAgent,
77
+ fx: appContract('a0-fx'),
78
+ pi: appContract('a0-pi'),
79
+ } satisfies Record<Harness, typeof appAgent>
49
80
 
50
81
  export const toolLabels: Record<string, string> = {
51
82
  bash: 'Running a command',
52
83
  show_preview: 'Showing the app',
84
+ // Pi's own tools.
85
+ read: 'Reading a file',
86
+ write: 'Writing a file',
87
+ edit: 'Editing a file',
53
88
  }
54
89
 
55
90
  /** While the model is still writing a call's input. */
56
91
  export const writingLabels: Record<string, string> = {
57
92
  bash: 'Writing a command',
93
+ write: 'Writing a file',
94
+ edit: 'Writing an edit',
58
95
  }
@@ -0,0 +1,4 @@
1
+ // Stands in for @silvia-odwyer/photon-node, whose WebAssembly the playground's
2
+ // bundler cannot trace. Pi treats a photon that fails to load as no image
3
+ // processing.
4
+ throw new Error('photon is not available in the playground')
@@ -0,0 +1,3 @@
1
+ // Stands in for quickjs-wasi, whose WebAssembly the playground's bundler
2
+ // cannot trace. Only Pi's codemode extension imports it, and A0 never starts it.
3
+ module.exports = {}
@@ -0,0 +1,9 @@
1
+ import { harnessEvents } from '../../server'
2
+
3
+ /** The events of apps whose agent runs Pi's loop. */
4
+ const events = harnessEvents('pi')
5
+
6
+ export { events as GET, events as POST }
7
+
8
+ // Agent work started by a push runs after the response, up to this long.
9
+ export const maxDuration = 300
@@ -0,0 +1,7 @@
1
+ import { agentServers, appSchedulers } from '../../server'
2
+
3
+ export const POST =
4
+ appSchedulers.pi?.handler(agentServers.pi) ??
5
+ (() => new Response('a0 pi scheduler is not configured', { status: 503 }))
6
+
7
+ export const maxDuration = 300
@@ -0,0 +1,11 @@
1
+ import { harnessEvents } from '../../../server'
2
+
3
+ /**
4
+ * Pushes from the dev tools' 5-second switch. Agent work they start stops
5
+ * with the function, and the scheduler resumes it in a full-length one.
6
+ */
7
+ const events = harnessEvents('pi')
8
+
9
+ export { events as POST }
10
+
11
+ export const maxDuration = 5
@@ -6,7 +6,7 @@ import { z } from 'zod'
6
6
  import { store } from '@/lib/store'
7
7
  import { isAppId } from './apps'
8
8
  import { deleteAppSandbox } from './sandbox'
9
- import { agentServer } from './server'
9
+ import { agentServers } from './server'
10
10
 
11
11
  // How long a new app's sandbox waits for a first prompt.
12
12
  const IDLE_GRACE_MS = 10 * 60 * 1000
@@ -38,9 +38,15 @@ export const sandboxServer = createServer({
38
38
  handlers: {
39
39
  'sandbox.idle.checked': async ({ event }) => {
40
40
  const { appId } = event.payload
41
- // A prompt since the sandbox opened makes it a real app's.
42
- const { state } = await agentServer.session(appId).state()
43
- if (state.messages.length === 0) await deleteAppSandbox({ appId })
41
+ // A prompt since the sandbox opened makes it a real app's, whichever
42
+ // harness's agent got it.
43
+ const reads = await Promise.all(
44
+ Object.values(agentServers).map((server) =>
45
+ server.session(appId).state(),
46
+ ),
47
+ )
48
+ if (reads.every(({ state }) => state.messages.length === 0))
49
+ await deleteAppSandbox({ appId })
44
50
  },
45
51
  },
46
52
  })