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.
- package/AGENTS.md +11 -0
- package/CHANGELOG.md +27 -0
- package/README.md +29 -0
- package/dist/ai-server.d.ts +1 -1
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +13 -11
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-286j79Mt.js → server-B2XNevQA.js} +123 -53
- package/dist/server-B2XNevQA.js.map +1 -0
- package/dist/{server-DgXmORIq.d.ts → server-DjPhHnbI.d.ts} +7 -4
- package/dist/server-DjPhHnbI.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-flRz1OWh.d.ts → store-RJO35BMj.d.ts} +25 -8
- package/dist/store-RJO35BMj.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +79 -19
- package/dist/store-memory.js.map +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +230 -101
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-DEYO8Ryv.js → store-redis-core-DT01r4GZ.js} +167 -29
- package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +2 -2
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +103 -19
- package/dist/store-sqlite.js.map +1 -1
- package/docs/concepts/02-handlers.mdx +4 -0
- package/docs/concepts/04-state.mdx +57 -9
- package/docs/guides/06-ai-agents.mdx +2 -1
- package/docs/guides/07-examples.mdx +56 -0
- package/docs/index.mdx +13 -0
- package/docs/reference/01-api.mdx +39 -16
- 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
- package/src/ai-server.ts +27 -10
- package/src/server.ts +242 -87
- package/src/store-memory.ts +138 -20
- package/src/store-postgres.ts +355 -138
- package/src/store-redis-core.ts +201 -27
- package/src/store-redis-http.ts +1 -1
- package/src/store-redis.ts +1 -1
- package/src/store-sqlite.ts +191 -34
- package/src/store.ts +27 -9
- package/dist/server-286j79Mt.js.map +0 -1
- package/dist/server-DgXmORIq.d.ts.map +0 -1
- package/dist/store-flRz1OWh.d.ts.map +0 -1
- 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'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'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,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,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,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,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.
|
|
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
|
-
"
|
|
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(
|
|
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))
|
|
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))
|
|
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))
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
1368
|
-
|
|
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))
|
|
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))
|
|
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 ||
|