@ouro.bot/cli 0.1.0-alpha.723 → 0.1.0-alpha.725

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 (55) hide show
  1. package/changelog.json +295 -275
  2. package/dist/heart/active-work.js +10 -3
  3. package/dist/heart/core.js +343 -77
  4. package/dist/heart/daemon/cli-exec.js +132 -15
  5. package/dist/heart/daemon/cli-help.js +13 -2
  6. package/dist/heart/daemon/cli-parse.js +82 -1
  7. package/dist/heart/daemon/daemon.js +286 -33
  8. package/dist/heart/daemon/doctor.js +82 -6
  9. package/dist/heart/daemon/habit-private-wake.js +6 -1
  10. package/dist/heart/habits/habit-cancel.js +810 -0
  11. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  12. package/dist/heart/habits/habit-migration.js +28 -23
  13. package/dist/heart/habits/habit-parser.js +72 -8
  14. package/dist/heart/habits/habit-runtime-state.js +3 -24
  15. package/dist/heart/habits/habit-scheduler.js +268 -50
  16. package/dist/heart/habits/habit-session.js +16 -0
  17. package/dist/heart/hatch/hatch-flow.js +6 -4
  18. package/dist/heart/mailbox/readers/runtime-readers.js +26 -27
  19. package/dist/heart/orientation-frame.js +58 -1
  20. package/dist/heart/provider-attempt.js +4 -1
  21. package/dist/heart/providers/anthropic.js +23 -2
  22. package/dist/heart/start-of-turn-packet.js +14 -11
  23. package/dist/heart/streaming.js +565 -185
  24. package/dist/mind/obligation-steering.js +15 -9
  25. package/dist/mind/prompt.js +58 -26
  26. package/dist/nerves/coverage/run-artifacts.js +55 -35
  27. package/dist/nerves/review/cli.js +2 -2
  28. package/dist/nerves/review/core.js +14 -7
  29. package/dist/repertoire/tools-base.js +2 -0
  30. package/dist/repertoire/tools-habits.js +103 -0
  31. package/dist/repertoire/tools.js +18 -3
  32. package/dist/rsvp/cli.js +126 -20
  33. package/dist/rsvp/diagnostics.js +12 -2
  34. package/dist/rsvp/habit-policy.js +7 -0
  35. package/dist/rsvp/habit-stage.js +19 -4
  36. package/dist/rsvp/native-habit-runner.js +151 -11
  37. package/dist/rsvp/outbound-state.js +360 -0
  38. package/dist/senses/bluebubbles/client.js +90 -17
  39. package/dist/senses/bluebubbles/inbound-log.js +1 -0
  40. package/dist/senses/bluebubbles/index.js +662 -425
  41. package/dist/senses/bluebubbles/model.js +57 -13
  42. package/dist/senses/bluebubbles/mutation-log.js +21 -10
  43. package/dist/senses/bluebubbles/reaction-policy.js +55 -0
  44. package/dist/senses/bluebubbles/semantic-receipts.js +1818 -0
  45. package/dist/senses/cli/tui-store.js +1 -0
  46. package/dist/senses/cli.js +1 -0
  47. package/dist/senses/habit-lifecycle-guard.js +24 -0
  48. package/dist/senses/ingress-evidence.js +2 -0
  49. package/dist/senses/mail.js +129 -0
  50. package/dist/senses/pipeline.js +90 -24
  51. package/dist/senses/private-runtime-worker.js +21 -20
  52. package/dist/senses/private-runtime.js +79 -29
  53. package/dist/senses/shared-turn.js +1 -0
  54. package/dist/senses/teams.js +1 -0
  55. package/package.json +6 -1
@@ -1,80 +1,285 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RESPONSES_FUNCTION_CALL_OUTPUT_CAP = exports.SettleStreamer = exports.SettleParser = void 0;
3
+ exports.RESPONSES_FUNCTION_CALL_OUTPUT_CAP = exports.ProviderTextOutput = exports.SettleStreamer = exports.SettleParser = exports.SettleFinalizationCallbackError = void 0;
4
+ exports.finalizeSettleStream = finalizeSettleStream;
4
5
  exports.truncateResponsesFunctionCallOutput = truncateResponsesFunctionCallOutput;
5
6
  exports.toResponsesInput = toResponsesInput;
6
7
  exports.toResponsesTools = toResponsesTools;
7
8
  exports.streamChatCompletion = streamChatCompletion;
8
9
  exports.streamResponsesApi = streamResponsesApi;
9
10
  const runtime_1 = require("../nerves/runtime");
10
- // Character-level state machine that extracts the answer value from
11
- // `settle` tool call JSON arguments as they stream in.
12
- // Scans for prefix `"answer":"` or `"answer": "` in the character stream,
13
- // then emits text handling JSON escapes, stopping at unescaped closing `"`.
11
+ class SettleFinalizationCallbackError extends Error {
12
+ retryable = false;
13
+ cause;
14
+ constructor(cause) {
15
+ const normalized = cause instanceof Error ? cause : new Error(String(cause));
16
+ super(`settle finalization callback failed: ${normalized.message}`);
17
+ this.name = "SettleFinalizationCallbackError";
18
+ this.cause = normalized;
19
+ }
20
+ }
21
+ exports.SettleFinalizationCallbackError = SettleFinalizationCallbackError;
22
+ function isJsonWhitespace(character) {
23
+ return character === " " || character === "\t" || character === "\n" || character === "\r";
24
+ }
25
+ function isHighSurrogate(character) {
26
+ const code = character.charCodeAt(0);
27
+ return code >= 0xD800 && code <= 0xDBFF;
28
+ }
29
+ function isLowSurrogate(character) {
30
+ const code = character.charCodeAt(0);
31
+ return code >= 0xDC00 && code <= 0xDFFF;
32
+ }
33
+ // Incremental JSON string decoder for the top-level `answer` property in
34
+ // streamed settle arguments. JSON.parse remains the terminal authority; this
35
+ // state machine exists only to emit validated string units before EOF.
14
36
  class SettleParser {
15
- // Possible prefixes to match (with and without space after colon)
16
- static PREFIXES = ['"answer":"', '"answer": "'];
17
- // Buffer of characters seen so far (pre-activation only)
18
- buf = "";
37
+ rawArguments = "";
19
38
  _active = false;
20
39
  _complete = false;
21
- inEscape = false;
40
+ invalid = false;
41
+ terminalResult;
42
+ containers = [];
43
+ rootLastSignificant = "";
44
+ scanInString = false;
45
+ scanEscape = false;
46
+ scanStringRaw = "";
47
+ scanStringIsRootKey = false;
48
+ answerKeyState = "none";
49
+ decodeState = "normal";
50
+ unicodeDigits = "";
51
+ pendingHighSurrogate = "";
52
+ decodedAnswer = "";
22
53
  get active() { return this._active; }
23
54
  get complete() { return this._complete; }
24
55
  process(delta) {
25
- if (this._complete)
56
+ if (this.terminalResult)
57
+ return "";
58
+ this.rawArguments += delta;
59
+ if (this._complete || this.invalid)
26
60
  return "";
27
61
  let out = "";
28
62
  for (let i = 0; i < delta.length; i++) {
29
63
  const ch = delta[i];
30
- if (!this._active) {
31
- this.buf += ch;
32
- // Check if any prefix has been fully matched in the buffer
33
- for (const prefix of SettleParser.PREFIXES) {
34
- if (this.buf.endsWith(prefix)) {
35
- this._active = true;
36
- break;
37
- }
38
- }
64
+ if (this._active) {
65
+ out += this.processAnswerCharacter(ch);
39
66
  }
40
67
  else {
41
- // Active: emit characters, handling JSON escapes
42
- if (this.inEscape) {
43
- this.inEscape = false;
44
- switch (ch) {
45
- case '"':
46
- out += '"';
47
- break;
48
- case '\\':
49
- out += '\\';
50
- break;
51
- case 'n':
52
- out += '\n';
53
- break;
54
- case 't':
55
- out += '\t';
56
- break;
57
- case '/':
58
- out += '/';
59
- break;
60
- default:
61
- out += ch;
62
- break; // unknown escape: pass through character
68
+ this.scanForTopLevelAnswer(ch);
69
+ }
70
+ if (this._complete || this.invalid)
71
+ break;
72
+ }
73
+ return out;
74
+ }
75
+ finish() {
76
+ if (this.terminalResult)
77
+ return this.terminalResult;
78
+ if (this.invalid) {
79
+ return this.setTerminalFailure("invalid_settle_arguments");
80
+ }
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(this.rawArguments);
84
+ }
85
+ catch (error) {
86
+ const errorCode = this.isIncompleteAtEof(error)
87
+ ? "incomplete_settle_arguments"
88
+ : "invalid_settle_arguments";
89
+ return this.setTerminalFailure(errorCode);
90
+ }
91
+ if (typeof parsed !== "object"
92
+ || parsed === null
93
+ || Array.isArray(parsed)
94
+ || typeof parsed.answer !== "string"
95
+ || !this._active
96
+ || !this._complete) {
97
+ return this.setTerminalFailure("invalid_settle_arguments");
98
+ }
99
+ const answer = parsed.answer;
100
+ if (answer !== this.decodedAnswer) {
101
+ return this.setTerminalFailure("invalid_settle_arguments");
102
+ }
103
+ this.terminalResult = { ok: true, answer };
104
+ return this.terminalResult;
105
+ }
106
+ scanForTopLevelAnswer(character) {
107
+ if (this.scanInString) {
108
+ if (this.scanEscape) {
109
+ this.scanStringRaw += character;
110
+ this.scanEscape = false;
111
+ return;
112
+ }
113
+ if (character === "\\") {
114
+ this.scanStringRaw += character;
115
+ this.scanEscape = true;
116
+ return;
117
+ }
118
+ if (character !== '"') {
119
+ this.scanStringRaw += character;
120
+ return;
121
+ }
122
+ this.scanInString = false;
123
+ this.rootLastSignificant = '"';
124
+ if (this.scanStringIsRootKey) {
125
+ try {
126
+ if (JSON.parse(`"${this.scanStringRaw}"`) === "answer") {
127
+ this.answerKeyState = "after_key";
63
128
  }
64
129
  }
65
- else if (ch === '\\') {
66
- this.inEscape = true;
67
- }
68
- else if (ch === '"') {
69
- this._complete = true;
70
- return out; // stop processing, closing quote found
71
- }
72
- else {
73
- out += ch;
130
+ catch {
131
+ this.invalid = true;
74
132
  }
75
133
  }
134
+ this.scanStringRaw = "";
135
+ this.scanStringIsRootKey = false;
136
+ return;
76
137
  }
77
- return out;
138
+ if (this.answerKeyState === "after_key") {
139
+ if (isJsonWhitespace(character))
140
+ return;
141
+ if (character === ":") {
142
+ this.answerKeyState = "after_colon";
143
+ this.rootLastSignificant = character;
144
+ return;
145
+ }
146
+ this.invalid = true;
147
+ return;
148
+ }
149
+ if (this.answerKeyState === "after_colon") {
150
+ if (isJsonWhitespace(character))
151
+ return;
152
+ this.answerKeyState = "none";
153
+ if (character === '"') {
154
+ this._active = true;
155
+ this.rootLastSignificant = "value";
156
+ return;
157
+ }
158
+ }
159
+ if (isJsonWhitespace(character))
160
+ return;
161
+ const atRootObject = this.containers.length === 1 && this.containers[0] === "object";
162
+ if (character === '"') {
163
+ this.scanInString = true;
164
+ this.scanEscape = false;
165
+ this.scanStringRaw = "";
166
+ this.scanStringIsRootKey = atRootObject
167
+ && (this.rootLastSignificant === "{" || this.rootLastSignificant === ",");
168
+ return;
169
+ }
170
+ if (character === "{" || character === "[") {
171
+ if (atRootObject)
172
+ this.rootLastSignificant = "value";
173
+ this.containers.push(character === "{" ? "object" : "array");
174
+ if (this.containers.length === 1 && character === "{") {
175
+ this.rootLastSignificant = character;
176
+ }
177
+ return;
178
+ }
179
+ if (character === "}" || character === "]") {
180
+ const expected = character === "}" ? "object" : "array";
181
+ if (this.containers.at(-1) !== expected) {
182
+ this.invalid = true;
183
+ return;
184
+ }
185
+ this.containers.pop();
186
+ if (this.containers.length === 1 && this.containers[0] === "object") {
187
+ this.rootLastSignificant = "value";
188
+ }
189
+ return;
190
+ }
191
+ if (atRootObject)
192
+ this.rootLastSignificant = character;
193
+ }
194
+ processAnswerCharacter(character) {
195
+ if (this.decodeState === "unicode") {
196
+ if (!/^[0-9A-Fa-f]$/.test(character)) {
197
+ this.invalid = true;
198
+ return "";
199
+ }
200
+ this.unicodeDigits += character;
201
+ if (this.unicodeDigits.length < 4)
202
+ return "";
203
+ const decoded = String.fromCharCode(Number.parseInt(this.unicodeDigits, 16));
204
+ this.unicodeDigits = "";
205
+ this.decodeState = "normal";
206
+ return this.appendDecodedUnit(decoded);
207
+ }
208
+ if (this.decodeState === "escape") {
209
+ this.decodeState = "normal";
210
+ switch (character) {
211
+ case '"': return this.appendDecodedUnit('"');
212
+ case "\\": return this.appendDecodedUnit("\\");
213
+ case "/": return this.appendDecodedUnit("/");
214
+ case "b": return this.appendDecodedUnit("\b");
215
+ case "f": return this.appendDecodedUnit("\f");
216
+ case "n": return this.appendDecodedUnit("\n");
217
+ case "r": return this.appendDecodedUnit("\r");
218
+ case "t": return this.appendDecodedUnit("\t");
219
+ case "u":
220
+ this.decodeState = "unicode";
221
+ return "";
222
+ default:
223
+ this.invalid = true;
224
+ return "";
225
+ }
226
+ }
227
+ if (character === "\\") {
228
+ this.decodeState = "escape";
229
+ return "";
230
+ }
231
+ if (character === '"') {
232
+ const trailing = this.flushPendingHighSurrogate();
233
+ this._complete = true;
234
+ return trailing;
235
+ }
236
+ if (character.charCodeAt(0) <= 0x1F) {
237
+ this.invalid = true;
238
+ return "";
239
+ }
240
+ return this.appendDecodedUnit(character);
241
+ }
242
+ appendDecodedUnit(character) {
243
+ let out = "";
244
+ if (this.pendingHighSurrogate) {
245
+ if (isLowSurrogate(character)) {
246
+ const pair = this.pendingHighSurrogate + character;
247
+ this.pendingHighSurrogate = "";
248
+ this.decodedAnswer += pair;
249
+ return pair;
250
+ }
251
+ out = this.flushPendingHighSurrogate();
252
+ }
253
+ if (isHighSurrogate(character)) {
254
+ this.pendingHighSurrogate = character;
255
+ return out;
256
+ }
257
+ this.decodedAnswer += character;
258
+ return out + character;
259
+ }
260
+ flushPendingHighSurrogate() {
261
+ if (!this.pendingHighSurrogate)
262
+ return "";
263
+ const pending = this.pendingHighSurrogate;
264
+ this.pendingHighSurrogate = "";
265
+ this.decodedAnswer += pending;
266
+ return pending;
267
+ }
268
+ isIncompleteAtEof(error) {
269
+ if (this._active && !this._complete)
270
+ return true;
271
+ if (this.rawArguments.length === 0)
272
+ return true;
273
+ if (!(error instanceof SyntaxError))
274
+ return false;
275
+ if (/Unexpected end|Unterminated string/i.test(error.message))
276
+ return true;
277
+ const position = /position (\d+)/i.exec(error.message);
278
+ return position ? Number(position[1]) >= this.rawArguments.length : false;
279
+ }
280
+ setTerminalFailure(errorCode) {
281
+ this.terminalResult = { ok: false, errorCode };
282
+ return this.terminalResult;
78
283
  }
79
284
  }
80
285
  exports.SettleParser = SettleParser;
@@ -84,14 +289,23 @@ exports.SettleParser = SettleParser;
84
289
  class SettleStreamer {
85
290
  parser = new SettleParser();
86
291
  _detected = false;
292
+ _streamed = false;
87
293
  callbacks;
88
294
  enabled;
295
+ mode;
296
+ observedArguments = "";
297
+ eagerAnswer = "";
298
+ terminalResult;
299
+ cancelled = false;
89
300
  constructor(callbacks, enabled = true) {
90
301
  this.callbacks = callbacks;
91
302
  this.enabled = enabled;
303
+ // Production callback owners declare their mode explicitly. Retain the
304
+ // historical retractable default for third-party/in-test callback objects.
305
+ this.mode = callbacks.settleOutputMode ?? "retractable_buffer";
92
306
  }
93
307
  get detected() { return this._detected; }
94
- get streamed() { return this.parser.active; }
308
+ get streamed() { return this._streamed; }
95
309
  /** Mark settle as detected. Calls onClearText on the callbacks. */
96
310
  activate() {
97
311
  if (!this.enabled)
@@ -99,7 +313,8 @@ class SettleStreamer {
99
313
  if (this._detected)
100
314
  return;
101
315
  this._detected = true;
102
- this.callbacks.onClearText?.();
316
+ if (this.mode === "retractable_buffer")
317
+ this.callbacks.onClearText?.();
103
318
  }
104
319
  /** Feed an argument delta through the parser. Emits text via onTextChunk. */
105
320
  processDelta(delta) {
@@ -107,12 +322,115 @@ class SettleStreamer {
107
322
  return;
108
323
  if (!this._detected)
109
324
  return;
325
+ if (this.cancelled || this.terminalResult)
326
+ return;
327
+ this.observedArguments += delta;
110
328
  const text = this.parser.process(delta);
111
- if (text)
329
+ if (text && this.mode === "retractable_buffer") {
112
330
  this.callbacks.onTextChunk(text);
331
+ this.eagerAnswer += text;
332
+ }
333
+ }
334
+ /** Validate the completed provider arguments exactly once, then commit or
335
+ * retract callback-visible output according to the callback owner's mode. */
336
+ finish(completedArguments) {
337
+ if (!this.enabled || !this._detected || this.cancelled)
338
+ return undefined;
339
+ if (this.terminalResult)
340
+ return this.terminalResult;
341
+ let finalSuffix = "";
342
+ let replacedObservedArguments = false;
343
+ if (completedArguments.startsWith(this.observedArguments)) {
344
+ const suffix = completedArguments.slice(this.observedArguments.length);
345
+ this.observedArguments = completedArguments;
346
+ finalSuffix = this.parser.process(suffix);
347
+ }
348
+ else {
349
+ // A provider may normalize or replace its streamed argument fragments in
350
+ // the completed tool call. Re-parse that one authoritative final value;
351
+ // any eager output from the divergent fragment is retracted below.
352
+ replacedObservedArguments = true;
353
+ this.parser = new SettleParser();
354
+ this.observedArguments = completedArguments;
355
+ finalSuffix = this.parser.process(completedArguments);
356
+ }
357
+ const result = this.parser.finish();
358
+ // Cache before invoking callbacks: a callback failure must never make a
359
+ // repeated finish parse or commit the answer again.
360
+ this.terminalResult = result;
361
+ if (!result.ok) {
362
+ if (this.mode === "retractable_buffer")
363
+ this.callbacks.onClearText?.();
364
+ return result;
365
+ }
366
+ if (this.mode === "retractable_buffer") {
367
+ if (replacedObservedArguments) {
368
+ this.callbacks.onClearText?.();
369
+ this.callbacks.onTextChunk(result.answer);
370
+ }
371
+ else if (finalSuffix) {
372
+ this.callbacks.onTextChunk(finalSuffix);
373
+ }
374
+ this._streamed = true;
375
+ }
376
+ return result;
377
+ }
378
+ /** Cancel a partial provider stream without terminal parsing or commit. */
379
+ cancel() {
380
+ if (!this.enabled || !this._detected || this.cancelled || this.terminalResult)
381
+ return;
382
+ this.cancelled = true;
383
+ if (this.mode === "retractable_buffer" && this.eagerAnswer) {
384
+ this.callbacks.onClearText?.();
385
+ }
113
386
  }
114
387
  }
115
388
  exports.SettleStreamer = SettleStreamer;
389
+ function finalizeSettleStream(streamer, completedArguments) {
390
+ try {
391
+ return streamer.finish(completedArguments);
392
+ }
393
+ catch (error) {
394
+ throw new SettleFinalizationCallbackError(error);
395
+ }
396
+ }
397
+ /** Holds ordinary provider text until a final-only response is known not to be
398
+ * an invalid settle. Retractable callback owners continue receiving deltas. */
399
+ class ProviderTextOutput {
400
+ callbacks;
401
+ bufferedText = "";
402
+ deferred;
403
+ suppressed = false;
404
+ constructor(callbacks) {
405
+ this.callbacks = callbacks;
406
+ this.deferred = callbacks.settleOutputMode === "final_only";
407
+ }
408
+ emit(text) {
409
+ if (this.suppressed)
410
+ return;
411
+ if (this.deferred) {
412
+ this.bufferedText += text;
413
+ }
414
+ else {
415
+ this.callbacks.onTextChunk(text);
416
+ }
417
+ }
418
+ suppress() {
419
+ this.bufferedText = "";
420
+ this.suppressed = true;
421
+ }
422
+ discard() {
423
+ this.bufferedText = "";
424
+ }
425
+ flush() {
426
+ if (!this.bufferedText)
427
+ return;
428
+ const text = this.bufferedText;
429
+ this.bufferedText = "";
430
+ this.callbacks.onTextChunk(text);
431
+ }
432
+ }
433
+ exports.ProviderTextOutput = ProviderTextOutput;
116
434
  exports.RESPONSES_FUNCTION_CALL_OUTPUT_CAP = 200_000;
117
435
  function truncateResponsesFunctionCallOutput(output, maxChars = exports.RESPONSES_FUNCTION_CALL_OUTPUT_CAP) {
118
436
  if (output.length <= maxChars)
@@ -259,6 +577,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal, eag
259
577
  let streamStarted = false;
260
578
  let usage;
261
579
  const answerStreamer = new SettleStreamer(callbacks, eagerSettleStreaming);
580
+ const textOutput = new ProviderTextOutput(callbacks);
262
581
  // State machine for parsing inline <think> tags (MiniMax pattern)
263
582
  let contentBuf = "";
264
583
  let inThinkTag = false;
@@ -301,7 +620,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal, eag
301
620
  if (start !== -1) {
302
621
  const text = contentBuf.slice(0, start);
303
622
  if (text)
304
- callbacks.onTextChunk(text);
623
+ textOutput.emit(text);
305
624
  contentBuf = contentBuf.slice(start + OPEN_TAG.length);
306
625
  inThinkTag = true;
307
626
  }
@@ -316,92 +635,117 @@ async function streamChatCompletion(client, createParams, callbacks, signal, eag
316
635
  if (retain > 0) {
317
636
  const text = contentBuf.slice(0, -retain);
318
637
  if (text)
319
- callbacks.onTextChunk(text);
638
+ textOutput.emit(text);
320
639
  contentBuf = contentBuf.slice(-retain);
321
640
  return;
322
641
  }
323
642
  }
324
643
  // All content, flush it
325
- callbacks.onTextChunk(contentBuf);
644
+ textOutput.emit(contentBuf);
326
645
  contentBuf = "";
327
646
  }
328
647
  }
329
648
  }
330
649
  }
331
- for await (const chunk of response) {
332
- if (signal?.aborted)
333
- break;
334
- // Capture usage from final chunk (sent when stream_options.include_usage is true)
335
- if (chunk.usage) {
336
- const u = chunk.usage;
337
- usage = {
338
- input_tokens: u.prompt_tokens,
339
- output_tokens: u.completion_tokens,
340
- reasoning_tokens: u.completion_tokens_details?.reasoning_tokens ?? 0,
341
- total_tokens: u.total_tokens,
342
- };
343
- }
344
- const d = chunk.choices[0]?.delta;
345
- if (!d)
346
- continue;
347
- // Handle reasoning_content (Azure AI models like DeepSeek-R1)
348
- if (d.reasoning_content) {
349
- if (!streamStarted) {
350
- callbacks.onModelStreamStart();
351
- streamStarted = true;
650
+ try {
651
+ for await (const chunk of response) {
652
+ if (signal?.aborted)
653
+ break;
654
+ // Capture usage from final chunk (sent when stream_options.include_usage is true)
655
+ if (chunk.usage) {
656
+ const u = chunk.usage;
657
+ usage = {
658
+ input_tokens: u.prompt_tokens,
659
+ output_tokens: u.completion_tokens,
660
+ reasoning_tokens: u.completion_tokens_details?.reasoning_tokens ?? 0,
661
+ total_tokens: u.total_tokens,
662
+ };
352
663
  }
353
- callbacks.onReasoningChunk(d.reasoning_content);
354
- }
355
- if (d.content) {
356
- if (!streamStarted) {
357
- callbacks.onModelStreamStart();
358
- streamStarted = true;
664
+ const d = chunk.choices[0]?.delta;
665
+ if (!d)
666
+ continue;
667
+ // Handle reasoning_content (Azure AI models like DeepSeek-R1)
668
+ if (d.reasoning_content) {
669
+ if (!streamStarted) {
670
+ callbacks.onModelStreamStart();
671
+ streamStarted = true;
672
+ }
673
+ callbacks.onReasoningChunk(d.reasoning_content);
359
674
  }
360
- content += d.content;
361
- contentBuf += d.content;
362
- processContentBuf(false);
363
- }
364
- if (d.tool_calls) {
365
- for (const tc of d.tool_calls) {
366
- if (!toolCalls[tc.index])
367
- toolCalls[tc.index] = {
368
- id: tc.id ?? "",
369
- name: tc.function?.name ?? "",
370
- arguments: "",
371
- };
372
- if (tc.id)
373
- toolCalls[tc.index].id = tc.id;
374
- if (tc.function?.name) {
375
- toolCalls[tc.index].name = tc.function.name;
376
- // Detect settle tool call on first name delta.
377
- // Only activate streaming if this is the sole tool call (index 0
378
- // and no other indices seen). Mixed calls are rejected by core.ts.
379
- if (tc.function.name === "settle" && !answerStreamer.detected
380
- && tc.index === 0 && Object.keys(toolCalls).length === 1) {
381
- answerStreamer.activate();
382
- }
675
+ if (d.content) {
676
+ if (!streamStarted) {
677
+ callbacks.onModelStreamStart();
678
+ streamStarted = true;
383
679
  }
384
- if (tc.function?.arguments) {
385
- toolCalls[tc.index].arguments += tc.function.arguments;
386
- // Feed settle argument deltas to the parser for progressive
387
- // streaming, but only when it appears to be the sole tool call.
388
- if (answerStreamer.detected && toolCalls[tc.index].name === "settle"
389
- && Object.keys(toolCalls).length === 1) {
390
- answerStreamer.processDelta(tc.function.arguments);
680
+ content += d.content;
681
+ contentBuf += d.content;
682
+ processContentBuf(false);
683
+ }
684
+ if (d.tool_calls) {
685
+ for (const tc of d.tool_calls) {
686
+ if (!toolCalls[tc.index])
687
+ toolCalls[tc.index] = {
688
+ id: tc.id ?? "",
689
+ name: tc.function?.name ?? "",
690
+ arguments: "",
691
+ };
692
+ if (tc.id)
693
+ toolCalls[tc.index].id = tc.id;
694
+ if (tc.function?.name) {
695
+ toolCalls[tc.index].name = tc.function.name;
696
+ // Detect settle tool call on first name delta.
697
+ // Only activate streaming if this is the sole tool call (index 0
698
+ // and no other indices seen). Mixed calls are rejected by core.ts.
699
+ if (tc.function.name === "settle" && !answerStreamer.detected
700
+ && tc.index === 0 && Object.keys(toolCalls).length === 1) {
701
+ answerStreamer.activate();
702
+ textOutput.suppress();
703
+ }
704
+ }
705
+ if (tc.function?.arguments) {
706
+ toolCalls[tc.index].arguments += tc.function.arguments;
707
+ // Feed settle argument deltas to the parser for progressive
708
+ // streaming, but only when it appears to be the sole tool call.
709
+ if (answerStreamer.detected && toolCalls[tc.index].name === "settle"
710
+ && Object.keys(toolCalls).length === 1) {
711
+ answerStreamer.processDelta(tc.function.arguments);
712
+ }
391
713
  }
392
714
  }
393
715
  }
394
716
  }
395
717
  }
718
+ catch (error) {
719
+ textOutput.discard();
720
+ answerStreamer.cancel();
721
+ throw error;
722
+ }
396
723
  // Flush any remaining buffer at end of stream
397
724
  if (contentBuf)
398
725
  processContentBuf(true);
726
+ const completedToolCalls = Object.values(toolCalls);
727
+ let settleFinalization;
728
+ if (signal?.aborted) {
729
+ textOutput.discard();
730
+ answerStreamer.cancel();
731
+ }
732
+ else if (completedToolCalls.length === 1
733
+ && completedToolCalls[0].name === "settle"
734
+ && answerStreamer.detected) {
735
+ textOutput.discard();
736
+ settleFinalization = finalizeSettleStream(answerStreamer, completedToolCalls[0].arguments);
737
+ }
738
+ else {
739
+ answerStreamer.cancel();
740
+ textOutput.flush();
741
+ }
399
742
  return {
400
743
  content,
401
- toolCalls: Object.values(toolCalls),
744
+ toolCalls: completedToolCalls,
402
745
  outputItems: [],
403
746
  usage,
404
747
  settleStreamed: answerStreamer.streamed,
748
+ ...(settleFinalization ? { settleFinalization } : {}),
405
749
  };
406
750
  }
407
751
  async function streamResponsesApi(client, createParams, callbacks, signal, eagerSettleStreaming = true) {
@@ -420,84 +764,119 @@ async function streamResponsesApi(client, createParams, callbacks, signal, eager
420
764
  let currentToolCall = null;
421
765
  let usage;
422
766
  const answerStreamer = new SettleStreamer(callbacks, eagerSettleStreaming);
767
+ const textOutput = new ProviderTextOutput(callbacks);
423
768
  let functionCallCount = 0;
424
- for await (const event of response) {
425
- if (signal?.aborted)
426
- break;
427
- switch (event.type) {
428
- case "response.output_text.delta":
429
- case "response.reasoning_summary_text.delta": {
430
- if (!streamStarted) {
431
- callbacks.onModelStreamStart();
432
- streamStarted = true;
433
- }
434
- const delta = String(event.delta);
435
- if (event.type === "response.output_text.delta") {
436
- callbacks.onTextChunk(delta);
437
- content += delta;
438
- }
439
- else {
440
- callbacks.onReasoningChunk(delta);
441
- }
769
+ try {
770
+ for await (const event of response) {
771
+ if (signal?.aborted)
442
772
  break;
443
- }
444
- case "response.output_item.added": {
445
- if (event.item?.type === "function_call") {
446
- functionCallCount++;
447
- currentToolCall = {
448
- call_id: String(event.item.call_id),
449
- name: String(event.item.name),
450
- arguments: "",
451
- };
452
- // Detect settle function call -- clear any streamed noise.
453
- // Only activate when this is the first (and so far only) function call.
454
- // Mixed calls are rejected by core.ts; no need to stream their args.
455
- if (String(event.item.name) === "settle" && functionCallCount === 1) {
456
- answerStreamer.activate();
773
+ switch (event.type) {
774
+ case "response.output_text.delta":
775
+ case "response.reasoning_summary_text.delta": {
776
+ if (!streamStarted) {
777
+ callbacks.onModelStreamStart();
778
+ streamStarted = true;
779
+ }
780
+ const delta = String(event.delta);
781
+ if (event.type === "response.output_text.delta") {
782
+ textOutput.emit(delta);
783
+ content += delta;
457
784
  }
785
+ else {
786
+ callbacks.onReasoningChunk(delta);
787
+ }
788
+ break;
458
789
  }
459
- break;
460
- }
461
- case "response.function_call_arguments.delta": {
462
- if (currentToolCall) {
463
- currentToolCall.arguments += event.delta;
464
- // Feed settle argument deltas to the parser for progressive
465
- // streaming, but only when it appears to be the sole function call.
466
- if (answerStreamer.detected && currentToolCall.name === "settle"
467
- && functionCallCount === 1) {
468
- answerStreamer.processDelta(String(event.delta));
790
+ case "response.output_item.added": {
791
+ if (event.item?.type === "function_call") {
792
+ functionCallCount++;
793
+ currentToolCall = {
794
+ call_id: String(event.item.call_id),
795
+ name: String(event.item.name),
796
+ arguments: "",
797
+ };
798
+ // Detect settle function call -- clear any streamed noise.
799
+ // Only activate when this is the first (and so far only) function call.
800
+ // Mixed calls are rejected by core.ts; no need to stream their args.
801
+ if (String(event.item.name) === "settle" && functionCallCount === 1) {
802
+ answerStreamer.activate();
803
+ textOutput.suppress();
804
+ }
469
805
  }
806
+ break;
470
807
  }
471
- break;
472
- }
473
- case "response.output_item.done": {
474
- outputItems.push(event.item);
475
- if (event.item?.type === "function_call") {
476
- toolCalls.push({
477
- id: String(event.item.call_id),
478
- name: String(event.item.name),
479
- arguments: String(event.item.arguments),
480
- });
481
- currentToolCall = null;
808
+ case "response.function_call_arguments.delta": {
809
+ if (currentToolCall) {
810
+ currentToolCall.arguments += event.delta;
811
+ // Feed settle argument deltas to the parser for progressive
812
+ // streaming, but only when it appears to be the sole function call.
813
+ if (answerStreamer.detected && currentToolCall.name === "settle"
814
+ && functionCallCount === 1) {
815
+ answerStreamer.processDelta(String(event.delta));
816
+ }
817
+ }
818
+ break;
482
819
  }
483
- break;
484
- }
485
- case "response.completed":
486
- case "response.done": {
487
- const u = event.response?.usage;
488
- if (u) {
489
- usage = {
490
- input_tokens: u.input_tokens,
491
- output_tokens: u.output_tokens,
492
- reasoning_tokens: u.output_tokens_details?.reasoning_tokens ?? 0,
493
- total_tokens: u.total_tokens,
494
- };
820
+ case "response.output_item.done": {
821
+ outputItems.push(event.item);
822
+ if (event.item?.type === "function_call") {
823
+ toolCalls.push({
824
+ id: String(event.item.call_id),
825
+ name: String(event.item.name),
826
+ arguments: String(event.item.arguments),
827
+ });
828
+ currentToolCall = null;
829
+ }
830
+ break;
495
831
  }
496
- break;
832
+ case "response.completed":
833
+ case "response.done": {
834
+ const u = event.response?.usage;
835
+ if (u) {
836
+ usage = {
837
+ input_tokens: u.input_tokens,
838
+ output_tokens: u.output_tokens,
839
+ reasoning_tokens: u.output_tokens_details?.reasoning_tokens ?? 0,
840
+ total_tokens: u.total_tokens,
841
+ };
842
+ }
843
+ break;
844
+ }
845
+ default:
846
+ // Unknown/unhandled events silently ignored
847
+ break;
497
848
  }
498
- default:
499
- // Unknown/unhandled events silently ignored
500
- break;
849
+ }
850
+ }
851
+ catch (error) {
852
+ textOutput.discard();
853
+ answerStreamer.cancel();
854
+ throw error;
855
+ }
856
+ let settleFinalization;
857
+ if (signal?.aborted) {
858
+ textOutput.discard();
859
+ answerStreamer.cancel();
860
+ }
861
+ else {
862
+ if (toolCalls.length === 0 && functionCallCount === 1 && currentToolCall?.name === "settle") {
863
+ toolCalls.push({
864
+ id: currentToolCall.call_id,
865
+ name: currentToolCall.name,
866
+ arguments: currentToolCall.arguments,
867
+ });
868
+ currentToolCall = null;
869
+ }
870
+ const completedSettle = toolCalls.length === 1 && toolCalls[0].name === "settle"
871
+ ? toolCalls[0]
872
+ : undefined;
873
+ if (completedSettle && answerStreamer.detected) {
874
+ textOutput.discard();
875
+ settleFinalization = finalizeSettleStream(answerStreamer, completedSettle.arguments);
876
+ }
877
+ else {
878
+ answerStreamer.cancel();
879
+ textOutput.flush();
501
880
  }
502
881
  }
503
882
  return {
@@ -506,5 +885,6 @@ async function streamResponsesApi(client, createParams, callbacks, signal, eager
506
885
  outputItems,
507
886
  usage,
508
887
  settleStreamed: answerStreamer.streamed,
888
+ ...(settleFinalization ? { settleFinalization } : {}),
509
889
  };
510
890
  }