@dtlabs/ai-toolkit 0.15.0 → 0.16.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 +21 -0
- package/lib/workflow-control.js +24 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.16.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.15.1...v0.16.0) (2026-10-02)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* 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))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* **workflows:** make structured-output extraction delimiter-agnostic ([0ad511b](https://github.com/dtlab-tech/ai-toolkit/commit/0ad511bfa3892a3c02ff91c9c8256cec5fbdd841))
|
|
13
|
+
* **workflows:** make structured-output extraction delimiter-agnostic ([3498c00](https://github.com/dtlab-tech/ai-toolkit/commit/3498c001725d1b5c52039506f79cacda31b1bc4e))
|
|
14
|
+
|
|
15
|
+
## [0.15.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.15.0...v0.15.1) (2026-10-01)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **workflows:** recover ```json structured output wrapped around a ne… ([a96add9](https://github.com/dtlab-tech/ai-toolkit/commit/a96add902195ea0a125e0fbbc9be06126dfbd602))
|
|
21
|
+
* **workflows:** recover ```json structured output wrapped around a nested untagged fence ([6e92dd2](https://github.com/dtlab-tech/ai-toolkit/commit/6e92dd28bf96f6e96947b45bcc7c8e54d7cb9c38))
|
|
22
|
+
|
|
2
23
|
## [0.15.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.3...v0.15.0) (2026-10-01)
|
|
3
24
|
|
|
4
25
|
|
package/lib/workflow-control.js
CHANGED
|
@@ -296,17 +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
|
-
|
|
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);
|
|
318
|
+
const first = tail.indexOf('{');
|
|
319
|
+
const last = tail.lastIndexOf('}');
|
|
320
|
+
if (first !== -1 && last > first) candidates.push(tail.slice(first, last + 1));
|
|
321
|
+
}
|
|
322
|
+
const first = text.indexOf('{');
|
|
323
|
+
const last = text.lastIndexOf('}');
|
|
324
|
+
if (first !== -1 && last > first) candidates.push(text.slice(first, last + 1));
|
|
310
325
|
for (const raw of candidates) {
|
|
311
326
|
try {
|
|
312
327
|
const parsed = JSON.parse(raw.trim());
|