@sensigo/realm-cli 0.28.0 → 0.30.0

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.
@@ -27,6 +27,14 @@ export declare abstract class LlmProvider {
27
27
  * Extend this class if your provider can drive tool-enabled workflow steps.
28
28
  */
29
29
  export declare abstract class ToolCapableLlmProvider extends LlmProvider {
30
+ /**
31
+ * issue #217 provider contract: every executor invocation MUST produce an entry in
32
+ * `toolCalls`, including failed/timed-out calls — the drive's schema-repair gate relies on
33
+ * `toolCalls.length === 0 ⇒ executor never invoked`. A custom `--provider-module` that violates
34
+ * this (e.g. swallows a failed call without recording it) is a trusted-injector residual — the
35
+ * repair gate would then wrongly treat a tool-using attempt as tool-free and repair it (cross-
36
+ * ref #224).
37
+ */
30
38
  abstract callStepWithTools(prompt: string, tools: ToolDefinition[], executor: ToolExecutor, options: {
31
39
  inputSchema?: Record<string, unknown>;
32
40
  maxToolCalls?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"llm-provider.d.ts","sourceRoot":"","sources":["../../../src/agent/providers/llm-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAElG;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,8BAAsB,WAAW;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,CACf,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,wBAAwB,CAAC,EAAE,MAAM,GAChC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEnC,6DAA6D;IAC7D,YAAY,IAAI,oBAAoB;CAGrC;AAED;;;GAGG;AACH,8BAAsB,sBAAuB,SAAQ,WAAW;IAC9D,QAAQ,CAAC,iBAAiB,CACxB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,cAAc,EAAE,EACvB,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE;QACP,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACtC,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,wBAAwB,CAAC,EAAE,MAAM,CAAC;KACnC,GACA,OAAO,CAAC,mBAAmB,CAAC;CAChC;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,IAAI,sBAAsB,CAEvF;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAC;AAElD;;;GAGG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,CAAC,CAmCtB"}
1
+ {"version":3,"file":"llm-provider.d.ts","sourceRoot":"","sources":["../../../src/agent/providers/llm-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAElG;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,8BAAsB,WAAW;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,CACf,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,wBAAwB,CAAC,EAAE,MAAM,GAChC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEnC,6DAA6D;IAC7D,YAAY,IAAI,oBAAoB;CAGrC;AAED;;;GAGG;AACH,8BAAsB,sBAAuB,SAAQ,WAAW;IAC9D;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,CACxB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,cAAc,EAAE,EACvB,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE;QACP,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACtC,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,wBAAwB,CAAC,EAAE,MAAM,CAAC;KACnC,GACA,OAAO,CAAC,mBAAmB,CAAC;CAChC;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,IAAI,sBAAsB,CAEvF;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAC;AAElD;;;GAGG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,CAAC,CAmCtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"llm-provider.js","sourceRoot":"","sources":["../../../src/agent/providers/llm-provider.ts"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,MAAM,OAAgB,WAAW;IAQ/B,6DAA6D;IAC7D,YAAY;QACV,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC7B,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAgB,sBAAuB,SAAQ,WAAW;CAa/D;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,QAAqB;IACjD,OAAO,QAAQ,YAAY,sBAAsB,CAAC;AACpD,CAAC;AAID;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAsC,EACtC,SAA6B,EAC7B,WAAoB;IAEpB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,SAAS,CAAC;IAC9D,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,KAAK,SAAS,CAAC;IAEpE,IAAI,CAAC,SAAS,IAAI,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,+EAA+E,CAChF,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,YAAY,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IAEtE,IAAI,WAAW,KAAK,SAAS,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CACb,wFAAwF;YACtF,wFAAwF,CAC3F,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,8EAA8E;QAC9E,sEAAsE;QACtE,+EAA+E;QAC/E,qDAAqD;QACrD,MAAM,gBAAgB,GAAG,WAAW,CAAC;QACrC,IAAI,SAAS,KAAK,SAAS,IAAI,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAChE,MAAM,EAAE,uBAAuB,EAAE,GAAG,MAAM,MAAM,CAAC,gCAAgC,CAAC,CAAC;YACnF,OAAO,IAAI,uBAAuB,CAAC,SAAS,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAChE,OAAO,IAAI,cAAc,CAAC,SAAS,IAAI,QAAQ,EAAE,WAAW,CAAC,CAAC;IAChE,CAAC;SAAM,CAAC;QACN,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;QACtE,OAAO,IAAI,iBAAiB,CAAC,SAAS,IAAI,mBAAmB,CAAC,CAAC;IACjE,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"llm-provider.js","sourceRoot":"","sources":["../../../src/agent/providers/llm-provider.ts"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,MAAM,OAAgB,WAAW;IAQ/B,6DAA6D;IAC7D,YAAY;QACV,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC7B,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAgB,sBAAuB,SAAQ,WAAW;CAqB/D;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,QAAqB;IACjD,OAAO,QAAQ,YAAY,sBAAsB,CAAC;AACpD,CAAC;AAID;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAsC,EACtC,SAA6B,EAC7B,WAAoB;IAEpB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,SAAS,CAAC;IAC9D,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,KAAK,SAAS,CAAC;IAEpE,IAAI,CAAC,SAAS,IAAI,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,+EAA+E,CAChF,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,YAAY,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IAEtE,IAAI,WAAW,KAAK,SAAS,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CACb,wFAAwF;YACtF,wFAAwF,CAC3F,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,8EAA8E;QAC9E,sEAAsE;QACtE,+EAA+E;QAC/E,qDAAqD;QACrD,MAAM,gBAAgB,GAAG,WAAW,CAAC;QACrC,IAAI,SAAS,KAAK,SAAS,IAAI,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAChE,MAAM,EAAE,uBAAuB,EAAE,GAAG,MAAM,MAAM,CAAC,gCAAgC,CAAC,CAAC;YACnF,OAAO,IAAI,uBAAuB,CAAC,SAAS,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAChE,OAAO,IAAI,cAAc,CAAC,SAAS,IAAI,QAAQ,EAAE,WAAW,CAAC,CAAC;IAChE,CAAC;SAAM,CAAC;QACN,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;QACtE,OAAO,IAAI,iBAAiB,CAAC,SAAS,IAAI,mBAAmB,CAAC,CAAC;IACjE,CAAC;AACH,CAAC"}
@@ -42,6 +42,16 @@ export interface AgentDeps {
42
42
  * `crypto.randomUUID()` is minted independently for EVERY step-attempt in the loop below.
43
43
  */
44
44
  mintWriterNonce?: boolean;
45
+ /**
46
+ * Budget for issue #217's in-drive schema-feedback repair loop: how many times the drive
47
+ * re-prompts an `execution: 'agent'` step after its output/input is rejected by
48
+ * output_schema/input_schema validation, appending the validator's errors to the prompt.
49
+ * Threaded exactly like `mintWriterNonce` above (agent.ts's `--schema-retries` flag → both
50
+ * runAgent call sites → this field). Default `2` when omitted (mirrors the CLI flag's own
51
+ * default) — `0` disables the loop entirely, reproducing today's single-attempt behavior
52
+ * byte-for-byte.
53
+ */
54
+ schemaRetries?: number;
45
55
  }
46
56
  export interface AgentRunOptions {
47
57
  /** Path to workflow.yaml file. Required when definition is not provided. */
@@ -1 +1 @@
1
- {"version":3,"file":"run-agent.d.ts","sourceRoot":"","sources":["../../src/agent/run-agent.ts"],"names":[],"mappings":"AAIA,OAAO,EAUL,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,KAAK,EAAE,SAAS,EAAgC,MAAM,yBAAyB,CAAC;AAGvF,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEpD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,QAAQ,CAAC;IAChB,aAAa,EAAE,iBAAiB,CAAC;IACjC,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,SAAS,CAAC;IACnF;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAcD,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,8GAA8G;IAC9G,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA0ED;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAqajG"}
1
+ {"version":3,"file":"run-agent.d.ts","sourceRoot":"","sources":["../../src/agent/run-agent.ts"],"names":[],"mappings":"AAIA,OAAO,EAYL,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EAEtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,KAAK,EAAE,SAAS,EAAgC,MAAM,yBAAyB,CAAC;AAGvF,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEpD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,QAAQ,CAAC;IAChB,aAAa,EAAE,iBAAiB,CAAC;IACjC,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,SAAS,CAAC;IACnF;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAcD,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,8GAA8G;IAC9G,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA6FD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA+iBjG"}
@@ -2,7 +2,7 @@
2
2
  // Exports runAgent(), AgentDeps, AgentRunOptions, and AgentRunResult.
3
3
  // All Slack-specific gate notification logic lives in slack-gate-notifier.ts.
4
4
  import { join } from 'node:path';
5
- import { loadWorkflowFromFile, findEligibleSteps, classifyInProgressClaims, executeChain, buildNextActions, findCapabilityBlockedSteps, unmetCapabilities, capabilityWarning, WorkflowError, } from '@sensigo/realm';
5
+ import { loadWorkflowFromFile, findEligibleSteps, classifyInProgressClaims, executeChain, buildNextActions, findCapabilityBlockedSteps, unmetCapabilities, capabilityWarning, buildFailedAttemptRecord, WorkflowError, DEFAULT_VALIDATION_EXHAUSTION_THRESHOLD, } from '@sensigo/realm';
6
6
  import { setAdditionalRedactionValues } from './providers/agent-utils.js';
7
7
  import { isToolCapable } from './providers/llm-provider.js';
8
8
  import { McpClient as McpClientImpl } from './mcp/mcp-client.js';
@@ -79,6 +79,24 @@ async function pollUntilGateResolved(store, runId, gateId, intervalMs, signal) {
79
79
  break;
80
80
  }
81
81
  }
82
+ /**
83
+ * Renders one whitelisted Ajv-error summary entry (issue #217, core's
84
+ * `buildFailedAttemptRecord(...).validation_error_summary`) as a single human-readable line for
85
+ * the schema-repair prompt trailer. Key NAMES only — the summary entry already strips value
86
+ * echoes (see failed-attempt-record.ts's `summarizeAjvErrors`), so nothing rendered here can leak
87
+ * a submitted or schema-declared VALUE (e.g. `enum.allowedValues`).
88
+ */
89
+ function renderValidationSummaryEntry(entry) {
90
+ const path = entry.instancePath !== '' ? entry.instancePath : '(root)';
91
+ let line = `- ${path}: ${entry.message} [${entry.keyword}]`;
92
+ if (entry.additional_property !== undefined) {
93
+ line += ` (additional property: '${entry.additional_property}')`;
94
+ }
95
+ if (entry.missing_property !== undefined) {
96
+ line += ` (missing property: '${entry.missing_property}')`;
97
+ }
98
+ return line;
99
+ }
82
100
  /**
83
101
  * Runs a workflow to completion using the provided dependencies.
84
102
  * Returns 'completed' when the run finishes normally; 'failed' otherwise.
@@ -92,6 +110,9 @@ export async function runAgent(deps, options) {
92
110
  // tool-execution errors serialized by the provider loop get these values masked
93
111
  // alongside process.env values.
94
112
  setAdditionalRedactionValues(deps.redactionValues ?? []);
113
+ // issue #217: resolved once per run (not per call, unlike shouldMintWriterNonce — there is no
114
+ // env-var strict-flip counterpart here). `0` disables the repair loop entirely.
115
+ const schemaRetries = deps.schemaRetries ?? 2;
95
116
  // Load or use provided definition.
96
117
  const definition = options.definition !== undefined
97
118
  ? options.definition
@@ -205,166 +226,278 @@ export async function runAgent(deps, options) {
205
226
  }
206
227
  const stepName = eligible[0];
207
228
  const stepDef = definition.steps[stepName];
208
- let stepInput;
229
+ // issue #217: the in-drive schema-feedback repair loop. `stepInput`/`toolCallsForMeta`/
230
+ // `result` are re-assigned on every attempt inside the `for` loop below; `repairsUsed`/
231
+ // `lastRejection` persist ACROSS attempts within this one step, and are fresh (0/undefined)
232
+ // for every new step. The loop body is exactly the former single-pass step-execution region
233
+ // (the agent/auto branch bodies + the executeChain call) — an auto step's
234
+ // `execution !== 'agent'` means the repair gate's conjunct (iii) can never hold for it, so
235
+ // it structurally can never iterate more than once: the loop is a no-op wrapper for every
236
+ // pre-existing (non-repair) case.
237
+ let stepInput = {};
209
238
  let toolCallsForMeta;
210
- if (stepDef.execution === 'agent') {
211
- // Resolve template-expanded prompt via buildNextActions so {{ context.resources.* }}
212
- // references are substituted before the LLM call.
213
- const nextActions = buildNextActions(definition, currentRun);
214
- const nextAction = nextActions.find((a) => a.instruction !== null &&
215
- a.instruction.call_with['command'] === stepName) ?? nextActions[0];
216
- const prompt = nextAction?.prompt ?? stepDef.description;
217
- // #robust-anthropic-provider Part 1: route the schema the ENGINE validates output against
218
- // (output_schema, execution-loop.ts validateOutputSchema) ahead of the execute_step-param
219
- // schema (input_schema / nextAction.input_schema) the provider was fed until now. Both-
220
- // declared-and-divergent degrades to a clean recoverable VALIDATION_*_SCHEMA error downstream,
221
- // not a parse-strand — see the Part 6 loader warning for the authoring-time signal.
222
- const inputSchema = stepDef.output_schema ??
223
- nextAction?.input_schema ??
224
- stepDef.input_schema;
225
- const agentProfileInstructions = stepDef.agent_profile !== undefined
226
- ? definition.resolved_profiles?.[stepDef.agent_profile]?.content
227
- : undefined;
228
- const descPreview = stepDef.description.slice(0, 80);
229
- console.log(`\n→ [agent] ${stepName}`);
230
- console.log(` ${descPreview}${stepDef.description.length > 80 ? '…' : ''}`);
231
- if (stepDef.tools && stepDef.tools.length > 0 && mcpClient) {
232
- // Tools path: build tool definitions, call callStepWithTools.
233
- const byServer = new Map();
234
- for (const entry of stepDef.tools) {
235
- const [serverId, toolName] = entry.split(':');
236
- if (!byServer.has(serverId))
237
- byServer.set(serverId, []);
238
- byServer.get(serverId).push(toolName);
239
+ let result;
240
+ let repairsUsed = 0;
241
+ let lastRejection;
242
+ for (;;) {
243
+ toolCallsForMeta = undefined;
244
+ // issue #217 conjunct (vi) ground truth — captured FRESH at the top of EVERY attempt
245
+ // (including the first), never once per step: a per-step capture is stale across the
246
+ // whole provider LLM call, so any concurrent writer (a second drive, a gate `respond`, a
247
+ // parallel-step settle) landing during that call would silently forfeit a legitimate
248
+ // repair. See the repair-gate comment below for the full discriminator rationale. Cost:
249
+ // one extra store read per attempt accepted.
250
+ const versionBeforeAttempt = (await deps.store.get(runId)).version;
251
+ if (stepDef.execution === 'agent') {
252
+ // Resolve template-expanded prompt via buildNextActions so {{ context.resources.* }}
253
+ // references are substituted before the LLM call. Pure w.r.t. `definition`/`currentRun`,
254
+ // both unchanged across repair attempts — a rejected attempt no longer leaves
255
+ // `currentRun` itself stale relative to what's persisted (issue #220: countRejection DOES
256
+ // persist a bounded rejection counter on a counted rejection — "nothing is ever
257
+ // persisted on a rejected attempt" is FALSE as of #220), but `currentRun`/`definition`
258
+ // are still safe to recompute per iteration here regardless, since neither is read from
259
+ // again until the NEXT step (this step's own next_actions/prompt derivation never
260
+ // consults `validation_rejections`).
261
+ const nextActions = buildNextActions(definition, currentRun);
262
+ const nextAction = nextActions.find((a) => a.instruction !== null &&
263
+ a.instruction.call_with['command'] === stepName) ?? nextActions[0];
264
+ // PRISTINE original prompt — never mutated across repair attempts. The prompt actually
265
+ // sent to the provider (`promptForAttempt` below) is always derived FRESH from this,
266
+ // plus at most the LATEST rejection's feedback — never accumulated, never stale.
267
+ const prompt = nextAction?.prompt ?? stepDef.description;
268
+ const promptForAttempt = lastRejection !== undefined
269
+ ? `${prompt}\n\nYour previous output was rejected by the ${lastRejection.kind} schema validator:\n${lastRejection.summary}\nEmit corrected JSON only, matching the schema exactly.`
270
+ : prompt;
271
+ // #robust-anthropic-provider Part 1: route the schema the ENGINE validates output against
272
+ // (output_schema, execution-loop.ts validateOutputSchema) ahead of the execute_step-param
273
+ // schema (input_schema / nextAction.input_schema) the provider was fed until now. Both-
274
+ // declared-and-divergent degrades to a clean recoverable VALIDATION_*_SCHEMA error downstream,
275
+ // not a parse-strand — see the Part 6 loader warning for the authoring-time signal.
276
+ const inputSchema = stepDef.output_schema ??
277
+ nextAction?.input_schema ??
278
+ stepDef.input_schema;
279
+ const agentProfileInstructions = stepDef.agent_profile !== undefined
280
+ ? definition.resolved_profiles?.[stepDef.agent_profile]?.content
281
+ : undefined;
282
+ if (repairsUsed === 0) {
283
+ const descPreview = stepDef.description.slice(0, 80);
284
+ console.log(`\n→ [agent] ${stepName}`);
285
+ console.log(` ${descPreview}${stepDef.description.length > 80 ? '…' : ''}`);
286
+ // issue #220 deliverable 7 — drive-time coherence warn: once per step (gated on the
287
+ // same `repairsUsed === 0` this banner uses), warn when the repair budget itself
288
+ // (schemaRetries + 1 attempts) exceeds the engine's own exhaustion threshold for this
289
+ // step — operator intent would be silently truncated mid-loop (the drive keeps
290
+ // repairing past the point the engine terminalizes the step with VALIDATION_EXHAUSTED).
291
+ const exhaustionThreshold = stepDef.validation_exhaustion?.threshold ?? DEFAULT_VALIDATION_EXHAUSTION_THRESHOLD;
292
+ if (schemaRetries + 1 > exhaustionThreshold) {
293
+ console.error(` ⚠ --schema-retries ${schemaRetries} (repair budget ${schemaRetries + 1} attempts) ` +
294
+ `exceeds step '${stepName}''s validation-exhaustion threshold ` +
295
+ `(${exhaustionThreshold}) — the engine will terminalize this step before the ` +
296
+ `repair loop's own budget is exhausted.`);
297
+ }
239
298
  }
240
- let toolsResult;
241
- try {
242
- const toolDefs = [];
243
- const barenameOwner = new Map(); // bareName serverId of first registration
244
- for (const [serverId, allowList] of byServer) {
245
- const mcpTools = await mcpClient.getTools(serverId, allowList);
246
- const returnedNames = new Set(mcpTools.map((t) => t.name));
247
- for (const name of allowList) {
248
- if (!returnedNames.has(name)) {
249
- throw new WorkflowError(`Step '${stepName}' declares tool '${serverId}:${name}' which is not exposed by MCP server '${serverId}'. ` +
250
- `Check the tool name against the server's published tool list.`, {
251
- code: 'MCP_TOOL_NOT_FOUND',
252
- category: 'ENGINE',
253
- agentAction: 'stop',
254
- retryable: false,
255
- });
299
+ if (stepDef.tools && stepDef.tools.length > 0 && mcpClient) {
300
+ // Tools path: build tool definitions, call callStepWithTools. Rebuilt every attempt
301
+ // (issue #217) — safe: repair only ever follows a ZERO-toolCall attempt, so no budget
302
+ // was spent and nothing can duplicate.
303
+ const byServer = new Map();
304
+ for (const entry of stepDef.tools) {
305
+ const [serverId, toolName] = entry.split(':');
306
+ if (!byServer.has(serverId))
307
+ byServer.set(serverId, []);
308
+ byServer.get(serverId).push(toolName);
309
+ }
310
+ let toolsResult;
311
+ try {
312
+ const toolDefs = [];
313
+ const barenameOwner = new Map(); // bareName → serverId of first registration
314
+ for (const [serverId, allowList] of byServer) {
315
+ const mcpTools = await mcpClient.getTools(serverId, allowList);
316
+ const returnedNames = new Set(mcpTools.map((t) => t.name));
317
+ for (const name of allowList) {
318
+ if (!returnedNames.has(name)) {
319
+ throw new WorkflowError(`Step '${stepName}' declares tool '${serverId}:${name}' which is not exposed by MCP server '${serverId}'. ` +
320
+ `Check the tool name against the server's published tool list.`, {
321
+ code: 'MCP_TOOL_NOT_FOUND',
322
+ category: 'ENGINE',
323
+ agentAction: 'stop',
324
+ retryable: false,
325
+ });
326
+ }
256
327
  }
257
- }
258
- for (const mcpTool of mcpTools) {
259
- const firstOwner = barenameOwner.get(mcpTool.name);
260
- if (firstOwner !== undefined) {
261
- throw new WorkflowError(`Tool name collision in step '${stepName}': '${mcpTool.name}' is exposed by both '${firstOwner}' and '${serverId}'. ` +
262
- `Tool names must be unique across all connected servers within a step.`, {
263
- code: 'MCP_TOOL_NAME_COLLISION',
264
- category: 'ENGINE',
265
- agentAction: 'stop',
266
- retryable: false,
328
+ for (const mcpTool of mcpTools) {
329
+ const firstOwner = barenameOwner.get(mcpTool.name);
330
+ if (firstOwner !== undefined) {
331
+ throw new WorkflowError(`Tool name collision in step '${stepName}': '${mcpTool.name}' is exposed by both '${firstOwner}' and '${serverId}'. ` +
332
+ `Tool names must be unique across all connected servers within a step.`, {
333
+ code: 'MCP_TOOL_NAME_COLLISION',
334
+ category: 'ENGINE',
335
+ agentAction: 'stop',
336
+ retryable: false,
337
+ });
338
+ }
339
+ barenameOwner.set(mcpTool.name, serverId);
340
+ toolDefs.push({
341
+ id: `${serverId}:${mcpTool.name}`,
342
+ serverId,
343
+ name: mcpTool.name,
344
+ description: mcpTool.description,
345
+ inputSchema: mcpTool.inputSchema,
267
346
  });
268
347
  }
269
- barenameOwner.set(mcpTool.name, serverId);
270
- toolDefs.push({
271
- id: `${serverId}:${mcpTool.name}`,
272
- serverId,
273
- name: mcpTool.name,
274
- description: mcpTool.description,
275
- inputSchema: mcpTool.inputSchema,
276
- });
277
348
  }
278
- }
279
- const baseExecutor = async (namespacedName, args) => {
280
- const [serverId, toolName] = namespacedName.split(':');
281
- return mcpClient.call(serverId, toolName, args);
282
- };
283
- // Wrap the executor to enforce max_fan_out when set.
284
- // Counts calls to start_run and start_run_batch (regardless of server prefix).
285
- let fanOutCallCount = 0;
286
- const maxFanOut = stepDef.max_fan_out;
287
- const executor = async (namespacedName, args) => {
288
- const toolName = namespacedName.includes(':')
289
- ? namespacedName.split(':')[1]
290
- : namespacedName;
291
- if (toolName === 'start_run' || toolName === 'start_run_batch') {
292
- fanOutCallCount += 1;
293
- if (maxFanOut !== undefined && fanOutCallCount > maxFanOut) {
294
- throw new WorkflowError(`max_fan_out of ${maxFanOut} reached for step '${stepName}'. ` +
295
- `No further start_run or start_run_batch calls are permitted in this step.`, {
296
- code: 'VALIDATION_BATCH_TOO_LARGE',
297
- category: 'VALIDATION',
298
- agentAction: 'provide_input',
299
- retryable: false,
300
- });
349
+ const baseExecutor = async (namespacedName, args) => {
350
+ const [serverId, toolName] = namespacedName.split(':');
351
+ return mcpClient.call(serverId, toolName, args);
352
+ };
353
+ // Wrap the executor to enforce max_fan_out when set.
354
+ // Counts calls to start_run and start_run_batch (regardless of server prefix).
355
+ let fanOutCallCount = 0;
356
+ const maxFanOut = stepDef.max_fan_out;
357
+ const executor = async (namespacedName, args) => {
358
+ const toolName = namespacedName.includes(':')
359
+ ? namespacedName.split(':')[1]
360
+ : namespacedName;
361
+ if (toolName === 'start_run' || toolName === 'start_run_batch') {
362
+ fanOutCallCount += 1;
363
+ if (maxFanOut !== undefined && fanOutCallCount > maxFanOut) {
364
+ throw new WorkflowError(`max_fan_out of ${maxFanOut} reached for step '${stepName}'. ` +
365
+ `No further start_run or start_run_batch calls are permitted in this step.`, {
366
+ code: 'VALIDATION_BATCH_TOO_LARGE',
367
+ category: 'VALIDATION',
368
+ agentAction: 'provide_input',
369
+ retryable: false,
370
+ });
371
+ }
301
372
  }
373
+ return baseExecutor(namespacedName, args);
374
+ };
375
+ if (!isToolCapable(deps.provider)) {
376
+ throw new Error('invariant: provider lost tool capability between startup and step execution');
302
377
  }
303
- return baseExecutor(namespacedName, args);
304
- };
305
- if (!isToolCapable(deps.provider)) {
306
- throw new Error('invariant: provider lost tool capability between startup and step execution');
378
+ // #robust-anthropic-provider Part 1: same output-over-input precedence as the callStep
379
+ // path above.
380
+ const toolsEffectiveOutputSchema = stepDef.output_schema ??
381
+ stepDef.input_schema;
382
+ toolsResult = await deps.provider.callStepWithTools(promptForAttempt, toolDefs, executor, {
383
+ ...(toolsEffectiveOutputSchema !== undefined
384
+ ? { inputSchema: toolsEffectiveOutputSchema }
385
+ : {}),
386
+ maxToolCalls: stepDef.max_tool_calls ?? 20,
387
+ ...(stepDef.max_fan_out !== undefined ? { maxFanOut: stepDef.max_fan_out } : {}),
388
+ toolTimeoutMs: (stepDef.tool_timeout ?? 30) * 1000,
389
+ ...(agentProfileInstructions !== undefined ? { agentProfileInstructions } : {}),
390
+ });
391
+ }
392
+ catch (err) {
393
+ console.error(`\n✗ Step '${stepName}' (tools) failed: ${err instanceof Error ? err.message : String(err)}`);
394
+ return 'failed';
307
395
  }
308
- // #robust-anthropic-provider Part 1: same output-over-input precedence as the callStep
309
- // path above.
310
- const toolsEffectiveOutputSchema = stepDef.output_schema ??
311
- stepDef.input_schema;
312
- toolsResult = await deps.provider.callStepWithTools(prompt, toolDefs, executor, {
313
- ...(toolsEffectiveOutputSchema !== undefined
314
- ? { inputSchema: toolsEffectiveOutputSchema }
315
- : {}),
316
- maxToolCalls: stepDef.max_tool_calls ?? 20,
317
- ...(stepDef.max_fan_out !== undefined ? { maxFanOut: stepDef.max_fan_out } : {}),
318
- toolTimeoutMs: (stepDef.tool_timeout ?? 30) * 1000,
319
- ...(agentProfileInstructions !== undefined ? { agentProfileInstructions } : {}),
320
- });
396
+ stepInput = toolsResult.output;
397
+ toolCallsForMeta = toolsResult.toolCalls;
321
398
  }
322
- catch (err) {
323
- console.error(`\n✗ Step '${stepName}' (tools) failed: ${err instanceof Error ? err.message : String(err)}`);
324
- return 'failed';
399
+ else {
400
+ // Retry the LLM call once on failure before giving up.
401
+ let callError;
402
+ stepInput = {};
403
+ for (let attempt = 0; attempt < 2; attempt++) {
404
+ try {
405
+ stepInput = await deps.provider.callStep(promptForAttempt, inputSchema, agentProfileInstructions);
406
+ callError = undefined;
407
+ break;
408
+ }
409
+ catch (err) {
410
+ callError = err;
411
+ console.warn(` ⚠ LLM call attempt ${attempt + 1} failed: ${err instanceof Error ? err.message : String(err)}`);
412
+ }
413
+ }
414
+ if (callError !== undefined) {
415
+ console.error(`\n✗ Step '${stepName}' LLM call failed after 2 attempts`);
416
+ return 'failed';
417
+ }
325
418
  }
326
- stepInput = toolsResult.output;
327
- toolCallsForMeta = toolsResult.toolCalls;
328
419
  }
329
420
  else {
330
- // Retry the LLM call once on failure before giving up.
331
- let callError;
421
+ // Auto step — the engine dispatches to the service adapter directly.
422
+ console.log(`→ [auto] ${stepName}`);
332
423
  stepInput = {};
333
- for (let attempt = 0; attempt < 2; attempt++) {
334
- try {
335
- stepInput = await deps.provider.callStep(prompt, inputSchema, agentProfileInstructions);
336
- callError = undefined;
337
- break;
338
- }
339
- catch (err) {
340
- callError = err;
341
- console.warn(` ⚠ LLM call attempt ${attempt + 1} failed: ${err instanceof Error ? err.message : String(err)}`);
342
- }
343
- }
344
- if (callError !== undefined) {
345
- console.error(`\n✗ Step '${stepName}' LLM call failed after 2 attempts`);
346
- return 'failed';
347
- }
348
424
  }
425
+ result = await executeChain(deps.store, definition, {
426
+ runId,
427
+ command: stepName,
428
+ input: stepInput,
429
+ dispatcher: async () => stepInput,
430
+ registry: deps.registry,
431
+ ...(deps.traceBufferStore !== undefined
432
+ ? { traceBufferStore: deps.traceBufferStore }
433
+ : {}),
434
+ ...(toolCallsForMeta !== undefined ? { stepMeta: { toolCalls: toolCallsForMeta } } : {}),
435
+ // issue #197 PR-2: a FRESH nonce per step-attempt — resolved per call, never cached, so
436
+ // the strict-flip (checked inside shouldMintWriterNonce) is honored even if the env var
437
+ // changes mid-process (tests flip it). Also fresh per issue #217 repair attempt, since
438
+ // this call sits inside the repair loop.
439
+ ...(shouldMintWriterNonce(deps) ? { writerNonce: crypto.randomUUID() } : {}),
440
+ });
441
+ // issue #217: the in-drive schema-feedback repair gate. Fires ONLY when ALL SIX conjuncts
442
+ // hold — see plans/issue-217/design-v2.md §Mechanism for the rationale on (i)-(v).
443
+ //
444
+ // Conjunct (vi) — CORRECTED from the design record's literal `result.command === stepName`
445
+ // (flagged as a divergence in the implementation report): the record's premise was that
446
+ // executeChain "returns the DEEPER step's own envelope" on a chain-replacement error,
447
+ // citing execution-loop.ts:2855-2859/:3015 (executeChainInternal's recursive early-return,
448
+ // which DOES set `command` to the deeper step). But run-agent.ts calls the PUBLIC
449
+ // `executeChain` wrapper, not executeChainInternal directly — and that wrapper
450
+ // unconditionally overwrites the returned envelope's `command` back to the TOP-LEVEL
451
+ // requested command on every call (execution-loop.ts:3094, `command: options.command`),
452
+ // confirmed empirically against the built engine. So `result.command` always equals
453
+ // `stepName` here and can never discriminate a deeper chained step's error from this step's
454
+ // own — the literal conjunct is vacuously true and provides zero protection.
455
+ //
456
+ // The corrected, structurally-sound discriminator: a pre-claim validation rejection is
457
+ // write-free (no run-record version bump — see execute-step.ts:77-80 / execution-loop.ts's
458
+ // Step 2b/2c, both before claimStep). So if `result.run_version` has advanced past
459
+ // `versionBeforeAttempt` (captured fresh at the top of each repair attempt), something
460
+ // committed to the run BEFORE this error occurred — e.g. THIS step's own claim+settle,
461
+ // followed by a DEEPER chained step's pre-claim rejection — so the error cannot be this
462
+ // step's own output/input. A concurrent external writer bumping the run mid-attempt also
463
+ // lands here — the gate then fails CLOSED (repair forfeited, today's failure path). See
464
+ // run-agent.test.ts's "chained-auto no-false-repair" and concurrent-writer tests.
465
+ //
466
+ // issue #220 (SHIPPED): countRejection now persists a bounded rejection counter via a
467
+ // real CAS write on a counted rejection — rejected attempts are NO LONGER write-free w.r.t.
468
+ // the run record. What keeps this conjunct sound anyway is bump-and-report: the write's
469
+ // return value is discarded, and the rejection's own ENVELOPE keeps reporting the
470
+ // PRE-write version (the Step-1 `run`), so `result.run_version` still equals
471
+ // `versionBeforeAttempt` here across repairs 2..N. Pin (a) (bump-and-report) guards this
472
+ // invariant — see execution-loop.ts's countRejection for the mechanism, and
473
+ // packages/core/src/engine/validation-exhaustion.test.ts's pin (a) for the pin.
474
+ if (result.status === 'error' &&
475
+ (result.error_code === 'VALIDATION_OUTPUT_SCHEMA' ||
476
+ result.error_code === 'VALIDATION_INPUT_SCHEMA') &&
477
+ stepDef.execution === 'agent' &&
478
+ (toolCallsForMeta === undefined || toolCallsForMeta.length === 0) &&
479
+ repairsUsed < schemaRetries &&
480
+ result.run_version === versionBeforeAttempt) {
481
+ repairsUsed++;
482
+ const record = buildFailedAttemptRecord({
483
+ run_id: runId,
484
+ workflow_id: definition.id,
485
+ step_id: stepName,
486
+ ts: new Date().toISOString(),
487
+ error_code: result.error_code,
488
+ ajv_errors: result.error_details?.['errors'] ?? [],
489
+ params: stepInput,
490
+ trace_entry_count: 0,
491
+ });
492
+ lastRejection = {
493
+ kind: result.error_code === 'VALIDATION_OUTPUT_SCHEMA' ? 'output' : 'input',
494
+ summary: record.validation_error_summary.map(renderValidationSummaryEntry).join('\n'),
495
+ };
496
+ console.error(` ⚠ output rejected (${result.error_code}); repairing (attempt ${repairsUsed}/${schemaRetries})`);
497
+ continue;
498
+ }
499
+ break;
349
500
  }
350
- else {
351
- // Auto step — the engine dispatches to the service adapter directly.
352
- console.log(`→ [auto] ${stepName}`);
353
- stepInput = {};
354
- }
355
- const result = await executeChain(deps.store, definition, {
356
- runId,
357
- command: stepName,
358
- input: stepInput,
359
- dispatcher: async () => stepInput,
360
- registry: deps.registry,
361
- ...(deps.traceBufferStore !== undefined ? { traceBufferStore: deps.traceBufferStore } : {}),
362
- ...(toolCallsForMeta !== undefined ? { stepMeta: { toolCalls: toolCallsForMeta } } : {}),
363
- // issue #197 PR-2: a FRESH nonce per step-attempt — resolved per call, never cached, so
364
- // the strict-flip (checked inside shouldMintWriterNonce) is honored even if the env var
365
- // changes mid-process (tests flip it).
366
- ...(shouldMintWriterNonce(deps) ? { writerNonce: crypto.randomUUID() } : {}),
367
- });
368
501
  if (result.status === 'error') {
369
502
  // #134: a NOT-REGISTERED handler/adapter settles RECOVERABLY — the run is NOT failed, the step
370
503
  // is parked awaiting a capable runner. Detect structurally via error_code (not message text) and
@@ -372,6 +505,9 @@ export async function runAgent(deps, options) {
372
505
  // (no 'blocked' AgentRunResult variant, by design) — the distinction lives in the message.
373
506
  const isCapabilityBlock = result.error_code === 'ENGINE_HANDLER_NOT_REGISTERED' ||
374
507
  result.error_code === 'ENGINE_ADAPTER_NOT_REGISTERED';
508
+ // issue #217: append the repair count ONLY when at least one repair actually ran — never
509
+ // "after 0 schema-repair attempts".
510
+ const repairSuffix = repairsUsed > 0 ? ` after ${repairsUsed} schema-repair attempts` : '';
375
511
  if (isCapabilityBlock) {
376
512
  currentRun = await deps.store.get(runId);
377
513
  const block = findCapabilityBlockedSteps(currentRun).find((b) => b.step === stepName);
@@ -384,7 +520,7 @@ export async function runAgent(deps, options) {
384
520
  `The run is NOT failed — add ${need} and re-attach (\`realm agent --run-id ${runId}\`).`);
385
521
  }
386
522
  else {
387
- console.error(`\n✗ Step '${stepName}' failed: ${result.errors.join(', ')}`);
523
+ console.error(`\n✗ Step '${stepName}' failed: ${result.errors.join(', ')}${repairSuffix}`);
388
524
  }
389
525
  return 'failed';
390
526
  }