@dtlabs/ai-toolkit 0.14.2 → 0.14.3
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 +10 -0
- package/docs/workflow-control-plane.md +17 -5
- package/lib/workflow-control.js +41 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.14.3](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.2...v0.14.3) (2026-10-01)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* **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))
|
|
8
|
+
* **workflows:** handle multiple model-chosen delimiters for the structured-output text fallback ([7d97758](https://github.com/dtlab-tech/ai-toolkit/commit/7d977589e10480e6bce66a4ca68a5dbc89b372b1))
|
|
9
|
+
* **workflows:** schema-validated text fallback for empty structured_output ([f2bd76f](https://github.com/dtlab-tech/ai-toolkit/commit/f2bd76fcba554c1b30a2c84629bfd2224a721d13))
|
|
10
|
+
* **workflows:** schema-validated text fallback for empty structured_output ([daa1411](https://github.com/dtlab-tech/ai-toolkit/commit/daa1411d15c919254ce948a785b31d56e6e4468c))
|
|
11
|
+
|
|
2
12
|
## [0.14.2](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.1...v0.14.2) (2026-10-01)
|
|
3
13
|
|
|
4
14
|
|
|
@@ -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.
|
|
95
|
-
`structured_output` from `--json-schema`.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
package/lib/workflow-control.js
CHANGED
|
@@ -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
|
-
|
|
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,6 +213,7 @@ 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);
|
|
@@ -277,6 +296,27 @@ 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
|
+
|
|
280
320
|
function validateSchema(value, schema) {
|
|
281
321
|
if (schema.type === 'object') {
|
|
282
322
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid structured object');
|