experimental-a2 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-Bjce9_5U.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-Bjce9_5U.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-UrWfKeuv.js → ai-BpOGm-zB.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-BpOGm-zB.js.map} +1 -1
- package/dist/{ai-vpGtcaCB.d.ts → ai-DrUaMyRt.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-DrUaMyRt.d.ts.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 +634 -0
- package/dist/ai-fx.js.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-harness-DvsPeJeB.d.ts +26 -0
- package/dist/ai-harness-DvsPeJeB.d.ts.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 +904 -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 +106 -37
- package/dist/ai-server.js.map +1 -1
- 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-BReKqwZC.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BReKqwZC.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-B2T3U4Rq.js} +8 -2
- package/dist/internal-B2T3U4Rq.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 +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-Rj64fNvX.js → server-Dw_k_7kO.js} +93 -60
- package/dist/server-Dw_k_7kO.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-En7cbi3v.d.ts} +36 -7
- package/dist/server-En7cbi3v.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-Bo-XTFEy.d.ts} +2 -2
- package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-Djdv0bnZ.d.ts} +31 -2
- package/dist/store-Djdv0bnZ.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 +32 -6
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-B5FcwJfo.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-B5FcwJfo.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +54 -3
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-BAwS8RWn.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-BAwS8RWn.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-BRITti_o.js} +187 -68
- package/dist/store-redis-notify-BRITti_o.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 +32 -2
- 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-rSUglYo8.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-rSUglYo8.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 +108 -144
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +855 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +722 -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 +141 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +322 -0
- package/docs/server/06-guarantees.mdx +123 -0
- package/examples/playground/DEPLOY.md +1 -1
- package/examples/playground/app/a0/[appId]/page.tsx +11 -6
- 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/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 +100 -45
- 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 +261 -87
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +833 -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 +1263 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/internal.ts +7 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/server.ts +177 -19
- package/src/store-memory.ts +42 -7
- package/src/store-postgres.ts +80 -0
- package/src/store-redis-core.ts +210 -79
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +60 -0
- package/src/store.ts +33 -0
- 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,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, crashesAllowed } 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,24 +17,29 @@ 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}
|
|
42
|
+
harness={harness}
|
|
38
43
|
devtools={
|
|
39
44
|
<DevTools
|
|
40
45
|
appId={appId}
|
|
@@ -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"
|
|
@@ -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
|
})
|
|
@@ -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
|
+
}
|