experimental-a2 0.8.0 → 0.8.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/AGENTS.md +11 -0
- package/CHANGELOG.md +8 -0
- package/README.md +29 -0
- package/docs/guides/07-examples.mdx +56 -0
- package/docs/index.mdx +13 -0
- package/examples/README.md +15 -0
- package/examples/playground/AGENTS.md +11 -0
- package/examples/playground/DEPLOY.md +106 -0
- package/examples/playground/README.md +19 -0
- package/examples/playground/activity-feed.test.ts +10 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +376 -0
- package/examples/playground/app/agent/[agentId]/page.tsx +29 -0
- package/examples/playground/app/agent/events/route.ts +4 -0
- package/examples/playground/app/agent/model.ts +3 -0
- package/examples/playground/app/agent/new-agent-session.tsx +98 -0
- package/examples/playground/app/agent/page.tsx +25 -0
- package/examples/playground/app/agent/scheduler/route.ts +5 -0
- package/examples/playground/app/agent/server.ts +153 -0
- package/examples/playground/app/agent/session.ts +13 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +682 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-replay.test.ts +68 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-replay.ts +19 -0
- package/examples/playground/app/canvas/[canvasId]/page.tsx +22 -0
- package/examples/playground/app/canvas/[canvasId]/session.ts +19 -0
- package/examples/playground/app/canvas/events/route.ts +13 -0
- package/examples/playground/app/canvas/model.ts +94 -0
- package/examples/playground/app/canvas/open-canvas.tsx +40 -0
- package/examples/playground/app/canvas/page.tsx +20 -0
- package/examples/playground/app/canvas/server.ts +9 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +118 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +316 -0
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +922 -0
- package/examples/playground/app/chat/[chatId]/chat-view.test.ts +152 -0
- package/examples/playground/app/chat/[chatId]/chat-view.ts +101 -0
- package/examples/playground/app/chat/[chatId]/composer.test.ts +44 -0
- package/examples/playground/app/chat/[chatId]/composer.ts +30 -0
- package/examples/playground/app/chat/[chatId]/page.tsx +30 -0
- package/examples/playground/app/chat/[chatId]/session.ts +7 -0
- package/examples/playground/app/chat/events/route.ts +7 -0
- package/examples/playground/app/chat/model.test.ts +155 -0
- package/examples/playground/app/chat/model.ts +310 -0
- package/examples/playground/app/chat/new-conversation.tsx +16 -0
- package/examples/playground/app/chat/page.tsx +25 -0
- package/examples/playground/app/chat/scheduler/route.ts +5 -0
- package/examples/playground/app/chat/server.ts +184 -0
- package/examples/playground/app/components/activity-feed.tsx +54 -0
- package/examples/playground/app/components/connection-pill.tsx +29 -0
- package/examples/playground/app/counter/counter-client.tsx +72 -0
- package/examples/playground/app/counter/events/route.ts +4 -0
- package/examples/playground/app/counter/model.test.ts +36 -0
- package/examples/playground/app/counter/model.ts +31 -0
- package/examples/playground/app/counter/page.tsx +24 -0
- package/examples/playground/app/counter/server.ts +9 -0
- package/examples/playground/app/counter/session.ts +13 -0
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +80 -0
- package/examples/playground/app/documents/[documentId]/document-client.tsx +525 -0
- package/examples/playground/app/documents/[documentId]/page.tsx +23 -0
- package/examples/playground/app/documents/[documentId]/session.ts +7 -0
- package/examples/playground/app/documents/events/route.ts +4 -0
- package/examples/playground/app/documents/model.ts +55 -0
- package/examples/playground/app/documents/open-document.tsx +40 -0
- package/examples/playground/app/documents/page.tsx +22 -0
- package/examples/playground/app/documents/server.ts +9 -0
- package/examples/playground/app/globals.css +2078 -0
- package/examples/playground/app/layout.tsx +44 -0
- package/examples/playground/app/orders/[orderId]/order-client.tsx +140 -0
- package/examples/playground/app/orders/[orderId]/page.tsx +29 -0
- package/examples/playground/app/orders/[orderId]/session.ts +11 -0
- package/examples/playground/app/orders/create/route.ts +30 -0
- package/examples/playground/app/orders/events/route.ts +4 -0
- package/examples/playground/app/orders/model.ts +79 -0
- package/examples/playground/app/orders/new-order-form.tsx +98 -0
- package/examples/playground/app/orders/page.tsx +22 -0
- package/examples/playground/app/orders/scheduler/route.ts +5 -0
- package/examples/playground/app/orders/server.ts +50 -0
- package/examples/playground/app/page.tsx +102 -0
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +31 -0
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +144 -0
- package/examples/playground/app/recovery/[recoveryId]/session.ts +7 -0
- package/examples/playground/app/recovery/events/route.ts +3 -0
- package/examples/playground/app/recovery/model.ts +55 -0
- package/examples/playground/app/recovery/new-recovery-session.tsx +20 -0
- package/examples/playground/app/recovery/page.tsx +22 -0
- package/examples/playground/app/recovery/scheduler/route.ts +7 -0
- package/examples/playground/app/recovery/server.ts +53 -0
- package/examples/playground/app/recovery/start/route.ts +41 -0
- package/examples/playground/css.d.ts +4 -0
- package/examples/playground/lib/store.ts +15 -0
- package/examples/playground/next-env.d.ts +5 -0
- package/examples/playground/next.config.ts +10 -0
- package/examples/playground/package.json +46 -0
- package/examples/playground/tsconfig.json +37 -0
- package/examples/playground/vercel.json +40 -0
- package/package.json +5 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { demoAgent } from '../model'
|
|
3
|
+
import { agentServer } from '../server'
|
|
4
|
+
import { SessionProvider } from '../session'
|
|
5
|
+
import { AgentClient } from './agent-client'
|
|
6
|
+
|
|
7
|
+
export default async function AgentSessionPage({
|
|
8
|
+
params,
|
|
9
|
+
}: {
|
|
10
|
+
params: Promise<{ agentId: string }>
|
|
11
|
+
}): Promise<ReactNode> {
|
|
12
|
+
const { agentId } = await params
|
|
13
|
+
const session = agentServer.session(agentId)
|
|
14
|
+
const { state, index } = await session.state(demoAgent.reducer)
|
|
15
|
+
const events = (await session.history()).filter(
|
|
16
|
+
(event) => event.index <= index,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<SessionProvider
|
|
21
|
+
sessionId={agentId}
|
|
22
|
+
initialState={state}
|
|
23
|
+
initialIndex={index}
|
|
24
|
+
initialEvents={events}
|
|
25
|
+
>
|
|
26
|
+
<AgentClient agentId={agentId} />
|
|
27
|
+
</SessionProvider>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import type {
|
|
3
|
+
FormEvent,
|
|
4
|
+
KeyboardEvent as ReactKeyboardEvent,
|
|
5
|
+
ReactNode,
|
|
6
|
+
} from 'react'
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
import { useRouter } from 'next/navigation'
|
|
9
|
+
import type { UIMessage } from 'ai'
|
|
10
|
+
import { inputs } from 'experimental-a2/ai'
|
|
11
|
+
import { agentClient } from './session'
|
|
12
|
+
|
|
13
|
+
const REMINDER_PROMPT =
|
|
14
|
+
'Set a reminder for yourself in 37 seconds to check checkout-api again.'
|
|
15
|
+
const INCIDENT_PROMPT =
|
|
16
|
+
'Investigate the checkout errors and take the safest action.'
|
|
17
|
+
|
|
18
|
+
function submitOnEnter(event: ReactKeyboardEvent<HTMLTextAreaElement>): void {
|
|
19
|
+
if (
|
|
20
|
+
event.key !== 'Enter' ||
|
|
21
|
+
event.shiftKey ||
|
|
22
|
+
event.nativeEvent.isComposing
|
|
23
|
+
) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
event.preventDefault()
|
|
27
|
+
event.currentTarget.form?.requestSubmit()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function NewAgentSession({
|
|
31
|
+
remindersEnabled,
|
|
32
|
+
}: {
|
|
33
|
+
remindersEnabled: boolean
|
|
34
|
+
}): ReactNode {
|
|
35
|
+
const router = useRouter()
|
|
36
|
+
const [prompt, setPrompt] = useState(
|
|
37
|
+
remindersEnabled ? REMINDER_PROMPT : INCIDENT_PROMPT,
|
|
38
|
+
)
|
|
39
|
+
const [submitting, setSubmitting] = useState(false)
|
|
40
|
+
const [error, setError] = useState<string | null>(null)
|
|
41
|
+
|
|
42
|
+
const start = async (event: FormEvent): Promise<void> => {
|
|
43
|
+
event.preventDefault()
|
|
44
|
+
const submittedPrompt = prompt
|
|
45
|
+
const text = prompt.trim()
|
|
46
|
+
if (!text || submitting) return
|
|
47
|
+
|
|
48
|
+
const agentId = crypto.randomUUID().slice(0, 8)
|
|
49
|
+
const message: UIMessage = {
|
|
50
|
+
id: crypto.randomUUID(),
|
|
51
|
+
role: 'user',
|
|
52
|
+
parts: [{ type: 'text', text }],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setPrompt('')
|
|
56
|
+
setSubmitting(true)
|
|
57
|
+
setError(null)
|
|
58
|
+
try {
|
|
59
|
+
await agentClient.session(agentId).push(...inputs.message(message))
|
|
60
|
+
router.push(`/agent/${agentId}`)
|
|
61
|
+
} catch (err) {
|
|
62
|
+
setPrompt((current) => (current === '' ? submittedPrompt : current))
|
|
63
|
+
setSubmitting(false)
|
|
64
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<section className="agent-conversation agent-new" aria-label="New agent">
|
|
70
|
+
<div className="agent-transcript">
|
|
71
|
+
<p className="agent-empty">
|
|
72
|
+
Ask the agent to investigate an incident or set a reminder for itself.
|
|
73
|
+
The conversation starts immediately while the durable events are sent
|
|
74
|
+
in the background.
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
<form className="agent-form" onSubmit={(event) => void start(event)}>
|
|
78
|
+
<label>
|
|
79
|
+
Message
|
|
80
|
+
<textarea
|
|
81
|
+
autoFocus
|
|
82
|
+
rows={3}
|
|
83
|
+
value={prompt}
|
|
84
|
+
onChange={(event) => setPrompt(event.target.value)}
|
|
85
|
+
onKeyDown={submitOnEnter}
|
|
86
|
+
disabled={submitting}
|
|
87
|
+
/>
|
|
88
|
+
</label>
|
|
89
|
+
<div className="agent-form-actions">
|
|
90
|
+
<button type="submit" disabled={submitting || !prompt.trim()}>
|
|
91
|
+
{submitting ? 'Opening…' : 'Send to agent'}
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</form>
|
|
95
|
+
{error ? <p className="error agent-new-error">{error}</p> : null}
|
|
96
|
+
</section>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { agentScheduler } from './server'
|
|
3
|
+
import { NewAgentSession } from './new-agent-session'
|
|
4
|
+
|
|
5
|
+
export default function AgentPage(): ReactNode {
|
|
6
|
+
return (
|
|
7
|
+
<article>
|
|
8
|
+
<h1>Agent</h1>
|
|
9
|
+
<p className="lede">
|
|
10
|
+
One agent turn becomes a durable trail of messages, model progress, tool
|
|
11
|
+
calls, sandboxed Bash commands, human approval, and scheduled reminders.
|
|
12
|
+
The agent can stop at a decision or timer and resume later because its
|
|
13
|
+
state lives in the log, not the request.
|
|
14
|
+
</p>
|
|
15
|
+
<NewAgentSession remindersEnabled={agentScheduler !== undefined} />
|
|
16
|
+
<p className="hint">
|
|
17
|
+
Each durable generation runs one AI SDK <code>streamText</code> step
|
|
18
|
+
with <code>openai/gpt-5.6-luna</code> through Vercel AI Gateway. Local
|
|
19
|
+
development needs Gateway credentials and a linked Vercel project for
|
|
20
|
+
Sandbox; Vercel deployments use their OIDC token. Reminder delivery also
|
|
21
|
+
needs the configured scheduler.
|
|
22
|
+
</p>
|
|
23
|
+
</article>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
import { Buffer } from 'node:buffer'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { Sandbox } from '@vercel/sandbox'
|
|
5
|
+
import { tool } from 'ai'
|
|
6
|
+
import { handlerContext, inputs } from 'experimental-a2/ai'
|
|
7
|
+
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
8
|
+
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
import { store } from '@/lib/store'
|
|
11
|
+
import { demoAgent } from './model'
|
|
12
|
+
|
|
13
|
+
const maxReminderDelaySeconds = 6 * 24 * 60 * 60
|
|
14
|
+
const bashTimeoutMs = 120_000
|
|
15
|
+
const maxBashOutputBytes = 64 * 1024
|
|
16
|
+
|
|
17
|
+
const sandboxName = (sessionId: string): string =>
|
|
18
|
+
`a2-playground-agent-${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`
|
|
19
|
+
|
|
20
|
+
const tools = {
|
|
21
|
+
inspectService: tool({
|
|
22
|
+
description: 'Inspect the current health and runbook for a service.',
|
|
23
|
+
inputSchema: z.object({ service: z.string() }),
|
|
24
|
+
execute: ({ service }) => ({
|
|
25
|
+
service,
|
|
26
|
+
errorRate: '12.4%',
|
|
27
|
+
runbook: 'restart after sustained 10%',
|
|
28
|
+
}),
|
|
29
|
+
}),
|
|
30
|
+
restartService: tool({
|
|
31
|
+
description: 'Restart every instance of a service.',
|
|
32
|
+
inputSchema: z.object({ service: z.string() }),
|
|
33
|
+
execute: ({ service }) => ({
|
|
34
|
+
service,
|
|
35
|
+
status: 'restarted',
|
|
36
|
+
instances: 3,
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
bash: tool({
|
|
40
|
+
description:
|
|
41
|
+
"Execute a Bash command in this session's persistent Vercel Sandbox. Files created by one command are available to later commands in the same session.",
|
|
42
|
+
inputSchema: z.object({
|
|
43
|
+
command: z.string().min(1).describe('The Bash command to execute.'),
|
|
44
|
+
}),
|
|
45
|
+
execute: async ({ command }) => {
|
|
46
|
+
const context = handlerContext(demoAgent)
|
|
47
|
+
const sandbox = await Sandbox.getOrCreate({
|
|
48
|
+
name: sandboxName(context.session.id),
|
|
49
|
+
persistent: true,
|
|
50
|
+
signal: context.signal,
|
|
51
|
+
})
|
|
52
|
+
const running = await sandbox.runCommand({
|
|
53
|
+
cmd: '/bin/bash',
|
|
54
|
+
args: ['-lc', command],
|
|
55
|
+
detached: true,
|
|
56
|
+
signal: context.signal,
|
|
57
|
+
timeoutMs: bashTimeoutMs,
|
|
58
|
+
})
|
|
59
|
+
const logs = running.logs({ signal: context.signal })
|
|
60
|
+
let stdout = ''
|
|
61
|
+
let stderr = ''
|
|
62
|
+
let remainingBytes = maxBashOutputBytes
|
|
63
|
+
let truncated = false
|
|
64
|
+
try {
|
|
65
|
+
for await (const log of logs) {
|
|
66
|
+
const bytes = Buffer.from(log.data)
|
|
67
|
+
const retained = bytes.subarray(0, remainingBytes)
|
|
68
|
+
const text =
|
|
69
|
+
retained.byteLength === bytes.byteLength
|
|
70
|
+
? log.data
|
|
71
|
+
: new TextDecoder().decode(retained, { stream: true })
|
|
72
|
+
if (log.stream === 'stdout') stdout += text
|
|
73
|
+
else stderr += text
|
|
74
|
+
remainingBytes -= retained.byteLength
|
|
75
|
+
if (retained.byteLength < bytes.byteLength) {
|
|
76
|
+
truncated = true
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
try {
|
|
82
|
+
await running.kill('SIGKILL')
|
|
83
|
+
} catch {}
|
|
84
|
+
throw error
|
|
85
|
+
} finally {
|
|
86
|
+
logs.close()
|
|
87
|
+
}
|
|
88
|
+
if (truncated) {
|
|
89
|
+
try {
|
|
90
|
+
await running.kill('SIGKILL')
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
const result = await running.wait({ signal: context.signal })
|
|
94
|
+
return { exitCode: result.exitCode, stdout, stderr, truncated }
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
setReminder: tool({
|
|
98
|
+
description:
|
|
99
|
+
'Set a reminder for yourself after any delay up to six days. When it is due, it arrives as a new user message.',
|
|
100
|
+
inputSchema: z.object({
|
|
101
|
+
delaySeconds: z
|
|
102
|
+
.number()
|
|
103
|
+
.int()
|
|
104
|
+
.min(1)
|
|
105
|
+
.max(maxReminderDelaySeconds)
|
|
106
|
+
.describe('Seconds from now, from 1 through 518400'),
|
|
107
|
+
reminder: z.string().min(1).max(500),
|
|
108
|
+
}),
|
|
109
|
+
execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
|
|
110
|
+
const context = handlerContext(demoAgent)
|
|
111
|
+
await context.session.schedule(
|
|
112
|
+
`reminder:${toolCallId}`,
|
|
113
|
+
{ delay: `${delaySeconds}s` },
|
|
114
|
+
...inputs.message({
|
|
115
|
+
id: `reminder:${context.event.id}`,
|
|
116
|
+
role: 'user',
|
|
117
|
+
parts: [{ type: 'text', text: `Reminder: ${reminder}` }],
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
return { scheduled: true, delaySeconds, reminder }
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const agentScheduler =
|
|
126
|
+
process.env.VERCEL || process.env.A2_QUEUES
|
|
127
|
+
? vercelQueues({ topic: 'a2-agent' })
|
|
128
|
+
: undefined
|
|
129
|
+
|
|
130
|
+
export const agentServer = createAgentServer({
|
|
131
|
+
...(store ? { store } : {}),
|
|
132
|
+
...(agentScheduler ? { scheduler: agentScheduler } : {}),
|
|
133
|
+
agent: demoAgent,
|
|
134
|
+
model: 'openai/gpt-5.6-luna',
|
|
135
|
+
tools,
|
|
136
|
+
instructions: `You are an incident-response agent for the playground.
|
|
137
|
+
When responding to an incident, inspect checkout-api before taking action. Its
|
|
138
|
+
runbook says to restart when the error rate remains above 10%. If it does,
|
|
139
|
+
immediately call restartService. Do not ask for approval in prose: that tool
|
|
140
|
+
automatically creates the built-in approval. After the operator responds,
|
|
141
|
+
continue and report the result.
|
|
142
|
+
|
|
143
|
+
When the operator asks for a reminder, always call setReminder and only say it
|
|
144
|
+
was scheduled after the tool succeeds. A user message beginning with
|
|
145
|
+
"Reminder:" is a reminder that is now due: respond to it, and do not schedule
|
|
146
|
+
it again.
|
|
147
|
+
|
|
148
|
+
When the operator asks you to run a shell command, call bash. Make one bash call
|
|
149
|
+
at a time and wait for its result before calling it again. Be concise.`,
|
|
150
|
+
generation: {
|
|
151
|
+
toolApproval: { restartService: 'user-approval' },
|
|
152
|
+
},
|
|
153
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { createClient } from 'experimental-a2/client'
|
|
3
|
+
import { createReact } from 'experimental-a2/react'
|
|
4
|
+
import { demoAgent } from './model'
|
|
5
|
+
|
|
6
|
+
export const agentClient = createClient({
|
|
7
|
+
reducer: demoAgent.reducer,
|
|
8
|
+
api: '/agent/events',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const { SessionProvider, useSession } = createReact({
|
|
12
|
+
client: agentClient,
|
|
13
|
+
})
|