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
package/AGENTS.md ADDED
@@ -0,0 +1,11 @@
1
+ # Using A2 from this package
2
+
3
+ When answering questions or editing applications that use A2:
4
+
5
+ 1. Read `docs/` for the supported API and guarantees.
6
+ 2. Read `examples/playground/` for complete application structure.
7
+ 3. Read `src/` only when implementation details are relevant.
8
+
9
+ Do not infer application patterns from `dist/`. The playground uses public
10
+ package entry points and shows where contracts, reducers, servers, route
11
+ handlers, clients, authorization, WebSocket capabilities, and schedulers live.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 3bff761: Include a standalone snapshot of the complete Next.js playground in the npm
8
+ package, with package and documentation indexes that direct coding agents to
9
+ the supported docs and example structure.
10
+
3
11
  ## 0.8.0
4
12
 
5
13
  ### Minor Changes
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # experimental-a2
2
+
3
+ Durable sync and reactions for things with a lifecycle. Each session has an
4
+ event log, derived state, handlers, timers, and a live client.
5
+
6
+ ## Read the package
7
+
8
+ The npm package includes more than compiled output:
9
+
10
+ - `docs/` defines the supported API.
11
+ - `examples/playground/` contains complete, standalone Next.js examples.
12
+ - `src/` contains the TypeScript implementation.
13
+
14
+ Coding agents should read `docs/` and `examples/playground/` before inferring
15
+ usage from `dist/` or internal implementation details. Every playground feature
16
+ owns its contract, reducer, server, route handlers, and scheduler when needed.
17
+ The package and copied playground both include `AGENTS.md` discovery files.
18
+
19
+ ## Copy the playground
20
+
21
+ ```sh
22
+ cp -R node_modules/experimental-a2/examples/playground ./a2-playground
23
+ cd a2-playground
24
+ pnpm install
25
+ pnpm dev
26
+ ```
27
+
28
+ The copied application has a standalone `package.json`. Its A2 dependency is
29
+ pinned to the package version that shipped the snapshot.
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: Complete examples
3
+ description: Read or copy the complete Next.js playground that ships inside the experimental-a2 package.
4
+ ---
5
+
6
+ ## Shipped with the package
7
+
8
+ Every `experimental-a2` installation includes a complete Next.js playground at
9
+ `node_modules/experimental-a2/examples/playground`.
10
+
11
+ The playground is not a collection of isolated snippets. Each feature owns its
12
+ contract, reducer, server, route handlers, client session, and scheduler when it
13
+ needs one. It uses only public package entry points.
14
+
15
+ Copy it into a new directory to run or modify it:
16
+
17
+ ```sh
18
+ cp -R node_modules/experimental-a2/examples/playground ./a2-playground
19
+ cd a2-playground
20
+ pnpm install
21
+ pnpm dev
22
+ ```
23
+
24
+ The copied `package.json` is standalone. Its A2 dependency is pinned to the
25
+ version that supplied the example.
26
+
27
+ The snapshot also includes an `AGENTS.md` file. Coding agents that support
28
+ repository instructions discover the example's structure as soon as they work
29
+ inside the copied directory.
30
+
31
+ ## Find the right example
32
+
33
+ - `app/counter` is the smallest complete contract, reducer, server, and React
34
+ client.
35
+ - `app/orders` adds handlers and a feature-owned scheduler.
36
+ - `app/chat` shows request-scoped authorization and durable AI generation.
37
+ - `app/agent` shows tools, approvals, reminders, and sandboxed commands.
38
+ - `app/canvas` adds optimistic state, presence, and WebSocket upgrades.
39
+ - `app/documents` combines A2 events with a collaborative document protocol.
40
+ - `app/recovery` demonstrates queue recovery across function invocations.
41
+
42
+ ## Guidance for coding agents
43
+
44
+ When A2 is installed locally, read these package directories in this order:
45
+
46
+ 1. `node_modules/experimental-a2/docs` for the supported API and guarantees.
47
+ 2. `node_modules/experimental-a2/examples/playground` for complete application
48
+ structure.
49
+ 3. `node_modules/experimental-a2/src` only when implementation details are
50
+ relevant.
51
+
52
+ Do not infer application patterns from `dist`. The playground is the reference
53
+ for how the public pieces fit together in a real Next.js application.
54
+
55
+ The npm snapshot is generated from `apps/playground` in the A2 repository when
56
+ the package is published, so the runnable app remains the canonical source.
package/docs/index.mdx CHANGED
@@ -250,6 +250,16 @@ See [A2 and your database](/guides/application-data).
250
250
 
251
251
  </details>
252
252
 
253
+ <details>
254
+ <summary>Does the package include complete examples?</summary>
255
+
256
+ Yes. Every installation includes a standalone Next.js playground at
257
+ `node_modules/experimental-a2/examples/playground`. It contains complete
258
+ contracts, reducers, servers, route handlers, clients, and schedulers using only
259
+ public package entry points. See [Complete examples](/guides/examples).
260
+
261
+ </details>
262
+
253
263
  ## Where to next
254
264
 
255
265
  <CardGroup cols={2}>
@@ -271,4 +281,7 @@ See [A2 and your database](/guides/application-data).
271
281
  <Card title="A2 and your database" href="/guides/application-data" icon="database">
272
282
  Decide what belongs in a session and what belongs in ordinary tables.
273
283
  </Card>
284
+ <Card title="Complete examples" href="/guides/examples" icon="code">
285
+ Read or copy the standalone Next.js playground included with the package.
286
+ </Card>
274
287
  </CardGroup>
@@ -0,0 +1,15 @@
1
+ # A2 examples
2
+
3
+ The complete Next.js playground is included at `examples/playground` in the
4
+ published `experimental-a2` package. It uses only public package entry points
5
+ and can be copied into a new directory as a standalone application.
6
+
7
+ For coding agents, inspect these paths before inventing an integration:
8
+
9
+ - `docs/` defines the supported API.
10
+ - `examples/playground/` shows complete applications using that API.
11
+ - `src/` is the library implementation, not the recommended application
12
+ structure.
13
+
14
+ The playground snapshot is generated from the repository's `apps/playground`
15
+ directory when the package is packed.
@@ -0,0 +1,11 @@
1
+ # A2 playground
2
+
3
+ This is a standalone reference application for `experimental-a2`.
4
+
5
+ - Read `node_modules/experimental-a2/docs` for the supported API.
6
+ - Treat each directory under `app/` as a self-contained feature.
7
+ - Keep its contract and reducer in `model.ts`.
8
+ - Keep handlers and scheduler configuration in `server.ts`.
9
+ - Expose A2 through the feature's `events/route.ts` using `server.fetch`.
10
+ - Add a feature-owned `scheduler/route.ts` only when handlers or timers need it.
11
+ - Use only public `experimental-a2` package entry points.
@@ -0,0 +1,106 @@
1
+ # Deploying the playground
2
+
3
+ The event log runs with zero setup in development (SQLite at `.a2/dev.db`,
4
+ no scheduler). The Agent demo also needs AI Gateway and Vercel Sandbox
5
+ credentials. Production needs the deliberate pieces from
6
+ `node_modules/experimental-a2/docs/guides/05-production.mdx`.
7
+
8
+ ## 1. A store
9
+
10
+ Two options — set one of:
11
+
12
+ ```
13
+ REDIS_URL=rediss://… # push-native streams (Upstash; recommended here)
14
+ DATABASE_URL=postgres://… # any Postgres (Neon, Supabase, RDS)
15
+ ```
16
+
17
+ `lib/store.ts` prefers Redis when both are set — the chat, document,
18
+ and canvas demos are stream-latency-sensitive, and the Redis backend
19
+ delivers to parked subscribers in single-digit milliseconds (Postgres
20
+ streams poll adaptively, 25–250ms). The canvas demo's presence plane
21
+ (cursors, drag ghosts) also rides the backend's delivery tier: push on
22
+ Redis, poll cadence elsewhere. Upstash is durable by default; use its TCP
23
+ `rediss://` URL, not the REST one. Each feature creates its own server with
24
+ the shared store; storage remains namespaced per contract. Without either variable, a
25
+ production boot fails fast with `STORE_NOT_CONFIGURED` (by design).
26
+ Schemas/keys are created lazily on first use; no migration step.
27
+
28
+ To verify a provider before leaning on it, run the full conformance
29
+ kit against it (throwaway database; the suite isolates by key prefix
30
+ and sweeps after itself):
31
+
32
+ ```sh
33
+ TEST_REDIS_URL=rediss://… pnpm --filter a2 exec vitest run test/store-redis.conformance.test.ts
34
+ ```
35
+
36
+ `pg` and `ioredis` are optional peer dependencies of `experimental-a2` and direct
37
+ dependencies of this app.
38
+
39
+ ## 2. Vercel Queues scheduler
40
+
41
+ Queues need no configuration beyond the triggers in `vercel.json`. Orders,
42
+ Chat, Agent, and Recovery each own a scheduler route and queue topic alongside
43
+ their contract and server. Each topic is created on first send, and its trigger
44
+ makes the scheduler route private (no public URL). Schedulers turn on
45
+ automatically when `VERCEL` is set (every deployment).
46
+
47
+ To exercise the scheduler locally against a linked project:
48
+
49
+ ```sh
50
+ vercel link
51
+ vercel env pull # OIDC credentials for the @vercel/queue SDK
52
+ A2_QUEUES=1 pnpm dev
53
+ ```
54
+
55
+ ## 3. AI Gateway
56
+
57
+ The Agent demo uses the AI SDK with `openai/gpt-5.6-luna`, while the Chat demo
58
+ uses `openai/gpt-5.4`. Both resolve through Vercel AI Gateway. Vercel
59
+ deployments use their OIDC token automatically. For local generation, link the
60
+ project and pull its environment or set an `AI_GATEWAY_API_KEY`:
61
+
62
+ ```sh
63
+ vercel link
64
+ vercel env pull
65
+ ```
66
+
67
+ New Chat rooms invoke the model only for `@agent` mentions. Participants can
68
+ enable ambient mode for the room to invoke it for every subsequent message.
69
+
70
+ The Counter, Orders, Canvas, and Documents demos do not call a model.
71
+
72
+ ## 4. Vercel Sandbox
73
+
74
+ The Agent demo's Bash tool executes commands in a named, persistent Vercel
75
+ Sandbox. Every A2 agent session gets its own sandbox, so files remain available
76
+ to later Bash calls in that conversation. Vercel deployments authenticate with
77
+ OIDC automatically; the `vercel link` and `vercel env pull` commands above
78
+ supply the same credentials during local development.
79
+
80
+ ## 5. WebSockets for the canvas
81
+
82
+ Deployed, the canvas demo upgrades its route to a WebSocket
83
+ (`experimental_upgradeWebSocket` from `@vercel/functions`, with `ws`
84
+ as the server-side socket — both direct dependencies of this app).
85
+ Vercel WebSockets need Fluid compute, which is the default on new
86
+ projects. Under `next dev` the upgrade never reaches the route (the
87
+ dev server drops the handshake), so the demo's session module picks
88
+ the SSE + POST pair in development — same route, same protocol
89
+ semantics, different wire.
90
+
91
+ ## 6. Deploy
92
+
93
+ ```sh
94
+ vercel deploy
95
+ ```
96
+
97
+ The Recovery lab tests a real function timeout. Its dedicated trigger and
98
+ queue consumer both export `maxDuration = 20`. The pipeline has four 8-second
99
+ handlers, so every handler fits in a fresh invocation but the 32-second chain
100
+ does not. A2 keeps dispatching events. Vercel stops the first invocation while
101
+ the third handler is running, leaving that event without a processed marker.
102
+ The deadline-capped claim expires, its watchdog retries the same event in a
103
+ fresh function, and already completed events do not run again. The dedicated
104
+ `a2-recovery` topic keeps this artificial budget away from the
105
+ playground's other recovery workloads. This example is disabled under
106
+ `next dev`, which supplies neither a Vercel deadline nor queue delivery.
@@ -0,0 +1,19 @@
1
+ # A2 playground
2
+
3
+ This is a standalone snapshot of the A2 playground included with the
4
+ `experimental-a2` npm package. It uses only public package entry points.
5
+
6
+ ## Run it
7
+
8
+ ```sh
9
+ pnpm install
10
+ pnpm dev
11
+ ```
12
+
13
+ Each directory under `app/` owns its contract, reducer, server, routes, and
14
+ scheduler when one is needed. Start with `app/counter` for the smallest
15
+ example, then read `app/orders`, `app/chat`, or `app/agent` for complete
16
+ handler-driven applications.
17
+
18
+ The canonical source lives in `apps/playground` in the A2 repository. This
19
+ copy is generated from it when the package is published.
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { formatActivityTime } from './app/components/activity-feed'
3
+
4
+ describe('activity feed', () => {
5
+ it('formats event times identically on the server and client', () => {
6
+ expect(formatActivityTime(new Date('2026-08-18T01:16:09.123Z'))).toBe(
7
+ '01:16:09 UTC',
8
+ )
9
+ })
10
+ })
@@ -0,0 +1,376 @@
1
+ 'use client'
2
+ import type {
3
+ FormEvent,
4
+ KeyboardEvent as ReactKeyboardEvent,
5
+ ReactNode,
6
+ } from 'react'
7
+ import { useEffect, useRef, useState } from 'react'
8
+ import type { UIMessage } from 'ai'
9
+ import { inputs } from 'experimental-a2/ai'
10
+ import { ActivityFeed } from '@/app/components/activity-feed'
11
+ import { ConnectionPill } from '@/app/components/connection-pill'
12
+ import { useSession } from '../session'
13
+
14
+ type MessagePart = UIMessage['parts'][number]
15
+ type ToolPartView = {
16
+ name: string
17
+ state: string
18
+ input: unknown
19
+ output?: unknown
20
+ approved?: boolean
21
+ }
22
+
23
+ const STATUS_LABEL = {
24
+ idle: 'idle',
25
+ generating: 'working…',
26
+ waiting: 'approval needed',
27
+ failed: 'failed',
28
+ closed: 'closed',
29
+ } as const
30
+
31
+ const toolTitle = (name: string): string =>
32
+ name === 'inspectService'
33
+ ? 'Inspect service'
34
+ : name === 'restartService'
35
+ ? 'Restart service'
36
+ : name === 'setReminder'
37
+ ? 'Set reminder'
38
+ : name === 'bash'
39
+ ? 'Bash'
40
+ : name
41
+
42
+ const toolState = (part: ToolPartView): string => {
43
+ switch (part.state) {
44
+ case 'approval-requested':
45
+ return 'waiting for approval'
46
+ case 'approval-responded':
47
+ return part.approved ? 'approved' : 'denied'
48
+ case 'output-available':
49
+ return 'completed'
50
+ case 'output-error':
51
+ return 'failed'
52
+ case 'output-denied':
53
+ return 'denied'
54
+ default:
55
+ return 'running'
56
+ }
57
+ }
58
+
59
+ const toolPartView = (part: MessagePart): ToolPartView | null => {
60
+ if (part.type !== 'dynamic-tool' && !part.type.startsWith('tool-'))
61
+ return null
62
+ const candidate = part as unknown as Record<string, unknown>
63
+ const name =
64
+ part.type === 'dynamic-tool'
65
+ ? candidate['toolName']
66
+ : part.type.slice('tool-'.length)
67
+ if (typeof name !== 'string' || typeof candidate['state'] !== 'string') {
68
+ return null
69
+ }
70
+ const approval = candidate['approval']
71
+ return {
72
+ name,
73
+ state: candidate['state'],
74
+ input: candidate['input'],
75
+ ...(candidate['output'] === undefined
76
+ ? {}
77
+ : { output: candidate['output'] }),
78
+ ...(typeof approval === 'object' &&
79
+ approval !== null &&
80
+ 'approved' in approval &&
81
+ typeof approval.approved === 'boolean'
82
+ ? { approved: approval.approved }
83
+ : {}),
84
+ }
85
+ }
86
+
87
+ function ToolCard({ part }: { part: ToolPartView }): ReactNode {
88
+ return (
89
+ <div className={`tool-card ${part.state}`}>
90
+ <div>
91
+ <strong>{toolTitle(part.name)}</strong>
92
+ <span>{toolState(part)}</span>
93
+ </div>
94
+ <code>{JSON.stringify(part.input)}</code>
95
+ {part.output === undefined ? null : (
96
+ <pre>{JSON.stringify(part.output, null, 2)}</pre>
97
+ )}
98
+ </div>
99
+ )
100
+ }
101
+
102
+ function MessagePartView({ part }: { part: MessagePart }): ReactNode {
103
+ if (part.type === 'text') return part.text ? <p>{part.text}</p> : null
104
+ const tool = toolPartView(part)
105
+ if (tool) return <ToolCard part={tool} />
106
+ return null
107
+ }
108
+
109
+ const hasVisibleParts = (message: UIMessage): boolean =>
110
+ message.parts.some(
111
+ (part) =>
112
+ (part.type === 'text' && part.text.length > 0) ||
113
+ toolPartView(part) !== null,
114
+ )
115
+
116
+ function Thinking(): ReactNode {
117
+ return (
118
+ <p className="agent-thinking" role="status">
119
+ <span aria-hidden="true" />
120
+ Thinking…
121
+ </p>
122
+ )
123
+ }
124
+
125
+ function AgentMessage({
126
+ message,
127
+ thinking,
128
+ }: {
129
+ message: UIMessage
130
+ thinking: boolean
131
+ }): ReactNode {
132
+ const role =
133
+ message.role === 'user'
134
+ ? message.id.startsWith('reminder:')
135
+ ? 'Reminder'
136
+ : 'You'
137
+ : 'Agent'
138
+
139
+ return (
140
+ <div className={`agent-message ${message.role}`}>
141
+ <span className="agent-role">{role}</span>
142
+ <div>
143
+ {message.parts.map((part, partIndex) => (
144
+ <MessagePartView key={`${message.id}:${partIndex}`} part={part} />
145
+ ))}
146
+ {thinking ? <Thinking /> : null}
147
+ </div>
148
+ </div>
149
+ )
150
+ }
151
+
152
+ function submitOnEnter(event: ReactKeyboardEvent<HTMLTextAreaElement>): void {
153
+ if (
154
+ event.key !== 'Enter' ||
155
+ event.shiftKey ||
156
+ event.nativeEvent.isComposing
157
+ ) {
158
+ return
159
+ }
160
+ event.preventDefault()
161
+ event.currentTarget.form?.requestSubmit()
162
+ }
163
+
164
+ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
165
+ const { state, push, events, index, connection } = useSession()
166
+ const promptRef = useRef<HTMLTextAreaElement>(null)
167
+ const [prompt, setPrompt] = useState('')
168
+ const [error, setError] = useState<string | null>(null)
169
+ const [responding, setResponding] = useState(false)
170
+
171
+ const generating = state.status === 'generating'
172
+ const canSend = state.status === 'idle' || state.status === 'failed'
173
+
174
+ useEffect(() => {
175
+ if (canSend) promptRef.current?.focus()
176
+ }, [canSend])
177
+
178
+ const send = async (event: FormEvent): Promise<void> => {
179
+ event.preventDefault()
180
+ const submittedPrompt = prompt
181
+ const text = prompt.trim()
182
+ if (!text || !canSend) return
183
+ setPrompt('')
184
+ setError(null)
185
+ try {
186
+ const message: UIMessage = {
187
+ id: crypto.randomUUID(),
188
+ role: 'user',
189
+ parts: [{ type: 'text', text }],
190
+ }
191
+ await push(...inputs.message(message))
192
+ } catch (err) {
193
+ setPrompt((current) => (current === '' ? submittedPrompt : current))
194
+ setError(err instanceof Error ? err.message : String(err))
195
+ }
196
+ }
197
+
198
+ const answerApproval = async (
199
+ messageId: string,
200
+ generationId: string,
201
+ approvalId: string,
202
+ approved: boolean,
203
+ ): Promise<void> => {
204
+ setResponding(true)
205
+ setError(null)
206
+ try {
207
+ await push(
208
+ ...inputs.approval({
209
+ messageId,
210
+ generationId,
211
+ approvalId,
212
+ approved,
213
+ ...(!approved ? { reason: 'operator denied the restart' } : {}),
214
+ }),
215
+ )
216
+ } catch (err) {
217
+ setError(err instanceof Error ? err.message : String(err))
218
+ } finally {
219
+ setResponding(false)
220
+ }
221
+ }
222
+
223
+ const stop = async (): Promise<void> => {
224
+ const active = state.activeGeneration
225
+ if (!active) return
226
+ setError(null)
227
+ try {
228
+ await push(
229
+ ...inputs.interrupt({
230
+ messageId: active.responseMessageId,
231
+ generationId: active.generationId,
232
+ reason: 'operator stopped the agent',
233
+ lastSeenIndex: index,
234
+ }),
235
+ )
236
+ } catch (err) {
237
+ setError(err instanceof Error ? err.message : String(err))
238
+ }
239
+ }
240
+
241
+ return (
242
+ <article>
243
+ <header className="session-header">
244
+ <h1>
245
+ Agent <code>{agentId}</code>
246
+ </h1>
247
+ <ConnectionPill connection={connection} />
248
+ <span
249
+ className={`badge${generating ? ' live' : ''}${
250
+ state.status === 'waiting' ? ' waiting' : ''
251
+ }${state.status === 'failed' ? ' cut' : ''}`}
252
+ >
253
+ {STATUS_LABEL[state.status]}
254
+ </span>
255
+ </header>
256
+
257
+ <section className="agent-conversation" aria-label="Conversation">
258
+ <div className="agent-transcript" aria-live="polite">
259
+ {state.messages.length === 0 ? (
260
+ <p className="agent-empty">
261
+ Ask the agent to investigate an incident, run a Bash command, or
262
+ set a reminder for itself. Its tools and decisions will appear
263
+ here as they enter the log.
264
+ </p>
265
+ ) : (
266
+ state.messages.map((message) => (
267
+ <AgentMessage
268
+ key={message.id}
269
+ message={message}
270
+ thinking={
271
+ generating &&
272
+ message.role === 'assistant' &&
273
+ !hasVisibleParts(message)
274
+ }
275
+ />
276
+ ))
277
+ )}
278
+ {generating && state.messages.at(-1)?.role === 'user' ? (
279
+ <div className="agent-message assistant">
280
+ <span className="agent-role">Agent</span>
281
+ <div>
282
+ <Thinking />
283
+ </div>
284
+ </div>
285
+ ) : null}
286
+ </div>
287
+
288
+ {state.pendingApprovals.map((approval) => (
289
+ <section className="approval-card" key={approval.approvalId}>
290
+ <div>
291
+ <p className="eyebrow">Human approval</p>
292
+ <h2>Restart checkout-api?</h2>
293
+ <p>
294
+ The agent inspected a 12.4% error rate. The runbook recommends a
295
+ restart after a sustained 10%.
296
+ </p>
297
+ </div>
298
+ <div className="approval-actions">
299
+ <button
300
+ type="button"
301
+ disabled={responding}
302
+ onClick={() =>
303
+ void answerApproval(
304
+ approval.messageId,
305
+ approval.generationId,
306
+ approval.approvalId,
307
+ true,
308
+ )
309
+ }
310
+ >
311
+ Approve restart
312
+ </button>
313
+ <button
314
+ type="button"
315
+ className="secondary"
316
+ disabled={responding}
317
+ onClick={() =>
318
+ void answerApproval(
319
+ approval.messageId,
320
+ approval.generationId,
321
+ approval.approvalId,
322
+ false,
323
+ )
324
+ }
325
+ >
326
+ Deny
327
+ </button>
328
+ </div>
329
+ </section>
330
+ ))}
331
+
332
+ {state.error ? (
333
+ <p className="error agent-generation-error" role="alert">
334
+ {state.error}
335
+ </p>
336
+ ) : null}
337
+
338
+ <form className="agent-form" onSubmit={(event) => void send(event)}>
339
+ <label>
340
+ Message
341
+ <textarea
342
+ ref={promptRef}
343
+ autoFocus
344
+ rows={3}
345
+ value={prompt}
346
+ onChange={(event) => setPrompt(event.target.value)}
347
+ onKeyDown={submitOnEnter}
348
+ disabled={!canSend}
349
+ />
350
+ </label>
351
+ <div className="agent-form-actions">
352
+ <button type="submit" disabled={!canSend || !prompt.trim()}>
353
+ Send to agent
354
+ </button>
355
+ <button
356
+ type="button"
357
+ className="danger"
358
+ disabled={!generating}
359
+ onClick={() => void stop()}
360
+ >
361
+ Stop
362
+ </button>
363
+ </div>
364
+ </form>
365
+ </section>
366
+ {error ? <p className="error">{error}</p> : null}
367
+ <p className="hint">
368
+ The browser only appends inputs. <code>createAgentServer()</code>{' '}
369
+ records the generation, progress snapshots, tool activity, and pause;
370
+ approving or delivering a reminder wakes the same durable session.
371
+ </p>
372
+
373
+ <ActivityFeed events={events} index={index} />
374
+ </article>
375
+ )
376
+ }