@hunterzhu/pulse-adapters 0.4.3 → 0.4.4

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.
@@ -232,23 +232,81 @@ function parseDsmlJson(value) {
232
232
  return undefined;
233
233
  }
234
234
  }
235
- /** DeepSeek sometimes writes only JSON tool objects instead of tool_calls. Each object needs an explicit tool name; argument-shaped JSON and trailing prose stay text. */
235
+ /** DeepSeek sometimes writes tool calls as JSON or markdown fences instead of tool_calls. A bare object still needs an explicit tool name. Argument-shaped JSON, a single tool object after prose, and any trailing prose stay text. */
236
236
  function recoverDeepSeekJsonToolCalls(text, toolNameAliases) {
237
- const leading = takeLeadingJsonValues(text.trim());
237
+ const trimmed = text.trim();
238
+ return recoverStrictJsonToolCalls(trimmed, toolNameAliases) ?? recoverMarkdownToolFences(trimmed, toolNameAliases);
239
+ }
240
+ function recoverStrictJsonToolCalls(text, toolNameAliases) {
241
+ const leading = takeLeadingJsonValues(text);
238
242
  if (leading === undefined || leading.values.length === 0 || leading.rest.trim().length > 0)
239
243
  return undefined;
240
244
  const objects = leading.values.filter((value) => !isEmptyJsonValue(value));
241
245
  if (objects.length === 0)
242
246
  return undefined;
247
+ if (objects.length === 1 && Array.isArray(objects[0]) && objects[0].every((item) => argumentObject(item) !== undefined))
248
+ return toolCallsFrom(objects[0], toolNameAliases);
243
249
  if (objects.length === 1 && objects[0] && typeof objects[0] === 'object' && !Array.isArray(objects[0])) {
244
250
  const batch = objects[0].tool_calls ?? objects[0].toolCalls;
245
- if (Array.isArray(batch)) {
246
- const calls = batch.map((item) => jsonToolCall(item, toolNameAliases));
247
- return calls.every((call) => call !== undefined) && calls.length > 0 ? calls : undefined;
251
+ if (Array.isArray(batch))
252
+ return toolCallsFrom(batch, toolNameAliases);
253
+ }
254
+ return toolCallsFrom(objects, toolNameAliases);
255
+ }
256
+ /** A message that is only `tool.name` headings followed by JSON fences is a tool batch, not a finished answer. */
257
+ function recoverMarkdownToolFences(text, toolNameAliases) {
258
+ const lines = text.split(/\r?\n/);
259
+ const calls = [];
260
+ let index = 0;
261
+ const skipBlank = () => { while (index < lines.length && lines[index].trim() === '')
262
+ index++; };
263
+ skipBlank();
264
+ if (index >= lines.length)
265
+ return undefined;
266
+ while (index < lines.length) {
267
+ skipBlank();
268
+ if (index >= lines.length)
269
+ break;
270
+ const providerToolName = lines[index].trim();
271
+ if (!/^[A-Za-z][\w-]*(?:\.[\w-]+)+$/.test(providerToolName))
272
+ return undefined;
273
+ index++;
274
+ if (lines[index]?.trim() !== '```json' && lines[index]?.trim() !== '```')
275
+ return undefined;
276
+ index++;
277
+ const body = [];
278
+ while (index < lines.length && lines[index].trim() !== '```')
279
+ body.push(lines[index++]);
280
+ if (index >= lines.length)
281
+ return undefined;
282
+ index++;
283
+ let parsed;
284
+ try {
285
+ parsed = JSON.parse(body.join('\n'));
248
286
  }
287
+ catch {
288
+ return undefined;
289
+ }
290
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
291
+ return undefined;
292
+ const input = parsed;
293
+ calls.push({ providerToolName, name: toolNameAliases?.get(providerToolName) ?? providerToolName, input, providerToolArguments: JSON.stringify(input) });
249
294
  }
250
- const calls = objects.map((value) => jsonToolCall(value, toolNameAliases));
251
- return calls.every((call) => call !== undefined) ? calls : undefined;
295
+ return calls.length > 0 ? calls : undefined;
296
+ }
297
+ function toolCallsFrom(values, toolNameAliases) {
298
+ const calls = values.map((value) => jsonToolCall(value, toolNameAliases));
299
+ return calls.every((call) => call !== undefined) && calls.length > 0 ? calls : undefined;
300
+ }
301
+ function argumentObject(value) {
302
+ if (!value || typeof value !== 'object' || Array.isArray(value))
303
+ return undefined;
304
+ const record = value;
305
+ const named = typeof record.tool === 'string' && record.tool.length > 0 ? record.tool : typeof record.name === 'string' && record.name.length > 0 ? record.name : undefined;
306
+ const raw = record.arguments ?? record.input ?? record.parameters ?? record.args;
307
+ if (named === undefined || !raw || typeof raw !== 'object' || Array.isArray(raw))
308
+ return undefined;
309
+ return raw;
252
310
  }
253
311
  function isEmptyJsonValue(value) {
254
312
  if (Array.isArray(value))
@@ -263,7 +321,7 @@ function jsonToolCall(value, toolNameAliases) {
263
321
  const providerToolName = named;
264
322
  if (providerToolName === undefined)
265
323
  return undefined;
266
- const raw = record.arguments ?? record.input ?? record.parameters;
324
+ const raw = record.arguments ?? record.input ?? record.parameters ?? record.args;
267
325
  const input = raw && typeof raw === 'object' && !Array.isArray(raw) ? { ...raw } : { ...record };
268
326
  if (raw === undefined) {
269
327
  delete input.tool;
@@ -47,6 +47,53 @@ class SlotPool {
47
47
  this.slots.set(key, slot);
48
48
  } ; return slot; }
49
49
  }
50
+ function jsonCandidates(text) {
51
+ const found = [];
52
+ const push = (value) => {
53
+ try {
54
+ const parsed = JSON.parse(value);
55
+ found.push(parsed);
56
+ if (typeof parsed === 'string' && parsed !== value)
57
+ push(parsed);
58
+ }
59
+ catch { /* keep scanning */ }
60
+ };
61
+ const trimmed = text.trim();
62
+ if (trimmed)
63
+ push(trimmed);
64
+ for (let start = 0; start < text.length; start++) {
65
+ if (text[start] !== '{')
66
+ continue;
67
+ let depth = 0;
68
+ let inString = false;
69
+ let escaped = false;
70
+ for (let end = start; end < text.length; end++) {
71
+ const char = text[end];
72
+ if (inString) {
73
+ if (escaped)
74
+ escaped = false;
75
+ else if (char === '\\')
76
+ escaped = true;
77
+ else if (char === '"')
78
+ inString = false;
79
+ continue;
80
+ }
81
+ if (char === '"')
82
+ inString = true;
83
+ else if (char === '{')
84
+ depth++;
85
+ else if (char === '}') {
86
+ depth--;
87
+ if (depth === 0) {
88
+ push(text.slice(start, end + 1));
89
+ start = end;
90
+ break;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ return found;
96
+ }
50
97
  function toJson(value, seen = new Set()) {
51
98
  if (value === null || typeof value === 'string' || typeof value === 'boolean')
52
99
  return value;
@@ -172,13 +219,15 @@ export function createModelEffectExecutor(config) {
172
219
  throw new OutputValidationError('adapter', 'MODEL_REFUSAL', output.refusal ?? 'Provider refused the request.');
173
220
  }
174
221
  if (input.outputSchema !== undefined) {
175
- const candidateValue = output.structured ?? output.text;
176
- if (!validateJsonSchema(candidateValue, input.outputSchema)) {
177
- lastSchemaViolation = toJson(candidateValue);
222
+ const candidates = output.structured === undefined ? jsonCandidates(output.text) : [output.structured, ...jsonCandidates(output.text)];
223
+ const matched = candidates.find((candidate) => validateJsonSchema(candidate, input.outputSchema));
224
+ if (matched === undefined) {
225
+ lastSchemaViolation = toJson(candidates[0] ?? output.text);
178
226
  failedForSchema = true;
179
227
  recordFeedback('schema_rejected', 0);
180
228
  throw new OutputValidationError('structured', 'OUTPUT_SCHEMA_VIOLATION', 'Provider output did not match the declared schema');
181
229
  }
230
+ output.structured = matched;
182
231
  }
183
232
  failedForSchema = false;
184
233
  recordFeedback('succeeded', 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-adapters",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "license": "PolyForm-Noncommercial-1.0.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@anthropic-ai/sandbox-runtime": "0.0.77",
21
- "@hunterzhu/pulse-runtime": "0.4.3",
22
- "@hunterzhu/pulse-tool-sdk": "0.4.3",
21
+ "@hunterzhu/pulse-runtime": "0.4.4",
22
+ "@hunterzhu/pulse-tool-sdk": "0.4.4",
23
23
  "exceljs": "4.4.0",
24
24
  "pdfjs-dist": "4.10.38"
25
25
  }