@zocomputer/agent-sdk 0.5.0
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/LICENSE +21 -0
- package/README.md +673 -0
- package/dist/attachments.js +52 -0
- package/dist/gateway-fetch.js +67 -0
- package/dist/index.js +4169 -0
- package/dist/initiator-auth.js +49 -0
- package/dist/platform/agent-sandbox/index.js +691 -0
- package/dist/platform/cloud-tools/image.js +498 -0
- package/dist/platform/cloud-tools/index.js +507 -0
- package/dist/platform/cloud-tools/web-search.js +87 -0
- package/dist/platform/runtime-ai/gateway.js +87 -0
- package/dist/platform/runtime-ai/index.js +91 -0
- package/dist/platform/runtime-ai/register.js +88 -0
- package/dist/platform/runtime-ai/session-fetch.js +54 -0
- package/dist/platform/runtime-auth/index.js +141 -0
- package/dist/state-files.js +287 -0
- package/dist/state-sandbox.js +383 -0
- package/dist/state.js +29 -0
- package/dist/steer-inbox.js +84 -0
- package/dist/steer.js +83 -0
- package/package.json +143 -0
- package/platform/agent-sandbox/api-client.ts +196 -0
- package/platform/agent-sandbox/index.ts +27 -0
- package/platform/agent-sandbox/pure.ts +83 -0
- package/platform/agent-sandbox/sftp.ts +141 -0
- package/platform/agent-sandbox/ssh-connection.ts +165 -0
- package/platform/agent-sandbox/ssh-exec.ts +98 -0
- package/platform/agent-sandbox/ssh-session.ts +487 -0
- package/platform/agent-sandbox/zo-backend.ts +88 -0
- package/platform/agent-sandbox/zo-sandbox.ts +39 -0
- package/platform/cloud-tools/image-path.ts +44 -0
- package/platform/cloud-tools/image.ts +225 -0
- package/platform/cloud-tools/index.ts +22 -0
- package/platform/cloud-tools/state-files.ts +368 -0
- package/platform/cloud-tools/tool-meta.ts +32 -0
- package/platform/cloud-tools/web-search.ts +15 -0
- package/platform/runtime-ai/gateway.ts +76 -0
- package/platform/runtime-ai/index.ts +20 -0
- package/platform/runtime-ai/register.ts +26 -0
- package/platform/runtime-ai/session-fetch.ts +124 -0
- package/platform/runtime-auth/index.ts +331 -0
- package/src/async-tasks.ts +273 -0
- package/src/attachments.ts +109 -0
- package/src/backgroundable.ts +88 -0
- package/src/bounded-output.ts +159 -0
- package/src/dir-conventions.ts +238 -0
- package/src/extract/cache.ts +40 -0
- package/src/extract/docx.ts +18 -0
- package/src/extract/pdf.ts +54 -0
- package/src/extract/sheet.ts +56 -0
- package/src/file-kind.ts +258 -0
- package/src/file-view.ts +80 -0
- package/src/gateway-fetch.ts +115 -0
- package/src/glob-match.ts +13 -0
- package/src/hooks.ts +213 -0
- package/src/index.ts +419 -0
- package/src/initiator-auth.ts +81 -0
- package/src/instructions.ts +224 -0
- package/src/list-files.ts +41 -0
- package/src/mock-model.ts +572 -0
- package/src/park-delivery.ts +247 -0
- package/src/path-locks.ts +52 -0
- package/src/read-file-content.ts +142 -0
- package/src/read-text.ts +40 -0
- package/src/redeliver.ts +155 -0
- package/src/run.ts +159 -0
- package/src/sandbox-io.ts +414 -0
- package/src/state-files.ts +460 -0
- package/src/state-sandbox.ts +710 -0
- package/src/state.ts +96 -0
- package/src/steer-inbox.ts +105 -0
- package/src/steer-tool.ts +83 -0
- package/src/steer.ts +146 -0
- package/src/task.ts +320 -0
- package/src/tools/bash.ts +143 -0
- package/src/tools/edit.ts +58 -0
- package/src/tools/glob.ts +56 -0
- package/src/tools/grep.ts +188 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/tasks.ts +241 -0
- package/src/tools/webfetch.ts +368 -0
- package/src/tools/write.ts +42 -0
- package/src/walk.ts +112 -0
- package/src/watch-output.ts +104 -0
- package/src/web-fetch.ts +324 -0
- package/src/web-page.ts +179 -0
- package/src/workspace-io.ts +225 -0
- package/src/workspace.ts +41 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
// A slow-streaming mock LanguageModelV4 for testing an eve agent's clients
|
|
2
|
+
// without inference credentials: everything past the model call — session
|
|
3
|
+
// routes, harness, framework tools, subagents, durable streams — runs REAL;
|
|
4
|
+
// only inference is canned. Gate it behind an env flag in the consumer's
|
|
5
|
+
// agent.ts (rib: RIB_MOCK_MODEL=1); never active in a normal run. Two modes, picked per turn from the user prompt:
|
|
6
|
+
//
|
|
7
|
+
// - Default: streams a long deterministic "story" as text deltas with a
|
|
8
|
+
// per-chunk delay, so a turn stays in-flight for a configurable window —
|
|
9
|
+
// what exercising the rail's in-progress states and chat switching needs.
|
|
10
|
+
// - Scripted tool calls, via a `[mock:<scenario>]` directive in the message:
|
|
11
|
+
// `[mock:hitl]` calls eve's ask_question (options with styles + freeform),
|
|
12
|
+
// `[mock:parallel]` calls ask_question twice in one response (parallel HITL),
|
|
13
|
+
// `[mock:todo]` writes then updates a todo list, `[mock:delegate]` delegates
|
|
14
|
+
// to a declared subagent. Each scenario ends with a short wrap-up text, so
|
|
15
|
+
// the HITL prompt, todo checklist, and subagent card render end-to-end.
|
|
16
|
+
import type {
|
|
17
|
+
LanguageModelV4,
|
|
18
|
+
LanguageModelV4CallOptions,
|
|
19
|
+
LanguageModelV4Prompt,
|
|
20
|
+
LanguageModelV4StreamPart,
|
|
21
|
+
LanguageModelV4Usage,
|
|
22
|
+
} from "@ai-sdk/provider";
|
|
23
|
+
|
|
24
|
+
export interface MockStoryModelOptions {
|
|
25
|
+
/** Text deltas per story turn. Default 240 (~60s at the default delay). */
|
|
26
|
+
chunkCount?: number;
|
|
27
|
+
/** Delay between deltas in ms. Default 250. */
|
|
28
|
+
chunkDelayMs?: number;
|
|
29
|
+
/** Deltas for a `[mock:burst]` turn (no pacing). Default 600. */
|
|
30
|
+
burstChunks?: number;
|
|
31
|
+
/** The declared subagent tool `[mock:delegate]` delegates to. Default "task_fast". */
|
|
32
|
+
delegateToolName?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Clock for response-metadata ids/timestamps. Default `Date.now`. Inject a
|
|
35
|
+
* fixed clock to make the full stream byte-deterministic across runs.
|
|
36
|
+
*/
|
|
37
|
+
now?: () => number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const STORY_SENTENCES = [
|
|
41
|
+
"The lighthouse keeper counted the waves as they broke against the rocks.",
|
|
42
|
+
"Every seventh wave carried a whisper from the old town beneath the sea.",
|
|
43
|
+
"Marisol had kept the light burning for forty-one years without a single dark night.",
|
|
44
|
+
"Tonight the fog rolled in thicker than she had ever seen it.",
|
|
45
|
+
"Somewhere out past the shoals, a bell rang that no ship had carried in decades.",
|
|
46
|
+
"She climbed the spiral stairs slowly, lantern in one hand, logbook in the other.",
|
|
47
|
+
"The glass at the top of the tower was cold and streaked with salt.",
|
|
48
|
+
"Below, the sea moved like a great animal turning in its sleep.",
|
|
49
|
+
"She wrote the date in the logbook and then paused, pen hovering.",
|
|
50
|
+
"The bell rang again, closer now, and the fog pressed against the windows.",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function storyChunk(index: number): string {
|
|
54
|
+
const sentence = STORY_SENTENCES[index % STORY_SENTENCES.length] ?? "The story went on.";
|
|
55
|
+
const paragraphBreak = index > 0 && index % 8 === 0 ? "\n\n" : " ";
|
|
56
|
+
return `${paragraphBreak}${sentence}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const delay = (ms: number): Promise<void> =>
|
|
60
|
+
ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
|
|
61
|
+
|
|
62
|
+
function usageFor(outputTokens: number): LanguageModelV4Usage {
|
|
63
|
+
return {
|
|
64
|
+
inputTokens: { total: 100, noCache: 100, cacheRead: 0, cacheWrite: 0 },
|
|
65
|
+
outputTokens: { total: outputTokens, text: outputTokens, reasoning: 0 },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Scripted scenarios (pure helpers, unit-tested) -------------------------
|
|
70
|
+
|
|
71
|
+
// Multi-step tool scripts plus four stream shapes: `fail` ends the stream in
|
|
72
|
+
// a terminal error part (the deterministic trigger for the failed-turn UX),
|
|
73
|
+
// `burst` streams RIB_MOCK_BURST_CHUNKS deltas with no pacing delay — a
|
|
74
|
+
// renderer-throughput probe for the cockpits' streaming pipelines —
|
|
75
|
+
// `markdown` streams structure-heavy markdown (fences, tables, nested lists)
|
|
76
|
+
// that opens blocks mid-delta (the streaming-renderer stability probe),
|
|
77
|
+
// `interleave` alternates reasoning and text blocks in one message (multiple
|
|
78
|
+
// reasoning/text parts per response — how extended-thinking models actually
|
|
79
|
+
// stream), and `empty` finishes with zero content parts (a real model edge
|
|
80
|
+
// case that has broken assistant-message rendering before).
|
|
81
|
+
export const MOCK_SCENARIOS = [
|
|
82
|
+
"hitl",
|
|
83
|
+
"parallel",
|
|
84
|
+
"todo",
|
|
85
|
+
"delegate",
|
|
86
|
+
"fail",
|
|
87
|
+
"burst",
|
|
88
|
+
"markdown",
|
|
89
|
+
"interleave",
|
|
90
|
+
"empty",
|
|
91
|
+
] as const;
|
|
92
|
+
export type MockScenario = (typeof MOCK_SCENARIOS)[number];
|
|
93
|
+
export type MockScriptedScenario = Extract<
|
|
94
|
+
MockScenario,
|
|
95
|
+
"hitl" | "parallel" | "todo" | "delegate"
|
|
96
|
+
>;
|
|
97
|
+
|
|
98
|
+
/** One tool call a scripted step emits. */
|
|
99
|
+
export interface MockToolCall {
|
|
100
|
+
toolName: string;
|
|
101
|
+
input: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The scripted action for a scenario at a step: one or more tool calls emitted
|
|
106
|
+
* in a single response (plural = parallel tool calls), or a wrap-up text that
|
|
107
|
+
* ends the turn.
|
|
108
|
+
*/
|
|
109
|
+
export type MockScriptAction =
|
|
110
|
+
| { kind: "tool-calls"; calls: readonly MockToolCall[] }
|
|
111
|
+
| { kind: "text"; text: string };
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Markdown chunks that deliberately split structure across deltas: a fence
|
|
115
|
+
* opens in one delta and closes several later, table rows arrive one at a
|
|
116
|
+
* time, list nesting grows mid-stream. A streaming renderer must keep the
|
|
117
|
+
* trailing block stable through all of it.
|
|
118
|
+
*/
|
|
119
|
+
export function markdownChunks(): readonly string[] {
|
|
120
|
+
return [
|
|
121
|
+
"## Streaming markdown stress\n\n",
|
|
122
|
+
"A paragraph with **bold**, _italic_, `inline code`, and a [link](https://example.com).\n\n",
|
|
123
|
+
"```ts\n",
|
|
124
|
+
"export function keeper(light: number): string {\n",
|
|
125
|
+
" // the fence stays open across deltas\n",
|
|
126
|
+
" return `burning for ${light} years`;\n",
|
|
127
|
+
"}\n",
|
|
128
|
+
"```\n\n",
|
|
129
|
+
"| tide | bell | fog |\n",
|
|
130
|
+
"| --- | --- | --- |\n",
|
|
131
|
+
"| low | quiet | thin |\n",
|
|
132
|
+
"| high | ringing | thick |\n\n",
|
|
133
|
+
"1. climb the stairs\n",
|
|
134
|
+
" - lantern in one hand\n",
|
|
135
|
+
" - logbook in the other\n",
|
|
136
|
+
"2. write the date\n",
|
|
137
|
+
" 1. pause, pen hovering\n",
|
|
138
|
+
" 2. listen for the bell\n\n",
|
|
139
|
+
"> The sea moved like a great animal turning in its sleep.\n\n",
|
|
140
|
+
"Unicode: emoji 🌊🔔, CJK 灯台守は波を数えた, RTL مرحبا, combining é ñ ü.\n\n",
|
|
141
|
+
"A very long unbroken token: " + "abcdefghij".repeat(40) + "\n\n",
|
|
142
|
+
"Done. ✅\n",
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The `[mock:<scenario>]` directive in the turn's user text, if any. */
|
|
147
|
+
export function mockScenarioFrom(text: string): MockScenario | null {
|
|
148
|
+
const match = /\[mock:([a-z]+)\]/.exec(text);
|
|
149
|
+
const name = match?.[1];
|
|
150
|
+
return (MOCK_SCENARIOS as readonly string[]).includes(name ?? "")
|
|
151
|
+
? (name as MockScenario)
|
|
152
|
+
: null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Which step of a scripted scenario this doStream call is: the number of tool
|
|
157
|
+
* RESULTS since the last user message. Step 0 emits the scenario's first tool
|
|
158
|
+
* call(s); each tool result advances the script — so a step that emitted two
|
|
159
|
+
* parallel calls advances by two once both resolve, regardless of whether the
|
|
160
|
+
* harness delivers the results as one tool message or several.
|
|
161
|
+
*/
|
|
162
|
+
export function scriptStepFrom(prompt: LanguageModelV4Prompt): number {
|
|
163
|
+
let step = 0;
|
|
164
|
+
for (const message of prompt) {
|
|
165
|
+
if (message.role === "user") step = 0;
|
|
166
|
+
else if (message.role === "tool") {
|
|
167
|
+
step += message.content.filter((part) => part.type === "tool-result").length;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return step;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The last user message's plain text, for topic echo + directive parsing. */
|
|
174
|
+
export function lastUserTextFrom(prompt: LanguageModelV4Prompt): string | undefined {
|
|
175
|
+
return [...prompt]
|
|
176
|
+
.reverse()
|
|
177
|
+
.flatMap((message) =>
|
|
178
|
+
message.role === "user"
|
|
179
|
+
? message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
180
|
+
: [],
|
|
181
|
+
)[0];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function askQuestionCall(prompt: string, topic: string): MockToolCall {
|
|
185
|
+
return {
|
|
186
|
+
toolName: "ask_question",
|
|
187
|
+
input: {
|
|
188
|
+
prompt,
|
|
189
|
+
options: [
|
|
190
|
+
{
|
|
191
|
+
id: "ship",
|
|
192
|
+
label: `Ship the ${topic}`,
|
|
193
|
+
style: "primary",
|
|
194
|
+
description: "Proceed with the happy path.",
|
|
195
|
+
},
|
|
196
|
+
{ id: "hold", label: "Hold for review" },
|
|
197
|
+
{ id: "abort", label: "Abort the run", style: "danger", description: "Stops everything." },
|
|
198
|
+
],
|
|
199
|
+
allowFreeform: true,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The scripted action for a scenario at a step; `text` actions end the turn. */
|
|
205
|
+
export function scriptActionFor(
|
|
206
|
+
scenario: MockScriptedScenario,
|
|
207
|
+
step: number,
|
|
208
|
+
delegateToolName = "task_fast",
|
|
209
|
+
): MockScriptAction {
|
|
210
|
+
switch (scenario) {
|
|
211
|
+
case "hitl":
|
|
212
|
+
if (step === 0) {
|
|
213
|
+
return {
|
|
214
|
+
kind: "tool-calls",
|
|
215
|
+
calls: [askQuestionCall("Mock HITL: how should this test proceed?", "change")],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return { kind: "text", text: "Answer received — the mock turn resumed and finished cleanly." };
|
|
219
|
+
case "parallel":
|
|
220
|
+
// Two ask_question calls in ONE response: both should pend on a single
|
|
221
|
+
// park, and one respond covering both should resume the turn (the
|
|
222
|
+
// batching contract rib's live parallel-questions eval pins). The step
|
|
223
|
+
// counts tool RESULTS, so the wrap-up must not claim both answers
|
|
224
|
+
// arrived until two results are actually in the prompt — a harness that
|
|
225
|
+
// resumes after a partial answer gets an honest early-exit text instead.
|
|
226
|
+
if (step === 0) {
|
|
227
|
+
return {
|
|
228
|
+
kind: "tool-calls",
|
|
229
|
+
calls: [
|
|
230
|
+
askQuestionCall("Mock parallel HITL (1 of 2): which color?", "color"),
|
|
231
|
+
askQuestionCall("Mock parallel HITL (2 of 2): which size?", "size"),
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (step === 1) {
|
|
236
|
+
return {
|
|
237
|
+
kind: "text",
|
|
238
|
+
text: "Only one answer arrived — the parallel HITL scenario ended without the second.",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
kind: "text",
|
|
243
|
+
text: "Both answers received — the parallel HITL scenario finished cleanly.",
|
|
244
|
+
};
|
|
245
|
+
case "todo":
|
|
246
|
+
if (step === 0) {
|
|
247
|
+
return {
|
|
248
|
+
kind: "tool-calls",
|
|
249
|
+
calls: [
|
|
250
|
+
{
|
|
251
|
+
toolName: "todo",
|
|
252
|
+
input: {
|
|
253
|
+
todos: [
|
|
254
|
+
{ content: "Survey the harbor charts", status: "completed", priority: "high" },
|
|
255
|
+
{ content: "Polish the tower glass", status: "in_progress", priority: "medium" },
|
|
256
|
+
{ content: "Refill the oil reserves", status: "pending", priority: "medium" },
|
|
257
|
+
{ content: "Log the evening tide", status: "pending", priority: "low" },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (step === 1) {
|
|
265
|
+
return {
|
|
266
|
+
kind: "tool-calls",
|
|
267
|
+
calls: [
|
|
268
|
+
{
|
|
269
|
+
toolName: "todo",
|
|
270
|
+
input: {
|
|
271
|
+
todos: [
|
|
272
|
+
{ content: "Survey the harbor charts", status: "completed", priority: "high" },
|
|
273
|
+
{ content: "Polish the tower glass", status: "completed", priority: "medium" },
|
|
274
|
+
{ content: "Refill the oil reserves", status: "in_progress", priority: "medium" },
|
|
275
|
+
{ content: "Log the evening tide", status: "cancelled", priority: "low" },
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return { kind: "text", text: "Todo list written and updated — checklist scenario complete." };
|
|
283
|
+
case "delegate":
|
|
284
|
+
if (step === 0) {
|
|
285
|
+
return {
|
|
286
|
+
kind: "tool-calls",
|
|
287
|
+
calls: [
|
|
288
|
+
{
|
|
289
|
+
toolName: delegateToolName,
|
|
290
|
+
input: {
|
|
291
|
+
message:
|
|
292
|
+
"Mock delegation: describe the lighthouse keeper's routine. Reply with a short report.",
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return { kind: "text", text: "The delegate reported back — delegation scenario complete." };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Split a tool call's JSON input into small fragments, the way real models
|
|
304
|
+
* stream tool arguments — so arg-streaming renderers see many partial-JSON
|
|
305
|
+
* deltas, not one complete blob.
|
|
306
|
+
*/
|
|
307
|
+
export function toolInputFragments(inputJson: string, fragmentSize = 24): readonly string[] {
|
|
308
|
+
if (inputJson.length === 0) return [];
|
|
309
|
+
const fragments: string[] = [];
|
|
310
|
+
for (let i = 0; i < inputJson.length; i += fragmentSize) {
|
|
311
|
+
fragments.push(inputJson.slice(i, i + fragmentSize));
|
|
312
|
+
}
|
|
313
|
+
return fragments;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- The model ---------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
export function createMockStoryModel(options: MockStoryModelOptions = {}): LanguageModelV4 {
|
|
319
|
+
const chunkCount = options.chunkCount ?? 240;
|
|
320
|
+
const chunkDelayMs = options.chunkDelayMs ?? 250;
|
|
321
|
+
const burstChunks = options.burstChunks ?? 600;
|
|
322
|
+
const delegateToolName = options.delegateToolName ?? "task_fast";
|
|
323
|
+
const now = options.now ?? Date.now;
|
|
324
|
+
return {
|
|
325
|
+
specificationVersion: "v4",
|
|
326
|
+
// eve's compaction compiler requires context-window metadata, which it
|
|
327
|
+
// resolves for provider instances via `formatLanguageModelGatewayId`
|
|
328
|
+
// (provider + modelId → an AI Gateway catalog slug). Borrow a real
|
|
329
|
+
// catalog identity so the mock compiles; routing stays "external", so no
|
|
330
|
+
// request ever reaches Anthropic.
|
|
331
|
+
provider: "anthropic",
|
|
332
|
+
modelId: "claude-sonnet-4-6",
|
|
333
|
+
supportedUrls: {},
|
|
334
|
+
async doGenerate() {
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text" as const, text: "(mock model, non-streaming reply)" }],
|
|
337
|
+
finishReason: { unified: "stop" as const, raw: "stop" },
|
|
338
|
+
usage: usageFor(10),
|
|
339
|
+
warnings: [],
|
|
340
|
+
};
|
|
341
|
+
},
|
|
342
|
+
async doStream(callOptions: LanguageModelV4CallOptions) {
|
|
343
|
+
const abortSignal = callOptions.abortSignal;
|
|
344
|
+
const lastUserText = lastUserTextFrom(callOptions.prompt);
|
|
345
|
+
const scenario = mockScenarioFrom(lastUserText ?? "");
|
|
346
|
+
const step = scriptStepFrom(callOptions.prompt);
|
|
347
|
+
// Echo the asking prompt into the output so parallel chats render
|
|
348
|
+
// distinguishable transcripts (the canned sentences repeat otherwise).
|
|
349
|
+
const topic = (lastUserText ?? "an untitled request").slice(0, 60);
|
|
350
|
+
const stream = new ReadableStream<LanguageModelV4StreamPart>({
|
|
351
|
+
async start(controller) {
|
|
352
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
353
|
+
controller.enqueue({
|
|
354
|
+
type: "response-metadata",
|
|
355
|
+
id: `mock-${now()}`,
|
|
356
|
+
modelId: "claude-sonnet-4-6",
|
|
357
|
+
timestamp: new Date(now()),
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (scenario === "fail") {
|
|
361
|
+
controller.enqueue({ type: "text-start", id: "t1" });
|
|
362
|
+
for (let i = 0; i < 6; i++) {
|
|
363
|
+
if (abortSignal?.aborted) break;
|
|
364
|
+
await delay(chunkDelayMs);
|
|
365
|
+
controller.enqueue({ type: "text-delta", id: "t1", delta: storyChunk(i) });
|
|
366
|
+
}
|
|
367
|
+
controller.enqueue({ type: "text-end", id: "t1" });
|
|
368
|
+
// A terminal stream failure mid-turn: the deterministic trigger
|
|
369
|
+
// for the failed-turn notice / error rail state in both cockpits.
|
|
370
|
+
controller.enqueue({
|
|
371
|
+
type: "error",
|
|
372
|
+
error: new Error("mock: injected mid-stream failure [mock:fail]"),
|
|
373
|
+
});
|
|
374
|
+
controller.close();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (scenario === "burst") {
|
|
379
|
+
controller.enqueue({ type: "text-start", id: "t1" });
|
|
380
|
+
controller.enqueue({
|
|
381
|
+
type: "text-delta",
|
|
382
|
+
id: "t1",
|
|
383
|
+
delta: `**Burst: ${burstChunks} deltas, no pacing.**\n\n`,
|
|
384
|
+
});
|
|
385
|
+
for (let i = 0; i < burstChunks; i++) {
|
|
386
|
+
if (abortSignal?.aborted) break;
|
|
387
|
+
controller.enqueue({
|
|
388
|
+
type: "text-delta",
|
|
389
|
+
id: "t1",
|
|
390
|
+
delta: i % 8 === 0 ? `${storyChunk(i)} [¶${i / 8 + 1}]` : storyChunk(i),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
controller.enqueue({ type: "text-delta", id: "t1", delta: "\n\n**Burst done.**" });
|
|
394
|
+
controller.enqueue({ type: "text-end", id: "t1" });
|
|
395
|
+
controller.enqueue({
|
|
396
|
+
type: "finish",
|
|
397
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
398
|
+
usage: usageFor(burstChunks * 12),
|
|
399
|
+
});
|
|
400
|
+
controller.close();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (scenario === "markdown") {
|
|
405
|
+
controller.enqueue({ type: "text-start", id: "t1" });
|
|
406
|
+
for (const chunk of markdownChunks()) {
|
|
407
|
+
if (abortSignal?.aborted) break;
|
|
408
|
+
await delay(Math.min(chunkDelayMs, 120));
|
|
409
|
+
controller.enqueue({ type: "text-delta", id: "t1", delta: chunk });
|
|
410
|
+
}
|
|
411
|
+
controller.enqueue({ type: "text-end", id: "t1" });
|
|
412
|
+
controller.enqueue({
|
|
413
|
+
type: "finish",
|
|
414
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
415
|
+
usage: usageFor(400),
|
|
416
|
+
});
|
|
417
|
+
controller.close();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (scenario === "interleave") {
|
|
422
|
+
// Reasoning and text alternate as SEPARATE blocks in one message —
|
|
423
|
+
// the shape extended-thinking models actually stream. Renderers
|
|
424
|
+
// must segment multiple reasoning/text parts, not assume one each.
|
|
425
|
+
const blocks: readonly { kind: "reasoning" | "text"; id: string; text: string }[] = [
|
|
426
|
+
{
|
|
427
|
+
kind: "reasoning",
|
|
428
|
+
id: "r1",
|
|
429
|
+
text: "First thought: check the tide tables before anything else.",
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
kind: "text",
|
|
433
|
+
id: "t1",
|
|
434
|
+
text: "The tide tables say low water at dusk.\n\nThat changes the plan.",
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
kind: "reasoning",
|
|
438
|
+
id: "r2",
|
|
439
|
+
text: "Second thought: the bell only rings when the fog is thick.",
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
kind: "text",
|
|
443
|
+
id: "t2",
|
|
444
|
+
text: "So the keeper waits for the bell — interleave scenario complete.",
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
for (const block of blocks) {
|
|
448
|
+
const startType = block.kind === "reasoning" ? "reasoning-start" : "text-start";
|
|
449
|
+
const deltaType = block.kind === "reasoning" ? "reasoning-delta" : "text-delta";
|
|
450
|
+
const endType = block.kind === "reasoning" ? "reasoning-end" : "text-end";
|
|
451
|
+
controller.enqueue({ type: startType, id: block.id });
|
|
452
|
+
for (const word of block.text.split(" ")) {
|
|
453
|
+
if (abortSignal?.aborted) break;
|
|
454
|
+
await delay(Math.min(chunkDelayMs, 80));
|
|
455
|
+
controller.enqueue({ type: deltaType, id: block.id, delta: `${word} ` });
|
|
456
|
+
}
|
|
457
|
+
controller.enqueue({ type: endType, id: block.id });
|
|
458
|
+
if (abortSignal?.aborted) break;
|
|
459
|
+
}
|
|
460
|
+
controller.enqueue({
|
|
461
|
+
type: "finish",
|
|
462
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
463
|
+
usage: usageFor(120),
|
|
464
|
+
});
|
|
465
|
+
controller.close();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (scenario === "empty") {
|
|
470
|
+
// A completion with zero content parts: models do occasionally
|
|
471
|
+
// return nothing, and empty assistant messages have broken
|
|
472
|
+
// renderers before. The stream is still grammatical.
|
|
473
|
+
controller.enqueue({
|
|
474
|
+
type: "finish",
|
|
475
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
476
|
+
usage: usageFor(0),
|
|
477
|
+
});
|
|
478
|
+
controller.close();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (scenario !== null) {
|
|
483
|
+
const action = scriptActionFor(scenario, step, delegateToolName);
|
|
484
|
+
// A short reasoning burst before each scripted step, so the
|
|
485
|
+
// Thinking… display renders too.
|
|
486
|
+
controller.enqueue({ type: "reasoning-start", id: "r1" });
|
|
487
|
+
for (const word of `Scripted ${scenario} scenario, step ${step}: deciding the next move.`.split(" ")) {
|
|
488
|
+
if (abortSignal?.aborted) break;
|
|
489
|
+
await delay(chunkDelayMs);
|
|
490
|
+
controller.enqueue({ type: "reasoning-delta", id: "r1", delta: `${word} ` });
|
|
491
|
+
}
|
|
492
|
+
controller.enqueue({ type: "reasoning-end", id: "r1" });
|
|
493
|
+
|
|
494
|
+
if (action.kind === "tool-calls") {
|
|
495
|
+
for (const [callIndex, call] of action.calls.entries()) {
|
|
496
|
+
const toolCallId = `mock-call-${scenario}-${step}-${callIndex}`;
|
|
497
|
+
const inputJson = JSON.stringify(call.input);
|
|
498
|
+
controller.enqueue({
|
|
499
|
+
type: "tool-input-start",
|
|
500
|
+
id: toolCallId,
|
|
501
|
+
toolName: call.toolName,
|
|
502
|
+
});
|
|
503
|
+
// Fragmented like a real model streams tool arguments —
|
|
504
|
+
// arg-streaming renderers see partial JSON, not one blob.
|
|
505
|
+
for (const fragment of toolInputFragments(inputJson)) {
|
|
506
|
+
if (abortSignal?.aborted) break;
|
|
507
|
+
await delay(Math.min(chunkDelayMs, 80));
|
|
508
|
+
controller.enqueue({ type: "tool-input-delta", id: toolCallId, delta: fragment });
|
|
509
|
+
}
|
|
510
|
+
controller.enqueue({ type: "tool-input-end", id: toolCallId });
|
|
511
|
+
controller.enqueue({
|
|
512
|
+
type: "tool-call",
|
|
513
|
+
toolCallId,
|
|
514
|
+
toolName: call.toolName,
|
|
515
|
+
input: inputJson,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
controller.enqueue({
|
|
519
|
+
type: "finish",
|
|
520
|
+
finishReason: { unified: "tool-calls", raw: "tool_use" },
|
|
521
|
+
usage: usageFor(50 * action.calls.length),
|
|
522
|
+
});
|
|
523
|
+
controller.close();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
controller.enqueue({ type: "text-start", id: "t1" });
|
|
527
|
+
for (const word of action.text.split(" ")) {
|
|
528
|
+
if (abortSignal?.aborted) break;
|
|
529
|
+
await delay(chunkDelayMs);
|
|
530
|
+
controller.enqueue({ type: "text-delta", id: "t1", delta: `${word} ` });
|
|
531
|
+
}
|
|
532
|
+
controller.enqueue({ type: "text-end", id: "t1" });
|
|
533
|
+
controller.enqueue({
|
|
534
|
+
type: "finish",
|
|
535
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
536
|
+
usage: usageFor(action.text.length),
|
|
537
|
+
});
|
|
538
|
+
controller.close();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
controller.enqueue({ type: "text-start", id: "t1" });
|
|
543
|
+
controller.enqueue({
|
|
544
|
+
type: "text-delta",
|
|
545
|
+
id: "t1",
|
|
546
|
+
delta: `**Story for: "${topic}…"**\n\n`,
|
|
547
|
+
});
|
|
548
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
549
|
+
if (abortSignal?.aborted) break;
|
|
550
|
+
await delay(chunkDelayMs);
|
|
551
|
+
controller.enqueue({
|
|
552
|
+
type: "text-delta",
|
|
553
|
+
id: "t1",
|
|
554
|
+
delta:
|
|
555
|
+
i % 8 === 0
|
|
556
|
+
? `${storyChunk(i)} [${topic.slice(0, 12)}… ¶${i / 8 + 1}]`
|
|
557
|
+
: storyChunk(i),
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
controller.enqueue({ type: "text-end", id: "t1" });
|
|
561
|
+
controller.enqueue({
|
|
562
|
+
type: "finish",
|
|
563
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
564
|
+
usage: usageFor(chunkCount * 12),
|
|
565
|
+
});
|
|
566
|
+
controller.close();
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
return { stream };
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
}
|