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.
Files changed (94) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +29 -0
  4. package/docs/guides/07-examples.mdx +56 -0
  5. package/docs/index.mdx +13 -0
  6. package/examples/README.md +15 -0
  7. package/examples/playground/AGENTS.md +11 -0
  8. package/examples/playground/DEPLOY.md +106 -0
  9. package/examples/playground/README.md +19 -0
  10. package/examples/playground/activity-feed.test.ts +10 -0
  11. package/examples/playground/app/agent/[agentId]/agent-client.tsx +376 -0
  12. package/examples/playground/app/agent/[agentId]/page.tsx +29 -0
  13. package/examples/playground/app/agent/events/route.ts +4 -0
  14. package/examples/playground/app/agent/model.ts +3 -0
  15. package/examples/playground/app/agent/new-agent-session.tsx +98 -0
  16. package/examples/playground/app/agent/page.tsx +25 -0
  17. package/examples/playground/app/agent/scheduler/route.ts +5 -0
  18. package/examples/playground/app/agent/server.ts +153 -0
  19. package/examples/playground/app/agent/session.ts +13 -0
  20. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +682 -0
  21. package/examples/playground/app/canvas/[canvasId]/canvas-replay.test.ts +68 -0
  22. package/examples/playground/app/canvas/[canvasId]/canvas-replay.ts +19 -0
  23. package/examples/playground/app/canvas/[canvasId]/page.tsx +22 -0
  24. package/examples/playground/app/canvas/[canvasId]/session.ts +19 -0
  25. package/examples/playground/app/canvas/events/route.ts +13 -0
  26. package/examples/playground/app/canvas/model.ts +94 -0
  27. package/examples/playground/app/canvas/open-canvas.tsx +40 -0
  28. package/examples/playground/app/canvas/page.tsx +20 -0
  29. package/examples/playground/app/canvas/server.ts +9 -0
  30. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +118 -0
  31. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +316 -0
  32. package/examples/playground/app/chat/[chatId]/chat-client.tsx +922 -0
  33. package/examples/playground/app/chat/[chatId]/chat-view.test.ts +152 -0
  34. package/examples/playground/app/chat/[chatId]/chat-view.ts +101 -0
  35. package/examples/playground/app/chat/[chatId]/composer.test.ts +44 -0
  36. package/examples/playground/app/chat/[chatId]/composer.ts +30 -0
  37. package/examples/playground/app/chat/[chatId]/page.tsx +30 -0
  38. package/examples/playground/app/chat/[chatId]/session.ts +7 -0
  39. package/examples/playground/app/chat/events/route.ts +7 -0
  40. package/examples/playground/app/chat/model.test.ts +155 -0
  41. package/examples/playground/app/chat/model.ts +310 -0
  42. package/examples/playground/app/chat/new-conversation.tsx +16 -0
  43. package/examples/playground/app/chat/page.tsx +25 -0
  44. package/examples/playground/app/chat/scheduler/route.ts +5 -0
  45. package/examples/playground/app/chat/server.ts +184 -0
  46. package/examples/playground/app/components/activity-feed.tsx +54 -0
  47. package/examples/playground/app/components/connection-pill.tsx +29 -0
  48. package/examples/playground/app/counter/counter-client.tsx +72 -0
  49. package/examples/playground/app/counter/events/route.ts +4 -0
  50. package/examples/playground/app/counter/model.test.ts +36 -0
  51. package/examples/playground/app/counter/model.ts +31 -0
  52. package/examples/playground/app/counter/page.tsx +24 -0
  53. package/examples/playground/app/counter/server.ts +9 -0
  54. package/examples/playground/app/counter/session.ts +13 -0
  55. package/examples/playground/app/documents/[documentId]/code-editor.tsx +80 -0
  56. package/examples/playground/app/documents/[documentId]/document-client.tsx +525 -0
  57. package/examples/playground/app/documents/[documentId]/page.tsx +23 -0
  58. package/examples/playground/app/documents/[documentId]/session.ts +7 -0
  59. package/examples/playground/app/documents/events/route.ts +4 -0
  60. package/examples/playground/app/documents/model.ts +55 -0
  61. package/examples/playground/app/documents/open-document.tsx +40 -0
  62. package/examples/playground/app/documents/page.tsx +22 -0
  63. package/examples/playground/app/documents/server.ts +9 -0
  64. package/examples/playground/app/globals.css +2078 -0
  65. package/examples/playground/app/layout.tsx +44 -0
  66. package/examples/playground/app/orders/[orderId]/order-client.tsx +140 -0
  67. package/examples/playground/app/orders/[orderId]/page.tsx +29 -0
  68. package/examples/playground/app/orders/[orderId]/session.ts +11 -0
  69. package/examples/playground/app/orders/create/route.ts +30 -0
  70. package/examples/playground/app/orders/events/route.ts +4 -0
  71. package/examples/playground/app/orders/model.ts +79 -0
  72. package/examples/playground/app/orders/new-order-form.tsx +98 -0
  73. package/examples/playground/app/orders/page.tsx +22 -0
  74. package/examples/playground/app/orders/scheduler/route.ts +5 -0
  75. package/examples/playground/app/orders/server.ts +50 -0
  76. package/examples/playground/app/page.tsx +102 -0
  77. package/examples/playground/app/recovery/[recoveryId]/page.tsx +31 -0
  78. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +144 -0
  79. package/examples/playground/app/recovery/[recoveryId]/session.ts +7 -0
  80. package/examples/playground/app/recovery/events/route.ts +3 -0
  81. package/examples/playground/app/recovery/model.ts +55 -0
  82. package/examples/playground/app/recovery/new-recovery-session.tsx +20 -0
  83. package/examples/playground/app/recovery/page.tsx +22 -0
  84. package/examples/playground/app/recovery/scheduler/route.ts +7 -0
  85. package/examples/playground/app/recovery/server.ts +53 -0
  86. package/examples/playground/app/recovery/start/route.ts +41 -0
  87. package/examples/playground/css.d.ts +4 -0
  88. package/examples/playground/lib/store.ts +15 -0
  89. package/examples/playground/next-env.d.ts +5 -0
  90. package/examples/playground/next.config.ts +10 -0
  91. package/examples/playground/package.json +46 -0
  92. package/examples/playground/tsconfig.json +37 -0
  93. package/examples/playground/vercel.json +40 -0
  94. 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,4 @@
1
+ import { agentServer } from '../server'
2
+
3
+ export const GET = agentServer.fetch
4
+ export const POST = agentServer.fetch
@@ -0,0 +1,3 @@
1
+ import { agent } from 'experimental-a2/ai'
2
+
3
+ export const demoAgent = agent({ name: 'agent' })
@@ -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,5 @@
1
+ import { agentScheduler, agentServer } from '../server'
2
+
3
+ export const POST =
4
+ agentScheduler?.handler(agentServer) ??
5
+ (() => new Response('agent scheduler is not configured', { status: 503 }))
@@ -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
+ })