@code-yeongyu/senpi-ai 2026.10.25-4 → 2026.10.26-2

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 (45) hide show
  1. package/dist/api/anthropic-messages.js +19 -2
  2. package/dist/api/azure-openai-responses.js +1 -1
  3. package/dist/api/bedrock-converse-stream.js +13 -1
  4. package/dist/api/cursor-agent.js +2 -2
  5. package/dist/api/devin-agent/stream-state.js +8 -0
  6. package/dist/api/devin-agent.js +1 -1
  7. package/dist/api/google-generative-ai.js +22 -10
  8. package/dist/api/google-vertex.js +22 -10
  9. package/dist/api/lazy.js +8 -2
  10. package/dist/api/mistral-conversations.js +1 -1
  11. package/dist/api/openai-codex-responses.js +1 -1
  12. package/dist/api/openai-completions.js +9 -4
  13. package/dist/api/openai-responses-shared.js +37 -3
  14. package/dist/api/openai-responses.js +1 -1
  15. package/dist/api/pi-messages.js +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/providers/data/.manifest.json +1 -1
  19. package/dist/providers/data/amazon-bedrock.json +1 -1
  20. package/dist/providers/data/opengateway.json +1 -1
  21. package/dist/providers/data/openrouter.json +1 -1
  22. package/dist/providers/faux.js +5 -1
  23. package/dist/tool-call-middleware/protocols/kimi-xtml/thinking-recovery-stream.js +37 -2
  24. package/dist/tool-call-middleware/recovery-event-stream.d.ts +12 -1
  25. package/dist/tool-call-middleware/recovery-event-stream.js +30 -4
  26. package/dist/tool-call-middleware/recovery-native-projection.d.ts +10 -0
  27. package/dist/tool-call-middleware/recovery-native-projection.js +36 -7
  28. package/dist/tool-call-middleware/recovery-stream-failure.js +3 -2
  29. package/dist/tool-call-middleware/recovery-stream-terminal.d.ts +6 -0
  30. package/dist/tool-call-middleware/recovery-stream-terminal.js +35 -17
  31. package/dist/tool-call-middleware/recovery-stream-wrapper.js +26 -12
  32. package/dist/tool-call-middleware/stream-wrapper.js +3 -1
  33. package/dist/utils/assistant-capture.d.ts +101 -0
  34. package/dist/utils/assistant-capture.js +327 -0
  35. package/dist/utils/event-stream.d.ts +15 -2
  36. package/dist/utils/event-stream.js +57 -8
  37. package/dist/utils/tool-argument-coercion.d.ts +2 -0
  38. package/dist/utils/tool-argument-coercion.js +74 -0
  39. package/dist/utils/tool-argument-validator.d.ts +20 -0
  40. package/dist/utils/tool-argument-validator.js +9 -0
  41. package/dist/utils/tool-schema-compat.d.ts +5 -0
  42. package/dist/utils/tool-schema-compat.js +5 -0
  43. package/dist/utils/validation.d.ts +1 -0
  44. package/dist/utils/validation.js +14 -75
  45. package/package.json +2 -2
@@ -0,0 +1,327 @@
1
+ const captures = new WeakMap();
2
+ const readers = new WeakMap();
3
+ const writers = new WeakMap();
4
+ /** `undefined` until a capture-enabled stream's first push decides it, `null` once decided off. */
5
+ const states = new WeakMap();
6
+ const demanded = new WeakSet();
7
+ const warned = new WeakSet();
8
+ const NO_CHANGE = Object.freeze({});
9
+ let subscribers = 0;
10
+ let capturedAny = false;
11
+ /** While held, every capture-enabled stream whose first push comes after it builds capture records. */
12
+ export function subscribeAssistantCapture() {
13
+ subscribers++;
14
+ let held = true;
15
+ return () => {
16
+ if (held)
17
+ subscribers--;
18
+ held = false;
19
+ };
20
+ }
21
+ export function setAssistantReader(owner, reader) {
22
+ readers.set(owner, reader);
23
+ }
24
+ export function setAssistantProgressWriter(owner, writer) {
25
+ writers.set(owner, writer);
26
+ }
27
+ export function enableAssistantCapture(owner) {
28
+ if (!states.has(owner))
29
+ states.set(owner, undefined);
30
+ }
31
+ /** Decided once, at the first push, so a stream never starts capturing midway with partial offsets. */
32
+ function activeState(owner) {
33
+ const state = states.get(owner);
34
+ if (state !== undefined || !states.has(owner))
35
+ return state ?? undefined;
36
+ const decided = subscribers > 0 || demanded.has(owner)
37
+ ? { positions: new Map(), metadata: new Map(), insertCopies: new Map(), failedBlocks: new Set() }
38
+ : null;
39
+ capturedAny ||= decided !== null;
40
+ states.set(owner, decided);
41
+ return decided ?? undefined;
42
+ }
43
+ /** A capture failure never reaches the provider stream: the block loses its records and one warning is logged. */
44
+ function dropCapture(owner, error, state, contentIndex) {
45
+ if (contentIndex !== undefined)
46
+ state?.failedBlocks.add(contentIndex);
47
+ if (warned.has(owner))
48
+ return;
49
+ warned.add(owner);
50
+ console.warn("[assistant-capture] could not build an internal capture record; that block's records are dropped", error);
51
+ }
52
+ export function pushAssistantProgress(owner, progress) {
53
+ const writer = writers.get(owner);
54
+ if (!writer)
55
+ return;
56
+ // A forwarded record is already a frozen public copy, made by the stream that captured it.
57
+ if (Object.isFrozen(progress)) {
58
+ writer(progress);
59
+ return;
60
+ }
61
+ const state = activeState(owner);
62
+ if (!state || state.failedBlocks.has(progress.contentIndex))
63
+ return;
64
+ try {
65
+ writer(freezeProgress(state, progress));
66
+ }
67
+ catch (error) {
68
+ dropCapture(owner, error, state, progress.contentIndex);
69
+ }
70
+ }
71
+ function freezeProgress(state, progress) {
72
+ if (progress.type === "message_tool_arguments_reset")
73
+ state.positions.set(progress.contentIndex, {
74
+ offset: progress.rawArgumentPrefix.length,
75
+ parsedThrough: progress.parsedThrough,
76
+ });
77
+ if (progress.type !== "message_block_state")
78
+ return Object.freeze({ ...progress });
79
+ const previous = state.insertCopies.get(progress.contentIndex);
80
+ const current = publicBlock(progress.block);
81
+ const block = previous !== undefined && sameValue(current, previous) ? previous : freezeCapture(current);
82
+ if (progress.phase === "insert")
83
+ state.insertCopies.set(progress.contentIndex, block);
84
+ else
85
+ state.insertCopies.delete(progress.contentIndex);
86
+ return Object.freeze({ ...progress, block });
87
+ }
88
+ /** A text or thinking signature replaced without a public event; an unchanged or empty one records nothing. */
89
+ export function pushAssistantSignature(owner, contentIndex, block, signature) {
90
+ if (!signature || signature === (block.type === "text" ? block.textSignature : block.thinkingSignature))
91
+ return;
92
+ pushAssistantProgress(owner, {
93
+ type: "message_block_metadata",
94
+ contentIndex,
95
+ ...(block.type === "text" ? { textSignature: signature } : { thinkingSignature: signature }),
96
+ });
97
+ }
98
+ /** A block appended complete with no public event: its unchanged end reuses the insert's frozen copy. */
99
+ export function pushAssistantCompleteBlock(owner, contentIndex, block) {
100
+ for (const phase of ["insert", "end"])
101
+ pushAssistantProgress(owner, { type: "message_block_state", contentIndex, phase, block });
102
+ }
103
+ /** Internal consumption is explicit; ordinary iterators are the downgrade boundary. Attached before the first push, it turns capture on. */
104
+ export function assistantRecords(source) {
105
+ demanded.add(source);
106
+ const read = readers.get(source);
107
+ return {
108
+ [Symbol.asyncIterator]: () => read?.((entry) => ({
109
+ value: "event" in entry ? entry.event : entry.progress,
110
+ done: false,
111
+ })) ?? source[Symbol.asyncIterator](),
112
+ };
113
+ }
114
+ /** Events only; each progress record goes to `onProgress` as it is read, so a forwarder spends no turn on it. */
115
+ export function assistantEvents(source, onProgress) {
116
+ const read = readers.get(source);
117
+ return {
118
+ [Symbol.asyncIterator]: () => read?.((entry) => {
119
+ if ("event" in entry)
120
+ return { value: entry.event, done: false };
121
+ onProgress(entry.progress);
122
+ return undefined;
123
+ }) ?? source[Symbol.asyncIterator](),
124
+ };
125
+ }
126
+ /** Internal readers address a block by `capture.contentIndex ?? event.contentIndex`; the public index can drift (Google/Vertex). */
127
+ export function getAssistantCapture(event) {
128
+ return captures.get(event);
129
+ }
130
+ export function isAssistantProgress(event) {
131
+ return (event.type === "message_block_state" ||
132
+ event.type === "message_block_metadata" ||
133
+ event.type === "message_tool_arguments_reset");
134
+ }
135
+ export function freezeCapture(value) {
136
+ const freeze = (part) => {
137
+ if (part === null || typeof part !== "object" || Object.isFrozen(part))
138
+ return;
139
+ for (const child of Object.values(part))
140
+ freeze(child);
141
+ Object.freeze(part);
142
+ };
143
+ const copy = structuredClone(value);
144
+ freeze(copy);
145
+ return copy;
146
+ }
147
+ /** Only declared public fields survive; provider scratch and symbols never enter a seed. */
148
+ export function publicBlock(block) {
149
+ const fields = {
150
+ text: ["type", "text", "audience", "textSignature"],
151
+ thinking: ["type", "thinking", "thinkingSignature", "redacted", "startedAt", "endedAt"],
152
+ toolCall: ["type", "id", "name", "arguments", "incomplete", "errorMessage", "thoughtSignature", "namespace"],
153
+ providerNative: ["type", "subtype", "raw"],
154
+ };
155
+ return Object.fromEntries(fields[block.type].filter((key) => key in block).map((key) => [key, Reflect.get(block, key)]));
156
+ }
157
+ /** Structural equality over plain data; any other object counts as changed, so a reused copy is never stale. */
158
+ function sameValue(left, right) {
159
+ if (left === right)
160
+ return true;
161
+ if (!isPlainData(left) || !isPlainData(right) || Array.isArray(left) !== Array.isArray(right))
162
+ return false;
163
+ const keys = Object.keys(left);
164
+ return (keys.length === Object.keys(right).length &&
165
+ keys.every((key) => Object.hasOwn(right, key) && sameValue(Reflect.get(left, key), Reflect.get(right, key))));
166
+ }
167
+ function isPlainData(value) {
168
+ if (typeof value !== "object" || value === null)
169
+ return false;
170
+ const prototype = Object.getPrototypeOf(value);
171
+ return prototype === Object.prototype || prototype === Array.prototype || prototype === null;
172
+ }
173
+ export function dropAssistantCapture(event) {
174
+ if (capturedAny)
175
+ captures.delete(event);
176
+ }
177
+ export function carryAssistantCapture(source, target, terminal) {
178
+ const capture = captures.get(source);
179
+ if (!capture)
180
+ return target;
181
+ try {
182
+ captures.set(target, terminal ? terminalCapture(capture, terminal) : capture);
183
+ }
184
+ catch (error) {
185
+ dropCapture(source, error);
186
+ }
187
+ return target;
188
+ }
189
+ /** A terminal replaced downstream: its usage and message, plus every scalar that differs from the source terminal. */
190
+ function terminalCapture(capture, terminal) {
191
+ const metadata = { ...capture.metadata };
192
+ const previous = capture.message ?? {};
193
+ for (const key of new Set([...Object.keys(previous), ...Object.keys(terminal)])) {
194
+ if (key === "content" || key === "usage")
195
+ continue;
196
+ const value = Reflect.get(terminal, key);
197
+ if (!sameValue(value, Reflect.get(previous, key)))
198
+ metadata[key] = key in terminal ? value : null;
199
+ }
200
+ return Object.freeze({
201
+ usage: frozenUsage(terminal.usage),
202
+ metadata: freezeCapture(metadata),
203
+ message: freezeCapture({ ...terminal, content: terminal.content.map(publicBlock) }),
204
+ ...(capture.rewrittenFrom === undefined ? {} : { rewrittenFrom: capture.rewrittenFrom }),
205
+ });
206
+ }
207
+ /** Never parses or traverses a block on a delta, including a no-growth delta. */
208
+ export function captureAssistantEvent(owner, event, hint = {}) {
209
+ const state = activeState(owner);
210
+ // A relay of a source that captured nothing, or captured a drifted index, gets no record: lack, never wrong.
211
+ if (!state || captures.has(event) || hint.carried === null || hint.carried?.contentIndex !== undefined)
212
+ return;
213
+ const contentIndex = "contentIndex" in event ? (hint.contentIndex ?? event.contentIndex) : undefined;
214
+ if (contentIndex !== undefined && state.failedBlocks.has(contentIndex))
215
+ return;
216
+ try {
217
+ const capture = buildCapture(state, event, hint);
218
+ if (capture)
219
+ captures.set(event, capture);
220
+ }
221
+ catch (error) {
222
+ dropCapture(owner, error, state, contentIndex);
223
+ }
224
+ }
225
+ function buildCapture(state, event, hint) {
226
+ const message = event.type === "done" ? event.message : event.type === "error" ? event.error : event.partial;
227
+ const usage = usageCopy(state, message.usage);
228
+ if (!("contentIndex" in event)) {
229
+ return Object.freeze({
230
+ usage,
231
+ metadata: changedMetadata(state, message),
232
+ message: freezeCapture({ ...message, content: message.content.map(publicBlock) }),
233
+ ...(hint.rewrittenFrom === undefined ? {} : { rewrittenFrom: hint.rewrittenFrom }),
234
+ });
235
+ }
236
+ const index = hint.contentIndex ?? event.contentIndex;
237
+ const own = index === event.contentIndex ? {} : { contentIndex: index };
238
+ const carried = hint.carried;
239
+ if ("delta" in event) {
240
+ if (carried) {
241
+ const { parsedThrough } = carried;
242
+ return Object.freeze({
243
+ usage,
244
+ offset: carried.offset,
245
+ ...(parsedThrough === undefined ? {} : { parsedThrough }),
246
+ });
247
+ }
248
+ const position = state.positions.get(index) ?? { offset: 0, parsedThrough: 0 };
249
+ const offset = hint.offset ?? position.offset;
250
+ position.offset = offset + event.delta.length;
251
+ if (event.type === "toolcall_delta" && event.delta.length > 0) {
252
+ position.parsedThrough = hint.parsedThrough ?? Math.max(position.parsedThrough, position.offset);
253
+ }
254
+ state.positions.set(index, position);
255
+ return Object.freeze({
256
+ usage,
257
+ offset,
258
+ ...own,
259
+ ...(event.type === "toolcall_delta" ? { parsedThrough: position.parsedThrough } : {}),
260
+ });
261
+ }
262
+ const metadata = changedMetadata(state, message);
263
+ if (carried) {
264
+ const { usage: _usage, metadata: _metadata, message: _message, offset: _offset, ...facts } = carried;
265
+ return Object.freeze({ ...facts, usage, metadata });
266
+ }
267
+ const starts = event.type === "text_start" || event.type === "thinking_start" || event.type === "toolcall_start";
268
+ const source = event.type === "toolcall_end" ? event.toolCall : message.content[index];
269
+ // A start announced before its block is appended has no seed to freeze yet.
270
+ if (starts && source === undefined)
271
+ return undefined;
272
+ const block = freezeCapture(publicBlock(source));
273
+ if (!starts)
274
+ return Object.freeze({ usage, metadata, block, ...own });
275
+ const rawArgumentPrefix = hint.rawArgumentPrefix ?? "";
276
+ const coveredCodeUnits = block.type === "text"
277
+ ? block.text.length
278
+ : block.type === "thinking"
279
+ ? block.thinking.length
280
+ : rawArgumentPrefix.length;
281
+ const parsedThrough = hint.parsedThrough ?? 0;
282
+ state.positions.set(index, { offset: hint.offset ?? coveredCodeUnits, parsedThrough });
283
+ return Object.freeze({
284
+ usage,
285
+ metadata,
286
+ block,
287
+ coveredCodeUnits,
288
+ ...own,
289
+ ...(block.type === "toolCall" ? { rawArgumentPrefix, parsedThrough } : {}),
290
+ });
291
+ }
292
+ function usageCopy(state, usage) {
293
+ if (state.usage !== undefined && sameValue(usage, state.usage))
294
+ return state.usage;
295
+ state.usage = frozenUsage(usage);
296
+ return state.usage;
297
+ }
298
+ function frozenUsage(usage) {
299
+ return Object.freeze({
300
+ ...usage,
301
+ cost: Object.freeze({ ...usage.cost }),
302
+ ...(usage.contextUsage ? { contextUsage: Object.freeze({ ...usage.contextUsage }) } : {}),
303
+ });
304
+ }
305
+ /** Scalar message fields changed since the last non-delta capture; the baseline moves only once its copy exists. */
306
+ function changedMetadata(state, message) {
307
+ const changed = {};
308
+ for (const key of Object.keys(message)) {
309
+ if (key === "content" || key === "usage")
310
+ continue;
311
+ const value = Reflect.get(message, key);
312
+ if (!state.metadata.has(key) || state.metadata.get(key) !== value)
313
+ changed[key] = value;
314
+ }
315
+ for (const key of state.metadata.keys())
316
+ if (!(key in message))
317
+ changed[key] = null;
318
+ const frozen = Object.keys(changed).length === 0 ? NO_CHANGE : freezeCapture(changed);
319
+ for (const key of Object.keys(changed)) {
320
+ if (key in message)
321
+ state.metadata.set(key, Reflect.get(message, key));
322
+ else
323
+ state.metadata.delete(key);
324
+ }
325
+ return frozen;
326
+ }
327
+ //# sourceMappingURL=assistant-capture.js.map
@@ -1,8 +1,14 @@
1
1
  import type { AssistantMessage, AssistantMessageEvent } from "../types.ts";
2
+ import { type AssistantProgress, type CaptureHint } from "./assistant-capture.ts";
3
+ type OrderedEntry<T> = {
4
+ event: T;
5
+ } | {
6
+ progress: AssistantProgress;
7
+ };
2
8
  export declare class EventStream<T, R = T> implements AsyncIterable<T> {
3
9
  #private;
4
10
  private waiting;
5
- private done;
11
+ protected done: boolean;
6
12
  private finalResultPromise;
7
13
  private resolveFinalResult;
8
14
  private rejectFinalResult;
@@ -11,9 +17,11 @@ export declare class EventStream<T, R = T> implements AsyncIterable<T> {
11
17
  constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R);
12
18
  get queue(): T[];
13
19
  push(event: T): void;
20
+ protected enqueueProgress(progress: AssistantProgress): void;
14
21
  end(result?: R): void;
15
22
  fail(error: unknown): void;
16
23
  [Symbol.asyncIterator](): AsyncIterator<T>;
24
+ protected mapIterator<V>(project: (entry: OrderedEntry<T>) => IteratorResult<V> | undefined): AsyncIterator<V>;
17
25
  result(): Promise<R>;
18
26
  /**
19
27
  * Track a locally executing unit of work (for example a server-requested
@@ -27,8 +35,13 @@ export declare class EventStream<T, R = T> implements AsyncIterable<T> {
27
35
  hasPendingLocalWork(): boolean;
28
36
  }
29
37
  export declare class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
30
- constructor();
38
+ constructor(options?: {
39
+ readonly captureBlocks?: boolean;
40
+ });
41
+ push(event: AssistantMessageEvent, hint?: CaptureHint): void;
42
+ [Symbol.asyncIterator](): AsyncIterator<AssistantMessageEvent>;
31
43
  }
32
44
  /** Factory function for AssistantMessageEventStream (for use in extensions) */
33
45
  export declare function createAssistantMessageEventStream(): AssistantMessageEventStream;
46
+ export {};
34
47
  //# sourceMappingURL=event-stream.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { captureAssistantEvent, dropAssistantCapture, enableAssistantCapture, setAssistantProgressWriter, setAssistantReader, } from "./assistant-capture.js";
1
2
  class FifoQueue {
2
3
  constructor() {
3
4
  this.incoming = [];
@@ -9,6 +10,9 @@ class FifoQueue {
9
10
  enqueue(value) {
10
11
  this.incoming.push(value);
11
12
  }
13
+ prepend(value) {
14
+ this.outgoing.push(value);
15
+ }
12
16
  dequeue() {
13
17
  if (this.outgoing.length === 0) {
14
18
  while (this.incoming.length > 0) {
@@ -41,7 +45,7 @@ export class EventStream {
41
45
  this.finalResultPromise.catch(() => { });
42
46
  }
43
47
  get queue() {
44
- return this.#queue.slice(this.#queueHead);
48
+ return this.#queue.slice(this.#queueHead).flatMap((entry) => ("event" in entry ? [entry.event] : []));
45
49
  }
46
50
  #enqueue(event) {
47
51
  this.#queue.push(event);
@@ -65,13 +69,19 @@ export class EventStream {
65
69
  this.done = true;
66
70
  this.resolveFinalResult(this.extractResult(event));
67
71
  }
68
- // Deliver to waiting consumer or queue it
72
+ this.#deliver({ event });
73
+ }
74
+ enqueueProgress(progress) {
75
+ if (!this.done)
76
+ this.#deliver({ progress });
77
+ }
78
+ #deliver(entry) {
69
79
  const waiter = this.waiting.dequeue();
70
80
  if (waiter) {
71
- waiter.resolve({ value: event, done: false });
81
+ waiter.resolve({ value: entry, done: false });
72
82
  }
73
83
  else {
74
- this.#enqueue(event);
84
+ this.#enqueue(entry);
75
85
  }
76
86
  }
77
87
  end(result) {
@@ -100,10 +110,15 @@ export class EventStream {
100
110
  }
101
111
  }
102
112
  [Symbol.asyncIterator]() {
113
+ return this.mapIterator((entry) => ("event" in entry ? { value: entry.event, done: false } : undefined));
114
+ }
115
+ mapIterator(project) {
103
116
  return {
104
117
  next: () => {
105
- if (this.#queueHead < this.#queue.length) {
106
- return Promise.resolve({ value: this.#dequeue(), done: false });
118
+ while (this.#queueHead < this.#queue.length) {
119
+ const result = project(this.#dequeue());
120
+ if (result)
121
+ return Promise.resolve(result);
107
122
  }
108
123
  if (this.#failed) {
109
124
  return Promise.reject(this.#error);
@@ -111,7 +126,19 @@ export class EventStream {
111
126
  if (this.done) {
112
127
  return Promise.resolve({ value: undefined, done: true });
113
128
  }
114
- return new Promise((resolve, reject) => this.waiting.enqueue({ resolve, reject }));
129
+ return new Promise((resolve, reject) => {
130
+ const waiter = {
131
+ reject,
132
+ resolve: (next) => {
133
+ const result = next.done ? { value: undefined, done: true } : project(next.value);
134
+ if (result)
135
+ resolve(result);
136
+ else
137
+ this.waiting.prepend(waiter);
138
+ },
139
+ };
140
+ this.waiting.enqueue(waiter);
141
+ });
115
142
  },
116
143
  };
117
144
  }
@@ -140,7 +167,7 @@ export class EventStream {
140
167
  }
141
168
  }
142
169
  export class AssistantMessageEventStream extends EventStream {
143
- constructor() {
170
+ constructor(options = {}) {
144
171
  super((event) => event.type === "done" || event.type === "error", (event) => {
145
172
  if (event.type === "done") {
146
173
  return event.message;
@@ -150,6 +177,28 @@ export class AssistantMessageEventStream extends EventStream {
150
177
  }
151
178
  throw new Error("Unexpected event type for final result");
152
179
  });
180
+ if (options.captureBlocks)
181
+ enableAssistantCapture(this);
182
+ setAssistantProgressWriter(this, (progress) => this.enqueueProgress(progress));
183
+ // A subclass that replaces iteration (cancellation, snapshots, remapping) stays unaware unless it registers a reader;
184
+ // one that remaps contentIndex in push must register its own reader too, or progress keeps the unmapped indices.
185
+ if (new.target.prototype[Symbol.asyncIterator] === AssistantMessageEventStream.prototype[Symbol.asyncIterator])
186
+ setAssistantReader(this, (project) => this.mapIterator(project));
187
+ }
188
+ push(event, hint) {
189
+ if (this.done)
190
+ return;
191
+ captureAssistantEvent(this, event, hint);
192
+ super.push(event);
193
+ }
194
+ [Symbol.asyncIterator]() {
195
+ return this.mapIterator((entry) => {
196
+ if (!("event" in entry))
197
+ return undefined;
198
+ // Unaware wrappers may snapshot, remap, buffer or discard this event. Never forward its carrier.
199
+ dropAssistantCapture(entry.event);
200
+ return { value: entry.event, done: false };
201
+ });
153
202
  }
154
203
  }
155
204
  /** Factory function for AssistantMessageEventStream (for use in extensions) */
@@ -0,0 +1,2 @@
1
+ export declare function coercePrimitiveByType(value: unknown, type: string): unknown;
2
+ //# sourceMappingURL=tool-argument-coercion.d.ts.map
@@ -0,0 +1,74 @@
1
+ export function coercePrimitiveByType(value, type) {
2
+ switch (type) {
3
+ case "number": {
4
+ if (value === null) {
5
+ return 0;
6
+ }
7
+ if (typeof value === "string" && value.trim() !== "") {
8
+ const parsed = Number(value);
9
+ if (Number.isFinite(parsed)) {
10
+ return parsed;
11
+ }
12
+ }
13
+ if (typeof value === "boolean") {
14
+ return value ? 1 : 0;
15
+ }
16
+ return value;
17
+ }
18
+ case "integer": {
19
+ if (value === null) {
20
+ return 0;
21
+ }
22
+ if (typeof value === "string" && value.trim() !== "") {
23
+ const parsed = Number(value);
24
+ if (Number.isInteger(parsed)) {
25
+ return parsed;
26
+ }
27
+ }
28
+ if (typeof value === "boolean") {
29
+ return value ? 1 : 0;
30
+ }
31
+ return value;
32
+ }
33
+ case "boolean": {
34
+ if (value === null) {
35
+ return false;
36
+ }
37
+ if (typeof value === "string") {
38
+ if (value === "true") {
39
+ return true;
40
+ }
41
+ if (value === "false") {
42
+ return false;
43
+ }
44
+ }
45
+ if (typeof value === "number") {
46
+ if (value === 1) {
47
+ return true;
48
+ }
49
+ if (value === 0) {
50
+ return false;
51
+ }
52
+ }
53
+ return value;
54
+ }
55
+ case "string": {
56
+ if (value === null) {
57
+ return "";
58
+ }
59
+ if (typeof value === "number" || typeof value === "boolean") {
60
+ return String(value);
61
+ }
62
+ return value;
63
+ }
64
+ case "null": {
65
+ if (value === "" || value === 0 || value === false) {
66
+ return null;
67
+ }
68
+ return value;
69
+ }
70
+ default:
71
+ return value;
72
+ }
73
+ }
74
+ //# sourceMappingURL=tool-argument-coercion.js.map
@@ -0,0 +1,20 @@
1
+ import type { TSchema } from "typebox";
2
+ export interface ToolArgumentValidationError {
3
+ readonly instancePath: string;
4
+ readonly keyword: string;
5
+ readonly message?: string;
6
+ readonly params: {
7
+ readonly [key: string]: unknown;
8
+ readonly missingProperty?: string;
9
+ readonly requiredProperties?: string[];
10
+ };
11
+ }
12
+ interface ToolArgumentValidator {
13
+ (value: unknown): boolean;
14
+ errors?: readonly ToolArgumentValidationError[] | null;
15
+ }
16
+ /** A protocol may own validation without permitting local argument coercion. */
17
+ export declare function registerToolArgumentValidator(schema: TSchema, validate: ToolArgumentValidator): void;
18
+ export declare function getToolArgumentValidator(schema: TSchema): ToolArgumentValidator | undefined;
19
+ export {};
20
+ //# sourceMappingURL=tool-argument-validator.d.ts.map
@@ -0,0 +1,9 @@
1
+ const validators = new WeakMap();
2
+ /** A protocol may own validation without permitting local argument coercion. */
3
+ export function registerToolArgumentValidator(schema, validate) {
4
+ validators.set(schema, validate);
5
+ }
6
+ export function getToolArgumentValidator(schema) {
7
+ return validators.get(schema);
8
+ }
9
+ //# sourceMappingURL=tool-argument-validator.js.map
@@ -4,6 +4,11 @@
4
4
  * subset that rejects a sibling `type` keyword on schemas that also declare
5
5
  * `anyOf` / `oneOf` / `allOf`. This helper removes that redundancy while
6
6
  * preserving the schema's semantics.
7
+ *
8
+ * This is provider wire adaptation, not an MCP schema conversion or argument
9
+ * validator. MCP supplies its full schema (boolean roots use the equivalent
10
+ * allOf container); the original schema and model arguments are never rewritten.
11
+ * Root-union conversion below is explicitly lossy for object-only providers.
7
12
  */
8
13
  export type ToolSchemaFlavor = "moonshot-mfjs";
9
14
  /**
@@ -4,6 +4,11 @@
4
4
  * subset that rejects a sibling `type` keyword on schemas that also declare
5
5
  * `anyOf` / `oneOf` / `allOf`. This helper removes that redundancy while
6
6
  * preserving the schema's semantics.
7
+ *
8
+ * This is provider wire adaptation, not an MCP schema conversion or argument
9
+ * validator. MCP supplies its full schema (boolean roots use the equivalent
10
+ * allOf container); the original schema and model arguments are never rewritten.
11
+ * Root-union conversion below is explicitly lossy for object-only providers.
7
12
  */
8
13
  const COMBINER_KEYS = ["anyOf", "oneOf", "allOf"];
9
14
  const SCHEMA_MAP_KEYS = ["properties", "patternProperties", "$defs", "definitions"];
@@ -1,4 +1,5 @@
1
1
  import type { Tool, ToolCall } from "../types.ts";
2
+ export { registerToolArgumentValidator } from "./tool-argument-validator.ts";
2
3
  /**
3
4
  * Finds a tool by name and validates the tool call arguments against its TypeBox schema
4
5
  * @param tools Array of tool definitions