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.
Files changed (36) hide show
  1. package/DOCS.md +546 -0
  2. package/README.md +340 -71
  3. package/SPEC.md +2779 -0
  4. package/dist/ai-sdk.d.mts +2 -2
  5. package/dist/ai-sdk.mjs +5 -3
  6. package/dist/{apply-BQdd-fdx.mjs → apply-00UmzDKL.mjs} +849 -730
  7. package/dist/bin.mjs +6 -3
  8. package/dist/{cli-pjOiHgCW.mjs → cli-D--Lel-e.mjs} +1374 -428
  9. package/dist/cli.mjs +6 -3
  10. package/dist/{coreTypes--6etkcwb.d.mts → coreTypes-BXhhz9Iq.d.mts} +1946 -794
  11. package/dist/coreTypes-Dful87E0.mjs +537 -0
  12. package/dist/index.d.mts +116 -19
  13. package/dist/index.mjs +5 -3
  14. package/dist/session-Bqnwi9wp.mjs +110 -0
  15. package/dist/session-DdAtY2Ni.mjs +4 -0
  16. package/dist/shared-D7gf27Tr.mjs +3 -0
  17. package/dist/shared-N_s1M-_K.mjs +176 -0
  18. package/dist/src-Dm8jZ5dl.mjs +7587 -0
  19. package/examples/celebrity-deep-research/celebrity-deep-research.form.md +912 -0
  20. package/examples/earnings-analysis/earnings-analysis.form.md +6 -1
  21. package/examples/earnings-analysis/earnings-analysis.valid.ts +119 -59
  22. package/examples/movie-research/movie-research-basic.form.md +164 -0
  23. package/examples/movie-research/movie-research-deep.form.md +486 -0
  24. package/examples/movie-research/movie-research-minimal.form.md +54 -0
  25. package/examples/simple/simple-mock-filled.form.md +17 -13
  26. package/examples/simple/simple-skipped-filled.form.md +32 -9
  27. package/examples/simple/simple-with-skips.session.yaml +102 -143
  28. package/examples/simple/simple.form.md +13 -13
  29. package/examples/simple/simple.session.yaml +80 -69
  30. package/examples/startup-deep-research/startup-deep-research.form.md +60 -8
  31. package/examples/startup-research/startup-research-mock-filled.form.md +1 -1
  32. package/examples/startup-research/startup-research.form.md +1 -1
  33. package/package.json +10 -14
  34. package/dist/src-Cs4_9lWP.mjs +0 -2151
  35. package/examples/political-research/political-research.form.md +0 -233
  36. package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
@@ -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 };