llmz 0.8.0 → 0.9.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.
Files changed (54) hide show
  1. package/README.md +45 -11
  2. package/dist/chat.d.ts +6 -6
  3. package/dist/{common-pgJjFu2Y.js → common-BlRFtdf2.js} +233 -22
  4. package/dist/{common-DdArNQhl.cjs → common-BnvrDnIT.cjs} +238 -21
  5. package/dist/{component-BXS8vAwO.cjs → component-C6Agl2ns.cjs} +1 -1
  6. package/dist/{component-Dq7ZLdvy.js → component-DGG7d_Mz.js} +1 -1
  7. package/dist/component.default.d.ts +10 -0
  8. package/dist/context.d.ts +4 -8
  9. package/dist/{custom-client-2V9lmzUf.js → custom-client-ChuZ63zp.js} +15 -12
  10. package/dist/{custom-client-HkM1Sdzv.cjs → custom-client-DAsGRnYc.cjs} +18 -15
  11. package/dist/{dual-modes-Cy0_W4O2.js → dual-modes-DDkoCp3R.js} +1246 -1170
  12. package/dist/{dual-modes-Cb7ThvoV.cjs → dual-modes-DPXiuvNe.cjs} +1239 -1169
  13. package/dist/example-format.d.ts +8 -0
  14. package/dist/{execute-Aw4cbIx-.js → execute-BWcdv9so.js} +64 -85
  15. package/dist/{execute-COIHOIKR.cjs → execute-DM2-7y86.cjs} +64 -85
  16. package/dist/index.cjs +254 -11
  17. package/dist/index.js +253 -10
  18. package/dist/index.workerd.js +8646 -8108
  19. package/dist/inspect.d.ts +2 -0
  20. package/dist/isPlainObject-CSxXVKCk.cjs +56 -0
  21. package/dist/isPlainObject-D39AzPO2.js +51 -0
  22. package/dist/{jsx-B0iYyvfr.js → jsx-CIgdGGVs.js} +2 -50
  23. package/dist/{jsx-D5Y2cPo-.cjs → jsx-owqxrquU.cjs} +2 -56
  24. package/dist/message-stream/instructions.d.ts +8 -0
  25. package/dist/message-stream/parser.d.ts +6 -0
  26. package/dist/message-stream/response-parser.d.ts +24 -0
  27. package/dist/message-stream/response-parser.test.d.ts +1 -0
  28. package/dist/message-stream/types.d.ts +3 -2
  29. package/dist/prompts/adherence.test.d.ts +1 -0
  30. package/dist/prompts/catalogue-examples.test.d.ts +1 -0
  31. package/dist/prompts/chat-mode/protocol.d.ts +2 -0
  32. package/dist/prompts/chat-mode/system.d.ts +1 -1
  33. package/dist/prompts/chat-mode/user.d.ts +1 -1
  34. package/dist/prompts/code-examples.d.ts +2 -0
  35. package/dist/prompts/code-examples.test.d.ts +1 -0
  36. package/dist/prompts/common.d.ts +2 -1
  37. package/dist/prompts/concrete-patterns.test.d.ts +1 -0
  38. package/dist/prompts/long-search.test.d.ts +1 -0
  39. package/dist/prompts/prompt.d.ts +5 -2
  40. package/dist/prompts/protocol-basics.d.ts +5 -0
  41. package/dist/prompts/protocol-simple.test.d.ts +1 -0
  42. package/dist/prompts/protocol.d.ts +13 -0
  43. package/dist/prompts/sections.test.d.ts +1 -0
  44. package/dist/prompts/worker-mode/protocol.d.ts +2 -0
  45. package/dist/prompts/worker-mode/system.d.ts +1 -1
  46. package/dist/prompts/worker-mode/user.d.ts +1 -1
  47. package/dist/runtime/citations.test.d.ts +1 -0
  48. package/dist/runtime/fixtures/long-search.d.ts +19 -0
  49. package/dist/runtime/fixtures/protocol-languages.d.ts +74 -0
  50. package/dist/runtime/fixtures/qwen-protocol-failures.d.ts +29 -0
  51. package/dist/runtime/generate.d.ts +2 -9
  52. package/dist/runtime/protocol-adherence.test.d.ts +1 -0
  53. package/dist/runtime/types.d.ts +4 -7
  54. package/package.json +1 -1
package/README.md CHANGED
@@ -64,6 +64,7 @@ const ticketId = await buyTicket({ from: 'quebec', to: 'new york' })
64
64
  Every model response is a sequence of streaming-native `■` blocks:
65
65
 
66
66
  ```
67
+ ■start
67
68
  ■send=message
68
69
  Let me check the next launch dates for the Moon...
69
70
  ■run
@@ -71,17 +72,50 @@ const dates = await checkAvailability({ destination: 'moon' })
71
72
  const reservation = await bookTrip({ destination: 'moon', date: dates[0], travelerName: 'Ada' })
72
73
  const payment = await processPayment({ reservationId: reservation.reservationId })
73
74
  return { ...payment, date: dates[0] }
75
+ ■end
74
76
  ```
75
77
 
76
- - **`■send=<component> {props?}`** — sends a message to the user, delivered (and streamable) the moment it is parsed
77
- - **`■run`** — executes the TypeScript body in the sandbox; the returned value is fed back to the model
78
+ - **`■send=<component> {props?}`** — sends a message to the user, streamed provisionally, then delivered after successful validation
79
+ - **`■run`** — executes the JavaScript body in the sandbox; the returned value is fed back to the model
78
80
  - **`■next=<exit> {props?}`** — ends the turn through a typed exit (the built-in `listen` hands the turn back to the user)
79
81
 
80
- Because the protocol is parsed incrementally, messages stream to your UI token-by-token, tool calls surface live, and code execution can start before the response has even finished streaming.
82
+ Every generated response must contain a standalone `■start` and end with `■end`. Text before the start marker is retained for debugging and discarded before parsing any blocks. There is no unwrapped response mode. Only explicit `■send` blocks can produce messages. Markdown (`■send=md`) remains supported.
81
83
 
82
- Only explicit `■send` blocks produce messages or message deltas. Unexpected text outside protocol blocks is discarded in streaming, fallback restarts, and nonstreaming execution. Explicit Markdown (`■send=md`) is supported. A response with only unexpected text follows the normal invalid-response retry/error path. For debugging, `iteration.llm.output` retains the raw response and `iteration.llm.diagnostics` records parser diagnostics, including `unexpected-text`.
84
+ Message bodies stream as provisional deltas. Completed message handlers and code execution wait for the entire valid response and successful generation. A missing start marker, unmarked prose inside the envelope, multiple run blocks, and messages after code reject the response through the normal malformed-response/error path. A failed or restarted attempt cannot execute code or commit a message. Any previews from it are retracted with a reset delta.
83
85
 
84
- As of 0.7.0, the parser options `strict` and `recoveryComponent` have been removed. All parsing requires explicit send blocks; callers relying on implicit messages must emit `■send=<component>` instead.
86
+ The first `■end` is a reserved terminator anywhere after `■start`, including inline and inside a JavaScript string or comment. Everything after it is discarded with a diagnostic; it never reaches message handlers or code execution. This intentionally accepts concatenated output such as `■end■start`. The prefix must still form valid blocks and valid JavaScript before any code runs. A terminator inside a string can therefore invalidate the response; do not use the raw sequence as content. In JavaScript, construct literal text with a Unicode escape such as `"\u25a0end"` instead. This is a protocol delimiter, not a JavaScript-aware scanner.
87
+
88
+ Cognitive receives `stopSequences: ['\n■end']` to stop generation at the end boundary. Since providers consume the stop string, normal `stop` metadata closes the response even when the literal end marker is absent. Cognitive does not distinguish a matched stop string from ordinary end-of-generation; the response must still have a start marker and valid blocks. Missing metadata, truncation, filtering, and transport failure never supply this closure.
89
+
90
+ Raw output and diagnostics remain available in `iteration.llm.output` and `iteration.llm.diagnostics`, including on failed streams. A valid framed exit alone permits intentional silence. Tools completed in earlier successful iterations remain completed during later recovery.
91
+
92
+ Prompt examples use standalone triple quotes (`"""`) to show their boundaries, with BAD/CORRECT comparisons and a menu of response shapes. These quotes are documentation, not protocol. If copied, standalone example delimiter lines are stripped before customer callbacks and recorded as `example-delimiter` diagnostics. Inline quotes, JSON props, and interior Markdown/code content remain literal. A standalone triple-quote line immediately before a protocol boundary is reserved for this purpose; to display it literally, include it inline or inside a fenced code sample.
93
+
94
+ The prompts use ordinary headings and put the response format after task context and the current execution budget. Historical replies are kept as recorded, without synthetic protocol headers. The low-level block parser remains useful for parsing individual blocks; execution uses the mandatory response envelope.
95
+
96
+ This is a breaking protocol change: custom model clients and cached wire responses must include the start boundary and a successful response termination. Completed handlers now run after successful generation; only deltas stream during generation. `■run` followed by `■send` is invalid. Return a tool result, then answer in a new iteration. The removed `strict` and `recoveryComponent` options are not restored.
97
+
98
+ To measure first-response adherence independently of runtime recovery, configure `CLOUD_PAT`, `CLOUD_BOT_ID`, and optionally `CLOUD_API_ENDPOINT`, then run:
99
+
100
+ ```bash
101
+ LLMZ_EVAL_MODELS=cerebras:qwen-3.8-27b LLMZ_EVAL_REPEATS=1 pnpm test:e2e e2e/protocol-matrix.test.ts
102
+ ```
103
+
104
+ The matrix contains 144 synthetic tasks across 12 languages, each in streaming and nonstreaming mode: greetings, intake, long context, tools, progress messages, tool results, silent actions, typed exits, buttons, JSON text, Markdown, and error recovery. It disables test retries and cache and rejects fallback credit. Optional `LLMZ_EVAL_RECORDS=/absolute/path/results.jsonl` records raw outputs, diagnostics, task checks, and actual model metadata. Failures remain visible: passing runtime rejection tests does not establish perfect model adherence. Protocol/control-flow assertions are separate from exact task wording checks, which are retained in the evaluation records.
105
+
106
+ ---
107
+
108
+ ## Long search results and citations
109
+
110
+ The RAG benchmark uses the same citation contract as VDK: a search tool registers source metadata, returns passages labelled with `【id】` through `ThinkSignal`, and the completed message handler extracts inline tags into citation metadata with text offsets. Its documents are entirely synthetic.
111
+
112
+ ```bash
113
+ LLMZ_EVAL_MODELS=groq:qwen3.8-27b LLMZ_EVAL_REPEATS=1 pnpm test:e2e e2e/long-search-citations.test.ts
114
+ ```
115
+
116
+ The 48 cases pair compact controls with long corpora in both streaming modes. They cover exact scope matching, superseded/future policies, joins across distant documents, and quota arithmetic, with answers near the beginning, middle, or end. Results span roughly 12,000–39,000 tokens and four response languages. Tests require correct facts, every required supporting source, no unrelated citations, intact evidence in the actual model request, no extra search/recovery hops, and the requested model without cache or fallback credit. Near-match sources may additionally support scope/date disambiguation (for example, explaining why an archived account does not apply); they cannot replace required evidence. These checks validate retrieval and source coverage, not every possible natural-language claim.
117
+
118
+ Latest tool-result strings are preserved until the request context budget is applied; they are no longer silently clipped at 4,096 characters. Inputs exceeding the model/context budget still undergo normal truncation. Separate deterministic tests cover citation offsets, grouped/repeated tags, structured component props, chunk boundaries, discarded preambles, stream restarts, failed delivery, and unknown source IDs.
85
119
 
86
120
  ---
87
121
 
@@ -331,7 +365,7 @@ const chat = new Chat({
331
365
  // Fires per token-chunk while the LLM is still generating
332
366
  onMessageDelta: (delta) => {
333
367
  if (delta.restart) {
334
- // Remove ALL this iteration's messages, including completed handler sends.
368
+ // Remove this iteration's provisional previews.
335
369
  return clearIterationMessages(delta.iterationId)
336
370
  }
337
371
  return updateBubble(delta.iterationId, delta.id, delta.content)
@@ -355,13 +389,13 @@ const result = await execute({
355
389
  - **Message deltas** stream to your UI token-by-token (`handler` remains the authoritative delivery)
356
390
  - **Live traces** cover the full turn lifecycle: `llm_call_started` → message deltas → `code_generation_started` → `llm_call_success` (with the code) → `tool_call`s → exit
357
391
  - **Fallback**: a `restart: true` delta invalidates the current iteration's messages before replacement output arrives. Replacement messages have fresh IDs under the same `iterationId`. Code executes only after the replacement stream succeeds; abandoned code never runs.
358
- - **Early execution**: without `midStreamFallback`, a closed `■run` block can start executing while the response tail is still streaming. A later generation failure returns an error without retrying the code. The VM pre-warms when code generation begins in either mode.
392
+ - **Execution**: code runs only after a complete, valid envelope and successful stream completion in every mode. The VM still pre-warms when a run block opens.
359
393
 
360
- Terminal failures return an `ErrorExecutionResult`; they do not emit a restart delta because there is no replacement attempt. Handle that result to clear or mark unfinished messages as failed. Already delivered messages remain delivered unless your consumer retracts them. Nonstreaming Cognitive fallback exposes only the successful response, so no message reset is needed.
394
+ Terminal failures return an `ErrorExecutionResult` and retract any provisional previews with a reset delta; no replacement is required for a reset. Completed handlers from abandoned attempts are never called. Nonstreaming fallback exposes only the surviving response.
361
395
 
362
- LLMz rejects explicit Cognitive errors, error metadata (`provider: "unknown"`), and responses stopped by a token limit or content filter before finalizing unfinished blocks. Such failures cannot turn a partial code prefix into executable code. With fallback disabled, this cannot undo code that already ran after a block boundary.
396
+ Explicit Cognitive errors, unknown-provider error metadata, missing streaming metadata, token-limit truncation, and content filtering cannot turn partial output into executable code. A complete code block inside an unfinished response is still provisional.
363
397
 
364
- `■run` followed by `■send` is currently accepted, but that message was generated without seeing the code's result. Prefer sending an acknowledgement before `■run`, returning the needed data, then answering in the next iteration. A top-level `return` always requests another iteration; side-effect-only code can finish immediately with `■next`.
398
+ Send messages before code. A returning run requests another iteration and must be followed directly by `■end`. Side-effect-only code may finish with `■next` before `■end`.
365
399
 
366
400
  Forward these events over a websocket or SSE stream and your frontend renders the agent live — see [example 22](https://github.com/botpress/botpress/tree/master/packages/llmz/examples/22_chat_streaming).
367
401
 
@@ -513,7 +547,7 @@ LLMz has been running in production for over a year:
513
547
  1. **Prompt Generation**: Injects tools, schemas, and context into dual-mode prompts
514
548
  2. **Streaming Generation**: The LLM streams ■ blocks — messages dispatch to the chat as they are parsed
515
549
  3. **Compilation**: Babel AST transformation with instrumentation plugins (line tracking, tool call tracking, variable extraction)
516
- 4. **Execution**: Runs in QuickJS WASM sandbox with full isolation — starting while the response tail is still streaming
550
+ 4. **Execution**: Runs in QuickJS WASM sandbox with full isolation — after successful generation and protocol validation
517
551
  5. **Result Processing**: Type-safe exit handling, thinking loops and error recovery
518
552
 
519
553
  **Security:**
package/dist/chat.d.ts CHANGED
@@ -39,9 +39,9 @@ export type MessageHandler = (input: RenderedComponent, metadata: MessageMetadat
39
39
  * Each chunk is forwarded to {@link Chat} `onMessageDelta` as soon as it is parsed, so the
40
40
  * client can render the message progressively (e.g. typewriter effect in a chat UI).
41
41
  *
42
- * In mid-stream fallback mode, both deltas and completed `handler` messages
43
- * are live and retractable: restart deltas invalidate every message from that
44
- * iteration. Consumers must retract them; only code execution waits for success.
42
+ * Deltas are provisional: restart deltas retract previews from that iteration
43
+ * after a provider restart, malformed response, or transport failure. Completed
44
+ * `handler` messages and code wait for a valid response and successful transport.
45
45
  */
46
46
  export type MessageDelta = {
47
47
  restart: false;
@@ -60,12 +60,12 @@ export type MessageDelta = {
60
60
  } | {
61
61
  /**
62
62
  * Reset-only delta, emitted before replacement text (even if none follows).
63
- * Clear ALL messages for iterationId, including completed `handler` sends.
64
- * Other iterations are unaffected. Irreversible sends cannot be undone.
63
+ * Clear provisional messages for iterationId. Other iterations are unaffected.
64
+ * Completed handler sends are never produced by an abandoned generation.
65
65
  */
66
66
  restart: true;
67
67
  iterationId: string;
68
- /** 1-based index of the new attempt. */
68
+ /** 1-based index of the next attempt; a terminal failure may have no replacement. */
69
69
  attempt: number;
70
70
  fromModel: string;
71
71
  toModel: string;
@@ -63,6 +63,7 @@ var StreamingMessageParser = class {
63
63
  _currentReady = false;
64
64
  _counter = 0;
65
65
  _finished = false;
66
+ _ended = false;
66
67
  _directiveBuffer = "";
67
68
  _nameBuffer = "";
68
69
  _propsBuffer = "";
@@ -74,6 +75,8 @@ var StreamingMessageParser = class {
74
75
  _pendingWhitespace = "";
75
76
  _preambleHeader;
76
77
  _closingFenceTicks;
78
+ _lineStart = true;
79
+ _exampleDelimiter = "";
77
80
  constructor(options = {}) {
78
81
  this._maxPropsLength = options.maxPropsLength ?? 1e5;
79
82
  }
@@ -88,7 +91,7 @@ var StreamingMessageParser = class {
88
91
  push(chunk) {
89
92
  if (this._finished) throw new Error("Parser has already finished. Call reset() before parsing a new stream.");
90
93
  const events = [];
91
- for (const char of chunk) this._processChar(char, events);
94
+ for (const char of chunk) this._processOutputChar(char, events);
92
95
  this._flushBodyDelta(events);
93
96
  return events;
94
97
  }
@@ -102,6 +105,9 @@ var StreamingMessageParser = class {
102
105
  if (this._finished) return [];
103
106
  this._finished = true;
104
107
  const events = [];
108
+ if (/^"""\s*$/.test(this._exampleDelimiter)) this._discardExampleDelimiter(events);
109
+ else if (reason === "end") this._flushExampleDelimiter(events);
110
+ else this._exampleDelimiter = "";
105
111
  if (this._closingFenceTicks !== void 0 && this._closingFenceTicks !== 3) this._skipUnexpectedText(events);
106
112
  this._closingFenceTicks = void 0;
107
113
  const status = reason === "interrupted" ? "interrupted" : void 0;
@@ -138,6 +144,7 @@ var StreamingMessageParser = class {
138
144
  this._currentReady = false;
139
145
  this._counter = 0;
140
146
  this._finished = false;
147
+ this._ended = false;
141
148
  this._directiveBuffer = "";
142
149
  this._nameBuffer = "";
143
150
  this._propsBuffer = "";
@@ -149,6 +156,50 @@ var StreamingMessageParser = class {
149
156
  this._pendingWhitespace = "";
150
157
  this._preambleHeader = void 0;
151
158
  this._closingFenceTicks = void 0;
159
+ this._lineStart = true;
160
+ this._exampleDelimiter = "";
161
+ }
162
+ _processOutputChar(char, events) {
163
+ const lineStart = this._lineStart;
164
+ this._lineStart = char === "\n" || lineStart && (char === " " || char === " " || char === "\r");
165
+ if (this._exampleDelimiter) {
166
+ if (this._exampleDelimiter.includes("\n")) {
167
+ if (isWhitespace(char)) {
168
+ this._exampleDelimiter += char;
169
+ return;
170
+ }
171
+ if (char === "■") this._discardExampleDelimiter(events);
172
+ else this._flushExampleDelimiter(events);
173
+ } else {
174
+ const candidate = this._exampleDelimiter + char;
175
+ if (/^(?:"{1,2}|"""[ \t]*\r?)$/.test(candidate)) {
176
+ this._exampleDelimiter = candidate;
177
+ return;
178
+ }
179
+ if (/^"""[ \t]*\r?\n$/.test(candidate)) {
180
+ this._exampleDelimiter = candidate;
181
+ if (this._state !== "body") this._discardExampleDelimiter(events);
182
+ return;
183
+ }
184
+ this._flushExampleDelimiter(events);
185
+ }
186
+ }
187
+ if (lineStart && char === "\"" && (this._state === "idle" || this._state === "skip" || this._state === "body-wait" || this._state === "body")) {
188
+ this._exampleDelimiter = char;
189
+ return;
190
+ }
191
+ this._processChar(char, events);
192
+ }
193
+ _flushExampleDelimiter(events) {
194
+ for (const char of this._exampleDelimiter) this._processChar(char, events);
195
+ this._exampleDelimiter = "";
196
+ }
197
+ _discardExampleDelimiter(events) {
198
+ this._exampleDelimiter = "";
199
+ this._diagnostic(events, {
200
+ code: "example-delimiter",
201
+ message: "Discarded a triple-quote example delimiter"
202
+ });
152
203
  }
153
204
  _processChar(char, events) {
154
205
  if (this._closingFenceTicks !== void 0) {
@@ -160,9 +211,18 @@ var StreamingMessageParser = class {
160
211
  this._closingFenceTicks = void 0;
161
212
  this._skipUnexpectedText(events);
162
213
  }
214
+ if (this._ended) {
215
+ if (this._state === "skip" || isWhitespace(char)) return;
216
+ if (char === "`") {
217
+ this._closingFenceTicks = 1;
218
+ return;
219
+ }
220
+ this._skipUnexpectedText(events);
221
+ return;
222
+ }
163
223
  switch (this._state) {
164
224
  case "idle":
165
- if (char === "■") this._beginItem();
225
+ if (char === "■") this._beginItem(events);
166
226
  else if (!isWhitespace(char)) {
167
227
  const last = this._items.at(-1);
168
228
  if (char === "`" && (last === null || last === void 0 ? void 0 : last.kind) === "next" && last.status === "complete") {
@@ -178,7 +238,7 @@ var StreamingMessageParser = class {
178
238
  else if (char === "■") {
179
239
  this._endDirective(events, void 0);
180
240
  this._completeCurrent(events);
181
- this._beginItem();
241
+ this._beginItem(events);
182
242
  } else if (char === "\n") this._endDirective(events, "\n");
183
243
  else if (isWhitespace(char)) this._endDirective(events, " ");
184
244
  else if (char === "{") this._endDirective(events, "{");
@@ -188,7 +248,7 @@ var StreamingMessageParser = class {
188
248
  if (char === "■") {
189
249
  this._endName(events);
190
250
  this._completeCurrent(events);
191
- this._beginItem();
251
+ this._beginItem(events);
192
252
  } else if (char === "{") {
193
253
  this._endName(events);
194
254
  this._afterHeaderChar(char, events);
@@ -209,7 +269,7 @@ var StreamingMessageParser = class {
209
269
  case "body-wait":
210
270
  if (char === "■") {
211
271
  this._completeCurrent(events);
212
- this._beginItem();
272
+ this._beginItem(events);
213
273
  } else if (!isWhitespace(char)) {
214
274
  this._startBody(events);
215
275
  this._appendBody(char);
@@ -220,7 +280,7 @@ var StreamingMessageParser = class {
220
280
  this._pendingWhitespace = "";
221
281
  this._flushBodyDelta(events);
222
282
  this._completeCurrent(events);
223
- this._beginItem();
283
+ this._beginItem(events);
224
284
  } else this._appendBody(char);
225
285
  return;
226
286
  case "skip":
@@ -233,7 +293,7 @@ var StreamingMessageParser = class {
233
293
  }
234
294
  return;
235
295
  }
236
- if (char === "■") this._beginItem();
296
+ if (char === "■") this._beginItem(events);
237
297
  return;
238
298
  }
239
299
  }
@@ -250,7 +310,7 @@ var StreamingMessageParser = class {
250
310
  const item = this._current;
251
311
  if (char === "■") {
252
312
  this._completeCurrent(events);
253
- this._beginItem();
313
+ this._beginItem(events);
254
314
  return;
255
315
  }
256
316
  if (char === "{" && (item.kind === "send" || item.kind === "next")) {
@@ -292,7 +352,7 @@ var StreamingMessageParser = class {
292
352
  else if (char === "■" && this._propsBroken) {
293
353
  this._failProps(events, "props were interrupted by a new block");
294
354
  this._completeCurrent(events);
295
- this._beginItem();
355
+ this._beginItem(events);
296
356
  }
297
357
  return;
298
358
  }
@@ -300,14 +360,18 @@ var StreamingMessageParser = class {
300
360
  else if (char === "■") {
301
361
  this._failProps(events, "props were interrupted by a new block");
302
362
  this._completeCurrent(events);
303
- this._beginItem();
363
+ this._beginItem(events);
304
364
  } else if (char === "{" || char === "[") this._propsDepth++;
305
365
  else if (char === "}" || char === "]") {
306
366
  this._propsDepth--;
307
367
  if (this._propsDepth <= 0) this._endProps(events);
308
368
  }
309
369
  }
310
- _beginItem() {
370
+ _beginItem(events) {
371
+ if (this._ended) {
372
+ this._skipUnexpectedText(events);
373
+ return;
374
+ }
311
375
  const item = {
312
376
  id: `item-${this._counter++}`,
313
377
  kind: "unknown",
@@ -506,6 +570,7 @@ var StreamingMessageParser = class {
506
570
  if (!item) return;
507
571
  this._ready(events);
508
572
  if (item.status !== "invalid") item.status = forcedStatus ?? "complete";
573
+ if (item.kind === "next" && item.status === "complete") this._ended = true;
509
574
  events.push({
510
575
  type: "item-complete",
511
576
  item
@@ -516,7 +581,7 @@ var StreamingMessageParser = class {
516
581
  _skipUnexpectedText(events) {
517
582
  this._diagnostic(events, {
518
583
  code: "unexpected-text",
519
- message: "Encountered text outside of a ■ block"
584
+ message: this._ended ? "Discarded content after terminal ■next" : "Encountered text outside of a ■ block"
520
585
  });
521
586
  this._state = "skip";
522
587
  }
@@ -533,17 +598,163 @@ var StreamingMessageParser = class {
533
598
  }
534
599
  };
535
600
  //#endregion
601
+ //#region src/message-stream/response-parser.ts
602
+ /** One wire format: a complete ■start … ■end envelope containing valid blocks. */
603
+ var ResponseParser = class {
604
+ _parser = new StreamingMessageParser();
605
+ _mode = "detect";
606
+ _discardPrefixLine = false;
607
+ _prefixDiagnostics = [];
608
+ _prefix = "";
609
+ _boundary = "";
610
+ _closed = false;
611
+ _finished = false;
612
+ _failure;
613
+ _trailer = "";
614
+ get framed() {
615
+ return this._mode === "framed";
616
+ }
617
+ get valid() {
618
+ return this._finished && this._closed && !this._failure;
619
+ }
620
+ get items() {
621
+ return this.valid ? this._parser.items : [];
622
+ }
623
+ get diagnostics() {
624
+ return [
625
+ ...this._prefixDiagnostics,
626
+ ...this._parser.diagnostics,
627
+ ...this._trailingDiagnostics,
628
+ ...this._failure ? [this._failure] : []
629
+ ];
630
+ }
631
+ push(chunk) {
632
+ if (this._finished) throw new Error("Response parser has already finished");
633
+ const events = [];
634
+ let forward = "";
635
+ const flush = () => {
636
+ if (forward) events.push(...this._parser.push(forward));
637
+ forward = "";
638
+ };
639
+ for (const char of chunk) {
640
+ if (this._mode === "detect") {
641
+ if (this._discardPrefixLine) {
642
+ if (char === "\n") this._discardPrefixLine = false;
643
+ continue;
644
+ }
645
+ if (!this._prefix && /\s/.test(char)) continue;
646
+ this._prefix += char;
647
+ const trimmed = this._prefix.trimEnd();
648
+ if (char === "\n" && trimmed === "\"\"\"") {
649
+ events.push(...this._parser.push(this._prefix));
650
+ this._prefix = "";
651
+ } else if (char === "\n" && trimmed === "■start") {
652
+ this._mode = "framed";
653
+ this._prefix = "";
654
+ } else if (!"■start".startsWith(this._prefix) && !/^■start[ \t\r]*$/.test(this._prefix) && !/^"{1,3}[ \t\r]*$/.test(this._prefix)) {
655
+ if (!this._prefixDiagnostics.length) {
656
+ const diagnostic = {
657
+ code: "unexpected-text",
658
+ message: "Discarded text before ■start"
659
+ };
660
+ this._prefixDiagnostics.push(diagnostic);
661
+ events.push({
662
+ type: "diagnostic",
663
+ diagnostic
664
+ });
665
+ }
666
+ this._prefix = "";
667
+ this._discardPrefixLine = char !== "\n";
668
+ }
669
+ continue;
670
+ }
671
+ if (this._closed) {
672
+ this._trailer += char;
673
+ continue;
674
+ }
675
+ if (this._boundary || char === "■") {
676
+ this._boundary += char;
677
+ if (this._boundary === "■end") {
678
+ flush();
679
+ events.push(...this._parser.finish());
680
+ this._boundary = "";
681
+ this._closed = true;
682
+ } else if (!"■end".startsWith(this._boundary)) {
683
+ forward += this._boundary;
684
+ this._boundary = "";
685
+ }
686
+ } else forward += char;
687
+ }
688
+ flush();
689
+ return events;
690
+ }
691
+ finish(stopReason) {
692
+ if (this._finished) return [];
693
+ this._finished = true;
694
+ const events = [];
695
+ if (this._mode === "detect") {
696
+ if (this._prefix.trim() === "■start") this._mode = "framed";
697
+ else this._fail("Missing ■start: every response must begin with the response envelope");
698
+ this._prefix = "";
699
+ }
700
+ if (this.framed) {
701
+ if (!this._boundary && stopReason === "stop") this._closed = true;
702
+ else if (this._boundary) events.push(...this._parser.push(this._boundary));
703
+ events.push(...this._parser.finish());
704
+ const trailer = this._trailer.trim();
705
+ if (!this._closed) this._fail("Missing ■end: the response envelope is incomplete");
706
+ else if (this._parser.diagnostics.some((d) => d.code !== "example-delimiter")) this._fail("The response envelope contains malformed protocol blocks");
707
+ if (!this._failure) this._validateBlocks();
708
+ if (trailer === "\"\"\"") {
709
+ const parser = new StreamingMessageParser();
710
+ events.push(...parser.push(trailer), ...parser.finish());
711
+ this._trailingDiagnostics = parser.diagnostics;
712
+ } else if (trailer) {
713
+ const diagnostic = {
714
+ code: "unexpected-text",
715
+ message: "Discarded content after ■end"
716
+ };
717
+ this._trailingDiagnostics.push(diagnostic);
718
+ events.push({
719
+ type: "diagnostic",
720
+ diagnostic
721
+ });
722
+ }
723
+ if (this._failure) events.push({
724
+ type: "diagnostic",
725
+ diagnostic: this._failure
726
+ });
727
+ } else if (this._failure) events.push({
728
+ type: "diagnostic",
729
+ diagnostic: this._failure
730
+ });
731
+ return events;
732
+ }
733
+ _trailingDiagnostics = [];
734
+ _validateBlocks() {
735
+ const items = this._parser.items;
736
+ const runs = items.filter((item) => item.kind === "run");
737
+ const next = items.findIndex((item) => item.kind === "next");
738
+ const run = items.findIndex((item) => item.kind === "run");
739
+ if (runs.length > 1 || run >= 0 && items.slice(run + 1).some((item) => item.kind === "send")) this._fail("Use at most one ■run, after all messages; never send a message after code");
740
+ else if (run >= 0 && hasTopLevelReturn(items[run].body ?? "")) {
741
+ if (run !== items.length - 1) this._fail("After code returning a result, write ■end and wait for the result");
742
+ } else if (next < 0 && run < 0) this._fail("Finish with ■next, or return a result from ■run for the next iteration");
743
+ }
744
+ _fail(message) {
745
+ this._failure = {
746
+ code: "invalid-envelope",
747
+ message
748
+ };
749
+ }
750
+ };
751
+ //#endregion
536
752
  //#region src/prompts/common.ts
537
753
  var common_exports = /* @__PURE__ */ __exportAll({
538
754
  parseAssistantResponse: () => parseAssistantResponse,
539
755
  replacePlaceholders: () => replacePlaceholders,
540
756
  toParsedAssistantResponse: () => toParsedAssistantResponse
541
757
  });
542
- /** Strips wrapping code fences the model may have added around the whole response. */
543
- const stripWrappingFences = (text) => text.trim().split("\n").filter((line, index, arr) => {
544
- if ((index === 0 || index === arr.length - 1) && line.trim().startsWith("```")) return false;
545
- return true;
546
- }).join("\n");
547
758
  /** Builds a {@link ParsedAssistantResponse} from parsed protocol items. */
548
759
  const toParsedAssistantResponse = (items, raw, diagnostics = []) => {
549
760
  var _run$body;
@@ -573,10 +784,10 @@ const toParsedAssistantResponse = (items, raw, diagnostics = []) => {
573
784
  } : void 0
574
785
  };
575
786
  };
576
- const parseAssistantResponse = (response) => {
577
- const parser = new StreamingMessageParser();
578
- parser.push(stripWrappingFences(response));
579
- parser.finish();
787
+ const parseAssistantResponse = (response, stopReason) => {
788
+ const parser = new ResponseParser();
789
+ parser.push(response);
790
+ parser.finish(stopReason);
580
791
  return toParsedAssistantResponse(parser.items, response, parser.diagnostics);
581
792
  };
582
793
  const replacePlaceholders = (prompt, values) => {
@@ -593,4 +804,4 @@ const replacePlaceholders = (prompt, values) => {
593
804
  return replaced.replace(/\n{3,}/g, "\n\n").trim();
594
805
  };
595
806
  //#endregion
596
- export { StreamingMessageParser as a, NAME_REGEX as c, toParsedAssistantResponse as i, parseAssistantResponse as n, tryParseJson as o, replacePlaceholders as r, MARKER as s, common_exports as t };
807
+ export { ResponseParser as a, MARKER as c, toParsedAssistantResponse as i, NAME_REGEX as l, parseAssistantResponse as n, StreamingMessageParser as o, replacePlaceholders as r, tryParseJson as s, common_exports as t };