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.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
- package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.map} +1 -1
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +692 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
- package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +950 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +47 -25
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +113 -40
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
- package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
- package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
- package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
- package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
- package/dist/internal-CscSY7K_.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
- package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/reducer-BAXc8JyF.js.map +1 -1
- package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
- package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +9 -10
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
- package/dist/server-DYAATFuT.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
- package/dist/store-CyLUy86v.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +56 -11
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +106 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
- package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +66 -21
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
- package/docs/01-quickstart.mdx +49 -83
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -86
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -150
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +109 -145
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +860 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +731 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +142 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +323 -0
- package/docs/server/06-guarantees.mdx +132 -0
- package/examples/playground/DEPLOY.md +3 -5
- package/examples/playground/app/a0/[appId]/page.tsx +12 -13
- package/examples/playground/app/a0/app-list.tsx +9 -2
- package/examples/playground/app/a0/app-view.tsx +9 -1
- package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
- package/examples/playground/app/a0/apps.test.ts +12 -0
- package/examples/playground/app/a0/apps.ts +22 -7
- package/examples/playground/app/a0/chat.tsx +47 -1
- package/examples/playground/app/a0/devtools.tsx +2 -8
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/model.test.ts +18 -1
- package/examples/playground/app/a0/model.ts +47 -10
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/sandbox-log.ts +10 -4
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/scheduler/route.ts +2 -2
- package/examples/playground/app/a0/server.ts +98 -54
- package/examples/playground/app/a0/session.ts +17 -5
- package/examples/playground/app/a0/shell.tsx +11 -3
- package/examples/playground/app/agent/ingress.test.ts +6 -3
- package/examples/playground/app/agent/server.ts +9 -6
- package/examples/playground/app/agent/telemetry.test.ts +71 -50
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +30 -26
- package/examples/playground/app/chat/server.ts +46 -46
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +4 -1
- package/examples/playground/vercel.json +18 -0
- package/package.json +27 -3
- package/src/ai-agent.ts +271 -90
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +912 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1322 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/ai-tool-input.ts +45 -0
- package/src/internal.ts +13 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +14 -15
- package/src/server.ts +233 -29
- package/src/store-memory.ts +92 -16
- package/src/store-postgres.ts +156 -25
- package/src/store-redis-core.ts +322 -110
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +121 -28
- package/src/store.ts +55 -3
- package/src/telemetry.ts +1 -0
- package/dist/internal-CbVvEwad.js.map +0 -1
- package/dist/server-CGM8WzRG.d.ts.map +0 -1
- package/dist/server-Rj64fNvX.js.map +0 -1
- package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
- package/dist/store-Gpkq7WBo.d.ts.map +0 -1
- package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
- package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -237
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -193
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -324
- package/docs/guides/04-local-first.mdx +0 -47
- package/docs/guides/05-production.mdx +0 -534
- package/docs/guides/06-ai-agents.mdx +0 -1408
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -262
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -2358
- package/docs/reference/02-errors.mdx +0 -150
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /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/
|
|
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,
|
|
122
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
24
|
-
? await
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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}${
|
|
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({
|
|
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 {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 = {
|
|
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({
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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={
|
|
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,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 {
|
|
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 =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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,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,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 {
|
|
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
|
-
|
|
43
|
-
|
|
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
|
})
|