@dtlabs/ai-toolkit 0.19.1 → 0.19.2
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,12 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.19.2](https://github.com/dtlab-tech/ai-toolkit/compare/v0.19.1...v0.19.2) (2026-10-19)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* **workflow-control, tech-spec-assembler:** unwrap whole response fences in Markdown documents to prevent content loss ([fe88582](https://github.com/dtlab-tech/ai-toolkit/commit/fe885821bb7b813237a1d53ed827c70e09b6c9d0))
|
|
8
|
+
* **workflow-control, tech-spec-assembler:** unwrap whole response fences in Markdown documents to prevent content loss ([358e484](https://github.com/dtlab-tech/ai-toolkit/commit/358e4847fe74c314c3c1158dbeda08669530c1f8))
|
|
9
|
+
|
|
2
10
|
## [0.19.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.19.0...v0.19.1) (2026-10-19)
|
|
3
11
|
|
|
4
12
|
|
package/lib/workflow-control.js
CHANGED
|
@@ -238,6 +238,13 @@ function createControl(options) {
|
|
|
238
238
|
const candidate = extractDelimitedStructuredOutput(result.result.result, opts.schema);
|
|
239
239
|
if (candidate != null) { payload = candidate; usedTextFallback = true; }
|
|
240
240
|
}
|
|
241
|
+
// A non-schema worker's payload IS the document — unwrap a whole-response fence/preamble
|
|
242
|
+
// before anything downstream (the caller's own write, or this function's own outputTarget
|
|
243
|
+
// write below) ever sees it. See extractMarkdownDocument's own comment for the full story.
|
|
244
|
+
if (!opts.schema && typeof payload === 'string') {
|
|
245
|
+
const cleaned = extractMarkdownDocument(payload);
|
|
246
|
+
if (cleaned !== payload) { payload = cleaned; usedTextFallback = true; }
|
|
247
|
+
}
|
|
241
248
|
if (payload == null || (typeof payload === 'string' && !payload.trim())) {
|
|
242
249
|
if (opts.schema) {
|
|
243
250
|
const fields = (opts.schema.required || []).join(', ') || '(see requested schema)';
|
|
@@ -476,6 +483,65 @@ function extractJsonDocument(text) {
|
|
|
476
483
|
return text;
|
|
477
484
|
}
|
|
478
485
|
|
|
486
|
+
// Same problem extractJsonDocument fixes, for a Markdown document deliverable (a component/
|
|
487
|
+
// use-case/task detail file, or a Requirements/Tech-Spec skeleton) instead of a JSON one: a
|
|
488
|
+
// worker's entire response becomes the file verbatim (these agents have no Write tool, see
|
|
489
|
+
// worker()'s own comment on why — the host writes whatever text they return), and a model that
|
|
490
|
+
// wraps its deliverable in conversational prose and/or an outer code fence silently corrupts the
|
|
491
|
+
// saved file. Found empirically in a real spec/components/*.md file: the agent's own real
|
|
492
|
+
// "## Change"/"## Tests" headings ended up INSIDE an outer ```markdown fence and were (correctly)
|
|
493
|
+
// treated as fenced, non-heading content by tech-spec-assembler.js's own fence-aware parser
|
|
494
|
+
// (FTR-025) — the file wrote to disk with its real content unreachable, surfacing only much later
|
|
495
|
+
// as an opaque "component file is missing its ## Change section" error during final assembly, with
|
|
496
|
+
// no filename attached to say which one (see loadTechSpec's own fix for that half of the bug).
|
|
497
|
+
//
|
|
498
|
+
// Same "try as-is first, re-validate the candidate, never a blind substring guess, never throws"
|
|
499
|
+
// discipline as extractJsonDocument: "valid" here means "has at least one unfenced Markdown
|
|
500
|
+
// heading" — the same fence-length-aware scan tech-spec-assembler.js's own
|
|
501
|
+
// _nonFencedHeadingIndexes uses, reimplemented here as a presence check rather than a line-index
|
|
502
|
+
// list, for the identical "sandboxed runtime has no require()"-shaped reason these two files are
|
|
503
|
+
// already siblings, not shared, elsewhere in this codebase. A document that legitimately ends in
|
|
504
|
+
// its own small fenced code example (a real Tests section's last line) is never mistaken for a
|
|
505
|
+
// whole-response wrap: its real headings already appear BEFORE that trailing fence, unfenced, so
|
|
506
|
+
// the as-is candidate already satisfies this check and no unwrapping is even attempted.
|
|
507
|
+
function _hasUnfencedHeading(text) {
|
|
508
|
+
let fenceLen = 0;
|
|
509
|
+
for (const line of text.split('\n')) {
|
|
510
|
+
const m = line.match(/^(`{3,})/);
|
|
511
|
+
if (m) { const n = m[1].length; fenceLen = fenceLen === 0 ? n : (n >= fenceLen ? 0 : fenceLen); continue; }
|
|
512
|
+
if (fenceLen === 0 && /^#{1,6}\s/.test(line)) return true;
|
|
513
|
+
}
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
// Line-range based, not regex-based: a regex scan for the "first ``` ... last ```" span would
|
|
517
|
+
// wrongly stop at the first NESTED closing fence instead of the real outer one (exactly the shape
|
|
518
|
+
// a Markdown document's own "## Data Model" section legitimately needs — a fenced JSON example
|
|
519
|
+
// nested inside the model's outer ```markdown wrap). Only the LAST non-blank line being a bare
|
|
520
|
+
// fence-close, matched back to the first fence-open of the SAME backtick-run length, correctly
|
|
521
|
+
// identifies the true outer boundary regardless of what's nested between them.
|
|
522
|
+
function _unwrapWholeResponseFence(text) {
|
|
523
|
+
const lines = text.split('\n');
|
|
524
|
+
let end = lines.length;
|
|
525
|
+
while (end > 0 && lines[end - 1].trim() === '') end--;
|
|
526
|
+
if (end === 0) return null;
|
|
527
|
+
const closeMatch = lines[end - 1].match(/^(`{3,})\s*$/);
|
|
528
|
+
if (!closeMatch) return null;
|
|
529
|
+
const fenceLen = closeMatch[1].length;
|
|
530
|
+
let start = -1;
|
|
531
|
+
for (let i = 0; i < end - 1; i++) {
|
|
532
|
+
const openMatch = lines[i].match(/^(`{3,})\S*\s*$/);
|
|
533
|
+
if (openMatch && openMatch[1].length === fenceLen) { start = i; break; }
|
|
534
|
+
}
|
|
535
|
+
if (start === -1) return null;
|
|
536
|
+
return lines.slice(start + 1, end - 1).join('\n').trim() + '\n';
|
|
537
|
+
}
|
|
538
|
+
function extractMarkdownDocument(text) {
|
|
539
|
+
if (_hasUnfencedHeading(text)) return text;
|
|
540
|
+
const unwrapped = _unwrapWholeResponseFence(text);
|
|
541
|
+
if (unwrapped != null && _hasUnfencedHeading(unwrapped)) return unwrapped;
|
|
542
|
+
return text;
|
|
543
|
+
}
|
|
544
|
+
|
|
479
545
|
function validateSchema(value, schema) {
|
|
480
546
|
if (schema.type === 'object') {
|
|
481
547
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid structured object');
|
package/package.json
CHANGED
|
@@ -148,7 +148,18 @@ function loadTechSpec(inputPath) {
|
|
|
148
148
|
for (const c of components) {
|
|
149
149
|
const compPath = path.join(inputPath, 'components', c.fileSlug)
|
|
150
150
|
if (!fs.existsSync(compPath)) throw new AssemblyError(`component "${c.filePath}" has no detail file at ${compPath}`)
|
|
151
|
-
|
|
151
|
+
try {
|
|
152
|
+
detailByPath[c.filePath] = parseComponentDetail(fs.readFileSync(compPath, 'utf8'))
|
|
153
|
+
} catch (err) {
|
|
154
|
+
// parseComponentDetail's own error names only the symptom ("missing its ## Change
|
|
155
|
+
// section"), never which file — a real diagnosability bug, found when exactly this error
|
|
156
|
+
// surfaced during final assembly with nothing identifying which of ~60 component files was
|
|
157
|
+
// actually malformed (the real culprit here was a whole-response-wrapped-in-a-fence worker
|
|
158
|
+
// reply — see lib/workflow-control.js's extractMarkdownDocument for the fix to that other
|
|
159
|
+
// half of this same incident). Re-thrown with the file identified, never silently swallowed.
|
|
160
|
+
if (err instanceof AssemblyError) throw new AssemblyError(`${err.message} (component "${c.filePath}" at ${compPath})`)
|
|
161
|
+
throw err
|
|
162
|
+
}
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
let doc = rewriteSection9(indexContent, components)
|