markform 0.1.1 → 0.1.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/DOCS.md +546 -0
- package/README.md +340 -71
- package/SPEC.md +2779 -0
- package/dist/ai-sdk.d.mts +2 -2
- package/dist/ai-sdk.mjs +5 -3
- package/dist/{apply-BQdd-fdx.mjs → apply-00UmzDKL.mjs} +849 -730
- package/dist/bin.mjs +6 -3
- package/dist/{cli-pjOiHgCW.mjs → cli-D--Lel-e.mjs} +1374 -428
- package/dist/cli.mjs +6 -3
- package/dist/{coreTypes--6etkcwb.d.mts → coreTypes-BXhhz9Iq.d.mts} +1946 -794
- package/dist/coreTypes-Dful87E0.mjs +537 -0
- package/dist/index.d.mts +116 -19
- package/dist/index.mjs +5 -3
- package/dist/session-Bqnwi9wp.mjs +110 -0
- package/dist/session-DdAtY2Ni.mjs +4 -0
- package/dist/shared-D7gf27Tr.mjs +3 -0
- package/dist/shared-N_s1M-_K.mjs +176 -0
- package/dist/src-Dm8jZ5dl.mjs +7587 -0
- package/examples/celebrity-deep-research/celebrity-deep-research.form.md +912 -0
- package/examples/earnings-analysis/earnings-analysis.form.md +6 -1
- package/examples/earnings-analysis/earnings-analysis.valid.ts +119 -59
- package/examples/movie-research/movie-research-basic.form.md +164 -0
- package/examples/movie-research/movie-research-deep.form.md +486 -0
- package/examples/movie-research/movie-research-minimal.form.md +54 -0
- package/examples/simple/simple-mock-filled.form.md +17 -13
- package/examples/simple/simple-skipped-filled.form.md +32 -9
- package/examples/simple/simple-with-skips.session.yaml +102 -143
- package/examples/simple/simple.form.md +13 -13
- package/examples/simple/simple.session.yaml +80 -69
- package/examples/startup-deep-research/startup-deep-research.form.md +60 -8
- package/examples/startup-research/startup-research-mock-filled.form.md +1 -1
- package/examples/startup-research/startup-research.form.md +1 -1
- package/package.json +10 -14
- package/dist/src-Cs4_9lWP.mjs +0 -2151
- package/examples/political-research/political-research.form.md +0 -233
- package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
package/dist/src-Cs4_9lWP.mjs
DELETED
|
@@ -1,2151 +0,0 @@
|
|
|
1
|
-
import { S as getWebSearchConfig, _ as DEFAULT_PRIORITY, at as SessionTranscriptSchema, et as PatchSchema, f as AGENT_ROLE, h as DEFAULT_MAX_TURNS, m as DEFAULT_MAX_PATCHES_PER_TURN, n as getFieldsForRoles, p as DEFAULT_MAX_ISSUES, r as inspect, t as applyPatches, u as serialize, v as DEFAULT_ROLE_INSTRUCTIONS } from "./apply-BQdd-fdx.mjs";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import Markdoc from "@markdoc/markdoc";
|
|
4
|
-
import YAML from "yaml";
|
|
5
|
-
import { sha256 } from "js-sha256";
|
|
6
|
-
import { generateText, stepCountIs, zodSchema } from "ai";
|
|
7
|
-
import { openai } from "@ai-sdk/openai";
|
|
8
|
-
import { google } from "@ai-sdk/google";
|
|
9
|
-
|
|
10
|
-
//#region src/engine/parse.ts
|
|
11
|
-
/**
|
|
12
|
-
* Markdoc parser for .form.md files.
|
|
13
|
-
*
|
|
14
|
-
* Parses Markdoc documents and extracts form schema, values, and documentation blocks.
|
|
15
|
-
*/
|
|
16
|
-
/** Parse error with source location info */
|
|
17
|
-
var ParseError = class extends Error {
|
|
18
|
-
constructor(message, line, col) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.line = line;
|
|
21
|
-
this.col = col;
|
|
22
|
-
this.name = "ParseError";
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
26
|
-
/**
|
|
27
|
-
* Extract YAML frontmatter from markdown content.
|
|
28
|
-
*/
|
|
29
|
-
function extractFrontmatter(content) {
|
|
30
|
-
const match = FRONTMATTER_REGEX.exec(content);
|
|
31
|
-
if (!match) return {
|
|
32
|
-
frontmatter: {},
|
|
33
|
-
body: content
|
|
34
|
-
};
|
|
35
|
-
const yamlContent = match[1];
|
|
36
|
-
const body = content.slice(match[0].length);
|
|
37
|
-
try {
|
|
38
|
-
const lines = (yamlContent ?? "").split("\n");
|
|
39
|
-
const result = {};
|
|
40
|
-
for (const line of lines) {
|
|
41
|
-
const colonIndex = line.indexOf(":");
|
|
42
|
-
if (colonIndex > 0 && !line.startsWith(" ") && !line.startsWith(" ")) {
|
|
43
|
-
const key = line.slice(0, colonIndex).trim();
|
|
44
|
-
const value = line.slice(colonIndex + 1).trim();
|
|
45
|
-
if (value.startsWith("\"") && value.endsWith("\"")) result[key] = value.slice(1, -1);
|
|
46
|
-
else if (value === "") result[key] = {};
|
|
47
|
-
else result[key] = value;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
frontmatter: result,
|
|
52
|
-
body
|
|
53
|
-
};
|
|
54
|
-
} catch (_error) {
|
|
55
|
-
throw new ParseError("Failed to parse frontmatter YAML");
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
/** Map checkbox marker to state value */
|
|
59
|
-
const CHECKBOX_MARKERS = {
|
|
60
|
-
"[ ]": "todo",
|
|
61
|
-
"[x]": "done",
|
|
62
|
-
"[X]": "done",
|
|
63
|
-
"[/]": "incomplete",
|
|
64
|
-
"[*]": "active",
|
|
65
|
-
"[-]": "na",
|
|
66
|
-
"[y]": "yes",
|
|
67
|
-
"[Y]": "yes",
|
|
68
|
-
"[n]": "no",
|
|
69
|
-
"[N]": "no"
|
|
70
|
-
};
|
|
71
|
-
const OPTION_TEXT_PATTERN = /^(\[[^\]]\])\s*(.*?)\s*$/;
|
|
72
|
-
/**
|
|
73
|
-
* Parse option text to extract marker and label.
|
|
74
|
-
* Text is like "[ ] Label" or "[x] Label".
|
|
75
|
-
*/
|
|
76
|
-
function parseOptionText(text) {
|
|
77
|
-
const match = OPTION_TEXT_PATTERN.exec(text);
|
|
78
|
-
if (!match) return null;
|
|
79
|
-
return {
|
|
80
|
-
marker: match[1] ?? "",
|
|
81
|
-
label: (match[2] ?? "").trim()
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Check if a node is a tag node with specific name.
|
|
86
|
-
* Works with raw AST nodes (not transformed Tags).
|
|
87
|
-
*/
|
|
88
|
-
function isTagNode(node, name) {
|
|
89
|
-
if (typeof node !== "object" || node === null) return false;
|
|
90
|
-
if (node.type === "tag" && node.tag) return name === void 0 || node.tag === name;
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Get string attribute value or undefined.
|
|
95
|
-
*/
|
|
96
|
-
function getStringAttr(node, name) {
|
|
97
|
-
const value = node.attributes?.[name];
|
|
98
|
-
return typeof value === "string" ? value : void 0;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Get number attribute value or undefined.
|
|
102
|
-
*/
|
|
103
|
-
function getNumberAttr(node, name) {
|
|
104
|
-
const value = node.attributes?.[name];
|
|
105
|
-
return typeof value === "number" ? value : void 0;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Get boolean attribute value or undefined.
|
|
109
|
-
*/
|
|
110
|
-
function getBooleanAttr(node, name) {
|
|
111
|
-
const value = node.attributes?.[name];
|
|
112
|
-
return typeof value === "boolean" ? value : void 0;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Get validator references from validate attribute.
|
|
116
|
-
* Handles both single string and array formats.
|
|
117
|
-
*/
|
|
118
|
-
function getValidateAttr(node) {
|
|
119
|
-
const value = node.attributes?.validate;
|
|
120
|
-
if (value === void 0 || value === null) return;
|
|
121
|
-
if (Array.isArray(value)) return value;
|
|
122
|
-
if (typeof value === "string") return [value];
|
|
123
|
-
if (typeof value === "object") return [value];
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Extract option items from node children (for option lists).
|
|
127
|
-
* Works with raw AST nodes. Collects text and ID from list items.
|
|
128
|
-
*/
|
|
129
|
-
function extractOptionItems(node) {
|
|
130
|
-
const items = [];
|
|
131
|
-
/**
|
|
132
|
-
* Collect all text content from a node tree into a single string.
|
|
133
|
-
*/
|
|
134
|
-
function collectText(n) {
|
|
135
|
-
let text = "";
|
|
136
|
-
if (n.type === "text" && typeof n.attributes?.content === "string") text += n.attributes.content;
|
|
137
|
-
if (n.type === "softbreak") text += "\n";
|
|
138
|
-
if (n.children && Array.isArray(n.children)) for (const c of n.children) text += collectText(c);
|
|
139
|
-
return text;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Traverse to find list items and extract their content.
|
|
143
|
-
*/
|
|
144
|
-
function traverse(child) {
|
|
145
|
-
if (!child || typeof child !== "object") return;
|
|
146
|
-
if (child.type === "item") {
|
|
147
|
-
const text = collectText(child);
|
|
148
|
-
const id = typeof child.attributes?.id === "string" ? child.attributes.id : null;
|
|
149
|
-
if (text.trim()) items.push({
|
|
150
|
-
id,
|
|
151
|
-
text: text.trim()
|
|
152
|
-
});
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (child.children && Array.isArray(child.children)) for (const c of child.children) traverse(c);
|
|
156
|
-
}
|
|
157
|
-
if (node.children && Array.isArray(node.children)) for (const child of node.children) traverse(child);
|
|
158
|
-
return items;
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Extract fence value from node children.
|
|
162
|
-
* Looks for ```value code blocks.
|
|
163
|
-
*/
|
|
164
|
-
function extractFenceValue(node) {
|
|
165
|
-
function traverse(child) {
|
|
166
|
-
if (!child || typeof child !== "object") return null;
|
|
167
|
-
if (child.type === "fence") {
|
|
168
|
-
if (child.attributes?.language === "value") return typeof child.attributes?.content === "string" ? child.attributes.content : null;
|
|
169
|
-
}
|
|
170
|
-
if (child.children && Array.isArray(child.children)) for (const c of child.children) {
|
|
171
|
-
const result = traverse(c);
|
|
172
|
-
if (result !== null) return result;
|
|
173
|
-
}
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
if (node.children && Array.isArray(node.children)) for (const child of node.children) {
|
|
177
|
-
const result = traverse(child);
|
|
178
|
-
if (result !== null) return result;
|
|
179
|
-
}
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Get priority attribute value or default to DEFAULT_PRIORITY.
|
|
184
|
-
*/
|
|
185
|
-
function getPriorityAttr(node) {
|
|
186
|
-
const value = getStringAttr(node, "priority");
|
|
187
|
-
if (value === "high" || value === "medium" || value === "low") return value;
|
|
188
|
-
return DEFAULT_PRIORITY;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Parse a string-field tag.
|
|
192
|
-
*/
|
|
193
|
-
function parseStringField(node) {
|
|
194
|
-
const id = getStringAttr(node, "id");
|
|
195
|
-
const label = getStringAttr(node, "label");
|
|
196
|
-
if (!id) throw new ParseError("string-field missing required 'id' attribute");
|
|
197
|
-
if (!label) throw new ParseError(`string-field '${id}' missing required 'label' attribute`);
|
|
198
|
-
const field = {
|
|
199
|
-
kind: "string",
|
|
200
|
-
id,
|
|
201
|
-
label,
|
|
202
|
-
required: getBooleanAttr(node, "required") ?? false,
|
|
203
|
-
priority: getPriorityAttr(node),
|
|
204
|
-
role: getStringAttr(node, "role") ?? AGENT_ROLE,
|
|
205
|
-
multiline: getBooleanAttr(node, "multiline"),
|
|
206
|
-
pattern: getStringAttr(node, "pattern"),
|
|
207
|
-
minLength: getNumberAttr(node, "minLength"),
|
|
208
|
-
maxLength: getNumberAttr(node, "maxLength"),
|
|
209
|
-
validate: getValidateAttr(node)
|
|
210
|
-
};
|
|
211
|
-
const fenceContent = extractFenceValue(node);
|
|
212
|
-
return {
|
|
213
|
-
field,
|
|
214
|
-
value: {
|
|
215
|
-
kind: "string",
|
|
216
|
-
value: fenceContent !== null ? fenceContent.trim() : null
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Parse a number-field tag.
|
|
222
|
-
*/
|
|
223
|
-
function parseNumberField(node) {
|
|
224
|
-
const id = getStringAttr(node, "id");
|
|
225
|
-
const label = getStringAttr(node, "label");
|
|
226
|
-
if (!id) throw new ParseError("number-field missing required 'id' attribute");
|
|
227
|
-
if (!label) throw new ParseError(`number-field '${id}' missing required 'label' attribute`);
|
|
228
|
-
const field = {
|
|
229
|
-
kind: "number",
|
|
230
|
-
id,
|
|
231
|
-
label,
|
|
232
|
-
required: getBooleanAttr(node, "required") ?? false,
|
|
233
|
-
priority: getPriorityAttr(node),
|
|
234
|
-
role: getStringAttr(node, "role") ?? AGENT_ROLE,
|
|
235
|
-
min: getNumberAttr(node, "min"),
|
|
236
|
-
max: getNumberAttr(node, "max"),
|
|
237
|
-
integer: getBooleanAttr(node, "integer"),
|
|
238
|
-
validate: getValidateAttr(node)
|
|
239
|
-
};
|
|
240
|
-
const fenceContent = extractFenceValue(node);
|
|
241
|
-
let numValue = null;
|
|
242
|
-
if (fenceContent !== null) {
|
|
243
|
-
const trimmed = fenceContent.trim();
|
|
244
|
-
if (trimmed) {
|
|
245
|
-
const parsed = Number(trimmed);
|
|
246
|
-
if (!Number.isNaN(parsed)) numValue = parsed;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return {
|
|
250
|
-
field,
|
|
251
|
-
value: {
|
|
252
|
-
kind: "number",
|
|
253
|
-
value: numValue
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Parse a string-list tag.
|
|
259
|
-
*/
|
|
260
|
-
function parseStringListField(node) {
|
|
261
|
-
const id = getStringAttr(node, "id");
|
|
262
|
-
const label = getStringAttr(node, "label");
|
|
263
|
-
if (!id) throw new ParseError("string-list missing required 'id' attribute");
|
|
264
|
-
if (!label) throw new ParseError(`string-list '${id}' missing required 'label' attribute`);
|
|
265
|
-
const field = {
|
|
266
|
-
kind: "string_list",
|
|
267
|
-
id,
|
|
268
|
-
label,
|
|
269
|
-
required: getBooleanAttr(node, "required") ?? false,
|
|
270
|
-
priority: getPriorityAttr(node),
|
|
271
|
-
role: getStringAttr(node, "role") ?? AGENT_ROLE,
|
|
272
|
-
minItems: getNumberAttr(node, "minItems"),
|
|
273
|
-
maxItems: getNumberAttr(node, "maxItems"),
|
|
274
|
-
itemMinLength: getNumberAttr(node, "itemMinLength"),
|
|
275
|
-
itemMaxLength: getNumberAttr(node, "itemMaxLength"),
|
|
276
|
-
uniqueItems: getBooleanAttr(node, "uniqueItems"),
|
|
277
|
-
validate: getValidateAttr(node)
|
|
278
|
-
};
|
|
279
|
-
const fenceContent = extractFenceValue(node);
|
|
280
|
-
const items = [];
|
|
281
|
-
if (fenceContent !== null) {
|
|
282
|
-
const lines = fenceContent.split("\n");
|
|
283
|
-
for (const line of lines) {
|
|
284
|
-
const trimmed = line.trim();
|
|
285
|
-
if (trimmed) items.push(trimmed);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return {
|
|
289
|
-
field,
|
|
290
|
-
value: {
|
|
291
|
-
kind: "string_list",
|
|
292
|
-
items
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Parse options from a select/checkbox field.
|
|
298
|
-
*/
|
|
299
|
-
function parseOptions(node, fieldId) {
|
|
300
|
-
const items = extractOptionItems(node);
|
|
301
|
-
const options = [];
|
|
302
|
-
const selected = {};
|
|
303
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
304
|
-
for (const item of items) {
|
|
305
|
-
const parsed = parseOptionText(item.text);
|
|
306
|
-
if (!parsed) continue;
|
|
307
|
-
if (!item.id) throw new ParseError(`Option in field '${fieldId}' missing ID annotation. Use {% #option_id %}`);
|
|
308
|
-
if (seenIds.has(item.id)) throw new ParseError(`Duplicate option ID '${item.id}' in field '${fieldId}'`);
|
|
309
|
-
seenIds.add(item.id);
|
|
310
|
-
options.push({
|
|
311
|
-
id: item.id,
|
|
312
|
-
label: parsed.label
|
|
313
|
-
});
|
|
314
|
-
const state = CHECKBOX_MARKERS[parsed.marker];
|
|
315
|
-
if (state !== void 0) selected[item.id] = state;
|
|
316
|
-
}
|
|
317
|
-
return {
|
|
318
|
-
options,
|
|
319
|
-
selected
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Parse a single-select tag.
|
|
324
|
-
*/
|
|
325
|
-
function parseSingleSelectField(node) {
|
|
326
|
-
const id = getStringAttr(node, "id");
|
|
327
|
-
const label = getStringAttr(node, "label");
|
|
328
|
-
if (!id) throw new ParseError("single-select missing required 'id' attribute");
|
|
329
|
-
if (!label) throw new ParseError(`single-select '${id}' missing required 'label' attribute`);
|
|
330
|
-
const { options, selected } = parseOptions(node, id);
|
|
331
|
-
const field = {
|
|
332
|
-
kind: "single_select",
|
|
333
|
-
id,
|
|
334
|
-
label,
|
|
335
|
-
required: getBooleanAttr(node, "required") ?? false,
|
|
336
|
-
priority: getPriorityAttr(node),
|
|
337
|
-
role: getStringAttr(node, "role") ?? AGENT_ROLE,
|
|
338
|
-
options,
|
|
339
|
-
validate: getValidateAttr(node)
|
|
340
|
-
};
|
|
341
|
-
let selectedOption = null;
|
|
342
|
-
for (const [optId, state] of Object.entries(selected)) if (state === "done") {
|
|
343
|
-
selectedOption = optId;
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
return {
|
|
347
|
-
field,
|
|
348
|
-
value: {
|
|
349
|
-
kind: "single_select",
|
|
350
|
-
selected: selectedOption
|
|
351
|
-
}
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Parse a multi-select tag.
|
|
356
|
-
*/
|
|
357
|
-
function parseMultiSelectField(node) {
|
|
358
|
-
const id = getStringAttr(node, "id");
|
|
359
|
-
const label = getStringAttr(node, "label");
|
|
360
|
-
if (!id) throw new ParseError("multi-select missing required 'id' attribute");
|
|
361
|
-
if (!label) throw new ParseError(`multi-select '${id}' missing required 'label' attribute`);
|
|
362
|
-
const { options, selected } = parseOptions(node, id);
|
|
363
|
-
const field = {
|
|
364
|
-
kind: "multi_select",
|
|
365
|
-
id,
|
|
366
|
-
label,
|
|
367
|
-
required: getBooleanAttr(node, "required") ?? false,
|
|
368
|
-
priority: getPriorityAttr(node),
|
|
369
|
-
role: getStringAttr(node, "role") ?? AGENT_ROLE,
|
|
370
|
-
options,
|
|
371
|
-
minSelections: getNumberAttr(node, "minSelections"),
|
|
372
|
-
maxSelections: getNumberAttr(node, "maxSelections"),
|
|
373
|
-
validate: getValidateAttr(node)
|
|
374
|
-
};
|
|
375
|
-
const selectedOptions = [];
|
|
376
|
-
for (const [optId, state] of Object.entries(selected)) if (state === "done") selectedOptions.push(optId);
|
|
377
|
-
return {
|
|
378
|
-
field,
|
|
379
|
-
value: {
|
|
380
|
-
kind: "multi_select",
|
|
381
|
-
selected: selectedOptions
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Parse a checkboxes tag.
|
|
387
|
-
*/
|
|
388
|
-
function parseCheckboxesField(node) {
|
|
389
|
-
const id = getStringAttr(node, "id");
|
|
390
|
-
const label = getStringAttr(node, "label");
|
|
391
|
-
if (!id) throw new ParseError("checkboxes missing required 'id' attribute");
|
|
392
|
-
if (!label) throw new ParseError(`checkboxes '${id}' missing required 'label' attribute`);
|
|
393
|
-
const { options, selected } = parseOptions(node, id);
|
|
394
|
-
const checkboxModeStr = getStringAttr(node, "checkboxMode");
|
|
395
|
-
let checkboxMode = "multi";
|
|
396
|
-
if (checkboxModeStr === "multi" || checkboxModeStr === "simple" || checkboxModeStr === "explicit") checkboxMode = checkboxModeStr;
|
|
397
|
-
const approvalModeStr = getStringAttr(node, "approvalMode");
|
|
398
|
-
let approvalMode = "none";
|
|
399
|
-
if (approvalModeStr === "blocking") approvalMode = "blocking";
|
|
400
|
-
const explicitRequired = getBooleanAttr(node, "required");
|
|
401
|
-
let required;
|
|
402
|
-
if (checkboxMode === "explicit") {
|
|
403
|
-
if (explicitRequired === false) throw new ParseError(`Checkbox field "${label}" has checkboxMode="explicit" which is inherently required. Cannot set required=false. Remove required attribute or change checkboxMode.`);
|
|
404
|
-
required = true;
|
|
405
|
-
} else required = explicitRequired ?? false;
|
|
406
|
-
const field = {
|
|
407
|
-
kind: "checkboxes",
|
|
408
|
-
id,
|
|
409
|
-
label,
|
|
410
|
-
required,
|
|
411
|
-
priority: getPriorityAttr(node),
|
|
412
|
-
role: getStringAttr(node, "role") ?? AGENT_ROLE,
|
|
413
|
-
checkboxMode,
|
|
414
|
-
minDone: getNumberAttr(node, "minDone"),
|
|
415
|
-
options,
|
|
416
|
-
approvalMode,
|
|
417
|
-
validate: getValidateAttr(node)
|
|
418
|
-
};
|
|
419
|
-
const values = {};
|
|
420
|
-
for (const opt of options) {
|
|
421
|
-
const state = selected[opt.id];
|
|
422
|
-
if (state === void 0 || state === "todo") values[opt.id] = checkboxMode === "explicit" ? "unfilled" : "todo";
|
|
423
|
-
else values[opt.id] = state;
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
field,
|
|
427
|
-
value: {
|
|
428
|
-
kind: "checkboxes",
|
|
429
|
-
values
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Parse a url-field tag.
|
|
435
|
-
*/
|
|
436
|
-
function parseUrlField(node) {
|
|
437
|
-
const id = getStringAttr(node, "id");
|
|
438
|
-
const label = getStringAttr(node, "label");
|
|
439
|
-
if (!id) throw new ParseError("url-field missing required 'id' attribute");
|
|
440
|
-
if (!label) throw new ParseError(`url-field '${id}' missing required 'label' attribute`);
|
|
441
|
-
const field = {
|
|
442
|
-
kind: "url",
|
|
443
|
-
id,
|
|
444
|
-
label,
|
|
445
|
-
required: getBooleanAttr(node, "required") ?? false,
|
|
446
|
-
priority: getPriorityAttr(node),
|
|
447
|
-
role: getStringAttr(node, "role") ?? AGENT_ROLE,
|
|
448
|
-
validate: getValidateAttr(node)
|
|
449
|
-
};
|
|
450
|
-
const fenceContent = extractFenceValue(node);
|
|
451
|
-
return {
|
|
452
|
-
field,
|
|
453
|
-
value: {
|
|
454
|
-
kind: "url",
|
|
455
|
-
value: fenceContent !== null ? fenceContent.trim() : null
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Parse a url-list tag.
|
|
461
|
-
*/
|
|
462
|
-
function parseUrlListField(node) {
|
|
463
|
-
const id = getStringAttr(node, "id");
|
|
464
|
-
const label = getStringAttr(node, "label");
|
|
465
|
-
if (!id) throw new ParseError("url-list missing required 'id' attribute");
|
|
466
|
-
if (!label) throw new ParseError(`url-list '${id}' missing required 'label' attribute`);
|
|
467
|
-
const field = {
|
|
468
|
-
kind: "url_list",
|
|
469
|
-
id,
|
|
470
|
-
label,
|
|
471
|
-
required: getBooleanAttr(node, "required") ?? false,
|
|
472
|
-
priority: getPriorityAttr(node),
|
|
473
|
-
role: getStringAttr(node, "role") ?? AGENT_ROLE,
|
|
474
|
-
minItems: getNumberAttr(node, "minItems"),
|
|
475
|
-
maxItems: getNumberAttr(node, "maxItems"),
|
|
476
|
-
uniqueItems: getBooleanAttr(node, "uniqueItems"),
|
|
477
|
-
validate: getValidateAttr(node)
|
|
478
|
-
};
|
|
479
|
-
const fenceContent = extractFenceValue(node);
|
|
480
|
-
const items = [];
|
|
481
|
-
if (fenceContent !== null) {
|
|
482
|
-
const lines = fenceContent.split("\n");
|
|
483
|
-
for (const line of lines) {
|
|
484
|
-
const trimmed = line.trim();
|
|
485
|
-
if (trimmed) items.push(trimmed);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return {
|
|
489
|
-
field,
|
|
490
|
-
value: {
|
|
491
|
-
kind: "url_list",
|
|
492
|
-
items
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Parse a field tag and return field schema and value.
|
|
498
|
-
*/
|
|
499
|
-
function parseField(node) {
|
|
500
|
-
if (!isTagNode(node)) return null;
|
|
501
|
-
switch (node.tag) {
|
|
502
|
-
case "string-field": return parseStringField(node);
|
|
503
|
-
case "number-field": return parseNumberField(node);
|
|
504
|
-
case "string-list": return parseStringListField(node);
|
|
505
|
-
case "single-select": return parseSingleSelectField(node);
|
|
506
|
-
case "multi-select": return parseMultiSelectField(node);
|
|
507
|
-
case "checkboxes": return parseCheckboxesField(node);
|
|
508
|
-
case "url-field": return parseUrlField(node);
|
|
509
|
-
case "url-list": return parseUrlListField(node);
|
|
510
|
-
default: return null;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Parse a field-group tag.
|
|
515
|
-
*/
|
|
516
|
-
function parseFieldGroup(node, valuesByFieldId, orderIndex, idIndex, parentId) {
|
|
517
|
-
const id = getStringAttr(node, "id");
|
|
518
|
-
const title = getStringAttr(node, "title");
|
|
519
|
-
if (!id) throw new ParseError("field-group missing required 'id' attribute");
|
|
520
|
-
if (idIndex.has(id)) throw new ParseError(`Duplicate ID '${id}'`);
|
|
521
|
-
idIndex.set(id, {
|
|
522
|
-
kind: "group",
|
|
523
|
-
parentId
|
|
524
|
-
});
|
|
525
|
-
const children = [];
|
|
526
|
-
function processChildren(child) {
|
|
527
|
-
if (!child || typeof child !== "object") return;
|
|
528
|
-
const result = parseField(child);
|
|
529
|
-
if (result) {
|
|
530
|
-
if (idIndex.has(result.field.id)) throw new ParseError(`Duplicate ID '${result.field.id}'`);
|
|
531
|
-
idIndex.set(result.field.id, {
|
|
532
|
-
kind: "field",
|
|
533
|
-
parentId: id
|
|
534
|
-
});
|
|
535
|
-
children.push(result.field);
|
|
536
|
-
valuesByFieldId[result.field.id] = result.value;
|
|
537
|
-
orderIndex.push(result.field.id);
|
|
538
|
-
if ("options" in result.field) for (const opt of result.field.options) {
|
|
539
|
-
const qualifiedRef = `${result.field.id}.${opt.id}`;
|
|
540
|
-
if (idIndex.has(qualifiedRef)) throw new ParseError(`Duplicate option ref '${qualifiedRef}'`);
|
|
541
|
-
idIndex.set(qualifiedRef, {
|
|
542
|
-
kind: "option",
|
|
543
|
-
parentId: id,
|
|
544
|
-
fieldId: result.field.id
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
if (child.children && Array.isArray(child.children)) for (const c of child.children) processChildren(c);
|
|
549
|
-
}
|
|
550
|
-
if (node.children && Array.isArray(node.children)) for (const child of node.children) processChildren(child);
|
|
551
|
-
return {
|
|
552
|
-
kind: "field_group",
|
|
553
|
-
id,
|
|
554
|
-
title,
|
|
555
|
-
validate: getValidateAttr(node),
|
|
556
|
-
children
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Parse a form tag.
|
|
561
|
-
*/
|
|
562
|
-
function parseFormTag(node, valuesByFieldId, orderIndex, idIndex) {
|
|
563
|
-
const id = getStringAttr(node, "id");
|
|
564
|
-
const title = getStringAttr(node, "title");
|
|
565
|
-
if (!id) throw new ParseError("form missing required 'id' attribute");
|
|
566
|
-
if (idIndex.has(id)) throw new ParseError(`Duplicate ID '${id}'`);
|
|
567
|
-
idIndex.set(id, { kind: "form" });
|
|
568
|
-
const groups = [];
|
|
569
|
-
function findFieldGroups(child) {
|
|
570
|
-
if (!child || typeof child !== "object") return;
|
|
571
|
-
if (isTagNode(child, "field-group")) {
|
|
572
|
-
const group = parseFieldGroup(child, valuesByFieldId, orderIndex, idIndex, id);
|
|
573
|
-
groups.push(group);
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
if (child.children && Array.isArray(child.children)) for (const c of child.children) findFieldGroups(c);
|
|
577
|
-
}
|
|
578
|
-
if (node.children && Array.isArray(node.children)) for (const child of node.children) findFieldGroups(child);
|
|
579
|
-
return {
|
|
580
|
-
id,
|
|
581
|
-
title,
|
|
582
|
-
groups
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
/** Valid documentation tag names */
|
|
586
|
-
const DOC_TAG_NAMES = [
|
|
587
|
-
"description",
|
|
588
|
-
"instructions",
|
|
589
|
-
"documentation"
|
|
590
|
-
];
|
|
591
|
-
/**
|
|
592
|
-
* Extract all documentation blocks from AST.
|
|
593
|
-
* Looks for {% description %}, {% instructions %}, and {% documentation %} tags.
|
|
594
|
-
*/
|
|
595
|
-
function extractDocBlocks(ast, idIndex) {
|
|
596
|
-
const docs = [];
|
|
597
|
-
const seenRefs = /* @__PURE__ */ new Set();
|
|
598
|
-
function traverse(node) {
|
|
599
|
-
if (!node || typeof node !== "object") return;
|
|
600
|
-
const nodeTag = node.type === "tag" && node.tag ? node.tag : null;
|
|
601
|
-
if (nodeTag && DOC_TAG_NAMES.includes(nodeTag)) {
|
|
602
|
-
const tag = nodeTag;
|
|
603
|
-
const ref = getStringAttr(node, "ref");
|
|
604
|
-
if (!ref) throw new ParseError(`${tag} block missing required 'ref' attribute`);
|
|
605
|
-
if (!idIndex.has(ref)) throw new ParseError(`${tag} block references unknown ID '${ref}'`);
|
|
606
|
-
const uniqueKey = `${ref}:${tag}`;
|
|
607
|
-
if (seenRefs.has(uniqueKey)) throw new ParseError(`Duplicate ${tag} block for ref='${ref}'`);
|
|
608
|
-
seenRefs.add(uniqueKey);
|
|
609
|
-
let bodyMarkdown = "";
|
|
610
|
-
function extractText(n) {
|
|
611
|
-
if (n.type === "text" && typeof n.attributes?.content === "string") bodyMarkdown += n.attributes.content;
|
|
612
|
-
if (n.children && Array.isArray(n.children)) for (const c of n.children) extractText(c);
|
|
613
|
-
}
|
|
614
|
-
if (node.children && Array.isArray(node.children)) for (const child of node.children) extractText(child);
|
|
615
|
-
docs.push({
|
|
616
|
-
tag,
|
|
617
|
-
ref,
|
|
618
|
-
bodyMarkdown: bodyMarkdown.trim()
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
if (node.children && Array.isArray(node.children)) for (const child of node.children) traverse(child);
|
|
622
|
-
}
|
|
623
|
-
traverse(ast);
|
|
624
|
-
return docs;
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* Parse a Markform .form.md document.
|
|
628
|
-
*
|
|
629
|
-
* @param markdown - The full markdown content including frontmatter
|
|
630
|
-
* @returns The parsed form representation
|
|
631
|
-
* @throws ParseError if the document is invalid
|
|
632
|
-
*/
|
|
633
|
-
function parseForm(markdown) {
|
|
634
|
-
const { body } = extractFrontmatter(markdown);
|
|
635
|
-
const ast = Markdoc.parse(body);
|
|
636
|
-
let formSchema = null;
|
|
637
|
-
const valuesByFieldId = {};
|
|
638
|
-
const orderIndex = [];
|
|
639
|
-
const idIndex = /* @__PURE__ */ new Map();
|
|
640
|
-
function findFormTag(node) {
|
|
641
|
-
if (!node || typeof node !== "object") return;
|
|
642
|
-
if (isTagNode(node, "form")) {
|
|
643
|
-
if (formSchema) throw new ParseError("Multiple form tags found - only one allowed");
|
|
644
|
-
formSchema = parseFormTag(node, valuesByFieldId, orderIndex, idIndex);
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
if (node.children && Array.isArray(node.children)) for (const child of node.children) findFormTag(child);
|
|
648
|
-
}
|
|
649
|
-
findFormTag(ast);
|
|
650
|
-
if (!formSchema) throw new ParseError("No form tag found in document");
|
|
651
|
-
const docs = extractDocBlocks(ast, idIndex);
|
|
652
|
-
return {
|
|
653
|
-
schema: formSchema,
|
|
654
|
-
valuesByFieldId,
|
|
655
|
-
skipsByFieldId: {},
|
|
656
|
-
docs,
|
|
657
|
-
orderIndex,
|
|
658
|
-
idIndex
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
//#endregion
|
|
663
|
-
//#region src/engine/session.ts
|
|
664
|
-
/**
|
|
665
|
-
* Session module - parsing and serializing session transcripts.
|
|
666
|
-
*
|
|
667
|
-
* Session transcripts are used for golden testing and session replay.
|
|
668
|
-
* They capture the full interaction between the harness and agent.
|
|
669
|
-
*/
|
|
670
|
-
/**
|
|
671
|
-
* Parse a session transcript from YAML string.
|
|
672
|
-
*
|
|
673
|
-
* Converts snake_case keys to camelCase for TypeScript consumption.
|
|
674
|
-
*
|
|
675
|
-
* @param yaml - YAML string containing session transcript
|
|
676
|
-
* @returns Parsed and validated SessionTranscript
|
|
677
|
-
* @throws Error if YAML is invalid or doesn't match schema
|
|
678
|
-
*/
|
|
679
|
-
function parseSession(yaml) {
|
|
680
|
-
let raw;
|
|
681
|
-
try {
|
|
682
|
-
raw = YAML.parse(yaml);
|
|
683
|
-
} catch (err) {
|
|
684
|
-
throw new Error(`Failed to parse session YAML: ${err instanceof Error ? err.message : String(err)}`);
|
|
685
|
-
}
|
|
686
|
-
const converted = toCamelCaseDeep(raw);
|
|
687
|
-
const result = SessionTranscriptSchema.safeParse(converted);
|
|
688
|
-
if (!result.success) {
|
|
689
|
-
const errors = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
690
|
-
throw new Error(`Invalid session transcript: ${errors}`);
|
|
691
|
-
}
|
|
692
|
-
return result.data;
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Serialize a session transcript to YAML string.
|
|
696
|
-
*
|
|
697
|
-
* Converts camelCase keys to snake_case for YAML output.
|
|
698
|
-
*
|
|
699
|
-
* @param session - Session transcript to serialize
|
|
700
|
-
* @returns YAML string
|
|
701
|
-
*/
|
|
702
|
-
function serializeSession(session) {
|
|
703
|
-
const snakeCased = toSnakeCaseDeep(session);
|
|
704
|
-
return YAML.stringify(snakeCased, {
|
|
705
|
-
indent: 2,
|
|
706
|
-
lineWidth: 0
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Convert a string from snake_case to camelCase.
|
|
711
|
-
*/
|
|
712
|
-
function snakeToCamel(str) {
|
|
713
|
-
return str.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Convert a string from camelCase to snake_case.
|
|
717
|
-
*/
|
|
718
|
-
function camelToSnake(str) {
|
|
719
|
-
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Recursively convert all object keys from snake_case to camelCase.
|
|
723
|
-
*
|
|
724
|
-
* Preserves keys that are user-defined identifiers (like option IDs in
|
|
725
|
-
* checkboxes `values` objects).
|
|
726
|
-
*
|
|
727
|
-
* @param obj - Object to convert
|
|
728
|
-
* @param preserveKeys - If true, don't convert keys in this object (but still recurse into values)
|
|
729
|
-
*/
|
|
730
|
-
function toCamelCaseDeep(obj, preserveKeys = false) {
|
|
731
|
-
if (obj === null || obj === void 0) return obj;
|
|
732
|
-
if (Array.isArray(obj)) return obj.map((item) => toCamelCaseDeep(item, false));
|
|
733
|
-
if (typeof obj === "object") {
|
|
734
|
-
const result = {};
|
|
735
|
-
const record = obj;
|
|
736
|
-
for (const [key, value] of Object.entries(record)) {
|
|
737
|
-
const resultKey = preserveKeys ? key : snakeToCamel(key);
|
|
738
|
-
result[resultKey] = toCamelCaseDeep(value, key === "values" && record.op === "set_checkboxes");
|
|
739
|
-
}
|
|
740
|
-
return result;
|
|
741
|
-
}
|
|
742
|
-
return obj;
|
|
743
|
-
}
|
|
744
|
-
/**
|
|
745
|
-
* Recursively convert all object keys from camelCase to snake_case.
|
|
746
|
-
*
|
|
747
|
-
* Preserves keys that are user-defined identifiers (like option IDs in
|
|
748
|
-
* checkboxes `values` objects).
|
|
749
|
-
*
|
|
750
|
-
* @param obj - Object to convert
|
|
751
|
-
* @param preserveKeys - If true, don't convert keys in this object (but still recurse into values)
|
|
752
|
-
*/
|
|
753
|
-
function toSnakeCaseDeep(obj, preserveKeys = false) {
|
|
754
|
-
if (obj === null || obj === void 0) return obj;
|
|
755
|
-
if (Array.isArray(obj)) return obj.map((item) => toSnakeCaseDeep(item, false));
|
|
756
|
-
if (typeof obj === "object") {
|
|
757
|
-
const result = {};
|
|
758
|
-
const record = obj;
|
|
759
|
-
for (const [key, value] of Object.entries(record)) {
|
|
760
|
-
const resultKey = preserveKeys ? key : camelToSnake(key);
|
|
761
|
-
result[resultKey] = toSnakeCaseDeep(value, key === "values" && record.op === "set_checkboxes");
|
|
762
|
-
}
|
|
763
|
-
return result;
|
|
764
|
-
}
|
|
765
|
-
return obj;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
//#endregion
|
|
769
|
-
//#region src/engine/valueCoercion.ts
|
|
770
|
-
/**
|
|
771
|
-
* Find a field by ID.
|
|
772
|
-
*
|
|
773
|
-
* Uses idIndex for O(1) validation that the ID exists and is a field,
|
|
774
|
-
* then retrieves the Field object from the schema.
|
|
775
|
-
*/
|
|
776
|
-
function findFieldById(form, fieldId) {
|
|
777
|
-
if (form.idIndex.get(fieldId)?.kind !== "field") return;
|
|
778
|
-
for (const group of form.schema.groups) for (const field of group.children) if (field.id === fieldId) return field;
|
|
779
|
-
}
|
|
780
|
-
function isPlainObject(value) {
|
|
781
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
782
|
-
}
|
|
783
|
-
function isStringArray(value) {
|
|
784
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
785
|
-
}
|
|
786
|
-
function coerceToString(fieldId, rawValue) {
|
|
787
|
-
if (rawValue === null) return {
|
|
788
|
-
ok: true,
|
|
789
|
-
patch: {
|
|
790
|
-
op: "set_string",
|
|
791
|
-
fieldId,
|
|
792
|
-
value: null
|
|
793
|
-
}
|
|
794
|
-
};
|
|
795
|
-
if (typeof rawValue === "string") return {
|
|
796
|
-
ok: true,
|
|
797
|
-
patch: {
|
|
798
|
-
op: "set_string",
|
|
799
|
-
fieldId,
|
|
800
|
-
value: rawValue
|
|
801
|
-
}
|
|
802
|
-
};
|
|
803
|
-
if (typeof rawValue === "number") return {
|
|
804
|
-
ok: true,
|
|
805
|
-
patch: {
|
|
806
|
-
op: "set_string",
|
|
807
|
-
fieldId,
|
|
808
|
-
value: String(rawValue)
|
|
809
|
-
},
|
|
810
|
-
warning: `Coerced number ${rawValue} to string for field '${fieldId}'`
|
|
811
|
-
};
|
|
812
|
-
if (typeof rawValue === "boolean") return {
|
|
813
|
-
ok: true,
|
|
814
|
-
patch: {
|
|
815
|
-
op: "set_string",
|
|
816
|
-
fieldId,
|
|
817
|
-
value: String(rawValue)
|
|
818
|
-
},
|
|
819
|
-
warning: `Coerced boolean ${rawValue} to string for field '${fieldId}'`
|
|
820
|
-
};
|
|
821
|
-
return {
|
|
822
|
-
ok: false,
|
|
823
|
-
error: `Cannot coerce ${typeof rawValue} to string for field '${fieldId}'`
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
function coerceToNumber(fieldId, rawValue) {
|
|
827
|
-
if (rawValue === null) return {
|
|
828
|
-
ok: true,
|
|
829
|
-
patch: {
|
|
830
|
-
op: "set_number",
|
|
831
|
-
fieldId,
|
|
832
|
-
value: null
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
if (typeof rawValue === "number") return {
|
|
836
|
-
ok: true,
|
|
837
|
-
patch: {
|
|
838
|
-
op: "set_number",
|
|
839
|
-
fieldId,
|
|
840
|
-
value: rawValue
|
|
841
|
-
}
|
|
842
|
-
};
|
|
843
|
-
if (typeof rawValue === "string") {
|
|
844
|
-
const parsed = Number(rawValue);
|
|
845
|
-
if (!Number.isNaN(parsed)) return {
|
|
846
|
-
ok: true,
|
|
847
|
-
patch: {
|
|
848
|
-
op: "set_number",
|
|
849
|
-
fieldId,
|
|
850
|
-
value: parsed
|
|
851
|
-
},
|
|
852
|
-
warning: `Coerced string '${rawValue}' to number for field '${fieldId}'`
|
|
853
|
-
};
|
|
854
|
-
return {
|
|
855
|
-
ok: false,
|
|
856
|
-
error: `Cannot coerce non-numeric string '${rawValue}' to number for field '${fieldId}'`
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
return {
|
|
860
|
-
ok: false,
|
|
861
|
-
error: `Cannot coerce ${typeof rawValue} to number for field '${fieldId}'`
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
function coerceToStringList(fieldId, rawValue) {
|
|
865
|
-
if (rawValue === null) return {
|
|
866
|
-
ok: true,
|
|
867
|
-
patch: {
|
|
868
|
-
op: "set_string_list",
|
|
869
|
-
fieldId,
|
|
870
|
-
items: []
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
if (isStringArray(rawValue)) return {
|
|
874
|
-
ok: true,
|
|
875
|
-
patch: {
|
|
876
|
-
op: "set_string_list",
|
|
877
|
-
fieldId,
|
|
878
|
-
items: rawValue
|
|
879
|
-
}
|
|
880
|
-
};
|
|
881
|
-
if (typeof rawValue === "string") return {
|
|
882
|
-
ok: true,
|
|
883
|
-
patch: {
|
|
884
|
-
op: "set_string_list",
|
|
885
|
-
fieldId,
|
|
886
|
-
items: [rawValue]
|
|
887
|
-
},
|
|
888
|
-
warning: `Coerced single string to array for field '${fieldId}'`
|
|
889
|
-
};
|
|
890
|
-
if (Array.isArray(rawValue)) {
|
|
891
|
-
const items = [];
|
|
892
|
-
for (const item of rawValue) if (typeof item === "string") items.push(item);
|
|
893
|
-
else if (typeof item === "number" || typeof item === "boolean") items.push(String(item));
|
|
894
|
-
else return {
|
|
895
|
-
ok: false,
|
|
896
|
-
error: `Cannot coerce array with non-string items to string_list for field '${fieldId}'`
|
|
897
|
-
};
|
|
898
|
-
return {
|
|
899
|
-
ok: true,
|
|
900
|
-
patch: {
|
|
901
|
-
op: "set_string_list",
|
|
902
|
-
fieldId,
|
|
903
|
-
items
|
|
904
|
-
},
|
|
905
|
-
warning: `Coerced array items to strings for field '${fieldId}'`
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
return {
|
|
909
|
-
ok: false,
|
|
910
|
-
error: `Cannot coerce ${typeof rawValue} to string_list for field '${fieldId}'`
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
function coerceToSingleSelect(field, rawValue) {
|
|
914
|
-
if (field.kind !== "single_select") return {
|
|
915
|
-
ok: false,
|
|
916
|
-
error: `Field '${field.id}' is not a single_select field`
|
|
917
|
-
};
|
|
918
|
-
if (rawValue === null) return {
|
|
919
|
-
ok: true,
|
|
920
|
-
patch: {
|
|
921
|
-
op: "set_single_select",
|
|
922
|
-
fieldId: field.id,
|
|
923
|
-
selected: null
|
|
924
|
-
}
|
|
925
|
-
};
|
|
926
|
-
if (typeof rawValue !== "string") return {
|
|
927
|
-
ok: false,
|
|
928
|
-
error: `single_select field '${field.id}' requires a string option ID, got ${typeof rawValue}`
|
|
929
|
-
};
|
|
930
|
-
const validOptions = new Set(field.options.map((o) => o.id));
|
|
931
|
-
if (!validOptions.has(rawValue)) return {
|
|
932
|
-
ok: false,
|
|
933
|
-
error: `Invalid option '${rawValue}' for single_select field '${field.id}'. Valid options: ${Array.from(validOptions).join(", ")}`
|
|
934
|
-
};
|
|
935
|
-
return {
|
|
936
|
-
ok: true,
|
|
937
|
-
patch: {
|
|
938
|
-
op: "set_single_select",
|
|
939
|
-
fieldId: field.id,
|
|
940
|
-
selected: rawValue
|
|
941
|
-
}
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
function coerceToMultiSelect(field, rawValue) {
|
|
945
|
-
if (field.kind !== "multi_select") return {
|
|
946
|
-
ok: false,
|
|
947
|
-
error: `Field '${field.id}' is not a multi_select field`
|
|
948
|
-
};
|
|
949
|
-
if (rawValue === null) return {
|
|
950
|
-
ok: true,
|
|
951
|
-
patch: {
|
|
952
|
-
op: "set_multi_select",
|
|
953
|
-
fieldId: field.id,
|
|
954
|
-
selected: []
|
|
955
|
-
}
|
|
956
|
-
};
|
|
957
|
-
const validOptions = new Set(field.options.map((o) => o.id));
|
|
958
|
-
let selected;
|
|
959
|
-
let warning;
|
|
960
|
-
if (typeof rawValue === "string") {
|
|
961
|
-
selected = [rawValue];
|
|
962
|
-
warning = `Coerced single string to array for multi_select field '${field.id}'`;
|
|
963
|
-
} else if (isStringArray(rawValue)) selected = rawValue;
|
|
964
|
-
else return {
|
|
965
|
-
ok: false,
|
|
966
|
-
error: `multi_select field '${field.id}' requires a string or string array, got ${typeof rawValue}`
|
|
967
|
-
};
|
|
968
|
-
for (const optId of selected) if (!validOptions.has(optId)) return {
|
|
969
|
-
ok: false,
|
|
970
|
-
error: `Invalid option '${optId}' for multi_select field '${field.id}'. Valid options: ${Array.from(validOptions).join(", ")}`
|
|
971
|
-
};
|
|
972
|
-
const patch = {
|
|
973
|
-
op: "set_multi_select",
|
|
974
|
-
fieldId: field.id,
|
|
975
|
-
selected
|
|
976
|
-
};
|
|
977
|
-
return warning ? {
|
|
978
|
-
ok: true,
|
|
979
|
-
patch,
|
|
980
|
-
warning
|
|
981
|
-
} : {
|
|
982
|
-
ok: true,
|
|
983
|
-
patch
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
function coerceToCheckboxes(field, rawValue) {
|
|
987
|
-
if (field.kind !== "checkboxes") return {
|
|
988
|
-
ok: false,
|
|
989
|
-
error: `Field '${field.id}' is not a checkboxes field`
|
|
990
|
-
};
|
|
991
|
-
if (rawValue === null) return {
|
|
992
|
-
ok: true,
|
|
993
|
-
patch: {
|
|
994
|
-
op: "set_checkboxes",
|
|
995
|
-
fieldId: field.id,
|
|
996
|
-
values: {}
|
|
997
|
-
}
|
|
998
|
-
};
|
|
999
|
-
if (!isPlainObject(rawValue)) return {
|
|
1000
|
-
ok: false,
|
|
1001
|
-
error: `checkboxes field '${field.id}' requires a Record<string, CheckboxValue>, got ${typeof rawValue}`
|
|
1002
|
-
};
|
|
1003
|
-
const validOptions = new Set(field.options.map((o) => o.id));
|
|
1004
|
-
const checkboxMode = field.checkboxMode;
|
|
1005
|
-
const values = {};
|
|
1006
|
-
const validValues = new Set(checkboxMode === "explicit" ? [
|
|
1007
|
-
"unfilled",
|
|
1008
|
-
"yes",
|
|
1009
|
-
"no"
|
|
1010
|
-
] : checkboxMode === "simple" ? ["todo", "done"] : [
|
|
1011
|
-
"todo",
|
|
1012
|
-
"done",
|
|
1013
|
-
"incomplete",
|
|
1014
|
-
"active",
|
|
1015
|
-
"na"
|
|
1016
|
-
]);
|
|
1017
|
-
for (const [optId, value] of Object.entries(rawValue)) {
|
|
1018
|
-
if (!validOptions.has(optId)) return {
|
|
1019
|
-
ok: false,
|
|
1020
|
-
error: `Invalid option '${optId}' for checkboxes field '${field.id}'. Valid options: ${Array.from(validOptions).join(", ")}`
|
|
1021
|
-
};
|
|
1022
|
-
if (typeof value !== "string" || !validValues.has(value)) return {
|
|
1023
|
-
ok: false,
|
|
1024
|
-
error: `Invalid checkbox value '${String(value)}' for option '${optId}' in field '${field.id}'. Valid values for ${checkboxMode} mode: ${Array.from(validValues).join(", ")}`
|
|
1025
|
-
};
|
|
1026
|
-
values[optId] = value;
|
|
1027
|
-
}
|
|
1028
|
-
return {
|
|
1029
|
-
ok: true,
|
|
1030
|
-
patch: {
|
|
1031
|
-
op: "set_checkboxes",
|
|
1032
|
-
fieldId: field.id,
|
|
1033
|
-
values
|
|
1034
|
-
}
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
function coerceToUrl(fieldId, rawValue) {
|
|
1038
|
-
if (rawValue === null) return {
|
|
1039
|
-
ok: true,
|
|
1040
|
-
patch: {
|
|
1041
|
-
op: "set_url",
|
|
1042
|
-
fieldId,
|
|
1043
|
-
value: null
|
|
1044
|
-
}
|
|
1045
|
-
};
|
|
1046
|
-
if (typeof rawValue === "string") return {
|
|
1047
|
-
ok: true,
|
|
1048
|
-
patch: {
|
|
1049
|
-
op: "set_url",
|
|
1050
|
-
fieldId,
|
|
1051
|
-
value: rawValue
|
|
1052
|
-
}
|
|
1053
|
-
};
|
|
1054
|
-
return {
|
|
1055
|
-
ok: false,
|
|
1056
|
-
error: `Cannot coerce ${typeof rawValue} to url for field '${fieldId}'`
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
function coerceToUrlList(fieldId, rawValue) {
|
|
1060
|
-
if (rawValue === null) return {
|
|
1061
|
-
ok: true,
|
|
1062
|
-
patch: {
|
|
1063
|
-
op: "set_url_list",
|
|
1064
|
-
fieldId,
|
|
1065
|
-
items: []
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
if (isStringArray(rawValue)) return {
|
|
1069
|
-
ok: true,
|
|
1070
|
-
patch: {
|
|
1071
|
-
op: "set_url_list",
|
|
1072
|
-
fieldId,
|
|
1073
|
-
items: rawValue
|
|
1074
|
-
}
|
|
1075
|
-
};
|
|
1076
|
-
if (typeof rawValue === "string") return {
|
|
1077
|
-
ok: true,
|
|
1078
|
-
patch: {
|
|
1079
|
-
op: "set_url_list",
|
|
1080
|
-
fieldId,
|
|
1081
|
-
items: [rawValue]
|
|
1082
|
-
},
|
|
1083
|
-
warning: `Coerced single string to array for field '${fieldId}'`
|
|
1084
|
-
};
|
|
1085
|
-
if (Array.isArray(rawValue)) {
|
|
1086
|
-
const items = [];
|
|
1087
|
-
for (const item of rawValue) if (typeof item === "string") items.push(item);
|
|
1088
|
-
else return {
|
|
1089
|
-
ok: false,
|
|
1090
|
-
error: `Cannot coerce array with non-string items to url_list for field '${fieldId}'`
|
|
1091
|
-
};
|
|
1092
|
-
return {
|
|
1093
|
-
ok: true,
|
|
1094
|
-
patch: {
|
|
1095
|
-
op: "set_url_list",
|
|
1096
|
-
fieldId,
|
|
1097
|
-
items
|
|
1098
|
-
}
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
return {
|
|
1102
|
-
ok: false,
|
|
1103
|
-
error: `Cannot coerce ${typeof rawValue} to url_list for field '${fieldId}'`
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Coerce a raw value to a Patch for a specific field.
|
|
1108
|
-
*/
|
|
1109
|
-
function coerceToFieldPatch(form, fieldId, rawValue) {
|
|
1110
|
-
const field = findFieldById(form, fieldId);
|
|
1111
|
-
if (!field) return {
|
|
1112
|
-
ok: false,
|
|
1113
|
-
error: `Field '${fieldId}' not found`
|
|
1114
|
-
};
|
|
1115
|
-
switch (field.kind) {
|
|
1116
|
-
case "string": return coerceToString(fieldId, rawValue);
|
|
1117
|
-
case "number": return coerceToNumber(fieldId, rawValue);
|
|
1118
|
-
case "string_list": return coerceToStringList(fieldId, rawValue);
|
|
1119
|
-
case "single_select": return coerceToSingleSelect(field, rawValue);
|
|
1120
|
-
case "multi_select": return coerceToMultiSelect(field, rawValue);
|
|
1121
|
-
case "checkboxes": return coerceToCheckboxes(field, rawValue);
|
|
1122
|
-
case "url": return coerceToUrl(fieldId, rawValue);
|
|
1123
|
-
case "url_list": return coerceToUrlList(fieldId, rawValue);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
/**
|
|
1127
|
-
* Coerce an entire InputContext to patches.
|
|
1128
|
-
*
|
|
1129
|
-
* Returns patches for valid entries, collects warnings for coercions,
|
|
1130
|
-
* and errors for invalid entries.
|
|
1131
|
-
*/
|
|
1132
|
-
function coerceInputContext(form, inputContext) {
|
|
1133
|
-
const patches = [];
|
|
1134
|
-
const warnings = [];
|
|
1135
|
-
const errors = [];
|
|
1136
|
-
for (const [fieldId, rawValue] of Object.entries(inputContext)) {
|
|
1137
|
-
if (rawValue === null) continue;
|
|
1138
|
-
const result = coerceToFieldPatch(form, fieldId, rawValue);
|
|
1139
|
-
if (result.ok) {
|
|
1140
|
-
patches.push(result.patch);
|
|
1141
|
-
if ("warning" in result && result.warning) warnings.push(result.warning);
|
|
1142
|
-
} else errors.push(result.error);
|
|
1143
|
-
}
|
|
1144
|
-
return {
|
|
1145
|
-
patches,
|
|
1146
|
-
warnings,
|
|
1147
|
-
errors
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
//#endregion
|
|
1152
|
-
//#region src/harness/harness.ts
|
|
1153
|
-
/**
|
|
1154
|
-
* Form Harness - Execution harness for form filling.
|
|
1155
|
-
*
|
|
1156
|
-
* Manages the step protocol for agent-driven form completion:
|
|
1157
|
-
* INIT -> STEP -> WAIT -> APPLY -> (repeat) -> COMPLETE
|
|
1158
|
-
*/
|
|
1159
|
-
const DEFAULT_CONFIG = {
|
|
1160
|
-
maxIssues: DEFAULT_MAX_ISSUES,
|
|
1161
|
-
maxPatchesPerTurn: DEFAULT_MAX_PATCHES_PER_TURN,
|
|
1162
|
-
maxTurns: DEFAULT_MAX_TURNS
|
|
1163
|
-
};
|
|
1164
|
-
/**
|
|
1165
|
-
* Form harness for managing agent-driven form filling.
|
|
1166
|
-
*/
|
|
1167
|
-
var FormHarness = class {
|
|
1168
|
-
form;
|
|
1169
|
-
config;
|
|
1170
|
-
state = "init";
|
|
1171
|
-
turnNumber = 0;
|
|
1172
|
-
turns = [];
|
|
1173
|
-
constructor(form, config = {}) {
|
|
1174
|
-
this.form = form;
|
|
1175
|
-
this.config = {
|
|
1176
|
-
...DEFAULT_CONFIG,
|
|
1177
|
-
...config
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
/**
|
|
1181
|
-
* Get the current harness state.
|
|
1182
|
-
*/
|
|
1183
|
-
getState() {
|
|
1184
|
-
return this.state;
|
|
1185
|
-
}
|
|
1186
|
-
/**
|
|
1187
|
-
* Get the current turn number.
|
|
1188
|
-
*/
|
|
1189
|
-
getTurnNumber() {
|
|
1190
|
-
return this.turnNumber;
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* Get the recorded session turns.
|
|
1194
|
-
*/
|
|
1195
|
-
getTurns() {
|
|
1196
|
-
return [...this.turns];
|
|
1197
|
-
}
|
|
1198
|
-
/**
|
|
1199
|
-
* Get the current form.
|
|
1200
|
-
*/
|
|
1201
|
-
getForm() {
|
|
1202
|
-
return this.form;
|
|
1203
|
-
}
|
|
1204
|
-
/**
|
|
1205
|
-
* Check if the harness has reached max turns.
|
|
1206
|
-
*
|
|
1207
|
-
* Returns true when we've completed all allowed turns. This happens when:
|
|
1208
|
-
* - turnNumber >= maxTurns AND we've already applied (state is "complete")
|
|
1209
|
-
* - OR turnNumber > maxTurns (we've exceeded the limit)
|
|
1210
|
-
*
|
|
1211
|
-
* This allows the harness loop to run N times when maxTurns=N by returning
|
|
1212
|
-
* false when we're at turn N but haven't applied yet (state is "wait").
|
|
1213
|
-
*/
|
|
1214
|
-
hasReachedMaxTurns() {
|
|
1215
|
-
if (this.turnNumber > this.config.maxTurns) return true;
|
|
1216
|
-
if (this.turnNumber === this.config.maxTurns && this.state === "complete") return true;
|
|
1217
|
-
return false;
|
|
1218
|
-
}
|
|
1219
|
-
/**
|
|
1220
|
-
* Perform a step - inspect the form and return current state.
|
|
1221
|
-
*
|
|
1222
|
-
* This transitions from INIT/WAIT -> STEP state.
|
|
1223
|
-
* Returns the current form state with prioritized issues.
|
|
1224
|
-
*
|
|
1225
|
-
* On first step with fillMode='overwrite', clears all target role fields
|
|
1226
|
-
* so they will be reported as needing to be filled.
|
|
1227
|
-
*/
|
|
1228
|
-
step() {
|
|
1229
|
-
if (this.state === "complete") throw new Error("Harness is complete - cannot step");
|
|
1230
|
-
if (this.state === "init" && this.config.fillMode === "overwrite") this.clearTargetRoleFields();
|
|
1231
|
-
this.turnNumber++;
|
|
1232
|
-
if (this.turnNumber > this.config.maxTurns) {
|
|
1233
|
-
this.state = "complete";
|
|
1234
|
-
const result$1 = inspect(this.form, { targetRoles: this.config.targetRoles });
|
|
1235
|
-
return {
|
|
1236
|
-
structureSummary: result$1.structureSummary,
|
|
1237
|
-
progressSummary: result$1.progressSummary,
|
|
1238
|
-
issues: [],
|
|
1239
|
-
stepBudget: 0,
|
|
1240
|
-
isComplete: result$1.isComplete,
|
|
1241
|
-
turnNumber: this.turnNumber
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
this.state = "step";
|
|
1245
|
-
const result = inspect(this.form, { targetRoles: this.config.targetRoles });
|
|
1246
|
-
const stepResult = this.computeStepResult(result);
|
|
1247
|
-
this.state = stepResult.issues.length === 0 ? "complete" : "wait";
|
|
1248
|
-
return stepResult;
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* Apply patches to the form.
|
|
1252
|
-
*
|
|
1253
|
-
* This transitions from WAIT -> STEP/COMPLETE state.
|
|
1254
|
-
* Records the turn in the session transcript.
|
|
1255
|
-
*
|
|
1256
|
-
* @param patches - Patches to apply
|
|
1257
|
-
* @param issues - Issues that were shown to the agent (for recording)
|
|
1258
|
-
* @param llmStats - Optional LLM stats for session logging
|
|
1259
|
-
* @returns StepResult after applying patches
|
|
1260
|
-
*/
|
|
1261
|
-
apply(patches, issues, llmStats) {
|
|
1262
|
-
if (this.state !== "wait") throw new Error(`Cannot apply in state: ${this.state}`);
|
|
1263
|
-
if (patches.length > this.config.maxPatchesPerTurn) throw new Error(`Too many patches: ${patches.length} > ${this.config.maxPatchesPerTurn}`);
|
|
1264
|
-
applyPatches(this.form, patches);
|
|
1265
|
-
const result = inspect(this.form, { targetRoles: this.config.targetRoles });
|
|
1266
|
-
const stepResult = this.computeStepResult(result);
|
|
1267
|
-
this.recordTurn(issues, patches, result, llmStats);
|
|
1268
|
-
if (stepResult.issues.length === 0 || this.turnNumber >= this.config.maxTurns) this.state = "complete";
|
|
1269
|
-
else this.state = "wait";
|
|
1270
|
-
return stepResult;
|
|
1271
|
-
}
|
|
1272
|
-
/**
|
|
1273
|
-
* Compute step result from inspect result.
|
|
1274
|
-
* Applies issue filtering and computes step budget.
|
|
1275
|
-
*/
|
|
1276
|
-
computeStepResult(result) {
|
|
1277
|
-
const limitedIssues = this.filterIssuesByScope(result.issues).slice(0, this.config.maxIssues);
|
|
1278
|
-
const stepBudget = Math.min(this.config.maxPatchesPerTurn, limitedIssues.length);
|
|
1279
|
-
return {
|
|
1280
|
-
structureSummary: result.structureSummary,
|
|
1281
|
-
progressSummary: result.progressSummary,
|
|
1282
|
-
issues: limitedIssues,
|
|
1283
|
-
stepBudget,
|
|
1284
|
-
isComplete: result.isComplete,
|
|
1285
|
-
turnNumber: this.turnNumber
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* Record a turn in the session transcript.
|
|
1290
|
-
*/
|
|
1291
|
-
recordTurn(issues, patches, result, llmStats) {
|
|
1292
|
-
const hash = sha256(serialize(this.form));
|
|
1293
|
-
const requiredIssueCount = result.issues.filter((i) => i.severity === "required").length;
|
|
1294
|
-
const turn = {
|
|
1295
|
-
turn: this.turnNumber,
|
|
1296
|
-
inspect: { issues },
|
|
1297
|
-
apply: { patches },
|
|
1298
|
-
after: {
|
|
1299
|
-
requiredIssueCount,
|
|
1300
|
-
markdownSha256: hash,
|
|
1301
|
-
answeredFieldCount: result.progressSummary.counts.answeredFields,
|
|
1302
|
-
skippedFieldCount: result.progressSummary.counts.skippedFields
|
|
1303
|
-
}
|
|
1304
|
-
};
|
|
1305
|
-
if (llmStats) turn.llm = llmStats;
|
|
1306
|
-
this.turns.push(turn);
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Check if the form is complete.
|
|
1310
|
-
*/
|
|
1311
|
-
isComplete() {
|
|
1312
|
-
return inspect(this.form, { targetRoles: this.config.targetRoles }).isComplete;
|
|
1313
|
-
}
|
|
1314
|
-
/**
|
|
1315
|
-
* Get the current markdown content of the form.
|
|
1316
|
-
*/
|
|
1317
|
-
getMarkdown() {
|
|
1318
|
-
return serialize(this.form);
|
|
1319
|
-
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Get the SHA256 hash of the current form markdown.
|
|
1322
|
-
*/
|
|
1323
|
-
getMarkdownHash() {
|
|
1324
|
-
return sha256(serialize(this.form));
|
|
1325
|
-
}
|
|
1326
|
-
/**
|
|
1327
|
-
* Filter issues based on maxFieldsPerTurn and maxGroupsPerTurn limits.
|
|
1328
|
-
*
|
|
1329
|
-
* Issues are processed in priority order. An issue is included if:
|
|
1330
|
-
* - Adding it doesn't exceed the field limit (for field/option scoped issues)
|
|
1331
|
-
* - Adding it doesn't exceed the group limit
|
|
1332
|
-
*
|
|
1333
|
-
* Form-level issues are always included.
|
|
1334
|
-
*/
|
|
1335
|
-
filterIssuesByScope(issues) {
|
|
1336
|
-
const maxFields = this.config.maxFieldsPerTurn;
|
|
1337
|
-
const maxGroups = this.config.maxGroupsPerTurn;
|
|
1338
|
-
if (maxFields === void 0 && maxGroups === void 0) return issues;
|
|
1339
|
-
const result = [];
|
|
1340
|
-
const seenFields = /* @__PURE__ */ new Set();
|
|
1341
|
-
const seenGroups = /* @__PURE__ */ new Set();
|
|
1342
|
-
for (const issue of issues) {
|
|
1343
|
-
if (issue.scope === "form") {
|
|
1344
|
-
result.push(issue);
|
|
1345
|
-
continue;
|
|
1346
|
-
}
|
|
1347
|
-
const fieldId = this.getFieldIdFromRef(issue.ref, issue.scope);
|
|
1348
|
-
const groupId = fieldId ? this.getGroupForField(fieldId) : void 0;
|
|
1349
|
-
if (maxFields !== void 0 && fieldId) {
|
|
1350
|
-
if (!seenFields.has(fieldId) && seenFields.size >= maxFields) continue;
|
|
1351
|
-
}
|
|
1352
|
-
if (maxGroups !== void 0 && groupId) {
|
|
1353
|
-
if (!seenGroups.has(groupId) && seenGroups.size >= maxGroups) continue;
|
|
1354
|
-
}
|
|
1355
|
-
result.push(issue);
|
|
1356
|
-
if (fieldId) seenFields.add(fieldId);
|
|
1357
|
-
if (groupId) seenGroups.add(groupId);
|
|
1358
|
-
}
|
|
1359
|
-
return result;
|
|
1360
|
-
}
|
|
1361
|
-
/**
|
|
1362
|
-
* Extract field ID from an issue ref.
|
|
1363
|
-
*/
|
|
1364
|
-
getFieldIdFromRef(ref, scope) {
|
|
1365
|
-
if (scope === "field") return ref;
|
|
1366
|
-
if (scope === "option") {
|
|
1367
|
-
const dotIndex = ref.indexOf(".");
|
|
1368
|
-
return dotIndex > 0 ? ref.slice(0, dotIndex) : void 0;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
/**
|
|
1372
|
-
* Get the parent group ID for a field.
|
|
1373
|
-
*/
|
|
1374
|
-
getGroupForField(fieldId) {
|
|
1375
|
-
const entry = this.form.idIndex.get(fieldId);
|
|
1376
|
-
if (!entry) return;
|
|
1377
|
-
if (entry.parentId) {
|
|
1378
|
-
if (this.form.idIndex.get(entry.parentId)?.kind === "group") return entry.parentId;
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
/**
|
|
1382
|
-
* Clear all fields that match the target roles.
|
|
1383
|
-
* Used when fillMode='overwrite' to re-fill already-filled fields.
|
|
1384
|
-
*/
|
|
1385
|
-
clearTargetRoleFields() {
|
|
1386
|
-
const targetRoles = this.config.targetRoles ?? [AGENT_ROLE];
|
|
1387
|
-
const clearPatches = getFieldsForRoles(this.form, targetRoles).map((field) => ({
|
|
1388
|
-
op: "clear_field",
|
|
1389
|
-
fieldId: field.id
|
|
1390
|
-
}));
|
|
1391
|
-
if (clearPatches.length > 0) applyPatches(this.form, clearPatches);
|
|
1392
|
-
}
|
|
1393
|
-
};
|
|
1394
|
-
/**
|
|
1395
|
-
* Create a new form harness.
|
|
1396
|
-
*
|
|
1397
|
-
* @param form - The parsed form to fill
|
|
1398
|
-
* @param config - Optional harness configuration
|
|
1399
|
-
* @returns A new FormHarness instance
|
|
1400
|
-
*/
|
|
1401
|
-
function createHarness(form, config) {
|
|
1402
|
-
return new FormHarness(form, config);
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
//#endregion
|
|
1406
|
-
//#region src/harness/mockAgent.ts
|
|
1407
|
-
/**
|
|
1408
|
-
* Mock agent that generates patches from a pre-filled form.
|
|
1409
|
-
*/
|
|
1410
|
-
var MockAgent = class {
|
|
1411
|
-
completedValues;
|
|
1412
|
-
fieldMap;
|
|
1413
|
-
/**
|
|
1414
|
-
* Create a mock agent from a completed form.
|
|
1415
|
-
*
|
|
1416
|
-
* @param completedForm - A fully-filled form to use as source of values
|
|
1417
|
-
*/
|
|
1418
|
-
constructor(completedForm) {
|
|
1419
|
-
this.completedValues = { ...completedForm.valuesByFieldId };
|
|
1420
|
-
this.fieldMap = /* @__PURE__ */ new Map();
|
|
1421
|
-
for (const group of completedForm.schema.groups) for (const field of group.children) this.fieldMap.set(field.id, field);
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* Generate patches from the completed mock to address issues.
|
|
1425
|
-
*
|
|
1426
|
-
* Processes issues in priority order, generating patches for
|
|
1427
|
-
* fields that have values in the completed mock. For fields with no
|
|
1428
|
-
* value (empty optional fields), generates skip_field patches.
|
|
1429
|
-
* Returns AgentResponse with patches but no stats (mock doesn't track LLM usage).
|
|
1430
|
-
*/
|
|
1431
|
-
async generatePatches(issues, _form, maxPatches) {
|
|
1432
|
-
const patches = [];
|
|
1433
|
-
const addressedFields = /* @__PURE__ */ new Set();
|
|
1434
|
-
for (const issue of issues) {
|
|
1435
|
-
if (patches.length >= maxPatches) break;
|
|
1436
|
-
if (issue.scope !== "field") continue;
|
|
1437
|
-
const fieldId = issue.ref;
|
|
1438
|
-
if (addressedFields.has(fieldId)) continue;
|
|
1439
|
-
const field = this.fieldMap.get(fieldId);
|
|
1440
|
-
if (!field) continue;
|
|
1441
|
-
const completedValue = this.completedValues[fieldId];
|
|
1442
|
-
if (!completedValue || !this.hasValue(completedValue)) {
|
|
1443
|
-
if (!field.required) {
|
|
1444
|
-
patches.push({
|
|
1445
|
-
op: "skip_field",
|
|
1446
|
-
fieldId,
|
|
1447
|
-
reason: "No value in mock form"
|
|
1448
|
-
});
|
|
1449
|
-
addressedFields.add(fieldId);
|
|
1450
|
-
}
|
|
1451
|
-
continue;
|
|
1452
|
-
}
|
|
1453
|
-
const patch = this.createPatch(fieldId, field, completedValue);
|
|
1454
|
-
if (patch) {
|
|
1455
|
-
patches.push(patch);
|
|
1456
|
-
addressedFields.add(fieldId);
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
return Promise.resolve({ patches });
|
|
1460
|
-
}
|
|
1461
|
-
/**
|
|
1462
|
-
* Check if a field value actually has content (not null/empty).
|
|
1463
|
-
*/
|
|
1464
|
-
hasValue(value) {
|
|
1465
|
-
switch (value.kind) {
|
|
1466
|
-
case "string": return value.value !== null && value.value !== "";
|
|
1467
|
-
case "number": return value.value !== null;
|
|
1468
|
-
case "string_list": return value.items.length > 0;
|
|
1469
|
-
case "single_select": return value.selected !== null;
|
|
1470
|
-
case "multi_select": return value.selected.length > 0;
|
|
1471
|
-
case "checkboxes": return true;
|
|
1472
|
-
case "url": return value.value !== null && value.value !== "";
|
|
1473
|
-
case "url_list": return value.items.length > 0;
|
|
1474
|
-
default: return false;
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
/**
|
|
1478
|
-
* Create a patch for a field based on its kind and completed value.
|
|
1479
|
-
*/
|
|
1480
|
-
createPatch(fieldId, field, value) {
|
|
1481
|
-
switch (field.kind) {
|
|
1482
|
-
case "string": return {
|
|
1483
|
-
op: "set_string",
|
|
1484
|
-
fieldId,
|
|
1485
|
-
value: value.value
|
|
1486
|
-
};
|
|
1487
|
-
case "number": return {
|
|
1488
|
-
op: "set_number",
|
|
1489
|
-
fieldId,
|
|
1490
|
-
value: value.value
|
|
1491
|
-
};
|
|
1492
|
-
case "string_list": return {
|
|
1493
|
-
op: "set_string_list",
|
|
1494
|
-
fieldId,
|
|
1495
|
-
items: value.items
|
|
1496
|
-
};
|
|
1497
|
-
case "single_select": return {
|
|
1498
|
-
op: "set_single_select",
|
|
1499
|
-
fieldId,
|
|
1500
|
-
selected: value.selected
|
|
1501
|
-
};
|
|
1502
|
-
case "multi_select": return {
|
|
1503
|
-
op: "set_multi_select",
|
|
1504
|
-
fieldId,
|
|
1505
|
-
selected: value.selected
|
|
1506
|
-
};
|
|
1507
|
-
case "checkboxes": return {
|
|
1508
|
-
op: "set_checkboxes",
|
|
1509
|
-
fieldId,
|
|
1510
|
-
values: value.values
|
|
1511
|
-
};
|
|
1512
|
-
case "url": return {
|
|
1513
|
-
op: "set_url",
|
|
1514
|
-
fieldId,
|
|
1515
|
-
value: value.value
|
|
1516
|
-
};
|
|
1517
|
-
case "url_list": return {
|
|
1518
|
-
op: "set_url_list",
|
|
1519
|
-
fieldId,
|
|
1520
|
-
items: value.items
|
|
1521
|
-
};
|
|
1522
|
-
default: return null;
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
};
|
|
1526
|
-
/**
|
|
1527
|
-
* Create a mock agent from a completed form.
|
|
1528
|
-
*
|
|
1529
|
-
* @param completedForm - A fully-filled form to use as source of values
|
|
1530
|
-
* @returns A new MockAgent instance
|
|
1531
|
-
*/
|
|
1532
|
-
function createMockAgent(completedForm) {
|
|
1533
|
-
return new MockAgent(completedForm);
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
//#endregion
|
|
1537
|
-
//#region src/harness/prompts.ts
|
|
1538
|
-
/**
|
|
1539
|
-
* Agent Prompts - Centralized prompt definitions for the live agent.
|
|
1540
|
-
*
|
|
1541
|
-
* All hardcoded prompts are defined here for easy review, modification,
|
|
1542
|
-
* and future configurability. This file serves as the single source of
|
|
1543
|
-
* truth for agent behavior instructions.
|
|
1544
|
-
*/
|
|
1545
|
-
/**
|
|
1546
|
-
* Default system prompt for the live agent.
|
|
1547
|
-
*
|
|
1548
|
-
* This is the base instruction set that defines the agent's core behavior
|
|
1549
|
-
* for form filling. It emphasizes accuracy over completeness and prohibits
|
|
1550
|
-
* fabrication of data.
|
|
1551
|
-
*/
|
|
1552
|
-
const DEFAULT_SYSTEM_PROMPT = `# Form Instructions
|
|
1553
|
-
Carefully research answers to all questions in the form, using all available tools you have.
|
|
1554
|
-
|
|
1555
|
-
Guidelines:
|
|
1556
|
-
1. Focus on required fields first (severity: "required"), then address optional fields (severity: "recommended")
|
|
1557
|
-
2. You MUST address ALL issues shown to you - both required AND recommended (optional)
|
|
1558
|
-
3. NEVER fabricate or guess information - only use data you can verify
|
|
1559
|
-
4. If you cannot find verifiable information for a field, use skip_field to mark it as skipped with a reason
|
|
1560
|
-
5. For string fields: use appropriate text from verified sources
|
|
1561
|
-
6. For number fields: use appropriate numeric values from verified sources
|
|
1562
|
-
7. For single_select: choose one valid option ID
|
|
1563
|
-
8. For multi_select: choose one or more valid option IDs
|
|
1564
|
-
9. For checkboxes: set appropriate states (done/todo for simple, yes/no for explicit)
|
|
1565
|
-
|
|
1566
|
-
CRITICAL: Accuracy is more important than completeness. Use skip_field when information cannot be verified.
|
|
1567
|
-
|
|
1568
|
-
Always use the generatePatches tool to submit your field values.
|
|
1569
|
-
`;
|
|
1570
|
-
/**
|
|
1571
|
-
* Web search instructions appended when web search tools are available.
|
|
1572
|
-
*
|
|
1573
|
-
* These instructions enforce that the agent must verify all information
|
|
1574
|
-
* through web search before filling fields.
|
|
1575
|
-
*/
|
|
1576
|
-
const WEB_SEARCH_INSTRUCTIONS = `# Web Search
|
|
1577
|
-
You have access to web search tools. You MUST use them to verify ALL information before filling fields.
|
|
1578
|
-
|
|
1579
|
-
Guidelines:
|
|
1580
|
-
1. Search for official sources (company websites, Crunchbase, LinkedIn, press releases)
|
|
1581
|
-
2. Cross-reference information across multiple sources when possible
|
|
1582
|
-
3. Only fill fields with data you found and verified through search
|
|
1583
|
-
4. If a search returns no results or uncertain information, use skip_field with a reason explaining what you searched for
|
|
1584
|
-
5. NEVER fill fields with guessed or assumed information
|
|
1585
|
-
`;
|
|
1586
|
-
/**
|
|
1587
|
-
* Description for the generatePatches tool.
|
|
1588
|
-
*
|
|
1589
|
-
* This tells the model how to use the patch submission tool.
|
|
1590
|
-
*/
|
|
1591
|
-
const GENERATE_PATCHES_TOOL_DESCRIPTION = "Generate patches to fill form fields. Each patch sets a field value. Use the field IDs from the issues list. Return patches for all issues you can address.";
|
|
1592
|
-
/**
|
|
1593
|
-
* Header for the issues section in the context prompt.
|
|
1594
|
-
*/
|
|
1595
|
-
const ISSUES_HEADER = "# Current Form Issues";
|
|
1596
|
-
/**
|
|
1597
|
-
* Template for the issues intro text.
|
|
1598
|
-
* @param maxPatches - Maximum number of patches to generate
|
|
1599
|
-
*/
|
|
1600
|
-
function getIssuesIntro(maxPatches) {
|
|
1601
|
-
return `You need to address up to ${maxPatches} issues. Here are the current issues:`;
|
|
1602
|
-
}
|
|
1603
|
-
/**
|
|
1604
|
-
* Instructions section for the context prompt.
|
|
1605
|
-
*
|
|
1606
|
-
* This explains the patch format for each field type.
|
|
1607
|
-
*/
|
|
1608
|
-
const PATCH_FORMAT_INSTRUCTIONS = `# Instructions
|
|
1609
|
-
|
|
1610
|
-
Use the generatePatches tool to submit patches for the fields above.
|
|
1611
|
-
Each patch should match the field type:
|
|
1612
|
-
- string: { op: "set_string", fieldId: "...", value: "..." }
|
|
1613
|
-
- number: { op: "set_number", fieldId: "...", value: 123 }
|
|
1614
|
-
- string_list: { op: "set_string_list", fieldId: "...", items: ["...", "..."] }
|
|
1615
|
-
- single_select: { op: "set_single_select", fieldId: "...", selected: "option_id" }
|
|
1616
|
-
- multi_select: { op: "set_multi_select", fieldId: "...", selected: ["opt1", "opt2"] }
|
|
1617
|
-
- checkboxes: { op: "set_checkboxes", fieldId: "...", values: { "opt1": "done", "opt2": "todo" } }
|
|
1618
|
-
- url: { op: "set_url", fieldId: "...", value: "https://..." }
|
|
1619
|
-
- url_list: { op: "set_url_list", fieldId: "...", items: ["https://...", "https://..."] }
|
|
1620
|
-
|
|
1621
|
-
If you cannot find verifiable information for a field, skip it:
|
|
1622
|
-
- skip: { op: "skip_field", fieldId: "...", reason: "Information not available" }`;
|
|
1623
|
-
/**
|
|
1624
|
-
* Section headers used when building the composed system prompt.
|
|
1625
|
-
*/
|
|
1626
|
-
const SECTION_HEADERS = {
|
|
1627
|
-
formInstructions: "# Form Instructions",
|
|
1628
|
-
roleInstructions: (role) => `# Instructions for ${role} role`,
|
|
1629
|
-
roleGuidance: "# Role guidance",
|
|
1630
|
-
fieldInstructions: "# Field-specific instructions",
|
|
1631
|
-
additionalContext: "# Additional Context"
|
|
1632
|
-
};
|
|
1633
|
-
|
|
1634
|
-
//#endregion
|
|
1635
|
-
//#region src/harness/liveAgent.ts
|
|
1636
|
-
/**
|
|
1637
|
-
* Live agent that uses an LLM to generate patches.
|
|
1638
|
-
*/
|
|
1639
|
-
var LiveAgent = class {
|
|
1640
|
-
model;
|
|
1641
|
-
maxStepsPerTurn;
|
|
1642
|
-
systemPromptAddition;
|
|
1643
|
-
targetRole;
|
|
1644
|
-
provider;
|
|
1645
|
-
enableWebSearch;
|
|
1646
|
-
webSearchTools = null;
|
|
1647
|
-
constructor(config) {
|
|
1648
|
-
this.model = config.model;
|
|
1649
|
-
this.maxStepsPerTurn = config.maxStepsPerTurn ?? 3;
|
|
1650
|
-
this.systemPromptAddition = config.systemPromptAddition;
|
|
1651
|
-
this.targetRole = config.targetRole ?? AGENT_ROLE;
|
|
1652
|
-
this.provider = config.provider;
|
|
1653
|
-
this.enableWebSearch = config.enableWebSearch ?? true;
|
|
1654
|
-
if (this.enableWebSearch && this.provider) this.webSearchTools = loadWebSearchTools(this.provider);
|
|
1655
|
-
}
|
|
1656
|
-
/**
|
|
1657
|
-
* Get list of available tool names for this agent.
|
|
1658
|
-
* Useful for logging what capabilities the agent has.
|
|
1659
|
-
*/
|
|
1660
|
-
getAvailableToolNames() {
|
|
1661
|
-
const tools = ["generatePatches"];
|
|
1662
|
-
if (this.webSearchTools) tools.push(...Object.keys(this.webSearchTools));
|
|
1663
|
-
return tools;
|
|
1664
|
-
}
|
|
1665
|
-
/**
|
|
1666
|
-
* Generate patches using the LLM.
|
|
1667
|
-
*
|
|
1668
|
-
* Each call is stateless - the full form context is provided fresh each turn.
|
|
1669
|
-
* The form itself carries all state (filled values, remaining issues).
|
|
1670
|
-
* Returns patches and per-turn stats for observability.
|
|
1671
|
-
*/
|
|
1672
|
-
async generatePatches(issues, form, maxPatches) {
|
|
1673
|
-
const contextPrompt = buildContextPrompt(issues, form, maxPatches);
|
|
1674
|
-
let systemPrompt = buildSystemPrompt(form, this.targetRole, issues);
|
|
1675
|
-
if (this.systemPromptAddition) systemPrompt += "\n\n# Additional Context\n" + this.systemPromptAddition;
|
|
1676
|
-
if (this.enableWebSearch && this.provider && !this.webSearchTools) this.webSearchTools = loadWebSearchTools(this.provider);
|
|
1677
|
-
if (this.webSearchTools && Object.keys(this.webSearchTools).length > 0) systemPrompt += "\n\n" + WEB_SEARCH_INSTRUCTIONS;
|
|
1678
|
-
const tools = {
|
|
1679
|
-
generatePatches: {
|
|
1680
|
-
description: GENERATE_PATCHES_TOOL_DESCRIPTION,
|
|
1681
|
-
inputSchema: zodSchema(z.object({ patches: z.array(PatchSchema).max(maxPatches).describe("Array of patches. Each patch sets a value for one field.") }))
|
|
1682
|
-
},
|
|
1683
|
-
...this.webSearchTools
|
|
1684
|
-
};
|
|
1685
|
-
const result = await generateText({
|
|
1686
|
-
model: this.model,
|
|
1687
|
-
system: systemPrompt,
|
|
1688
|
-
prompt: contextPrompt,
|
|
1689
|
-
tools,
|
|
1690
|
-
stopWhen: stepCountIs(this.maxStepsPerTurn)
|
|
1691
|
-
});
|
|
1692
|
-
const patches = [];
|
|
1693
|
-
const toolCallCounts = /* @__PURE__ */ new Map();
|
|
1694
|
-
for (const step of result.steps) for (const toolCall of step.toolCalls) {
|
|
1695
|
-
const count = toolCallCounts.get(toolCall.toolName) ?? 0;
|
|
1696
|
-
toolCallCounts.set(toolCall.toolName, count + 1);
|
|
1697
|
-
if (toolCall.toolName === "generatePatches" && "input" in toolCall) {
|
|
1698
|
-
const input = toolCall.input;
|
|
1699
|
-
patches.push(...input.patches);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
const toolCalls = [];
|
|
1703
|
-
for (const [name, count] of toolCallCounts) toolCalls.push({
|
|
1704
|
-
name,
|
|
1705
|
-
count
|
|
1706
|
-
});
|
|
1707
|
-
const requiredRemaining = issues.filter((i) => i.severity === "required").length;
|
|
1708
|
-
const optionalRemaining = issues.filter((i) => i.severity === "recommended").length;
|
|
1709
|
-
const stats = {
|
|
1710
|
-
inputTokens: result.usage?.inputTokens,
|
|
1711
|
-
outputTokens: result.usage?.outputTokens,
|
|
1712
|
-
toolCalls,
|
|
1713
|
-
formProgress: {
|
|
1714
|
-
answeredFields: Object.keys(form.valuesByFieldId).filter((id) => form.valuesByFieldId[id] !== null).length,
|
|
1715
|
-
skippedFields: Object.keys(form.skipsByFieldId ?? {}).filter((id) => form.skipsByFieldId?.[id]?.skipped).length,
|
|
1716
|
-
requiredRemaining,
|
|
1717
|
-
optionalRemaining
|
|
1718
|
-
},
|
|
1719
|
-
prompts: {
|
|
1720
|
-
system: systemPrompt,
|
|
1721
|
-
context: contextPrompt
|
|
1722
|
-
}
|
|
1723
|
-
};
|
|
1724
|
-
return {
|
|
1725
|
-
patches: patches.slice(0, maxPatches),
|
|
1726
|
-
stats
|
|
1727
|
-
};
|
|
1728
|
-
}
|
|
1729
|
-
};
|
|
1730
|
-
/**
|
|
1731
|
-
* Extract doc blocks of a specific tag type for a given ref.
|
|
1732
|
-
*/
|
|
1733
|
-
function getDocBlocks(docs, ref, tag) {
|
|
1734
|
-
return docs.filter((d) => d.ref === ref && d.tag === tag);
|
|
1735
|
-
}
|
|
1736
|
-
/**
|
|
1737
|
-
* Build a composed system prompt from form instructions.
|
|
1738
|
-
*
|
|
1739
|
-
* Instruction sources (later ones augment earlier):
|
|
1740
|
-
* 1. Base form instructions - Doc blocks with ref=formId and tag="instructions"
|
|
1741
|
-
* 2. Role-specific instructions - From form.metadata.roleInstructions[targetRole]
|
|
1742
|
-
* 3. Per-field instructions - Doc blocks with ref=fieldId and tag="instructions"
|
|
1743
|
-
* 4. System defaults - DEFAULT_ROLE_INSTRUCTIONS[targetRole] or DEFAULT_SYSTEM_PROMPT
|
|
1744
|
-
*/
|
|
1745
|
-
function buildSystemPrompt(form, targetRole, issues) {
|
|
1746
|
-
const sections = [];
|
|
1747
|
-
sections.push(DEFAULT_SYSTEM_PROMPT);
|
|
1748
|
-
const formInstructions = getDocBlocks(form.docs, form.schema.id, "instructions");
|
|
1749
|
-
if (formInstructions.length > 0) {
|
|
1750
|
-
sections.push("");
|
|
1751
|
-
sections.push(SECTION_HEADERS.formInstructions);
|
|
1752
|
-
for (const doc of formInstructions) sections.push(doc.bodyMarkdown.trim());
|
|
1753
|
-
}
|
|
1754
|
-
const roleInstructions = form.metadata?.roleInstructions?.[targetRole];
|
|
1755
|
-
if (roleInstructions) {
|
|
1756
|
-
sections.push("");
|
|
1757
|
-
sections.push(SECTION_HEADERS.roleInstructions(targetRole));
|
|
1758
|
-
sections.push(roleInstructions);
|
|
1759
|
-
} else {
|
|
1760
|
-
const defaultRoleInstr = DEFAULT_ROLE_INSTRUCTIONS[targetRole];
|
|
1761
|
-
if (defaultRoleInstr) {
|
|
1762
|
-
sections.push("");
|
|
1763
|
-
sections.push(SECTION_HEADERS.roleGuidance);
|
|
1764
|
-
sections.push(defaultRoleInstr);
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
const fieldIds = new Set(issues.filter((i) => i.scope === "field").map((i) => i.ref));
|
|
1768
|
-
const fieldInstructions = [];
|
|
1769
|
-
for (const fieldId of fieldIds) {
|
|
1770
|
-
const fieldDocs = getDocBlocks(form.docs, fieldId, "instructions");
|
|
1771
|
-
if (fieldDocs.length > 0) for (const doc of fieldDocs) fieldInstructions.push(`**${fieldId}:** ${doc.bodyMarkdown.trim()}`);
|
|
1772
|
-
}
|
|
1773
|
-
if (fieldInstructions.length > 0) {
|
|
1774
|
-
sections.push("");
|
|
1775
|
-
sections.push(SECTION_HEADERS.fieldInstructions);
|
|
1776
|
-
sections.push(...fieldInstructions);
|
|
1777
|
-
}
|
|
1778
|
-
return sections.join("\n");
|
|
1779
|
-
}
|
|
1780
|
-
/**
|
|
1781
|
-
* Build a context prompt with full form state and remaining issues.
|
|
1782
|
-
*
|
|
1783
|
-
* The form markdown shows the agent exactly what's been filled so far,
|
|
1784
|
-
* making each turn stateless - all state is in the form itself.
|
|
1785
|
-
*/
|
|
1786
|
-
function buildContextPrompt(issues, form, maxPatches) {
|
|
1787
|
-
const lines = [];
|
|
1788
|
-
lines.push("# Current Form State");
|
|
1789
|
-
lines.push("");
|
|
1790
|
-
lines.push("Below is the complete form with all currently filled values.");
|
|
1791
|
-
lines.push("Fields marked with `[ ]` or empty values still need to be filled.");
|
|
1792
|
-
lines.push("");
|
|
1793
|
-
lines.push("```markdown");
|
|
1794
|
-
lines.push(serialize(form));
|
|
1795
|
-
lines.push("```");
|
|
1796
|
-
lines.push("");
|
|
1797
|
-
lines.push(ISSUES_HEADER);
|
|
1798
|
-
lines.push("");
|
|
1799
|
-
lines.push(getIssuesIntro(maxPatches));
|
|
1800
|
-
lines.push("");
|
|
1801
|
-
for (const issue of issues) {
|
|
1802
|
-
lines.push(`- **${issue.ref}** (${issue.scope}): ${issue.message}`);
|
|
1803
|
-
lines.push(` Severity: ${issue.severity}, Priority: P${issue.priority}`);
|
|
1804
|
-
if (issue.scope === "field") {
|
|
1805
|
-
const field = findField(form, issue.ref);
|
|
1806
|
-
if (field) {
|
|
1807
|
-
lines.push(` Type: ${field.kind}`);
|
|
1808
|
-
if ("options" in field && field.options) {
|
|
1809
|
-
const optionIds = field.options.map((o) => o.id).join(", ");
|
|
1810
|
-
lines.push(` Options: ${optionIds}`);
|
|
1811
|
-
}
|
|
1812
|
-
if (field.kind === "checkboxes" && "checkboxMode" in field) lines.push(` Mode: ${field.checkboxMode ?? "multi"}`);
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
lines.push("");
|
|
1816
|
-
}
|
|
1817
|
-
lines.push(PATCH_FORMAT_INSTRUCTIONS);
|
|
1818
|
-
return lines.join("\n");
|
|
1819
|
-
}
|
|
1820
|
-
/**
|
|
1821
|
-
* Find a field by ID in the form.
|
|
1822
|
-
*/
|
|
1823
|
-
function findField(form, fieldId) {
|
|
1824
|
-
for (const group of form.schema.groups) for (const field of group.children) if (field.id === fieldId) return field;
|
|
1825
|
-
return null;
|
|
1826
|
-
}
|
|
1827
|
-
/**
|
|
1828
|
-
* Load web search tools for a provider.
|
|
1829
|
-
*
|
|
1830
|
-
* Uses statically imported provider modules to get web search tools.
|
|
1831
|
-
* Returns empty object if provider doesn't support web search.
|
|
1832
|
-
*/
|
|
1833
|
-
function loadWebSearchTools(provider) {
|
|
1834
|
-
if (!getWebSearchConfig(provider)) return {};
|
|
1835
|
-
switch (provider) {
|
|
1836
|
-
case "openai":
|
|
1837
|
-
if (openai.tools?.webSearch) return { web_search: openai.tools.webSearch({}) };
|
|
1838
|
-
if (openai.tools?.webSearchPreview) return { web_search: openai.tools.webSearchPreview({}) };
|
|
1839
|
-
return {};
|
|
1840
|
-
case "google":
|
|
1841
|
-
if (google.tools?.googleSearch) return { google_search: google.tools.googleSearch({}) };
|
|
1842
|
-
return {};
|
|
1843
|
-
case "xai": return {};
|
|
1844
|
-
default: return {};
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
/**
|
|
1848
|
-
* Create a live agent with the given configuration.
|
|
1849
|
-
*/
|
|
1850
|
-
function createLiveAgent(config) {
|
|
1851
|
-
return new LiveAgent(config);
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
//#endregion
|
|
1855
|
-
//#region src/harness/modelResolver.ts
|
|
1856
|
-
/**
|
|
1857
|
-
* Map of provider names to their npm package and env var.
|
|
1858
|
-
*/
|
|
1859
|
-
const PROVIDERS = {
|
|
1860
|
-
anthropic: {
|
|
1861
|
-
package: "@ai-sdk/anthropic",
|
|
1862
|
-
envVar: "ANTHROPIC_API_KEY",
|
|
1863
|
-
createFn: "createAnthropic"
|
|
1864
|
-
},
|
|
1865
|
-
openai: {
|
|
1866
|
-
package: "@ai-sdk/openai",
|
|
1867
|
-
envVar: "OPENAI_API_KEY",
|
|
1868
|
-
createFn: "createOpenAI"
|
|
1869
|
-
},
|
|
1870
|
-
google: {
|
|
1871
|
-
package: "@ai-sdk/google",
|
|
1872
|
-
envVar: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
1873
|
-
createFn: "createGoogleGenerativeAI"
|
|
1874
|
-
},
|
|
1875
|
-
xai: {
|
|
1876
|
-
package: "@ai-sdk/xai",
|
|
1877
|
-
envVar: "XAI_API_KEY",
|
|
1878
|
-
createFn: "createXai"
|
|
1879
|
-
},
|
|
1880
|
-
deepseek: {
|
|
1881
|
-
package: "@ai-sdk/deepseek",
|
|
1882
|
-
envVar: "DEEPSEEK_API_KEY",
|
|
1883
|
-
createFn: "createDeepSeek"
|
|
1884
|
-
}
|
|
1885
|
-
};
|
|
1886
|
-
/**
|
|
1887
|
-
* Parse a model ID string into provider and model components.
|
|
1888
|
-
*
|
|
1889
|
-
* @param modelIdString - Model ID in format `provider/model-id`
|
|
1890
|
-
* @returns Parsed model identifier
|
|
1891
|
-
* @throws Error if format is invalid
|
|
1892
|
-
*/
|
|
1893
|
-
function parseModelId(modelIdString) {
|
|
1894
|
-
const slashIndex = modelIdString.indexOf("/");
|
|
1895
|
-
if (slashIndex === -1) throw new Error(`Invalid model ID format: "${modelIdString}". Expected format: provider/model-id (e.g., anthropic/claude-sonnet-4-5)`);
|
|
1896
|
-
const provider = modelIdString.slice(0, slashIndex);
|
|
1897
|
-
const modelId = modelIdString.slice(slashIndex + 1);
|
|
1898
|
-
if (!provider || !modelId) throw new Error(`Invalid model ID format: "${modelIdString}". Both provider and model ID are required.`);
|
|
1899
|
-
const supportedProviders = Object.keys(PROVIDERS);
|
|
1900
|
-
if (!supportedProviders.includes(provider)) throw new Error(`Unknown provider: "${provider}". Supported providers: ${supportedProviders.join(", ")}`);
|
|
1901
|
-
return {
|
|
1902
|
-
provider,
|
|
1903
|
-
modelId
|
|
1904
|
-
};
|
|
1905
|
-
}
|
|
1906
|
-
/**
|
|
1907
|
-
* Resolve a model ID string to an AI SDK language model.
|
|
1908
|
-
*
|
|
1909
|
-
* Dynamically imports the provider package and creates the model instance.
|
|
1910
|
-
*
|
|
1911
|
-
* @param modelIdString - Model ID in format `provider/model-id`
|
|
1912
|
-
* @returns Resolved model with provider info
|
|
1913
|
-
* @throws Error if provider not installed or API key missing
|
|
1914
|
-
*/
|
|
1915
|
-
async function resolveModel(modelIdString) {
|
|
1916
|
-
const { provider, modelId } = parseModelId(modelIdString);
|
|
1917
|
-
const providerConfig = PROVIDERS[provider];
|
|
1918
|
-
const apiKey = process.env[providerConfig.envVar];
|
|
1919
|
-
if (!apiKey) throw new Error(`Missing API key for "${provider}" provider (model: ${modelIdString}).\nSet the ${providerConfig.envVar} environment variable or add it to your .env file.`);
|
|
1920
|
-
let providerModule;
|
|
1921
|
-
try {
|
|
1922
|
-
providerModule = await import(providerConfig.package);
|
|
1923
|
-
} catch (error) {
|
|
1924
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1925
|
-
if (message.includes("Cannot find module") || message.includes("ERR_MODULE_NOT_FOUND")) throw new Error(`Provider package not installed for model "${modelIdString}".\nInstall with: pnpm add ${providerConfig.package}`);
|
|
1926
|
-
throw error;
|
|
1927
|
-
}
|
|
1928
|
-
const createFn = providerModule[providerConfig.createFn];
|
|
1929
|
-
let model;
|
|
1930
|
-
if (createFn && typeof createFn === "function") model = createFn({ apiKey })(modelId);
|
|
1931
|
-
else {
|
|
1932
|
-
const providerFn = providerModule[provider];
|
|
1933
|
-
if (typeof providerFn !== "function") throw new Error(`Provider package "${providerConfig.package}" does not export expected function "${provider}" or "${providerConfig.createFn}"`);
|
|
1934
|
-
model = providerFn(modelId);
|
|
1935
|
-
}
|
|
1936
|
-
return {
|
|
1937
|
-
model,
|
|
1938
|
-
provider,
|
|
1939
|
-
modelId
|
|
1940
|
-
};
|
|
1941
|
-
}
|
|
1942
|
-
/**
|
|
1943
|
-
* Get list of supported provider names.
|
|
1944
|
-
*/
|
|
1945
|
-
function getProviderNames() {
|
|
1946
|
-
return Object.keys(PROVIDERS);
|
|
1947
|
-
}
|
|
1948
|
-
/**
|
|
1949
|
-
* Get provider info for display purposes.
|
|
1950
|
-
*/
|
|
1951
|
-
function getProviderInfo(provider) {
|
|
1952
|
-
const config = PROVIDERS[provider];
|
|
1953
|
-
return {
|
|
1954
|
-
package: config.package,
|
|
1955
|
-
envVar: config.envVar
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
//#endregion
|
|
1960
|
-
//#region src/harness/programmaticFill.ts
|
|
1961
|
-
function buildErrorResult(form, errors, warnings) {
|
|
1962
|
-
return {
|
|
1963
|
-
status: {
|
|
1964
|
-
ok: false,
|
|
1965
|
-
reason: "error",
|
|
1966
|
-
message: errors.join("; ")
|
|
1967
|
-
},
|
|
1968
|
-
markdown: serialize(form),
|
|
1969
|
-
values: { ...form.valuesByFieldId },
|
|
1970
|
-
form,
|
|
1971
|
-
turns: 0,
|
|
1972
|
-
totalPatches: 0,
|
|
1973
|
-
inputContextWarnings: warnings.length > 0 ? warnings : void 0
|
|
1974
|
-
};
|
|
1975
|
-
}
|
|
1976
|
-
function buildResult(form, turns, totalPatches, status, inputContextWarnings, remainingIssues) {
|
|
1977
|
-
const result = {
|
|
1978
|
-
status,
|
|
1979
|
-
markdown: serialize(form),
|
|
1980
|
-
values: { ...form.valuesByFieldId },
|
|
1981
|
-
form,
|
|
1982
|
-
turns,
|
|
1983
|
-
totalPatches
|
|
1984
|
-
};
|
|
1985
|
-
if (inputContextWarnings && inputContextWarnings.length > 0) result.inputContextWarnings = inputContextWarnings;
|
|
1986
|
-
if (remainingIssues && remainingIssues.length > 0) result.remainingIssues = remainingIssues.map((issue) => ({
|
|
1987
|
-
ref: issue.ref,
|
|
1988
|
-
message: issue.message,
|
|
1989
|
-
severity: issue.severity,
|
|
1990
|
-
priority: issue.priority
|
|
1991
|
-
}));
|
|
1992
|
-
return result;
|
|
1993
|
-
}
|
|
1994
|
-
/**
|
|
1995
|
-
* Fill a form using an LLM agent.
|
|
1996
|
-
*
|
|
1997
|
-
* This is the primary programmatic entry point for markform. It encapsulates
|
|
1998
|
-
* the harness loop with LiveAgent and provides a single-function call for
|
|
1999
|
-
* form filling.
|
|
2000
|
-
*
|
|
2001
|
-
* @param options - Fill options
|
|
2002
|
-
* @returns Fill result with status, values, and markdown
|
|
2003
|
-
*
|
|
2004
|
-
* @example
|
|
2005
|
-
* ```typescript
|
|
2006
|
-
* import { fillForm } from 'markform';
|
|
2007
|
-
*
|
|
2008
|
-
* const result = await fillForm({
|
|
2009
|
-
* form: formMarkdown,
|
|
2010
|
-
* model: 'anthropic/claude-sonnet-4-5',
|
|
2011
|
-
* inputContext: {
|
|
2012
|
-
* company_name: 'Apple Inc.',
|
|
2013
|
-
* },
|
|
2014
|
-
* systemPromptAddition: `
|
|
2015
|
-
* ## Additional Context
|
|
2016
|
-
* ${backgroundInfo}
|
|
2017
|
-
* `,
|
|
2018
|
-
* onTurnComplete: (progress) => {
|
|
2019
|
-
* console.log(`Turn ${progress.turnNumber}: ${progress.requiredIssuesRemaining} remaining`);
|
|
2020
|
-
* },
|
|
2021
|
-
* });
|
|
2022
|
-
*
|
|
2023
|
-
* if (result.status.ok) {
|
|
2024
|
-
* console.log('Values:', result.values);
|
|
2025
|
-
* }
|
|
2026
|
-
* ```
|
|
2027
|
-
*/
|
|
2028
|
-
async function fillForm(options) {
|
|
2029
|
-
let form;
|
|
2030
|
-
try {
|
|
2031
|
-
form = typeof options.form === "string" ? parseForm(options.form) : structuredClone(options.form);
|
|
2032
|
-
} catch (error) {
|
|
2033
|
-
return {
|
|
2034
|
-
status: {
|
|
2035
|
-
ok: false,
|
|
2036
|
-
reason: "error",
|
|
2037
|
-
message: `Form parse error: ${error instanceof Error ? error.message : String(error)}`
|
|
2038
|
-
},
|
|
2039
|
-
markdown: typeof options.form === "string" ? options.form : "",
|
|
2040
|
-
values: {},
|
|
2041
|
-
form: {
|
|
2042
|
-
schema: {
|
|
2043
|
-
id: "",
|
|
2044
|
-
groups: []
|
|
2045
|
-
},
|
|
2046
|
-
valuesByFieldId: {},
|
|
2047
|
-
skipsByFieldId: {},
|
|
2048
|
-
docs: [],
|
|
2049
|
-
orderIndex: [],
|
|
2050
|
-
idIndex: /* @__PURE__ */ new Map()
|
|
2051
|
-
},
|
|
2052
|
-
turns: 0,
|
|
2053
|
-
totalPatches: 0
|
|
2054
|
-
};
|
|
2055
|
-
}
|
|
2056
|
-
let model;
|
|
2057
|
-
let provider;
|
|
2058
|
-
if (!options._testAgent) try {
|
|
2059
|
-
if (typeof options.model === "string") {
|
|
2060
|
-
const resolved = await resolveModel(options.model);
|
|
2061
|
-
model = resolved.model;
|
|
2062
|
-
provider = resolved.provider;
|
|
2063
|
-
} else model = options.model;
|
|
2064
|
-
} catch (error) {
|
|
2065
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2066
|
-
return buildErrorResult(form, [`Model resolution error: ${message}`], []);
|
|
2067
|
-
}
|
|
2068
|
-
let totalPatches = 0;
|
|
2069
|
-
let inputContextWarnings = [];
|
|
2070
|
-
if (options.inputContext) {
|
|
2071
|
-
const coercionResult = coerceInputContext(form, options.inputContext);
|
|
2072
|
-
if (coercionResult.errors.length > 0) return buildErrorResult(form, coercionResult.errors, coercionResult.warnings);
|
|
2073
|
-
if (coercionResult.patches.length > 0) {
|
|
2074
|
-
applyPatches(form, coercionResult.patches);
|
|
2075
|
-
totalPatches = coercionResult.patches.length;
|
|
2076
|
-
}
|
|
2077
|
-
inputContextWarnings = coercionResult.warnings;
|
|
2078
|
-
}
|
|
2079
|
-
const maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
2080
|
-
const maxPatchesPerTurn = options.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN;
|
|
2081
|
-
const maxIssues = options.maxIssues ?? DEFAULT_MAX_ISSUES;
|
|
2082
|
-
const targetRoles = options.targetRoles ?? [AGENT_ROLE];
|
|
2083
|
-
const harness = createHarness(form, {
|
|
2084
|
-
maxTurns,
|
|
2085
|
-
maxPatchesPerTurn,
|
|
2086
|
-
maxIssues,
|
|
2087
|
-
targetRoles,
|
|
2088
|
-
fillMode: options.fillMode
|
|
2089
|
-
});
|
|
2090
|
-
const agent = options._testAgent ?? createLiveAgent({
|
|
2091
|
-
model,
|
|
2092
|
-
systemPromptAddition: options.systemPromptAddition,
|
|
2093
|
-
targetRole: targetRoles[0] ?? AGENT_ROLE,
|
|
2094
|
-
provider,
|
|
2095
|
-
enableWebSearch: true
|
|
2096
|
-
});
|
|
2097
|
-
let turnCount = 0;
|
|
2098
|
-
let stepResult = harness.step();
|
|
2099
|
-
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
2100
|
-
if (options.signal?.aborted) return buildResult(form, turnCount, totalPatches, {
|
|
2101
|
-
ok: false,
|
|
2102
|
-
reason: "cancelled"
|
|
2103
|
-
}, inputContextWarnings, stepResult.issues);
|
|
2104
|
-
const { patches, stats } = await agent.generatePatches(stepResult.issues, form, maxPatchesPerTurn);
|
|
2105
|
-
if (options.signal?.aborted) return buildResult(form, turnCount, totalPatches, {
|
|
2106
|
-
ok: false,
|
|
2107
|
-
reason: "cancelled"
|
|
2108
|
-
}, inputContextWarnings, stepResult.issues);
|
|
2109
|
-
let llmStats;
|
|
2110
|
-
if (stats) llmStats = {
|
|
2111
|
-
inputTokens: stats.inputTokens,
|
|
2112
|
-
outputTokens: stats.outputTokens,
|
|
2113
|
-
toolCalls: stats.toolCalls.length > 0 ? stats.toolCalls : void 0
|
|
2114
|
-
};
|
|
2115
|
-
stepResult = harness.apply(patches, stepResult.issues, llmStats);
|
|
2116
|
-
totalPatches += patches.length;
|
|
2117
|
-
turnCount++;
|
|
2118
|
-
if (options.onTurnComplete) try {
|
|
2119
|
-
const requiredIssues = stepResult.issues.filter((i) => i.severity === "required");
|
|
2120
|
-
options.onTurnComplete({
|
|
2121
|
-
turnNumber: turnCount,
|
|
2122
|
-
issuesShown: stepResult.issues.length,
|
|
2123
|
-
patchesApplied: patches.length,
|
|
2124
|
-
requiredIssuesRemaining: requiredIssues.length,
|
|
2125
|
-
isComplete: stepResult.isComplete,
|
|
2126
|
-
stats
|
|
2127
|
-
});
|
|
2128
|
-
} catch {}
|
|
2129
|
-
if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
|
|
2130
|
-
}
|
|
2131
|
-
if (stepResult.isComplete) return buildResult(form, turnCount, totalPatches, { ok: true }, inputContextWarnings);
|
|
2132
|
-
return buildResult(form, turnCount, totalPatches, {
|
|
2133
|
-
ok: false,
|
|
2134
|
-
reason: "max_turns",
|
|
2135
|
-
message: `Reached maximum turns (${maxTurns})`
|
|
2136
|
-
}, inputContextWarnings, stepResult.issues);
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
//#endregion
|
|
2140
|
-
//#region src/index.ts
|
|
2141
|
-
/**
|
|
2142
|
-
* Markform - Agent-friendly, human-readable, editable forms.
|
|
2143
|
-
*
|
|
2144
|
-
* This is the main library entry point that exports the core engine,
|
|
2145
|
-
* types, and utilities for working with .form.md files.
|
|
2146
|
-
*/
|
|
2147
|
-
/** Markform version. */
|
|
2148
|
-
const VERSION = "0.1.0";
|
|
2149
|
-
|
|
2150
|
-
//#endregion
|
|
2151
|
-
export { parseForm as _, resolveModel as a, createMockAgent as c, coerceInputContext as d, coerceToFieldPatch as f, ParseError as g, serializeSession as h, getProviderNames as i, FormHarness as l, parseSession as m, fillForm as n, createLiveAgent as o, findFieldById as p, getProviderInfo as r, MockAgent as s, VERSION as t, createHarness as u };
|