loopforge 3.7.0 → 3.8.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/LICENSE +21 -21
- package/README.md +171 -157
- package/dist/audit.d.ts +53 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +106 -22
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +75 -69
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -168
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +111 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +13 -0
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +30 -16
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/cognitive-governance.d.ts +30 -6
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +74 -18
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +109 -24
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +235 -52
- package/dist/committed-round.js.map +1 -1
- package/dist/contract-items.d.ts +55 -0
- package/dist/contract-items.d.ts.map +1 -0
- package/dist/contract-items.js +180 -0
- package/dist/contract-items.js.map +1 -0
- package/dist/enforcement-gate.d.ts +43 -20
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +269 -378
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +11 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +102 -67
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +4 -4
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +16 -14
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +91 -90
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +349 -160
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +76 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +127 -0
- package/dist/explain.js.map +1 -0
- package/dist/generate-schema.js +250 -53
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -40
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +275 -515
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +0 -1
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +0 -4
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +3 -1
- package/dist/loop-projection.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +40 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +173 -120
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +63 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -7
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +240 -139
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +89 -0
- package/dist/mcp/submission-boundary.d.ts.map +1 -0
- package/dist/mcp/submission-boundary.js +203 -0
- package/dist/mcp/submission-boundary.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +377 -125
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.js +2 -2
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +65 -95
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +151 -39
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +7 -33
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +313 -473
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +6 -7
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +411 -168
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +23 -69
- package/dist/protocol.js.map +1 -1
- package/dist/replay.js +2 -2
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +69 -57
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +104 -64
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +55 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +179 -57
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +7 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +2 -1
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +96 -0
- package/dist/round-facts.d.ts.map +1 -0
- package/dist/round-facts.js +216 -0
- package/dist/round-facts.js.map +1 -0
- package/dist/round-transaction.d.ts +23 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +99 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +127 -11
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +517 -36
- package/dist/self-eval.js.map +1 -1
- package/dist/subgoal-state.d.ts +77 -0
- package/dist/subgoal-state.d.ts.map +1 -0
- package/dist/subgoal-state.js +198 -0
- package/dist/subgoal-state.js.map +1 -0
- package/dist/token-utils.d.ts +52 -19
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +83 -72
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +27 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +308 -557
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +17 -0
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +17 -25
- package/package.json +7 -7
- package/skills/perception/SKILL.md +266 -64
package/dist/mcp/tools.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* status is the unified inspection tool (view=session|loop|all|audit).
|
|
6
6
|
* Each handler receives SessionManager + parsed input, returns the output object.
|
|
7
7
|
*/
|
|
8
|
-
import { buildSelfEvaluation } from "../engine.js";
|
|
9
8
|
import { validateCoreSelfEvaluation } from "../self-eval.js";
|
|
10
9
|
import { isRecord } from "../token-utils.js";
|
|
11
|
-
import { validateLoopId } from "../policy.js";
|
|
10
|
+
import { getPolicy, validateLoopId } from "../policy.js";
|
|
11
|
+
import { deriveEvidenceCapability } from "../evidence-provider.js";
|
|
12
12
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
13
|
// Tool schemas (MCP JSON Schema format)
|
|
14
14
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -30,7 +30,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
30
30
|
maxRounds: {
|
|
31
31
|
type: "integer",
|
|
32
32
|
minimum: 1,
|
|
33
|
-
description: "Maximum rounds before auto-stop. Default:
|
|
33
|
+
description: "Maximum rounds before auto-stop. Default: 200 from policy.",
|
|
34
34
|
},
|
|
35
35
|
domain: {
|
|
36
36
|
type: "string",
|
|
@@ -101,9 +101,10 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
101
101
|
emerged_subtasks: {
|
|
102
102
|
type: "array",
|
|
103
103
|
items: { type: "string" },
|
|
104
|
-
description: "Optional. Sub-problems that surfaced during execution."
|
|
104
|
+
description: "Optional. Sub-problems that surfaced during execution. The only " +
|
|
105
|
+
"sub-goal creation channel — sg-XXXXXXXX ID literals are rejected here.",
|
|
105
106
|
},
|
|
106
|
-
|
|
107
|
+
execution_report: {
|
|
107
108
|
type: "object",
|
|
108
109
|
description: "Optional. Structured record of what actually happened this round.",
|
|
109
110
|
properties: {
|
|
@@ -112,7 +113,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
112
113
|
items: { type: "string" },
|
|
113
114
|
description: "Files modified this round.",
|
|
114
115
|
},
|
|
115
|
-
|
|
116
|
+
tests_reported: {
|
|
116
117
|
type: "object",
|
|
117
118
|
description: "Test results from this round.",
|
|
118
119
|
properties: {
|
|
@@ -121,15 +122,31 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
121
122
|
skipped: { type: "integer", minimum: 0, description: "Number of skipped tests." },
|
|
122
123
|
},
|
|
123
124
|
},
|
|
124
|
-
|
|
125
|
+
criterion_claims: {
|
|
125
126
|
type: "array",
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
description: "v3.8: advisory claims about objective criteria. LENIENT — unknown or malformed ids are dropped with a warning, never a rejection.",
|
|
128
|
+
items: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
criterion_id: { type: "string", description: "cr-XXXXXXXX criterion id (free text allowed)." },
|
|
132
|
+
outcome: { type: "string", enum: ["met", "remaining"] },
|
|
133
|
+
},
|
|
134
|
+
required: ["criterion_id", "outcome"],
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
},
|
|
128
137
|
},
|
|
129
|
-
|
|
138
|
+
contract_item_claims: {
|
|
130
139
|
type: "array",
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
description: "Claims about items of the ACTIVE contract, cited by the rci-XXXXXXXX id the prompt rendered. LENIENT at declaration: an unknown/malformed id is reported as contract_invalid so you can correct the payload and resubmit the same roundId. A claim never verifies anything by itself — the runtime only marks an item verified when its bound commands are observed passing.",
|
|
141
|
+
items: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
item_id: { type: "string", description: "rci-XXXXXXXX id of an item of the ACTIVE contract." },
|
|
145
|
+
outcome: { type: "string", enum: ["met", "remaining"], description: "Your claim for that item this round." },
|
|
146
|
+
},
|
|
147
|
+
required: ["item_id", "outcome"],
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
},
|
|
133
150
|
},
|
|
134
151
|
progress_estimate: {
|
|
135
152
|
type: "number",
|
|
@@ -162,7 +179,10 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
162
179
|
},
|
|
163
180
|
worker_results: {
|
|
164
181
|
type: "array",
|
|
165
|
-
description: "Optional. Results of sub-agent / Worker delegations this round."
|
|
182
|
+
description: "Optional. Results of sub-agent / Worker delegations this round. " +
|
|
183
|
+
"Each entry must declare an outcome — the single reported fact. " +
|
|
184
|
+
"Entries without a valid outcome are dropped by the runtime. " +
|
|
185
|
+
"Omit or empty when nothing was delegated.",
|
|
166
186
|
items: {
|
|
167
187
|
type: "object",
|
|
168
188
|
properties: {
|
|
@@ -170,18 +190,16 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
170
190
|
subAgentType: { type: "string" },
|
|
171
191
|
subTask: { type: "string" },
|
|
172
192
|
resultSummary: { type: "string" },
|
|
173
|
-
success: { type: "boolean" },
|
|
174
193
|
outcome: {
|
|
175
194
|
type: "string",
|
|
176
195
|
enum: ["success", "partial", "failed"],
|
|
177
|
-
description: "Optional. Worker outcome; derived from success when absent.",
|
|
178
196
|
},
|
|
179
197
|
discoveredConstraints: {
|
|
180
198
|
type: "array",
|
|
181
199
|
items: { type: "string" },
|
|
182
200
|
},
|
|
183
201
|
},
|
|
184
|
-
required: ["agentId", "
|
|
202
|
+
required: ["agentId", "subTask", "resultSummary", "outcome"],
|
|
185
203
|
},
|
|
186
204
|
},
|
|
187
205
|
compression_checkpoint: {
|
|
@@ -192,24 +210,33 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
192
210
|
type: "string",
|
|
193
211
|
description: "Optional. Human-readable label for this checkpoint (e.g. 'core-functions-complete'). Only meaningful when compression_checkpoint is true.",
|
|
194
212
|
},
|
|
195
|
-
|
|
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: {
|
|
213
|
+
subgoal_updates: {
|
|
200
214
|
type: "array",
|
|
201
|
-
|
|
202
|
-
description: "Optional.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
maxItems: 20,
|
|
216
|
+
description: "Optional. Explicit status transitions for EXISTING sub-goals. " +
|
|
217
|
+
"Each entry references an ACTIVE sub-goal ID (sg-XXXXXXXX) from " +
|
|
218
|
+
"the dashboard with a target status. Unknown IDs, done/canceled " +
|
|
219
|
+
"(terminal) references, and illegal migrations are rejected " +
|
|
220
|
+
"before the round advances. done/canceled items must never be " +
|
|
221
|
+
"referenced again — re-open via a NEW emerged_subtasks item.",
|
|
222
|
+
items: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
id: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "Active sub-goal ID (sg-XXXXXXXX).",
|
|
228
|
+
},
|
|
229
|
+
status: {
|
|
230
|
+
type: "string",
|
|
231
|
+
enum: ["in_progress", "done", "blocked", "canceled"],
|
|
232
|
+
},
|
|
233
|
+
note: {
|
|
234
|
+
type: "string",
|
|
235
|
+
description: "Optional free-text note (≤ 300 chars).",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
required: ["id", "status"],
|
|
239
|
+
},
|
|
213
240
|
},
|
|
214
241
|
stop_reason: {
|
|
215
242
|
type: "string",
|
|
@@ -229,6 +256,14 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
229
256
|
type: "string",
|
|
230
257
|
description: "Optional. Flat blocker description, required only when outcome=blocked. Also feeds the user/agent gate classification. Max 500 chars.",
|
|
231
258
|
},
|
|
259
|
+
gate_ids: {
|
|
260
|
+
type: "array",
|
|
261
|
+
maxItems: 20,
|
|
262
|
+
description: "Optional. gate_opened ids this round's work depended on (from " +
|
|
263
|
+
"loopforge_gate_check). Enforced only when policy.gate.enabled: " +
|
|
264
|
+
"an unapproved cited gate rejects the round.",
|
|
265
|
+
items: { type: "string" },
|
|
266
|
+
},
|
|
232
267
|
retroactiveClaims: {
|
|
233
268
|
type: "array",
|
|
234
269
|
items: {
|
|
@@ -242,10 +277,6 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
242
277
|
},
|
|
243
278
|
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
279
|
},
|
|
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
280
|
prompt_requests: {
|
|
250
281
|
type: "object",
|
|
251
282
|
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.",
|
|
@@ -253,7 +284,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
253
284
|
emphasize: {
|
|
254
285
|
type: "array",
|
|
255
286
|
items: { type: "string" },
|
|
256
|
-
description: "Constraints, discoveries, or decisions that need emphasis in the next prompt.
|
|
287
|
+
description: "Constraints, discoveries, or decisions that need emphasis in the next prompt. Each entry is matched against active state items by STABLE ID (c-/cr-/sg-XXXXXXXX) or by exact text after normalization — a paraphrase matches nothing and renders nothing. Matched items appear in a 'Critical Context' section. Max 5 entries. Pure reordering — no token overhead.",
|
|
257
288
|
},
|
|
258
289
|
confusion_points: {
|
|
259
290
|
type: "array",
|
|
@@ -264,29 +295,49 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
264
295
|
},
|
|
265
296
|
round_contract: {
|
|
266
297
|
type: "object",
|
|
267
|
-
description: "Optional. Proposal for the NEXT round's contract (v3.
|
|
298
|
+
description: "Optional. Proposal for the NEXT round's contract (v3.8 item model). Declared here, it becomes the ACTIVE contract (rendered as the Current Task) only after this round commits. It closes when EVERY item is machine-verified (its bound commands observed passing, untampered, with the declared config), or when a round reports outcome=blocked. While it is open, restate it unchanged here — a different proposal is ignored. DECLARATION IS STRICT: an item with no bound command, one naming an unknown/disabled/non-after-capable command, a scope entry leaving the workspace, a malformed reference, or a count over the declared limits returns contract_invalid and the round may be retried with the same roundId.",
|
|
268
299
|
properties: {
|
|
269
300
|
work_item: {
|
|
270
301
|
type: "string",
|
|
271
302
|
description: "Optional. Round focus, one line. Rendered as the active contract's Current Task first line. Max 200 chars.",
|
|
272
303
|
},
|
|
273
|
-
|
|
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: {
|
|
304
|
+
items: {
|
|
279
305
|
type: "array",
|
|
280
|
-
|
|
281
|
-
|
|
306
|
+
description: "Required, at least one (max 20). Each item is a verification unit bound to evidence commands. The runtime derives a stable rci-XXXXXXXX id per item and renders it in the prompt — cite that id in execution_report.contract_item_claims.",
|
|
307
|
+
items: {
|
|
308
|
+
type: "object",
|
|
309
|
+
properties: {
|
|
310
|
+
description: {
|
|
311
|
+
type: "string",
|
|
312
|
+
description: "What must be true for this item. Max 200 chars.",
|
|
313
|
+
},
|
|
314
|
+
criterion_refs: {
|
|
315
|
+
type: "array",
|
|
316
|
+
items: { type: "string" },
|
|
317
|
+
description: "Optional. cr-XXXXXXXX criterion ids this item is evidence for (free text allowed). Max 20.",
|
|
318
|
+
},
|
|
319
|
+
subgoal_refs: {
|
|
320
|
+
type: "array",
|
|
321
|
+
items: { type: "string" },
|
|
322
|
+
description: "Optional. sg-XXXXXXXX sub-goals THIS ITEM is evidence for. Must name sub-goals that exist (or that this same submission creates via emerged_subtasks). Max 20 ids. A machine-verified item produces the derived verified-subgoal fact for exactly the ids it names here.",
|
|
323
|
+
},
|
|
324
|
+
verify_with: {
|
|
325
|
+
type: "array",
|
|
326
|
+
items: { type: "string" },
|
|
327
|
+
description: "Required, at least one. Names of configured, enabled, after-capable evidence.commands (loop_policy.json) that verify this item. Unknown or disabled names are rejected.",
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
required: ["description", "verify_with"],
|
|
331
|
+
additionalProperties: false,
|
|
332
|
+
},
|
|
282
333
|
},
|
|
283
334
|
scope: {
|
|
284
335
|
type: "array",
|
|
285
336
|
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.",
|
|
337
|
+
description: "Files/directories this round may touch (workspace-relative; './' and trailing slashes accepted; must stay inside the workspace). Out-of-scope git changes trigger round_scope_drift, which is a machine fact and is not waivable by explanation. Max 50 items.",
|
|
287
338
|
},
|
|
288
339
|
},
|
|
289
|
-
required: ["
|
|
340
|
+
required: ["items", "scope"],
|
|
290
341
|
},
|
|
291
342
|
},
|
|
292
343
|
},
|
|
@@ -310,8 +361,12 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
310
361
|
},
|
|
311
362
|
view: {
|
|
312
363
|
type: "string",
|
|
313
|
-
enum: ["session", "loop", "all", "audit"],
|
|
314
|
-
description: "Which view to return. Defaults to session.",
|
|
364
|
+
enum: ["session", "loop", "all", "audit", "explain"],
|
|
365
|
+
description: "Which view to return. Defaults to session. explain (v3.8) is the read-only per-round \"why\" view over committed facts.",
|
|
366
|
+
},
|
|
367
|
+
round: {
|
|
368
|
+
type: "number",
|
|
369
|
+
description: "view=explain only: return that single round's \"why\" view. Omit for the whole timeline.",
|
|
315
370
|
},
|
|
316
371
|
},
|
|
317
372
|
required: [],
|
|
@@ -385,21 +440,64 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
385
440
|
},
|
|
386
441
|
{
|
|
387
442
|
name: "loopforge_gate_check",
|
|
388
|
-
description: "
|
|
443
|
+
description: "v3.7.1: Structured preflight for a high-risk action. Classifies a " +
|
|
444
|
+
"GateActionDescriptor as user_required (needs human authorization — " +
|
|
445
|
+
"persists a gate_opened record you must cite via evaluation.gate_ids " +
|
|
446
|
+
"once approved) or agent_allowed (no record, no human needed). " +
|
|
447
|
+
"Never executes the action, never approves it, never advances a round. " +
|
|
448
|
+
"Hidden when policy.gate.enabled=false (direct calls return gate_disabled).",
|
|
389
449
|
inputSchema: {
|
|
390
450
|
type: "object",
|
|
391
451
|
properties: {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
452
|
+
sessionId: { type: "string", description: "Session ID." },
|
|
453
|
+
roundId: { type: "string", description: "RoundId from the latest response." },
|
|
454
|
+
action: {
|
|
455
|
+
type: "object",
|
|
456
|
+
description: "The action the agent plans to perform.",
|
|
457
|
+
properties: {
|
|
458
|
+
description: {
|
|
459
|
+
type: "string",
|
|
460
|
+
description: "What the action does, e.g. 'Deploy to production'.",
|
|
461
|
+
},
|
|
462
|
+
scope: {
|
|
463
|
+
type: "array",
|
|
464
|
+
items: { type: "string" },
|
|
465
|
+
description: "Files / systems / services the action touches.",
|
|
466
|
+
},
|
|
467
|
+
effects: {
|
|
468
|
+
type: "array",
|
|
469
|
+
items: {
|
|
470
|
+
type: "string",
|
|
471
|
+
enum: [
|
|
472
|
+
"workspace_write", "production", "credentials", "data_migration",
|
|
473
|
+
"public_api", "publish", "payment", "external_communication",
|
|
474
|
+
"network",
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
reversibility: {
|
|
479
|
+
type: "string",
|
|
480
|
+
enum: ["reversible", "recoverable", "irreversible", "unknown"],
|
|
481
|
+
},
|
|
482
|
+
authorization: {
|
|
483
|
+
type: "string",
|
|
484
|
+
enum: ["agent_allowed", "user_required", "unknown"],
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
required: ["description", "scope", "effects", "reversibility", "authorization"],
|
|
395
488
|
},
|
|
396
489
|
},
|
|
397
|
-
required: ["
|
|
490
|
+
required: ["sessionId", "roundId", "action"],
|
|
398
491
|
},
|
|
399
492
|
},
|
|
400
493
|
{
|
|
401
494
|
name: "loopforge_gate_resolve",
|
|
402
|
-
description: "
|
|
495
|
+
description: "v3.7.1: Record a human decision for a previously opened user gate. " +
|
|
496
|
+
"The caller is still the Agent — LoopForge cannot machine-verify that a " +
|
|
497
|
+
"human is present; the decision is recorded and audited, and the gate's " +
|
|
498
|
+
"approval binds to the canonical action (any change expires it). " +
|
|
499
|
+
"Agent gates are resolved by submitting evidence, never here. " +
|
|
500
|
+
"Hidden when policy.gate.enabled=false (direct calls return gate_disabled).",
|
|
403
501
|
inputSchema: {
|
|
404
502
|
type: "object",
|
|
405
503
|
properties: {
|
|
@@ -409,7 +507,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
409
507
|
},
|
|
410
508
|
gateId: {
|
|
411
509
|
type: "string",
|
|
412
|
-
description: "Gate id returned by loopforge_gate_check
|
|
510
|
+
description: "Gate id returned by loopforge_gate_check.",
|
|
413
511
|
},
|
|
414
512
|
approved: {
|
|
415
513
|
type: "boolean",
|
|
@@ -427,7 +525,8 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
427
525
|
const ADVANCE_OUTPUT_SCHEMA = {
|
|
428
526
|
type: "object",
|
|
429
527
|
properties: {
|
|
430
|
-
|
|
528
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
529
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
431
530
|
details: { type: "object", additionalProperties: true },
|
|
432
531
|
sessionId: { type: "string" },
|
|
433
532
|
round: { type: "number" },
|
|
@@ -459,7 +558,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
459
558
|
loopforge_status: {
|
|
460
559
|
type: "object",
|
|
461
560
|
properties: {
|
|
462
|
-
|
|
561
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
562
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
463
563
|
sessionId: { type: "string" },
|
|
464
564
|
loopId: { type: "string" },
|
|
465
565
|
round: { type: "number" },
|
|
@@ -479,7 +579,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
479
579
|
loopforge_stop: {
|
|
480
580
|
type: "object",
|
|
481
581
|
properties: {
|
|
482
|
-
|
|
582
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
583
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
483
584
|
success: { type: "boolean" },
|
|
484
585
|
roundsCompleted: { type: "number" },
|
|
485
586
|
successTrajectory: { type: "array", items: { type: "boolean" } },
|
|
@@ -489,7 +590,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
489
590
|
loopforge_pause: {
|
|
490
591
|
type: "object",
|
|
491
592
|
properties: {
|
|
492
|
-
|
|
593
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
594
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
493
595
|
sessionId: { type: "string" },
|
|
494
596
|
round: { type: "number" },
|
|
495
597
|
status: { type: "string" },
|
|
@@ -499,7 +601,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
499
601
|
loopforge_replay: {
|
|
500
602
|
type: "object",
|
|
501
603
|
properties: {
|
|
502
|
-
|
|
604
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
605
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
503
606
|
sessionId: { type: "string" },
|
|
504
607
|
loopId: { type: "string" },
|
|
505
608
|
timeline: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
@@ -509,22 +612,25 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
509
612
|
loopforge_gate_check: {
|
|
510
613
|
type: "object",
|
|
511
614
|
properties: {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
615
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
616
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
617
|
+
sessionId: { type: "string" },
|
|
618
|
+
gateId: { type: "string" },
|
|
619
|
+
risk: { type: "string", enum: ["low", "high", "unknown"] },
|
|
620
|
+
decision: { type: "string", enum: ["agent_allowed", "user_required"] },
|
|
621
|
+
reasonCodes: { type: "array", items: { type: "string" } },
|
|
516
622
|
blockedScope: { type: "array", items: { type: "string" } },
|
|
517
|
-
|
|
518
|
-
problem: { type: "string" },
|
|
623
|
+
allowedBeforeApproval: { type: "array", items: { type: "string" } },
|
|
519
624
|
requiredEvidence: { type: "array", items: { type: "string" } },
|
|
520
|
-
|
|
625
|
+
approvalQuestion: { type: "string" },
|
|
521
626
|
},
|
|
522
627
|
additionalProperties: true,
|
|
523
628
|
},
|
|
524
629
|
loopforge_gate_resolve: {
|
|
525
630
|
type: "object",
|
|
526
631
|
properties: {
|
|
527
|
-
|
|
632
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
633
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
528
634
|
sessionId: { type: "string" },
|
|
529
635
|
loopId: { type: "string" },
|
|
530
636
|
gateId: { type: "string" },
|
|
@@ -705,6 +811,16 @@ export function validateToolOutput(name, output) {
|
|
|
705
811
|
return;
|
|
706
812
|
validateSchema(output, schema, `output.${name}`);
|
|
707
813
|
}
|
|
814
|
+
/** v3.8: capability warnings for the current policy, merged with the compile's
|
|
815
|
+
* own warnings. Derived through the SAME function `RoundDriver.prepare()`
|
|
816
|
+
* uses, so the start/resume banner and the prepared round cannot tell two
|
|
817
|
+
* different stories about what the runtime can observe. Never persisted. */
|
|
818
|
+
function capabilityWarningList(existing) {
|
|
819
|
+
return [...new Set([
|
|
820
|
+
...(existing ?? []),
|
|
821
|
+
...deriveEvidenceCapability(getPolicy()).warnings,
|
|
822
|
+
])];
|
|
823
|
+
}
|
|
708
824
|
export const TOOL_HANDLERS = {
|
|
709
825
|
async loopforge_start(mgr, input) {
|
|
710
826
|
const startInput = {
|
|
@@ -718,7 +834,10 @@ export const TOOL_HANDLERS = {
|
|
|
718
834
|
: undefined,
|
|
719
835
|
};
|
|
720
836
|
if (!startInput.task.trim()) {
|
|
721
|
-
return {
|
|
837
|
+
return {
|
|
838
|
+
error: "invalid_argument",
|
|
839
|
+
errorMessage: "task is required and must be non-empty",
|
|
840
|
+
};
|
|
722
841
|
}
|
|
723
842
|
// v2.14: entry-point validation — a bad maxRounds previously passed
|
|
724
843
|
// through and stopped the loop on the first round; an invalid loopId
|
|
@@ -726,61 +845,86 @@ export const TOOL_HANDLERS = {
|
|
|
726
845
|
// argument error.
|
|
727
846
|
if (startInput.maxRounds !== undefined &&
|
|
728
847
|
(!Number.isInteger(startInput.maxRounds) || startInput.maxRounds < 1)) {
|
|
729
|
-
return {
|
|
848
|
+
return {
|
|
849
|
+
error: "invalid_argument",
|
|
850
|
+
errorMessage: "maxRounds must be a positive integer",
|
|
851
|
+
};
|
|
730
852
|
}
|
|
731
853
|
if (startInput.loopId !== undefined) {
|
|
732
854
|
try {
|
|
733
855
|
validateLoopId(startInput.loopId);
|
|
734
856
|
}
|
|
735
857
|
catch (error) {
|
|
736
|
-
return {
|
|
858
|
+
return {
|
|
859
|
+
error: "invalid_argument",
|
|
860
|
+
errorMessage: `invalid loopId: ${error.message}`,
|
|
861
|
+
};
|
|
737
862
|
}
|
|
738
863
|
}
|
|
739
|
-
|
|
864
|
+
// A malformed loop_policy.json is a configuration defect, not a bad
|
|
865
|
+
// argument — it must not escape as an opaque JSON-RPC internal error.
|
|
866
|
+
let result;
|
|
867
|
+
try {
|
|
868
|
+
result = await mgr.create(startInput);
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
return {
|
|
872
|
+
error: "policy_invalid",
|
|
873
|
+
errorMessage: `the runtime policy could not be loaded: ${error.message}`,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
740
876
|
// v2.14: a same-process duplicate loopId surfaces as a clean argument
|
|
741
877
|
// error instead of a prompt-less "success" the client cannot act on.
|
|
742
878
|
if (typeof result.stopReason === "string" &&
|
|
743
879
|
result.stopReason.startsWith("loop_already_running:")) {
|
|
744
|
-
return {
|
|
880
|
+
return {
|
|
881
|
+
error: "loop_already_running",
|
|
882
|
+
errorMessage: result.stopDetail ?? "loop already running",
|
|
883
|
+
};
|
|
745
884
|
}
|
|
746
|
-
|
|
885
|
+
// v3.8: the static capability warning — a loop with no provider or no
|
|
886
|
+
// enabled command can still run; its success claims are recorded as
|
|
887
|
+
// insufficient instead of verified, and the agent is told so up front.
|
|
888
|
+
const capabilityWarnings = capabilityWarningList(result.warnings);
|
|
889
|
+
return capabilityWarnings.length > 0
|
|
890
|
+
? { ...result, warnings: capabilityWarnings }
|
|
891
|
+
: { ...result };
|
|
747
892
|
},
|
|
748
893
|
async loopforge_next(mgr, input) {
|
|
749
894
|
const sessionId = String(input.sessionId ?? "");
|
|
750
895
|
const output = String(input.output ?? "");
|
|
751
896
|
const rawEval = input.evaluation;
|
|
752
897
|
const roundId = input.roundId === undefined ? undefined : String(input.roundId);
|
|
753
|
-
if (!sessionId)
|
|
754
|
-
return { error: "sessionId is required" };
|
|
898
|
+
if (!sessionId) {
|
|
899
|
+
return { error: "invalid_argument", errorMessage: "sessionId is required" };
|
|
900
|
+
}
|
|
755
901
|
// v3.0.1: every submission is anchored to the round it reports on. A
|
|
756
902
|
// missing roundId would silently re-process a stale submission against a
|
|
757
903
|
// later round — reject it here so the contract is enforced even when the
|
|
758
904
|
// handler is invoked directly (bypassing schema validation).
|
|
759
905
|
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
906
|
return {
|
|
764
|
-
error: "
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
invalid: [{ field: "evaluation", expected: "object" }],
|
|
768
|
-
},
|
|
769
|
-
sessionId,
|
|
770
|
-
roundId,
|
|
907
|
+
error: "round_id_required",
|
|
908
|
+
errorMessage: "roundId is required — pass the roundId from the most recent " +
|
|
909
|
+
"loopforge_start / loopforge_next / loopforge_resume / loopforge_status response",
|
|
771
910
|
};
|
|
772
911
|
}
|
|
773
|
-
|
|
774
|
-
|
|
912
|
+
// v3.8.1: the submission boundary belongs to the RUNTIME, not to this
|
|
913
|
+
// transport. `mgr.advance` applies all of it — object shape, core fields,
|
|
914
|
+
// subgoal_updates structure and references, and the strict Round Contract
|
|
915
|
+
// declaration/claims boundary — before touching any state, and reports a
|
|
916
|
+
// refusal through `submissionError`. This handler only maps that onto the
|
|
917
|
+
// tool envelope, so a non-MCP caller gets exactly the same strictness.
|
|
918
|
+
const result = await mgr.advance(sessionId, output, rawEval, roundId);
|
|
919
|
+
if (result.submissionError) {
|
|
775
920
|
return {
|
|
776
|
-
error:
|
|
777
|
-
|
|
921
|
+
error: result.submissionError.code,
|
|
922
|
+
errorMessage: result.stopDetail ?? "the submission was rejected",
|
|
923
|
+
details: result.submissionError.details,
|
|
778
924
|
sessionId,
|
|
779
925
|
roundId,
|
|
780
926
|
};
|
|
781
927
|
}
|
|
782
|
-
const preExtractedEval = buildSelfEvaluation(rawEval);
|
|
783
|
-
const result = await mgr.advance(sessionId, output, preExtractedEval, roundId);
|
|
784
928
|
// v2.12: Typed projection — runtime facts for the main agent, attached
|
|
785
929
|
// on every accepted/continued round (not on errors).
|
|
786
930
|
let projection = null;
|
|
@@ -791,7 +935,8 @@ export const TOOL_HANDLERS = {
|
|
|
791
935
|
return projection ? { ...result, projection } : { ...result };
|
|
792
936
|
},
|
|
793
937
|
async loopforge_status(mgr, input) {
|
|
794
|
-
const view = input.view === "loop" || input.view === "all" ||
|
|
938
|
+
const view = input.view === "loop" || input.view === "all" ||
|
|
939
|
+
input.view === "audit" || input.view === "explain"
|
|
795
940
|
? input.view
|
|
796
941
|
: "session";
|
|
797
942
|
const sessionId = String(input.sessionId ?? "");
|
|
@@ -802,25 +947,55 @@ export const TOOL_HANDLERS = {
|
|
|
802
947
|
}
|
|
803
948
|
// v2.12: view=audit — read-only verification audit.
|
|
804
949
|
if (view === "audit") {
|
|
805
|
-
if (!loopId)
|
|
806
|
-
return { error: "loopId is required for view=audit" };
|
|
950
|
+
if (!loopId) {
|
|
951
|
+
return { error: "invalid_argument", errorMessage: "loopId is required for view=audit" };
|
|
952
|
+
}
|
|
807
953
|
const audit = mgr.getAudit(loopId);
|
|
808
|
-
if (!audit)
|
|
809
|
-
return {
|
|
954
|
+
if (!audit) {
|
|
955
|
+
return {
|
|
956
|
+
error: "state_unavailable",
|
|
957
|
+
errorMessage: `no audit data found for loop "${loopId}"`,
|
|
958
|
+
sessionId,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
810
961
|
return audit;
|
|
811
962
|
}
|
|
963
|
+
// v3.8: view=explain — the read-only per-round "why" view.
|
|
964
|
+
if (view === "explain") {
|
|
965
|
+
if (!loopId) {
|
|
966
|
+
return { error: "invalid_argument", errorMessage: "loopId is required for view=explain" };
|
|
967
|
+
}
|
|
968
|
+
const roundValue = input.round;
|
|
969
|
+
const round = typeof roundValue === "number" && Number.isInteger(roundValue) && roundValue >= 1
|
|
970
|
+
? roundValue
|
|
971
|
+
: undefined;
|
|
972
|
+
return mgr.getExplain(loopId, round);
|
|
973
|
+
}
|
|
812
974
|
// v2.12: view=loop — loop health (formerly loopforge_health).
|
|
813
975
|
if (view === "loop") {
|
|
814
|
-
if (!loopId)
|
|
815
|
-
return { error: "loopId is required for view=loop" };
|
|
976
|
+
if (!loopId) {
|
|
977
|
+
return { error: "invalid_argument", errorMessage: "loopId is required for view=loop" };
|
|
978
|
+
}
|
|
816
979
|
const health = mgr.getHealth(loopId);
|
|
817
|
-
return health ?? {
|
|
980
|
+
return health ?? {
|
|
981
|
+
error: "state_unavailable",
|
|
982
|
+
errorMessage: `no health data for loop "${loopId}"`,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
if (!sessionId) {
|
|
986
|
+
return {
|
|
987
|
+
error: "invalid_argument",
|
|
988
|
+
errorMessage: "sessionId is required for view=session",
|
|
989
|
+
};
|
|
818
990
|
}
|
|
819
|
-
if (!sessionId)
|
|
820
|
-
return { error: "sessionId is required for view=session" };
|
|
821
991
|
const session = mgr.get(sessionId);
|
|
822
|
-
if (!session)
|
|
823
|
-
return {
|
|
992
|
+
if (!session) {
|
|
993
|
+
return {
|
|
994
|
+
error: "session_not_found",
|
|
995
|
+
errorMessage: `session not found: ${sessionId} (process restarted?) — pass loopId for view=audit/loop, or loopforge_resume`,
|
|
996
|
+
sessionId,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
824
999
|
const metrics = session.engine.getMetrics();
|
|
825
1000
|
return {
|
|
826
1001
|
sessionId: session.sessionId,
|
|
@@ -835,6 +1010,11 @@ export const TOOL_HANDLERS = {
|
|
|
835
1010
|
// verification gates derive the same value from the same committed
|
|
836
1011
|
// evals via the shared round-contract walker).
|
|
837
1012
|
activeContract: mgr.getActiveContract(sessionId),
|
|
1013
|
+
// v3.8: the ONE capability fact — the static half the compile hashes
|
|
1014
|
+
// (what verification is possible) plus the live half rendered from the
|
|
1015
|
+
// last baseline. `available` is provider-registry state: reported here
|
|
1016
|
+
// and by `doctor`, never hashed.
|
|
1017
|
+
capability: deriveEvidenceCapability(getPolicy(), session.evidenceBaseline ?? []),
|
|
838
1018
|
projection: mgr.getProjection(sessionId),
|
|
839
1019
|
metrics: {
|
|
840
1020
|
vaultWriteErrors: metrics.vaultWriteErrors,
|
|
@@ -849,11 +1029,17 @@ export const TOOL_HANDLERS = {
|
|
|
849
1029
|
},
|
|
850
1030
|
async loopforge_stop(mgr, input) {
|
|
851
1031
|
const sessionId = String(input.sessionId ?? "");
|
|
852
|
-
if (!sessionId)
|
|
853
|
-
return { error: "sessionId is required" };
|
|
1032
|
+
if (!sessionId) {
|
|
1033
|
+
return { error: "invalid_argument", errorMessage: "sessionId is required" };
|
|
1034
|
+
}
|
|
854
1035
|
const session = mgr.get(sessionId);
|
|
855
|
-
if (!session)
|
|
856
|
-
return {
|
|
1036
|
+
if (!session) {
|
|
1037
|
+
return {
|
|
1038
|
+
error: "session_not_found",
|
|
1039
|
+
errorMessage: `session not found: ${sessionId}`,
|
|
1040
|
+
sessionId,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
857
1043
|
const roundsCompleted = session.currentRound;
|
|
858
1044
|
const successTrajectory = [...session.successTrajectory];
|
|
859
1045
|
mgr.delete(sessionId);
|
|
@@ -861,8 +1047,9 @@ export const TOOL_HANDLERS = {
|
|
|
861
1047
|
},
|
|
862
1048
|
async loopforge_pause(mgr, input) {
|
|
863
1049
|
const sessionId = String(input.sessionId ?? "");
|
|
864
|
-
if (!sessionId)
|
|
865
|
-
return { error: "sessionId is required" };
|
|
1050
|
+
if (!sessionId) {
|
|
1051
|
+
return { error: "invalid_argument", errorMessage: "sessionId is required" };
|
|
1052
|
+
}
|
|
866
1053
|
const result = mgr.pause(sessionId);
|
|
867
1054
|
return { ...result };
|
|
868
1055
|
},
|
|
@@ -870,7 +1057,11 @@ export const TOOL_HANDLERS = {
|
|
|
870
1057
|
const sessionId = String(input.sessionId ?? "");
|
|
871
1058
|
const loopId = String(input.loopId ?? "");
|
|
872
1059
|
if (!sessionId && !loopId) {
|
|
873
|
-
return {
|
|
1060
|
+
return {
|
|
1061
|
+
error: "invalid_argument",
|
|
1062
|
+
errorMessage: "sessionId or loopId is required — pass loopId to replay a loop " +
|
|
1063
|
+
"after a restart (no live session)",
|
|
1064
|
+
};
|
|
874
1065
|
}
|
|
875
1066
|
// Live session first: it disambiguates re-used loopIds and the session
|
|
876
1067
|
// registry is the cheapest lookup.
|
|
@@ -883,27 +1074,39 @@ export const TOOL_HANDLERS = {
|
|
|
883
1074
|
// v3.3.1: session not in memory (restart). Fall through to the vault
|
|
884
1075
|
// path when a loopId was also provided; otherwise say so plainly.
|
|
885
1076
|
if (!loopId) {
|
|
886
|
-
return {
|
|
1077
|
+
return {
|
|
1078
|
+
error: "session_not_found",
|
|
1079
|
+
errorMessage: `session not found: ${sessionId} (process restarted?) — ` +
|
|
1080
|
+
"pass loopId to replay from the vault",
|
|
1081
|
+
sessionId,
|
|
1082
|
+
};
|
|
887
1083
|
}
|
|
888
1084
|
}
|
|
889
1085
|
// Vault-direct replay — the timeline lives in the committed round
|
|
890
1086
|
// documents, not in the session registry.
|
|
891
1087
|
const timeline = mgr.replayByLoop(loopId);
|
|
892
1088
|
if (!timeline) {
|
|
893
|
-
return {
|
|
1089
|
+
return {
|
|
1090
|
+
error: "state_unavailable",
|
|
1091
|
+
errorMessage: `no committed rounds found for loop "${loopId}"`,
|
|
1092
|
+
};
|
|
894
1093
|
}
|
|
895
1094
|
// sessionId omitted on the vault path (its output schema is string-only).
|
|
896
1095
|
return { ...(sessionId ? { sessionId } : {}), loopId, timeline };
|
|
897
1096
|
},
|
|
898
1097
|
async loopforge_resume(mgr, input) {
|
|
899
1098
|
const loopId = String(input.loopId ?? "");
|
|
900
|
-
if (!loopId)
|
|
901
|
-
return { error: "loopId is required" };
|
|
1099
|
+
if (!loopId) {
|
|
1100
|
+
return { error: "invalid_argument", errorMessage: "loopId is required" };
|
|
1101
|
+
}
|
|
902
1102
|
try {
|
|
903
1103
|
validateLoopId(loopId);
|
|
904
1104
|
}
|
|
905
1105
|
catch (error) {
|
|
906
|
-
return {
|
|
1106
|
+
return {
|
|
1107
|
+
error: "invalid_argument",
|
|
1108
|
+
errorMessage: `invalid loopId: ${error.message}`,
|
|
1109
|
+
};
|
|
907
1110
|
}
|
|
908
1111
|
// Paused recovery must run first so the persisted status is atomically
|
|
909
1112
|
// changed back to running before a prompt is returned.
|
|
@@ -911,17 +1114,66 @@ export const TOOL_HANDLERS = {
|
|
|
911
1114
|
if (!result) {
|
|
912
1115
|
result = await mgr.resume(loopId);
|
|
913
1116
|
}
|
|
914
|
-
if (!result)
|
|
915
|
-
return {
|
|
1117
|
+
if (!result) {
|
|
1118
|
+
return {
|
|
1119
|
+
error: "state_unavailable",
|
|
1120
|
+
errorMessage: `no saved session found for loop "${loopId}"`,
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
916
1123
|
return { ...result };
|
|
917
1124
|
},
|
|
918
1125
|
async loopforge_gate_check(mgr, input) {
|
|
919
|
-
|
|
920
|
-
if (!
|
|
921
|
-
return { error: "
|
|
922
|
-
|
|
1126
|
+
// v3.7.1: hidden when disabled — direct calls get a stable error.
|
|
1127
|
+
if (!getPolicy().gate.enabled)
|
|
1128
|
+
return { error: "gate_disabled" };
|
|
1129
|
+
const sessionId = String(input.sessionId ?? "");
|
|
1130
|
+
const roundId = String(input.roundId ?? "");
|
|
1131
|
+
const action = input.action;
|
|
1132
|
+
if (!sessionId) {
|
|
1133
|
+
return { error: "invalid_argument", errorMessage: "sessionId is required" };
|
|
1134
|
+
}
|
|
1135
|
+
if (!roundId) {
|
|
1136
|
+
return {
|
|
1137
|
+
error: "round_id_required",
|
|
1138
|
+
errorMessage: "roundId is required — pass the roundId from the latest response",
|
|
1139
|
+
sessionId,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
if (!action || !isRecord(action)) {
|
|
1143
|
+
return { error: "invalid_argument", errorMessage: "action is required", sessionId, roundId };
|
|
1144
|
+
}
|
|
1145
|
+
const descriptor = {
|
|
1146
|
+
description: String(action.description ?? ""),
|
|
1147
|
+
scope: Array.isArray(action.scope)
|
|
1148
|
+
? action.scope.filter((v) => typeof v === "string")
|
|
1149
|
+
: [],
|
|
1150
|
+
effects: Array.isArray(action.effects)
|
|
1151
|
+
? action.effects.filter((v) => typeof v === "string")
|
|
1152
|
+
.filter((_v) => true)
|
|
1153
|
+
: [],
|
|
1154
|
+
reversibility: (["reversible", "recoverable", "irreversible", "unknown"]
|
|
1155
|
+
.includes(String(action.reversibility))
|
|
1156
|
+
? String(action.reversibility)
|
|
1157
|
+
: "unknown"),
|
|
1158
|
+
authorization: (["agent_allowed", "user_required", "unknown"]
|
|
1159
|
+
.includes(String(action.authorization))
|
|
1160
|
+
? String(action.authorization)
|
|
1161
|
+
: "unknown"),
|
|
1162
|
+
};
|
|
1163
|
+
if (!descriptor.description.trim()) {
|
|
1164
|
+
return {
|
|
1165
|
+
error: "invalid_argument",
|
|
1166
|
+
errorMessage: "action.description is required",
|
|
1167
|
+
sessionId,
|
|
1168
|
+
roundId,
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
return mgr.checkGate(sessionId, roundId, descriptor);
|
|
923
1172
|
},
|
|
924
1173
|
async loopforge_gate_resolve(mgr, input) {
|
|
1174
|
+
// v3.7.1: hidden when disabled — direct calls get a stable error.
|
|
1175
|
+
if (!getPolicy().gate.enabled)
|
|
1176
|
+
return { error: "gate_disabled" };
|
|
925
1177
|
return mgr.resolveGate(String(input.sessionId ?? ""), String(input.gateId ?? ""), input.approved === true, input.note === undefined ? undefined : String(input.note));
|
|
926
1178
|
},
|
|
927
1179
|
};
|