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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
import type { Sandbox } from '@vercel/sandbox'
|
|
3
|
+
import type { Experimental_SandboxSession } from 'ai'
|
|
4
|
+
import { appDir } from './sandbox'
|
|
5
|
+
|
|
6
|
+
// A job the command backgrounds, like `next dev &`, keeps whatever output it
|
|
7
|
+
// started with, and a read of a pipe would wait on it forever. Output goes to
|
|
8
|
+
// a file instead; background jobs live on.
|
|
9
|
+
const CAPTURE = `out=$(mktemp); bash -lc "$0" > "$out" 2>&1 < /dev/null; code=$?; cat "$out"; rm -f "$out"; exit $code`
|
|
10
|
+
|
|
11
|
+
type Run = Parameters<Experimental_SandboxSession['run']>[0]
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The app's Vercel Sandbox as an AI SDK sandbox session, for harnesses whose
|
|
15
|
+
* own tools need one. Commands run in the app's directory.
|
|
16
|
+
*/
|
|
17
|
+
export function sandboxSession(sandbox: Sandbox): Experimental_SandboxSession {
|
|
18
|
+
const params = ({ command, workingDirectory, env, abortSignal }: Run) => ({
|
|
19
|
+
cmd: 'bash',
|
|
20
|
+
args: ['-c', CAPTURE, command],
|
|
21
|
+
cwd: workingDirectory ?? appDir,
|
|
22
|
+
...(env ? { env } : {}),
|
|
23
|
+
...(abortSignal ? { signal: abortSignal } : {}),
|
|
24
|
+
})
|
|
25
|
+
const readBinaryFile = async ({
|
|
26
|
+
path,
|
|
27
|
+
abortSignal,
|
|
28
|
+
}: {
|
|
29
|
+
path: string
|
|
30
|
+
abortSignal?: AbortSignal
|
|
31
|
+
}): Promise<Uint8Array | null> => {
|
|
32
|
+
const buffer = await sandbox.readFileToBuffer(
|
|
33
|
+
{ path, cwd: appDir },
|
|
34
|
+
abortSignal ? { signal: abortSignal } : {},
|
|
35
|
+
)
|
|
36
|
+
return buffer && new Uint8Array(buffer)
|
|
37
|
+
}
|
|
38
|
+
// The sandbox extracts written files at its root, creating their directories.
|
|
39
|
+
const write = (path: string, content: Uint8Array, signal?: AbortSignal) =>
|
|
40
|
+
sandbox.writeFiles(
|
|
41
|
+
[{ path: path.startsWith('/') ? path : `${appDir}/${path}`, content }],
|
|
42
|
+
signal ? { signal } : {},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
description: `A Vercel Sandbox. The app is in ${appDir}.`,
|
|
47
|
+
// Streams, so a reader that stops early stops the download.
|
|
48
|
+
readFile: async ({ path, abortSignal }) => {
|
|
49
|
+
const stream = await sandbox.readFile(
|
|
50
|
+
{ path, cwd: appDir },
|
|
51
|
+
abortSignal ? { signal: abortSignal } : {},
|
|
52
|
+
)
|
|
53
|
+
if (stream === null) return null
|
|
54
|
+
const chunks = stream[Symbol.asyncIterator]()
|
|
55
|
+
return new ReadableStream<Uint8Array>({
|
|
56
|
+
pull: async (controller) => {
|
|
57
|
+
const { done, value } = await chunks.next()
|
|
58
|
+
if (done) controller.close()
|
|
59
|
+
else
|
|
60
|
+
controller.enqueue(
|
|
61
|
+
typeof value === 'string'
|
|
62
|
+
? new TextEncoder().encode(value)
|
|
63
|
+
: value,
|
|
64
|
+
)
|
|
65
|
+
},
|
|
66
|
+
cancel: async () => {
|
|
67
|
+
await chunks.return?.()
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
readBinaryFile,
|
|
72
|
+
readTextFile: async ({ encoding, startLine, endLine, ...options }) => {
|
|
73
|
+
const bytes = await readBinaryFile(options)
|
|
74
|
+
if (bytes === null) return null
|
|
75
|
+
const text = new TextDecoder(encoding).decode(bytes)
|
|
76
|
+
if (startLine === undefined && endLine === undefined) return text
|
|
77
|
+
return text
|
|
78
|
+
.split('\n')
|
|
79
|
+
.slice((startLine ?? 1) - 1, endLine)
|
|
80
|
+
.join('\n')
|
|
81
|
+
},
|
|
82
|
+
writeFile: async ({ path, content, abortSignal }) =>
|
|
83
|
+
write(
|
|
84
|
+
path,
|
|
85
|
+
new Uint8Array(await new Response(content).arrayBuffer()),
|
|
86
|
+
abortSignal,
|
|
87
|
+
),
|
|
88
|
+
writeBinaryFile: ({ path, content, abortSignal }) =>
|
|
89
|
+
write(path, content, abortSignal),
|
|
90
|
+
writeTextFile: ({ path, content, abortSignal }) =>
|
|
91
|
+
write(path, new TextEncoder().encode(content), abortSignal),
|
|
92
|
+
run: async (options) => {
|
|
93
|
+
const finished = await sandbox.runCommand(params(options))
|
|
94
|
+
return {
|
|
95
|
+
exitCode: finished.exitCode,
|
|
96
|
+
stdout: await finished.stdout(),
|
|
97
|
+
stderr: await finished.stderr(),
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
spawn: async (options) => {
|
|
101
|
+
const { abortSignal } = options
|
|
102
|
+
const command = await sandbox.runCommand({
|
|
103
|
+
...params(options),
|
|
104
|
+
detached: true,
|
|
105
|
+
})
|
|
106
|
+
// For a detached command the signal only cancels its start; aborting
|
|
107
|
+
// kills it, as the AI SDK's sandbox contract says.
|
|
108
|
+
const kill = () => void command.kill().catch(() => {})
|
|
109
|
+
if (abortSignal?.aborted) kill()
|
|
110
|
+
abortSignal?.addEventListener('abort', kill, { once: true })
|
|
111
|
+
const encoder = new TextEncoder()
|
|
112
|
+
let stdout!: ReadableStreamDefaultController<Uint8Array>
|
|
113
|
+
let stderr!: ReadableStreamDefaultController<Uint8Array>
|
|
114
|
+
const streams = {
|
|
115
|
+
stdout: new ReadableStream<Uint8Array>({
|
|
116
|
+
start: (c) => void (stdout = c),
|
|
117
|
+
}),
|
|
118
|
+
stderr: new ReadableStream<Uint8Array>({
|
|
119
|
+
start: (c) => void (stderr = c),
|
|
120
|
+
}),
|
|
121
|
+
}
|
|
122
|
+
// The command's log ends when it exits, and closes both streams.
|
|
123
|
+
void (async () => {
|
|
124
|
+
try {
|
|
125
|
+
for await (const line of command.logs())
|
|
126
|
+
(line.stream === 'stdout' ? stdout : stderr).enqueue(
|
|
127
|
+
encoder.encode(line.data),
|
|
128
|
+
)
|
|
129
|
+
stdout.close()
|
|
130
|
+
stderr.close()
|
|
131
|
+
} catch (error) {
|
|
132
|
+
stdout.error(error)
|
|
133
|
+
stderr.error(error)
|
|
134
|
+
}
|
|
135
|
+
})()
|
|
136
|
+
return {
|
|
137
|
+
...streams,
|
|
138
|
+
wait: async () => {
|
|
139
|
+
try {
|
|
140
|
+
const finished = await command.wait(
|
|
141
|
+
abortSignal ? { signal: abortSignal } : {},
|
|
142
|
+
)
|
|
143
|
+
return { exitCode: finished.exitCode }
|
|
144
|
+
} finally {
|
|
145
|
+
abortSignal?.removeEventListener('abort', kill)
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
kill: () => command.kill(),
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { agentServer,
|
|
1
|
+
import { agentServer, appSchedulers } from '../server'
|
|
2
2
|
|
|
3
3
|
export const POST =
|
|
4
|
-
|
|
4
|
+
appSchedulers['ai-sdk']?.handler(agentServer) ??
|
|
5
5
|
(() => new Response('a0 scheduler is not configured', { status: 503 }))
|
|
6
6
|
|
|
7
7
|
export const maxDuration = 300
|
|
@@ -2,6 +2,8 @@ import 'server-only'
|
|
|
2
2
|
import { waitUntil } from '@vercel/functions'
|
|
3
3
|
import { tool } from 'ai'
|
|
4
4
|
import { handlerContext } from 'experimental-a2/ai'
|
|
5
|
+
import { fx } from 'experimental-a2/ai/fx'
|
|
6
|
+
import { pi } from 'experimental-a2/ai/pi'
|
|
5
7
|
import { agent } from 'experimental-a2/ai/server'
|
|
6
8
|
import { createServer } from 'experimental-a2/server'
|
|
7
9
|
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
@@ -10,7 +12,7 @@ import { store } from '@/lib/store'
|
|
|
10
12
|
import { appList, isAppId } from './apps'
|
|
11
13
|
import { crashOnRequest } from './crash-on-request'
|
|
12
14
|
import { crashLog, crashLogId, isCrashLogId } from './crashes'
|
|
13
|
-
import {
|
|
15
|
+
import { appPort, type Harness, harnessAgents, type ModelChoice } from './model'
|
|
14
16
|
import {
|
|
15
17
|
appDir,
|
|
16
18
|
appSandbox,
|
|
@@ -19,8 +21,11 @@ import {
|
|
|
19
21
|
START_DEV,
|
|
20
22
|
workspace,
|
|
21
23
|
} from './sandbox'
|
|
24
|
+
import { sandboxSession } from './sandbox-session'
|
|
22
25
|
|
|
23
|
-
const
|
|
26
|
+
const instructions = (
|
|
27
|
+
tools: string,
|
|
28
|
+
): string => `You are a coding agent that builds a Next.js app in your own Linux sandbox, ${tools}. The user watches the app in a live preview next to this chat.
|
|
24
29
|
- ${appDir} already holds a blank Next.js app (App Router, TypeScript, Tailwind) with its dependencies installed, and next dev serves it on port ${appPort} with hot reload. It may still be starting when you begin: don't wait for it, poll it, or start another one. Build what the user asks for there; don't create another app.
|
|
25
30
|
- Until you change them, app/page.tsx renders an empty <main />, app/layout.tsx only sets the title, and app/globals.css only imports Tailwind. Write them directly; there's no need to look first.
|
|
26
31
|
- Assume your edits work. Don't curl the dev server, sleep, or read the page back to check, and don't run builds, type checks or lint unless the user asks.
|
|
@@ -48,9 +53,7 @@ const tools = {
|
|
|
48
53
|
execute: ({ path }) =>
|
|
49
54
|
timed(async () =>
|
|
50
55
|
previewUrl({
|
|
51
|
-
sandbox: await appSandbox({
|
|
52
|
-
appId: handlerContext(appAgent).session.id,
|
|
53
|
-
}),
|
|
56
|
+
sandbox: await appSandbox({ appId: handlerContext().session.id }),
|
|
54
57
|
path,
|
|
55
58
|
}),
|
|
56
59
|
),
|
|
@@ -63,7 +66,7 @@ const tools = {
|
|
|
63
66
|
timeoutSeconds: z.number().int().min(1).max(240).default(120),
|
|
64
67
|
}),
|
|
65
68
|
execute: ({ command, timeoutSeconds }) => {
|
|
66
|
-
const context = handlerContext(
|
|
69
|
+
const context = handlerContext()
|
|
67
70
|
return timed(async () =>
|
|
68
71
|
bash({
|
|
69
72
|
sandbox: await appSandbox({ appId: context.session.id }),
|
|
@@ -76,56 +79,106 @@ const tools = {
|
|
|
76
79
|
}),
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
const queued = !!(process.env.VERCEL || process.env.A2_QUEUES)
|
|
83
|
+
|
|
79
84
|
/**
|
|
80
85
|
* Wakes agent work in a fresh invocation when the one that started it ends,
|
|
81
|
-
* and recovers from crashed ones.
|
|
86
|
+
* and recovers from crashed ones. Each harness's agent has its own topic.
|
|
82
87
|
*/
|
|
83
|
-
export const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
export const appSchedulers = {
|
|
89
|
+
'ai-sdk': queued ? vercelQueues({ topic: 'a2-a0' }) : undefined,
|
|
90
|
+
fx: queued ? vercelQueues({ topic: 'a2-a0-fx' }) : undefined,
|
|
91
|
+
pi: queued ? vercelQueues({ topic: 'a2-a0-pi' }) : undefined,
|
|
92
|
+
}
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The app's model choice for a step. The browser pushes it, and the
|
|
96
|
+
* contract's schema checks it on the way in.
|
|
97
|
+
*/
|
|
98
|
+
function chosenModel({
|
|
99
|
+
harness,
|
|
100
|
+
event,
|
|
101
|
+
state,
|
|
102
|
+
}: {
|
|
103
|
+
harness: Harness
|
|
104
|
+
event: { sessionId: string }
|
|
105
|
+
state: { modelChoice: ModelChoice }
|
|
106
|
+
}) {
|
|
107
|
+
// Every step starts here, in the process that runs it, which is what
|
|
108
|
+
// the dev tools' crash has to end.
|
|
109
|
+
const appId = event.sessionId
|
|
110
|
+
waitUntil(
|
|
111
|
+
crashOnRequest({
|
|
112
|
+
appId,
|
|
113
|
+
app: agentServers[harness].session(appId),
|
|
114
|
+
log: crashServer.session(crashLogId(appId)),
|
|
115
|
+
}).catch(() => {}),
|
|
116
|
+
)
|
|
117
|
+
const { model, reasoning, fast } = state.modelChoice
|
|
118
|
+
return { model, reasoning, speed: fast ? 'fast' : 'standard' } as const
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const shared = (harness: Harness) => ({
|
|
91
122
|
...(store ? { store } : {}),
|
|
92
|
-
...(
|
|
93
|
-
contract:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return { model, reasoning, speed: fast ? 'fast' : 'standard' }
|
|
123
|
+
...(appSchedulers[harness] ? { scheduler: appSchedulers[harness] } : {}),
|
|
124
|
+
contract: harnessAgents[harness],
|
|
125
|
+
maxSteps: 80,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
export const agentServer = agent({
|
|
129
|
+
...shared('ai-sdk'),
|
|
130
|
+
model: ({ event, state }) => chosenModel({ harness: 'ai-sdk', event, state }),
|
|
131
|
+
harness: {
|
|
132
|
+
type: 'ai-sdk',
|
|
133
|
+
generation: () => ({
|
|
134
|
+
// Claude leaves its thinking's text out unless asked for a summary.
|
|
135
|
+
providerOptions: {
|
|
136
|
+
anthropic: { thinking: { type: 'adaptive', display: 'summarized' } },
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
109
139
|
},
|
|
110
|
-
|
|
111
|
-
// Claude leaves its thinking's text out unless asked for a summary.
|
|
112
|
-
providerOptions: {
|
|
113
|
-
anthropic: { thinking: { type: 'adaptive', display: 'summarized' } },
|
|
114
|
-
},
|
|
115
|
-
}),
|
|
116
|
-
instructions: INSTRUCTIONS,
|
|
140
|
+
instructions: instructions('through the bash tool'),
|
|
117
141
|
tools,
|
|
118
|
-
maxSteps: 80,
|
|
119
142
|
})
|
|
120
143
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
/** FX's own loop, with the same tools and prompt. */
|
|
145
|
+
const fxServer = agent({
|
|
146
|
+
...shared('fx'),
|
|
147
|
+
harness: fx(),
|
|
148
|
+
model: ({ event, state }) => chosenModel({ harness: 'fx', event, state }),
|
|
149
|
+
instructions: instructions('through the bash tool'),
|
|
150
|
+
tools,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
/** Pi's own loop and tools, working in the app's sandbox, plus the preview. */
|
|
154
|
+
const piServer = agent({
|
|
155
|
+
...shared('pi'),
|
|
156
|
+
harness: pi({
|
|
157
|
+
sandbox: async ({ session }) =>
|
|
158
|
+
sandboxSession(await appSandbox({ appId: session.id })),
|
|
159
|
+
}),
|
|
160
|
+
model: ({ event, state }) => chosenModel({ harness: 'pi', event, state }),
|
|
161
|
+
instructions: instructions('with your tools'),
|
|
162
|
+
tools: { show_preview: tools.show_preview },
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
export const agentServers = {
|
|
166
|
+
'ai-sdk': agentServer,
|
|
167
|
+
fx: fxServer,
|
|
168
|
+
pi: piServer,
|
|
127
169
|
}
|
|
128
170
|
|
|
171
|
+
/** An app's event stream and pushes, for one harness's agent; only an app id names a session. */
|
|
172
|
+
export const harnessEvents =
|
|
173
|
+
(harness: Harness) =>
|
|
174
|
+
(request: Request): Promise<Response> =>
|
|
175
|
+
agentServers[harness].fetch(request, {
|
|
176
|
+
// The agent refuses the AI protocol's server-authored events itself.
|
|
177
|
+
authorize: (operation) => isAppId(operation.sessionId),
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
export const appEvents = harnessEvents('ai-sdk')
|
|
181
|
+
|
|
129
182
|
/** The apps sidebar's lists, which only the browser writes. */
|
|
130
183
|
export const listServer = createServer({
|
|
131
184
|
...(store ? { store } : {}),
|
|
@@ -138,20 +191,11 @@ export const crashServer = createServer({
|
|
|
138
191
|
contract: crashLog,
|
|
139
192
|
})
|
|
140
193
|
|
|
141
|
-
/**
|
|
142
|
-
* A crash ends the function instance, and anything else it's serving, so
|
|
143
|
-
* only previews allow it, behind Deployment Protection, unless A0_CRASHES
|
|
144
|
-
* is on. Under next dev it would take the dev server down.
|
|
145
|
-
*/
|
|
146
|
-
export const crashesAllowed =
|
|
147
|
-
process.env.VERCEL_ENV === 'preview' || process.env.A0_CRASHES === 'on'
|
|
148
|
-
|
|
149
194
|
/** The browser can only ask for a crash; the process it ends records it. */
|
|
150
195
|
export function crashEvents(request: Request): Promise<Response> {
|
|
151
196
|
return crashServer.fetch(request, {
|
|
152
197
|
authorize: (operation) =>
|
|
153
198
|
isCrashLogId(operation.sessionId) &&
|
|
154
|
-
(crashesAllowed || operation.type !== 'push') &&
|
|
155
199
|
(operation.type !== 'push' ||
|
|
156
200
|
operation.events.every((event) => event.type === 'crash.requested')),
|
|
157
201
|
})
|
|
@@ -3,7 +3,7 @@ import { createClient } from 'experimental-a2/client'
|
|
|
3
3
|
import { createReact } from 'experimental-a2/react'
|
|
4
4
|
import { appList } from './apps'
|
|
5
5
|
import { crashLog } from './crashes'
|
|
6
|
-
import { appAgent } from './model'
|
|
6
|
+
import { appAgent, type Harness, harnessFrom } from './model'
|
|
7
7
|
|
|
8
8
|
/** The dev tools' switch, `?limit=5s`, for the app at this URL, while they're open. */
|
|
9
9
|
export function shortFunctions(appId: string): boolean {
|
|
@@ -15,17 +15,29 @@ export function shortFunctions(appId: string): boolean {
|
|
|
15
15
|
)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** The harness the app at this URL runs, from `?harness=`. */
|
|
19
|
+
export function appHarness(appId: string): Harness {
|
|
20
|
+
return location.pathname === `/a0/${appId}`
|
|
21
|
+
? harnessFrom(new URLSearchParams(location.search).get('harness'))
|
|
22
|
+
: 'ai-sdk'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const routes = (appId: string): string => {
|
|
26
|
+
const harness = appHarness(appId)
|
|
27
|
+
return harness === 'ai-sdk' ? '/a0' : `/a0/${harness}`
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
/**
|
|
19
|
-
* Follows an app's conversation log over
|
|
20
|
-
* 5-second function instead while the dev tools' switch is on.
|
|
31
|
+
* Follows an app's conversation log over its harness's events route. Pushes
|
|
32
|
+
* go to a 5-second function instead while the dev tools' switch is on.
|
|
21
33
|
*/
|
|
22
34
|
export const appClient = createClient({
|
|
23
35
|
contract: appAgent,
|
|
24
36
|
api: {
|
|
25
37
|
type: 'http',
|
|
26
|
-
stream:
|
|
38
|
+
stream: (appId) => `${routes(appId)}/events`,
|
|
27
39
|
push: (appId) =>
|
|
28
|
-
shortFunctions(appId) ? '/
|
|
40
|
+
`${routes(appId)}${shortFunctions(appId) ? '/short' : ''}/events`,
|
|
29
41
|
},
|
|
30
42
|
})
|
|
31
43
|
|
|
@@ -4,6 +4,7 @@ import { createContext, useContext, useEffect, useState } from 'react'
|
|
|
4
4
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
5
5
|
import { SiteNav } from '../components/site-header'
|
|
6
6
|
import { type App, byLatestPrompt } from './apps'
|
|
7
|
+
import type { Harness } from './model'
|
|
7
8
|
import { AppList } from './app-list'
|
|
8
9
|
import { ColumnEdge, Columns } from './columns'
|
|
9
10
|
import { listCookie, readCookie, type Widths, writeCookie } from './cookies'
|
|
@@ -13,7 +14,7 @@ import { useList } from './session'
|
|
|
13
14
|
type Apps = {
|
|
14
15
|
listed: (id: string) => App | null
|
|
15
16
|
/** Records a prompt to an app, listing it under `name` the first time. */
|
|
16
|
-
prompted: (app: { id: string; name: string }) => void
|
|
17
|
+
prompted: (app: { id: string; name: string; harness: Harness }) => void
|
|
17
18
|
/** Makes a new app on screen the one "+" opens until it's prompted. */
|
|
18
19
|
opened: (appId: string) => void
|
|
19
20
|
}
|
|
@@ -85,12 +86,19 @@ export function A0Shell({
|
|
|
85
86
|
apps.find((app) => app.id === id) ?? null
|
|
86
87
|
const context: Apps = {
|
|
87
88
|
listed,
|
|
88
|
-
prompted: ({ id, name }) => {
|
|
89
|
+
prompted: ({ id, name, harness }) => {
|
|
89
90
|
if (listed(id)) {
|
|
90
91
|
write({ type: 'app.prompted', payload: { appId: id } })
|
|
91
92
|
return
|
|
92
93
|
}
|
|
93
|
-
write({
|
|
94
|
+
write({
|
|
95
|
+
type: 'app.created',
|
|
96
|
+
payload: {
|
|
97
|
+
appId: id,
|
|
98
|
+
name,
|
|
99
|
+
...(harness === 'ai-sdk' ? {} : { harness }),
|
|
100
|
+
},
|
|
101
|
+
})
|
|
94
102
|
if (id === ready) setReady(crypto.randomUUID())
|
|
95
103
|
},
|
|
96
104
|
opened: setReady,
|
|
@@ -19,9 +19,12 @@ const server = () => {
|
|
|
19
19
|
contract: demoAgent,
|
|
20
20
|
store: memory(),
|
|
21
21
|
model: 'test/model',
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
harness: {
|
|
23
|
+
type: 'ai-sdk',
|
|
24
|
+
generate: () => {
|
|
25
|
+
generations += 1
|
|
26
|
+
return new ReadableStream({ start: (controller) => controller.close() })
|
|
27
|
+
},
|
|
25
28
|
},
|
|
26
29
|
})
|
|
27
30
|
return { agentServer, generations: () => generations }
|
|
@@ -137,8 +137,15 @@ export const agentServer = agent({
|
|
|
137
137
|
...(agentScheduler ? { scheduler: agentScheduler } : {}),
|
|
138
138
|
telemetry,
|
|
139
139
|
model: ({ state }) => modelSettings(state.modelChoice),
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
harness: {
|
|
141
|
+
type: 'ai-sdk',
|
|
142
|
+
messageMetadata: traceMetadata,
|
|
143
|
+
compaction: ({ state }) => compactionOptions(state.compactionSettings),
|
|
144
|
+
generation: () => ({
|
|
145
|
+
telemetry: modelTelemetry(),
|
|
146
|
+
toolApproval: { restartService: 'user-approval' },
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
142
149
|
tools,
|
|
143
150
|
instructions: `You are an incident-response agent for the playground.
|
|
144
151
|
When responding to an incident, inspect checkout-api before taking action. Its
|
|
@@ -154,8 +161,4 @@ it again.
|
|
|
154
161
|
|
|
155
162
|
When the operator asks you to run a shell command, call bash. Make one bash call
|
|
156
163
|
at a time and wait for its result before calling it again. Be concise.`,
|
|
157
|
-
generation: () => ({
|
|
158
|
-
telemetry: modelTelemetry(),
|
|
159
|
-
toolApproval: { restartService: 'user-approval' },
|
|
160
|
-
}),
|
|
161
164
|
})
|
|
@@ -11,7 +11,15 @@ import { inputs } from 'experimental-a2/ai'
|
|
|
11
11
|
import { agent } from 'experimental-a2/ai/server'
|
|
12
12
|
import { memory } from 'experimental-a2/store-memory'
|
|
13
13
|
import { otel } from 'experimental-a2/otel'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
afterAll,
|
|
16
|
+
beforeAll,
|
|
17
|
+
beforeEach,
|
|
18
|
+
expect,
|
|
19
|
+
it,
|
|
20
|
+
onTestFinished,
|
|
21
|
+
vi,
|
|
22
|
+
} from 'vitest'
|
|
15
23
|
import { demoAgent } from './model'
|
|
16
24
|
import { modelTelemetry, traceMetadata } from './telemetry'
|
|
17
25
|
import { traceUrl } from './trace'
|
|
@@ -51,60 +59,73 @@ afterAll(async () => {
|
|
|
51
59
|
it.each(['complete', 'stop'] as const)(
|
|
52
60
|
'correlates standard SDK traces and releases spans when generation ends with %s',
|
|
53
61
|
async (outcome) => {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
usage: {
|
|
75
|
-
inputTokens: {
|
|
76
|
-
total: 5,
|
|
77
|
-
noCache: 5,
|
|
78
|
-
cacheRead: 0,
|
|
79
|
-
cacheWrite: 0,
|
|
80
|
-
},
|
|
81
|
-
outputTokens: { total: 2, text: 2, reasoning: 0 },
|
|
82
|
-
},
|
|
83
|
-
providerMetadata: {
|
|
84
|
-
test: { secret: 'PRIVATE_METADATA' },
|
|
62
|
+
const model = new MockLanguageModelV4({
|
|
63
|
+
modelId: 'test-model',
|
|
64
|
+
doStream: async ({ abortSignal }) => ({
|
|
65
|
+
stream:
|
|
66
|
+
outcome === 'complete'
|
|
67
|
+
? simulateReadableStream({
|
|
68
|
+
chunks: [
|
|
69
|
+
{ type: 'stream-start', warnings: [] },
|
|
70
|
+
{ type: 'text-start', id: 'text' },
|
|
71
|
+
{ type: 'text-delta', id: 'text', delta: 'PRIVATE_OUTPUT' },
|
|
72
|
+
{ type: 'text-end', id: 'text' },
|
|
73
|
+
{
|
|
74
|
+
type: 'finish',
|
|
75
|
+
finishReason: { unified: 'stop', raw: 'stop' },
|
|
76
|
+
usage: {
|
|
77
|
+
inputTokens: {
|
|
78
|
+
total: 5,
|
|
79
|
+
noCache: 5,
|
|
80
|
+
cacheRead: 0,
|
|
81
|
+
cacheWrite: 0,
|
|
85
82
|
},
|
|
83
|
+
outputTokens: { total: 2, text: 2, reasoning: 0 },
|
|
84
|
+
},
|
|
85
|
+
providerMetadata: {
|
|
86
|
+
test: { secret: 'PRIVATE_METADATA' },
|
|
86
87
|
},
|
|
87
|
-
],
|
|
88
|
-
})
|
|
89
|
-
: new ReadableStream({
|
|
90
|
-
start(controller) {
|
|
91
|
-
controller.enqueue({ type: 'stream-start', warnings: [] })
|
|
92
|
-
controller.enqueue({ type: 'text-start', id: 'text' })
|
|
93
|
-
controller.enqueue({
|
|
94
|
-
type: 'text-delta',
|
|
95
|
-
id: 'text',
|
|
96
|
-
delta: 'PRIVATE_PARTIAL',
|
|
97
|
-
})
|
|
98
|
-
abortSignal?.addEventListener(
|
|
99
|
-
'abort',
|
|
100
|
-
() => controller.close(),
|
|
101
|
-
{ once: true },
|
|
102
|
-
)
|
|
103
88
|
},
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
],
|
|
90
|
+
})
|
|
91
|
+
: new ReadableStream({
|
|
92
|
+
start(controller) {
|
|
93
|
+
controller.enqueue({ type: 'stream-start', warnings: [] })
|
|
94
|
+
controller.enqueue({ type: 'text-start', id: 'text' })
|
|
95
|
+
controller.enqueue({
|
|
96
|
+
type: 'text-delta',
|
|
97
|
+
id: 'text',
|
|
98
|
+
delta: 'PRIVATE_PARTIAL',
|
|
99
|
+
})
|
|
100
|
+
abortSignal?.addEventListener(
|
|
101
|
+
'abort',
|
|
102
|
+
() => controller.close(),
|
|
103
|
+
{ once: true },
|
|
104
|
+
)
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
106
107
|
}),
|
|
107
108
|
})
|
|
109
|
+
// Agents take AI Gateway model IDs; the AI SDK's global provider serves
|
|
110
|
+
// this one from the mock.
|
|
111
|
+
vi.stubGlobal('AI_SDK_DEFAULT_PROVIDER', {
|
|
112
|
+
specificationVersion: 'v4',
|
|
113
|
+
languageModel: () => model,
|
|
114
|
+
})
|
|
115
|
+
onTestFinished(() => {
|
|
116
|
+
vi.unstubAllGlobals()
|
|
117
|
+
})
|
|
118
|
+
const server = agent({
|
|
119
|
+
contract: demoAgent,
|
|
120
|
+
store: memory(),
|
|
121
|
+
telemetry: otel(),
|
|
122
|
+
harness: {
|
|
123
|
+
type: 'ai-sdk',
|
|
124
|
+
generation: { telemetry: modelTelemetry() },
|
|
125
|
+
messageMetadata: traceMetadata,
|
|
126
|
+
},
|
|
127
|
+
model: 'test-model',
|
|
128
|
+
})
|
|
108
129
|
const session = server.session(`trace-${outcome}`)
|
|
109
130
|
await session.append(
|
|
110
131
|
...inputs.message({
|
|
@@ -330,7 +330,7 @@ function CanvasClient({
|
|
|
330
330
|
|
|
331
331
|
// A ghost is dead once the log has moved past it: show `dragging`
|
|
332
332
|
// only while its `seen` frontier reaches the shape's durable
|
|
333
|
-
// `movedAt` (/
|
|
333
|
+
// `movedAt` (/client/presence#reconciling-the-planes-seen).
|
|
334
334
|
const ghosts = new Map<
|
|
335
335
|
string,
|
|
336
336
|
{ x: number; y: number; color: PaletteColor }
|