mixdog 0.9.50 → 0.9.52

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 (134) hide show
  1. package/package.json +5 -3
  2. package/scripts/abort-recovery-test.mjs +17 -1
  3. package/scripts/agent-model-liveness-test.mjs +79 -2
  4. package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
  5. package/scripts/anthropic-transport-policy-test.mjs +466 -0
  6. package/scripts/atomic-lock-tryonce-test.mjs +60 -1
  7. package/scripts/build-tui.mjs +13 -1
  8. package/scripts/channel-daemon-smoke.mjs +630 -10
  9. package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
  10. package/scripts/code-graph-disk-hit-test.mjs +11 -0
  11. package/scripts/code-graph-root-federation-test.mjs +273 -0
  12. package/scripts/compact-pressure-test.mjs +55 -1
  13. package/scripts/compact-smoke.mjs +80 -0
  14. package/scripts/context-mcp-metering-test.mjs +1350 -19
  15. package/scripts/deferred-tool-loading-test.mjs +17 -0
  16. package/scripts/gemini-provider-test.mjs +1053 -0
  17. package/scripts/hook-bus-test.mjs +23 -0
  18. package/scripts/internal-tools-normalization-test.mjs +10 -0
  19. package/scripts/interrupted-turn-history-test.mjs +371 -0
  20. package/scripts/lifecycle-api-test.mjs +76 -0
  21. package/scripts/max-output-recovery-test.mjs +55 -0
  22. package/scripts/mcp-grace-deferred-test.mjs +89 -13
  23. package/scripts/memory-pg-recovery-test.mjs +59 -0
  24. package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
  25. package/scripts/process-lifecycle-test.mjs +389 -0
  26. package/scripts/provider-admission-scheduler-test.mjs +582 -0
  27. package/scripts/provider-contract-test.mjs +268 -0
  28. package/scripts/provider-toolcall-test.mjs +520 -3
  29. package/scripts/reactive-compact-persist-smoke.mjs +59 -0
  30. package/scripts/resource-admission-test.mjs +789 -0
  31. package/scripts/session-bench-cache-break-test.mjs +102 -0
  32. package/scripts/session-bench.mjs +101 -42
  33. package/scripts/shell-failure-diagnostics-test.mjs +73 -4
  34. package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
  35. package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
  36. package/scripts/smoke-loop-failure-summary.mjs +16 -0
  37. package/scripts/smoke-loop.mjs +2 -7
  38. package/scripts/steering-drain-buckets-test.mjs +18 -0
  39. package/scripts/tool-failures.mjs +15 -1
  40. package/scripts/toolcall-args-test.mjs +14 -6
  41. package/scripts/tui-transcript-perf-test.mjs +43 -7
  42. package/scripts/web-fetch-routing-test.mjs +158 -0
  43. package/src/cli.mjs +15 -2
  44. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
  45. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
  46. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
  47. package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
  48. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
  49. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
  50. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
  51. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
  52. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
  53. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
  54. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
  55. package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
  56. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
  57. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
  60. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
  61. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
  62. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
  63. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
  64. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
  66. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
  67. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
  68. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
  69. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
  71. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
  72. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
  73. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
  74. package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
  75. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
  76. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
  77. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
  78. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
  79. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
  80. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
  81. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
  83. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
  84. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
  85. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
  86. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
  87. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
  88. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
  89. package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
  90. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
  91. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
  92. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
  93. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
  94. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
  95. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
  96. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
  97. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
  98. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
  99. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
  100. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
  101. package/src/runtime/memory/index.mjs +22 -4
  102. package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
  103. package/src/runtime/memory/lib/pg/process.mjs +91 -47
  104. package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
  105. package/src/runtime/search/index.mjs +41 -0
  106. package/src/runtime/search/lib/http-fetch.mjs +154 -0
  107. package/src/runtime/search/tool-defs.mjs +23 -0
  108. package/src/runtime/shared/atomic-file.mjs +28 -150
  109. package/src/runtime/shared/process-lifecycle.mjs +363 -0
  110. package/src/runtime/shared/process-shutdown.mjs +27 -4
  111. package/src/runtime/shared/resource-admission.mjs +359 -0
  112. package/src/runtime/shared/staged-child-result.mjs +19 -0
  113. package/src/session-runtime/context-status.mjs +38 -27
  114. package/src/session-runtime/lifecycle-api.mjs +26 -6
  115. package/src/session-runtime/provider-request-tools.mjs +72 -0
  116. package/src/session-runtime/runtime-core.mjs +43 -18
  117. package/src/session-runtime/session-turn-api.mjs +5 -4
  118. package/src/session-runtime/tool-catalog.mjs +375 -15
  119. package/src/standalone/agent-tool.mjs +17 -38
  120. package/src/standalone/channel-daemon-client.mjs +200 -38
  121. package/src/standalone/channel-daemon-transport.mjs +136 -9
  122. package/src/standalone/channel-worker.mjs +79 -12
  123. package/src/standalone/hook-bus/handlers.mjs +4 -0
  124. package/src/standalone/hook-bus/rules.mjs +7 -1
  125. package/src/standalone/hook-bus.mjs +10 -2
  126. package/src/tui/App.jsx +17 -11
  127. package/src/tui/app/live-spinner-visibility.mjs +20 -0
  128. package/src/tui/dist/index.mjs +101 -281
  129. package/src/tui/engine/session-api-ext.mjs +13 -2
  130. package/src/tui/engine/session-api.mjs +1 -1
  131. package/src/tui/engine/turn.mjs +10 -6
  132. package/src/tui/engine.mjs +13 -4
  133. package/src/tui/index.jsx +4 -1
  134. package/src/tui/lib/voice-setup.mjs +16 -0
@@ -12,6 +12,91 @@ import { SchemaType } from '@google/generative-ai';
12
12
  import { traceHash, stableTraceStringify } from './trace-utils.mjs';
13
13
  import { normalizeContentForGeminiParts, splitToolContentForGemini } from './media-normalization.mjs';
14
14
 
15
+ function explicitGeminiMediaPart(part) {
16
+ if (!part || typeof part !== 'object') return null;
17
+ const inline = part.inlineData || part.inline_data;
18
+ if (typeof inline?.data === 'string' && inline.data) {
19
+ const mimeType = inline.mimeType || inline.mime_type || inline.mediaType || inline.media_type;
20
+ if (typeof mimeType === 'string' && mimeType) {
21
+ return { inlineData: { mimeType, data: inline.data } };
22
+ }
23
+ }
24
+ const file = part.fileData || part.file_data;
25
+ const fileUri = file?.fileUri || file?.file_uri;
26
+ const mimeType = file?.mimeType || file?.mime_type || file?.mediaType || file?.media_type;
27
+ if (typeof fileUri === 'string' && fileUri && typeof mimeType === 'string' && mimeType) {
28
+ return { fileData: { mimeType, fileUri } };
29
+ }
30
+ return null;
31
+ }
32
+
33
+ // The shared normalizer intentionally defaults unknown media to image/png for
34
+ // image-oriented providers. Gemini accepts audio/video/document MIME types too,
35
+ // so preserve explicit Gemini wire parts before using the shared fallback.
36
+ export function normalizeGeminiParts(content) {
37
+ if (typeof content === 'string') return normalizeContentForGeminiParts(content);
38
+ const parts = Array.isArray(content)
39
+ ? content
40
+ : (content && typeof content === 'object' && Array.isArray(content.content)
41
+ ? content.content
42
+ : [content]);
43
+ const out = [];
44
+ for (const part of parts) {
45
+ const media = explicitGeminiMediaPart(part);
46
+ if (media) out.push(media);
47
+ else if (typeof part === 'string') out.push({ text: part });
48
+ else if (part && typeof part === 'object' && typeof part.text === 'string') out.push({ text: part.text });
49
+ else out.push(...normalizeContentForGeminiParts(part));
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function splitGeminiToolContent(content) {
55
+ const normalized = normalizeGeminiParts(content);
56
+ const explicitMedia = normalized.filter((part) => part?.inlineData || part?.fileData);
57
+ if (!explicitMedia.length) return splitToolContentForGemini(content);
58
+ const text = normalized
59
+ .filter((part) => typeof part?.text === 'string')
60
+ .map((part) => part.text)
61
+ .filter(Boolean)
62
+ .join('\n');
63
+ return {
64
+ response: { result: text || '[tool result included media content]' },
65
+ mediaParts: explicitMedia,
66
+ };
67
+ }
68
+
69
+ const GEMINI_FUNCTION_RESPONSE_MIME = /^(?:image\/(?:png|jpeg|webp)|application\/pdf|text\/plain)$/i;
70
+
71
+ function toGeminiFunctionResponseMedia(mediaParts) {
72
+ const parts = [];
73
+ const refs = [];
74
+ const external = [];
75
+ const omitted = [];
76
+ for (const media of mediaParts || []) {
77
+ const payload = media?.inlineData || media?.fileData;
78
+ const mimeType = String(payload?.mimeType || '').trim();
79
+ if (!GEMINI_FUNCTION_RESPONSE_MIME.test(mimeType)) {
80
+ if (mimeType) omitted.push(mimeType);
81
+ continue;
82
+ }
83
+ const displayName = `tool_media_${parts.length + 1}`;
84
+ if (media.inlineData) {
85
+ parts.push({ inlineData: { ...media.inlineData, mimeType, displayName } });
86
+ refs.push({ $ref: displayName });
87
+ } else {
88
+ // FunctionResponse.parts currently documents inlineData only.
89
+ // fileData is valid in ordinary Content parts but can 400 when
90
+ // nested here, so retain file-backed media as plain JSON metadata.
91
+ external.push({
92
+ mimeType,
93
+ fileUri: String(media.fileData?.fileUri || ''),
94
+ });
95
+ }
96
+ }
97
+ return { parts, refs, external, omitted };
98
+ }
99
+
15
100
  /**
16
101
  * Convert JSON Schema type string to Gemini SchemaType.
17
102
  * Gemini SDK uses its own enum instead of plain strings.
@@ -31,34 +116,282 @@ function toSchemaType(t) {
31
116
  /**
32
117
  * Recursively convert a JSON Schema object to Gemini's FunctionDeclarationSchema.
33
118
  * Gemini requires `type` to be a SchemaType enum, not a plain string, and
34
- * rejects several JSON Schema fields the API does not understand
35
- * (additionalProperties, $schema, $ref, const, examples, definitions,
36
- * patternProperties). We strip those at every level.
119
+ * accepts only a documented subset of JSON Schema. Project onto that subset
120
+ * at every level and convert `const` into Gemini's string enum form.
37
121
  */
38
- const GEMINI_SCHEMA_STRIP = new Set([
39
- 'additionalProperties',
40
- '$schema',
41
- '$ref',
122
+ const GEMINI_SCHEMA_FIELDS = new Set([
123
+ 'type',
124
+ 'format',
125
+ 'title',
126
+ 'description',
127
+ 'nullable',
128
+ 'enum',
129
+ 'items',
130
+ 'properties',
131
+ 'required',
132
+ 'example',
133
+ 'default',
134
+ 'anyOf',
135
+ 'oneOf',
136
+ 'allOf',
137
+ 'not',
138
+ 'minimum',
139
+ 'maximum',
140
+ 'exclusiveMinimum',
141
+ 'exclusiveMaximum',
142
+ 'minItems',
143
+ 'maxItems',
144
+ 'minProperties',
145
+ 'maxProperties',
146
+ 'minLength',
147
+ 'maxLength',
148
+ 'pattern',
149
+ 'propertyOrdering',
42
150
  'const',
43
- 'examples',
44
- 'definitions',
45
- 'patternProperties',
46
151
  ]);
152
+
153
+ const SCHEMA_CONFLICT_ENUM = '__mixdog_unrepresentable_schema_conjunction__';
154
+ const schemaValueKey = (value) => stableTraceStringify(value);
155
+ const sameSchemaValue = (a, b) => schemaValueKey(a) === schemaValueKey(b);
156
+ const defineSchemaProperty = (target, name, value) => {
157
+ Object.defineProperty(target, name, {
158
+ value,
159
+ enumerable: true,
160
+ writable: true,
161
+ configurable: true,
162
+ });
163
+ };
164
+
165
+ function schemaFallback(reason) {
166
+ return {
167
+ type: SchemaType.STRING,
168
+ enum: [SCHEMA_CONFLICT_ENUM],
169
+ description: `Schema conjunction could not be represented safely: ${String(reason || 'conflict').slice(0, 300)}`,
170
+ };
171
+ }
172
+
173
+ function typeInfo(schema) {
174
+ const declared = Array.isArray(schema?.type)
175
+ ? schema.type
176
+ : (typeof schema?.type === 'string' ? [schema.type] : []);
177
+ const nonNull = [...new Set(declared.filter((type) => type !== 'null'))];
178
+ let type = nonNull[0] || null;
179
+ if (nonNull.length > 1) {
180
+ if (nonNull.length === 2 && nonNull.includes('integer') && nonNull.includes('number')) type = 'integer';
181
+ else return { conflict: `unsupported type union ${nonNull.join('|')}` };
182
+ }
183
+ return {
184
+ type,
185
+ allowsNull: declared.length === 0 || declared.includes('null') || schema?.nullable === true,
186
+ };
187
+ }
188
+
189
+ function intersectTypeInfo(a, b) {
190
+ const left = typeInfo(a);
191
+ const right = typeInfo(b);
192
+ if (left.conflict || right.conflict) return { conflict: left.conflict || right.conflict };
193
+ let type = left.type || right.type;
194
+ if (left.type && right.type && left.type !== right.type) {
195
+ if ([left.type, right.type].includes('integer') && [left.type, right.type].includes('number')) type = 'integer';
196
+ else return { conflict: `incompatible types ${left.type} and ${right.type}` };
197
+ }
198
+ return { type, allowsNull: left.allowsNull && right.allowsNull };
199
+ }
200
+
201
+ function enumValues(schema) {
202
+ const values = Array.isArray(schema?.enum) ? schema.enum : null;
203
+ if (schema?.const === undefined) return { values };
204
+ if (values && !values.some((value) => sameSchemaValue(value, schema.const))) {
205
+ return { values: null, conflict: 'empty enum intersection' };
206
+ }
207
+ return { values: [schema.const] };
208
+ }
209
+
210
+ function strongerLower(a, b) {
211
+ if (!a) return b;
212
+ if (!b) return a;
213
+ if (b.value > a.value) return b;
214
+ if (b.value < a.value) return a;
215
+ return { value: a.value, exclusive: a.exclusive || b.exclusive };
216
+ }
217
+
218
+ function strongerUpper(a, b) {
219
+ if (!a) return b;
220
+ if (!b) return a;
221
+ if (b.value < a.value) return b;
222
+ if (b.value > a.value) return a;
223
+ return { value: a.value, exclusive: a.exclusive || b.exclusive };
224
+ }
225
+
226
+ function rawBound(schema, side) {
227
+ const inclusive = side === 'lower' ? schema?.minimum : schema?.maximum;
228
+ const exclusive = side === 'lower' ? schema?.exclusiveMinimum : schema?.exclusiveMaximum;
229
+ let out = Number.isFinite(Number(inclusive)) ? { value: Number(inclusive), exclusive: false } : null;
230
+ if (Number.isFinite(Number(exclusive))) {
231
+ const next = { value: Number(exclusive), exclusive: true };
232
+ out = side === 'lower' ? strongerLower(out, next) : strongerUpper(out, next);
233
+ }
234
+ return out;
235
+ }
236
+
237
+ function mergeSchemaConjunction(leftInput, rightInput) {
238
+ const left = flattenAllOf(leftInput);
239
+ const right = flattenAllOf(rightInput);
240
+ if (left.conflict || right.conflict) return { conflict: left.conflict || right.conflict };
241
+ for (const key of ['anyOf', 'oneOf', 'not']) {
242
+ if (right.schema[key] !== undefined) return { conflict: `${key} inside allOf` };
243
+ }
244
+ const a = left.schema;
245
+ const b = right.schema;
246
+ for (const source of [a, b]) {
247
+ for (const key of Object.keys(source)) {
248
+ if (GEMINI_SCHEMA_FIELDS.has(key)) continue;
249
+ return { conflict: `unsupported allOf keyword ${key}` };
250
+ }
251
+ }
252
+ const merged = { ...a };
253
+ const types = intersectTypeInfo(a, b);
254
+ if (types.conflict) return types;
255
+ delete merged.type;
256
+ delete merged.nullable;
257
+ if (types.type) merged.type = types.type;
258
+ if (types.allowsNull && types.type) merged.nullable = true;
259
+
260
+ const aEnum = enumValues(a);
261
+ const bEnum = enumValues(b);
262
+ if (aEnum.conflict || bEnum.conflict) {
263
+ return { conflict: aEnum.conflict || bEnum.conflict };
264
+ }
265
+ const ae = aEnum.values;
266
+ const be = bEnum.values;
267
+ if (ae || be) {
268
+ const values = ae && be
269
+ ? ae.filter((value) => be.some((candidate) => sameSchemaValue(value, candidate)))
270
+ : [...(ae || be)];
271
+ if (!values.length) return { conflict: 'empty enum intersection' };
272
+ merged.enum = values;
273
+ delete merged.const;
274
+ }
275
+
276
+ const lower = strongerLower(rawBound(a, 'lower'), rawBound(b, 'lower'));
277
+ const upper = strongerUpper(rawBound(a, 'upper'), rawBound(b, 'upper'));
278
+ for (const key of ['minimum', 'exclusiveMinimum', 'maximum', 'exclusiveMaximum']) delete merged[key];
279
+ if (lower) merged[lower.exclusive ? 'exclusiveMinimum' : 'minimum'] = lower.value;
280
+ if (upper) merged[upper.exclusive ? 'exclusiveMaximum' : 'maximum'] = upper.value;
281
+ if (lower && upper && (lower.value > upper.value
282
+ || (lower.value === upper.value && (lower.exclusive || upper.exclusive)))) {
283
+ return { conflict: 'empty numeric range' };
284
+ }
285
+
286
+ for (const [minKey, maxKey] of [
287
+ ['minLength', 'maxLength'],
288
+ ['minItems', 'maxItems'],
289
+ ['minProperties', 'maxProperties'],
290
+ ]) {
291
+ const min = Math.max(...[a[minKey], b[minKey]].filter(Number.isFinite), 0);
292
+ const maxima = [a[maxKey], b[maxKey]].filter(Number.isFinite);
293
+ const max = maxima.length ? Math.min(...maxima) : null;
294
+ if (max != null && min > max) return { conflict: `${minKey}/${maxKey}` };
295
+ if (min > 0 || a[minKey] === 0 || b[minKey] === 0) merged[minKey] = min;
296
+ if (max != null) merged[maxKey] = max;
297
+ }
298
+
299
+ for (const key of ['pattern', 'format']) {
300
+ if (a[key] !== undefined && b[key] !== undefined && a[key] !== b[key]) {
301
+ return { conflict: `incompatible ${key}` };
302
+ }
303
+ if (b[key] !== undefined) merged[key] = b[key];
304
+ }
305
+ if (a.description && b.description && a.description !== b.description) {
306
+ merged.description = `${a.description}\n${b.description}`;
307
+ } else if (b.description !== undefined) merged.description = b.description;
308
+ if (a.default !== undefined && b.default !== undefined && !sameSchemaValue(a.default, b.default)) {
309
+ delete merged.default;
310
+ } else if (b.default !== undefined) merged.default = b.default;
311
+ for (const key of ['title', 'example', 'propertyOrdering']) {
312
+ if (merged[key] === undefined && b[key] !== undefined) merged[key] = b[key];
313
+ }
314
+
315
+ const properties = {};
316
+ for (const [name, schema] of Object.entries(a.properties || {})) {
317
+ defineSchemaProperty(properties, name, schema);
318
+ }
319
+ for (const [name, schema] of Object.entries(b.properties || {})) {
320
+ if (!Object.hasOwn(properties, name)) defineSchemaProperty(properties, name, schema);
321
+ else {
322
+ const child = mergeSchemaConjunction(properties[name], schema);
323
+ if (child.conflict) return { conflict: `property ${name}: ${child.conflict}` };
324
+ defineSchemaProperty(properties, name, child.schema);
325
+ }
326
+ }
327
+ if (Object.keys(properties).length) merged.properties = properties;
328
+ const required = [...new Set([
329
+ ...(Array.isArray(a.required) ? a.required : []),
330
+ ...(Array.isArray(b.required) ? b.required : []),
331
+ ].filter((name) => typeof name === 'string'))];
332
+ if (required.length) merged.required = required;
333
+
334
+ if (a.items && b.items) {
335
+ const items = mergeSchemaConjunction(a.items, b.items);
336
+ if (items.conflict) return { conflict: `items: ${items.conflict}` };
337
+ merged.items = items.schema;
338
+ } else if (b.items) merged.items = b.items;
339
+ return { schema: merged };
340
+ }
341
+
342
+ function flattenAllOf(input) {
343
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return { schema: input };
344
+ let schema = { ...input };
345
+ const branches = Array.isArray(schema.allOf) ? schema.allOf : [];
346
+ const hadAllOf = branches.length > 0;
347
+ delete schema.allOf;
348
+ for (const branch of branches) {
349
+ const merged = mergeSchemaConjunction(schema, branch);
350
+ if (merged.conflict) return merged;
351
+ schema = merged.schema;
352
+ }
353
+ return { schema, hadAllOf };
354
+ }
355
+
47
356
  export function convertSchema(schema) {
48
357
  if (!schema || typeof schema !== 'object') return schema;
358
+ const flattened = flattenAllOf(schema);
359
+ if (flattened.conflict) return schemaFallback(flattened.conflict);
360
+ schema = flattened.schema;
49
361
  const result = {};
50
362
  for (const [k, v] of Object.entries(schema)) {
51
- if (GEMINI_SCHEMA_STRIP.has(k)) continue;
363
+ // Project onto Gemini's documented Schema surface instead of trying
364
+ // to blacklist the much larger JSON Schema vocabulary. Unknown
365
+ // extension keywords are rejected by generateContent.
366
+ if (!GEMINI_SCHEMA_FIELDS.has(k)) continue;
52
367
  result[k] = v;
53
368
  }
54
- // Gemini's Schema validator requires every `enum` entry to be a string,
55
- // even when the parent `type` is integer/number/boolean. Drop the enum in
56
- // that case rather than emit an invalid typed enum — `type` plus the
57
- // description still guides the model, and the tool handler revalidates.
58
- const rawType = typeof result.type === 'string' ? result.type : undefined;
369
+ // Gemini's Schema validator requires every `enum` entry to be a string.
370
+ // An unrepresentable typed enum must become an explicit conflict schema;
371
+ // dropping it would broaden the accepted tool arguments.
372
+ // Gemini's schema dialect represents JSON Schema null unions with
373
+ // `nullable`, not a type array.
374
+ if (result.type === undefined && (result.properties || result.required)) result.type = 'object';
375
+ if (result.type === undefined && result.items) result.type = 'array';
376
+ const normalizedType = typeInfo(result);
377
+ if (normalizedType.conflict) return schemaFallback(normalizedType.conflict);
378
+ const rawType = normalizedType.type || undefined;
379
+ if (rawType) result.type = rawType;
380
+ else delete result.type;
381
+ if (normalizedType.allowsNull && rawType) result.nullable = true;
382
+ else if (result.nullable !== true) delete result.nullable;
383
+ const localEnum = enumValues(result);
384
+ if (localEnum.conflict) return schemaFallback(localEnum.conflict);
385
+ if (result.const !== undefined) {
386
+ if (rawType === 'integer' || rawType === 'number' || rawType === 'boolean') {
387
+ return schemaFallback(`Gemini does not support ${rawType} const`);
388
+ }
389
+ result.enum = [String(result.const)];
390
+ delete result.const;
391
+ }
59
392
  if (Array.isArray(result.enum) && (rawType === 'integer' || rawType === 'number' || rawType === 'boolean')) {
60
393
  if (result.enum.some((item) => typeof item !== 'string')) {
61
- delete result.enum;
394
+ return schemaFallback(`Gemini does not support ${rawType} enum conjunction`);
62
395
  }
63
396
  }
64
397
  // Gemini rejects array schemas that omit `items`; fill a permissive
@@ -69,20 +402,46 @@ export function convertSchema(schema) {
69
402
  if (typeof result.type === 'string') {
70
403
  result.type = toSchemaType(result.type);
71
404
  }
72
- if (result.properties && typeof result.properties === 'object') {
405
+ const lower = rawBound(result, 'lower');
406
+ const upper = rawBound(result, 'upper');
407
+ delete result.exclusiveMinimum;
408
+ delete result.exclusiveMaximum;
409
+ if ((lower?.exclusive || upper?.exclusive) && rawType !== 'integer') {
410
+ return schemaFallback('exclusive numeric bound is not supported by Gemini Schema');
411
+ }
412
+ if (rawType === 'integer') {
413
+ if (lower?.exclusive) result.minimum = Math.floor(lower.value) + 1;
414
+ if (upper?.exclusive) result.maximum = Math.ceil(upper.value) - 1;
415
+ }
416
+ if (Number.isFinite(result.minimum) && Number.isFinite(result.maximum)
417
+ && result.minimum > result.maximum) return schemaFallback('empty numeric range');
418
+ if ((result.properties && typeof result.properties === 'object') || Array.isArray(result.required)) {
73
419
  const props = {};
74
- for (const [key, val] of Object.entries(result.properties)) {
75
- props[key] = convertSchema(val);
420
+ for (const [key, val] of Object.entries(result.properties || {})) {
421
+ defineSchemaProperty(props, key, convertSchema(val));
422
+ }
423
+ if (Array.isArray(result.required)) {
424
+ result.required = [...new Set(result.required.filter((key) => typeof key === 'string'))];
425
+ for (const key of result.required) {
426
+ if (!Object.hasOwn(props, key)) {
427
+ defineSchemaProperty(
428
+ props,
429
+ key,
430
+ schemaFallback(`required property ${key} has no representable schema`),
431
+ );
432
+ }
433
+ }
434
+ if (!result.required.length) delete result.required;
76
435
  }
77
436
  result.properties = props;
78
437
  }
79
438
  if (result.items && typeof result.items === 'object') {
80
439
  result.items = convertSchema(result.items);
81
440
  }
82
- // Recurse into JSON Schema combinator keys so disallowed fields
83
- // (additionalProperties, $schema, etc.) get stripped at every nesting
84
- // level. Without this, schemas using anyOf/oneOf/allOf/not pass the
85
- // shallow strip but fail Gemini validation at depth.
441
+ // Gemini function declarations support anyOf, but not oneOf/allOf/not.
442
+ // oneOf is safely relaxed to anyOf. Object allOf branches are projected
443
+ // into the local properties/required set where representable; other allOf
444
+ // and all not constraints are conservatively dropped.
86
445
  //
87
446
  // Two Gemini-specific normalizations are also applied per combinator
88
447
  // subschema:
@@ -94,9 +453,13 @@ export function convertSchema(schema) {
94
453
  // validates that every name in `required` exists in *this*
95
454
  // subschema's `properties` (it does not inherit from the parent
96
455
  // the way JSON Schema's compositional model does).
97
- for (const combinator of ['anyOf', 'oneOf', 'allOf']) {
98
- if (Array.isArray(result[combinator])) {
99
- result[combinator] = result[combinator].map((s) => {
456
+ const unionBranches = [
457
+ ...(Array.isArray(result.anyOf) ? result.anyOf : []),
458
+ ...(Array.isArray(result.oneOf) ? result.oneOf : []),
459
+ ];
460
+ delete result.oneOf;
461
+ if (unionBranches.length) {
462
+ result.anyOf = unionBranches.map((s) => {
100
463
  const sub = convertSchema(s);
101
464
  if (sub && typeof sub === 'object') {
102
465
  const usesObjectKeys = sub.required !== undefined || sub.properties !== undefined;
@@ -106,18 +469,48 @@ export function convertSchema(schema) {
106
469
  if (Array.isArray(sub.required) && !sub.properties && result.properties) {
107
470
  const projected = {};
108
471
  for (const k of sub.required) {
109
- if (result.properties[k]) projected[k] = result.properties[k];
472
+ if (Object.hasOwn(result.properties, k)) {
473
+ defineSchemaProperty(projected, k, result.properties[k]);
474
+ }
110
475
  }
111
476
  if (Object.keys(projected).length > 0) sub.properties = projected;
112
477
  }
113
478
  }
114
479
  return sub;
115
- });
116
- }
480
+ }).filter(Boolean);
481
+ if (!result.anyOf.length) delete result.anyOf;
117
482
  }
118
- if (result.not && typeof result.not === 'object') {
119
- result.not = convertSchema(result.not);
483
+ delete result.allOf;
484
+ delete result.not;
485
+ const typeSpecific = {
486
+ string: new Set(['minLength', 'maxLength', 'pattern', 'format']),
487
+ array: new Set(['minItems', 'maxItems', 'items']),
488
+ object: new Set(['minProperties', 'maxProperties', 'properties', 'required', 'propertyOrdering']),
489
+ number: new Set(['minimum', 'maximum']),
490
+ integer: new Set(['minimum', 'maximum']),
491
+ };
492
+ for (const key of [
493
+ 'minLength', 'maxLength', 'pattern', 'format',
494
+ 'minItems', 'maxItems', 'items',
495
+ 'minProperties', 'maxProperties', 'properties', 'required', 'propertyOrdering',
496
+ 'minimum', 'maximum',
497
+ ]) {
498
+ if (!typeSpecific[rawType]?.has(key)) delete result[key];
120
499
  }
500
+ for (const [minKey, maxKey] of [
501
+ ['minLength', 'maxLength'],
502
+ ['minItems', 'maxItems'],
503
+ ['minProperties', 'maxProperties'],
504
+ ]) {
505
+ if (result[minKey] !== undefined
506
+ && (!Number.isInteger(result[minKey]) || result[minKey] < 0)) delete result[minKey];
507
+ if (result[maxKey] !== undefined
508
+ && (!Number.isInteger(result[maxKey]) || result[maxKey] < 0)) delete result[maxKey];
509
+ if (result[minKey] !== undefined && result[maxKey] !== undefined
510
+ && result[minKey] > result[maxKey]) return schemaFallback(`${minKey}/${maxKey}`);
511
+ }
512
+ if (result.minimum !== undefined && !Number.isFinite(result.minimum)) delete result.minimum;
513
+ if (result.maximum !== undefined && !Number.isFinite(result.maximum)) delete result.maximum;
121
514
  return result;
122
515
  }
123
516
 
@@ -192,16 +585,27 @@ export function toGeminiToolConfig(toolChoice) {
192
585
  function toGeminiContent(message, toolNameByCallId) {
193
586
  if (!message || message.role === 'system') return null;
194
587
  if (message.role === 'assistant' && message.toolCalls?.length) {
195
- const parts = [];
196
- if (message.content) parts.push(...normalizeContentForGeminiParts(message.content));
588
+ const parts = geminiThoughtPartsFromMetadata(message);
589
+ if (message.content) {
590
+ parts.push(...(geminiTextPartsFromMetadata(message) || normalizeGeminiParts(message.content)));
591
+ }
197
592
  for (const tc of message.toolCalls) {
198
593
  // Gemini 3 thinking models require the original thoughtSignature
199
594
  // echoed back on every prior functionCall so the cached thinking
200
595
  // prefix stays valid. v1beta places the field at the Part level
201
596
  // (sibling of functionCall) — putting it inside functionCall returns
202
597
  // 400 "Unknown name". Older models / first turn have no signature.
203
- const part = { functionCall: { name: tc.name, args: tc.arguments } };
204
- if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;
598
+ const part = {
599
+ functionCall: {
600
+ name: tc.name,
601
+ args: tc.arguments,
602
+ ...(typeof tc.id === 'string' && tc.id ? { id: tc.id } : {}),
603
+ },
604
+ };
605
+ if (typeof tc.thoughtSignature === 'string'
606
+ && tc.thoughtSignature && tc.thoughtSignature.length <= 16_384) {
607
+ part.thoughtSignature = tc.thoughtSignature;
608
+ }
205
609
  parts.push(part);
206
610
  }
207
611
  return { role: 'model', parts };
@@ -219,9 +623,19 @@ function toGeminiContent(message, toolNameByCallId) {
219
623
  const functionName = (toolNameByCallId && toolNameByCallId.get(message.toolCallId))
220
624
  || message.toolCallId
221
625
  || '';
222
- const { response, mediaParts } = splitToolContentForGemini(message.content);
223
- const parts = [{ functionResponse: { name: functionName, response } }];
224
- if (mediaParts.length) parts.push(...mediaParts);
626
+ const { response, mediaParts } = splitGeminiToolContent(message.content);
627
+ const media = toGeminiFunctionResponseMedia(mediaParts);
628
+ if (media.refs.length) response.media = media.refs;
629
+ if (media.external.length) response.externalMedia = media.external;
630
+ if (media.omitted.length) {
631
+ response.omittedMediaTypes = media.omitted;
632
+ }
633
+ const functionResponse = { name: functionName, response };
634
+ if (typeof message.toolCallId === 'string' && message.toolCallId) {
635
+ functionResponse.id = message.toolCallId;
636
+ }
637
+ if (media.parts.length) functionResponse.parts = media.parts;
638
+ const parts = [{ functionResponse }];
225
639
  return {
226
640
  role: 'user',
227
641
  parts,
@@ -229,10 +643,52 @@ function toGeminiContent(message, toolNameByCallId) {
229
643
  }
230
644
  return {
231
645
  role: message.role === 'assistant' ? 'model' : 'user',
232
- parts: normalizeContentForGeminiParts(message.content),
646
+ parts: message.role === 'assistant'
647
+ ? (geminiTextPartsFromMetadata(message) || normalizeGeminiParts(message.content))
648
+ : normalizeGeminiParts(message.content),
233
649
  };
234
650
  }
235
651
 
652
+ function geminiTextPartsFromMetadata(message) {
653
+ const parts = message?.providerMetadata?.gemini?.textParts;
654
+ if (!Array.isArray(parts) || !parts.length || parts.length > 128) return null;
655
+ const normalized = [];
656
+ for (const part of parts) {
657
+ if (!part || typeof part !== 'object'
658
+ || typeof part.text !== 'string' || part.text.length > 1_000_000) return null;
659
+ const next = { text: part.text };
660
+ if (part.thoughtSignature !== undefined) {
661
+ if (typeof part.thoughtSignature !== 'string'
662
+ || !part.thoughtSignature || part.thoughtSignature.length > 16_384) return null;
663
+ next.thoughtSignature = part.thoughtSignature;
664
+ }
665
+ normalized.push(next);
666
+ }
667
+ const expected = typeof message.content === 'string' ? message.content : '';
668
+ return normalized.map((part) => part.text).join('') === expected ? normalized : null;
669
+ }
670
+
671
+ function geminiThoughtPartsFromMetadata(message) {
672
+ const parts = message?.providerMetadata?.gemini?.thoughtParts;
673
+ if (!Array.isArray(parts) || !parts.length || parts.length > 128) return [];
674
+ const normalized = [];
675
+ for (const part of parts) {
676
+ // Hidden prompt content is replayable only when every persisted part is
677
+ // structurally valid and signed by Gemini. Reject the whole collection
678
+ // on malformed/unsigned data rather than partially accepting metadata.
679
+ if (!part || typeof part !== 'object'
680
+ || typeof part.text !== 'string' || part.text.length > 1_000_000
681
+ || typeof part.thoughtSignature !== 'string' || !part.thoughtSignature
682
+ || part.thoughtSignature.length > 16_384) return [];
683
+ normalized.push({
684
+ text: part.text,
685
+ thought: true,
686
+ thoughtSignature: part.thoughtSignature,
687
+ });
688
+ }
689
+ return normalized;
690
+ }
691
+
236
692
  export function toGeminiContents(messages) {
237
693
  const contents = [];
238
694
  // Map synthetic toolCallId -> function name from prior assistant
@@ -252,6 +708,57 @@ export function toGeminiContents(messages) {
252
708
  return contents;
253
709
  }
254
710
 
711
+ export function parseGeminiThinkingParts(parts) {
712
+ if (!Array.isArray(parts)) return undefined;
713
+ const blocks = [];
714
+ for (const part of parts) {
715
+ if (part?.thought !== true) continue;
716
+ const block = {
717
+ type: 'thinking',
718
+ thinking: typeof part.text === 'string' ? part.text : '',
719
+ };
720
+ const signature = part.thoughtSignature || part.thought_signature;
721
+ if (typeof signature === 'string' && signature && signature.length <= 16_384) {
722
+ block.signature = signature;
723
+ }
724
+ blocks.push(block);
725
+ }
726
+ return blocks.length ? blocks : undefined;
727
+ }
728
+
729
+ export function parseGeminiTextPartMetadata(parts) {
730
+ if (!Array.isArray(parts)) return undefined;
731
+ const thoughtParts = parts
732
+ .filter((part) => part?.thought === true && typeof part?.text === 'string')
733
+ .map((part) => {
734
+ const out = { text: part.text };
735
+ const signature = part.thoughtSignature || part.thought_signature;
736
+ if (typeof signature === 'string' && signature && signature.length <= 16_384) {
737
+ out.thoughtSignature = signature;
738
+ }
739
+ return out;
740
+ })
741
+ // Unsigned reasoning must never become persisted hidden prompt text.
742
+ .filter((part) => typeof part.thoughtSignature === 'string' && part.thoughtSignature);
743
+ const textParts = parts
744
+ .filter((part) => part?.thought !== true && typeof part?.text === 'string')
745
+ .map((part) => {
746
+ const out = { text: part.text };
747
+ const signature = part.thoughtSignature || part.thought_signature;
748
+ if (typeof signature === 'string' && signature && signature.length <= 16_384) {
749
+ out.thoughtSignature = signature;
750
+ }
751
+ return out;
752
+ });
753
+ if (!thoughtParts.length && !textParts.some((part) => part.thoughtSignature)) return undefined;
754
+ return {
755
+ gemini: {
756
+ ...(thoughtParts.length ? { thoughtParts } : {}),
757
+ ...(textParts.some((part) => part.thoughtSignature) ? { textParts } : {}),
758
+ },
759
+ };
760
+ }
761
+
255
762
  export function parseToolCalls(parts) {
256
763
  const calls = parts.filter((p) => 'functionCall' in p && !!p.functionCall);
257
764
  if (!calls.length)
@@ -282,11 +789,16 @@ export function parseToolCalls(parts) {
282
789
  args: fc.args ?? {},
283
790
  })).slice(0, 16);
284
791
  const call = {
285
- id: `gemini_${idHash}`,
792
+ // Gemini may provide an opaque server call id. It is a protocol
793
+ // correlation token: preserve it byte-for-byte and synthesize a
794
+ // deterministic id only for older responses that omit it.
795
+ id: typeof fc.id === 'string' && fc.id ? fc.id : `gemini_${idHash}`,
286
796
  name: fc.name,
287
797
  arguments: (fc.args ?? {}),
288
798
  };
289
- if (sig) call.thoughtSignature = sig;
799
+ if (typeof sig === 'string' && sig && sig.length <= 16_384) {
800
+ call.thoughtSignature = sig;
801
+ }
290
802
  return call;
291
803
  });
292
804
  }