experimental-a2 0.7.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 (150) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +29 -0
  4. package/dist/ai-server.d.ts +1 -1
  5. package/dist/ai-server.d.ts.map +1 -1
  6. package/dist/ai-server.js +13 -11
  7. package/dist/ai-server.js.map +1 -1
  8. package/dist/ai.d.ts +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/scheduler-qstash.d.ts +2 -2
  11. package/dist/scheduler-qstash.js +1 -1
  12. package/dist/scheduler-vercel.d.ts +2 -2
  13. package/dist/scheduler-vercel.js +1 -1
  14. package/dist/{server-286j79Mt.js → server-B2XNevQA.js} +123 -53
  15. package/dist/server-B2XNevQA.js.map +1 -0
  16. package/dist/{server-DgXmORIq.d.ts → server-DjPhHnbI.d.ts} +7 -4
  17. package/dist/server-DjPhHnbI.d.ts.map +1 -0
  18. package/dist/server.d.ts +3 -3
  19. package/dist/server.js +1 -1
  20. package/dist/store-N8PXxDAS.js.map +1 -1
  21. package/dist/{store-flRz1OWh.d.ts → store-RJO35BMj.d.ts} +25 -8
  22. package/dist/store-RJO35BMj.d.ts.map +1 -0
  23. package/dist/store-memory.d.ts +1 -1
  24. package/dist/store-memory.d.ts.map +1 -1
  25. package/dist/store-memory.js +79 -19
  26. package/dist/store-memory.js.map +1 -1
  27. package/dist/store-postgres.d.ts +1 -1
  28. package/dist/store-postgres.d.ts.map +1 -1
  29. package/dist/store-postgres.js +230 -101
  30. package/dist/store-postgres.js.map +1 -1
  31. package/dist/{store-redis-core-DEYO8Ryv.js → store-redis-core-DT01r4GZ.js} +167 -29
  32. package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
  33. package/dist/store-redis-http.d.ts +1 -1
  34. package/dist/store-redis-http.js +2 -2
  35. package/dist/store-redis-http.js.map +1 -1
  36. package/dist/store-redis.d.ts +1 -1
  37. package/dist/store-redis.js +2 -2
  38. package/dist/store-redis.js.map +1 -1
  39. package/dist/store-sqlite.d.ts +1 -1
  40. package/dist/store-sqlite.d.ts.map +1 -1
  41. package/dist/store-sqlite.js +103 -19
  42. package/dist/store-sqlite.js.map +1 -1
  43. package/docs/concepts/02-handlers.mdx +4 -0
  44. package/docs/concepts/04-state.mdx +57 -9
  45. package/docs/guides/06-ai-agents.mdx +2 -1
  46. package/docs/guides/07-examples.mdx +56 -0
  47. package/docs/index.mdx +13 -0
  48. package/docs/reference/01-api.mdx +39 -16
  49. package/examples/README.md +15 -0
  50. package/examples/playground/AGENTS.md +11 -0
  51. package/examples/playground/DEPLOY.md +106 -0
  52. package/examples/playground/README.md +19 -0
  53. package/examples/playground/activity-feed.test.ts +10 -0
  54. package/examples/playground/app/agent/[agentId]/agent-client.tsx +376 -0
  55. package/examples/playground/app/agent/[agentId]/page.tsx +29 -0
  56. package/examples/playground/app/agent/events/route.ts +4 -0
  57. package/examples/playground/app/agent/model.ts +3 -0
  58. package/examples/playground/app/agent/new-agent-session.tsx +98 -0
  59. package/examples/playground/app/agent/page.tsx +25 -0
  60. package/examples/playground/app/agent/scheduler/route.ts +5 -0
  61. package/examples/playground/app/agent/server.ts +153 -0
  62. package/examples/playground/app/agent/session.ts +13 -0
  63. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +682 -0
  64. package/examples/playground/app/canvas/[canvasId]/canvas-replay.test.ts +68 -0
  65. package/examples/playground/app/canvas/[canvasId]/canvas-replay.ts +19 -0
  66. package/examples/playground/app/canvas/[canvasId]/page.tsx +22 -0
  67. package/examples/playground/app/canvas/[canvasId]/session.ts +19 -0
  68. package/examples/playground/app/canvas/events/route.ts +13 -0
  69. package/examples/playground/app/canvas/model.ts +94 -0
  70. package/examples/playground/app/canvas/open-canvas.tsx +40 -0
  71. package/examples/playground/app/canvas/page.tsx +20 -0
  72. package/examples/playground/app/canvas/server.ts +9 -0
  73. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +118 -0
  74. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +316 -0
  75. package/examples/playground/app/chat/[chatId]/chat-client.tsx +922 -0
  76. package/examples/playground/app/chat/[chatId]/chat-view.test.ts +152 -0
  77. package/examples/playground/app/chat/[chatId]/chat-view.ts +101 -0
  78. package/examples/playground/app/chat/[chatId]/composer.test.ts +44 -0
  79. package/examples/playground/app/chat/[chatId]/composer.ts +30 -0
  80. package/examples/playground/app/chat/[chatId]/page.tsx +30 -0
  81. package/examples/playground/app/chat/[chatId]/session.ts +7 -0
  82. package/examples/playground/app/chat/events/route.ts +7 -0
  83. package/examples/playground/app/chat/model.test.ts +155 -0
  84. package/examples/playground/app/chat/model.ts +310 -0
  85. package/examples/playground/app/chat/new-conversation.tsx +16 -0
  86. package/examples/playground/app/chat/page.tsx +25 -0
  87. package/examples/playground/app/chat/scheduler/route.ts +5 -0
  88. package/examples/playground/app/chat/server.ts +184 -0
  89. package/examples/playground/app/components/activity-feed.tsx +54 -0
  90. package/examples/playground/app/components/connection-pill.tsx +29 -0
  91. package/examples/playground/app/counter/counter-client.tsx +72 -0
  92. package/examples/playground/app/counter/events/route.ts +4 -0
  93. package/examples/playground/app/counter/model.test.ts +36 -0
  94. package/examples/playground/app/counter/model.ts +31 -0
  95. package/examples/playground/app/counter/page.tsx +24 -0
  96. package/examples/playground/app/counter/server.ts +9 -0
  97. package/examples/playground/app/counter/session.ts +13 -0
  98. package/examples/playground/app/documents/[documentId]/code-editor.tsx +80 -0
  99. package/examples/playground/app/documents/[documentId]/document-client.tsx +525 -0
  100. package/examples/playground/app/documents/[documentId]/page.tsx +23 -0
  101. package/examples/playground/app/documents/[documentId]/session.ts +7 -0
  102. package/examples/playground/app/documents/events/route.ts +4 -0
  103. package/examples/playground/app/documents/model.ts +55 -0
  104. package/examples/playground/app/documents/open-document.tsx +40 -0
  105. package/examples/playground/app/documents/page.tsx +22 -0
  106. package/examples/playground/app/documents/server.ts +9 -0
  107. package/examples/playground/app/globals.css +2078 -0
  108. package/examples/playground/app/layout.tsx +44 -0
  109. package/examples/playground/app/orders/[orderId]/order-client.tsx +140 -0
  110. package/examples/playground/app/orders/[orderId]/page.tsx +29 -0
  111. package/examples/playground/app/orders/[orderId]/session.ts +11 -0
  112. package/examples/playground/app/orders/create/route.ts +30 -0
  113. package/examples/playground/app/orders/events/route.ts +4 -0
  114. package/examples/playground/app/orders/model.ts +79 -0
  115. package/examples/playground/app/orders/new-order-form.tsx +98 -0
  116. package/examples/playground/app/orders/page.tsx +22 -0
  117. package/examples/playground/app/orders/scheduler/route.ts +5 -0
  118. package/examples/playground/app/orders/server.ts +50 -0
  119. package/examples/playground/app/page.tsx +102 -0
  120. package/examples/playground/app/recovery/[recoveryId]/page.tsx +31 -0
  121. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +144 -0
  122. package/examples/playground/app/recovery/[recoveryId]/session.ts +7 -0
  123. package/examples/playground/app/recovery/events/route.ts +3 -0
  124. package/examples/playground/app/recovery/model.ts +55 -0
  125. package/examples/playground/app/recovery/new-recovery-session.tsx +20 -0
  126. package/examples/playground/app/recovery/page.tsx +22 -0
  127. package/examples/playground/app/recovery/scheduler/route.ts +7 -0
  128. package/examples/playground/app/recovery/server.ts +53 -0
  129. package/examples/playground/app/recovery/start/route.ts +41 -0
  130. package/examples/playground/css.d.ts +4 -0
  131. package/examples/playground/lib/store.ts +15 -0
  132. package/examples/playground/next-env.d.ts +5 -0
  133. package/examples/playground/next.config.ts +10 -0
  134. package/examples/playground/package.json +46 -0
  135. package/examples/playground/tsconfig.json +37 -0
  136. package/examples/playground/vercel.json +40 -0
  137. package/package.json +5 -2
  138. package/src/ai-server.ts +27 -10
  139. package/src/server.ts +242 -87
  140. package/src/store-memory.ts +138 -20
  141. package/src/store-postgres.ts +355 -138
  142. package/src/store-redis-core.ts +201 -27
  143. package/src/store-redis-http.ts +1 -1
  144. package/src/store-redis.ts +1 -1
  145. package/src/store-sqlite.ts +191 -34
  146. package/src/store.ts +27 -9
  147. package/dist/server-286j79Mt.js.map +0 -1
  148. package/dist/server-DgXmORIq.d.ts.map +0 -1
  149. package/dist/store-flRz1OWh.d.ts.map +0 -1
  150. package/dist/store-redis-core-DEYO8Ryv.js.map +0 -1
@@ -0,0 +1,144 @@
1
+ 'use client'
2
+ import type { ReactNode } from 'react'
3
+ import { useState } from 'react'
4
+ import { ActivityFeed } from '@/app/components/activity-feed'
5
+ import { ConnectionPill } from '@/app/components/connection-pill'
6
+ import { useSession } from './session'
7
+
8
+ const FUNCTION_LIMIT_SECONDS = 20
9
+ const STEP_SECONDS = 8
10
+ const STEP_COUNT = 4
11
+
12
+ export function RecoveryClient({
13
+ sessionId,
14
+ onVercel,
15
+ }: {
16
+ sessionId: string
17
+ onVercel: boolean
18
+ }): ReactNode {
19
+ const { state, events, index, connection } = useSession()
20
+ const [error, setError] = useState<string | null>(null)
21
+ const [starting, setStarting] = useState(false)
22
+
23
+ const run = async (): Promise<void> => {
24
+ setError(null)
25
+ setStarting(true)
26
+ try {
27
+ const response = await fetch('/recovery/start', {
28
+ method: 'POST',
29
+ headers: { 'content-type': 'application/json' },
30
+ body: JSON.stringify({ sessionId }),
31
+ })
32
+ if (!response.ok) {
33
+ throw new Error(`recovery trigger returned ${response.status}`)
34
+ }
35
+ } catch (err) {
36
+ setError(err instanceof Error ? err.message : String(err))
37
+ } finally {
38
+ setStarting(false)
39
+ }
40
+ }
41
+
42
+ const running = state.phase === 'running'
43
+ const completed = state.phase === 'completed'
44
+
45
+ return (
46
+ <article>
47
+ <header className="session-header">
48
+ <h1>Cross-invocation handler chain</h1>
49
+ <ConnectionPill connection={connection} />
50
+ <span
51
+ className={`badge${completed ? ' done' : running ? ' waiting' : ''}`}
52
+ >
53
+ {completed
54
+ ? 'completed'
55
+ : running
56
+ ? `${state.completedSteps}/${state.stepCount ?? STEP_COUNT} steps`
57
+ : 'ready'}
58
+ </span>
59
+ </header>
60
+
61
+ <p className="lede">
62
+ Four handlers fit individually, but their 32-second chain does not fit
63
+ in one 20-second function. Every invocation has the same limit.
64
+ </p>
65
+
66
+ <dl className="facts">
67
+ <dt>Function limit</dt>
68
+ <dd>{FUNCTION_LIMIT_SECONDS} seconds</dd>
69
+ <dt>Each handler</dt>
70
+ <dd>{STEP_SECONDS} seconds</dd>
71
+ <dt>Whole chain</dt>
72
+ <dd>
73
+ {STEP_COUNT} × {STEP_SECONDS} = {STEP_COUNT * STEP_SECONDS} seconds
74
+ </dd>
75
+ <dt>Queue consumer limit</dt>
76
+ <dd>{FUNCTION_LIMIT_SECONDS} seconds</dd>
77
+ </dl>
78
+
79
+ <button
80
+ disabled={state.phase !== 'idle' || starting || !onVercel}
81
+ onClick={() => void run()}
82
+ >
83
+ {starting ? 'Committing…' : 'Start four-step chain'}
84
+ </button>
85
+ {!onVercel ? (
86
+ <p className="hint">
87
+ Deploy this playground to Vercel to supply a real invocation deadline
88
+ and queue redelivery. <code>next dev</code> has neither boundary.
89
+ </p>
90
+ ) : null}
91
+
92
+ {running ? (
93
+ <section className="timeout-callout">
94
+ <h2>The chain is crossing function invocations</h2>
95
+ <p>
96
+ Each step waits eight seconds before appending its completion fact.
97
+ A2 does not predict whether the next handler fits. The first
98
+ invocation completes two handlers, starts the third, and is stopped
99
+ by Vercel at 20 seconds. That event stays pending. Its claim
100
+ watchdog acquires the expired claim and retries the same event in a
101
+ fresh function.
102
+ </p>
103
+ <p className="hint">
104
+ Keep this page open. Claim heartbeats keep scheduling watchdogs
105
+ while a handler is alive. The known function deadline caps the last
106
+ claim window, so the scheduler arrives just after Vercel stops the
107
+ worker. Reloading can also trigger A2&apos;s separate
108
+ interaction-healing path.
109
+ </p>
110
+ </section>
111
+ ) : null}
112
+
113
+ {completed ? (
114
+ <p className="proof">
115
+ All {state.completedSteps} bounded handlers finished. The scheduler
116
+ added no event of its own; it only continued draining the pending
117
+ application events.
118
+ </p>
119
+ ) : null}
120
+ {error ? <p className="error">{error}</p> : null}
121
+
122
+ <ActivityFeed
123
+ events={events}
124
+ index={index}
125
+ status={
126
+ running ? (
127
+ state.completedSteps < STEP_COUNT ? (
128
+ <>
129
+ Waiting for step {state.completedSteps + 1} of {STEP_COUNT}. The
130
+ event log stays quiet while its {STEP_SECONDS}-second handler
131
+ runs; the next row is the handler&apos;s real completion fact.
132
+ </>
133
+ ) : (
134
+ <>
135
+ All four handlers finished. Waiting for the final{' '}
136
+ <code>pipeline.completed</code> fact from the active drain.
137
+ </>
138
+ )
139
+ ) : undefined
140
+ }
141
+ />
142
+ </article>
143
+ )
144
+ }
@@ -0,0 +1,7 @@
1
+ 'use client'
2
+ import { createReact } from 'experimental-a2/react'
3
+ import { recoveryReducer } from '../model'
4
+
5
+ export const { SessionProvider, useSession } = createReact({
6
+ reducer: recoveryReducer,
7
+ })
@@ -0,0 +1,3 @@
1
+ import { recoveryServer } from '../server'
2
+
3
+ export const GET = recoveryServer.fetch
@@ -0,0 +1,55 @@
1
+ import * as a2 from 'experimental-a2'
2
+ import { z } from 'zod'
3
+
4
+ export const recovery = a2.contract({
5
+ name: 'recovery',
6
+ events: {
7
+ 'pipeline.started': z.object({
8
+ stepCount: z.literal(4),
9
+ stepDurationSeconds: z.literal(8),
10
+ }),
11
+ 'step.completed': z.object({
12
+ step: z.number().int().min(1).max(4),
13
+ durationSeconds: z.literal(8),
14
+ }),
15
+ 'pipeline.completed': z.object({ stepCount: z.literal(4) }),
16
+ },
17
+ })
18
+
19
+ const recoveryState = z.object({
20
+ phase: z.enum(['idle', 'running', 'completed']),
21
+ stepCount: z.number().int().positive().nullable(),
22
+ stepDurationSeconds: z.number().int().positive().nullable(),
23
+ completedSteps: z.number().int().nonnegative(),
24
+ })
25
+
26
+ export type RecoveryState = z.output<typeof recoveryState>
27
+
28
+ export const recoveryReducer = recovery
29
+ .reducer({
30
+ name: 'recovery',
31
+ initialState: {
32
+ phase: 'idle',
33
+ stepCount: null,
34
+ stepDurationSeconds: null,
35
+ completedSteps: 0,
36
+ },
37
+ stateSchema: recoveryState,
38
+ })
39
+ .fold((state, event) => {
40
+ switch (event.type) {
41
+ case 'pipeline.started':
42
+ return {
43
+ ...state,
44
+ phase: 'running',
45
+ stepCount: event.payload.stepCount,
46
+ stepDurationSeconds: event.payload.stepDurationSeconds,
47
+ }
48
+ case 'step.completed':
49
+ return { ...state, completedSteps: event.payload.step }
50
+ case 'pipeline.completed':
51
+ return { ...state, phase: 'completed' }
52
+ default:
53
+ return state
54
+ }
55
+ })
@@ -0,0 +1,20 @@
1
+ 'use client'
2
+ import type { ReactNode } from 'react'
3
+ import { useRouter } from 'next/navigation'
4
+
5
+ const newSessionId = (): string =>
6
+ `recovery-${Math.random().toString(36).slice(2, 8)}`
7
+
8
+ export function NewRecoverySession(): ReactNode {
9
+ const router = useRouter()
10
+
11
+ return (
12
+ <button
13
+ onClick={() =>
14
+ router.push(`/recovery/${encodeURIComponent(newSessionId())}`)
15
+ }
16
+ >
17
+ Start a recovery run
18
+ </button>
19
+ )
20
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from 'react'
2
+ import { NewRecoverySession } from './new-recovery-session'
3
+
4
+ export default function RecoveryPage(): ReactNode {
5
+ return (
6
+ <article>
7
+ <h1>Recovery lab</h1>
8
+ <p className="lede">
9
+ Run four 8-second handlers in a chain. Each fits inside a 20-second
10
+ Vercel function, but all 32 seconds cannot. Watch Vercel stop one
11
+ mid-handler, then let A2 retry that pending event in a fresh invocation
12
+ with the same limit.
13
+ </p>
14
+ <NewRecoverySession />
15
+ <p className="hint">
16
+ Each run gets a fresh session. This is enabled only on Vercel, where the
17
+ platform can supply the deadline and deliver the armed queue message.
18
+ The event log contains pipeline facts only.
19
+ </p>
20
+ </article>
21
+ )
22
+ }
@@ -0,0 +1,7 @@
1
+ import { recoveryScheduler, recoveryServer } from '../server'
2
+
3
+ export const maxDuration = 20
4
+
5
+ export const POST =
6
+ recoveryScheduler?.handler(recoveryServer) ??
7
+ (() => new Response('recovery scheduler is not configured', { status: 503 }))
@@ -0,0 +1,53 @@
1
+ import 'server-only'
2
+ import { vercelQueues } from 'experimental-a2/scheduler-vercel'
3
+ import { createServer } from 'experimental-a2/server'
4
+ import { store } from '@/lib/store'
5
+ import { recovery } from './model'
6
+
7
+ const stepCount = 4 as const
8
+ const stepDurationSeconds = 8 as const
9
+
10
+ const wait = (ms: number): Promise<void> =>
11
+ new Promise((resolve) => setTimeout(resolve, ms))
12
+
13
+ export const recoveryScheduler =
14
+ process.env.VERCEL || process.env.A2_QUEUES
15
+ ? vercelQueues({ topic: 'a2-recovery' })
16
+ : undefined
17
+
18
+ export const recoveryServer = createServer({
19
+ ...(store ? { store } : {}),
20
+ ...(recoveryScheduler ? { scheduler: recoveryScheduler } : {}),
21
+ contract: recovery,
22
+ handlers: {
23
+ 'pipeline.started': {
24
+ lane: 'pipeline',
25
+ handler: async () => {
26
+ await wait(stepDurationSeconds * 1_000)
27
+ return {
28
+ type: 'step.completed',
29
+ payload: { step: 1, durationSeconds: stepDurationSeconds },
30
+ }
31
+ },
32
+ },
33
+ 'step.completed': {
34
+ lane: 'pipeline',
35
+ handler: async ({ event }) => {
36
+ if (event.payload.step === stepCount) {
37
+ return {
38
+ type: 'pipeline.completed',
39
+ payload: { stepCount },
40
+ }
41
+ }
42
+ await wait(stepDurationSeconds * 1_000)
43
+ return {
44
+ type: 'step.completed',
45
+ payload: {
46
+ step: event.payload.step + 1,
47
+ durationSeconds: stepDurationSeconds,
48
+ },
49
+ }
50
+ },
51
+ },
52
+ },
53
+ })
@@ -0,0 +1,41 @@
1
+ import { A2Error } from 'experimental-a2'
2
+ import { recoveryServer } from '../server'
3
+
4
+ const errorResponse = (error: unknown): Response => {
5
+ if (error instanceof A2Error) {
6
+ return Response.json(
7
+ { error: { code: error.code, message: error.message } },
8
+ { status: error.code === 'STORE_UNAVAILABLE' ? 503 : 400 },
9
+ )
10
+ }
11
+ return Response.json(
12
+ { error: { message: 'internal error' } },
13
+ { status: 500 },
14
+ )
15
+ }
16
+
17
+ export const maxDuration = 20
18
+
19
+ export async function POST(request: Request): Promise<Response> {
20
+ try {
21
+ const body: unknown = await request.json()
22
+ const sessionId =
23
+ typeof body === 'object' && body !== null && 'sessionId' in body
24
+ ? Reflect.get(body, 'sessionId')
25
+ : undefined
26
+ if (typeof sessionId !== 'string' || sessionId.length === 0) {
27
+ throw new A2Error(
28
+ 'INVALID_PAYLOAD',
29
+ 'sessionId must be a non-empty string',
30
+ )
31
+ }
32
+ const result = await recoveryServer.session(sessionId).append({
33
+ id: `${sessionId}:pipeline`,
34
+ type: 'pipeline.started',
35
+ payload: { stepCount: 4, stepDurationSeconds: 8 },
36
+ })
37
+ return Response.json(result)
38
+ } catch (error) {
39
+ return errorResponse(error)
40
+ }
41
+ }
@@ -0,0 +1,4 @@
1
+ // Next's shipped types only declare '*.module.css'; plain global
2
+ // stylesheets need this for `tsc --noEmit` to accept the side-effect
3
+ // import in app/layout.tsx.
4
+ declare module '*.css'
@@ -0,0 +1,15 @@
1
+ import 'server-only'
2
+ import { postgres, type PostgresStore } from 'experimental-a2/store-postgres'
3
+ import { redis, type RedisStore } from 'experimental-a2/store-redis'
4
+
5
+ const redisUrl = process.env.REDIS_URL
6
+ const postgresUrl = process.env.DATABASE_URL?.replace(
7
+ /sslmode=(require|prefer|verify-ca)/,
8
+ 'sslmode=verify-full',
9
+ )
10
+
11
+ export const store: RedisStore | PostgresStore | undefined = redisUrl
12
+ ? redis({ url: redisUrl })
13
+ : postgresUrl
14
+ ? postgres({ connectionString: postgresUrl })
15
+ : undefined
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,10 @@
1
+ // No workarounds needed: experimental-a2/server is a separate server-only entry
2
+ // point, so client bundles never reach a store backend; the sqlite dev
3
+ // default carries its own turbopackIgnore annotation in a2's dist, so
4
+ // output tracing stays scoped without externalizing anything; and
5
+ // Next 16 transpiles this file with the TS7 (tsgo) compiler.
6
+ import type { NextConfig } from 'next'
7
+
8
+ const nextConfig: NextConfig = {}
9
+
10
+ export default nextConfig
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "a2-playground",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "Complete Next.js examples for experimental-a2.",
6
+ "type": "module",
7
+ "packageManager": "pnpm@11.20.0",
8
+ "engines": {
9
+ "node": ">=22.18"
10
+ },
11
+ "scripts": {
12
+ "dev": "next dev",
13
+ "build": "next build",
14
+ "test": "vitest run",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "@codemirror/state": "^6.7.1",
19
+ "@codemirror/view": "^6.43.8",
20
+ "@vercel/functions": "^3.9.3",
21
+ "@vercel/queue": "^0.2.0",
22
+ "@vercel/sandbox": "^3.0.0",
23
+ "ai": "^7.0.58",
24
+ "codemirror": "^6.0.2",
25
+ "experimental-a2": "0.8.1",
26
+ "ioredis": "^5.9.0",
27
+ "next": "^16.3.0",
28
+ "pg": "^8.16.0",
29
+ "react": "^19.0.0",
30
+ "react-dom": "^19.0.0",
31
+ "server-only": "^0.0.1",
32
+ "ws": "^8.21.3",
33
+ "y-codemirror.next": "^0.3.5",
34
+ "y-protocols": "^1.0.7",
35
+ "yjs": "^13.6.32",
36
+ "zod": "^4.4.3"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^24.0.0",
40
+ "@types/react": "^19.0.0",
41
+ "@types/react-dom": "^19.0.0",
42
+ "@types/ws": "^8.18.1",
43
+ "typescript": "^7.0.2",
44
+ "vitest": "^4.1.10"
45
+ }
46
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "strict": true,
10
+ "noUncheckedIndexedAccess": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "verbatimModuleSyntax": true,
13
+ "isolatedModules": true,
14
+ "esModuleInterop": true,
15
+ "resolveJsonModule": true,
16
+ "skipLibCheck": true,
17
+ "noEmit": true,
18
+ "allowJs": true,
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": ["./*"]
27
+ }
28
+ },
29
+ "include": [
30
+ "next-env.d.ts",
31
+ "**/*.ts",
32
+ "**/*.tsx",
33
+ ".next/types/**/*.ts",
34
+ ".next/dev/types/**/*.ts"
35
+ ],
36
+ "exclude": ["node_modules"]
37
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "functions": {
3
+ "app/orders/scheduler/route.ts": {
4
+ "experimentalTriggers": [
5
+ {
6
+ "type": "queue/v2beta",
7
+ "topic": "a2-orders",
8
+ "retryAfterSeconds": 30
9
+ }
10
+ ]
11
+ },
12
+ "app/chat/scheduler/route.ts": {
13
+ "experimentalTriggers": [
14
+ {
15
+ "type": "queue/v2beta",
16
+ "topic": "a2-chat",
17
+ "retryAfterSeconds": 30
18
+ }
19
+ ]
20
+ },
21
+ "app/agent/scheduler/route.ts": {
22
+ "experimentalTriggers": [
23
+ {
24
+ "type": "queue/v2beta",
25
+ "topic": "a2-agent",
26
+ "retryAfterSeconds": 30
27
+ }
28
+ ]
29
+ },
30
+ "app/recovery/scheduler/route.ts": {
31
+ "experimentalTriggers": [
32
+ {
33
+ "type": "queue/v2beta",
34
+ "topic": "a2-recovery",
35
+ "retryAfterSeconds": 30
36
+ }
37
+ ]
38
+ }
39
+ }
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-a2",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,8 +20,11 @@
20
20
  "files": [
21
21
  "dist",
22
22
  "docs",
23
+ "examples",
23
24
  "src",
24
- "CHANGELOG.md"
25
+ "AGENTS.md",
26
+ "CHANGELOG.md",
27
+ "README.md"
25
28
  ],
26
29
  "exports": {
27
30
  ".": "./dist/index.js",
package/src/ai-server.ts CHANGED
@@ -842,13 +842,16 @@ export function createHandlers<
842
842
  const scheduleNext = async (
843
843
  ctx: Pick<HandlerContext<D>, 'session'>,
844
844
  ): Promise<AppendInput<D> | void> =>
845
- requestForNextMessage((await ctx.session.state(coordinator)).state)
845
+ requestForNextMessage(
846
+ (await ctx.session.state(coordinator, { through: 'latest' })).state,
847
+ )
846
848
 
847
849
  const continueIfReady = async (
848
850
  ctx: Pick<HandlerContext<D>, 'session'>,
849
851
  generationId: string,
850
852
  ): Promise<void> => {
851
- const state = (await ctx.session.state(coordinator)).state
853
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
854
+ .state
852
855
  const response = state.response
853
856
  if (response?.generation?.generationId !== generationId) {
854
857
  return
@@ -1092,6 +1095,7 @@ export function createHandlers<
1092
1095
  call,
1093
1096
  ctx.session.history,
1094
1097
  )
1098
+ if (ctx.signal.aborted) return
1095
1099
  const scope: AmbientToolScope = {
1096
1100
  contract: options.agent.contract,
1097
1101
  context: ctx,
@@ -1106,7 +1110,8 @@ export function createHandlers<
1106
1110
  const handleToolCall = async (
1107
1111
  ctx: HandlerContext<D, 'ai.tool.called'>,
1108
1112
  ): Promise<AppendInput<D> | void> => {
1109
- const state = (await ctx.session.state(coordinator)).state
1113
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1114
+ .state
1110
1115
  const response = state.response
1111
1116
  const current = response?.calls.find(
1112
1117
  (candidate) => candidate.index === ctx.event.index,
@@ -1130,7 +1135,8 @@ export function createHandlers<
1130
1135
  const handleApproval = async (
1131
1136
  ctx: HandlerContext<D, 'ai.approval.responded'>,
1132
1137
  ): Promise<AppendInput<D> | void> => {
1133
- const state = (await ctx.session.state(coordinator)).state
1138
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1139
+ .state
1134
1140
  const response = state.response
1135
1141
  const current = response?.calls.find(
1136
1142
  (candidate) =>
@@ -1163,7 +1169,9 @@ export function createHandlers<
1163
1169
  const history = await ctx.session.history()
1164
1170
  const requestId = ctx.event.id
1165
1171
  const request = ctx.event.payload
1166
- const coordinatorState = (await ctx.session.state(coordinator)).state
1172
+ const coordinatorState = (
1173
+ await ctx.session.state(coordinator, { through: 'latest' })
1174
+ ).state
1167
1175
  if (
1168
1176
  coordinatorState.closed ||
1169
1177
  coordinatorState.response?.activeRequestId !== requestId
@@ -1317,7 +1325,9 @@ export function createHandlers<
1317
1325
  } as AppendInput<D>)
1318
1326
  await ctx.session.append('generation-start', ...startEvents)
1319
1327
 
1320
- const startedCoordinatorState = (await ctx.session.state(coordinator)).state
1328
+ const startedCoordinatorState = (
1329
+ await ctx.session.state(coordinator, { through: 'latest' })
1330
+ ).state
1321
1331
  if (
1322
1332
  startedCoordinatorState.response?.activeRequestId !== requestId ||
1323
1333
  startedCoordinatorState.response.generation?.generationId !== generationId
@@ -1364,8 +1374,12 @@ export function createHandlers<
1364
1374
  }
1365
1375
  }
1366
1376
 
1367
- const currentState = (await ctx.session.state(options.agent.reducer)).state
1368
- const currentCoordinatorState = (await ctx.session.state(coordinator)).state
1377
+ const currentState = (
1378
+ await ctx.session.state(options.agent.reducer, { through: 'latest' })
1379
+ ).state
1380
+ const currentCoordinatorState = (
1381
+ await ctx.session.state(coordinator, { through: 'latest' })
1382
+ ).state
1369
1383
  const generationMessages = activeContextMessages(
1370
1384
  currentState,
1371
1385
  currentCoordinatorState,
@@ -1381,6 +1395,7 @@ export function createHandlers<
1381
1395
  options.instructions === undefined
1382
1396
  ? undefined
1383
1397
  : await resolve(options.instructions, resolverContext)
1398
+ if (ctx.signal.aborted) return
1384
1399
  const generateContext: AgentGenerateContext<M, D, T> = {
1385
1400
  request: ctx.event,
1386
1401
  requestId,
@@ -1541,7 +1556,8 @@ export function createHandlers<
1541
1556
  const handleRetry = async (
1542
1557
  ctx: HandlerContext<D, 'ai.retry.requested'>,
1543
1558
  ): Promise<AppendInput<D> | void> => {
1544
- const state = (await ctx.session.state(coordinator)).state
1559
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1560
+ .state
1545
1561
  const response = state.response
1546
1562
  if (
1547
1563
  response?.status !== 'failed' ||
@@ -1564,7 +1580,8 @@ export function createHandlers<
1564
1580
  const handleInputResponse = async (
1565
1581
  ctx: HandlerContext<D, 'ai.input.responded'>,
1566
1582
  ): Promise<void> => {
1567
- const state = (await ctx.session.state(coordinator)).state
1583
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1584
+ .state
1568
1585
  const response = state.response
1569
1586
  if (
1570
1587
  response?.responseMessageId !== ctx.event.payload.messageId ||