loopforge 3.0.0 → 3.6.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/README.md +126 -143
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +131 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +98 -23
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +257 -181
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -33
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +17 -0
- package/dist/cognitive-facts.d.ts.map +1 -0
- package/dist/cognitive-facts.js +99 -0
- package/dist/cognitive-facts.js.map +1 -0
- package/dist/cognitive-governance.d.ts +25 -33
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +38 -138
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +59 -0
- package/dist/committed-round.d.ts.map +1 -0
- package/dist/committed-round.js +222 -0
- package/dist/committed-round.js.map +1 -0
- package/dist/enforcement-gate.d.ts +78 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +1109 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +111 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +887 -0
- package/dist/engine.js.map +1 -0
- package/dist/evidence-claims.d.ts +50 -0
- package/dist/evidence-claims.d.ts.map +1 -0
- package/dist/evidence-claims.js +116 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/evidence-provider.d.ts +50 -23
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +167 -50
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +1 -84
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +19 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +58 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +841 -304
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +2 -2
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +2 -2
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts +6 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +26 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +115 -24
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +365 -218
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +214 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +935 -0
- package/dist/mcp/round-lifecycle.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +29 -43
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +64 -101
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +433 -1110
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +20 -11
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +587 -413
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +12 -24
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +78 -184
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +61 -50
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +48 -146
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +37 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +487 -278
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -8
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +8 -26
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +398 -236
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +147 -17
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -13
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +32 -69
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +64 -0
- package/dist/round-contract.d.ts.map +1 -0
- package/dist/round-contract.js +80 -0
- package/dist/round-contract.js.map +1 -0
- package/dist/round-coordinator.d.ts +23 -25
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +103 -92
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +13 -13
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +25 -14
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +14 -18
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +37 -47
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +48 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +248 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +9 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +18 -6
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +15 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +24 -5
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +149 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +1343 -0
- package/dist/verification-gate.js.map +1 -0
- package/dist/workspace.d.ts +29 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +61 -0
- package/dist/workspace.js.map +1 -0
- package/loop_policy.json +37 -16
- package/package.json +5 -10
- package/skills/perception/SKILL.md +184 -202
- package/dist/canonical-event-writer.d.ts +0 -20
- package/dist/canonical-event-writer.d.ts.map +0 -1
- package/dist/canonical-event-writer.js +0 -131
- package/dist/canonical-event-writer.js.map +0 -1
- package/dist/compiler-subgoals.d.ts +0 -7
- package/dist/compiler-subgoals.d.ts.map +0 -1
- package/dist/compiler-subgoals.js +0 -93
- package/dist/compiler-subgoals.js.map +0 -1
- package/dist/derive-id.d.ts +0 -12
- package/dist/derive-id.d.ts.map +0 -1
- package/dist/derive-id.js +0 -31
- package/dist/derive-id.js.map +0 -1
- package/dist/mcp/session-projection.d.ts +0 -5
- package/dist/mcp/session-projection.d.ts.map +0 -1
- package/dist/mcp/session-projection.js +0 -18
- package/dist/mcp/session-projection.js.map +0 -1
- package/dist/mcp/session-queue.d.ts +0 -3
- package/dist/mcp/session-queue.d.ts.map +0 -1
- package/dist/mcp/session-queue.js +0 -18
- package/dist/mcp/session-queue.js.map +0 -1
- package/dist/mcp/session-types.d.ts +0 -72
- package/dist/mcp/session-types.d.ts.map +0 -1
- package/dist/mcp/session-types.js +0 -2
- package/dist/mcp/session-types.js.map +0 -1
- package/dist/round-commit-store.d.ts +0 -12
- package/dist/round-commit-store.d.ts.map +0 -1
- package/dist/round-commit-store.js +0 -13
- package/dist/round-commit-store.js.map +0 -1
- package/dist/round-gate.d.ts +0 -31
- package/dist/round-gate.d.ts.map +0 -1
- package/dist/round-gate.js +0 -259
- package/dist/round-gate.js.map +0 -1
- package/dist/round-recovery.d.ts +0 -17
- package/dist/round-recovery.d.ts.map +0 -1
- package/dist/round-recovery.js +0 -117
- package/dist/round-recovery.js.map +0 -1
- package/dist/round-report.d.ts +0 -16
- package/dist/round-report.d.ts.map +0 -1
- package/dist/round-report.js +0 -401
- package/dist/round-report.js.map +0 -1
- package/dist/runtime-services.d.ts +0 -59
- package/dist/runtime-services.d.ts.map +0 -1
- package/dist/runtime-services.js +0 -460
- package/dist/runtime-services.js.map +0 -1
- package/dist/vault-format.d.ts +0 -4
- package/dist/vault-format.d.ts.map +0 -1
- package/dist/vault-format.js +0 -4
- package/dist/vault-format.js.map +0 -1
- package/dist/vault-types.d.ts +0 -24
- package/dist/vault-types.d.ts.map +0 -1
- package/dist/vault-types.js +0 -2
- package/dist/vault-types.js.map +0 -1
- package/loopforge-policy.json +0 -340
package/dist/mcp/tools.js
CHANGED
|
@@ -1,106 +1,17 @@
|
|
|
1
1
|
/** LoopForge MCP — Tool definitions and handlers.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
3
|
+
* 9 tools: start, next, status, stop, pause, resume, replay,
|
|
4
|
+
* gate_check, gate_resolve.
|
|
5
|
+
* status is the unified inspection tool (view=session|loop|all|audit).
|
|
4
6
|
* Each handler receives SessionManager + parsed input, returns the output object.
|
|
5
7
|
*/
|
|
8
|
+
import { buildSelfEvaluation } from "../engine.js";
|
|
9
|
+
import { validateCoreSelfEvaluation } from "../self-eval.js";
|
|
6
10
|
import { isRecord } from "../token-utils.js";
|
|
7
|
-
import {
|
|
8
|
-
import { getPolicy } from "../policy.js";
|
|
11
|
+
import { validateLoopId } from "../policy.js";
|
|
9
12
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
13
|
// Tool schemas (MCP JSON Schema format)
|
|
11
14
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
-
const GATE_ACTION_SCHEMA = {
|
|
13
|
-
type: "object",
|
|
14
|
-
properties: {
|
|
15
|
-
description: { type: "string" },
|
|
16
|
-
scope: { type: "array", items: { type: "string" } },
|
|
17
|
-
effects: { type: "array", items: { type: "string", enum: ["workspace_write", "production", "credentials", "data_migration", "public_api", "publish", "payment", "external_communication", "network"] } },
|
|
18
|
-
reversibility: { type: "string", enum: ["reversible", "recoverable", "irreversible", "unknown"] },
|
|
19
|
-
authorization: { type: "string", enum: ["agent_allowed", "user_required", "unknown"] },
|
|
20
|
-
},
|
|
21
|
-
required: ["description", "scope", "effects", "reversibility", "authorization"],
|
|
22
|
-
};
|
|
23
|
-
const ROUND_REPORT_SCHEMA = {
|
|
24
|
-
type: "object",
|
|
25
|
-
required: ["format", "outcome", "continuation", "summary", "violations"],
|
|
26
|
-
properties: {
|
|
27
|
-
format: { type: "string", const: "loopforge.round-report" },
|
|
28
|
-
outcome: { type: "string", enum: ["success", "partial", "failed", "blocked"] },
|
|
29
|
-
continuation: { type: "string", enum: ["continue", "stop"] },
|
|
30
|
-
summary: { type: "string" },
|
|
31
|
-
violations: { type: "array", items: { type: "string" } },
|
|
32
|
-
agentIntent: { type: "string" },
|
|
33
|
-
driftClarification: {
|
|
34
|
-
type: "object",
|
|
35
|
-
properties: {
|
|
36
|
-
explanation: { type: "string", minLength: 1, maxLength: 2000 },
|
|
37
|
-
anchors: { type: "array", maxItems: 8, items: { type: "string", maxLength: 240 } },
|
|
38
|
-
},
|
|
39
|
-
required: ["explanation", "anchors"],
|
|
40
|
-
},
|
|
41
|
-
evidence: {
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: {
|
|
44
|
-
files: { type: "array", items: { type: "string" } },
|
|
45
|
-
checks: { type: "array", items: { type: "object", properties: {
|
|
46
|
-
name: { type: "string" }, status: { type: "string", enum: ["passed", "failed", "not_run"] },
|
|
47
|
-
summary: { type: "string" }, counts: { type: "object", properties: {
|
|
48
|
-
passed: { type: "integer", minimum: 0 }, failed: { type: "integer", minimum: 0 }, skipped: { type: "integer", minimum: 0 },
|
|
49
|
-
} },
|
|
50
|
-
}, required: ["name", "status"] } },
|
|
51
|
-
claims: { type: "array", items: { type: "object", properties: {
|
|
52
|
-
id: { type: "string", pattern: "^claim-[a-f0-9]{12}$" }, targetId: { type: "string" }, evidenceRefs: { type: "array", items: { type: "string" } },
|
|
53
|
-
}, required: ["id", "targetId", "evidenceRefs"] } },
|
|
54
|
-
provenance: {
|
|
55
|
-
type: "object",
|
|
56
|
-
description: "Runtime-owned evidence provenance keyed by evidence reference.",
|
|
57
|
-
additionalProperties: {
|
|
58
|
-
type: "object",
|
|
59
|
-
properties: {
|
|
60
|
-
source: { type: "string", enum: ["agent", "command", "provider", "verification"] },
|
|
61
|
-
observedAt: { type: "string" },
|
|
62
|
-
status: { type: "string", enum: ["verified", "claimed", "contradicted", "unavailable"] },
|
|
63
|
-
detail: { type: "string" },
|
|
64
|
-
},
|
|
65
|
-
required: ["source", "observedAt", "status"],
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
noChangeReason: { type: "string" },
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
blocker: { type: "object", properties: {
|
|
72
|
-
kind: { type: "string", enum: ["dependency", "external", "needs_human_input", "agent"] },
|
|
73
|
-
reason: { type: "string" }, references: { type: "array", items: { type: "string" } },
|
|
74
|
-
action: GATE_ACTION_SCHEMA,
|
|
75
|
-
}, required: ["kind", "reason"] },
|
|
76
|
-
discoveries: { type: "object", properties: {
|
|
77
|
-
wrongAssumptions: { type: "array", items: { type: "string" } },
|
|
78
|
-
emergedWork: { type: "array", items: { type: "string" } },
|
|
79
|
-
facts: { type: "array", items: { type: "string" } },
|
|
80
|
-
newConstraints: { type: "array", items: { type: "string" } },
|
|
81
|
-
} },
|
|
82
|
-
subGoalEvents: { type: "object", properties: {
|
|
83
|
-
completed: { type: "array", items: { type: "string" } },
|
|
84
|
-
blocked: { type: "array", items: { type: "string" } },
|
|
85
|
-
canceled: { type: "array", items: { type: "string" } },
|
|
86
|
-
} },
|
|
87
|
-
constraintEvents: { type: "object", properties: {
|
|
88
|
-
retired: { type: "array", items: { type: "string" } },
|
|
89
|
-
} },
|
|
90
|
-
criterionRevisions: { type: "array", items: { type: "object", properties: {
|
|
91
|
-
old: { type: "string" }, new: { type: "string" },
|
|
92
|
-
}, required: ["old", "new"] } },
|
|
93
|
-
objectiveRefinement: { type: "string" },
|
|
94
|
-
phaseBoundaryProposal: { type: "object", properties: {
|
|
95
|
-
label: { type: "string" }, reason: { type: "string" },
|
|
96
|
-
}, required: ["label", "reason"] },
|
|
97
|
-
contextRequest: { type: "object", properties: {
|
|
98
|
-
emphasize: { type: "array", items: { type: "string" } },
|
|
99
|
-
expand: { type: "array", items: { type: "string" } },
|
|
100
|
-
confusion_points: { type: "array", items: { type: "string" } },
|
|
101
|
-
} },
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
15
|
const TOOL_BASE_SCHEMAS = [
|
|
105
16
|
{
|
|
106
17
|
name: "loopforge_start",
|
|
@@ -117,10 +28,9 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
117
28
|
description: "Optional loop ID. Generated automatically if not provided.",
|
|
118
29
|
},
|
|
119
30
|
maxRounds: {
|
|
120
|
-
type: "
|
|
31
|
+
type: "integer",
|
|
121
32
|
minimum: 1,
|
|
122
|
-
|
|
123
|
-
description: "Maximum rounds before auto-stop. Default: 100 from policy.",
|
|
33
|
+
description: "Maximum rounds before auto-stop. Default: 20 from policy.",
|
|
124
34
|
},
|
|
125
35
|
domain: {
|
|
126
36
|
type: "string",
|
|
@@ -135,125 +45,328 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
135
45
|
items: { type: "string" },
|
|
136
46
|
description: "Hard constraints to enforce across all rounds.",
|
|
137
47
|
},
|
|
138
|
-
successCriteria: {
|
|
139
|
-
type: "array",
|
|
140
|
-
items: { type: "string" },
|
|
141
|
-
description: "Explicit success criteria to verify across the loop.",
|
|
142
|
-
},
|
|
143
48
|
},
|
|
144
49
|
required: ["task"],
|
|
145
50
|
},
|
|
146
51
|
},
|
|
147
52
|
{
|
|
148
53
|
name: "loopforge_next",
|
|
149
|
-
description: "Submit the
|
|
54
|
+
description: "Submit the output from the current round and advance to the next. Returns the next-round prompt, or null with a stopReason when the loop ends. A structured evaluation is required; output is optional supporting context.",
|
|
150
55
|
inputSchema: {
|
|
151
56
|
type: "object",
|
|
152
|
-
additionalProperties: false,
|
|
153
57
|
properties: {
|
|
154
58
|
sessionId: {
|
|
155
59
|
type: "string",
|
|
156
60
|
description: "Session ID returned by loopforge_start.",
|
|
157
61
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
description: "
|
|
62
|
+
roundId: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The roundId from the most recent loopforge_start / loopforge_next / loopforge_resume / loopforge_status response. Anchors this submission to the exact round it reports on: a submission whose roundId no longer matches the current round (the round already committed) is not processed — the current held prompt is returned instead, so lost responses can be recovered without skipping or double-committing a round.",
|
|
65
|
+
},
|
|
66
|
+
output: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Optional. The agent's full output from executing the current round's prompt. The structured evaluation is the authoritative report.",
|
|
69
|
+
},
|
|
70
|
+
evaluation: {
|
|
71
|
+
type: "object",
|
|
72
|
+
description: "Required structured self-evaluation for this round. Free-text or embedded evaluation blocks are not accepted.",
|
|
73
|
+
required: ["success", "output_summary", "should_continue", "constraint_violations"],
|
|
74
|
+
properties: {
|
|
75
|
+
success: {
|
|
76
|
+
type: "boolean",
|
|
77
|
+
description: "true ONLY if all hard constraints met AND the task goal achieved.",
|
|
78
|
+
},
|
|
79
|
+
output_summary: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Specific, actionable summary of what was DONE this round — not what was attempted.",
|
|
82
|
+
},
|
|
83
|
+
should_continue: {
|
|
84
|
+
type: "boolean",
|
|
85
|
+
description: "false ONLY when the ENTIRE task is complete. Partial progress = true.",
|
|
86
|
+
},
|
|
87
|
+
constraint_violations: {
|
|
88
|
+
type: "array",
|
|
89
|
+
items: { type: "string" },
|
|
90
|
+
description: "Constraints the agent actually violated this round. Be honest.",
|
|
91
|
+
},
|
|
92
|
+
discovered_constraints: {
|
|
93
|
+
type: "array",
|
|
94
|
+
items: { type: "string" },
|
|
95
|
+
description: "Optional. New constraints discovered this round.",
|
|
96
|
+
},
|
|
97
|
+
objective_refinement: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "Optional. If this round deepened understanding of the task objective.",
|
|
100
|
+
},
|
|
101
|
+
emerged_subtasks: {
|
|
102
|
+
type: "array",
|
|
103
|
+
items: { type: "string" },
|
|
104
|
+
description: "Optional. Sub-problems that surfaced during execution.",
|
|
105
|
+
},
|
|
106
|
+
execution_evidence: {
|
|
107
|
+
type: "object",
|
|
108
|
+
description: "Optional. Structured record of what actually happened this round.",
|
|
109
|
+
properties: {
|
|
110
|
+
files_changed: {
|
|
111
|
+
type: "array",
|
|
112
|
+
items: { type: "string" },
|
|
113
|
+
description: "Files modified this round.",
|
|
114
|
+
},
|
|
115
|
+
test_results: {
|
|
116
|
+
type: "object",
|
|
117
|
+
description: "Test results from this round.",
|
|
118
|
+
properties: {
|
|
119
|
+
passed: { type: "integer", minimum: 0, description: "Number of passing tests." },
|
|
120
|
+
failed: { type: "integer", minimum: 0, description: "Number of failing tests." },
|
|
121
|
+
skipped: { type: "integer", minimum: 0, description: "Number of skipped tests." },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
success_criteria_met: {
|
|
125
|
+
type: "array",
|
|
126
|
+
items: { type: "string" },
|
|
127
|
+
description: "Success criteria satisfied this round.",
|
|
128
|
+
},
|
|
129
|
+
success_criteria_remaining: {
|
|
130
|
+
type: "array",
|
|
131
|
+
items: { type: "string" },
|
|
132
|
+
description: "Success criteria still outstanding.",
|
|
133
|
+
},
|
|
134
|
+
progress_estimate: {
|
|
135
|
+
type: "number",
|
|
136
|
+
minimum: 0,
|
|
137
|
+
maximum: 1,
|
|
138
|
+
description: "Estimated progress toward task completion (0.0–1.0).",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
retracted_constraints: {
|
|
143
|
+
type: "array",
|
|
144
|
+
items: { type: "string" },
|
|
145
|
+
description: "Optional. Constraints the agent now believes are wrong.",
|
|
146
|
+
},
|
|
147
|
+
revised_success_criteria: {
|
|
148
|
+
type: "array",
|
|
149
|
+
description: "Optional. Success criteria that need reformulation.",
|
|
150
|
+
items: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
old: { type: "string", description: "Original criterion." },
|
|
154
|
+
new: { type: "string", description: "Revised criterion." },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
wrong_assumptions: {
|
|
159
|
+
type: "array",
|
|
160
|
+
items: { type: "string" },
|
|
161
|
+
description: "Optional. Assumptions from earlier rounds that were incorrect.",
|
|
162
|
+
},
|
|
163
|
+
worker_results: {
|
|
164
|
+
type: "array",
|
|
165
|
+
description: "Optional. Results of sub-agent / Worker delegations this round.",
|
|
166
|
+
items: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
agentId: { type: "string" },
|
|
170
|
+
subAgentType: { type: "string" },
|
|
171
|
+
subTask: { type: "string" },
|
|
172
|
+
resultSummary: { type: "string" },
|
|
173
|
+
success: { type: "boolean" },
|
|
174
|
+
outcome: {
|
|
175
|
+
type: "string",
|
|
176
|
+
enum: ["success", "partial", "failed"],
|
|
177
|
+
description: "Optional. Worker outcome; derived from success when absent.",
|
|
178
|
+
},
|
|
179
|
+
discoveredConstraints: {
|
|
180
|
+
type: "array",
|
|
181
|
+
items: { type: "string" },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
required: ["agentId", "subAgentType", "subTask", "resultSummary", "success"],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
compression_checkpoint: {
|
|
188
|
+
type: "boolean",
|
|
189
|
+
description: "Optional. Set true at subtask boundaries to trigger a full-state checkpoint (L2 prompt) that survives rolling-window eviction. The checkpoint label is carried into future prompts.",
|
|
190
|
+
},
|
|
191
|
+
checkpoint_label: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "Optional. Human-readable label for this checkpoint (e.g. 'core-functions-complete'). Only meaningful when compression_checkpoint is true.",
|
|
194
|
+
},
|
|
195
|
+
next_action: {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "Optional. What the agent plans to do in the next round. Helps LoopForge detect task drift early by comparing declared intent with actual work.",
|
|
198
|
+
},
|
|
199
|
+
completed_subtasks: {
|
|
200
|
+
type: "array",
|
|
201
|
+
items: { type: "string" },
|
|
202
|
+
description: "Optional. Sub-tasks completed this round. Matched to previously emerged sub-tasks by description similarity.",
|
|
203
|
+
},
|
|
204
|
+
blocked_subtasks: {
|
|
205
|
+
type: "array",
|
|
206
|
+
items: { type: "string" },
|
|
207
|
+
description: "Optional. Sub-tasks that are now blocked. Describe what is blocking and why.",
|
|
208
|
+
},
|
|
209
|
+
canceled_subtasks: {
|
|
210
|
+
type: "array",
|
|
211
|
+
items: { type: "string" },
|
|
212
|
+
description: "Optional. Sub-tasks no longer needed. Removes them from the active sub-goal list.",
|
|
213
|
+
},
|
|
214
|
+
stop_reason: {
|
|
215
|
+
type: "string",
|
|
216
|
+
enum: ["gave_up", "blocked", "needs_human_input"],
|
|
217
|
+
description: "Optional. Why the agent is stopping. Only meaningful when should_continue=false and success=false. 'blocked' means work cannot proceed under current constraints.",
|
|
218
|
+
},
|
|
219
|
+
no_change_reason: {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: "Optional. Declared reason for claiming success with no machine-verifiable evidence (e.g. documentation-only round). Downgrades the success_without_verified_evidence check from error to info. Max 200 chars.",
|
|
222
|
+
},
|
|
223
|
+
outcome: {
|
|
224
|
+
type: "string",
|
|
225
|
+
enum: ["success", "partial", "failed", "blocked"],
|
|
226
|
+
description: "Optional. Tri-state outcome. When absent, derived from success (true→success, false→failed). 'partial' and 'blocked' must be declared explicitly. A declared non-success outcome suppresses the success-class checks even when success=true.",
|
|
227
|
+
},
|
|
228
|
+
blocker: {
|
|
229
|
+
type: "string",
|
|
230
|
+
description: "Optional. Flat blocker description, required only when outcome=blocked. Also feeds the user/agent gate classification. Max 500 chars.",
|
|
231
|
+
},
|
|
232
|
+
retroactiveClaims: {
|
|
233
|
+
type: "array",
|
|
234
|
+
items: {
|
|
235
|
+
type: "object",
|
|
236
|
+
properties: {
|
|
237
|
+
round: { type: "integer", minimum: 1 },
|
|
238
|
+
claim: { type: "string" },
|
|
239
|
+
},
|
|
240
|
+
required: ["round", "claim"],
|
|
241
|
+
additionalProperties: false,
|
|
242
|
+
},
|
|
243
|
+
description: "Optional. Claims that a PRIOR round satisfied a criterion. The runtime verifies them against the prior round's git evidence. Max 20 entries.",
|
|
244
|
+
},
|
|
245
|
+
drift_clarification: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "Optional. Explain why this round's actions diverged from your previous round's next_action. Only required when the previous round was flagged for intent_drift or subgoal_drift. Mention concrete anchors: sub-goal IDs (sg-XXXXXXXX), constraint IDs (c-XXXXXXXX), criterion IDs (cr-XXXXXXXX), or file paths.",
|
|
248
|
+
},
|
|
249
|
+
prompt_requests: {
|
|
250
|
+
type: "object",
|
|
251
|
+
description: "Optional. Information needs for the next round's prompt. Use when you know what context you'll need to succeed in the next round.",
|
|
252
|
+
properties: {
|
|
253
|
+
emphasize: {
|
|
254
|
+
type: "array",
|
|
255
|
+
items: { type: "string" },
|
|
256
|
+
description: "Constraints, discoveries, or decisions that need emphasis in the next prompt. Matched by meaning — not exact text. Matched items appear in a 'Critical Context' section. Max 5 entries. Pure reordering — no token overhead.",
|
|
257
|
+
},
|
|
258
|
+
confusion_points: {
|
|
259
|
+
type: "array",
|
|
260
|
+
items: { type: "string" },
|
|
261
|
+
description: "Things you're confused about. Rendered at the TOP of the next prompt before the objective. Max 3 entries, each ≤ 200 chars recommended. Be specific — 'I don't understand why milestone 3 is marked complete when phase 2 appears unfinished.'",
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
round_contract: {
|
|
266
|
+
type: "object",
|
|
267
|
+
description: "Optional. Proposal for the NEXT round's contract (v3.4). Declared here, it becomes the ACTIVE contract (rendered as the Current Task) only after this round commits, and stays active until a later round lists every done_when item in success_criteria_met or reports outcome=blocked. While it is active, restate it unchanged here; on completion or block, declare the next contract instead (or omit the field — the Current Task reverts to the original task). Checks: round_underspecified / round_unverifiable target this proposal; round_scope_drift and premature_boundary target the ACTIVE contract.",
|
|
268
|
+
properties: {
|
|
269
|
+
work_item: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "Optional. Round focus, one line. Rendered as the active contract's Current Task first line. Max 200 chars.",
|
|
272
|
+
},
|
|
273
|
+
done_when: {
|
|
274
|
+
type: "array",
|
|
275
|
+
items: { type: "string" },
|
|
276
|
+
description: "Completion conditions: cr-XXXXXXXX criterion IDs or free text. Satisfied = list in success_criteria_met; still open = list in success_criteria_remaining. Max 20 items.",
|
|
277
|
+
},
|
|
278
|
+
verification_plan: {
|
|
279
|
+
type: "array",
|
|
280
|
+
items: { type: "string" },
|
|
281
|
+
description: "Names of configured, enabled evidence.commands (see loop_policy.json evidence.commands) that verify the done_when claims. Max 20 items.",
|
|
282
|
+
},
|
|
283
|
+
scope: {
|
|
284
|
+
type: "array",
|
|
285
|
+
items: { type: "string" },
|
|
286
|
+
description: "Files/directories this round may touch (workspace-relative; './' and trailing slashes accepted). Out-of-scope git changes trigger round_scope_drift. Max 50 items.",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
required: ["done_when", "verification_plan", "scope"],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
161
292
|
},
|
|
162
293
|
},
|
|
163
|
-
required: ["sessionId", "
|
|
294
|
+
required: ["sessionId", "roundId", "evaluation"],
|
|
164
295
|
},
|
|
165
296
|
},
|
|
166
297
|
{
|
|
167
298
|
name: "loopforge_status",
|
|
168
|
-
description: "
|
|
299
|
+
description: "Inspect loop state. view=session (default): session status, round identity, success trajectory and typed projection. view=loop: loop health (goal alignment, constraints, drift). view=all: summary of every persisted loop. view=audit: read-only end-of-loop verification audit (claims, gates, verdict, sequence integrity).",
|
|
169
300
|
inputSchema: {
|
|
170
301
|
type: "object",
|
|
171
302
|
properties: {
|
|
172
|
-
scope: { type: "string", enum: ["session", "loop", "all"] },
|
|
173
303
|
sessionId: {
|
|
174
304
|
type: "string",
|
|
175
|
-
description: "Session ID returned by loopforge_start.",
|
|
305
|
+
description: "Session ID returned by loopforge_start (views session/audit).",
|
|
176
306
|
},
|
|
177
|
-
loopId: {
|
|
178
|
-
detail: { type: "string", enum: ["summary", "full"] },
|
|
179
|
-
},
|
|
180
|
-
required: ["scope"],
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
name: "loopforge_retry",
|
|
185
|
-
description: "Submit a corrected round report for the current rejected logical round. The round ID and pre-round evidence baseline remain unchanged.",
|
|
186
|
-
inputSchema: {
|
|
187
|
-
type: "object",
|
|
188
|
-
properties: {
|
|
189
|
-
sessionId: {
|
|
307
|
+
loopId: {
|
|
190
308
|
type: "string",
|
|
191
|
-
description: "
|
|
309
|
+
description: "Loop ID (views loop/audit).",
|
|
192
310
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
311
|
+
view: {
|
|
312
|
+
type: "string",
|
|
313
|
+
enum: ["session", "loop", "all", "audit"],
|
|
314
|
+
description: "Which view to return. Defaults to session.",
|
|
196
315
|
},
|
|
197
316
|
},
|
|
198
|
-
required: [
|
|
317
|
+
required: [],
|
|
199
318
|
},
|
|
200
319
|
},
|
|
201
320
|
{
|
|
202
|
-
name: "
|
|
203
|
-
description: "
|
|
321
|
+
name: "loopforge_stop",
|
|
322
|
+
description: "Manually stop a loop session and return its final trajectory.",
|
|
204
323
|
inputSchema: {
|
|
205
324
|
type: "object",
|
|
206
325
|
properties: {
|
|
207
326
|
sessionId: {
|
|
208
327
|
type: "string",
|
|
209
|
-
description: "Session ID
|
|
328
|
+
description: "Session ID to stop.",
|
|
210
329
|
},
|
|
211
330
|
},
|
|
212
331
|
required: ["sessionId"],
|
|
213
332
|
},
|
|
214
333
|
},
|
|
215
334
|
{
|
|
216
|
-
name: "
|
|
217
|
-
description: "
|
|
218
|
-
inputSchema: {
|
|
219
|
-
type: "object",
|
|
220
|
-
properties: {
|
|
221
|
-
loopId: { type: "string" },
|
|
222
|
-
view: { type: "string", enum: ["round", "timeline"] },
|
|
223
|
-
round: { type: "integer", minimum: 1 },
|
|
224
|
-
fromSequence: { type: "integer", minimum: 1 },
|
|
225
|
-
toSequence: { type: "integer", minimum: 1 },
|
|
226
|
-
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
227
|
-
},
|
|
228
|
-
required: ["loopId", "view"],
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
name: "loopforge_stop",
|
|
233
|
-
description: "Manually stop a loop session and return its final trajectory.",
|
|
335
|
+
name: "loopforge_pause",
|
|
336
|
+
description: "Pause a running loop session. The loop suspends at the next round boundary and its state is persisted to vault. Paused loops can be resumed with loopforge_resume. Use this to interrupt a long-running loop without losing progress.",
|
|
234
337
|
inputSchema: {
|
|
235
338
|
type: "object",
|
|
236
339
|
properties: {
|
|
237
340
|
sessionId: {
|
|
238
341
|
type: "string",
|
|
239
|
-
description: "Session ID to
|
|
342
|
+
description: "Session ID to pause.",
|
|
240
343
|
},
|
|
241
344
|
},
|
|
242
345
|
required: ["sessionId"],
|
|
243
346
|
},
|
|
244
347
|
},
|
|
245
348
|
{
|
|
246
|
-
name: "
|
|
247
|
-
description: "
|
|
349
|
+
name: "loopforge_replay",
|
|
350
|
+
description: "Replay a completed or running loop as an auditable round timeline. " +
|
|
351
|
+
"Pass sessionId for a live session, or loopId to replay straight from " +
|
|
352
|
+
"the vault (works after a process restart, when no session is in " +
|
|
353
|
+
"memory) — the timeline is derived from committed round documents.",
|
|
248
354
|
inputSchema: {
|
|
249
355
|
type: "object",
|
|
250
356
|
properties: {
|
|
251
357
|
sessionId: {
|
|
252
358
|
type: "string",
|
|
253
|
-
description: "Session ID to
|
|
359
|
+
description: "Session ID to replay (live sessions).",
|
|
360
|
+
},
|
|
361
|
+
loopId: {
|
|
362
|
+
type: "string",
|
|
363
|
+
description: "Loop ID to replay from the vault — works without a " +
|
|
364
|
+
"live session, e.g. after a restart.",
|
|
254
365
|
},
|
|
255
366
|
},
|
|
256
|
-
|
|
367
|
+
// v3.3.1: either identifier resolves the timeline; sessionId wins when
|
|
368
|
+
// both are given (it also disambiguates re-used loopIds).
|
|
369
|
+
required: [],
|
|
257
370
|
},
|
|
258
371
|
},
|
|
259
372
|
{
|
|
@@ -272,37 +385,42 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
272
385
|
},
|
|
273
386
|
{
|
|
274
387
|
name: "loopforge_gate_check",
|
|
275
|
-
description: "Classify
|
|
388
|
+
description: "v2.12: Classify a high-risk action or blocker text as a user gate (needs human authorization) or an agent gate (resolvable with evidence). Read-only — nothing is recorded. Use the returned gate id with loopforge_gate_resolve after a human decision.",
|
|
276
389
|
inputSchema: {
|
|
277
390
|
type: "object",
|
|
278
391
|
properties: {
|
|
279
|
-
|
|
392
|
+
gateText: {
|
|
393
|
+
type: "string",
|
|
394
|
+
description: "The action or blocker text to classify, e.g. 'Deploy to production'.",
|
|
395
|
+
},
|
|
280
396
|
},
|
|
281
|
-
required: ["
|
|
397
|
+
required: ["gateText"],
|
|
282
398
|
},
|
|
283
399
|
},
|
|
284
400
|
{
|
|
285
401
|
name: "loopforge_gate_resolve",
|
|
286
|
-
description: "Record
|
|
402
|
+
description: "v2.12: Record a human decision for a previously opened user gate. The gate id embeds the action hash — if the action text changed, the approval expires automatically. Read-only for agent gates (they are resolved by submitting evidence).",
|
|
287
403
|
inputSchema: {
|
|
288
404
|
type: "object",
|
|
289
405
|
properties: {
|
|
290
|
-
sessionId: {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
406
|
+
sessionId: {
|
|
407
|
+
type: "string",
|
|
408
|
+
description: "Session ID whose loop owns the gate.",
|
|
409
|
+
},
|
|
410
|
+
gateId: {
|
|
411
|
+
type: "string",
|
|
412
|
+
description: "Gate id returned by loopforge_gate_check or recorded from a blocked round.",
|
|
413
|
+
},
|
|
414
|
+
approved: {
|
|
415
|
+
type: "boolean",
|
|
416
|
+
description: "Whether the human authorizes the action.",
|
|
417
|
+
},
|
|
418
|
+
note: {
|
|
419
|
+
type: "string",
|
|
420
|
+
description: "Optional human note recorded with the decision.",
|
|
421
|
+
},
|
|
295
422
|
},
|
|
296
|
-
required: ["sessionId", "gateId", "
|
|
297
|
-
},
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
name: "loopforge_audit",
|
|
301
|
-
description: "Run a read-only final audit over accepted vault events, evidence claims, checks, and gate decisions.",
|
|
302
|
-
inputSchema: {
|
|
303
|
-
type: "object",
|
|
304
|
-
properties: { loopId: { type: "string" } },
|
|
305
|
-
required: ["loopId"],
|
|
423
|
+
required: ["sessionId", "gateId", "approved"],
|
|
306
424
|
},
|
|
307
425
|
},
|
|
308
426
|
];
|
|
@@ -310,33 +428,34 @@ const ADVANCE_OUTPUT_SCHEMA = {
|
|
|
310
428
|
type: "object",
|
|
311
429
|
properties: {
|
|
312
430
|
error: { type: "string" },
|
|
431
|
+
details: { type: "object", additionalProperties: true },
|
|
313
432
|
sessionId: { type: "string" },
|
|
314
433
|
round: { type: "number" },
|
|
315
434
|
roundId: { type: ["string", "null"] },
|
|
316
435
|
prompt: { type: ["string", "null"] },
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
level: { type: "string" },
|
|
322
|
-
roundSuccess: { type: "boolean" },
|
|
436
|
+
stopReason: { type: ["string", "null"] },
|
|
437
|
+
stopDetail: { type: ["string", "null"], description: "Human-readable context for why the loop stopped. Provides facts without prescribing a specific agent action." },
|
|
438
|
+
level: { type: ["string", "null"] },
|
|
439
|
+
roundSuccess: { type: ["boolean", "null"] },
|
|
323
440
|
enforcementAction: { enum: ["accept", "reject", "terminate", "backtrack"] },
|
|
324
|
-
enforcementReason: { type: "string" },
|
|
325
|
-
projection: { type: "object", additionalProperties: true },
|
|
326
|
-
evidenceSummary: { type: "object", additionalProperties: true },
|
|
327
|
-
stateHash: { type: "string" },
|
|
328
|
-
promptHash: { type: "string" },
|
|
441
|
+
enforcementReason: { type: ["string", "null"] },
|
|
329
442
|
warnings: { type: "array", items: { type: "string" } },
|
|
443
|
+
projection: {
|
|
444
|
+
type: "object",
|
|
445
|
+
additionalProperties: true,
|
|
446
|
+
description: "v2.12: Typed cognitive state projection (focus/todo/phase/delegation/handoff).",
|
|
447
|
+
},
|
|
330
448
|
},
|
|
331
449
|
additionalProperties: true,
|
|
332
450
|
};
|
|
333
|
-
|
|
451
|
+
/** Declared structured-output contracts for every tool. The MCP server
|
|
452
|
+
* validates each handler's output against these before returning it —
|
|
453
|
+
* a mismatch is a contract bug, surfaced as a JSON-RPC error instead of
|
|
454
|
+
* silently shipping an output that violates the declared schema. */
|
|
455
|
+
export const TOOL_OUTPUT_SCHEMAS = {
|
|
334
456
|
loopforge_start: ADVANCE_OUTPUT_SCHEMA,
|
|
335
457
|
loopforge_next: ADVANCE_OUTPUT_SCHEMA,
|
|
336
|
-
loopforge_retry: ADVANCE_OUTPUT_SCHEMA,
|
|
337
|
-
loopforge_backtrack: ADVANCE_OUTPUT_SCHEMA,
|
|
338
458
|
loopforge_resume: ADVANCE_OUTPUT_SCHEMA,
|
|
339
|
-
loopforge_history: { type: "object", additionalProperties: true },
|
|
340
459
|
loopforge_status: {
|
|
341
460
|
type: "object",
|
|
342
461
|
properties: {
|
|
@@ -350,6 +469,10 @@ const TOOL_OUTPUT_SCHEMAS = {
|
|
|
350
469
|
successTrajectory: { type: "array", items: { type: "boolean" } },
|
|
351
470
|
lease: { type: ["object", "null"], additionalProperties: true },
|
|
352
471
|
metrics: { type: "object", additionalProperties: true },
|
|
472
|
+
// v3.5: the ACTIVE Round Contract governing the next round (derived
|
|
473
|
+
// from committed rounds — null when the whole task is the Current
|
|
474
|
+
// Task).
|
|
475
|
+
activeContract: { type: ["object", "null"] },
|
|
353
476
|
},
|
|
354
477
|
additionalProperties: true,
|
|
355
478
|
},
|
|
@@ -373,17 +496,47 @@ const TOOL_OUTPUT_SCHEMAS = {
|
|
|
373
496
|
},
|
|
374
497
|
additionalProperties: true,
|
|
375
498
|
},
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
499
|
+
loopforge_replay: {
|
|
500
|
+
type: "object",
|
|
501
|
+
properties: {
|
|
502
|
+
error: { type: "string" },
|
|
503
|
+
sessionId: { type: "string" },
|
|
504
|
+
loopId: { type: "string" },
|
|
505
|
+
timeline: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
506
|
+
},
|
|
507
|
+
additionalProperties: true,
|
|
508
|
+
},
|
|
509
|
+
loopforge_gate_check: {
|
|
510
|
+
type: "object",
|
|
511
|
+
properties: {
|
|
512
|
+
error: { type: "string" },
|
|
513
|
+
id: { type: "string" },
|
|
514
|
+
kind: { type: "string" },
|
|
515
|
+
question: { type: "string" },
|
|
516
|
+
blockedScope: { type: "array", items: { type: "string" } },
|
|
517
|
+
allowedWork: { type: "array", items: { type: "string" } },
|
|
518
|
+
problem: { type: "string" },
|
|
519
|
+
requiredEvidence: { type: "array", items: { type: "string" } },
|
|
520
|
+
suggestedResolution: { type: "string" },
|
|
521
|
+
},
|
|
522
|
+
additionalProperties: true,
|
|
523
|
+
},
|
|
524
|
+
loopforge_gate_resolve: {
|
|
525
|
+
type: "object",
|
|
526
|
+
properties: {
|
|
527
|
+
error: { type: "string" },
|
|
528
|
+
sessionId: { type: "string" },
|
|
529
|
+
loopId: { type: "string" },
|
|
530
|
+
gateId: { type: "string" },
|
|
531
|
+
approved: { type: "boolean" },
|
|
532
|
+
},
|
|
533
|
+
additionalProperties: true,
|
|
534
|
+
},
|
|
379
535
|
};
|
|
380
536
|
function closeObjectSchemas(schema) {
|
|
381
537
|
const result = { ...schema };
|
|
382
538
|
if (result.type === "object") {
|
|
383
|
-
|
|
384
|
-
// Fixed-shape objects remain closed by default.
|
|
385
|
-
if (!("additionalProperties" in result))
|
|
386
|
-
result.additionalProperties = false;
|
|
539
|
+
result.additionalProperties = false;
|
|
387
540
|
if (isRecord(result.properties)) {
|
|
388
541
|
result.properties = Object.fromEntries(Object.entries(result.properties).map(([key, value]) => [
|
|
389
542
|
key,
|
|
@@ -396,10 +549,36 @@ function closeObjectSchemas(schema) {
|
|
|
396
549
|
}
|
|
397
550
|
return result;
|
|
398
551
|
}
|
|
552
|
+
const CORE_EVALUATION_FIELDS = new Set([
|
|
553
|
+
"success",
|
|
554
|
+
"output_summary",
|
|
555
|
+
"constraint_violations",
|
|
556
|
+
"should_continue",
|
|
557
|
+
]);
|
|
558
|
+
/** Keep the four authoritative fields schema-strict while leaving optional
|
|
559
|
+
* reporting fields available for buildSelfEvaluation's lossy normalization.
|
|
560
|
+
* Their descriptions remain in tools/list, but malformed optional values do
|
|
561
|
+
* not become JSON-RPC argument failures before the runtime can ignore them. */
|
|
562
|
+
function prepareInputSchema(name, schema) {
|
|
563
|
+
const result = closeObjectSchemas(schema);
|
|
564
|
+
if (name !== "loopforge_next" || !isRecord(result.properties))
|
|
565
|
+
return result;
|
|
566
|
+
const evaluation = result.properties.evaluation;
|
|
567
|
+
if (!isRecord(evaluation) || !isRecord(evaluation.properties))
|
|
568
|
+
return result;
|
|
569
|
+
evaluation.properties = Object.fromEntries(Object.entries(evaluation.properties).map(([field, value]) => {
|
|
570
|
+
if (CORE_EVALUATION_FIELDS.has(field) || !isRecord(value))
|
|
571
|
+
return [field, value];
|
|
572
|
+
return [field, typeof value.description === "string"
|
|
573
|
+
? { description: value.description }
|
|
574
|
+
: {}];
|
|
575
|
+
}));
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
399
578
|
/** MCP tool contracts include strict input and structured output schemas. */
|
|
400
579
|
export const TOOL_SCHEMAS = TOOL_BASE_SCHEMAS.map((schema) => ({
|
|
401
580
|
...schema,
|
|
402
|
-
inputSchema:
|
|
581
|
+
inputSchema: prepareInputSchema(schema.name, schema.inputSchema),
|
|
403
582
|
outputSchema: TOOL_OUTPUT_SCHEMAS[schema.name] ?? {
|
|
404
583
|
type: "object",
|
|
405
584
|
additionalProperties: true,
|
|
@@ -411,12 +590,28 @@ export class ToolInputValidationError extends Error {
|
|
|
411
590
|
this.name = "ToolInputValidationError";
|
|
412
591
|
}
|
|
413
592
|
}
|
|
593
|
+
function typeMatches(value, type) {
|
|
594
|
+
switch (type) {
|
|
595
|
+
case "object": return isRecord(value);
|
|
596
|
+
case "array": return Array.isArray(value);
|
|
597
|
+
case "string": return typeof value === "string";
|
|
598
|
+
case "boolean": return typeof value === "boolean";
|
|
599
|
+
case "number": return typeof value === "number" && Number.isFinite(value);
|
|
600
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
601
|
+
case "null": return value === null;
|
|
602
|
+
default: return true; // undeclared type — no constraint
|
|
603
|
+
}
|
|
604
|
+
}
|
|
414
605
|
function validateSchema(value, schema, path) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
606
|
+
// v2.14: an explicitly-undefined optional field is the same as absent —
|
|
607
|
+
// output objects carry `key: undefined` after spreads of optional fields.
|
|
608
|
+
if (value === undefined)
|
|
609
|
+
return;
|
|
610
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
611
|
+
if (!types.some((type) => typeMatches(value, type))) {
|
|
612
|
+
throw new ToolInputValidationError(`${path} must be ${types.join(" or ")}`);
|
|
613
|
+
}
|
|
614
|
+
if (types.some((type) => type === "object") && isRecord(value)) {
|
|
420
615
|
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
421
616
|
const required = Array.isArray(schema.required)
|
|
422
617
|
? schema.required.filter((item) => typeof item === "string")
|
|
@@ -440,34 +635,23 @@ function validateSchema(value, schema, path) {
|
|
|
440
635
|
}
|
|
441
636
|
return;
|
|
442
637
|
}
|
|
443
|
-
if (type === "array") {
|
|
444
|
-
if (!Array.isArray(value)) {
|
|
445
|
-
throw new ToolInputValidationError(`${path} must be an array`);
|
|
446
|
-
}
|
|
638
|
+
if (types.some((type) => type === "array") && Array.isArray(value)) {
|
|
447
639
|
if (isRecord(schema.items)) {
|
|
448
640
|
value.forEach((item, index) => validateSchema(item, schema.items, `${path}[${index}]`));
|
|
449
641
|
}
|
|
450
642
|
return;
|
|
451
643
|
}
|
|
452
|
-
if (type === "string" && typeof value
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
if (type === "string" && typeof value === "string" &&
|
|
459
|
-
typeof schema.pattern === "string" && !new RegExp(schema.pattern).test(value)) {
|
|
460
|
-
throw new ToolInputValidationError(`${path} must match ${schema.pattern}`);
|
|
461
|
-
}
|
|
462
|
-
if (type === "boolean" && typeof value !== "boolean") {
|
|
463
|
-
throw new ToolInputValidationError(`${path} must be a boolean`);
|
|
464
|
-
}
|
|
465
|
-
if (type === "number" || type === "integer") {
|
|
466
|
-
if (typeof value !== "number" ||
|
|
467
|
-
!Number.isFinite(value) ||
|
|
468
|
-
(type === "integer" && !Number.isInteger(value))) {
|
|
469
|
-
throw new ToolInputValidationError(`${path} must be ${type === "integer" ? "an integer" : "a number"}`);
|
|
644
|
+
if (types.some((type) => type === "string") && typeof value === "string") {
|
|
645
|
+
// Enforce declared enums — the schemas advertise them, so validation
|
|
646
|
+
// must too (v2.12: view/gateText enums were previously unchecked).
|
|
647
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
|
|
648
|
+
throw new ToolInputValidationError(`${path} must be one of: ${schema.enum.join(", ")}`);
|
|
470
649
|
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if ((types.some((type) => type === "number") ||
|
|
653
|
+
types.some((type) => type === "integer")) &&
|
|
654
|
+
typeof value === "number") {
|
|
471
655
|
if (typeof schema.minimum === "number" && value < schema.minimum) {
|
|
472
656
|
throw new ToolInputValidationError(`${path} must be >= ${schema.minimum}`);
|
|
473
657
|
}
|
|
@@ -482,54 +666,44 @@ export function validateToolInput(name, input) {
|
|
|
482
666
|
throw new ToolInputValidationError(`Unknown tool: ${name}`);
|
|
483
667
|
validateSchema(input, contract.inputSchema, "arguments");
|
|
484
668
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
: stopReason ? "stopped" : "accepted";
|
|
496
|
-
const artifact = session?.roundSnapshot?.promptArtifact;
|
|
497
|
-
const criteria = isRecord(audit?.criteria) ? audit.criteria : {};
|
|
498
|
-
const checks = isRecord(audit?.checks) ? audit.checks : {};
|
|
499
|
-
return {
|
|
500
|
-
...result,
|
|
501
|
-
decision,
|
|
502
|
-
attempt: session?.roundSnapshot?.attempt ?? result.attempt,
|
|
503
|
-
projection: projection ? {
|
|
504
|
-
phase: Array.isArray(projection.phaseHistory)
|
|
505
|
-
? projection.phaseHistory.at(-1)?.label ?? ""
|
|
506
|
-
: "",
|
|
507
|
-
focus: projection.currentFocus ?? "",
|
|
508
|
-
todo: projection.todoProjection ?? null,
|
|
509
|
-
gate: projection.activeGate ?? null,
|
|
510
|
-
} : undefined,
|
|
511
|
-
evidenceSummary: {
|
|
512
|
-
verified: Array.isArray(criteria.evidenced) ? criteria.evidenced : [],
|
|
513
|
-
contradicted: Array.isArray(checks.failed) ? checks.failed : [],
|
|
514
|
-
unavailable: Array.isArray(checks.unavailable) ? checks.unavailable : [],
|
|
515
|
-
},
|
|
516
|
-
stateHash: artifact?.stateHash,
|
|
517
|
-
promptHash: artifact?.promptHash,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
/** Uniform decision enrichment for advance-family tools.
|
|
521
|
-
*
|
|
522
|
-
* Handler-level `{error}` responses and session-level lease conflicts are
|
|
523
|
-
* not loop decisions — without this guard the enforcement-only derivation
|
|
524
|
-
* above would label them "stopped" (any stopReason) or even "accepted"
|
|
525
|
-
* (a bare error with no enforcement fields). */
|
|
526
|
-
function finalizeAdvanceOutput(mgr, result) {
|
|
527
|
-
const stopReason = String(result.stopReason ?? "");
|
|
528
|
-
if ((result.error !== undefined && result.error !== null) ||
|
|
529
|
-
stopReason.startsWith("session_owned_elsewhere")) {
|
|
530
|
-
return { ...result, decision: "errored" };
|
|
669
|
+
/** Validate a server dispatch without turning a malformed evaluation into a
|
|
670
|
+
* JSON-RPC transport error. The advertised schema stays strict so clients can
|
|
671
|
+
* construct valid calls; the runtime envelope validates every outer argument,
|
|
672
|
+
* then lets loopforge_next return its retryable evaluation_invalid payload for
|
|
673
|
+
* core evaluation mistakes. Once the core is valid, the full schema still
|
|
674
|
+
* rejects unknown fields and other contract violations. */
|
|
675
|
+
export function validateToolDispatchInput(name, input) {
|
|
676
|
+
if (name !== "loopforge_next") {
|
|
677
|
+
validateToolInput(name, input);
|
|
678
|
+
return;
|
|
531
679
|
}
|
|
532
|
-
|
|
680
|
+
const contract = TOOL_SCHEMAS.find((schema) => schema.name === name);
|
|
681
|
+
if (!contract)
|
|
682
|
+
throw new ToolInputValidationError(`Unknown tool: ${name}`);
|
|
683
|
+
const properties = isRecord(contract.inputSchema.properties)
|
|
684
|
+
? contract.inputSchema.properties
|
|
685
|
+
: {};
|
|
686
|
+
validateSchema(input, {
|
|
687
|
+
...contract.inputSchema,
|
|
688
|
+
required: ["sessionId", "roundId"],
|
|
689
|
+
properties: { ...properties, evaluation: {} },
|
|
690
|
+
}, "arguments");
|
|
691
|
+
if (!isRecord(input.evaluation))
|
|
692
|
+
return;
|
|
693
|
+
const validation = validateCoreSelfEvaluation(input.evaluation);
|
|
694
|
+
if (validation.missing.length > 0 || validation.invalid.length > 0)
|
|
695
|
+
return;
|
|
696
|
+
validateSchema(input, contract.inputSchema, "arguments");
|
|
697
|
+
}
|
|
698
|
+
/** Validate a handler's output against the tool's declared output schema.
|
|
699
|
+
* v2.14: the schemas were advertised in tools/list but never enforced —
|
|
700
|
+
* a mismatched output silently violated the declared contract. Throws
|
|
701
|
+
* ToolInputValidationError on mismatch. */
|
|
702
|
+
export function validateToolOutput(name, output) {
|
|
703
|
+
const schema = TOOL_OUTPUT_SCHEMAS[name];
|
|
704
|
+
if (!schema)
|
|
705
|
+
return;
|
|
706
|
+
validateSchema(output, schema, `output.${name}`);
|
|
533
707
|
}
|
|
534
708
|
export const TOOL_HANDLERS = {
|
|
535
709
|
async loopforge_start(mgr, input) {
|
|
@@ -542,92 +716,113 @@ export const TOOL_HANDLERS = {
|
|
|
542
716
|
constraints: Array.isArray(input.constraints)
|
|
543
717
|
? input.constraints
|
|
544
718
|
: undefined,
|
|
545
|
-
successCriteria: Array.isArray(input.successCriteria)
|
|
546
|
-
? input.successCriteria
|
|
547
|
-
: undefined,
|
|
548
719
|
};
|
|
549
720
|
if (!startInput.task.trim()) {
|
|
550
|
-
return { error: "task is required and must be non-empty"
|
|
721
|
+
return { error: "task is required and must be non-empty" };
|
|
551
722
|
}
|
|
552
|
-
|
|
723
|
+
// v2.14: entry-point validation — a bad maxRounds previously passed
|
|
724
|
+
// through and stopped the loop on the first round; an invalid loopId
|
|
725
|
+
// exploded later as an internal storage error instead of a clean
|
|
726
|
+
// argument error.
|
|
553
727
|
if (startInput.maxRounds !== undefined &&
|
|
554
|
-
(!Number.isInteger(startInput.maxRounds) || startInput.maxRounds < 1
|
|
555
|
-
return { error:
|
|
728
|
+
(!Number.isInteger(startInput.maxRounds) || startInput.maxRounds < 1)) {
|
|
729
|
+
return { error: "maxRounds must be a positive integer" };
|
|
730
|
+
}
|
|
731
|
+
if (startInput.loopId !== undefined) {
|
|
732
|
+
try {
|
|
733
|
+
validateLoopId(startInput.loopId);
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
return { error: `invalid loopId: ${error.message}` };
|
|
737
|
+
}
|
|
556
738
|
}
|
|
557
739
|
const result = await mgr.create(startInput);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
};
|
|
740
|
+
// v2.14: a same-process duplicate loopId surfaces as a clean argument
|
|
741
|
+
// error instead of a prompt-less "success" the client cannot act on.
|
|
742
|
+
if (typeof result.stopReason === "string" &&
|
|
743
|
+
result.stopReason.startsWith("loop_already_running:")) {
|
|
744
|
+
return { error: result.stopDetail ?? "loop already running" };
|
|
745
|
+
}
|
|
746
|
+
return { ...result };
|
|
565
747
|
},
|
|
566
748
|
async loopforge_next(mgr, input) {
|
|
567
749
|
const sessionId = String(input.sessionId ?? "");
|
|
750
|
+
const output = String(input.output ?? "");
|
|
751
|
+
const rawEval = input.evaluation;
|
|
752
|
+
const roundId = input.roundId === undefined ? undefined : String(input.roundId);
|
|
568
753
|
if (!sessionId)
|
|
569
|
-
return { error: "sessionId is required"
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
754
|
+
return { error: "sessionId is required" };
|
|
755
|
+
// v3.0.1: every submission is anchored to the round it reports on. A
|
|
756
|
+
// missing roundId would silently re-process a stale submission against a
|
|
757
|
+
// later round — reject it here so the contract is enforced even when the
|
|
758
|
+
// handler is invoked directly (bypassing schema validation).
|
|
759
|
+
if (!roundId || !roundId.trim()) {
|
|
760
|
+
return { error: "roundId is required — pass the roundId from the most recent loopforge_start / loopforge_next / loopforge_resume / loopforge_status response" };
|
|
761
|
+
}
|
|
762
|
+
if (!rawEval || typeof rawEval !== "object" || Array.isArray(rawEval)) {
|
|
763
|
+
return {
|
|
764
|
+
error: "evaluation_invalid",
|
|
765
|
+
details: {
|
|
766
|
+
missing: ["evaluation"],
|
|
767
|
+
invalid: [{ field: "evaluation", expected: "object" }],
|
|
768
|
+
},
|
|
769
|
+
sessionId,
|
|
770
|
+
roundId,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const validation = validateCoreSelfEvaluation(rawEval);
|
|
774
|
+
if (validation.missing.length > 0 || validation.invalid.length > 0) {
|
|
775
|
+
return {
|
|
776
|
+
error: "evaluation_invalid",
|
|
777
|
+
details: validation,
|
|
778
|
+
sessionId,
|
|
779
|
+
roundId,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const preExtractedEval = buildSelfEvaluation(rawEval);
|
|
783
|
+
const result = await mgr.advance(sessionId, output, preExtractedEval, roundId);
|
|
784
|
+
// v2.12: Typed projection — runtime facts for the main agent, attached
|
|
785
|
+
// on every accepted/continued round (not on errors).
|
|
786
|
+
let projection = null;
|
|
787
|
+
if (result.enforcementAction !== "reject" && result.enforcementAction !== "terminate" &&
|
|
788
|
+
result.stopReason !== "session_not_found" && result.stopReason !== "stalled") {
|
|
789
|
+
projection = mgr.getProjection(sessionId);
|
|
790
|
+
}
|
|
791
|
+
return projection ? { ...result, projection } : { ...result };
|
|
592
792
|
},
|
|
593
793
|
async loopforge_status(mgr, input) {
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
794
|
+
const view = input.view === "loop" || input.view === "all" || input.view === "audit"
|
|
795
|
+
? input.view
|
|
796
|
+
: "session";
|
|
797
|
+
const sessionId = String(input.sessionId ?? "");
|
|
798
|
+
const loopId = String(input.loopId ?? "");
|
|
799
|
+
// v2.12: view=all — every persisted loop (formerly loopforge_list).
|
|
800
|
+
if (view === "all") {
|
|
801
|
+
return { sessions: mgr.list() };
|
|
601
802
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return { error: "scope=loop does not accept sessionId" };
|
|
605
|
-
const loopId = String(input.loopId ?? "").trim();
|
|
803
|
+
// v2.12: view=audit — read-only verification audit.
|
|
804
|
+
if (view === "audit") {
|
|
606
805
|
if (!loopId)
|
|
607
|
-
return { error: "loopId is required for
|
|
806
|
+
return { error: "loopId is required for view=audit" };
|
|
807
|
+
const audit = mgr.getAudit(loopId);
|
|
808
|
+
if (!audit)
|
|
809
|
+
return { error: `no audit data found for loop "${loopId}"` };
|
|
810
|
+
return audit;
|
|
811
|
+
}
|
|
812
|
+
// v2.12: view=loop — loop health (formerly loopforge_health).
|
|
813
|
+
if (view === "loop") {
|
|
814
|
+
if (!loopId)
|
|
815
|
+
return { error: "loopId is required for view=loop" };
|
|
608
816
|
const health = mgr.getHealth(loopId);
|
|
609
|
-
|
|
610
|
-
return { error: `no data found for loop "${loopId}"` };
|
|
611
|
-
const audit = mgr.audit(loopId);
|
|
612
|
-
return input.detail === "full"
|
|
613
|
-
? { scope, loopId, health, audit }
|
|
614
|
-
: { scope, loopId, health };
|
|
817
|
+
return health ?? { error: `no health data for loop "${loopId}"` };
|
|
615
818
|
}
|
|
616
|
-
if (scope !== "session")
|
|
617
|
-
return { error: "scope must be session, loop, or all" };
|
|
618
|
-
if (hasLoopId)
|
|
619
|
-
return { error: "scope=session does not accept loopId" };
|
|
620
|
-
const sessionId = String(input.sessionId ?? "");
|
|
621
819
|
if (!sessionId)
|
|
622
|
-
return { error: "sessionId is required" };
|
|
820
|
+
return { error: "sessionId is required for view=session" };
|
|
623
821
|
const session = mgr.get(sessionId);
|
|
624
822
|
if (!session)
|
|
625
823
|
return { error: `session not found: ${sessionId}` };
|
|
626
|
-
const metrics = session.
|
|
627
|
-
|
|
628
|
-
const artifact = session.roundSnapshot?.promptArtifact;
|
|
629
|
-
const audit = mgr.audit(session.loopId);
|
|
630
|
-
const summary = {
|
|
824
|
+
const metrics = session.engine.getMetrics();
|
|
825
|
+
return {
|
|
631
826
|
sessionId: session.sessionId,
|
|
632
827
|
loopId: session.loopId,
|
|
633
828
|
round: session.currentRound,
|
|
@@ -636,40 +831,19 @@ export const TOOL_HANDLERS = {
|
|
|
636
831
|
status: session.status,
|
|
637
832
|
successTrajectory: session.successTrajectory,
|
|
638
833
|
lease: mgr.getLeaseStatus(session.loopId),
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
cognition: state ? {
|
|
645
|
-
phaseHistory: state.phaseHistory ?? [],
|
|
646
|
-
currentFocus: state.currentFocus ?? "",
|
|
647
|
-
todoProjection: state.todoProjection ?? null,
|
|
648
|
-
suggestedNextAction: state.suggestedNextAction ?? "",
|
|
649
|
-
activeGate: state.activeGate ?? null,
|
|
650
|
-
handoff: state.handoff ?? null,
|
|
651
|
-
stateFreshness: state.stateFreshness ?? "missing",
|
|
652
|
-
regressionObligations: state.regressionObligations ?? [],
|
|
653
|
-
} : null,
|
|
654
|
-
promptDiagnostics: artifact ? {
|
|
655
|
-
level: artifact.level,
|
|
656
|
-
reasons: artifact.levelReasons,
|
|
657
|
-
includedSections: artifact.includedSections,
|
|
658
|
-
omittedSections: artifact.omittedSections,
|
|
659
|
-
stateHash: artifact.stateHash,
|
|
660
|
-
promptHash: artifact.promptHash,
|
|
661
|
-
lastL2Round: artifact.lastL2Round,
|
|
662
|
-
lastMeaningfulProgressRound: artifact.lastMeaningfulProgressRound,
|
|
663
|
-
stateAgeRounds: artifact.stateAgeRounds,
|
|
664
|
-
} : null,
|
|
665
|
-
audit: {
|
|
666
|
-
verdict: audit.verdict,
|
|
667
|
-
missingCriteria: isRecord(audit.criteria) ? audit.criteria.missing : [],
|
|
668
|
-
unresolvedUserGates: audit.unresolvedUserGates ?? [],
|
|
669
|
-
},
|
|
834
|
+
// v3.5: derived ACTIVE Round Contract (display-only — the compile and
|
|
835
|
+
// verification gates derive the same value from the same committed
|
|
836
|
+
// evals via the shared round-contract walker).
|
|
837
|
+
activeContract: mgr.getActiveContract(sessionId),
|
|
838
|
+
projection: mgr.getProjection(sessionId),
|
|
670
839
|
metrics: {
|
|
671
840
|
vaultWriteErrors: metrics.vaultWriteErrors,
|
|
672
|
-
|
|
841
|
+
// v3.3.1: engine sessionStart was written but never read — surface it
|
|
842
|
+
// so a status view shows how long this engine session has been alive
|
|
843
|
+
// (observability design: the runtime is the loop's watchdog).
|
|
844
|
+
startedAt: metrics.sessionStart,
|
|
845
|
+
sessionAgeSeconds: Math.max(0, Math.floor((Date.now() - metrics.sessionStart) / 1000)),
|
|
846
|
+
policy: mgr.getPolicyMetrics(session.loopId),
|
|
673
847
|
},
|
|
674
848
|
};
|
|
675
849
|
},
|
|
@@ -683,17 +857,7 @@ export const TOOL_HANDLERS = {
|
|
|
683
857
|
const roundsCompleted = session.currentRound;
|
|
684
858
|
const successTrajectory = [...session.successTrajectory];
|
|
685
859
|
mgr.delete(sessionId);
|
|
686
|
-
|
|
687
|
-
return {
|
|
688
|
-
success: true,
|
|
689
|
-
sessionId,
|
|
690
|
-
loopId: session.loopId,
|
|
691
|
-
stopReason: "cancelled",
|
|
692
|
-
stopDetail: "The loop was manually stopped. Vault state was retained for audit and inspection.",
|
|
693
|
-
roundsCompleted,
|
|
694
|
-
successTrajectory,
|
|
695
|
-
audit,
|
|
696
|
-
};
|
|
860
|
+
return { success: true, roundsCompleted, successTrajectory };
|
|
697
861
|
},
|
|
698
862
|
async loopforge_pause(mgr, input) {
|
|
699
863
|
const sessionId = String(input.sessionId ?? "");
|
|
@@ -702,53 +866,63 @@ export const TOOL_HANDLERS = {
|
|
|
702
866
|
const result = mgr.pause(sessionId);
|
|
703
867
|
return { ...result };
|
|
704
868
|
},
|
|
705
|
-
async
|
|
706
|
-
const
|
|
707
|
-
const
|
|
708
|
-
if (!loopId)
|
|
709
|
-
return { error: "loopId is required" };
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
869
|
+
async loopforge_replay(mgr, input) {
|
|
870
|
+
const sessionId = String(input.sessionId ?? "");
|
|
871
|
+
const loopId = String(input.loopId ?? "");
|
|
872
|
+
if (!sessionId && !loopId) {
|
|
873
|
+
return { error: "sessionId or loopId is required — pass loopId to replay a loop after a restart (no live session)" };
|
|
874
|
+
}
|
|
875
|
+
// Live session first: it disambiguates re-used loopIds and the session
|
|
876
|
+
// registry is the cheapest lookup.
|
|
877
|
+
if (sessionId) {
|
|
878
|
+
const session = mgr.get(sessionId);
|
|
879
|
+
if (session) {
|
|
880
|
+
const timeline = mgr.replayTimeline(sessionId) ?? [];
|
|
881
|
+
return { sessionId, loopId: session.loopId, timeline };
|
|
882
|
+
}
|
|
883
|
+
// v3.3.1: session not in memory (restart). Fall through to the vault
|
|
884
|
+
// path when a loopId was also provided; otherwise say so plainly.
|
|
885
|
+
if (!loopId) {
|
|
886
|
+
return { error: `session not found: ${sessionId} (process restarted?) — pass loopId to replay from the vault` };
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// Vault-direct replay — the timeline lives in the committed round
|
|
890
|
+
// documents, not in the session registry.
|
|
891
|
+
const timeline = mgr.replayByLoop(loopId);
|
|
892
|
+
if (!timeline) {
|
|
893
|
+
return { error: `no committed rounds found for loop "${loopId}"` };
|
|
718
894
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const limit = typeof input.limit === "number" ? input.limit : 50;
|
|
722
|
-
const result = mgr.replayTimelineByLoop(loopId, fromSequence, toSequence, limit);
|
|
723
|
-
return { loopId, view, timeline: result.timeline, nextSequence: result.nextSequence };
|
|
895
|
+
// sessionId omitted on the vault path (its output schema is string-only).
|
|
896
|
+
return { ...(sessionId ? { sessionId } : {}), loopId, timeline };
|
|
724
897
|
},
|
|
725
898
|
async loopforge_resume(mgr, input) {
|
|
726
899
|
const loopId = String(input.loopId ?? "");
|
|
727
900
|
if (!loopId)
|
|
728
|
-
return { error: "loopId is required"
|
|
901
|
+
return { error: "loopId is required" };
|
|
902
|
+
try {
|
|
903
|
+
validateLoopId(loopId);
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
return { error: `invalid loopId: ${error.message}` };
|
|
907
|
+
}
|
|
729
908
|
// Paused recovery must run first so the persisted status is atomically
|
|
730
909
|
// changed back to running before a prompt is returned.
|
|
731
910
|
let result = await mgr.unpause(loopId);
|
|
732
911
|
if (!result) {
|
|
733
|
-
result =
|
|
912
|
+
result = mgr.resume(loopId);
|
|
734
913
|
}
|
|
735
914
|
if (!result)
|
|
736
|
-
return { error: `no saved session found for loop "${loopId}"
|
|
737
|
-
return
|
|
915
|
+
return { error: `no saved session found for loop "${loopId}"` };
|
|
916
|
+
return { ...result };
|
|
738
917
|
},
|
|
739
918
|
async loopforge_gate_check(mgr, input) {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
919
|
+
const gateText = String(input.gateText ?? "");
|
|
920
|
+
if (!gateText.trim())
|
|
921
|
+
return { error: "gateText is required" };
|
|
922
|
+
return mgr.checkGate(gateText);
|
|
743
923
|
},
|
|
744
924
|
async loopforge_gate_resolve(mgr, input) {
|
|
745
|
-
return mgr.resolveGate(String(input.sessionId ?? ""), String(input.gateId ?? ""),
|
|
746
|
-
},
|
|
747
|
-
async loopforge_audit(mgr, input) {
|
|
748
|
-
const loopId = String(input.loopId ?? "").trim();
|
|
749
|
-
if (!loopId)
|
|
750
|
-
return { error: "loopId is required" };
|
|
751
|
-
return mgr.audit(loopId);
|
|
925
|
+
return mgr.resolveGate(String(input.sessionId ?? ""), String(input.gateId ?? ""), input.approved === true, input.note === undefined ? undefined : String(input.note));
|
|
752
926
|
},
|
|
753
927
|
};
|
|
754
928
|
//# sourceMappingURL=tools.js.map
|