@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.
- package/dist/agent/providers/llm-provider.d.ts +8 -0
- package/dist/agent/providers/llm-provider.d.ts.map +1 -1
- package/dist/agent/providers/llm-provider.js.map +1 -1
- package/dist/agent/run-agent.d.ts +10 -0
- package/dist/agent/run-agent.d.ts.map +1 -1
- package/dist/agent/run-agent.js +282 -146
- package/dist/agent/run-agent.js.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +24 -1
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +36 -1
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/list.d.ts +9 -5
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +98 -44
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/reclaim.d.ts +15 -1
- package/dist/commands/reclaim.d.ts.map +1 -1
- package/dist/commands/reclaim.js +37 -11
- package/dist/commands/reclaim.js.map +1 -1
- package/dist/commands/replay.d.ts.map +1 -1
- package/dist/commands/replay.js +10 -1
- package/dist/commands/replay.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -4
|
@@ -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;
|
|
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,
|
|
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"}
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
//
|
|
331
|
-
|
|
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
|
}
|