markform 0.1.0 → 0.1.1

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.
@@ -1,9 +1,11 @@
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";
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
2
  import { z } from "zod";
3
3
  import Markdoc from "@markdoc/markdoc";
4
4
  import YAML from "yaml";
5
- import { createHash } from "node:crypto";
5
+ import { sha256 } from "js-sha256";
6
6
  import { generateText, stepCountIs, zodSchema } from "ai";
7
+ import { openai } from "@ai-sdk/openai";
8
+ import { google } from "@ai-sdk/google";
7
9
 
8
10
  //#region src/engine/parse.ts
9
11
  /**
@@ -395,11 +397,17 @@ function parseCheckboxesField(node) {
395
397
  const approvalModeStr = getStringAttr(node, "approvalMode");
396
398
  let approvalMode = "none";
397
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;
398
406
  const field = {
399
407
  kind: "checkboxes",
400
408
  id,
401
409
  label,
402
- required: getBooleanAttr(node, "required") ?? false,
410
+ required,
403
411
  priority: getPriorityAttr(node),
404
412
  role: getStringAttr(node, "role") ?? AGENT_ROLE,
405
413
  checkboxMode,
@@ -423,6 +431,69 @@ function parseCheckboxesField(node) {
423
431
  };
424
432
  }
425
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
+ /**
426
497
  * Parse a field tag and return field schema and value.
427
498
  */
428
499
  function parseField(node) {
@@ -434,6 +505,8 @@ function parseField(node) {
434
505
  case "single-select": return parseSingleSelectField(node);
435
506
  case "multi-select": return parseMultiSelectField(node);
436
507
  case "checkboxes": return parseCheckboxesField(node);
508
+ case "url-field": return parseUrlField(node);
509
+ case "url-list": return parseUrlListField(node);
437
510
  default: return null;
438
511
  }
439
512
  }
@@ -579,6 +652,7 @@ function parseForm(markdown) {
579
652
  return {
580
653
  schema: formSchema,
581
654
  valuesByFieldId,
655
+ skipsByFieldId: {},
582
656
  docs,
583
657
  orderIndex,
584
658
  idIndex
@@ -960,6 +1034,75 @@ function coerceToCheckboxes(field, rawValue) {
960
1034
  }
961
1035
  };
962
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
+ }
963
1106
  /**
964
1107
  * Coerce a raw value to a Patch for a specific field.
965
1108
  */
@@ -976,10 +1119,8 @@ function coerceToFieldPatch(form, fieldId, rawValue) {
976
1119
  case "single_select": return coerceToSingleSelect(field, rawValue);
977
1120
  case "multi_select": return coerceToMultiSelect(field, rawValue);
978
1121
  case "checkboxes": return coerceToCheckboxes(field, rawValue);
979
- default: return {
980
- ok: false,
981
- error: `Unknown field kind: ${field.kind}`
982
- };
1122
+ case "url": return coerceToUrl(fieldId, rawValue);
1123
+ case "url_list": return coerceToUrlList(fieldId, rawValue);
983
1124
  }
984
1125
  }
985
1126
  /**
@@ -1062,9 +1203,18 @@ var FormHarness = class {
1062
1203
  }
1063
1204
  /**
1064
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").
1065
1213
  */
1066
1214
  hasReachedMaxTurns() {
1067
- return this.turnNumber >= this.config.maxTurns;
1215
+ if (this.turnNumber > this.config.maxTurns) return true;
1216
+ if (this.turnNumber === this.config.maxTurns && this.state === "complete") return true;
1217
+ return false;
1068
1218
  }
1069
1219
  /**
1070
1220
  * Perform a step - inspect the form and return current state.
@@ -1093,18 +1243,9 @@ var FormHarness = class {
1093
1243
  }
1094
1244
  this.state = "step";
1095
1245
  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
- };
1246
+ const stepResult = this.computeStepResult(result);
1247
+ this.state = stepResult.issues.length === 0 ? "complete" : "wait";
1248
+ return stepResult;
1108
1249
  }
1109
1250
  /**
1110
1251
  * Apply patches to the form.
@@ -1114,29 +1255,27 @@ var FormHarness = class {
1114
1255
  *
1115
1256
  * @param patches - Patches to apply
1116
1257
  * @param issues - Issues that were shown to the agent (for recording)
1258
+ * @param llmStats - Optional LLM stats for session logging
1117
1259
  * @returns StepResult after applying patches
1118
1260
  */
1119
- apply(patches, issues) {
1261
+ apply(patches, issues, llmStats) {
1120
1262
  if (this.state !== "wait") throw new Error(`Cannot apply in state: ${this.state}`);
1121
1263
  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";
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";
1139
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);
1140
1279
  return {
1141
1280
  structureSummary: result.structureSummary,
1142
1281
  progressSummary: result.progressSummary,
@@ -1147,6 +1286,26 @@ var FormHarness = class {
1147
1286
  };
1148
1287
  }
1149
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
+ /**
1150
1309
  * Check if the form is complete.
1151
1310
  */
1152
1311
  isComplete() {
@@ -1162,8 +1321,7 @@ var FormHarness = class {
1162
1321
  * Get the SHA256 hash of the current form markdown.
1163
1322
  */
1164
1323
  getMarkdownHash() {
1165
- const markdown = serialize(this.form);
1166
- return createHash("sha256").update(markdown).digest("hex");
1324
+ return sha256(serialize(this.form));
1167
1325
  }
1168
1326
  /**
1169
1327
  * Filter issues based on maxFieldsPerTurn and maxGroupsPerTurn limits.
@@ -1266,7 +1424,9 @@ var MockAgent = class {
1266
1424
  * Generate patches from the completed mock to address issues.
1267
1425
  *
1268
1426
  * Processes issues in priority order, generating patches for
1269
- * fields that have values in the completed mock.
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).
1270
1430
  */
1271
1431
  async generatePatches(issues, _form, maxPatches) {
1272
1432
  const patches = [];
@@ -1276,17 +1436,43 @@ var MockAgent = class {
1276
1436
  if (issue.scope !== "field") continue;
1277
1437
  const fieldId = issue.ref;
1278
1438
  if (addressedFields.has(fieldId)) continue;
1279
- const completedValue = this.completedValues[fieldId];
1280
- if (!completedValue) continue;
1281
1439
  const field = this.fieldMap.get(fieldId);
1282
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
+ }
1283
1453
  const patch = this.createPatch(fieldId, field, completedValue);
1284
1454
  if (patch) {
1285
1455
  patches.push(patch);
1286
1456
  addressedFields.add(fieldId);
1287
1457
  }
1288
1458
  }
1289
- return Promise.resolve(patches);
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
+ }
1290
1476
  }
1291
1477
  /**
1292
1478
  * Create a patch for a field based on its kind and completed value.
@@ -1323,6 +1509,16 @@ var MockAgent = class {
1323
1509
  fieldId,
1324
1510
  values: value.values
1325
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
+ };
1326
1522
  default: return null;
1327
1523
  }
1328
1524
  }
@@ -1337,6 +1533,104 @@ function createMockAgent(completedForm) {
1337
1533
  return new MockAgent(completedForm);
1338
1534
  }
1339
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
+
1340
1634
  //#endregion
1341
1635
  //#region src/harness/liveAgent.ts
1342
1636
  /**
@@ -1347,58 +1641,93 @@ var LiveAgent = class {
1347
1641
  maxStepsPerTurn;
1348
1642
  systemPromptAddition;
1349
1643
  targetRole;
1644
+ provider;
1645
+ enableWebSearch;
1646
+ webSearchTools = null;
1350
1647
  constructor(config) {
1351
1648
  this.model = config.model;
1352
1649
  this.maxStepsPerTurn = config.maxStepsPerTurn ?? 3;
1353
1650
  this.systemPromptAddition = config.systemPromptAddition;
1354
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;
1355
1664
  }
1356
1665
  /**
1357
1666
  * Generate patches using the LLM.
1358
1667
  *
1359
- * Calls the model with the current form state and issues,
1360
- * and extracts patches from the tool calls.
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.
1361
1671
  */
1362
1672
  async generatePatches(issues, form, maxPatches) {
1363
1673
  const contextPrompt = buildContextPrompt(issues, form, maxPatches);
1364
1674
  let systemPrompt = buildSystemPrompt(form, this.targetRole, issues);
1365
1675
  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.") }))
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
1369
1684
  };
1370
1685
  const result = await generateText({
1371
1686
  model: this.model,
1372
1687
  system: systemPrompt,
1373
1688
  prompt: contextPrompt,
1374
- tools: { generatePatches: generatePatchesTool },
1689
+ tools,
1375
1690
  stopWhen: stepCountIs(this.maxStepsPerTurn)
1376
1691
  });
1377
1692
  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);
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
+ }
1381
1701
  }
1382
- return patches.slice(0, maxPatches);
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
+ };
1383
1728
  }
1384
1729
  };
1385
1730
  /**
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
1731
  * Extract doc blocks of a specific tag type for a given ref.
1403
1732
  */
1404
1733
  function getDocBlocks(docs, ref, tag) {
@@ -1419,19 +1748,19 @@ function buildSystemPrompt(form, targetRole, issues) {
1419
1748
  const formInstructions = getDocBlocks(form.docs, form.schema.id, "instructions");
1420
1749
  if (formInstructions.length > 0) {
1421
1750
  sections.push("");
1422
- sections.push("# Form Instructions");
1751
+ sections.push(SECTION_HEADERS.formInstructions);
1423
1752
  for (const doc of formInstructions) sections.push(doc.bodyMarkdown.trim());
1424
1753
  }
1425
1754
  const roleInstructions = form.metadata?.roleInstructions?.[targetRole];
1426
1755
  if (roleInstructions) {
1427
1756
  sections.push("");
1428
- sections.push(`# Instructions for ${targetRole} role`);
1757
+ sections.push(SECTION_HEADERS.roleInstructions(targetRole));
1429
1758
  sections.push(roleInstructions);
1430
1759
  } else {
1431
1760
  const defaultRoleInstr = DEFAULT_ROLE_INSTRUCTIONS[targetRole];
1432
1761
  if (defaultRoleInstr) {
1433
1762
  sections.push("");
1434
- sections.push(`# Role guidance`);
1763
+ sections.push(SECTION_HEADERS.roleGuidance);
1435
1764
  sections.push(defaultRoleInstr);
1436
1765
  }
1437
1766
  }
@@ -1443,19 +1772,31 @@ function buildSystemPrompt(form, targetRole, issues) {
1443
1772
  }
1444
1773
  if (fieldInstructions.length > 0) {
1445
1774
  sections.push("");
1446
- sections.push("# Field-specific instructions");
1775
+ sections.push(SECTION_HEADERS.fieldInstructions);
1447
1776
  sections.push(...fieldInstructions);
1448
1777
  }
1449
1778
  return sections.join("\n");
1450
1779
  }
1451
1780
  /**
1452
- * Build a context prompt with issues and form information.
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.
1453
1785
  */
1454
1786
  function buildContextPrompt(issues, form, maxPatches) {
1455
1787
  const lines = [];
1456
- lines.push("# Current Form Issues");
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.");
1457
1792
  lines.push("");
1458
- lines.push(`You need to address up to ${maxPatches} issues. Here are the current issues:`);
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));
1459
1800
  lines.push("");
1460
1801
  for (const issue of issues) {
1461
1802
  lines.push(`- **${issue.ref}** (${issue.scope}): ${issue.message}`);
@@ -1473,16 +1814,7 @@ function buildContextPrompt(issues, form, maxPatches) {
1473
1814
  }
1474
1815
  lines.push("");
1475
1816
  }
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\" } }");
1817
+ lines.push(PATCH_FORMAT_INSTRUCTIONS);
1486
1818
  return lines.join("\n");
1487
1819
  }
1488
1820
  /**
@@ -1493,6 +1825,26 @@ function findField(form, fieldId) {
1493
1825
  return null;
1494
1826
  }
1495
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
+ /**
1496
1848
  * Create a live agent with the given configuration.
1497
1849
  */
1498
1850
  function createLiveAgent(config) {
@@ -1692,6 +2044,7 @@ async function fillForm(options) {
1692
2044
  groups: []
1693
2045
  },
1694
2046
  valuesByFieldId: {},
2047
+ skipsByFieldId: {},
1695
2048
  docs: [],
1696
2049
  orderIndex: [],
1697
2050
  idIndex: /* @__PURE__ */ new Map()
@@ -1701,9 +2054,13 @@ async function fillForm(options) {
1701
2054
  };
1702
2055
  }
1703
2056
  let model;
2057
+ let provider;
1704
2058
  if (!options._testAgent) try {
1705
- if (typeof options.model === "string") model = (await resolveModel(options.model)).model;
1706
- else model = options.model;
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;
1707
2064
  } catch (error) {
1708
2065
  const message = error instanceof Error ? error.message : String(error);
1709
2066
  return buildErrorResult(form, [`Model resolution error: ${message}`], []);
@@ -1733,7 +2090,9 @@ async function fillForm(options) {
1733
2090
  const agent = options._testAgent ?? createLiveAgent({
1734
2091
  model,
1735
2092
  systemPromptAddition: options.systemPromptAddition,
1736
- targetRole: targetRoles[0] ?? AGENT_ROLE
2093
+ targetRole: targetRoles[0] ?? AGENT_ROLE,
2094
+ provider,
2095
+ enableWebSearch: true
1737
2096
  });
1738
2097
  let turnCount = 0;
1739
2098
  let stepResult = harness.step();
@@ -1742,12 +2101,18 @@ async function fillForm(options) {
1742
2101
  ok: false,
1743
2102
  reason: "cancelled"
1744
2103
  }, inputContextWarnings, stepResult.issues);
1745
- const patches = await agent.generatePatches(stepResult.issues, form, maxPatchesPerTurn);
2104
+ const { patches, stats } = await agent.generatePatches(stepResult.issues, form, maxPatchesPerTurn);
1746
2105
  if (options.signal?.aborted) return buildResult(form, turnCount, totalPatches, {
1747
2106
  ok: false,
1748
2107
  reason: "cancelled"
1749
2108
  }, inputContextWarnings, stepResult.issues);
1750
- stepResult = harness.apply(patches, 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);
1751
2116
  totalPatches += patches.length;
1752
2117
  turnCount++;
1753
2118
  if (options.onTurnComplete) try {
@@ -1757,10 +2122,11 @@ async function fillForm(options) {
1757
2122
  issuesShown: stepResult.issues.length,
1758
2123
  patchesApplied: patches.length,
1759
2124
  requiredIssuesRemaining: requiredIssues.length,
1760
- isComplete: stepResult.isComplete
2125
+ isComplete: stepResult.isComplete,
2126
+ stats
1761
2127
  });
1762
2128
  } catch {}
1763
- if (!stepResult.isComplete) stepResult = harness.step();
2129
+ if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
1764
2130
  }
1765
2131
  if (stepResult.isComplete) return buildResult(form, turnCount, totalPatches, { ok: true }, inputContextWarnings);
1766
2132
  return buildResult(form, turnCount, totalPatches, {