@dtlabs/ai-toolkit 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,28 @@
1
1
  ## 0.1.4 (2026-07-21)
2
+ ## [0.15.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.3...v0.15.0) (2026-10-01)
3
+
4
+
5
+ ### Features
6
+
7
+ * Add isolated parallel task execution and task checkpoints documentation ([e17121f](https://github.com/dtlab-tech/ai-toolkit/commit/e17121fbf118673831634f981294d58e1a665d49))
8
+
9
+
10
+ ### Bug Fixes
11
+
12
+ * **workflows:** never fail-closed inside extractJsonDocument itself ([dd5f0ce](https://github.com/dtlab-tech/ai-toolkit/commit/dd5f0ce07b7145ef73e29479e4282105000cfd42))
13
+ * **workflows:** recover JSON worker output wrapped in prose or a code fence ([b7a0d08](https://github.com/dtlab-tech/ai-toolkit/commit/b7a0d0817cf6056893906aacff87b40578c6625b))
14
+ * **workflows:** recover JSON worker output wrapped in prose or a code fence ([0dbaece](https://github.com/dtlab-tech/ai-toolkit/commit/0dbaeceb193521c2aca59a15de3762def4881935))
15
+
16
+ ## [0.14.3](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.2...v0.14.3) (2026-10-01)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **workflows:** accept a schema-valid <structured_output> text block when the CLI field is empty ([6814d96](https://github.com/dtlab-tech/ai-toolkit/commit/6814d96dd10dc5807b8c20417c5a2767cce05216))
22
+ * **workflows:** handle multiple model-chosen delimiters for the structured-output text fallback ([7d97758](https://github.com/dtlab-tech/ai-toolkit/commit/7d977589e10480e6bce66a4ca68a5dbc89b372b1))
23
+ * **workflows:** schema-validated text fallback for empty structured_output ([f2bd76f](https://github.com/dtlab-tech/ai-toolkit/commit/f2bd76fcba554c1b30a2c84629bfd2224a721d13))
24
+ * **workflows:** schema-validated text fallback for empty structured_output ([daa1411](https://github.com/dtlab-tech/ai-toolkit/commit/daa1411d15c919254ce948a785b31d56e6e4468c))
25
+
2
26
  ## [0.14.2](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.1...v0.14.2) (2026-10-01)
3
27
 
4
28
 
@@ -91,11 +91,23 @@ negative semantic verdict, unknown scopes, paths with spaces and explicit approv
91
91
  The live paid Claude model/permission flow has not been exercised by these tests.
92
92
  Document validation requires both a written Validation Report and a structured
93
93
  verdict (`valid: boolean`, `findings: string[]`). The validator performs one pass;
94
- the host owns revisions. A report or JSON in the plain-text result cannot replace
95
- `structured_output` from `--json-schema`. Missing structured output stops the
96
- workflow with an error naming the worker and required fields. Regression tests
97
- cover missing/malformed verdicts, a clean verdict without a report, and a negative
98
- verdict followed by host revision and a clean result; model responses are mocked.
94
+ the host owns revisions. Bare plain-text JSON, with no delimiter, never substitutes
95
+ for `structured_output` from `--json-schema`. However, the CLI's own structured-output
96
+ extraction is turn-bound: it only populates `structured_output` when the model's final
97
+ turn ends in a tool call carrying the schema payload, and a long multi-phase agent
98
+ response (full report text, then a closing JSON object) ends that same turn in plain
99
+ text instead — so `structured_output` can come back empty even though the model
100
+ emitted a schema-valid payload in a `<structured_output>...</structured_output>`
101
+ block, exactly as its own agent definition instructs. The host falls back to
102
+ extracting and schema-validating that one delimited block from the plain-text result
103
+ when `structured_output` is absent; this is a deterministic, strictly-delimited parse
104
+ (never "guess the JSON somewhere in the text"), so it does not reintroduce LLM-driven
105
+ I/O. A missing field, a missing/malformed delimited block, or a block that fails
106
+ schema validation all still stop the workflow with the original error naming the
107
+ worker and required fields. Regression tests cover missing/malformed verdicts, a
108
+ clean verdict without a report, the delimited-text fallback (both the accepted and
109
+ the schema-rejected case), and a negative verdict followed by host revision and a
110
+ clean result; model responses are mocked.
99
111
  The subprocess adapter reuses FTR-018's existing qualification; no new cross-platform
100
112
  process-supervision guarantee is claimed. On Windows, the full executor test suite
101
113
  requires Git Bash ahead of the WSL launcher in PATH. User-level installations are
@@ -186,7 +186,25 @@ function createControl(options) {
186
186
  }
187
187
  if (result.exitCode !== 0 || result.parseError || result.timedOut ||
188
188
  !result.result || result.result.is_error !== false) throw new Error('Worker failed: ' + id);
189
- const payload = opts.schema ? result.result.structured_output : result.result.result;
189
+ let payload = opts.schema ? result.result.structured_output : result.result.result;
190
+ let usedTextFallback = false;
191
+ // The CLI's structured-output extraction is turn-bound: it only populates
192
+ // `structured_output` when the model's final turn ends in a tool call carrying the
193
+ // schema payload. A long, multi-phase agent response (visible report text, then a
194
+ // closing JSON object) ends that same turn in plain text instead, so `structured_output`
195
+ // comes back empty even though the model faithfully emitted a schema-valid payload in
196
+ // the plain-text result. Observed empirically (not a transient flake) with
197
+ // gaia-validate-feature-docs across multiple toolkit versions and multiple runs: the
198
+ // model never misses or mangles the payload, only the CLI's own field never gets
199
+ // populated — but the model is NOT consistent about how it delimits that payload inside
200
+ // its text (an `<structured_output>` tag one run, a ```json fence the next, same agent,
201
+ // same schema). Falling back to a strictly-delimited, schema-validated extraction that
202
+ // tries each known convention — never a bare "parse the whole result as JSON" guess —
203
+ // keeps the host's parse deterministic rather than asking the LLM to do I/O.
204
+ if (opts.schema && payload == null && typeof result.result.result === 'string') {
205
+ const candidate = extractDelimitedStructuredOutput(result.result.result, opts.schema);
206
+ if (candidate != null) { payload = candidate; usedTextFallback = true; }
207
+ }
190
208
  if (payload == null || (typeof payload === 'string' && !payload.trim())) {
191
209
  if (opts.schema) {
192
210
  const fields = (opts.schema.required || []).join(', ') || '(see requested schema)';
@@ -195,10 +213,11 @@ function createControl(options) {
195
213
  throw new Error('Worker returned no result: ' + id);
196
214
  }
197
215
  if (opts.schema) validateSchema(payload, opts.schema);
216
+ if (usedTextFallback) evidence.save({ usedTextFallback: true });
198
217
  if (outputTarget) {
199
218
  const content = opts.schema ? payload.report : payload;
200
219
  if (typeof content !== 'string' || !content.trim()) throw new Error('Worker output content missing or empty: ' + id);
201
- writeOutput(outputTarget, content);
220
+ writeOutput(outputTarget, /\.json$/i.test(outputTarget) ? extractJsonDocument(content) : content);
202
221
  }
203
222
  evidence.save({ outcome: 'done', completedAt: new Date().toISOString() });
204
223
  return payload;
@@ -277,6 +296,57 @@ function createControl(options) {
277
296
  return api;
278
297
  }
279
298
 
299
+ // Tries each known convention a model has been observed using to delimit its final
300
+ // structured answer inside otherwise-free text, in order of explicitness, and returns the
301
+ // first candidate that is both valid JSON and schema-conformant. Returns null (never throws)
302
+ // if no candidate qualifies, so the caller's original "no structured output" error still fires
303
+ // unchanged when the text holds nothing usable.
304
+ function extractDelimitedStructuredOutput(text, schema) {
305
+ const candidates = [];
306
+ const tagMatch = text.match(/<structured_output>([\s\S]*?)<\/structured_output>/);
307
+ if (tagMatch) candidates.push(tagMatch[1]);
308
+ const fenceMatches = [...text.matchAll(/```json\s*([\s\S]*?)```/gi)];
309
+ if (fenceMatches.length) candidates.push(fenceMatches[fenceMatches.length - 1][1]);
310
+ for (const raw of candidates) {
311
+ try {
312
+ const parsed = JSON.parse(raw.trim());
313
+ validateSchema(parsed, schema);
314
+ return parsed;
315
+ } catch (e) { /* try the next candidate, or fall through to null */ }
316
+ }
317
+ return null;
318
+ }
319
+
320
+ // A worker with a declared JSON output has no Write tool (see worker()'s own comment on why):
321
+ // its entire response text becomes the file verbatim. Some models still wrap that deliverable
322
+ // in conversational prose and/or a Markdown code fence despite being told not to (observed
323
+ // empirically with gaia-generate-work-breakdown on haiku: "Perfect. I have all the necessary
324
+ // information..." before the JSON), which is a hard JSON.parse failure for every downstream
325
+ // consumer of this file. Recover the document with the same "try each known convention, always
326
+ // re-validate as parseable JSON, never a blind substring guess" discipline as
327
+ // extractDelimitedStructuredOutput: the content as-is first, then the last fenced code block,
328
+ // then the outermost brace/bracket span. Never throws — genuinely malformed JSON (not merely
329
+ // wrapped) is passed through unchanged so the downstream deterministic validator (wb-validate.js)
330
+ // remains the one thing that fails closed on it; this function only undoes wrapping, it is not
331
+ // itself a validation gate.
332
+ function extractJsonDocument(text) {
333
+ const tryParse = candidate => { try { JSON.parse(candidate); return candidate; } catch (e) { return null; } };
334
+ const whole = tryParse(text.trim());
335
+ if (whole != null) return whole;
336
+ const fenceMatches = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)];
337
+ for (let i = fenceMatches.length - 1; i >= 0; i--) {
338
+ const candidate = tryParse(fenceMatches[i][1].trim());
339
+ if (candidate != null) return candidate;
340
+ }
341
+ const first = text.search(/[[{]/);
342
+ const last = Math.max(text.lastIndexOf('}'), text.lastIndexOf(']'));
343
+ if (first >= 0 && last > first) {
344
+ const candidate = tryParse(text.slice(first, last + 1).trim());
345
+ if (candidate != null) return candidate;
346
+ }
347
+ return text;
348
+ }
349
+
280
350
  function validateSchema(value, schema) {
281
351
  if (schema.type === 'object') {
282
352
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid structured object');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",