@dtlabs/ai-toolkit 0.15.1 → 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 CHANGED
@@ -1,4 +1,17 @@
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
+
2
15
  ## [0.15.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.15.0...v0.15.1) (2026-10-01)
3
16
 
4
17
 
@@ -296,30 +296,32 @@ function createControl(options) {
296
296
  return api;
297
297
  }
298
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.
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 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
- // 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",