@dtlabs/ai-toolkit 0.15.1 → 0.16.1
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 +21 -0
- package/lib/workflow-control.js +20 -18
- package/package.json +1 -1
- package/src/claude/scripts/wb-validate.js +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.16.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.16.0...v0.16.1) (2026-10-02)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* **wb-validate:** require groupingRationale only when outputCount > 1 ([e3cb71a](https://github.com/dtlab-tech/ai-toolkit/commit/e3cb71a7f38fbd974c70f3dbdd99223a6aa05048))
|
|
8
|
+
* **wb-validate:** require groupingRationale only when outputCount > 1 ([7406fbe](https://github.com/dtlab-tech/ai-toolkit/commit/7406fbe336304c2ebcd012fc6a6b1b4980121a64))
|
|
9
|
+
|
|
10
|
+
## [0.16.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.15.1...v0.16.0) (2026-10-02)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* Add FTR-017 and FTR-018 to the Feature Registry with detailed summaries and keywords ([9be5d95](https://github.com/dtlab-tech/ai-toolkit/commit/9be5d95abf934cca91ebde11ade60bda837ea719))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **workflows:** make structured-output extraction delimiter-agnostic ([0ad511b](https://github.com/dtlab-tech/ai-toolkit/commit/0ad511bfa3892a3c02ff91c9c8256cec5fbdd841))
|
|
21
|
+
* **workflows:** make structured-output extraction delimiter-agnostic ([3498c00](https://github.com/dtlab-tech/ai-toolkit/commit/3498c001725d1b5c52039506f79cacda31b1bc4e))
|
|
22
|
+
|
|
2
23
|
## [0.15.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.15.0...v0.15.1) (2026-10-01)
|
|
3
24
|
|
|
4
25
|
|
package/lib/workflow-control.js
CHANGED
|
@@ -296,30 +296,32 @@ function createControl(options) {
|
|
|
296
296
|
return api;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
299
|
+
// The model is not consistent about which delimiter it uses to mark its final structured
|
|
300
|
+
// answer inside otherwise-free text — observed empirically, same agent and schema, run to
|
|
301
|
+
// run: an <structured_output> tag, a ```json fence, and an unprompted <json> tag. Growing
|
|
302
|
+
// one hardcoded pattern per newly observed convention doesn't converge, so matching is
|
|
303
|
+
// delimiter-agnostic: any <tag>...</tag> pair and any fenced code block are tried as
|
|
304
|
+
// candidates, most-recent-first (the real answer conventionally comes after any earlier
|
|
305
|
+
// in-text example), each independently re-validated as schema-conformant JSON before being
|
|
306
|
+
// accepted — an unmatched or malformed text never silently satisfies the contract. A
|
|
307
|
+
// non-greedy delimiter match is itself unsafe when the schema has a free-text field (e.g. a
|
|
308
|
+
// "report") that legitimately embeds its own nested, identically-delimited example before
|
|
309
|
+
// the real closing delimiter, so brace-balance recovery anchored after the last ```json or
|
|
310
|
+
// <json> marker is tried next — immune to that nesting because it tracks braces, not
|
|
311
|
+
// delimiters — followed by an unanchored whole-text brace span as the last resort.
|
|
304
312
|
function extractDelimitedStructuredOutput(text, schema) {
|
|
305
313
|
const candidates = [];
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
// The non-greedy fence match above stops at the FIRST ``` following ```json, which is
|
|
311
|
-
// wrong when a schema string field (e.g. a "report") legitimately embeds its own
|
|
312
|
-
// untagged ``` ... ``` example (a markdown table, say) before the real closing fence —
|
|
313
|
-
// observed empirically with gaia-validate-feature-docs. Recover with a brace-balance
|
|
314
|
-
// span instead of a fence-delimiter match, anchored after the last ```json marker so it
|
|
315
|
-
// is immune to backtick nesting inside string values.
|
|
316
|
-
const lastJsonFence = text.lastIndexOf('```json');
|
|
317
|
-
if (lastJsonFence !== -1) {
|
|
318
|
-
const tail = text.slice(lastJsonFence + '```json'.length);
|
|
314
|
+
for (const m of [...text.matchAll(/<(\w+)>([\s\S]*?)<\/\1>/g)].reverse()) candidates.push(m[2]);
|
|
315
|
+
for (const m of [...text.matchAll(/```[a-zA-Z]*\s*\n?([\s\S]*?)```/g)].reverse()) candidates.push(m[1]);
|
|
316
|
+
for (const start of [...text.matchAll(/```json|<json>/gi)].map(m => m.index + m[0].length).reverse()) {
|
|
317
|
+
const tail = text.slice(start);
|
|
319
318
|
const first = tail.indexOf('{');
|
|
320
319
|
const last = tail.lastIndexOf('}');
|
|
321
320
|
if (first !== -1 && last > first) candidates.push(tail.slice(first, last + 1));
|
|
322
321
|
}
|
|
322
|
+
const first = text.indexOf('{');
|
|
323
|
+
const last = text.lastIndexOf('}');
|
|
324
|
+
if (first !== -1 && last > first) candidates.push(text.slice(first, last + 1));
|
|
323
325
|
for (const raw of candidates) {
|
|
324
326
|
try {
|
|
325
327
|
const parsed = JSON.parse(raw.trim());
|
package/package.json
CHANGED
|
@@ -359,7 +359,7 @@ for (const phase of phases) {
|
|
|
359
359
|
|
|
360
360
|
const topLevelFields = [
|
|
361
361
|
'id', 'title', 'outcome', 'domain', 'agentType', 'dependsOn',
|
|
362
|
-
'acceptanceCriteria', 'outputCount', '
|
|
362
|
+
'acceptanceCriteria', 'outputCount', 'commit',
|
|
363
363
|
]
|
|
364
364
|
for (const field of topLevelFields) {
|
|
365
365
|
if (task[field] == null) {
|
|
@@ -374,6 +374,20 @@ for (const phase of phases) {
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// groupingRationale is required only when outputCount > 1 (the generator agent's own
|
|
378
|
+
// contract: a single-output task has nothing to justify grouping, so it emits null) —
|
|
379
|
+
// null is the documented, correct value there, not a missing field.
|
|
380
|
+
if (task.outputCount > 1 && task.groupingRationale == null) {
|
|
381
|
+
report.errors.push({
|
|
382
|
+
category: ERRORS.MISSING_FIELD,
|
|
383
|
+
severity: 'error',
|
|
384
|
+
taskId,
|
|
385
|
+
field: 'groupingRationale',
|
|
386
|
+
message: `Task "${taskId}" has outputCount > 1 but is missing required field "groupingRationale"`,
|
|
387
|
+
details: { taskId, field: 'groupingRationale' },
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
377
391
|
// Nested: verification.commands — existence and array type only; emptiness checked later
|
|
378
392
|
if (task.verification == null || task.verification.commands == null) {
|
|
379
393
|
report.errors.push({
|