loopforge 3.0.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -145
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +158 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +28 -20
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +82 -173
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +25 -16
- package/dist/cli.js.map +1 -1
- 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/enforcement-gate.d.ts +76 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +1071 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +99 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +839 -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 +134 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/evidence-provider.d.ts +21 -23
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +74 -34
- 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 +41 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +632 -273
- 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 +26 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +142 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +112 -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 +216 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +926 -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 +28 -42
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -101
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +306 -1105
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -16
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +479 -413
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +12 -17
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +95 -161
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +44 -35
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +31 -115
- 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 +389 -160
- 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 +357 -229
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +139 -15
- 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 +19 -55
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +20 -24
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +99 -92
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +14 -13
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +26 -14
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +15 -18
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +38 -47
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +53 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +284 -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 +108 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +971 -0
- package/dist/verification-gate.js.map +1 -0
- package/loop_policy.json +44 -15
- package/package.json +5 -10
- package/skills/perception/SKILL.md +182 -199
- 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 { collectSelfEvalGaps } 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,122 +45,301 @@ 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. The evaluation parameter provides structured self-assessment — prefer this over embedding a ---loopforge-eval block in the output text.",
|
|
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. May be omitted if evaluation parameter is provided.",
|
|
69
|
+
},
|
|
70
|
+
evaluation: {
|
|
71
|
+
type: "object",
|
|
72
|
+
description: "Structured self-evaluation for this round. Either this or an output containing a ---loopforge-eval block is required for the loop to continue. Preferred over embedding ---loopforge-eval blocks in output text.",
|
|
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
|
+
expand: {
|
|
259
|
+
type: "array",
|
|
260
|
+
items: {
|
|
261
|
+
type: "string",
|
|
262
|
+
enum: ["milestones", "sub_goals", "constraint_lifecycle", "agent_trust", "progress", "loop_synthesis"],
|
|
263
|
+
},
|
|
264
|
+
description: "Sections to expand to full detail in the next prompt. Useful when you need full context on a specific area (e.g. full sub-goal dashboard when replanning). L1 max 1 section. L2 already expanded — redundant. L0 ignored.",
|
|
265
|
+
},
|
|
266
|
+
confusion_points: {
|
|
267
|
+
type: "array",
|
|
268
|
+
items: { type: "string" },
|
|
269
|
+
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.'",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
161
274
|
},
|
|
162
275
|
},
|
|
163
|
-
|
|
276
|
+
// v3.2.1: evaluation is optional at the schema level — the handler
|
|
277
|
+
// requires EITHER the evaluation parameter OR output containing a
|
|
278
|
+
// ---loopforge-eval block. Requiring it here made the embedded-block
|
|
279
|
+
// path dead code (input validation rejected it before the handler).
|
|
280
|
+
required: ["sessionId", "roundId"],
|
|
164
281
|
},
|
|
165
282
|
},
|
|
166
283
|
{
|
|
167
284
|
name: "loopforge_status",
|
|
168
|
-
description: "
|
|
285
|
+
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
286
|
inputSchema: {
|
|
170
287
|
type: "object",
|
|
171
288
|
properties: {
|
|
172
|
-
scope: { type: "string", enum: ["session", "loop", "all"] },
|
|
173
289
|
sessionId: {
|
|
174
290
|
type: "string",
|
|
175
|
-
description: "Session ID returned by loopforge_start.",
|
|
291
|
+
description: "Session ID returned by loopforge_start (views session/audit).",
|
|
176
292
|
},
|
|
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: {
|
|
293
|
+
loopId: {
|
|
190
294
|
type: "string",
|
|
191
|
-
description: "
|
|
295
|
+
description: "Loop ID (views loop/audit).",
|
|
192
296
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
297
|
+
view: {
|
|
298
|
+
type: "string",
|
|
299
|
+
enum: ["session", "loop", "all", "audit"],
|
|
300
|
+
description: "Which view to return. Defaults to session.",
|
|
196
301
|
},
|
|
197
302
|
},
|
|
198
|
-
required: [
|
|
303
|
+
required: [],
|
|
199
304
|
},
|
|
200
305
|
},
|
|
201
306
|
{
|
|
202
|
-
name: "
|
|
203
|
-
description: "
|
|
307
|
+
name: "loopforge_stop",
|
|
308
|
+
description: "Manually stop a loop session and return its final trajectory.",
|
|
204
309
|
inputSchema: {
|
|
205
310
|
type: "object",
|
|
206
311
|
properties: {
|
|
207
312
|
sessionId: {
|
|
208
313
|
type: "string",
|
|
209
|
-
description: "Session ID
|
|
314
|
+
description: "Session ID to stop.",
|
|
210
315
|
},
|
|
211
316
|
},
|
|
212
317
|
required: ["sessionId"],
|
|
213
318
|
},
|
|
214
319
|
},
|
|
215
320
|
{
|
|
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.",
|
|
321
|
+
name: "loopforge_pause",
|
|
322
|
+
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
323
|
inputSchema: {
|
|
235
324
|
type: "object",
|
|
236
325
|
properties: {
|
|
237
326
|
sessionId: {
|
|
238
327
|
type: "string",
|
|
239
|
-
description: "Session ID to
|
|
328
|
+
description: "Session ID to pause.",
|
|
240
329
|
},
|
|
241
330
|
},
|
|
242
331
|
required: ["sessionId"],
|
|
243
332
|
},
|
|
244
333
|
},
|
|
245
334
|
{
|
|
246
|
-
name: "
|
|
247
|
-
description: "
|
|
335
|
+
name: "loopforge_replay",
|
|
336
|
+
description: "Replay a completed or running loop session as an auditable round timeline.",
|
|
248
337
|
inputSchema: {
|
|
249
338
|
type: "object",
|
|
250
339
|
properties: {
|
|
251
340
|
sessionId: {
|
|
252
341
|
type: "string",
|
|
253
|
-
description: "Session ID to
|
|
342
|
+
description: "Session ID to replay.",
|
|
254
343
|
},
|
|
255
344
|
},
|
|
256
345
|
required: ["sessionId"],
|
|
@@ -272,37 +361,42 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
272
361
|
},
|
|
273
362
|
{
|
|
274
363
|
name: "loopforge_gate_check",
|
|
275
|
-
description: "Classify
|
|
364
|
+
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
365
|
inputSchema: {
|
|
277
366
|
type: "object",
|
|
278
367
|
properties: {
|
|
279
|
-
|
|
368
|
+
gateText: {
|
|
369
|
+
type: "string",
|
|
370
|
+
description: "The action or blocker text to classify, e.g. 'Deploy to production'.",
|
|
371
|
+
},
|
|
280
372
|
},
|
|
281
|
-
required: ["
|
|
373
|
+
required: ["gateText"],
|
|
282
374
|
},
|
|
283
375
|
},
|
|
284
376
|
{
|
|
285
377
|
name: "loopforge_gate_resolve",
|
|
286
|
-
description: "Record
|
|
378
|
+
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
379
|
inputSchema: {
|
|
288
380
|
type: "object",
|
|
289
381
|
properties: {
|
|
290
|
-
sessionId: {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
382
|
+
sessionId: {
|
|
383
|
+
type: "string",
|
|
384
|
+
description: "Session ID whose loop owns the gate.",
|
|
385
|
+
},
|
|
386
|
+
gateId: {
|
|
387
|
+
type: "string",
|
|
388
|
+
description: "Gate id returned by loopforge_gate_check or recorded from a blocked round.",
|
|
389
|
+
},
|
|
390
|
+
approved: {
|
|
391
|
+
type: "boolean",
|
|
392
|
+
description: "Whether the human authorizes the action.",
|
|
393
|
+
},
|
|
394
|
+
note: {
|
|
395
|
+
type: "string",
|
|
396
|
+
description: "Optional human note recorded with the decision.",
|
|
397
|
+
},
|
|
295
398
|
},
|
|
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"],
|
|
399
|
+
required: ["sessionId", "gateId", "approved"],
|
|
306
400
|
},
|
|
307
401
|
},
|
|
308
402
|
];
|
|
@@ -314,29 +408,34 @@ const ADVANCE_OUTPUT_SCHEMA = {
|
|
|
314
408
|
round: { type: "number" },
|
|
315
409
|
roundId: { type: ["string", "null"] },
|
|
316
410
|
prompt: { type: ["string", "null"] },
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
level: { type: "string" },
|
|
322
|
-
roundSuccess: { type: "boolean" },
|
|
411
|
+
stopReason: { type: ["string", "null"] },
|
|
412
|
+
stopDetail: { type: ["string", "null"], description: "Human-readable context for why the loop stopped. Provides facts without prescribing a specific agent action." },
|
|
413
|
+
level: { type: ["string", "null"] },
|
|
414
|
+
roundSuccess: { type: ["boolean", "null"] },
|
|
323
415
|
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" },
|
|
416
|
+
enforcementReason: { type: ["string", "null"] },
|
|
329
417
|
warnings: { type: "array", items: { type: "string" } },
|
|
418
|
+
parseGaps: {
|
|
419
|
+
type: "array",
|
|
420
|
+
items: { type: "object", additionalProperties: true },
|
|
421
|
+
description: "v2.12: Field-level format diagnostics for tolerated evaluation issues.",
|
|
422
|
+
},
|
|
423
|
+
projection: {
|
|
424
|
+
type: "object",
|
|
425
|
+
additionalProperties: true,
|
|
426
|
+
description: "v2.12: Typed cognitive state projection (focus/todo/phase/delegation/handoff).",
|
|
427
|
+
},
|
|
330
428
|
},
|
|
331
429
|
additionalProperties: true,
|
|
332
430
|
};
|
|
333
|
-
|
|
431
|
+
/** Declared structured-output contracts for every tool. The MCP server
|
|
432
|
+
* validates each handler's output against these before returning it —
|
|
433
|
+
* a mismatch is a contract bug, surfaced as a JSON-RPC error instead of
|
|
434
|
+
* silently shipping an output that violates the declared schema. */
|
|
435
|
+
export const TOOL_OUTPUT_SCHEMAS = {
|
|
334
436
|
loopforge_start: ADVANCE_OUTPUT_SCHEMA,
|
|
335
437
|
loopforge_next: ADVANCE_OUTPUT_SCHEMA,
|
|
336
|
-
loopforge_retry: ADVANCE_OUTPUT_SCHEMA,
|
|
337
|
-
loopforge_backtrack: ADVANCE_OUTPUT_SCHEMA,
|
|
338
438
|
loopforge_resume: ADVANCE_OUTPUT_SCHEMA,
|
|
339
|
-
loopforge_history: { type: "object", additionalProperties: true },
|
|
340
439
|
loopforge_status: {
|
|
341
440
|
type: "object",
|
|
342
441
|
properties: {
|
|
@@ -373,17 +472,47 @@ const TOOL_OUTPUT_SCHEMAS = {
|
|
|
373
472
|
},
|
|
374
473
|
additionalProperties: true,
|
|
375
474
|
},
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
475
|
+
loopforge_replay: {
|
|
476
|
+
type: "object",
|
|
477
|
+
properties: {
|
|
478
|
+
error: { type: "string" },
|
|
479
|
+
sessionId: { type: "string" },
|
|
480
|
+
loopId: { type: "string" },
|
|
481
|
+
timeline: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
482
|
+
},
|
|
483
|
+
additionalProperties: true,
|
|
484
|
+
},
|
|
485
|
+
loopforge_gate_check: {
|
|
486
|
+
type: "object",
|
|
487
|
+
properties: {
|
|
488
|
+
error: { type: "string" },
|
|
489
|
+
id: { type: "string" },
|
|
490
|
+
kind: { type: "string" },
|
|
491
|
+
question: { type: "string" },
|
|
492
|
+
blockedScope: { type: "array", items: { type: "string" } },
|
|
493
|
+
allowedWork: { type: "array", items: { type: "string" } },
|
|
494
|
+
problem: { type: "string" },
|
|
495
|
+
requiredEvidence: { type: "array", items: { type: "string" } },
|
|
496
|
+
suggestedResolution: { type: "string" },
|
|
497
|
+
},
|
|
498
|
+
additionalProperties: true,
|
|
499
|
+
},
|
|
500
|
+
loopforge_gate_resolve: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
error: { type: "string" },
|
|
504
|
+
sessionId: { type: "string" },
|
|
505
|
+
loopId: { type: "string" },
|
|
506
|
+
gateId: { type: "string" },
|
|
507
|
+
approved: { type: "boolean" },
|
|
508
|
+
},
|
|
509
|
+
additionalProperties: true,
|
|
510
|
+
},
|
|
379
511
|
};
|
|
380
512
|
function closeObjectSchemas(schema) {
|
|
381
513
|
const result = { ...schema };
|
|
382
514
|
if (result.type === "object") {
|
|
383
|
-
|
|
384
|
-
// Fixed-shape objects remain closed by default.
|
|
385
|
-
if (!("additionalProperties" in result))
|
|
386
|
-
result.additionalProperties = false;
|
|
515
|
+
result.additionalProperties = false;
|
|
387
516
|
if (isRecord(result.properties)) {
|
|
388
517
|
result.properties = Object.fromEntries(Object.entries(result.properties).map(([key, value]) => [
|
|
389
518
|
key,
|
|
@@ -411,12 +540,28 @@ export class ToolInputValidationError extends Error {
|
|
|
411
540
|
this.name = "ToolInputValidationError";
|
|
412
541
|
}
|
|
413
542
|
}
|
|
543
|
+
function typeMatches(value, type) {
|
|
544
|
+
switch (type) {
|
|
545
|
+
case "object": return isRecord(value);
|
|
546
|
+
case "array": return Array.isArray(value);
|
|
547
|
+
case "string": return typeof value === "string";
|
|
548
|
+
case "boolean": return typeof value === "boolean";
|
|
549
|
+
case "number": return typeof value === "number" && Number.isFinite(value);
|
|
550
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
551
|
+
case "null": return value === null;
|
|
552
|
+
default: return true; // undeclared type — no constraint
|
|
553
|
+
}
|
|
554
|
+
}
|
|
414
555
|
function validateSchema(value, schema, path) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
556
|
+
// v2.14: an explicitly-undefined optional field is the same as absent —
|
|
557
|
+
// output objects carry `key: undefined` after spreads of optional fields.
|
|
558
|
+
if (value === undefined)
|
|
559
|
+
return;
|
|
560
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
561
|
+
if (!types.some((type) => typeMatches(value, type))) {
|
|
562
|
+
throw new ToolInputValidationError(`${path} must be ${types.join(" or ")}`);
|
|
563
|
+
}
|
|
564
|
+
if (types.some((type) => type === "object") && isRecord(value)) {
|
|
420
565
|
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
421
566
|
const required = Array.isArray(schema.required)
|
|
422
567
|
? schema.required.filter((item) => typeof item === "string")
|
|
@@ -440,34 +585,23 @@ function validateSchema(value, schema, path) {
|
|
|
440
585
|
}
|
|
441
586
|
return;
|
|
442
587
|
}
|
|
443
|
-
if (type === "array") {
|
|
444
|
-
if (!Array.isArray(value)) {
|
|
445
|
-
throw new ToolInputValidationError(`${path} must be an array`);
|
|
446
|
-
}
|
|
588
|
+
if (types.some((type) => type === "array") && Array.isArray(value)) {
|
|
447
589
|
if (isRecord(schema.items)) {
|
|
448
590
|
value.forEach((item, index) => validateSchema(item, schema.items, `${path}[${index}]`));
|
|
449
591
|
}
|
|
450
592
|
return;
|
|
451
593
|
}
|
|
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"}`);
|
|
594
|
+
if (types.some((type) => type === "string") && typeof value === "string") {
|
|
595
|
+
// Enforce declared enums — the schemas advertise them, so validation
|
|
596
|
+
// must too (v2.12: view/gateText enums were previously unchecked).
|
|
597
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
|
|
598
|
+
throw new ToolInputValidationError(`${path} must be one of: ${schema.enum.join(", ")}`);
|
|
470
599
|
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if ((types.some((type) => type === "number") ||
|
|
603
|
+
types.some((type) => type === "integer")) &&
|
|
604
|
+
typeof value === "number") {
|
|
471
605
|
if (typeof schema.minimum === "number" && value < schema.minimum) {
|
|
472
606
|
throw new ToolInputValidationError(`${path} must be >= ${schema.minimum}`);
|
|
473
607
|
}
|
|
@@ -482,54 +616,15 @@ export function validateToolInput(name, input) {
|
|
|
482
616
|
throw new ToolInputValidationError(`Unknown tool: ${name}`);
|
|
483
617
|
validateSchema(input, contract.inputSchema, "arguments");
|
|
484
618
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
: enforcement === "terminate" ? "terminated"
|
|
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" };
|
|
531
|
-
}
|
|
532
|
-
return enrichAdvanceResult(mgr, result);
|
|
619
|
+
/** Validate a handler's output against the tool's declared output schema.
|
|
620
|
+
* v2.14: the schemas were advertised in tools/list but never enforced —
|
|
621
|
+
* a mismatched output silently violated the declared contract. Throws
|
|
622
|
+
* ToolInputValidationError on mismatch. */
|
|
623
|
+
export function validateToolOutput(name, output) {
|
|
624
|
+
const schema = TOOL_OUTPUT_SCHEMAS[name];
|
|
625
|
+
if (!schema)
|
|
626
|
+
return;
|
|
627
|
+
validateSchema(output, schema, `output.${name}`);
|
|
533
628
|
}
|
|
534
629
|
export const TOOL_HANDLERS = {
|
|
535
630
|
async loopforge_start(mgr, input) {
|
|
@@ -542,92 +637,112 @@ export const TOOL_HANDLERS = {
|
|
|
542
637
|
constraints: Array.isArray(input.constraints)
|
|
543
638
|
? input.constraints
|
|
544
639
|
: undefined,
|
|
545
|
-
successCriteria: Array.isArray(input.successCriteria)
|
|
546
|
-
? input.successCriteria
|
|
547
|
-
: undefined,
|
|
548
640
|
};
|
|
549
641
|
if (!startInput.task.trim()) {
|
|
550
|
-
return { error: "task is required and must be non-empty"
|
|
642
|
+
return { error: "task is required and must be non-empty" };
|
|
551
643
|
}
|
|
552
|
-
|
|
644
|
+
// v2.14: entry-point validation — a bad maxRounds previously passed
|
|
645
|
+
// through and stopped the loop on the first round; an invalid loopId
|
|
646
|
+
// exploded later as an internal storage error instead of a clean
|
|
647
|
+
// argument error.
|
|
553
648
|
if (startInput.maxRounds !== undefined &&
|
|
554
|
-
(!Number.isInteger(startInput.maxRounds) || startInput.maxRounds < 1
|
|
555
|
-
return { error:
|
|
649
|
+
(!Number.isInteger(startInput.maxRounds) || startInput.maxRounds < 1)) {
|
|
650
|
+
return { error: "maxRounds must be a positive integer" };
|
|
651
|
+
}
|
|
652
|
+
if (startInput.loopId !== undefined) {
|
|
653
|
+
try {
|
|
654
|
+
validateLoopId(startInput.loopId);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
return { error: `invalid loopId: ${error.message}` };
|
|
658
|
+
}
|
|
556
659
|
}
|
|
557
660
|
const result = await mgr.create(startInput);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
};
|
|
661
|
+
// v2.14: a same-process duplicate loopId surfaces as a clean argument
|
|
662
|
+
// error instead of a prompt-less "success" the client cannot act on.
|
|
663
|
+
if (typeof result.stopReason === "string" &&
|
|
664
|
+
result.stopReason.startsWith("loop_already_running:")) {
|
|
665
|
+
return { error: result.stopDetail ?? "loop already running" };
|
|
666
|
+
}
|
|
667
|
+
return { ...result };
|
|
565
668
|
},
|
|
566
669
|
async loopforge_next(mgr, input) {
|
|
567
670
|
const sessionId = String(input.sessionId ?? "");
|
|
671
|
+
const output = String(input.output ?? "");
|
|
672
|
+
const rawEval = input.evaluation;
|
|
673
|
+
const roundId = input.roundId === undefined ? undefined : String(input.roundId);
|
|
568
674
|
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
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
675
|
+
return { error: "sessionId is required" };
|
|
676
|
+
// v3.0.1: every submission is anchored to the round it reports on. A
|
|
677
|
+
// missing roundId would silently re-process a stale submission against a
|
|
678
|
+
// later round — reject it here so the contract is enforced even when the
|
|
679
|
+
// handler is invoked directly (bypassing schema validation).
|
|
680
|
+
if (!roundId || !roundId.trim()) {
|
|
681
|
+
return { error: "roundId is required — pass the roundId from the most recent loopforge_start / loopforge_next / loopforge_resume / loopforge_status response" };
|
|
682
|
+
}
|
|
683
|
+
// Build SelfEvaluation from structured evaluation parameter
|
|
684
|
+
const preExtractedEval = rawEval ? buildSelfEvaluation(rawEval) : undefined;
|
|
685
|
+
// Require at least one of: evaluation parameter or output with embedded eval block
|
|
686
|
+
if (!preExtractedEval && !output.trim()) {
|
|
687
|
+
return { error: "Either evaluation parameter or output with ---loopforge-eval block is required" };
|
|
688
|
+
}
|
|
689
|
+
const result = await mgr.advance(sessionId, output, preExtractedEval, roundId);
|
|
690
|
+
// v2.12: Typed projection — runtime facts for the main agent, attached
|
|
691
|
+
// on every accepted/continued round (not on errors).
|
|
692
|
+
let projection = null;
|
|
693
|
+
if (result.enforcementAction !== "reject" && result.enforcementAction !== "terminate" &&
|
|
694
|
+
result.stopReason !== "session_not_found" && result.stopReason !== "stalled") {
|
|
695
|
+
projection = mgr.getProjection(sessionId);
|
|
696
|
+
}
|
|
697
|
+
// v2.12: Field-level diagnostics for tolerated format issues. The
|
|
698
|
+
// lenient parser still accepts the evaluation — the gaps are surfaced
|
|
699
|
+
// as structured warnings so the agent can correct its template.
|
|
700
|
+
if (rawEval) {
|
|
701
|
+
const gaps = collectSelfEvalGaps(rawEval);
|
|
702
|
+
if (gaps.length > 0) {
|
|
703
|
+
return {
|
|
704
|
+
...result,
|
|
705
|
+
projection,
|
|
706
|
+
warnings: [...(result.warnings ?? []), ...gaps.map((g) => `[${g.field}] ${g.issue}`)],
|
|
707
|
+
parseGaps: gaps,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return projection ? { ...result, projection } : { ...result };
|
|
592
712
|
},
|
|
593
713
|
async loopforge_status(mgr, input) {
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
714
|
+
const view = input.view === "loop" || input.view === "all" || input.view === "audit"
|
|
715
|
+
? input.view
|
|
716
|
+
: "session";
|
|
717
|
+
const sessionId = String(input.sessionId ?? "");
|
|
718
|
+
const loopId = String(input.loopId ?? "");
|
|
719
|
+
// v2.12: view=all — every persisted loop (formerly loopforge_list).
|
|
720
|
+
if (view === "all") {
|
|
721
|
+
return { sessions: mgr.list() };
|
|
601
722
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return { error: "scope=loop does not accept sessionId" };
|
|
605
|
-
const loopId = String(input.loopId ?? "").trim();
|
|
723
|
+
// v2.12: view=audit — read-only verification audit.
|
|
724
|
+
if (view === "audit") {
|
|
606
725
|
if (!loopId)
|
|
607
|
-
return { error: "loopId is required for
|
|
726
|
+
return { error: "loopId is required for view=audit" };
|
|
727
|
+
const audit = mgr.getAudit(loopId);
|
|
728
|
+
if (!audit)
|
|
729
|
+
return { error: `no audit data found for loop "${loopId}"` };
|
|
730
|
+
return audit;
|
|
731
|
+
}
|
|
732
|
+
// v2.12: view=loop — loop health (formerly loopforge_health).
|
|
733
|
+
if (view === "loop") {
|
|
734
|
+
if (!loopId)
|
|
735
|
+
return { error: "loopId is required for view=loop" };
|
|
608
736
|
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 };
|
|
737
|
+
return health ?? { error: `no health data for loop "${loopId}"` };
|
|
615
738
|
}
|
|
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
739
|
if (!sessionId)
|
|
622
|
-
return { error: "sessionId is required" };
|
|
740
|
+
return { error: "sessionId is required for view=session" };
|
|
623
741
|
const session = mgr.get(sessionId);
|
|
624
742
|
if (!session)
|
|
625
743
|
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 = {
|
|
744
|
+
const metrics = session.engine.getMetrics();
|
|
745
|
+
return {
|
|
631
746
|
sessionId: session.sessionId,
|
|
632
747
|
loopId: session.loopId,
|
|
633
748
|
round: session.currentRound,
|
|
@@ -636,40 +751,10 @@ export const TOOL_HANDLERS = {
|
|
|
636
751
|
status: session.status,
|
|
637
752
|
successTrajectory: session.successTrajectory,
|
|
638
753
|
lease: mgr.getLeaseStatus(session.loopId),
|
|
639
|
-
|
|
640
|
-
if (input.detail !== "full")
|
|
641
|
-
return summary;
|
|
642
|
-
return {
|
|
643
|
-
...summary,
|
|
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
|
-
},
|
|
754
|
+
projection: mgr.getProjection(sessionId),
|
|
670
755
|
metrics: {
|
|
671
756
|
vaultWriteErrors: metrics.vaultWriteErrors,
|
|
672
|
-
policy: mgr.
|
|
757
|
+
policy: mgr.getPolicyMetrics(session.loopId),
|
|
673
758
|
},
|
|
674
759
|
};
|
|
675
760
|
},
|
|
@@ -683,17 +768,7 @@ export const TOOL_HANDLERS = {
|
|
|
683
768
|
const roundsCompleted = session.currentRound;
|
|
684
769
|
const successTrajectory = [...session.successTrajectory];
|
|
685
770
|
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
|
-
};
|
|
771
|
+
return { success: true, roundsCompleted, successTrajectory };
|
|
697
772
|
},
|
|
698
773
|
async loopforge_pause(mgr, input) {
|
|
699
774
|
const sessionId = String(input.sessionId ?? "");
|
|
@@ -702,53 +777,44 @@ export const TOOL_HANDLERS = {
|
|
|
702
777
|
const result = mgr.pause(sessionId);
|
|
703
778
|
return { ...result };
|
|
704
779
|
},
|
|
705
|
-
async
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (
|
|
711
|
-
return { error:
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
if (!Number.isInteger(round) || round < 1)
|
|
715
|
-
return { error: "round must be a positive integer" };
|
|
716
|
-
const inspected = mgr.inspect(loopId, round);
|
|
717
|
-
return inspected ?? { error: `round not found: ${loopId}#${round}` };
|
|
718
|
-
}
|
|
719
|
-
const fromSequence = typeof input.fromSequence === "number" ? input.fromSequence : undefined;
|
|
720
|
-
const toSequence = typeof input.toSequence === "number" ? input.toSequence : undefined;
|
|
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 };
|
|
780
|
+
async loopforge_replay(mgr, input) {
|
|
781
|
+
const sessionId = String(input.sessionId ?? "");
|
|
782
|
+
if (!sessionId)
|
|
783
|
+
return { error: "sessionId is required" };
|
|
784
|
+
const session = mgr.get(sessionId);
|
|
785
|
+
if (!session)
|
|
786
|
+
return { error: `session not found: ${sessionId}` };
|
|
787
|
+
const timeline = mgr.replayTimeline(sessionId) ?? [];
|
|
788
|
+
return { sessionId, loopId: session.loopId, timeline };
|
|
724
789
|
},
|
|
725
790
|
async loopforge_resume(mgr, input) {
|
|
726
791
|
const loopId = String(input.loopId ?? "");
|
|
727
792
|
if (!loopId)
|
|
728
|
-
return { error: "loopId is required"
|
|
793
|
+
return { error: "loopId is required" };
|
|
794
|
+
try {
|
|
795
|
+
validateLoopId(loopId);
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
return { error: `invalid loopId: ${error.message}` };
|
|
799
|
+
}
|
|
729
800
|
// Paused recovery must run first so the persisted status is atomically
|
|
730
801
|
// changed back to running before a prompt is returned.
|
|
731
802
|
let result = await mgr.unpause(loopId);
|
|
732
803
|
if (!result) {
|
|
733
|
-
result =
|
|
804
|
+
result = mgr.resume(loopId);
|
|
734
805
|
}
|
|
735
806
|
if (!result)
|
|
736
|
-
return { error: `no saved session found for loop "${loopId}"
|
|
737
|
-
return
|
|
807
|
+
return { error: `no saved session found for loop "${loopId}"` };
|
|
808
|
+
return { ...result };
|
|
738
809
|
},
|
|
739
810
|
async loopforge_gate_check(mgr, input) {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
811
|
+
const gateText = String(input.gateText ?? "");
|
|
812
|
+
if (!gateText.trim())
|
|
813
|
+
return { error: "gateText is required" };
|
|
814
|
+
return mgr.checkGate(gateText);
|
|
743
815
|
},
|
|
744
816
|
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);
|
|
817
|
+
return mgr.resolveGate(String(input.sessionId ?? ""), String(input.gateId ?? ""), input.approved === true, input.note === undefined ? undefined : String(input.note));
|
|
752
818
|
},
|
|
753
819
|
};
|
|
754
820
|
//# sourceMappingURL=tools.js.map
|