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