agentfootprint 7.17.0 → 7.18.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/AGENTS.md +1 -1
- package/CLAUDE.md +2 -2
- package/ai-instructions/claude-code/SKILL.md +1 -1
- package/dist/core/Agent.js +51 -2
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/RunnerBase.js +16 -1
- package/dist/core/RunnerBase.js.map +1 -1
- package/dist/core/agent/AgentBuilder.js +117 -1
- package/dist/core/agent/AgentBuilder.js.map +1 -1
- package/dist/core/agent/middleware/errors.js +54 -0
- package/dist/core/agent/middleware/errors.js.map +1 -0
- package/dist/core/agent/middleware/index.js +23 -0
- package/dist/core/agent/middleware/index.js.map +1 -0
- package/dist/core/agent/middleware/ledger.js +61 -0
- package/dist/core/agent/middleware/ledger.js.map +1 -0
- package/dist/core/agent/middleware/outcomes.js +66 -0
- package/dist/core/agent/middleware/outcomes.js.map +1 -0
- package/dist/core/agent/middleware/runChain.js +144 -0
- package/dist/core/agent/middleware/runChain.js.map +1 -0
- package/dist/core/agent/middleware/types.js +41 -0
- package/dist/core/agent/middleware/types.js.map +1 -0
- package/dist/core/agent/stages/prepareFinal.js +10 -0
- package/dist/core/agent/stages/prepareFinal.js.map +1 -1
- package/dist/core/agent/stages/route.js +62 -1
- package/dist/core/agent/stages/route.js.map +1 -1
- package/dist/core/agent/stages/seed.js +130 -85
- package/dist/core/agent/stages/seed.js.map +1 -1
- package/dist/core/agent/stages/toolCalls.js +195 -8
- package/dist/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/core/pause.js +19 -1
- package/dist/core/pause.js.map +1 -1
- package/dist/esm/core/Agent.d.ts +9 -1
- package/dist/esm/core/Agent.js +52 -3
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/RunnerBase.js +16 -1
- package/dist/esm/core/RunnerBase.js.map +1 -1
- package/dist/esm/core/agent/AgentBuilder.d.ts +97 -0
- package/dist/esm/core/agent/AgentBuilder.js +117 -1
- package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
- package/dist/esm/core/agent/buildAgentChart.d.ts +2 -2
- package/dist/esm/core/agent/middleware/errors.d.ts +51 -0
- package/dist/esm/core/agent/middleware/errors.js +50 -0
- package/dist/esm/core/agent/middleware/errors.js.map +1 -0
- package/dist/esm/core/agent/middleware/index.d.ts +16 -0
- package/dist/esm/core/agent/middleware/index.js +16 -0
- package/dist/esm/core/agent/middleware/index.js.map +1 -0
- package/dist/esm/core/agent/middleware/ledger.d.ts +40 -0
- package/dist/esm/core/agent/middleware/ledger.js +57 -0
- package/dist/esm/core/agent/middleware/ledger.js.map +1 -0
- package/dist/esm/core/agent/middleware/outcomes.d.ts +45 -0
- package/dist/esm/core/agent/middleware/outcomes.js +60 -0
- package/dist/esm/core/agent/middleware/outcomes.js.map +1 -0
- package/dist/esm/core/agent/middleware/runChain.d.ts +104 -0
- package/dist/esm/core/agent/middleware/runChain.js +139 -0
- package/dist/esm/core/agent/middleware/runChain.js.map +1 -0
- package/dist/esm/core/agent/middleware/types.d.ts +191 -0
- package/dist/esm/core/agent/middleware/types.js +40 -0
- package/dist/esm/core/agent/middleware/types.js.map +1 -0
- package/dist/esm/core/agent/stages/prepareFinal.d.ts +10 -0
- package/dist/esm/core/agent/stages/prepareFinal.js +10 -0
- package/dist/esm/core/agent/stages/prepareFinal.js.map +1 -1
- package/dist/esm/core/agent/stages/route.d.ts +29 -0
- package/dist/esm/core/agent/stages/route.js +60 -0
- package/dist/esm/core/agent/stages/route.js.map +1 -1
- package/dist/esm/core/agent/stages/seed.d.ts +16 -1
- package/dist/esm/core/agent/stages/seed.js +130 -85
- package/dist/esm/core/agent/stages/seed.js.map +1 -1
- package/dist/esm/core/agent/stages/toolCalls.d.ts +23 -0
- package/dist/esm/core/agent/stages/toolCalls.js +195 -8
- package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/esm/core/agent/types.d.ts +18 -0
- package/dist/esm/core/pause.d.ts +42 -0
- package/dist/esm/core/pause.js +17 -0
- package/dist/esm/core/pause.js.map +1 -1
- package/dist/esm/events/dispatcher.d.ts +1 -1
- package/dist/esm/events/dispatcher.js.map +1 -1
- package/dist/esm/events/payloads.d.ts +26 -0
- package/dist/esm/events/registry.d.ts +5 -1
- package/dist/esm/events/registry.js +4 -0
- package/dist/esm/events/registry.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +13 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/mcp/mcpServe.js +39 -6
- package/dist/esm/lib/mcp/mcpServe.js.map +1 -1
- package/dist/esm/lib/mcp/types.d.ts +16 -0
- package/dist/events/dispatcher.js.map +1 -1
- package/dist/events/registry.js +4 -0
- package/dist/events/registry.js.map +1 -1
- package/dist/index.js +31 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/mcp/mcpServe.js +39 -6
- package/dist/lib/mcp/mcpServe.js.map +1 -1
- package/dist/types/core/Agent.d.ts +9 -1
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/RunnerBase.d.ts.map +1 -1
- package/dist/types/core/agent/AgentBuilder.d.ts +97 -0
- package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
- package/dist/types/core/agent/buildAgentChart.d.ts +2 -2
- package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
- package/dist/types/core/agent/middleware/errors.d.ts +52 -0
- package/dist/types/core/agent/middleware/errors.d.ts.map +1 -0
- package/dist/types/core/agent/middleware/index.d.ts +17 -0
- package/dist/types/core/agent/middleware/index.d.ts.map +1 -0
- package/dist/types/core/agent/middleware/ledger.d.ts +41 -0
- package/dist/types/core/agent/middleware/ledger.d.ts.map +1 -0
- package/dist/types/core/agent/middleware/outcomes.d.ts +46 -0
- package/dist/types/core/agent/middleware/outcomes.d.ts.map +1 -0
- package/dist/types/core/agent/middleware/runChain.d.ts +105 -0
- package/dist/types/core/agent/middleware/runChain.d.ts.map +1 -0
- package/dist/types/core/agent/middleware/types.d.ts +192 -0
- package/dist/types/core/agent/middleware/types.d.ts.map +1 -0
- package/dist/types/core/agent/stages/prepareFinal.d.ts +10 -0
- package/dist/types/core/agent/stages/prepareFinal.d.ts.map +1 -1
- package/dist/types/core/agent/stages/route.d.ts +29 -0
- package/dist/types/core/agent/stages/route.d.ts.map +1 -1
- package/dist/types/core/agent/stages/seed.d.ts +16 -1
- package/dist/types/core/agent/stages/seed.d.ts.map +1 -1
- package/dist/types/core/agent/stages/toolCalls.d.ts +23 -0
- package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
- package/dist/types/core/agent/types.d.ts +18 -0
- package/dist/types/core/agent/types.d.ts.map +1 -1
- package/dist/types/core/pause.d.ts +42 -0
- package/dist/types/core/pause.d.ts.map +1 -1
- package/dist/types/events/dispatcher.d.ts +1 -1
- package/dist/types/events/dispatcher.d.ts.map +1 -1
- package/dist/types/events/payloads.d.ts +26 -0
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/events/registry.d.ts +5 -1
- package/dist/types/events/registry.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/mcp/mcpServe.d.ts.map +1 -1
- package/dist/types/lib/mcp/types.d.ts +16 -0
- package/dist/types/lib/mcp/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import type { TypedScope } from 'footprintjs';
|
|
20
20
|
import type { LLMMessage, LLMToolSchema } from '../../../adapters/types.js';
|
|
21
21
|
import type { AgentInput, AgentState, RunConfig } from '../types.js';
|
|
22
|
+
import type { MessageMiddleware } from '../middleware/types.js';
|
|
22
23
|
export interface SeedStageDeps {
|
|
23
24
|
/** Resolved `clampIterations(opts.maxIterations ?? 10)`. Frozen at
|
|
24
25
|
* chart-build time. */
|
|
@@ -53,10 +54,24 @@ export interface SeedStageDeps {
|
|
|
53
54
|
* written, so the commit log is byte-identical to earlier releases.
|
|
54
55
|
*/
|
|
55
56
|
readonly resolveRunConfig?: (input: AgentInput) => RunConfig | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* The message chain (`.messageMiddleware(...)`), walked here at the
|
|
59
|
+
* `'input'` phase — BEFORE `userMessage` and `history` are written.
|
|
60
|
+
*
|
|
61
|
+
* This is the only placement that keeps the run honest. Everything
|
|
62
|
+
* downstream reads `scope.history`: the window strategies, the injection
|
|
63
|
+
* engine, all three slots, the request that goes on the wire, and every
|
|
64
|
+
* slice taken afterwards. Transform later than this and those components
|
|
65
|
+
* disagree about what the user actually said — the trace would show one
|
|
66
|
+
* message and the model would have answered another.
|
|
67
|
+
*
|
|
68
|
+
* Empty / undefined → seed stays the synchronous stage it always was.
|
|
69
|
+
*/
|
|
70
|
+
readonly messageMiddleware?: readonly MessageMiddleware[];
|
|
56
71
|
}
|
|
57
72
|
/**
|
|
58
73
|
* Build the seed stage function for an Agent instance. Captures both
|
|
59
74
|
* the chart-build-time constants and the per-run mutable accessors
|
|
60
75
|
* via the deps object.
|
|
61
76
|
*/
|
|
62
|
-
export declare function buildSeedStage(deps: SeedStageDeps): (scope: TypedScope<AgentState>) => void
|
|
77
|
+
export declare function buildSeedStage(deps: SeedStageDeps): (scope: TypedScope<AgentState>) => void | Promise<void>;
|
|
@@ -17,99 +17,144 @@
|
|
|
17
17
|
* features need.
|
|
18
18
|
*/
|
|
19
19
|
import { typedEmit } from '../../../recorders/core/typedEmit.js';
|
|
20
|
+
import { runMessageChain } from '../middleware/runChain.js';
|
|
21
|
+
import { recordDecisions } from '../middleware/ledger.js';
|
|
20
22
|
/**
|
|
21
23
|
* Build the seed stage function for an Agent instance. Captures both
|
|
22
24
|
* the chart-build-time constants and the per-run mutable accessors
|
|
23
25
|
* via the deps object.
|
|
24
26
|
*/
|
|
25
27
|
export function buildSeedStage(deps) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// (without resumeOnError) start fresh.
|
|
34
|
-
const resumeHistory = deps.consumePendingResumeHistory();
|
|
35
|
-
if (resumeHistory && resumeHistory.length > 0) {
|
|
36
|
-
scope.history = [...resumeHistory];
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
scope.history = [{ role: 'user', content: args.message }];
|
|
40
|
-
}
|
|
41
|
-
// Default identity uses the runId so multi-run isolation works
|
|
42
|
-
// without consumer changes; explicit identity (multi-tenant)
|
|
43
|
-
// overrides via `agent.run({ identity })`.
|
|
44
|
-
scope.runIdentity = args.identity ?? {
|
|
45
|
-
conversationId: deps.getCurrentRunId() ?? 'default',
|
|
28
|
+
const chain = deps.messageMiddleware ?? [];
|
|
29
|
+
// No chain → the same synchronous function this stage has always been.
|
|
30
|
+
// Not an optimisation: an agent without middleware must produce the same
|
|
31
|
+
// stage shape, the same committed keys and the same request bytes as before.
|
|
32
|
+
if (chain.length === 0) {
|
|
33
|
+
return (scope) => {
|
|
34
|
+
seedFrom(scope, scope.$getArgs().message, deps);
|
|
46
35
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
scope.totalOutputTokens = 0;
|
|
58
|
-
scope.turnStartMs = Date.now();
|
|
59
|
-
scope.systemPromptInjections = [];
|
|
60
|
-
scope.messagesInjections = [];
|
|
61
|
-
scope.toolsInjections = [];
|
|
62
|
-
scope.llmLatestContent = '';
|
|
63
|
-
scope.llmLatestToolCalls = [];
|
|
64
|
-
// v2.14 — initialize thinking blocks. Empty array means "no thinking
|
|
65
|
-
// this iteration"; the NormalizeThinking sub-subflow overwrites
|
|
66
|
-
// this AFTER each CallLLM when a ThinkingHandler is configured.
|
|
67
|
-
scope.thinkingBlocks = [];
|
|
68
|
-
scope.pausedToolCallId = '';
|
|
69
|
-
scope.pausedToolName = '';
|
|
70
|
-
scope.pausedToolStartMs = 0;
|
|
71
|
-
scope.cumTokensInput = 0;
|
|
72
|
-
scope.cumTokensOutput = 0;
|
|
73
|
-
scope.cumEstimatedUsd = 0;
|
|
74
|
-
scope.costBudgetHit = false;
|
|
75
|
-
scope.activeInjections = [];
|
|
76
|
-
scope.activatedInjectionIds = [];
|
|
77
|
-
scope.dynamicToolSchemas = deps.toolSchemas;
|
|
78
|
-
// Cache layer state (v2.6) — initialized to inert defaults.
|
|
79
|
-
// CacheDecision subflow populates `cacheMarkers` per iteration;
|
|
80
|
-
// UpdateSkillHistory + CacheGate consume `cachingDisabled`,
|
|
81
|
-
// `recentHitRate`, `skillHistory`. Empty defaults mean the
|
|
82
|
-
// CacheGate falls through to 'apply-markers' on iter 1 (no
|
|
83
|
-
// history yet → no churn detected; recentHitRate undefined →
|
|
84
|
-
// hit-rate floor doesn't fire).
|
|
85
|
-
scope.cacheMarkers = [];
|
|
86
|
-
scope.cachingDisabled = deps.cachingDisabled;
|
|
87
|
-
scope.recentHitRate = undefined;
|
|
88
|
-
scope.skillHistory = [];
|
|
89
|
-
// Skill-graph cursor — reset per turn so each new user message re-enters the
|
|
90
|
-
// graph through the entry router (cold start). The Injection Engine advances
|
|
91
|
-
// it each iteration; undefined for agents without a skillGraph().
|
|
92
|
-
scope.currentSkillId = undefined;
|
|
93
|
-
// `.configure()` — resolved ONCE here (seed runs exactly once per run)
|
|
94
|
-
// and written to scope, which means the run's commit log records the
|
|
95
|
-
// model and instructions the run actually used. A run that changed its
|
|
96
|
-
// own model without committing that fact would produce a trace that
|
|
97
|
-
// reads as if the built-in default answered.
|
|
98
|
-
//
|
|
99
|
-
// Only what the resolver actually returned is written: an agent with no
|
|
100
|
-
// `.configure()`, or one whose resolver returned `{}`, commits nothing
|
|
101
|
-
// extra and behaves exactly as before.
|
|
102
|
-
if (deps.resolveRunConfig) {
|
|
103
|
-
const resolved = deps.resolveRunConfig(args);
|
|
104
|
-
if (resolved?.model !== undefined)
|
|
105
|
-
scope.resolvedModel = resolved.model;
|
|
106
|
-
if (resolved?.instructions !== undefined)
|
|
107
|
-
scope.resolvedInstructions = resolved.instructions;
|
|
108
|
-
}
|
|
109
|
-
typedEmit(scope, 'agentfootprint.agent.turn_start', {
|
|
110
|
-
turnIndex: 0,
|
|
111
|
-
userPrompt: args.message,
|
|
36
|
+
}
|
|
37
|
+
return async (scope) => {
|
|
38
|
+
const args = scope.$getArgs();
|
|
39
|
+
const verdict = await runMessageChain(chain, {
|
|
40
|
+
phase: 'input',
|
|
41
|
+
content: args.message,
|
|
42
|
+
history: [],
|
|
43
|
+
// The input boundary runs before iteration 1 exists.
|
|
44
|
+
iteration: 0,
|
|
45
|
+
...(args.identity && { identity: args.identity }),
|
|
112
46
|
});
|
|
47
|
+
recordDecisions(scope, verdict.decisions);
|
|
48
|
+
if (verdict.kind === 'deny') {
|
|
49
|
+
// Seed the run anyway, with the content as it stood when it was
|
|
50
|
+
// refused, then stop. Committing it costs nothing (a refusal is a fact
|
|
51
|
+
// about a run, and hiding what was refused would make the record
|
|
52
|
+
// useless), and a fully-seeded state means `resumeOnError` and every
|
|
53
|
+
// recorder see the shape they expect rather than a half-built one.
|
|
54
|
+
seedFrom(scope, verdict.content, deps);
|
|
55
|
+
scope.messageDeniedReason = verdict.reason;
|
|
56
|
+
scope.messageDeniedPhase = 'input';
|
|
57
|
+
scope.messageDeniedBy = verdict.middleware;
|
|
58
|
+
// Stops the chart here: no injections, no slots, no LLM call. The
|
|
59
|
+
// boundary turns these flags into a MessageDeniedError.
|
|
60
|
+
scope.$break(`message denied at input: ${verdict.reason}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
seedFrom(scope, verdict.content, deps);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Initialise every mutable field of `AgentState` from `message` + the run
|
|
68
|
+
* args. Split out so the message the run proceeds with can come either
|
|
69
|
+
* straight from the caller or from the `'input'` middleware chain — one
|
|
70
|
+
* initialiser, so the two paths cannot drift.
|
|
71
|
+
*/
|
|
72
|
+
function seedFrom(scope, message, deps) {
|
|
73
|
+
const args = scope.$getArgs();
|
|
74
|
+
scope.userMessage = message;
|
|
75
|
+
// If `resumeOnError(...)` set the side channel, restore the
|
|
76
|
+
// checkpointed conversation history. The next iteration sees
|
|
77
|
+
// the prior messages and continues from the failure point.
|
|
78
|
+
// Always clear the field after reading so subsequent runs
|
|
79
|
+
// (without resumeOnError) start fresh.
|
|
80
|
+
const resumeHistory = deps.consumePendingResumeHistory();
|
|
81
|
+
if (resumeHistory && resumeHistory.length > 0) {
|
|
82
|
+
scope.history = [...resumeHistory];
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
scope.history = [{ role: 'user', content: message }];
|
|
86
|
+
}
|
|
87
|
+
// Default identity uses the runId so multi-run isolation works
|
|
88
|
+
// without consumer changes; explicit identity (multi-tenant)
|
|
89
|
+
// overrides via `agent.run({ identity })`.
|
|
90
|
+
scope.runIdentity = args.identity ?? {
|
|
91
|
+
conversationId: deps.getCurrentRunId() ?? 'default',
|
|
113
92
|
};
|
|
93
|
+
scope.newMessages = [];
|
|
94
|
+
scope.turnNumber = 1;
|
|
95
|
+
// Permissive default — explicit cap will land when PricingTable
|
|
96
|
+
// gets a context-window field. Memory pickByBudget treats anything
|
|
97
|
+
// ≥ minimumTokens as "fits", so this just enables the budget path.
|
|
98
|
+
scope.contextTokensRemaining = 32_000;
|
|
99
|
+
scope.iteration = 1;
|
|
100
|
+
scope.maxIterations = deps.maxIterations;
|
|
101
|
+
scope.finalContent = '';
|
|
102
|
+
scope.totalInputTokens = 0;
|
|
103
|
+
scope.totalOutputTokens = 0;
|
|
104
|
+
scope.turnStartMs = Date.now();
|
|
105
|
+
scope.systemPromptInjections = [];
|
|
106
|
+
scope.messagesInjections = [];
|
|
107
|
+
scope.toolsInjections = [];
|
|
108
|
+
scope.llmLatestContent = '';
|
|
109
|
+
scope.llmLatestToolCalls = [];
|
|
110
|
+
// v2.14 — initialize thinking blocks. Empty array means "no thinking
|
|
111
|
+
// this iteration"; the NormalizeThinking sub-subflow overwrites
|
|
112
|
+
// this AFTER each CallLLM when a ThinkingHandler is configured.
|
|
113
|
+
scope.thinkingBlocks = [];
|
|
114
|
+
scope.pausedToolCallId = '';
|
|
115
|
+
scope.pausedToolName = '';
|
|
116
|
+
scope.pausedToolStartMs = 0;
|
|
117
|
+
scope.cumTokensInput = 0;
|
|
118
|
+
scope.cumTokensOutput = 0;
|
|
119
|
+
scope.cumEstimatedUsd = 0;
|
|
120
|
+
scope.costBudgetHit = false;
|
|
121
|
+
scope.activeInjections = [];
|
|
122
|
+
scope.activatedInjectionIds = [];
|
|
123
|
+
scope.dynamicToolSchemas = deps.toolSchemas;
|
|
124
|
+
// Cache layer state (v2.6) — initialized to inert defaults.
|
|
125
|
+
// CacheDecision subflow populates `cacheMarkers` per iteration;
|
|
126
|
+
// UpdateSkillHistory + CacheGate consume `cachingDisabled`,
|
|
127
|
+
// `recentHitRate`, `skillHistory`. Empty defaults mean the
|
|
128
|
+
// CacheGate falls through to 'apply-markers' on iter 1 (no
|
|
129
|
+
// history yet → no churn detected; recentHitRate undefined →
|
|
130
|
+
// hit-rate floor doesn't fire).
|
|
131
|
+
scope.cacheMarkers = [];
|
|
132
|
+
scope.cachingDisabled = deps.cachingDisabled;
|
|
133
|
+
scope.recentHitRate = undefined;
|
|
134
|
+
scope.skillHistory = [];
|
|
135
|
+
// Skill-graph cursor — reset per turn so each new user message re-enters the
|
|
136
|
+
// graph through the entry router (cold start). The Injection Engine advances
|
|
137
|
+
// it each iteration; undefined for agents without a skillGraph().
|
|
138
|
+
scope.currentSkillId = undefined;
|
|
139
|
+
// `.configure()` — resolved ONCE here (seed runs exactly once per run)
|
|
140
|
+
// and written to scope, which means the run's commit log records the
|
|
141
|
+
// model and instructions the run actually used. A run that changed its
|
|
142
|
+
// own model without committing that fact would produce a trace that
|
|
143
|
+
// reads as if the built-in default answered.
|
|
144
|
+
//
|
|
145
|
+
// Only what the resolver actually returned is written: an agent with no
|
|
146
|
+
// `.configure()`, or one whose resolver returned `{}`, commits nothing
|
|
147
|
+
// extra and behaves exactly as before.
|
|
148
|
+
if (deps.resolveRunConfig) {
|
|
149
|
+
const resolved = deps.resolveRunConfig(args);
|
|
150
|
+
if (resolved?.model !== undefined)
|
|
151
|
+
scope.resolvedModel = resolved.model;
|
|
152
|
+
if (resolved?.instructions !== undefined)
|
|
153
|
+
scope.resolvedInstructions = resolved.instructions;
|
|
154
|
+
}
|
|
155
|
+
typedEmit(scope, 'agentfootprint.agent.turn_start', {
|
|
156
|
+
turnIndex: 0,
|
|
157
|
+
userPrompt: message,
|
|
158
|
+
});
|
|
114
159
|
}
|
|
115
160
|
//# sourceMappingURL=seed.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../../../../../src/core/agent/stages/seed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../../../../../src/core/agent/stages/seed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAGjE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAoD1D;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAmB;IAEnB,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAAC;IAC3C,uEAAuE;IACvE,yEAAyE;IACzE,6EAA6E;IAC7E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,EAAE,EAAE;YACf,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC9D,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE;QACrB,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAc,CAAC;QAC1C,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE;YAC3C,KAAK,EAAE,OAAO;YACd,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE,EAAE;YACX,qDAAqD;YACrD,SAAS,EAAE,CAAC;YACZ,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;SAClD,CAAC,CAAC;QACH,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAC1C,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC5B,gEAAgE;YAChE,uEAAuE;YACvE,iEAAiE;YACjE,qEAAqE;YACrE,mEAAmE;YACnE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACvC,KAAK,CAAC,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;YAC3C,KAAK,CAAC,kBAAkB,GAAG,OAAO,CAAC;YACnC,KAAK,CAAC,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC;YAC3C,kEAAkE;YAClE,wDAAwD;YACxD,KAAK,CAAC,MAAM,CAAC,4BAA4B,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QACD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACzC,CAAC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,KAA6B,EAAE,OAAe,EAAE,IAAmB;IACnF,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAc,CAAC;IAC1C,KAAK,CAAC,WAAW,GAAG,OAAO,CAAC;IAE5B,4DAA4D;IAC5D,6DAA6D;IAC7D,2DAA2D;IAC3D,0DAA0D;IAC1D,uCAAuC;IACvC,MAAM,aAAa,GAAG,IAAI,CAAC,2BAA2B,EAAE,CAAC;IACzD,IAAI,aAAa,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,KAAK,CAAC,OAAO,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC;IACrC,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,OAAO,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,+DAA+D;IAC/D,6DAA6D;IAC7D,2CAA2C;IAC3C,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,QAAQ,IAAI;QACnC,cAAc,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,SAAS;KACpD,CAAC;IACF,KAAK,CAAC,WAAW,GAAG,EAAE,CAAC;IACvB,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC;IACrB,gEAAgE;IAChE,mEAAmE;IACnE,mEAAmE;IACnE,KAAK,CAAC,sBAAsB,GAAG,MAAM,CAAC;IACtC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;IACpB,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;IACzC,KAAK,CAAC,YAAY,GAAG,EAAE,CAAC;IACxB,KAAK,CAAC,gBAAgB,GAAG,CAAC,CAAC;IAC3B,KAAK,CAAC,iBAAiB,GAAG,CAAC,CAAC;IAC5B,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC/B,KAAK,CAAC,sBAAsB,GAAG,EAAE,CAAC;IAClC,KAAK,CAAC,kBAAkB,GAAG,EAAE,CAAC;IAC9B,KAAK,CAAC,eAAe,GAAG,EAAE,CAAC;IAC3B,KAAK,CAAC,gBAAgB,GAAG,EAAE,CAAC;IAC5B,KAAK,CAAC,kBAAkB,GAAG,EAAE,CAAC;IAC9B,qEAAqE;IACrE,gEAAgE;IAChE,gEAAgE;IAChE,KAAK,CAAC,cAAc,GAAG,EAAE,CAAC;IAC1B,KAAK,CAAC,gBAAgB,GAAG,EAAE,CAAC;IAC5B,KAAK,CAAC,cAAc,GAAG,EAAE,CAAC;IAC1B,KAAK,CAAC,iBAAiB,GAAG,CAAC,CAAC;IAC5B,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC;IACzB,KAAK,CAAC,eAAe,GAAG,CAAC,CAAC;IAC1B,KAAK,CAAC,eAAe,GAAG,CAAC,CAAC;IAC1B,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC;IAC5B,KAAK,CAAC,gBAAgB,GAAG,EAAE,CAAC;IAC5B,KAAK,CAAC,qBAAqB,GAAG,EAAE,CAAC;IACjC,KAAK,CAAC,kBAAkB,GAAG,IAAI,CAAC,WAAW,CAAC;IAC5C,4DAA4D;IAC5D,gEAAgE;IAChE,4DAA4D;IAC5D,2DAA2D;IAC3D,2DAA2D;IAC3D,6DAA6D;IAC7D,gCAAgC;IAChC,KAAK,CAAC,YAAY,GAAG,EAAE,CAAC;IACxB,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,eAAe,CAAC;IAC7C,KAAK,CAAC,aAAa,GAAG,SAAS,CAAC;IAChC,KAAK,CAAC,YAAY,GAAG,EAAE,CAAC;IACxB,6EAA6E;IAC7E,6EAA6E;IAC7E,kEAAkE;IAClE,KAAK,CAAC,cAAc,GAAG,SAAS,CAAC;IAEjC,uEAAuE;IACvE,qEAAqE;IACrE,uEAAuE;IACvE,oEAAoE;IACpE,6CAA6C;IAC7C,EAAE;IACF,wEAAwE;IACxE,uEAAuE;IACvE,uCAAuC;IACvC,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,QAAQ,EAAE,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC;QACxE,IAAI,QAAQ,EAAE,YAAY,KAAK,SAAS;YAAE,KAAK,CAAC,oBAAoB,GAAG,QAAQ,CAAC,YAAY,CAAC;IAC/F,CAAC;IAED,SAAS,CAAC,KAAK,EAAE,iCAAiC,EAAE;QAClD,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,OAAO;KACpB,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -20,6 +20,19 @@
|
|
|
20
20
|
* `tool.execute`. Deny → tool not executed; result is a synthetic
|
|
21
21
|
* denial string. Allow / gate_open → execution proceeds.
|
|
22
22
|
*
|
|
23
|
+
* Gate order for one call, and why it is this order:
|
|
24
|
+
*
|
|
25
|
+
* permission → MIDDLEWARE CHAIN → arg validation → check-in →
|
|
26
|
+
* credentials → execute
|
|
27
|
+
*
|
|
28
|
+
* The chain sits after the permission gate so an existing checker still
|
|
29
|
+
* decides first (a denial there means no middleware runs), and before arg
|
|
30
|
+
* validation so validation judges the args that will actually be sent —
|
|
31
|
+
* a middleware that transformed args into something the tool's schema
|
|
32
|
+
* rejects must be caught, not forwarded. A middleware answering `ask`
|
|
33
|
+
* pauses on the SAME wire `checkIn` uses; the human's answer is a
|
|
34
|
+
* decision, not a result, so the chain resumes and the REAL tool runs.
|
|
35
|
+
*
|
|
23
36
|
* `read_skill` is the auto-attached activation tool — when the LLM
|
|
24
37
|
* calls it with a valid Skill id, the next InjectionEngine pass
|
|
25
38
|
* activates that Skill (lifetime: turn).
|
|
@@ -31,6 +44,7 @@ import type { CredentialProvider } from '../../../identity/types.js';
|
|
|
31
44
|
import { type ResolvedCheckInConfig } from '../../checkin.js';
|
|
32
45
|
import type { ProviderToolCache } from '../../slots/buildToolsSlot.js';
|
|
33
46
|
import type { Tool } from '../../tools.js';
|
|
47
|
+
import type { ToolMiddleware } from '../middleware/types.js';
|
|
34
48
|
import { type ToolArgValidationMode } from '../toolArgsValidation.js';
|
|
35
49
|
import type { AgentState } from '../types.js';
|
|
36
50
|
export interface ToolCallsHandlerDeps {
|
|
@@ -78,6 +92,15 @@ export interface ToolCallsHandlerDeps {
|
|
|
78
92
|
* Agent (no check-in). The gate fires ONLY for tools that declared `checkIn`.
|
|
79
93
|
*/
|
|
80
94
|
readonly checkIn?: ResolvedCheckInConfig;
|
|
95
|
+
/**
|
|
96
|
+
* The tool-dispatch governance chain (`.toolMiddleware(...)`), in
|
|
97
|
+
* declaration order. Walked AFTER the permission gate — so an existing
|
|
98
|
+
* `PermissionChecker` still decides first and a denial there means no
|
|
99
|
+
* middleware runs at all — and BEFORE arg validation, so validation judges
|
|
100
|
+
* the args that will actually be sent rather than the ones the model
|
|
101
|
+
* proposed. Empty / undefined → not walked, no ledger key, no events.
|
|
102
|
+
*/
|
|
103
|
+
readonly toolMiddleware?: readonly ToolMiddleware[];
|
|
81
104
|
}
|
|
82
105
|
/**
|
|
83
106
|
* Build the pausable tool-call handler for the agent's chart.
|
|
@@ -20,6 +20,19 @@
|
|
|
20
20
|
* `tool.execute`. Deny → tool not executed; result is a synthetic
|
|
21
21
|
* denial string. Allow / gate_open → execution proceeds.
|
|
22
22
|
*
|
|
23
|
+
* Gate order for one call, and why it is this order:
|
|
24
|
+
*
|
|
25
|
+
* permission → MIDDLEWARE CHAIN → arg validation → check-in →
|
|
26
|
+
* credentials → execute
|
|
27
|
+
*
|
|
28
|
+
* The chain sits after the permission gate so an existing checker still
|
|
29
|
+
* decides first (a denial there means no middleware runs), and before arg
|
|
30
|
+
* validation so validation judges the args that will actually be sent —
|
|
31
|
+
* a middleware that transformed args into something the tool's schema
|
|
32
|
+
* rejects must be caught, not forwarded. A middleware answering `ask`
|
|
33
|
+
* pauses on the SAME wire `checkIn` uses; the human's answer is a
|
|
34
|
+
* decision, not a result, so the chain resumes and the REAL tool runs.
|
|
35
|
+
*
|
|
23
36
|
* `read_skill` is the auto-attached activation tool — when the LLM
|
|
24
37
|
* calls it with a valid Skill id, the next InjectionEngine pass
|
|
25
38
|
* activates that Skill (lifetime: turn).
|
|
@@ -29,6 +42,8 @@ import { extractSequence } from '../../../security/extractSequence.js';
|
|
|
29
42
|
import { unconfiguredCredentialProvider } from '../../../identity/types.js';
|
|
30
43
|
import { isPauseRequest } from '../../pause.js';
|
|
31
44
|
import { shouldCheckIn, isCheckInDecision, checkInDeclined, } from '../../checkin.js';
|
|
45
|
+
import { runToolChain } from '../middleware/runChain.js';
|
|
46
|
+
import { recordDecisions } from '../middleware/ledger.js';
|
|
32
47
|
import { formatToolArgIssues, validateToolArgs, } from '../toolArgsValidation.js';
|
|
33
48
|
import { safeStringify } from '../validators.js';
|
|
34
49
|
/**
|
|
@@ -202,6 +217,12 @@ export function buildToolCallsHandler(deps) {
|
|
|
202
217
|
// identity, and abort signal — enough surface to build sequence-
|
|
203
218
|
// aware policies (forbidden chains, idempotency limits, cost
|
|
204
219
|
// guards) without maintaining parallel state.
|
|
220
|
+
// Args as they will actually be used. The middleware chain below may
|
|
221
|
+
// replace this; everything downstream (validation, the check-in
|
|
222
|
+
// evidence a human approves, credentials, execute, the read_skill
|
|
223
|
+
// gate) reads `callArgs`, never `tc.args`, so there is exactly one
|
|
224
|
+
// answer to "what did this call really run with".
|
|
225
|
+
let callArgs = tc.args;
|
|
205
226
|
let denied = false;
|
|
206
227
|
let haltContext;
|
|
207
228
|
if (permissionChecker) {
|
|
@@ -268,6 +289,52 @@ export function buildToolCallsHandler(deps) {
|
|
|
268
289
|
result = `[permission denied: checker error: ${msg}]`;
|
|
269
290
|
}
|
|
270
291
|
}
|
|
292
|
+
// ── The middleware chain ─────────────────────────────────────────
|
|
293
|
+
// Walked only for a call the permission gate let through, so an
|
|
294
|
+
// existing checker keeps deciding first and a denial there costs
|
|
295
|
+
// nothing. A denial from the chain lands as the tool result, exactly
|
|
296
|
+
// like every other refusal in this loop — the model reads it and
|
|
297
|
+
// adapts. An `ask` commits partial state and pauses, on the same wire
|
|
298
|
+
// the check-in gate uses.
|
|
299
|
+
if (!denied && deps.toolMiddleware && deps.toolMiddleware.length > 0) {
|
|
300
|
+
const chain = await runToolChain(deps.toolMiddleware, {
|
|
301
|
+
toolName: tc.name,
|
|
302
|
+
toolCallId: tc.id,
|
|
303
|
+
iteration,
|
|
304
|
+
args: callArgs,
|
|
305
|
+
history: newHistory,
|
|
306
|
+
...(runIdentity && { identity: runIdentity }),
|
|
307
|
+
...(env.signal && { signal: env.signal }),
|
|
308
|
+
});
|
|
309
|
+
recordDecisions(scope, chain.decisions);
|
|
310
|
+
callArgs = chain.args;
|
|
311
|
+
if (chain.kind === 'deny') {
|
|
312
|
+
denied = true;
|
|
313
|
+
result = chain.reason;
|
|
314
|
+
}
|
|
315
|
+
else if (chain.kind === 'ask') {
|
|
316
|
+
// Commit partial state so resume() finds history intact (the
|
|
317
|
+
// pauseHere / check-in path does the same). The TRANSFORMED args
|
|
318
|
+
// ride the checkpoint: a person approves what the chain produced,
|
|
319
|
+
// not what the model originally proposed.
|
|
320
|
+
scope.history = newHistory;
|
|
321
|
+
scope.pausedToolCallId = tc.id;
|
|
322
|
+
scope.pausedToolName = tc.name;
|
|
323
|
+
scope.pausedToolStartMs = startMs;
|
|
324
|
+
scope.pausedAsk = true;
|
|
325
|
+
scope.pausedAskArgs = chain.args;
|
|
326
|
+
scope.pausedAskIndex = chain.index;
|
|
327
|
+
scope.pausedAskMiddleware = chain.middleware;
|
|
328
|
+
// A defined return value triggers the footprintjs pause; this
|
|
329
|
+
// object becomes the checkpoint's pauseData, and detectPause
|
|
330
|
+
// surfaces `pauseData.ask` as `outcome.ask`.
|
|
331
|
+
return {
|
|
332
|
+
toolCallId: tc.id,
|
|
333
|
+
toolName: tc.name,
|
|
334
|
+
ask: { ...chain.payload, middleware: chain.middleware },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
271
338
|
// Tool-args validation (#9) — AFTER the permission gate (policy must
|
|
272
339
|
// see every attempted call, valid or not) and BEFORE credential
|
|
273
340
|
// resolution (never acquire credentials for a call that won't run).
|
|
@@ -278,7 +345,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
278
345
|
// tools (their inputSchema is the contract the LLM was shown).
|
|
279
346
|
let argsRejected = false;
|
|
280
347
|
if (!denied && tool && toolArgValidation !== 'off') {
|
|
281
|
-
const verdict = validateToolArgs(
|
|
348
|
+
const verdict = validateToolArgs(callArgs, tool.schema.inputSchema);
|
|
282
349
|
if (!verdict.ok) {
|
|
283
350
|
typedEmit(scope, 'agentfootprint.validation.args_invalid', {
|
|
284
351
|
toolName: tc.name,
|
|
@@ -316,7 +383,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
316
383
|
const historyForEvidence = systemPrompt
|
|
317
384
|
? [{ role: 'system', content: systemPrompt }, ...newHistory]
|
|
318
385
|
: newHistory;
|
|
319
|
-
if (!shouldCheckIn(tool.checkIn,
|
|
386
|
+
if (!shouldCheckIn(tool.checkIn, callArgs, {
|
|
320
387
|
iteration,
|
|
321
388
|
toolCallId: tc.id,
|
|
322
389
|
history: historyForEvidence,
|
|
@@ -328,7 +395,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
328
395
|
const intent = scope.llmLatestContent ? String(scope.llmLatestContent) : undefined;
|
|
329
396
|
const evidence = await deps.checkIn.assembler({
|
|
330
397
|
tool: { name: tc.name, description: tool.schema.description },
|
|
331
|
-
args:
|
|
398
|
+
args: callArgs,
|
|
332
399
|
...(intent !== undefined && { intent }),
|
|
333
400
|
iteration,
|
|
334
401
|
history: historyForEvidence,
|
|
@@ -337,7 +404,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
337
404
|
});
|
|
338
405
|
const request = {
|
|
339
406
|
tool: tc.name,
|
|
340
|
-
args:
|
|
407
|
+
args: callArgs,
|
|
341
408
|
...(intent !== undefined && { intent }),
|
|
342
409
|
evidence,
|
|
343
410
|
};
|
|
@@ -355,7 +422,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
355
422
|
scope.pausedToolName = tc.name;
|
|
356
423
|
scope.pausedToolStartMs = startMs;
|
|
357
424
|
scope.pausedCheckIn = true;
|
|
358
|
-
scope.pausedCheckInArgs =
|
|
425
|
+
scope.pausedCheckInArgs = callArgs;
|
|
359
426
|
// Returning a defined value triggers the footprintjs pause; the
|
|
360
427
|
// returned object becomes the checkpoint's pauseData. detectPause
|
|
361
428
|
// surfaces `pauseData.checkIn` as `outcome.checkIn`.
|
|
@@ -420,7 +487,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
420
487
|
try {
|
|
421
488
|
if (!tool)
|
|
422
489
|
throw new Error(`Unknown tool: ${tc.name}`);
|
|
423
|
-
result = await tool.execute(
|
|
490
|
+
result = await tool.execute(callArgs, {
|
|
424
491
|
toolCallId: tc.id,
|
|
425
492
|
iteration,
|
|
426
493
|
...(env.signal && { signal: env.signal }),
|
|
@@ -459,7 +526,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
459
526
|
// so plain read_skill agents are byte-for-byte unaffected.
|
|
460
527
|
let skillRejected = false;
|
|
461
528
|
if (deps.allowedSkillIds && tc.name === 'read_skill' && !error && !denied) {
|
|
462
|
-
const reqId =
|
|
529
|
+
const reqId = callArgs.id;
|
|
463
530
|
if (typeof reqId === 'string' && reqId.length > 0) {
|
|
464
531
|
const currentSkillId = scope.currentSkillId;
|
|
465
532
|
const allowed = deps.allowedSkillIds(currentSkillId);
|
|
@@ -505,7 +572,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
505
572
|
// NEXT pass activates that Skill (lifetime: turn — stays
|
|
506
573
|
// active until the turn ends).
|
|
507
574
|
if (tc.name === 'read_skill' && !error && !denied && !skillRejected) {
|
|
508
|
-
const requestedId =
|
|
575
|
+
const requestedId = callArgs.id;
|
|
509
576
|
if (typeof requestedId === 'string' && requestedId.length > 0) {
|
|
510
577
|
const current = scope.activatedInjectionIds;
|
|
511
578
|
if (!current.includes(requestedId)) {
|
|
@@ -567,6 +634,126 @@ export function buildToolCallsHandler(deps) {
|
|
|
567
634
|
const toolCallId = scope.pausedToolCallId;
|
|
568
635
|
const toolName = scope.pausedToolName;
|
|
569
636
|
const startMs = scope.pausedToolStartMs;
|
|
637
|
+
// ── Middleware-ask decision path ─────────────────────────────────
|
|
638
|
+
// Discriminated by `scope.pausedAsk`, restored from the checkpoint.
|
|
639
|
+
//
|
|
640
|
+
// The answer is a DECISION, not a result. That is the whole reason the
|
|
641
|
+
// outcome union has no `result` arm: a middleware asks a person whether
|
|
642
|
+
// this call may proceed, and on approval the REAL tool runs — the person
|
|
643
|
+
// never writes the tool's answer, and neither does the middleware.
|
|
644
|
+
//
|
|
645
|
+
// A malformed resume DECLINES, for the same reason the check-in path
|
|
646
|
+
// does: a governed call must never execute because a message was
|
|
647
|
+
// mis-shaped.
|
|
648
|
+
if (scope.pausedAsk === true) {
|
|
649
|
+
const iteration = scope.iteration;
|
|
650
|
+
const args = (scope.pausedAskArgs ?? {});
|
|
651
|
+
const askIndex = (scope.pausedAskIndex ?? 0);
|
|
652
|
+
const askedBy = (scope.pausedAskMiddleware ?? 'middleware');
|
|
653
|
+
const decision = isCheckInDecision(input)
|
|
654
|
+
? input
|
|
655
|
+
: checkInDeclined({ by: 'unknown', note: 'resume input was not a CheckInDecision' });
|
|
656
|
+
let result;
|
|
657
|
+
let error;
|
|
658
|
+
if (!decision.approved) {
|
|
659
|
+
result = decision.note ? `declined by human: ${decision.note}` : 'declined by human';
|
|
660
|
+
recordDecisions(scope, [
|
|
661
|
+
{
|
|
662
|
+
middleware: askedBy,
|
|
663
|
+
at: 'tool',
|
|
664
|
+
toolName,
|
|
665
|
+
toolCallId,
|
|
666
|
+
iteration,
|
|
667
|
+
outcome: 'deny',
|
|
668
|
+
changed: false,
|
|
669
|
+
why: `declined by ${decision.by}${decision.note ? `: ${decision.note}` : ''}`,
|
|
670
|
+
},
|
|
671
|
+
]);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
recordDecisions(scope, [
|
|
675
|
+
{
|
|
676
|
+
middleware: askedBy,
|
|
677
|
+
at: 'tool',
|
|
678
|
+
toolName,
|
|
679
|
+
toolCallId,
|
|
680
|
+
iteration,
|
|
681
|
+
outcome: 'allow',
|
|
682
|
+
changed: false,
|
|
683
|
+
why: `approved by ${decision.by}${decision.note ? `: ${decision.note}` : ''}`,
|
|
684
|
+
},
|
|
685
|
+
]);
|
|
686
|
+
// Continue the chain from the link AFTER the one that asked. Its
|
|
687
|
+
// decision is already on the checkpoint; re-running it would ask the
|
|
688
|
+
// same question twice and file a duplicate row.
|
|
689
|
+
//
|
|
690
|
+
// `askPolicy: 'refuse'` because footprintjs's `PausableHandler.resume`
|
|
691
|
+
// returns void — a resumed dispatch has no second checkpoint to give.
|
|
692
|
+
// A link further down the chain that also wants a person gets a named,
|
|
693
|
+
// model-visible refusal and the tool does NOT run. That is the same
|
|
694
|
+
// rule already applied to a tool that tries to pause during an
|
|
695
|
+
// approved check-in resume: at most one human question per resume.
|
|
696
|
+
const rest = await runToolChain(deps.toolMiddleware ?? [], {
|
|
697
|
+
toolName,
|
|
698
|
+
toolCallId,
|
|
699
|
+
iteration,
|
|
700
|
+
args,
|
|
701
|
+
history: [...scope.history],
|
|
702
|
+
startIndex: askIndex + 1,
|
|
703
|
+
askPolicy: 'refuse',
|
|
704
|
+
});
|
|
705
|
+
recordDecisions(scope, rest.decisions);
|
|
706
|
+
const tool = lookupTool(toolName);
|
|
707
|
+
if (rest.kind === 'deny') {
|
|
708
|
+
result = rest.reason;
|
|
709
|
+
}
|
|
710
|
+
else if (tool?.checkIn !== undefined) {
|
|
711
|
+
// Same one-question rule, from the other direction: this tool also
|
|
712
|
+
// demands consent, and there is no checkpoint left to ask with.
|
|
713
|
+
// Refusing loudly beats executing a tool whose consent gate we
|
|
714
|
+
// silently skipped.
|
|
715
|
+
error = true;
|
|
716
|
+
result =
|
|
717
|
+
`tool '${toolName}' also declares checkIn, and a resumed dispatch cannot pause ` +
|
|
718
|
+
`again to ask a second time. The call was not executed — approve it through one ` +
|
|
719
|
+
`gate, not both.`;
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
const env = scope.$getEnv();
|
|
723
|
+
const dispatched = await resolveCredentialAndExecute(scope, tool, toolName, rest.args, toolCallId, iteration, env);
|
|
724
|
+
result = dispatched.result;
|
|
725
|
+
error = dispatched.error;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const askResultStr = typeof result === 'string' ? result : safeStringify(result);
|
|
729
|
+
const askHistory = [
|
|
730
|
+
...scope.history,
|
|
731
|
+
{ role: 'tool', content: askResultStr, toolCallId, toolName },
|
|
732
|
+
];
|
|
733
|
+
scope.history = askHistory;
|
|
734
|
+
scope.lastToolResult = { toolName, result: askResultStr };
|
|
735
|
+
typedEmit(scope, 'agentfootprint.stream.tool_end', {
|
|
736
|
+
toolCallId,
|
|
737
|
+
result,
|
|
738
|
+
durationMs: Date.now() - startMs,
|
|
739
|
+
...(error === true && { error: true }),
|
|
740
|
+
});
|
|
741
|
+
typedEmit(scope, 'agentfootprint.agent.iteration_end', {
|
|
742
|
+
turnIndex: 0,
|
|
743
|
+
iterIndex: iteration,
|
|
744
|
+
toolCallCount: 1,
|
|
745
|
+
history: askHistory,
|
|
746
|
+
});
|
|
747
|
+
scope.iteration = iteration + 1;
|
|
748
|
+
scope.pausedToolCallId = '';
|
|
749
|
+
scope.pausedToolName = '';
|
|
750
|
+
scope.pausedToolStartMs = 0;
|
|
751
|
+
scope.pausedAsk = false;
|
|
752
|
+
scope.pausedAskArgs = undefined;
|
|
753
|
+
scope.pausedAskIndex = undefined;
|
|
754
|
+
scope.pausedAskMiddleware = undefined;
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
570
757
|
// ── Check-in decision path ───────────────────────────────────────
|
|
571
758
|
// A check-in pause is discriminated by `scope.pausedCheckIn` (restored
|
|
572
759
|
// from the checkpoint). The resume input is a `CheckInDecision`. On
|