experimental-a2 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -0
- package/CHANGELOG.md +8 -0
- package/README.md +29 -0
- package/docs/guides/07-examples.mdx +56 -0
- package/docs/index.mdx +13 -0
- package/examples/README.md +15 -0
- package/examples/playground/AGENTS.md +11 -0
- package/examples/playground/DEPLOY.md +106 -0
- package/examples/playground/README.md +19 -0
- package/examples/playground/activity-feed.test.ts +10 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +376 -0
- package/examples/playground/app/agent/[agentId]/page.tsx +29 -0
- package/examples/playground/app/agent/events/route.ts +4 -0
- package/examples/playground/app/agent/model.ts +3 -0
- package/examples/playground/app/agent/new-agent-session.tsx +98 -0
- package/examples/playground/app/agent/page.tsx +25 -0
- package/examples/playground/app/agent/scheduler/route.ts +5 -0
- package/examples/playground/app/agent/server.ts +153 -0
- package/examples/playground/app/agent/session.ts +13 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +682 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-replay.test.ts +68 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-replay.ts +19 -0
- package/examples/playground/app/canvas/[canvasId]/page.tsx +22 -0
- package/examples/playground/app/canvas/[canvasId]/session.ts +19 -0
- package/examples/playground/app/canvas/events/route.ts +13 -0
- package/examples/playground/app/canvas/model.ts +94 -0
- package/examples/playground/app/canvas/open-canvas.tsx +40 -0
- package/examples/playground/app/canvas/page.tsx +20 -0
- package/examples/playground/app/canvas/server.ts +9 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +118 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +316 -0
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +922 -0
- package/examples/playground/app/chat/[chatId]/chat-view.test.ts +152 -0
- package/examples/playground/app/chat/[chatId]/chat-view.ts +101 -0
- package/examples/playground/app/chat/[chatId]/composer.test.ts +44 -0
- package/examples/playground/app/chat/[chatId]/composer.ts +30 -0
- package/examples/playground/app/chat/[chatId]/page.tsx +30 -0
- package/examples/playground/app/chat/[chatId]/session.ts +7 -0
- package/examples/playground/app/chat/events/route.ts +7 -0
- package/examples/playground/app/chat/model.test.ts +155 -0
- package/examples/playground/app/chat/model.ts +310 -0
- package/examples/playground/app/chat/new-conversation.tsx +16 -0
- package/examples/playground/app/chat/page.tsx +25 -0
- package/examples/playground/app/chat/scheduler/route.ts +5 -0
- package/examples/playground/app/chat/server.ts +184 -0
- package/examples/playground/app/components/activity-feed.tsx +54 -0
- package/examples/playground/app/components/connection-pill.tsx +29 -0
- package/examples/playground/app/counter/counter-client.tsx +72 -0
- package/examples/playground/app/counter/events/route.ts +4 -0
- package/examples/playground/app/counter/model.test.ts +36 -0
- package/examples/playground/app/counter/model.ts +31 -0
- package/examples/playground/app/counter/page.tsx +24 -0
- package/examples/playground/app/counter/server.ts +9 -0
- package/examples/playground/app/counter/session.ts +13 -0
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +80 -0
- package/examples/playground/app/documents/[documentId]/document-client.tsx +525 -0
- package/examples/playground/app/documents/[documentId]/page.tsx +23 -0
- package/examples/playground/app/documents/[documentId]/session.ts +7 -0
- package/examples/playground/app/documents/events/route.ts +4 -0
- package/examples/playground/app/documents/model.ts +55 -0
- package/examples/playground/app/documents/open-document.tsx +40 -0
- package/examples/playground/app/documents/page.tsx +22 -0
- package/examples/playground/app/documents/server.ts +9 -0
- package/examples/playground/app/globals.css +2078 -0
- package/examples/playground/app/layout.tsx +44 -0
- package/examples/playground/app/orders/[orderId]/order-client.tsx +140 -0
- package/examples/playground/app/orders/[orderId]/page.tsx +29 -0
- package/examples/playground/app/orders/[orderId]/session.ts +11 -0
- package/examples/playground/app/orders/create/route.ts +30 -0
- package/examples/playground/app/orders/events/route.ts +4 -0
- package/examples/playground/app/orders/model.ts +79 -0
- package/examples/playground/app/orders/new-order-form.tsx +98 -0
- package/examples/playground/app/orders/page.tsx +22 -0
- package/examples/playground/app/orders/scheduler/route.ts +5 -0
- package/examples/playground/app/orders/server.ts +50 -0
- package/examples/playground/app/page.tsx +102 -0
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +31 -0
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +144 -0
- package/examples/playground/app/recovery/[recoveryId]/session.ts +7 -0
- package/examples/playground/app/recovery/events/route.ts +3 -0
- package/examples/playground/app/recovery/model.ts +55 -0
- package/examples/playground/app/recovery/new-recovery-session.tsx +20 -0
- package/examples/playground/app/recovery/page.tsx +22 -0
- package/examples/playground/app/recovery/scheduler/route.ts +7 -0
- package/examples/playground/app/recovery/server.ts +53 -0
- package/examples/playground/app/recovery/start/route.ts +41 -0
- package/examples/playground/css.d.ts +4 -0
- package/examples/playground/lib/store.ts +15 -0
- package/examples/playground/next-env.d.ts +5 -0
- package/examples/playground/next.config.ts +10 -0
- package/examples/playground/package.json +46 -0
- package/examples/playground/tsconfig.json +37 -0
- package/examples/playground/vercel.json +40 -0
- package/package.json +5 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ContractEvent } from 'experimental-a2'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { canvas, type CanvasState } from '../model'
|
|
4
|
+
import { confirmedCanvasEventsSince, replayCanvas } from './canvas-replay'
|
|
5
|
+
|
|
6
|
+
type CanvasEvents = typeof canvas.events
|
|
7
|
+
type CanvasEvent<
|
|
8
|
+
K extends keyof CanvasEvents & string = keyof CanvasEvents & string,
|
|
9
|
+
> = ContractEvent<CanvasEvents, K>
|
|
10
|
+
|
|
11
|
+
const event = <K extends keyof CanvasEvents & string>(
|
|
12
|
+
index: number,
|
|
13
|
+
type: K,
|
|
14
|
+
payload: CanvasEvent<K>['payload'],
|
|
15
|
+
): CanvasEvent<K> =>
|
|
16
|
+
({
|
|
17
|
+
id: `event-${index}`,
|
|
18
|
+
type,
|
|
19
|
+
payload,
|
|
20
|
+
index,
|
|
21
|
+
sessionId: 'canvas-1',
|
|
22
|
+
createdAt: new Date(index * 1_000),
|
|
23
|
+
}) as CanvasEvent<K>
|
|
24
|
+
|
|
25
|
+
describe('canvas replay', () => {
|
|
26
|
+
it('replays confirmed events after the server snapshot', () => {
|
|
27
|
+
const snapshot: CanvasState = {
|
|
28
|
+
shapes: {
|
|
29
|
+
square: {
|
|
30
|
+
id: 'square',
|
|
31
|
+
kind: 'rect',
|
|
32
|
+
x: 10,
|
|
33
|
+
y: 10,
|
|
34
|
+
w: 80,
|
|
35
|
+
h: 60,
|
|
36
|
+
color: 'teal',
|
|
37
|
+
movedAt: 4,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
const observed: CanvasEvent[] = [
|
|
42
|
+
event(4, 'shape.moved', { id: 'square', x: 99, y: 99 }),
|
|
43
|
+
event(5, 'shape.moved', { id: 'square', x: 20, y: 30 }),
|
|
44
|
+
event(6, 'shape.deleted', { id: 'square' }),
|
|
45
|
+
event(7, 'shape.created', {
|
|
46
|
+
id: 'optimistic',
|
|
47
|
+
kind: 'ellipse',
|
|
48
|
+
x: 0,
|
|
49
|
+
y: 0,
|
|
50
|
+
w: 50,
|
|
51
|
+
h: 50,
|
|
52
|
+
color: 'rose',
|
|
53
|
+
}),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const confirmed = confirmedCanvasEventsSince(observed, 4, 6)
|
|
57
|
+
|
|
58
|
+
expect(confirmed.map(({ index }) => index)).toEqual([5, 6])
|
|
59
|
+
expect(replayCanvas(snapshot, confirmed, 0)).toEqual(snapshot)
|
|
60
|
+
expect(replayCanvas(snapshot, confirmed, 1).shapes['square']).toEqual({
|
|
61
|
+
...snapshot.shapes['square'],
|
|
62
|
+
x: 20,
|
|
63
|
+
y: 30,
|
|
64
|
+
movedAt: 5,
|
|
65
|
+
})
|
|
66
|
+
expect(replayCanvas(snapshot, confirmed, 2).shapes).toEqual({})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ContractEvent } from 'experimental-a2'
|
|
2
|
+
import { canvas, canvasReducer, type CanvasState } from '../model'
|
|
3
|
+
|
|
4
|
+
type CanvasEvent = ContractEvent<(typeof canvas)['events']>
|
|
5
|
+
|
|
6
|
+
export const confirmedCanvasEventsSince = (
|
|
7
|
+
events: CanvasEvent[],
|
|
8
|
+
snapshotIndex: number,
|
|
9
|
+
frontier: number,
|
|
10
|
+
): CanvasEvent[] =>
|
|
11
|
+
events.filter(
|
|
12
|
+
(event) => event.index > snapshotIndex && event.index <= frontier,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export const replayCanvas = (
|
|
16
|
+
snapshot: CanvasState,
|
|
17
|
+
events: CanvasEvent[],
|
|
18
|
+
through: number,
|
|
19
|
+
): CanvasState => events.slice(0, through).reduce(canvasReducer.fold, snapshot)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { canvasReducer } from '../model'
|
|
3
|
+
import { canvasServer } from '../server'
|
|
4
|
+
import { CanvasSession } from './canvas-client'
|
|
5
|
+
|
|
6
|
+
export default async function CanvasSessionPage({
|
|
7
|
+
params,
|
|
8
|
+
}: {
|
|
9
|
+
params: Promise<{ canvasId: string }>
|
|
10
|
+
}): Promise<ReactNode> {
|
|
11
|
+
const { canvasId } = await params
|
|
12
|
+
const session = canvasServer.session(canvasId)
|
|
13
|
+
const { state, index } = await session.state(canvasReducer)
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<CanvasSession
|
|
17
|
+
canvasId={canvasId}
|
|
18
|
+
initialState={state}
|
|
19
|
+
initialIndex={index}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { createClient } from 'experimental-a2/client'
|
|
3
|
+
import { createReact } from 'experimental-a2/react'
|
|
4
|
+
import { canvasReducer } from '../model'
|
|
5
|
+
|
|
6
|
+
// One route, two wires: on Vercel (production and previews), pushes
|
|
7
|
+
// and presence ride one WebSocket. Anywhere else, including `next
|
|
8
|
+
// dev` (which drops the upgrade handshake before the route runs) and
|
|
9
|
+
// a local `next start`, the same route serves the SSE + POST pair.
|
|
10
|
+
export const canvasClient = createClient({
|
|
11
|
+
reducer: canvasReducer,
|
|
12
|
+
api: process.env.NEXT_PUBLIC_VERCEL_ENV
|
|
13
|
+
? { type: 'ws', url: '/canvas/events' }
|
|
14
|
+
: '/canvas/events',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const { SessionProvider, useSession } = createReact({
|
|
18
|
+
client: canvasClient,
|
|
19
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { experimental_upgradeWebSocket } from '@vercel/functions'
|
|
2
|
+
import { canvasServer } from '../server'
|
|
3
|
+
|
|
4
|
+
export const POST = canvasServer.fetch
|
|
5
|
+
|
|
6
|
+
export function GET(request: Request): Promise<Response> {
|
|
7
|
+
return canvasServer.fetch(request, {
|
|
8
|
+
upgradeWebSocket: (attach) =>
|
|
9
|
+
experimental_upgradeWebSocket(attach, {
|
|
10
|
+
maxPayload: 4 * 1024 * 1024,
|
|
11
|
+
}),
|
|
12
|
+
})
|
|
13
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as a2 from 'experimental-a2'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
const paletteSchema = z.enum(['teal', 'amber', 'blue', 'violet', 'rose'])
|
|
5
|
+
|
|
6
|
+
export type PaletteColor = z.output<typeof paletteSchema>
|
|
7
|
+
|
|
8
|
+
export const canvas = a2.contract({
|
|
9
|
+
name: 'canvas',
|
|
10
|
+
events: {
|
|
11
|
+
'shape.created': z.object({
|
|
12
|
+
id: z.string().min(1),
|
|
13
|
+
kind: z.enum(['rect', 'ellipse']),
|
|
14
|
+
x: z.number(),
|
|
15
|
+
y: z.number(),
|
|
16
|
+
w: z.number().positive(),
|
|
17
|
+
h: z.number().positive(),
|
|
18
|
+
color: paletteSchema,
|
|
19
|
+
}),
|
|
20
|
+
'shape.moved': z.object({
|
|
21
|
+
id: z.string().min(1),
|
|
22
|
+
x: z.number(),
|
|
23
|
+
y: z.number(),
|
|
24
|
+
}),
|
|
25
|
+
'shape.deleted': z.object({ id: z.string().min(1) }),
|
|
26
|
+
},
|
|
27
|
+
presence: {
|
|
28
|
+
profile: z.object({
|
|
29
|
+
name: z.string().min(1).max(40),
|
|
30
|
+
color: paletteSchema,
|
|
31
|
+
}),
|
|
32
|
+
cursor: z.object({ x: z.number(), y: z.number() }),
|
|
33
|
+
dragging: z.object({ shapeId: z.string(), x: z.number(), y: z.number() }),
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const canvasShapeSchema = z.object({
|
|
38
|
+
id: z.string(),
|
|
39
|
+
kind: z.enum(['rect', 'ellipse']),
|
|
40
|
+
x: z.number(),
|
|
41
|
+
y: z.number(),
|
|
42
|
+
w: z.number(),
|
|
43
|
+
h: z.number(),
|
|
44
|
+
color: paletteSchema,
|
|
45
|
+
movedAt: z.number(),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const canvasState = z.object({
|
|
49
|
+
shapes: z.record(z.string(), canvasShapeSchema),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
export type CanvasState = z.output<typeof canvasState>
|
|
53
|
+
export type CanvasShape = CanvasState['shapes'][string]
|
|
54
|
+
|
|
55
|
+
export const canvasReducer = canvas
|
|
56
|
+
.reducer({
|
|
57
|
+
name: 'canvas-scene',
|
|
58
|
+
initialState: { shapes: {} },
|
|
59
|
+
stateSchema: canvasState,
|
|
60
|
+
})
|
|
61
|
+
.fold((state, event) => {
|
|
62
|
+
switch (event.type) {
|
|
63
|
+
case 'shape.created':
|
|
64
|
+
return {
|
|
65
|
+
shapes: {
|
|
66
|
+
...state.shapes,
|
|
67
|
+
[event.payload.id]: { ...event.payload, movedAt: event.index },
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
case 'shape.moved': {
|
|
71
|
+
const shape = state.shapes[event.payload.id]
|
|
72
|
+
if (!shape) return state
|
|
73
|
+
return {
|
|
74
|
+
shapes: {
|
|
75
|
+
...state.shapes,
|
|
76
|
+
[shape.id]: {
|
|
77
|
+
...shape,
|
|
78
|
+
x: event.payload.x,
|
|
79
|
+
y: event.payload.y,
|
|
80
|
+
movedAt: event.index,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
case 'shape.deleted': {
|
|
86
|
+
if (!state.shapes[event.payload.id]) return state
|
|
87
|
+
const shapes = { ...state.shapes }
|
|
88
|
+
delete shapes[event.payload.id]
|
|
89
|
+
return { shapes }
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return state
|
|
93
|
+
}
|
|
94
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import type { FormEvent, ReactNode } from 'react'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
export function OpenCanvas(): ReactNode {
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const [canvasId, setCanvasId] = useState('')
|
|
9
|
+
|
|
10
|
+
const open = (event: FormEvent): void => {
|
|
11
|
+
event.preventDefault()
|
|
12
|
+
const id = canvasId.trim()
|
|
13
|
+
if (id) router.push(`/canvas/${encodeURIComponent(id)}`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<form className="document-picker" onSubmit={open}>
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={() =>
|
|
21
|
+
router.push(`/canvas/${crypto.randomUUID().slice(0, 8)}`)
|
|
22
|
+
}
|
|
23
|
+
>
|
|
24
|
+
New canvas
|
|
25
|
+
</button>
|
|
26
|
+
<span className="hint">or</span>
|
|
27
|
+
<label>
|
|
28
|
+
Open a canvas ID
|
|
29
|
+
<input
|
|
30
|
+
value={canvasId}
|
|
31
|
+
onChange={(event) => setCanvasId(event.target.value)}
|
|
32
|
+
placeholder="moodboard"
|
|
33
|
+
/>
|
|
34
|
+
</label>
|
|
35
|
+
<button className="secondary" type="submit" disabled={!canvasId.trim()}>
|
|
36
|
+
Open
|
|
37
|
+
</button>
|
|
38
|
+
</form>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { OpenCanvas } from './open-canvas'
|
|
3
|
+
|
|
4
|
+
export default function CanvasPage(): ReactNode {
|
|
5
|
+
return (
|
|
6
|
+
<article>
|
|
7
|
+
<h1>A multiplayer canvas on two planes</h1>
|
|
8
|
+
<p className="lede">
|
|
9
|
+
Shapes are durable events: create, drag, delete, then scrub the events
|
|
10
|
+
this page receives to watch the reducer replay from its server snapshot.
|
|
11
|
+
Cursors and drag ghosts are presence — replicated live, stored never.
|
|
12
|
+
</p>
|
|
13
|
+
<OpenCanvas />
|
|
14
|
+
<p className="hint">
|
|
15
|
+
Open the same canvas in two tabs. While a shape is mid-drag you are
|
|
16
|
+
watching presence; the moment it drops you are watching the log.
|
|
17
|
+
</p>
|
|
18
|
+
</article>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import type { ChatAgentMessage } from '../model'
|
|
4
|
+
import { AgentStreamDrawer } from './agent-stream-drawer'
|
|
5
|
+
import type { AgentMessageMetadata, AgentStreamSelection } from './chat-view'
|
|
6
|
+
|
|
7
|
+
const usage = (
|
|
8
|
+
inputTokens: number,
|
|
9
|
+
outputTokens: number,
|
|
10
|
+
reasoningTokens: number,
|
|
11
|
+
) => ({
|
|
12
|
+
inputTokens,
|
|
13
|
+
inputTokenDetails: {
|
|
14
|
+
noCacheTokens: inputTokens - 2,
|
|
15
|
+
cacheReadTokens: 2,
|
|
16
|
+
cacheWriteTokens: 0,
|
|
17
|
+
},
|
|
18
|
+
outputTokens,
|
|
19
|
+
outputTokenDetails: {
|
|
20
|
+
textTokens: outputTokens - reasoningTokens,
|
|
21
|
+
reasoningTokens,
|
|
22
|
+
},
|
|
23
|
+
totalTokens: inputTokens + outputTokens,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('AgentStreamDrawer', () => {
|
|
27
|
+
it('renders the complete ordered UIMessage stream and completed metadata', () => {
|
|
28
|
+
const parts: ChatAgentMessage['parts'] = [
|
|
29
|
+
{ type: 'step-start' },
|
|
30
|
+
{
|
|
31
|
+
type: 'reasoning',
|
|
32
|
+
text: 'Consider whether this would help the room.',
|
|
33
|
+
state: 'done',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'text',
|
|
37
|
+
text: 'Private model text that was not posted.',
|
|
38
|
+
state: 'done',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'tool-post',
|
|
42
|
+
toolCallId: 'call-1',
|
|
43
|
+
state: 'output-available',
|
|
44
|
+
input: { message: 'Visible room message from the post tool.' },
|
|
45
|
+
output: { posted: true },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'custom',
|
|
49
|
+
kind: 'openai.trace',
|
|
50
|
+
providerMetadata: { openai: { traceId: 'trace-1' } },
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
const metadata: AgentMessageMetadata = {
|
|
54
|
+
kind: 'agent',
|
|
55
|
+
author: 'agent',
|
|
56
|
+
mode: 'ambient',
|
|
57
|
+
steps: [
|
|
58
|
+
{
|
|
59
|
+
generationId: 'generation-1',
|
|
60
|
+
model: 'openai/gpt-first',
|
|
61
|
+
finishReason: 'tool-calls',
|
|
62
|
+
usage: usage(12, 5, 2),
|
|
63
|
+
completedAt: '2026-08-16T12:00:00.000Z',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
generationId: 'generation-2',
|
|
67
|
+
model: 'openai/gpt-second',
|
|
68
|
+
finishReason: 'stop',
|
|
69
|
+
usage: usage(15, 3, 0),
|
|
70
|
+
completedAt: '2026-08-16T12:00:01.000Z',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
const message: ChatAgentMessage = {
|
|
75
|
+
id: 'assistant-1',
|
|
76
|
+
role: 'assistant',
|
|
77
|
+
metadata,
|
|
78
|
+
parts,
|
|
79
|
+
}
|
|
80
|
+
const selection: AgentStreamSelection = {
|
|
81
|
+
message,
|
|
82
|
+
parts,
|
|
83
|
+
metadata,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const markup = renderToStaticMarkup(
|
|
87
|
+
<AgentStreamDrawer
|
|
88
|
+
responseMessageId={message.id}
|
|
89
|
+
selection={selection}
|
|
90
|
+
summary="ambient · 35 tokens · 2 model steps"
|
|
91
|
+
status="completed"
|
|
92
|
+
onClose={vi.fn<() => void>()}
|
|
93
|
+
/>,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const orderedMarkers = [
|
|
97
|
+
'Model step',
|
|
98
|
+
'Consider whether this would help the room.',
|
|
99
|
+
'Private model text that was not posted.',
|
|
100
|
+
'Visible room message from the post tool.',
|
|
101
|
+
'openai.trace',
|
|
102
|
+
]
|
|
103
|
+
const positions = orderedMarkers.map((marker) => markup.indexOf(marker))
|
|
104
|
+
expect(positions.every((position) => position >= 0)).toBe(true)
|
|
105
|
+
expect(positions).toEqual(
|
|
106
|
+
[...positions].toSorted((left, right) => left - right),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
expect(markup).toContain('openai/gpt-first')
|
|
110
|
+
expect(markup).toContain('tool-calls')
|
|
111
|
+
expect(markup).toContain('generation-1')
|
|
112
|
+
expect(markup).toContain('openai/gpt-second')
|
|
113
|
+
expect(markup).toContain('generation-2')
|
|
114
|
+
expect(markup).toContain('Full AI SDK usage')
|
|
115
|
+
expect(markup).toContain('cacheReadTokens')
|
|
116
|
+
expect(markup).toContain('Raw extended UIMessage')
|
|
117
|
+
})
|
|
118
|
+
})
|