loopforge 3.9.2 → 4.0.7
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 +236 -112
- package/dist/audit.d.ts +73 -15
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +131 -61
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +152 -40
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +382 -206
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +0 -2
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +0 -4
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +105 -141
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +249 -372
- package/dist/committed-round.js.map +1 -1
- package/dist/constraint-lifecycle.d.ts +73 -0
- package/dist/constraint-lifecycle.d.ts.map +1 -0
- package/dist/constraint-lifecycle.js +174 -0
- package/dist/constraint-lifecycle.js.map +1 -0
- package/dist/context-boundary.d.ts +83 -0
- package/dist/context-boundary.d.ts.map +1 -0
- package/dist/context-boundary.js +101 -0
- package/dist/context-boundary.js.map +1 -0
- package/dist/contract-items.d.ts +12 -8
- package/dist/contract-items.d.ts.map +1 -1
- package/dist/contract-items.js +31 -26
- package/dist/contract-items.js.map +1 -1
- package/dist/contract-requirement.d.ts +47 -0
- package/dist/contract-requirement.d.ts.map +1 -0
- package/dist/contract-requirement.js +51 -0
- package/dist/contract-requirement.js.map +1 -0
- package/dist/criterion-status.d.ts +35 -0
- package/dist/criterion-status.d.ts.map +1 -0
- package/dist/criterion-status.js +89 -0
- package/dist/criterion-status.js.map +1 -0
- package/dist/enforcement-gate.d.ts +32 -67
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +203 -439
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +9 -103
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +28 -887
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +69 -0
- package/dist/errors.js.map +1 -0
- package/dist/evidence-claims.d.ts +33 -31
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +58 -72
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-posture-view.d.ts +19 -0
- package/dist/evidence-posture-view.d.ts.map +1 -0
- package/dist/evidence-posture-view.js +116 -0
- package/dist/evidence-posture-view.js.map +1 -0
- package/dist/evidence-posture.d.ts +73 -0
- package/dist/evidence-posture.d.ts.map +1 -0
- package/dist/evidence-posture.js +281 -0
- package/dist/evidence-posture.js.map +1 -0
- package/dist/evidence-provider.d.ts +55 -37
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +182 -332
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +167 -27
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +373 -58
- package/dist/explain.js.map +1 -1
- package/dist/field-accounting.d.ts.map +1 -1
- package/dist/field-accounting.js +3 -22
- package/dist/field-accounting.js.map +1 -1
- package/dist/generate-schema.js +13 -8
- package/dist/generate-schema.js.map +1 -1
- package/dist/git-boundary.d.ts +128 -0
- package/dist/git-boundary.d.ts.map +1 -0
- package/dist/git-boundary.js +625 -0
- package/dist/git-boundary.js.map +1 -0
- package/dist/git-facts.d.ts +22 -0
- package/dist/git-facts.d.ts.map +1 -0
- package/dist/git-facts.js +29 -0
- package/dist/git-facts.js.map +1 -0
- package/dist/git-motion.d.ts +37 -0
- package/dist/git-motion.d.ts.map +1 -0
- package/dist/git-motion.js +59 -0
- package/dist/git-motion.js.map +1 -0
- package/dist/history.d.ts +44 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +77 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +16 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +80 -14
- package/dist/init.js.map +1 -1
- package/dist/loop-compiler.d.ts +51 -19
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +423 -507
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +6 -5
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +153 -35
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +285 -81
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +1629 -574
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +61 -19
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +81 -39
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +963 -271
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +23 -31
- package/dist/mcp/submission-boundary.d.ts.map +1 -1
- package/dist/mcp/submission-boundary.js +242 -82
- package/dist/mcp/submission-boundary.js.map +1 -1
- package/dist/mcp/submission-handle.d.ts +66 -0
- package/dist/mcp/submission-handle.d.ts.map +1 -0
- package/dist/mcp/submission-handle.js +173 -0
- package/dist/mcp/submission-handle.js.map +1 -0
- package/dist/mcp/tools.d.ts +17 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +763 -244
- package/dist/mcp/tools.js.map +1 -1
- package/dist/open-question.d.ts +7 -0
- package/dist/open-question.d.ts.map +1 -0
- package/dist/open-question.js +14 -0
- package/dist/open-question.js.map +1 -0
- package/dist/policy-metrics.d.ts +31 -21
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +117 -64
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +15 -40
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +13 -13
- package/dist/policy.js.map +1 -1
- package/dist/progress.d.ts +45 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +136 -0
- package/dist/progress.js.map +1 -0
- package/dist/prompt-assembler.d.ts +28 -9
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +592 -603
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +39 -0
- package/dist/prompt-coverage.d.ts.map +1 -0
- package/dist/prompt-coverage.js +80 -0
- package/dist/prompt-coverage.js.map +1 -0
- package/dist/prompt-freshness.d.ts +25 -0
- package/dist/prompt-freshness.d.ts.map +1 -0
- package/dist/prompt-freshness.js +88 -0
- package/dist/prompt-freshness.js.map +1 -0
- package/dist/prompt-policy.d.ts +14 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +29 -10
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +875 -419
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +73 -99
- package/dist/protocol.js.map +1 -1
- package/dist/recovery-guidance.d.ts +13 -0
- package/dist/recovery-guidance.d.ts.map +1 -0
- package/dist/recovery-guidance.js +15 -0
- package/dist/recovery-guidance.js.map +1 -0
- package/dist/recovery.d.ts +68 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +234 -0
- package/dist/recovery.js.map +1 -0
- package/dist/replay.d.ts +42 -8
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +311 -84
- package/dist/replay.js.map +1 -1
- package/dist/result-facts.d.ts +45 -0
- package/dist/result-facts.d.ts.map +1 -0
- package/dist/result-facts.js +86 -0
- package/dist/result-facts.js.map +1 -0
- package/dist/round-contract.d.ts +38 -8
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +71 -31
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +45 -91
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +254 -271
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +56 -16
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +225 -39
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +33 -52
- package/dist/round-facts.d.ts.map +1 -1
- package/dist/round-facts.js +54 -157
- package/dist/round-facts.js.map +1 -1
- package/dist/round-guidance.d.ts +56 -0
- package/dist/round-guidance.d.ts.map +1 -0
- package/dist/round-guidance.js +161 -0
- package/dist/round-guidance.js.map +1 -0
- package/dist/round-outcome.d.ts +21 -0
- package/dist/round-outcome.d.ts.map +1 -0
- package/dist/round-outcome.js +31 -0
- package/dist/round-outcome.js.map +1 -0
- package/dist/round-transaction.d.ts +168 -21
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +856 -39
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +40 -60
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +455 -384
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +16 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -4
- package/dist/storage.js.map +1 -1
- package/dist/subgoal-state.d.ts +2 -3
- package/dist/subgoal-state.d.ts.map +1 -1
- package/dist/subgoal-state.js +2 -3
- package/dist/subgoal-state.js.map +1 -1
- package/dist/submission-guidance.d.ts +43 -21
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +368 -262
- package/dist/submission-guidance.js.map +1 -1
- package/dist/submission-receipt.d.ts +16 -8
- package/dist/submission-receipt.d.ts.map +1 -1
- package/dist/submission-receipt.js +146 -51
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +47 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +66 -15
- package/dist/token-utils.js.map +1 -1
- package/dist/transaction-identity.d.ts +9 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +69 -0
- package/dist/transaction-identity.js.map +1 -0
- package/dist/verification-actions.d.ts +13 -0
- package/dist/verification-actions.d.ts.map +1 -0
- package/dist/verification-actions.js +44 -0
- package/dist/verification-actions.js.map +1 -0
- package/dist/verification-gate.d.ts +26 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +218 -427
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +187 -0
- package/dist/working-memory.d.ts.map +1 -0
- package/dist/working-memory.js +767 -0
- package/dist/working-memory.js.map +1 -0
- package/dist/workspace-checkpoint.d.ts +88 -0
- package/dist/workspace-checkpoint.d.ts.map +1 -0
- package/dist/workspace-checkpoint.js +326 -0
- package/dist/workspace-checkpoint.js.map +1 -0
- package/dist/workspace-restore.d.ts +68 -0
- package/dist/workspace-restore.d.ts.map +1 -0
- package/dist/workspace-restore.js +604 -0
- package/dist/workspace-restore.js.map +1 -0
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +0 -17
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +5 -19
- package/package.json +2 -2
- package/skills/forge/SKILL.md +212 -0
- package/skills/forge/references/evidence-and-contracts.md +204 -0
- package/skills/forge/references/recovery-and-views.md +188 -0
- package/dist/cognitive-facts.d.ts +0 -30
- package/dist/cognitive-facts.d.ts.map +0 -1
- package/dist/cognitive-facts.js +0 -113
- package/dist/cognitive-facts.js.map +0 -1
- package/dist/loop-extras-parser.d.ts +0 -81
- package/dist/loop-extras-parser.d.ts.map +0 -1
- package/dist/loop-extras-parser.js +0 -213
- package/dist/loop-extras-parser.js.map +0 -1
- package/dist/loop-projection.d.ts +0 -6
- package/dist/loop-projection.d.ts.map +0 -1
- package/dist/loop-projection.js +0 -28
- package/dist/loop-projection.js.map +0 -1
- package/skills/perception/SKILL.md +0 -370
package/dist/mcp/tools.js
CHANGED
|
@@ -1,22 +1,76 @@
|
|
|
1
1
|
/** LoopForge MCP — Tool definitions and handlers.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* status is the unified inspection tool (view=session|loop|all|audit).
|
|
3
|
+
* 10 tools: start, next, status, stop, pause, resume, replay,
|
|
4
|
+
* checkpoint, gate_check, gate_resolve.
|
|
5
|
+
* status is the unified inspection tool (view=session|loop|all|audit|explain).
|
|
6
6
|
* Each handler receives SessionManager + parsed input, returns the output object.
|
|
7
7
|
*/
|
|
8
|
+
import { InvocationInputError, MANUAL_STOP_REASON, StartCriteriaError, } from "./round-lifecycle.js";
|
|
9
|
+
import { PROMPT_CONTEXT_REASONS } from "../protocol.js";
|
|
10
|
+
import { ProjectionCoverageError } from "../prompt-coverage.js";
|
|
11
|
+
import { LegacyHistoryError, LoopExistsError, OperatorAcceptanceConflictError, StartFailureError, } from "../errors.js";
|
|
8
12
|
import { validateCoreSelfEvaluation } from "../self-eval.js";
|
|
9
13
|
import { isRecord } from "../token-utils.js";
|
|
10
14
|
import { getPolicy, validateLoopId } from "../policy.js";
|
|
11
15
|
import { deriveEvidenceCapability } from "../evidence-provider.js";
|
|
12
|
-
import { fieldGuideDescription } from "../submission-guidance.js";
|
|
16
|
+
import { CORE_SELF_EVALUATION_FIELDS, fieldGuideDescription, } from "../submission-guidance.js";
|
|
17
|
+
import { LOST_HANDLE_ROUTING } from "../recovery-guidance.js";
|
|
18
|
+
import { CONTEXT_MODE_DESCRIPTION } from "../round-guidance.js";
|
|
19
|
+
import { SessionLeaseConflictError } from "../storage.js";
|
|
20
|
+
import { ReplayWindowError } from "../replay.js";
|
|
13
21
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
22
|
// Tool schemas (MCP JSON Schema format)
|
|
15
23
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
/** v4.0.0: the ONE held-round anchor. The object is closed so a client cannot
|
|
25
|
+
* smuggle separate anchor fields alongside it. */
|
|
26
|
+
const SUBMISSION_HANDLE_SCHEMA = {
|
|
27
|
+
type: "object",
|
|
28
|
+
description: "The submissionHandle object returned with the held prompt. Copy it " +
|
|
29
|
+
"verbatim; never edit, reconstruct, or derive it. A stale handle never " +
|
|
30
|
+
"submits work. " + LOST_HANDLE_ROUTING,
|
|
31
|
+
properties: {
|
|
32
|
+
sessionId: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "The owning session identity.",
|
|
35
|
+
},
|
|
36
|
+
roundId: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "The stable logical round identity.",
|
|
39
|
+
},
|
|
40
|
+
transactionId: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "The exact held transaction attempt.",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["sessionId", "roundId", "transactionId"],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
};
|
|
48
|
+
/** Output form: absent/null on terminal or non-held results, exact shape when
|
|
49
|
+
* present. */
|
|
50
|
+
const SUBMISSION_HANDLE_OUTPUT_SCHEMA = {
|
|
51
|
+
...SUBMISSION_HANDLE_SCHEMA,
|
|
52
|
+
type: ["object", "null"],
|
|
53
|
+
};
|
|
54
|
+
/** The closed argument set for `loopforge_start`.
|
|
55
|
+
*
|
|
56
|
+
* The handler's unknown-argument guard reads this list; `schema-sync.test`
|
|
57
|
+
* asserts that the advertised tool schema exposes exactly the same keys. A
|
|
58
|
+
* field removed from the protocol (for example `domain` in 4.0.1) must not
|
|
59
|
+
* survive in only one of them: the schema says what clients may send, and
|
|
60
|
+
* the guard makes a stale caller fail loudly instead of silently dropping
|
|
61
|
+
* the field. */
|
|
62
|
+
export const START_INPUT_KEYS = [
|
|
63
|
+
"task",
|
|
64
|
+
"loopId",
|
|
65
|
+
"maxRounds",
|
|
66
|
+
"planSource",
|
|
67
|
+
"constraints",
|
|
68
|
+
"criteria",
|
|
69
|
+
];
|
|
16
70
|
const TOOL_BASE_SCHEMAS = [
|
|
17
71
|
{
|
|
18
72
|
name: "loopforge_start",
|
|
19
|
-
description: "Start a new LoopForge loop
|
|
73
|
+
description: "Start a new LoopForge loop for a NEW objective. Compiles the first-round prompt from the task description and returns it. Use loopforge_status/loopforge_resume instead for an objective that already has a loopId.",
|
|
20
74
|
inputSchema: {
|
|
21
75
|
type: "object",
|
|
22
76
|
properties: {
|
|
@@ -26,17 +80,16 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
26
80
|
},
|
|
27
81
|
loopId: {
|
|
28
82
|
type: "string",
|
|
29
|
-
description: "Optional loop ID. Generated
|
|
83
|
+
description: "Optional durable loop ID. Generated when omitted and returned " +
|
|
84
|
+
"with the first prompt. Keep it: loopforge_resume and every " +
|
|
85
|
+
"loopforge_status view use loopId after a restart, and a live " +
|
|
86
|
+
"session can re-anchor a lost handle with loopId.",
|
|
30
87
|
},
|
|
31
88
|
maxRounds: {
|
|
32
89
|
type: "integer",
|
|
33
90
|
minimum: 1,
|
|
34
91
|
description: "Maximum rounds before auto-stop. Default: 200 from policy.",
|
|
35
92
|
},
|
|
36
|
-
domain: {
|
|
37
|
-
type: "string",
|
|
38
|
-
description: "Domain hint (e.g. 'solidity', 'react', 'rust') for state context.",
|
|
39
|
-
},
|
|
40
93
|
planSource: {
|
|
41
94
|
type: "string",
|
|
42
95
|
description: "Plan reference for constraint extraction (e.g. 'docs/plan.md').",
|
|
@@ -46,36 +99,73 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
46
99
|
items: { type: "string" },
|
|
47
100
|
description: "Hard constraints to enforce across all rounds.",
|
|
48
101
|
},
|
|
102
|
+
criteria: {
|
|
103
|
+
type: "array",
|
|
104
|
+
items: { type: "string" },
|
|
105
|
+
description: "Initial success criteria — the operator's acceptance surface. Phrase " +
|
|
106
|
+
"each as an observable outcome; implementation detail belongs in " +
|
|
107
|
+
"sub-goals or task text. Each becomes a stable cr-XXXXXXXX anchor " +
|
|
108
|
+
"rendered in every prompt and usable by contract items. Omit to keep " +
|
|
109
|
+
"the default criterion.",
|
|
110
|
+
},
|
|
49
111
|
},
|
|
50
112
|
required: ["task"],
|
|
51
113
|
},
|
|
52
114
|
},
|
|
53
115
|
{
|
|
54
116
|
name: "loopforge_next",
|
|
55
|
-
description: "Submit the
|
|
117
|
+
description: "Submit one evaluation for the held attempt. Returns the next prompt (with its derived contextMode/contextReason and a strategy label), or null with stopReason/stopDetail when the loop ends. The submission may be accepted, rejected (same logical round, attempt+1, new handle), replayed, or answered with a rollback directive. A structured evaluation is required unless currentUserInstruction is the only argument: then the held prompt is recompiled for that instruction without submitting or advancing. If the held prompt is same_round_retry and the original invocation is gone, do NOT use next — call loopforge_resume(loopId, invocationLost: true) so LoopForge recompiles the same round/attempt as a cold-context prompt.",
|
|
56
118
|
inputSchema: {
|
|
57
119
|
type: "object",
|
|
58
120
|
properties: {
|
|
59
|
-
|
|
121
|
+
submissionHandle: SUBMISSION_HANDLE_SCHEMA,
|
|
122
|
+
output: {
|
|
60
123
|
type: "string",
|
|
61
|
-
description: "
|
|
124
|
+
description: "Optional. The agent's full output from executing the current round's prompt. The structured evaluation is the authoritative report.",
|
|
62
125
|
},
|
|
63
|
-
|
|
126
|
+
currentUserInstruction: {
|
|
64
127
|
type: "string",
|
|
65
|
-
description: "
|
|
128
|
+
description: "v4.0.5: the host's current user instruction for this invocation, " +
|
|
129
|
+
"trimmed and non-empty. It is invocation input, not a task update: " +
|
|
130
|
+
"it never rewrites session.task, the Objective, Success Criteria, " +
|
|
131
|
+
"hard constraints or the Round Contract. With evaluation/recovery " +
|
|
132
|
+
"it is carried into the retry/redo/next prompt compiled after the " +
|
|
133
|
+
"submission; with only the handle it recompiles the held prompt " +
|
|
134
|
+
"without submitting or advancing the round.",
|
|
66
135
|
},
|
|
67
|
-
|
|
68
|
-
type: "
|
|
69
|
-
description: "
|
|
136
|
+
recovery: {
|
|
137
|
+
type: "object",
|
|
138
|
+
description: "v3.9.7: Agent-initiated recovery. Abandon the current branch and " +
|
|
139
|
+
"restore a visible workspace checkpoint (the held before-image, a " +
|
|
140
|
+
"mid-round savepoint, or a committed round's before-image). " +
|
|
141
|
+
"Mutually exclusive with evaluation. LoopForge records the directive, " +
|
|
142
|
+
"executes the restore, verifies the full Git boundary, then issues " +
|
|
143
|
+
"the redo prompt. It never chooses a target or judges the reason.",
|
|
144
|
+
properties: {
|
|
145
|
+
target_checkpoint_id: {
|
|
146
|
+
type: "string",
|
|
147
|
+
pattern: "^ck-[a-f0-9]{24}$",
|
|
148
|
+
description: "Checkpoint id (ck-<24 hex>) from the held prompt, a loopforge_checkpoint result, or a committed round's before-image.",
|
|
149
|
+
},
|
|
150
|
+
reason: {
|
|
151
|
+
type: "string",
|
|
152
|
+
minLength: 1,
|
|
153
|
+
maxLength: 500,
|
|
154
|
+
description: "The Agent's reason for abandoning the branch. Recorded verbatim; never judged.",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
required: ["target_checkpoint_id", "reason"],
|
|
158
|
+
additionalProperties: false,
|
|
70
159
|
},
|
|
71
160
|
evaluation: {
|
|
72
161
|
type: "object",
|
|
73
|
-
description: "
|
|
74
|
-
required: ["
|
|
162
|
+
description: "Structured self-evaluation for this round. Required when recovery is absent. Free-text or embedded evaluation blocks are not accepted.",
|
|
163
|
+
required: ["outcome", "output_summary", "should_continue", "constraint_violations"],
|
|
75
164
|
properties: {
|
|
76
|
-
|
|
77
|
-
type: "
|
|
78
|
-
|
|
165
|
+
outcome: {
|
|
166
|
+
type: "string",
|
|
167
|
+
enum: ["success", "partial", "failed", "blocked"],
|
|
168
|
+
description: fieldGuideDescription("outcome"),
|
|
79
169
|
},
|
|
80
170
|
output_summary: {
|
|
81
171
|
type: "string",
|
|
@@ -95,10 +185,6 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
95
185
|
items: { type: "string" },
|
|
96
186
|
description: fieldGuideDescription("discovered_constraints"),
|
|
97
187
|
},
|
|
98
|
-
objective_refinement: {
|
|
99
|
-
type: "string",
|
|
100
|
-
description: fieldGuideDescription("objective_refinement"),
|
|
101
|
-
},
|
|
102
188
|
emerged_subtasks: {
|
|
103
189
|
type: "array",
|
|
104
190
|
items: { type: "string" },
|
|
@@ -113,38 +199,21 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
113
199
|
items: { type: "string" },
|
|
114
200
|
description: fieldGuideDescription("execution_report.files_changed"),
|
|
115
201
|
},
|
|
116
|
-
|
|
117
|
-
type: "object",
|
|
118
|
-
description: fieldGuideDescription("execution_report.tests_reported"),
|
|
119
|
-
properties: {
|
|
120
|
-
passed: { type: "integer", minimum: 0, description: "Number of passing tests." },
|
|
121
|
-
failed: { type: "integer", minimum: 0, description: "Number of failing tests." },
|
|
122
|
-
skipped: { type: "integer", minimum: 0, description: "Number of skipped tests." },
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
criterion_claims: {
|
|
202
|
+
claims: {
|
|
126
203
|
type: "array",
|
|
127
|
-
|
|
204
|
+
maxItems: 100,
|
|
205
|
+
description: fieldGuideDescription("execution_report.claims"),
|
|
128
206
|
items: {
|
|
129
207
|
type: "object",
|
|
130
208
|
properties: {
|
|
131
|
-
|
|
209
|
+
id: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "cr-XXXXXXXX objective criterion id or rci-XXXXXXXX " +
|
|
212
|
+
"id of an item of the ACTIVE contract.",
|
|
213
|
+
},
|
|
132
214
|
outcome: { type: "string", enum: ["met", "remaining"] },
|
|
133
215
|
},
|
|
134
|
-
required: ["
|
|
135
|
-
additionalProperties: false,
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
contract_item_claims: {
|
|
139
|
-
type: "array",
|
|
140
|
-
description: fieldGuideDescription("execution_report.contract_item_claims"),
|
|
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"],
|
|
216
|
+
required: ["id", "outcome"],
|
|
148
217
|
additionalProperties: false,
|
|
149
218
|
},
|
|
150
219
|
},
|
|
@@ -155,22 +224,26 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
155
224
|
items: { type: "string" },
|
|
156
225
|
description: fieldGuideDescription("retracted_constraints"),
|
|
157
226
|
},
|
|
158
|
-
revised_success_criteria: {
|
|
159
|
-
type: "array",
|
|
160
|
-
description: fieldGuideDescription("revised_success_criteria"),
|
|
161
|
-
items: {
|
|
162
|
-
type: "object",
|
|
163
|
-
properties: {
|
|
164
|
-
old: { type: "string", description: "Original criterion." },
|
|
165
|
-
new: { type: "string", description: "Revised criterion." },
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
227
|
wrong_assumptions: {
|
|
170
228
|
type: "array",
|
|
171
229
|
items: { type: "string" },
|
|
172
230
|
description: fieldGuideDescription("wrong_assumptions"),
|
|
173
231
|
},
|
|
232
|
+
open_questions: {
|
|
233
|
+
type: "array",
|
|
234
|
+
maxItems: 3,
|
|
235
|
+
items: { type: "string", maxLength: 300 },
|
|
236
|
+
description: fieldGuideDescription("open_questions"),
|
|
237
|
+
},
|
|
238
|
+
resolved_open_question_ids: {
|
|
239
|
+
type: "array",
|
|
240
|
+
maxItems: 20,
|
|
241
|
+
items: {
|
|
242
|
+
type: "string",
|
|
243
|
+
pattern: "^oq-[a-f0-9]{12}$",
|
|
244
|
+
},
|
|
245
|
+
description: fieldGuideDescription("resolved_open_question_ids"),
|
|
246
|
+
},
|
|
174
247
|
worker_results: {
|
|
175
248
|
type: "array",
|
|
176
249
|
description: fieldGuideDescription("worker_results"),
|
|
@@ -178,28 +251,23 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
178
251
|
type: "object",
|
|
179
252
|
properties: {
|
|
180
253
|
agentId: { type: "string" },
|
|
181
|
-
subAgentType: { type: "string" },
|
|
182
254
|
subTask: { type: "string" },
|
|
183
255
|
resultSummary: { type: "string" },
|
|
184
256
|
outcome: {
|
|
185
257
|
type: "string",
|
|
186
258
|
enum: ["success", "partial", "failed"],
|
|
187
259
|
},
|
|
188
|
-
discoveredConstraints: {
|
|
189
|
-
type: "array",
|
|
190
|
-
items: { type: "string" },
|
|
191
|
-
},
|
|
192
260
|
},
|
|
193
261
|
required: ["agentId", "subTask", "resultSummary", "outcome"],
|
|
194
262
|
},
|
|
195
263
|
},
|
|
196
|
-
|
|
264
|
+
subtask_boundary: {
|
|
197
265
|
type: "boolean",
|
|
198
|
-
description: fieldGuideDescription("
|
|
266
|
+
description: fieldGuideDescription("subtask_boundary"),
|
|
199
267
|
},
|
|
200
|
-
|
|
268
|
+
subtask_boundary_label: {
|
|
201
269
|
type: "string",
|
|
202
|
-
description: fieldGuideDescription("
|
|
270
|
+
description: fieldGuideDescription("subtask_boundary_label"),
|
|
203
271
|
},
|
|
204
272
|
subgoal_updates: {
|
|
205
273
|
type: "array",
|
|
@@ -224,22 +292,18 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
224
292
|
required: ["id", "status"],
|
|
225
293
|
},
|
|
226
294
|
},
|
|
227
|
-
stop_reason: {
|
|
228
|
-
type: "string",
|
|
229
|
-
enum: ["gave_up", "blocked", "needs_human_input"],
|
|
230
|
-
description: fieldGuideDescription("stop_reason"),
|
|
231
|
-
},
|
|
232
295
|
no_change_reason: {
|
|
233
296
|
type: "string",
|
|
234
297
|
description: fieldGuideDescription("no_change_reason"),
|
|
235
298
|
},
|
|
236
|
-
|
|
299
|
+
objective_refinement: {
|
|
237
300
|
type: "string",
|
|
238
|
-
|
|
239
|
-
description: fieldGuideDescription("
|
|
301
|
+
maxLength: 1000,
|
|
302
|
+
description: fieldGuideDescription("objective_refinement"),
|
|
240
303
|
},
|
|
241
304
|
blocker: {
|
|
242
305
|
type: "string",
|
|
306
|
+
maxLength: 500,
|
|
243
307
|
description: fieldGuideDescription("blocker"),
|
|
244
308
|
},
|
|
245
309
|
gate_ids: {
|
|
@@ -261,54 +325,53 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
261
325
|
},
|
|
262
326
|
description: fieldGuideDescription("retroactiveClaims"),
|
|
263
327
|
},
|
|
264
|
-
prompt_requests: {
|
|
265
|
-
type: "object",
|
|
266
|
-
description: fieldGuideDescription("prompt_requests"),
|
|
267
|
-
properties: {
|
|
268
|
-
emphasize: {
|
|
269
|
-
type: "array",
|
|
270
|
-
items: { type: "string" },
|
|
271
|
-
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.",
|
|
272
|
-
},
|
|
273
|
-
confusion_points: {
|
|
274
|
-
type: "array",
|
|
275
|
-
items: { type: "string" },
|
|
276
|
-
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.'",
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
328
|
round_contract: {
|
|
281
329
|
type: "object",
|
|
282
330
|
description: fieldGuideDescription("round_contract"),
|
|
283
331
|
properties: {
|
|
284
332
|
work_item: {
|
|
285
333
|
type: "string",
|
|
286
|
-
|
|
334
|
+
minLength: 1,
|
|
335
|
+
maxLength: 200,
|
|
336
|
+
description: "Required. The bounded deliverable this contract slice " +
|
|
337
|
+
"produces — an outcome, not an activity. Rendered as the " +
|
|
338
|
+
"Current Task heading. 1–200 chars.",
|
|
287
339
|
},
|
|
288
340
|
items: {
|
|
289
341
|
type: "array",
|
|
290
|
-
|
|
342
|
+
minItems: 1,
|
|
343
|
+
maxItems: 20,
|
|
344
|
+
description: "Required, at least one (max 20). Each item is an " +
|
|
345
|
+
"acceptance condition for the deliverable, bound to its own " +
|
|
346
|
+
"evidence commands. The runtime derives a stable " +
|
|
347
|
+
"rci-XXXXXXXX id per item and renders it in the prompt — " +
|
|
348
|
+
"cite that id in execution_report.claims.",
|
|
291
349
|
items: {
|
|
292
350
|
type: "object",
|
|
293
351
|
properties: {
|
|
294
352
|
description: {
|
|
295
353
|
type: "string",
|
|
296
|
-
|
|
354
|
+
minLength: 1,
|
|
355
|
+
maxLength: 200,
|
|
356
|
+
description: "What must be true for this acceptance condition. " +
|
|
357
|
+
"1–200 chars.",
|
|
297
358
|
},
|
|
298
359
|
criterion_refs: {
|
|
299
360
|
type: "array",
|
|
361
|
+
maxItems: 20,
|
|
300
362
|
items: { type: "string" },
|
|
301
|
-
description: "Optional. cr-XXXXXXXX
|
|
302
|
-
},
|
|
303
|
-
subgoal_refs: {
|
|
304
|
-
type: "array",
|
|
305
|
-
items: { type: "string" },
|
|
306
|
-
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.",
|
|
363
|
+
description: "Optional. Real cr-XXXXXXXX ids of ACTIVE criteria rendered in this prompt. Free text and unknown ids are rejected. Max 20.",
|
|
307
364
|
},
|
|
308
365
|
verify_with: {
|
|
309
366
|
type: "array",
|
|
367
|
+
minItems: 1,
|
|
368
|
+
maxItems: 20,
|
|
310
369
|
items: { type: "string" },
|
|
311
|
-
description: "Required, at least one. Names of configured,
|
|
370
|
+
description: "Required, at least one. Names of configured, " +
|
|
371
|
+
"enabled, after-capable evidence.commands " +
|
|
372
|
+
"(loop_policy.json) that check this acceptance " +
|
|
373
|
+
"condition. Unknown or disabled names are rejected. A " +
|
|
374
|
+
"command may be shared by multiple conditions.",
|
|
312
375
|
},
|
|
313
376
|
},
|
|
314
377
|
required: ["description", "verify_with"],
|
|
@@ -317,31 +380,87 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
317
380
|
},
|
|
318
381
|
scope: {
|
|
319
382
|
type: "array",
|
|
383
|
+
maxItems: 50,
|
|
320
384
|
items: { type: "string" },
|
|
321
|
-
description: "
|
|
385
|
+
description: "Optional, report-only. The files/directories you intend " +
|
|
386
|
+
"this slice to touch. Omission and [] both mean no declared " +
|
|
387
|
+
"intent. It is NOT enforced and is not part of the rc- id: " +
|
|
388
|
+
"out-of-scope changes are reported as an info fact and " +
|
|
389
|
+
"never reject the round. Max 50 items.",
|
|
390
|
+
},
|
|
391
|
+
supersedes: {
|
|
392
|
+
type: "object",
|
|
393
|
+
description: "Optional redirect modifier. Replaces the named ACTIVE " +
|
|
394
|
+
"contract with this proposal's slice; its acceptance items " +
|
|
395
|
+
"must differ. The old contract closes as superseded. Not " +
|
|
396
|
+
"allowed with should_continue=false or outcome=blocked.",
|
|
397
|
+
properties: {
|
|
398
|
+
contract_id: {
|
|
399
|
+
type: "string",
|
|
400
|
+
pattern: "^rc-[a-f0-9]{8}$",
|
|
401
|
+
description: "The rc-XXXXXXXX id of the ACTIVE contract rendered in " +
|
|
402
|
+
"this prompt. A stale/other id is rejected.",
|
|
403
|
+
},
|
|
404
|
+
reason: {
|
|
405
|
+
type: "string",
|
|
406
|
+
maxLength: 500,
|
|
407
|
+
description: "Optional Agent report, recorded verbatim for audit only.",
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
required: ["contract_id"],
|
|
411
|
+
additionalProperties: false,
|
|
322
412
|
},
|
|
323
413
|
},
|
|
324
|
-
required: ["
|
|
414
|
+
required: ["work_item", "items"],
|
|
415
|
+
additionalProperties: false,
|
|
325
416
|
},
|
|
326
417
|
},
|
|
327
418
|
},
|
|
328
419
|
},
|
|
329
|
-
required: ["
|
|
420
|
+
required: ["submissionHandle"],
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "loopforge_checkpoint",
|
|
425
|
+
description: "v3.9.7: Create an explicit mid-round workspace checkpoint (savepoint). " +
|
|
426
|
+
"The call itself is the Agent's decision that this state is worth " +
|
|
427
|
+
"restoring to. It creates no round, no commit and no progress; the " +
|
|
428
|
+
"checkpoint becomes a visible recovery target only through the held " +
|
|
429
|
+
"transaction's ref list. Requires the same submissionHandle as " +
|
|
430
|
+
"loopforge_next; the handle is unchanged after a successful checkpoint.",
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: "object",
|
|
433
|
+
properties: {
|
|
434
|
+
submissionHandle: SUBMISSION_HANDLE_SCHEMA,
|
|
435
|
+
},
|
|
436
|
+
required: ["submissionHandle"],
|
|
437
|
+
additionalProperties: false,
|
|
330
438
|
},
|
|
331
439
|
},
|
|
332
440
|
{
|
|
333
441
|
name: "loopforge_status",
|
|
334
|
-
description: "
|
|
442
|
+
description: "Read-only inspection. view=session (default): live session status, " +
|
|
443
|
+
"round identity, the current held submissionHandle when one exists, " +
|
|
444
|
+
"success trajectory, active contract, current guidance and the latest " +
|
|
445
|
+
"committed receipt. It never returns the held prompt; a paused or " +
|
|
446
|
+
"stalled session must resume before submitting. view=loop: committed " +
|
|
447
|
+
"round counts, unverified-item rounds and policy metrics. view=all: " +
|
|
448
|
+
"summary of every persisted loop. view=audit: end-of-loop verification " +
|
|
449
|
+
"audit. view=explain: per-round \"why\" view over committed facts.",
|
|
335
450
|
inputSchema: {
|
|
336
451
|
type: "object",
|
|
337
452
|
properties: {
|
|
338
453
|
sessionId: {
|
|
339
454
|
type: "string",
|
|
340
|
-
description: "
|
|
455
|
+
description: "Live session ID returned by loopforge_start or loopforge_resume. " +
|
|
456
|
+
"Resolvable only in the current process; after a restart use loopId. " +
|
|
457
|
+
"view=session returns the current held submissionHandle.",
|
|
341
458
|
},
|
|
342
459
|
loopId: {
|
|
343
460
|
type: "string",
|
|
344
|
-
description: "
|
|
461
|
+
description: "Durable loop ID. Required for views loop/audit/explain; also " +
|
|
462
|
+
"accepted for view=session, which resolves it to the live session " +
|
|
463
|
+
"and returns its current held submissionHandle.",
|
|
345
464
|
},
|
|
346
465
|
view: {
|
|
347
466
|
type: "string",
|
|
@@ -358,7 +477,8 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
358
477
|
},
|
|
359
478
|
{
|
|
360
479
|
name: "loopforge_stop",
|
|
361
|
-
description: "Manually stop a loop session and return its final trajectory."
|
|
480
|
+
description: "Manually stop a loop session and return its final trajectory. The " +
|
|
481
|
+
"returned `success` is tool-call success, not a task-success verdict.",
|
|
362
482
|
inputSchema: {
|
|
363
483
|
type: "object",
|
|
364
484
|
properties: {
|
|
@@ -372,7 +492,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
372
492
|
},
|
|
373
493
|
{
|
|
374
494
|
name: "loopforge_pause",
|
|
375
|
-
description: "Pause a running loop session
|
|
495
|
+
description: "Pause a running loop session: it immediately stops accepting submissions (an in-flight advance may still finish) 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.",
|
|
376
496
|
inputSchema: {
|
|
377
497
|
type: "object",
|
|
378
498
|
properties: {
|
|
@@ -386,10 +506,12 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
386
506
|
},
|
|
387
507
|
{
|
|
388
508
|
name: "loopforge_replay",
|
|
389
|
-
description: "Replay
|
|
390
|
-
"
|
|
391
|
-
"
|
|
392
|
-
"
|
|
509
|
+
description: "Replay committed rounds as an auditable timeline. Pass sessionId for a " +
|
|
510
|
+
"live session, or loopId to replay straight from the vault (works after " +
|
|
511
|
+
"a process restart). Returns the last 10 committed rounds by default; " +
|
|
512
|
+
"pass start/end for an explicit window (at most 50 rounds). Each round " +
|
|
513
|
+
"carries its context boundary, committed report, assumptions, open " +
|
|
514
|
+
"questions, blocker and contract redirect. Read-only projection.",
|
|
393
515
|
inputSchema: {
|
|
394
516
|
type: "object",
|
|
395
517
|
properties: {
|
|
@@ -402,6 +524,16 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
402
524
|
description: "Loop ID to replay from the vault — works without a " +
|
|
403
525
|
"live session, e.g. after a restart.",
|
|
404
526
|
},
|
|
527
|
+
start: {
|
|
528
|
+
type: "integer",
|
|
529
|
+
minimum: 1,
|
|
530
|
+
description: "First round of the explicit window (inclusive).",
|
|
531
|
+
},
|
|
532
|
+
end: {
|
|
533
|
+
type: "integer",
|
|
534
|
+
minimum: 1,
|
|
535
|
+
description: "Last round of the explicit window (inclusive).",
|
|
536
|
+
},
|
|
405
537
|
},
|
|
406
538
|
// v3.3.1: either identifier resolves the timeline; sessionId wins when
|
|
407
539
|
// both are given (it also disambiguates re-used loopIds).
|
|
@@ -410,7 +542,20 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
410
542
|
},
|
|
411
543
|
{
|
|
412
544
|
name: "loopforge_resume",
|
|
413
|
-
description: "Resume a loop from vault state.
|
|
545
|
+
description: "Resume a loop from vault state. Requires the durable loopId returned by " +
|
|
546
|
+
"loopforge_start (not sessionId). Use this when the held prompt was lost, " +
|
|
547
|
+
"no live session exists, or the session is paused/stalled; if only the " +
|
|
548
|
+
"submissionHandle was lost while the prompt is still in context, prefer " +
|
|
549
|
+
"loopforge_status. Returns the held prompt and its current " +
|
|
550
|
+
"submissionHandle, or a newly compiled prompt when none is held. A " +
|
|
551
|
+
"terminal loop returns no prompt, the committed stopReason when one was " +
|
|
552
|
+
"committed, and/or sessionStatus (paused/stopped/stalled); a stopped " +
|
|
553
|
+
"loop without a committed stop round reports sessionStatus only. " +
|
|
554
|
+
"v4.0.5: pass invocationLost=true when the original in-context retry " +
|
|
555
|
+
"invocation no longer exists; LoopForge recompiles the SAME logical " +
|
|
556
|
+
"round and attempt as a cold-context invocation_recovery prompt. " +
|
|
557
|
+
"currentUserInstruction carries the host's current instruction into " +
|
|
558
|
+
"that recompile or redelivery.",
|
|
414
559
|
inputSchema: {
|
|
415
560
|
type: "object",
|
|
416
561
|
properties: {
|
|
@@ -418,6 +563,24 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
418
563
|
type: "string",
|
|
419
564
|
description: "Loop ID to resume. Must have a saved session_state entry from a previous start/run.",
|
|
420
565
|
},
|
|
566
|
+
invocationLost: {
|
|
567
|
+
type: "boolean",
|
|
568
|
+
description: "v4.0.5: set true when the invocation that received the held " +
|
|
569
|
+
"same_round_retry prompt is gone. A held retry is then recompiled " +
|
|
570
|
+
"as fresh_invocation / invocation_recovery for the SAME round and " +
|
|
571
|
+
"attempt (L2, with Machine Evidence, Working Memory and rejection " +
|
|
572
|
+
"lessons). On an already clean-context prompt this is an " +
|
|
573
|
+
"idempotent no-op.",
|
|
574
|
+
},
|
|
575
|
+
currentUserInstruction: {
|
|
576
|
+
type: "string",
|
|
577
|
+
description: "v4.0.5: the host's current user instruction, trimmed and " +
|
|
578
|
+
"non-empty. It is invocation input only; it never rewrites " +
|
|
579
|
+
"session.task, the Objective, criteria, constraints or the Round " +
|
|
580
|
+
"Contract. An instruction identical (trimmed, case-sensitive) to " +
|
|
581
|
+
"the held prompt's is idempotent; a different one recompiles the " +
|
|
582
|
+
"same round/attempt with a new handle.",
|
|
583
|
+
},
|
|
421
584
|
},
|
|
422
585
|
required: ["loopId"],
|
|
423
586
|
},
|
|
@@ -433,8 +596,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
433
596
|
inputSchema: {
|
|
434
597
|
type: "object",
|
|
435
598
|
properties: {
|
|
436
|
-
|
|
437
|
-
roundId: { type: "string", description: "RoundId from the latest response." },
|
|
599
|
+
submissionHandle: SUBMISSION_HANDLE_SCHEMA,
|
|
438
600
|
action: {
|
|
439
601
|
type: "object",
|
|
440
602
|
description: "The action the agent plans to perform.",
|
|
@@ -471,7 +633,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
471
633
|
required: ["description", "scope", "effects", "reversibility", "authorization"],
|
|
472
634
|
},
|
|
473
635
|
},
|
|
474
|
-
required: ["
|
|
636
|
+
required: ["submissionHandle", "action"],
|
|
475
637
|
},
|
|
476
638
|
},
|
|
477
639
|
{
|
|
@@ -510,19 +672,79 @@ const ADVANCE_OUTPUT_SCHEMA = {
|
|
|
510
672
|
type: "object",
|
|
511
673
|
properties: {
|
|
512
674
|
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
513
|
-
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?,
|
|
675
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, submissionHandle?, details? }" },
|
|
514
676
|
details: { type: "object", additionalProperties: true },
|
|
515
677
|
sessionId: { type: "string" },
|
|
678
|
+
loopId: {
|
|
679
|
+
type: "string",
|
|
680
|
+
description: "Durable loop identity. Returned with every held prompt so a restarted " +
|
|
681
|
+
"agent can call loopforge_resume without reconstructing IDs.",
|
|
682
|
+
},
|
|
516
683
|
round: { type: "number" },
|
|
517
|
-
|
|
684
|
+
submissionHandle: SUBMISSION_HANDLE_OUTPUT_SCHEMA,
|
|
518
685
|
prompt: { type: ["string", "null"] },
|
|
519
|
-
stopReason: {
|
|
520
|
-
|
|
686
|
+
stopReason: {
|
|
687
|
+
type: ["string", "null"],
|
|
688
|
+
enum: [
|
|
689
|
+
"completed", "failed", "blocked", "cancelled", "max_rounds",
|
|
690
|
+
"stalled", "enforcement_terminated", "paused", "incomplete", null,
|
|
691
|
+
],
|
|
692
|
+
description: "v4.0.4: LoopForge's terminal reason. Never a session status, a tool " +
|
|
693
|
+
"error or the Agent's round outcome.",
|
|
694
|
+
},
|
|
695
|
+
sessionStatus: {
|
|
696
|
+
enum: ["running", "stopped", "paused", "stalled"],
|
|
697
|
+
description: "v4.0.4: the live session status when the result is not a committed " +
|
|
698
|
+
"loop terminal reason.",
|
|
699
|
+
},
|
|
700
|
+
stopDetail: { type: ["string", "null"], description: "Human-readable context for why the loop stopped. Three fact blocks: Agent report, LoopForge decision, Machine evidence. Provides facts without prescribing a specific agent action." },
|
|
521
701
|
level: { type: ["string", "null"] },
|
|
522
|
-
|
|
523
|
-
|
|
702
|
+
contextMode: {
|
|
703
|
+
enum: ["fresh_invocation", "same_round_retry"],
|
|
704
|
+
description: CONTEXT_MODE_DESCRIPTION,
|
|
705
|
+
},
|
|
706
|
+
contextReason: {
|
|
707
|
+
// v4.0.5: the ONE runtime whitelist; a new reason cannot drift between
|
|
708
|
+
// the protocol, the decoder and this output schema.
|
|
709
|
+
enum: [...PROMPT_CONTEXT_REASONS],
|
|
710
|
+
description: "v4.0.2: why the context boundary was selected.",
|
|
711
|
+
},
|
|
712
|
+
agentSuccessClaim: { type: ["boolean", "null"] },
|
|
713
|
+
agentOutcome: {
|
|
714
|
+
type: ["string", "null"],
|
|
715
|
+
enum: ["success", "partial", "failed", "blocked", null],
|
|
716
|
+
description: "v4.0.6: the Agent-declared round outcome.",
|
|
717
|
+
},
|
|
718
|
+
agentShouldContinue: {
|
|
719
|
+
type: ["boolean", "null"],
|
|
720
|
+
description: "v4.0.4: the Agent's declared should_continue for that same round.",
|
|
721
|
+
},
|
|
722
|
+
checkpoints: {
|
|
723
|
+
type: ["array", "null"],
|
|
724
|
+
items: { type: "object", additionalProperties: true },
|
|
725
|
+
description: "v3.9.7: the held transaction's checkpoint refs (before-image + explicit mid savepoints).",
|
|
726
|
+
},
|
|
727
|
+
beforeCheckpointError: {
|
|
728
|
+
type: ["object", "null"],
|
|
729
|
+
additionalProperties: true,
|
|
730
|
+
description: "v3.9.7: explicit machine fact when the automatic before-image could not be captured.",
|
|
731
|
+
},
|
|
732
|
+
enforcementAction: { enum: ["accept", "reject", "terminate", "backtrack", "recovery"] },
|
|
524
733
|
enforcementReason: { type: ["string", "null"] },
|
|
525
|
-
|
|
734
|
+
recovery: {
|
|
735
|
+
type: ["object", "null"],
|
|
736
|
+
additionalProperties: true,
|
|
737
|
+
description: "v3.9.7: committed rollback directive facts and the verified restore that produced the redo prompt.",
|
|
738
|
+
},
|
|
739
|
+
evidencePosture: {
|
|
740
|
+
type: ["object", "null"],
|
|
741
|
+
additionalProperties: true,
|
|
742
|
+
description: "v4.0.3: the ONE evidence posture of the round this result is about " +
|
|
743
|
+
"(status, machineBackedSuccess, verificationConfigured, workspace, " +
|
|
744
|
+
"files reconciliation, providers, commands, contract snapshot, " +
|
|
745
|
+
"findings and omitted counts). Accepted rounds carry the persisted " +
|
|
746
|
+
"posture; a rejection carries its transient copy.",
|
|
747
|
+
},
|
|
526
748
|
submission_guidance: {
|
|
527
749
|
type: ["object", "null"],
|
|
528
750
|
additionalProperties: true,
|
|
@@ -538,11 +760,6 @@ const ADVANCE_OUTPUT_SCHEMA = {
|
|
|
538
760
|
"Returned by loopforge_next only.",
|
|
539
761
|
},
|
|
540
762
|
warnings: { type: "array", items: { type: "string" } },
|
|
541
|
-
projection: {
|
|
542
|
-
type: "object",
|
|
543
|
-
additionalProperties: true,
|
|
544
|
-
description: "v2.12: Typed cognitive state projection (focus/todo/phase/delegation/handoff).",
|
|
545
|
-
},
|
|
546
763
|
},
|
|
547
764
|
additionalProperties: true,
|
|
548
765
|
};
|
|
@@ -550,22 +767,59 @@ const ADVANCE_OUTPUT_SCHEMA = {
|
|
|
550
767
|
* validates each handler's output against these before returning it —
|
|
551
768
|
* a mismatch is a contract bug, surfaced as a JSON-RPC error instead of
|
|
552
769
|
* silently shipping an output that violates the declared schema. */
|
|
770
|
+
const CHECKPOINT_OUTPUT_SCHEMA = {
|
|
771
|
+
type: "object",
|
|
772
|
+
properties: {
|
|
773
|
+
ok: { type: "boolean" },
|
|
774
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, submissionHandle?, details? }" },
|
|
775
|
+
sessionId: { type: "string" },
|
|
776
|
+
submissionHandle: SUBMISSION_HANDLE_SCHEMA,
|
|
777
|
+
checkpointId: { type: "string" },
|
|
778
|
+
round: { type: "number" },
|
|
779
|
+
role: { const: "mid" },
|
|
780
|
+
redoRound: { type: "number" },
|
|
781
|
+
createdAt: { type: "number" },
|
|
782
|
+
head: { type: ["string", "null"] },
|
|
783
|
+
fileCount: { type: "number" },
|
|
784
|
+
contentCount: { type: "number" },
|
|
785
|
+
contentHash: { type: "string" },
|
|
786
|
+
},
|
|
787
|
+
additionalProperties: true,
|
|
788
|
+
};
|
|
553
789
|
export const TOOL_OUTPUT_SCHEMAS = {
|
|
554
790
|
loopforge_start: ADVANCE_OUTPUT_SCHEMA,
|
|
555
791
|
loopforge_next: ADVANCE_OUTPUT_SCHEMA,
|
|
556
792
|
loopforge_resume: ADVANCE_OUTPUT_SCHEMA,
|
|
793
|
+
loopforge_checkpoint: CHECKPOINT_OUTPUT_SCHEMA,
|
|
557
794
|
loopforge_status: {
|
|
558
795
|
type: "object",
|
|
559
796
|
properties: {
|
|
560
797
|
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
561
|
-
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?,
|
|
798
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, submissionHandle?, details? }" },
|
|
562
799
|
sessionId: { type: "string" },
|
|
563
800
|
loopId: { type: "string" },
|
|
564
801
|
round: { type: "number" },
|
|
565
|
-
|
|
802
|
+
submissionHandle: SUBMISSION_HANDLE_OUTPUT_SCHEMA,
|
|
803
|
+
checkpoints: { type: ["array", "null"], items: { type: "object", additionalProperties: true } },
|
|
804
|
+
beforeCheckpointError: { type: ["object", "null"], additionalProperties: true },
|
|
566
805
|
maxRounds: { type: "number" },
|
|
567
806
|
status: { enum: ["running", "stopped", "stalled", "paused"] },
|
|
568
|
-
|
|
807
|
+
// v4.0.4: a stopped loop's committed terminal facts are readable from
|
|
808
|
+
// view=session without calling resume.
|
|
809
|
+
loopStopReason: {
|
|
810
|
+
type: ["string", "null"],
|
|
811
|
+
enum: [
|
|
812
|
+
"completed", "failed", "blocked", "cancelled", "max_rounds",
|
|
813
|
+
"stalled", "enforcement_terminated", "paused", "incomplete", null,
|
|
814
|
+
],
|
|
815
|
+
},
|
|
816
|
+
agentShouldContinue: { type: ["boolean", "null"] },
|
|
817
|
+
stopDetail: { type: ["string", "null"] },
|
|
818
|
+
machineBackedSuccessTrajectory: { type: "array", items: { type: "boolean" } },
|
|
819
|
+
evidencePosture: { type: ["object", "null"], additionalProperties: true },
|
|
820
|
+
machineBackedSuccess: { type: ["boolean", "null"] },
|
|
821
|
+
agentSuccessClaim: { type: ["boolean", "null"] },
|
|
822
|
+
agentOutcome: { type: ["string", "null"] },
|
|
569
823
|
lease: { type: ["object", "null"], additionalProperties: true },
|
|
570
824
|
metrics: { type: "object", additionalProperties: true },
|
|
571
825
|
// v3.5: the ACTIVE Round Contract governing the next round (derived
|
|
@@ -583,10 +837,16 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
583
837
|
type: "object",
|
|
584
838
|
properties: {
|
|
585
839
|
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
586
|
-
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?,
|
|
587
|
-
success: { type: "boolean" },
|
|
588
|
-
|
|
589
|
-
|
|
840
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, submissionHandle?, details? }" },
|
|
841
|
+
success: { type: "boolean", description: "Tool-call success, not a task-success verdict." },
|
|
842
|
+
sessionId: { type: "string" },
|
|
843
|
+
loopId: { type: "string" },
|
|
844
|
+
roundsCompleted: { type: "number", description: "Deprecated alias of roundCursor (the session round cursor, not a committed count)." },
|
|
845
|
+
roundCursor: { type: "number", description: "The session's current round cursor." },
|
|
846
|
+
sessionStatus: { const: "stopped" },
|
|
847
|
+
loopStopReason: { const: MANUAL_STOP_REASON, description: "The manual-cancel terminal reason sent to the terminal sink; not a committed round fact." },
|
|
848
|
+
machineBackedSuccessRounds: { type: "number", description: "Machine-backed successful rounds (same committed-view source as the trajectory)." },
|
|
849
|
+
machineBackedSuccessTrajectory: { type: "array", items: { type: "boolean" } },
|
|
590
850
|
},
|
|
591
851
|
additionalProperties: true,
|
|
592
852
|
},
|
|
@@ -594,7 +854,7 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
594
854
|
type: "object",
|
|
595
855
|
properties: {
|
|
596
856
|
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
597
|
-
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?,
|
|
857
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, submissionHandle?, details? }" },
|
|
598
858
|
sessionId: { type: "string" },
|
|
599
859
|
round: { type: "number" },
|
|
600
860
|
status: { type: "string" },
|
|
@@ -605,10 +865,16 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
605
865
|
type: "object",
|
|
606
866
|
properties: {
|
|
607
867
|
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
608
|
-
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?,
|
|
868
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, submissionHandle?, details? }" },
|
|
609
869
|
sessionId: { type: "string" },
|
|
610
870
|
loopId: { type: "string" },
|
|
611
871
|
timeline: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
872
|
+
window: {
|
|
873
|
+
type: "object",
|
|
874
|
+
additionalProperties: true,
|
|
875
|
+
description: "v4.0.2: the returned round window: { start, end, totalRounds, " +
|
|
876
|
+
"hasBefore, hasAfter }.",
|
|
877
|
+
},
|
|
612
878
|
legacyRounds: {
|
|
613
879
|
type: "array",
|
|
614
880
|
items: { type: "object", additionalProperties: true },
|
|
@@ -622,8 +888,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
622
888
|
type: "object",
|
|
623
889
|
properties: {
|
|
624
890
|
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
625
|
-
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?,
|
|
626
|
-
|
|
891
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, submissionHandle?, details? }" },
|
|
892
|
+
submissionHandle: SUBMISSION_HANDLE_SCHEMA,
|
|
627
893
|
gateId: { type: "string" },
|
|
628
894
|
risk: { type: "string", enum: ["low", "high", "unknown"] },
|
|
629
895
|
decision: { type: "string", enum: ["agent_allowed", "user_required"] },
|
|
@@ -639,7 +905,7 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
639
905
|
type: "object",
|
|
640
906
|
properties: {
|
|
641
907
|
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
642
|
-
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?,
|
|
908
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, submissionHandle?, details? }" },
|
|
643
909
|
sessionId: { type: "string" },
|
|
644
910
|
loopId: { type: "string" },
|
|
645
911
|
gateId: { type: "string" },
|
|
@@ -664,12 +930,6 @@ function closeObjectSchemas(schema) {
|
|
|
664
930
|
}
|
|
665
931
|
return result;
|
|
666
932
|
}
|
|
667
|
-
const CORE_EVALUATION_FIELDS = new Set([
|
|
668
|
-
"success",
|
|
669
|
-
"output_summary",
|
|
670
|
-
"constraint_violations",
|
|
671
|
-
"should_continue",
|
|
672
|
-
]);
|
|
673
933
|
/** Keep the four authoritative fields schema-strict while leaving optional
|
|
674
934
|
* reporting fields available for buildSelfEvaluation's lossy normalization.
|
|
675
935
|
* Their descriptions remain in tools/list, but malformed optional values do
|
|
@@ -682,7 +942,7 @@ function prepareInputSchema(name, schema) {
|
|
|
682
942
|
if (!isRecord(evaluation) || !isRecord(evaluation.properties))
|
|
683
943
|
return result;
|
|
684
944
|
evaluation.properties = Object.fromEntries(Object.entries(evaluation.properties).map(([field, value]) => {
|
|
685
|
-
if (
|
|
945
|
+
if (CORE_SELF_EVALUATION_FIELDS.has(field) || !isRecord(value))
|
|
686
946
|
return [field, value];
|
|
687
947
|
return [field, typeof value.description === "string"
|
|
688
948
|
? { description: value.description }
|
|
@@ -805,7 +1065,7 @@ export function validateToolDispatchInput(name, input) {
|
|
|
805
1065
|
: {};
|
|
806
1066
|
validateSchema(input, {
|
|
807
1067
|
...contract.inputSchema,
|
|
808
|
-
required: ["
|
|
1068
|
+
required: ["submissionHandle"],
|
|
809
1069
|
properties: { ...properties, evaluation: {} },
|
|
810
1070
|
}, "arguments");
|
|
811
1071
|
if (!isRecord(input.evaluation))
|
|
@@ -835,18 +1095,76 @@ function capabilityWarningList(existing) {
|
|
|
835
1095
|
...deriveEvidenceCapability(getPolicy()).warnings,
|
|
836
1096
|
])];
|
|
837
1097
|
}
|
|
1098
|
+
/** v3.9.4: the stable envelope for a prompt-projection code defect. */
|
|
1099
|
+
function projectionErrorResult(error) {
|
|
1100
|
+
if (error instanceof ProjectionCoverageError) {
|
|
1101
|
+
return {
|
|
1102
|
+
error: "projection_incomplete",
|
|
1103
|
+
errorMessage: error.message,
|
|
1104
|
+
details: { coverage: error.coverage },
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
/** Stable state-integrity errors. They are never retryable with the same
|
|
1110
|
+
* payload: the loop data or the operator surface must be repaired first. */
|
|
1111
|
+
function stateErrorResult(error) {
|
|
1112
|
+
if (error instanceof LegacyHistoryError) {
|
|
1113
|
+
return {
|
|
1114
|
+
error: error.code,
|
|
1115
|
+
errorMessage: error.message,
|
|
1116
|
+
details: { loopId: error.loopId, rounds: error.rounds },
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
if (error instanceof OperatorAcceptanceConflictError) {
|
|
1120
|
+
return {
|
|
1121
|
+
error: error.code,
|
|
1122
|
+
errorMessage: error.message,
|
|
1123
|
+
details: { loopId: error.loopId },
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
838
1128
|
export const TOOL_HANDLERS = {
|
|
839
1129
|
async loopforge_start(mgr, input) {
|
|
1130
|
+
// A removed or misspelled start argument must not disappear silently: the
|
|
1131
|
+
// schema is permissive about extra top-level keys, and the handler used to
|
|
1132
|
+
// rebuild StartInput from a whitelist. Without this guard a stale manual
|
|
1133
|
+
// (for example one still naming `domain`) produces a successful start with
|
|
1134
|
+
// part of the caller's intent ignored.
|
|
1135
|
+
const unknownStartArgs = Object.keys(input).filter((key) => !START_INPUT_KEYS.includes(key));
|
|
1136
|
+
if (unknownStartArgs.length > 0) {
|
|
1137
|
+
const retired = unknownStartArgs.includes("domain")
|
|
1138
|
+
? " `domain` was removed in 4.0.1."
|
|
1139
|
+
: "";
|
|
1140
|
+
return {
|
|
1141
|
+
error: "invalid_argument",
|
|
1142
|
+
errorMessage: `unknown loopforge_start argument(s): ${unknownStartArgs.join(", ")}.` +
|
|
1143
|
+
`${retired} Accepted: ${START_INPUT_KEYS.join(", ")}.`,
|
|
1144
|
+
details: {
|
|
1145
|
+
unknown: unknownStartArgs,
|
|
1146
|
+
accepted: [...START_INPUT_KEYS],
|
|
1147
|
+
},
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
840
1150
|
const startInput = {
|
|
841
1151
|
task: String(input.task ?? ""),
|
|
842
1152
|
loopId: input.loopId,
|
|
843
1153
|
maxRounds: typeof input.maxRounds === "number" ? input.maxRounds : undefined,
|
|
844
|
-
domain: input.domain,
|
|
845
1154
|
planSource: input.planSource,
|
|
846
1155
|
constraints: Array.isArray(input.constraints)
|
|
847
1156
|
? input.constraints
|
|
848
1157
|
: undefined,
|
|
1158
|
+
criteria: Array.isArray(input.criteria)
|
|
1159
|
+
? input.criteria
|
|
1160
|
+
: undefined,
|
|
849
1161
|
};
|
|
1162
|
+
if (input.criteria !== undefined && !Array.isArray(input.criteria)) {
|
|
1163
|
+
return {
|
|
1164
|
+
error: "invalid_argument",
|
|
1165
|
+
errorMessage: "criteria must be an array of non-empty strings",
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
850
1168
|
if (!startInput.task.trim()) {
|
|
851
1169
|
return {
|
|
852
1170
|
error: "invalid_argument",
|
|
@@ -882,20 +1200,43 @@ export const TOOL_HANDLERS = {
|
|
|
882
1200
|
result = await mgr.create(startInput);
|
|
883
1201
|
}
|
|
884
1202
|
catch (error) {
|
|
1203
|
+
if (error instanceof StartCriteriaError) {
|
|
1204
|
+
return {
|
|
1205
|
+
error: "invalid_argument",
|
|
1206
|
+
errorMessage: error.message,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
const projectionFailure = projectionErrorResult(error);
|
|
1210
|
+
if (projectionFailure)
|
|
1211
|
+
return projectionFailure;
|
|
1212
|
+
const stateFailure = stateErrorResult(error);
|
|
1213
|
+
if (stateFailure)
|
|
1214
|
+
return stateFailure;
|
|
1215
|
+
if (error instanceof LoopExistsError) {
|
|
1216
|
+
return {
|
|
1217
|
+
error: error.code,
|
|
1218
|
+
errorMessage: error.message,
|
|
1219
|
+
details: { loopId: error.loopId, reason: error.reason },
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
if (error instanceof StartFailureError) {
|
|
1223
|
+
return {
|
|
1224
|
+
error: error.code,
|
|
1225
|
+
errorMessage: error.message,
|
|
1226
|
+
details: { retry: "call loopforge_start again" },
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
if (error instanceof SessionLeaseConflictError) {
|
|
1230
|
+
return {
|
|
1231
|
+
error: "lease_conflict",
|
|
1232
|
+
errorMessage: `loop "${error.loopId}" is owned by another running LoopForge process`,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
885
1235
|
return {
|
|
886
1236
|
error: "policy_invalid",
|
|
887
1237
|
errorMessage: `the runtime policy could not be loaded: ${error.message}`,
|
|
888
1238
|
};
|
|
889
1239
|
}
|
|
890
|
-
// v2.14: a same-process duplicate loopId surfaces as a clean argument
|
|
891
|
-
// error instead of a prompt-less "success" the client cannot act on.
|
|
892
|
-
if (typeof result.stopReason === "string" &&
|
|
893
|
-
result.stopReason.startsWith("loop_already_running:")) {
|
|
894
|
-
return {
|
|
895
|
-
error: "loop_already_running",
|
|
896
|
-
errorMessage: result.stopDetail ?? "loop already running",
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
1240
|
// v3.8: the static capability warning — a loop with no provider or no
|
|
900
1241
|
// enabled command can still run; its success claims are recorded as
|
|
901
1242
|
// insufficient instead of verified, and the agent is told so up front.
|
|
@@ -905,48 +1246,86 @@ export const TOOL_HANDLERS = {
|
|
|
905
1246
|
: { ...result };
|
|
906
1247
|
},
|
|
907
1248
|
async loopforge_next(mgr, input) {
|
|
908
|
-
const sessionId = String(input.sessionId ?? "");
|
|
909
1249
|
const output = String(input.output ?? "");
|
|
910
1250
|
const rawEval = input.evaluation;
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
// v3.0.1: every submission is anchored to the round it reports on. A
|
|
916
|
-
// missing roundId would silently re-process a stale submission against a
|
|
917
|
-
// later round — reject it here so the contract is enforced even when the
|
|
918
|
-
// handler is invoked directly (bypassing schema validation).
|
|
919
|
-
if (!roundId || !roundId.trim()) {
|
|
1251
|
+
const rawRecovery = input.recovery;
|
|
1252
|
+
const handle = input.submissionHandle;
|
|
1253
|
+
const rawInstruction = input.currentUserInstruction;
|
|
1254
|
+
if (rawRecovery !== undefined && rawEval !== undefined) {
|
|
920
1255
|
return {
|
|
921
|
-
error: "
|
|
922
|
-
errorMessage: "
|
|
923
|
-
"loopforge_start / loopforge_next / loopforge_resume / loopforge_status response",
|
|
1256
|
+
error: "recovery_invalid",
|
|
1257
|
+
errorMessage: "evaluation and recovery are mutually exclusive",
|
|
924
1258
|
};
|
|
925
1259
|
}
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
|
|
1260
|
+
// v4.0.0: the handle is the only identity. The runtime validates its
|
|
1261
|
+
// shape, resolves the session, checks the exact held attempt, then runs
|
|
1262
|
+
// the payload boundary — all before its queue/lease heartbeat. This
|
|
1263
|
+
// handler only maps the refusal onto the tool envelope.
|
|
1264
|
+
// v4.0.5: handle + instruction alone is the instruction-recompile mode:
|
|
1265
|
+
// no submission and no round/attempt advance.
|
|
1266
|
+
let result;
|
|
1267
|
+
try {
|
|
1268
|
+
if (rawRecovery !== undefined) {
|
|
1269
|
+
result = await mgr.recover(handle, rawRecovery, rawInstruction !== undefined
|
|
1270
|
+
? { currentUserInstruction: String(rawInstruction) }
|
|
1271
|
+
: undefined);
|
|
1272
|
+
}
|
|
1273
|
+
else if (rawEval !== undefined) {
|
|
1274
|
+
result = await mgr.advance(handle, output, rawEval, rawInstruction !== undefined
|
|
1275
|
+
? { currentUserInstruction: String(rawInstruction) }
|
|
1276
|
+
: undefined);
|
|
1277
|
+
}
|
|
1278
|
+
else if (rawInstruction !== undefined) {
|
|
1279
|
+
result = await mgr.recompileHeld(handle, String(rawInstruction));
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
result = await mgr.advance(handle, output, rawEval);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
catch (error) {
|
|
1286
|
+
if (error instanceof InvocationInputError) {
|
|
1287
|
+
return { error: "invalid_argument", errorMessage: error.message };
|
|
1288
|
+
}
|
|
1289
|
+
const projectionFailure = projectionErrorResult(error);
|
|
1290
|
+
if (projectionFailure)
|
|
1291
|
+
return projectionFailure;
|
|
1292
|
+
const stateFailure = stateErrorResult(error);
|
|
1293
|
+
if (stateFailure)
|
|
1294
|
+
return stateFailure;
|
|
1295
|
+
throw error;
|
|
1296
|
+
}
|
|
1297
|
+
const loopId = result.sessionId
|
|
1298
|
+
? mgr.get(result.sessionId)?.loopId
|
|
1299
|
+
: undefined;
|
|
933
1300
|
if (result.submissionError) {
|
|
934
1301
|
return {
|
|
935
1302
|
error: result.submissionError.code,
|
|
936
|
-
errorMessage: result.stopDetail ??
|
|
1303
|
+
errorMessage: result.stopDetail ??
|
|
1304
|
+
(rawRecovery !== undefined
|
|
1305
|
+
? "the recovery request was rejected"
|
|
1306
|
+
: "the submission was rejected"),
|
|
937
1307
|
details: result.submissionError.details,
|
|
938
|
-
sessionId,
|
|
939
|
-
|
|
1308
|
+
...(result.sessionId ? { sessionId: result.sessionId } : {}),
|
|
1309
|
+
...(loopId ? { loopId } : {}),
|
|
1310
|
+
...(result.submissionHandle ? { submissionHandle: result.submissionHandle } : {}),
|
|
940
1311
|
};
|
|
941
1312
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1313
|
+
return { ...result, ...(loopId ? { loopId } : {}) };
|
|
1314
|
+
},
|
|
1315
|
+
async loopforge_checkpoint(mgr, input) {
|
|
1316
|
+
const result = await mgr.checkpoint(input.submissionHandle);
|
|
1317
|
+
if (result.error) {
|
|
1318
|
+
return {
|
|
1319
|
+
error: String(result.error),
|
|
1320
|
+
errorMessage: String(result.errorMessage ?? "the checkpoint was rejected"),
|
|
1321
|
+
details: result.details,
|
|
1322
|
+
...(typeof result.sessionId === "string" ? { sessionId: result.sessionId } : {}),
|
|
1323
|
+
...(isRecord(result.submissionHandle)
|
|
1324
|
+
? { submissionHandle: result.submissionHandle }
|
|
1325
|
+
: {}),
|
|
1326
|
+
};
|
|
948
1327
|
}
|
|
949
|
-
return
|
|
1328
|
+
return { ok: true, ...result };
|
|
950
1329
|
},
|
|
951
1330
|
async loopforge_status(mgr, input) {
|
|
952
1331
|
const view = input.view === "loop" || input.view === "all" ||
|
|
@@ -996,52 +1375,125 @@ export const TOOL_HANDLERS = {
|
|
|
996
1375
|
errorMessage: `no health data for loop "${loopId}"`,
|
|
997
1376
|
};
|
|
998
1377
|
}
|
|
999
|
-
if (!sessionId) {
|
|
1378
|
+
if (!sessionId && !loopId) {
|
|
1000
1379
|
return {
|
|
1001
1380
|
error: "invalid_argument",
|
|
1002
|
-
errorMessage: "sessionId is required for view=session",
|
|
1381
|
+
errorMessage: "sessionId or loopId is required for view=session",
|
|
1003
1382
|
};
|
|
1004
1383
|
}
|
|
1005
|
-
|
|
1384
|
+
// sessionId is a live-process key; loopId is the durable identity. Prefer
|
|
1385
|
+
// a live sessionId, then fall back to loopId so a model that kept only the
|
|
1386
|
+
// durable key can still read session state in the same process. A stale
|
|
1387
|
+
// sessionId after a restart resolves through loopId when both are present.
|
|
1388
|
+
const session = (sessionId ? mgr.get(sessionId) : undefined) ??
|
|
1389
|
+
(loopId ? mgr.getByLoopId(loopId) : undefined);
|
|
1006
1390
|
if (!session) {
|
|
1391
|
+
// v4.0.4: a persisted STOPPED loop has no live session, but its
|
|
1392
|
+
// committed terminal facts are still readable. This is a projection of
|
|
1393
|
+
// committed history — no lease, no session save, no resume side effect.
|
|
1394
|
+
// A running/paused/stalled loop with no live session still goes through
|
|
1395
|
+
// resume; "no live session" is never treated as "stopped".
|
|
1396
|
+
if (loopId) {
|
|
1397
|
+
let stopped = null;
|
|
1398
|
+
try {
|
|
1399
|
+
stopped = mgr.getStoppedLoopSummary(loopId);
|
|
1400
|
+
}
|
|
1401
|
+
catch (error) {
|
|
1402
|
+
const stateFailure = stateErrorResult(error);
|
|
1403
|
+
if (stateFailure)
|
|
1404
|
+
return stateFailure;
|
|
1405
|
+
throw error;
|
|
1406
|
+
}
|
|
1407
|
+
if (stopped) {
|
|
1408
|
+
return {
|
|
1409
|
+
ok: true,
|
|
1410
|
+
loopId,
|
|
1411
|
+
round: stopped.round,
|
|
1412
|
+
status: stopped.sessionStatus ?? "stopped",
|
|
1413
|
+
loopStopReason: stopped.stopReason ?? null,
|
|
1414
|
+
agentSuccessClaim: stopped.agentSuccessClaim ?? null,
|
|
1415
|
+
agentOutcome: stopped.agentOutcome ?? null,
|
|
1416
|
+
agentShouldContinue: stopped.agentShouldContinue ?? null,
|
|
1417
|
+
stopDetail: stopped.stopDetail ?? null,
|
|
1418
|
+
last_submission: stopped.submission_receipt ?? null,
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1007
1422
|
return {
|
|
1008
1423
|
error: "session_not_found",
|
|
1009
|
-
errorMessage:
|
|
1010
|
-
|
|
1424
|
+
errorMessage: sessionId
|
|
1425
|
+
? `session not found: ${sessionId} (process restarted?) — call ` +
|
|
1426
|
+
`loopforge_status with view="all" to find the loopId; if the ` +
|
|
1427
|
+
`held prompt is still in context, call loopforge_status with ` +
|
|
1428
|
+
`that loopId to fetch the current handle; otherwise call ` +
|
|
1429
|
+
`loopforge_resume with it`
|
|
1430
|
+
: `no live session for loop "${loopId}" — call loopforge_resume with this loopId`,
|
|
1431
|
+
...(sessionId ? { sessionId } : {}),
|
|
1432
|
+
...(loopId ? { loopId } : {}),
|
|
1433
|
+
details: {
|
|
1434
|
+
next: "loopforge_resume",
|
|
1435
|
+
...(loopId ? { loopId } : {}),
|
|
1436
|
+
},
|
|
1011
1437
|
};
|
|
1012
1438
|
}
|
|
1013
|
-
|
|
1439
|
+
// Every live-session-derived projection must use the resolved session's
|
|
1440
|
+
// identity. The caller's raw sessionId is empty on a loopId-only query and
|
|
1441
|
+
// may be stale when the loopId fallback resolved the session; using it here
|
|
1442
|
+
// silently nulled the handle/contract/guidance and forced a full resume.
|
|
1443
|
+
const liveSessionId = session.sessionId;
|
|
1444
|
+
const policyMetricsView = mgr.getPolicyMetrics(session.loopId);
|
|
1445
|
+
// v4.0.3: a live held prompt has no result yet; the last committed
|
|
1446
|
+
// receipt is the correct current machine posture then.
|
|
1447
|
+
const lastReceipt = mgr.getLastSubmissionReceiptByLoopId(session.loopId);
|
|
1448
|
+
const posture = session.roundSnapshot?.result?.evidencePosture ??
|
|
1449
|
+
lastReceipt?.verification.evidence_posture ??
|
|
1450
|
+
null;
|
|
1014
1451
|
return {
|
|
1015
|
-
sessionId:
|
|
1452
|
+
sessionId: liveSessionId,
|
|
1016
1453
|
loopId: session.loopId,
|
|
1017
1454
|
round: session.currentRound,
|
|
1018
|
-
|
|
1455
|
+
submissionHandle: mgr.getSubmissionHandle(liveSessionId),
|
|
1456
|
+
checkpoints: session.roundSnapshot
|
|
1457
|
+
? [
|
|
1458
|
+
...(session.roundSnapshot.beforeCheckpoint
|
|
1459
|
+
? [session.roundSnapshot.beforeCheckpoint]
|
|
1460
|
+
: []),
|
|
1461
|
+
...(session.roundSnapshot.checkpoints ?? []),
|
|
1462
|
+
]
|
|
1463
|
+
: [],
|
|
1464
|
+
beforeCheckpointError: session.roundSnapshot?.beforeCheckpointError ?? null,
|
|
1019
1465
|
maxRounds: session.maxRounds,
|
|
1020
1466
|
status: session.status,
|
|
1021
|
-
|
|
1467
|
+
evidencePosture: posture,
|
|
1468
|
+
// v4.0.4: no committed/held posture is null (unknown), not false.
|
|
1469
|
+
machineBackedSuccess: posture?.machineBackedSuccess ?? null,
|
|
1470
|
+
agentSuccessClaim: session.roundSnapshot?.result?.agentSuccessClaim ??
|
|
1471
|
+
lastReceipt?.agent_success_claim ?? null,
|
|
1472
|
+
agentOutcome: session.roundSnapshot?.result?.agentOutcome ??
|
|
1473
|
+
lastReceipt?.agent_outcome ?? null,
|
|
1474
|
+
machineBackedSuccessTrajectory: mgr.getMachineBackedSuccessTrajectory(session.loopId),
|
|
1022
1475
|
lease: mgr.getLeaseStatus(session.loopId),
|
|
1023
1476
|
// v3.5: derived ACTIVE Round Contract (display-only — the compile and
|
|
1024
1477
|
// verification gates derive the same value from the same committed
|
|
1025
1478
|
// evals via the shared round-contract walker).
|
|
1026
|
-
activeContract: mgr.getActiveContract(
|
|
1479
|
+
activeContract: mgr.getActiveContract(liveSessionId),
|
|
1027
1480
|
// v3.9.1: current-fact submission guidance for the held round. Same
|
|
1028
1481
|
// derivation as start/next/resume; read-only and never persisted.
|
|
1029
|
-
submission_guidance: mgr.getSubmissionGuidance(
|
|
1030
|
-
last_submission:
|
|
1482
|
+
submission_guidance: mgr.getSubmissionGuidance(liveSessionId),
|
|
1483
|
+
last_submission: lastReceipt,
|
|
1484
|
+
last_rollback: mgr.replayRollbacks(session.loopId).at(-1) ?? null,
|
|
1031
1485
|
// v3.8: the ONE capability fact — the static half the compile hashes
|
|
1032
1486
|
// (what verification is possible) plus the live half rendered from the
|
|
1033
1487
|
// last baseline. `available` is provider-registry state: reported here
|
|
1034
1488
|
// and by `doctor`, never hashed.
|
|
1035
|
-
capability: deriveEvidenceCapability(getPolicy(), session.
|
|
1036
|
-
projection: mgr.getProjection(sessionId),
|
|
1489
|
+
capability: deriveEvidenceCapability(getPolicy(), session.roundSnapshot?.beforeEvidence ?? []),
|
|
1037
1490
|
metrics: {
|
|
1038
|
-
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
policy: mgr.getPolicyMetrics(session.loopId),
|
|
1491
|
+
// The vault write counters live in the shared policy metrics now;
|
|
1492
|
+
// the engine no longer owns a second copy.
|
|
1493
|
+
vaultWriteErrors: policyMetricsView.vaultWriteErrors,
|
|
1494
|
+
startedAt: session.createdAt,
|
|
1495
|
+
sessionAgeSeconds: Math.max(0, Math.floor((Date.now() - session.createdAt) / 1000)),
|
|
1496
|
+
policy: policyMetricsView,
|
|
1045
1497
|
},
|
|
1046
1498
|
};
|
|
1047
1499
|
},
|
|
@@ -1058,10 +1510,27 @@ export const TOOL_HANDLERS = {
|
|
|
1058
1510
|
sessionId,
|
|
1059
1511
|
};
|
|
1060
1512
|
}
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1513
|
+
const loopId = session.loopId;
|
|
1514
|
+
const roundCursor = session.currentRound;
|
|
1515
|
+
const roundsCompleted = roundCursor;
|
|
1516
|
+
const machineBackedSuccessTrajectory = mgr.getMachineBackedSuccessTrajectory(loopId);
|
|
1063
1517
|
mgr.delete(sessionId);
|
|
1064
|
-
|
|
1518
|
+
// v4.0.4: make the stop response self-describing. `cancelled` is the
|
|
1519
|
+
// terminal reason the terminal sink just received; it is NOT persisted as
|
|
1520
|
+
// a committed round fact, so resume/status still report "no committed
|
|
1521
|
+
// terminal reason" after a restart. `success` remains tool-call success.
|
|
1522
|
+
return {
|
|
1523
|
+
ok: true,
|
|
1524
|
+
success: true,
|
|
1525
|
+
sessionId,
|
|
1526
|
+
loopId,
|
|
1527
|
+
roundsCompleted,
|
|
1528
|
+
roundCursor,
|
|
1529
|
+
sessionStatus: "stopped",
|
|
1530
|
+
loopStopReason: MANUAL_STOP_REASON,
|
|
1531
|
+
machineBackedSuccessRounds: machineBackedSuccessTrajectory.length,
|
|
1532
|
+
machineBackedSuccessTrajectory,
|
|
1533
|
+
};
|
|
1065
1534
|
},
|
|
1066
1535
|
async loopforge_pause(mgr, input) {
|
|
1067
1536
|
const sessionId = String(input.sessionId ?? "");
|
|
@@ -1081,16 +1550,46 @@ export const TOOL_HANDLERS = {
|
|
|
1081
1550
|
"after a restart (no live session)",
|
|
1082
1551
|
};
|
|
1083
1552
|
}
|
|
1553
|
+
// v4.0.2: an explicit window is validated at the tool boundary. The
|
|
1554
|
+
// backend owns the rule; the boundary only converts the type.
|
|
1555
|
+
const windowOptions = {};
|
|
1556
|
+
for (const name of ["start", "end"]) {
|
|
1557
|
+
const value = input[name];
|
|
1558
|
+
if (value === undefined)
|
|
1559
|
+
continue;
|
|
1560
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
1561
|
+
return {
|
|
1562
|
+
error: "invalid_argument",
|
|
1563
|
+
errorMessage: `${name} must be a positive integer`,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
windowOptions[name] = value;
|
|
1567
|
+
}
|
|
1568
|
+
const pageFor = (resolvePage) => {
|
|
1569
|
+
try {
|
|
1570
|
+
return resolvePage();
|
|
1571
|
+
}
|
|
1572
|
+
catch (error) {
|
|
1573
|
+
if (error instanceof ReplayWindowError) {
|
|
1574
|
+
return { error: "invalid_argument", errorMessage: error.message };
|
|
1575
|
+
}
|
|
1576
|
+
throw error;
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1084
1579
|
// Live session first: it disambiguates re-used loopIds and the session
|
|
1085
1580
|
// registry is the cheapest lookup.
|
|
1086
1581
|
if (sessionId) {
|
|
1087
1582
|
const session = mgr.get(sessionId);
|
|
1088
1583
|
if (session) {
|
|
1089
|
-
const
|
|
1584
|
+
const page = pageFor(() => mgr.replayPage(sessionId, windowOptions));
|
|
1585
|
+
if ("error" in page)
|
|
1586
|
+
return page;
|
|
1090
1587
|
return {
|
|
1091
1588
|
sessionId,
|
|
1092
1589
|
loopId: session.loopId,
|
|
1093
|
-
timeline,
|
|
1590
|
+
timeline: page.timeline,
|
|
1591
|
+
window: page.window,
|
|
1592
|
+
rollbacks: mgr.replayRollbacks(session.loopId),
|
|
1094
1593
|
legacyRounds: mgr.replayLegacyRounds(session.loopId),
|
|
1095
1594
|
};
|
|
1096
1595
|
}
|
|
@@ -1107,8 +1606,10 @@ export const TOOL_HANDLERS = {
|
|
|
1107
1606
|
}
|
|
1108
1607
|
// Vault-direct replay — the timeline lives in the committed round
|
|
1109
1608
|
// documents, not in the session registry.
|
|
1110
|
-
const
|
|
1111
|
-
if (
|
|
1609
|
+
const page = pageFor(() => mgr.replayPageByLoop(loopId, windowOptions));
|
|
1610
|
+
if ("error" in page)
|
|
1611
|
+
return page;
|
|
1612
|
+
if (page.window.totalRounds === 0 && page.timeline.length === 0) {
|
|
1112
1613
|
return {
|
|
1113
1614
|
error: "state_unavailable",
|
|
1114
1615
|
errorMessage: `no committed rounds found for loop "${loopId}"`,
|
|
@@ -1118,7 +1619,9 @@ export const TOOL_HANDLERS = {
|
|
|
1118
1619
|
return {
|
|
1119
1620
|
...(sessionId ? { sessionId } : {}),
|
|
1120
1621
|
loopId,
|
|
1121
|
-
timeline,
|
|
1622
|
+
timeline: page.timeline,
|
|
1623
|
+
window: page.window,
|
|
1624
|
+
rollbacks: mgr.replayRollbacks(loopId),
|
|
1122
1625
|
legacyRounds: mgr.replayLegacyRounds(loopId),
|
|
1123
1626
|
};
|
|
1124
1627
|
},
|
|
@@ -1136,39 +1639,57 @@ export const TOOL_HANDLERS = {
|
|
|
1136
1639
|
errorMessage: `invalid loopId: ${error.message}`,
|
|
1137
1640
|
};
|
|
1138
1641
|
}
|
|
1642
|
+
const inputs = {
|
|
1643
|
+
...(input.invocationLost === true ? { invocationLost: true } : {}),
|
|
1644
|
+
...(input.currentUserInstruction !== undefined
|
|
1645
|
+
? { currentUserInstruction: String(input.currentUserInstruction) }
|
|
1646
|
+
: {}),
|
|
1647
|
+
};
|
|
1139
1648
|
// Paused recovery must run first so the persisted status is atomically
|
|
1140
1649
|
// changed back to running before a prompt is returned.
|
|
1141
|
-
let result
|
|
1142
|
-
|
|
1143
|
-
result = await mgr.
|
|
1650
|
+
let result;
|
|
1651
|
+
try {
|
|
1652
|
+
result = await mgr.unpause(loopId, inputs);
|
|
1653
|
+
if (!result) {
|
|
1654
|
+
result = await mgr.resume(loopId, inputs);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
catch (error) {
|
|
1658
|
+
if (error instanceof InvocationInputError) {
|
|
1659
|
+
return { error: "invalid_argument", errorMessage: error.message };
|
|
1660
|
+
}
|
|
1661
|
+
if (error instanceof SessionLeaseConflictError) {
|
|
1662
|
+
return {
|
|
1663
|
+
error: "lease_conflict",
|
|
1664
|
+
errorMessage: `loop "${error.loopId}" is owned by another running LoopForge process`,
|
|
1665
|
+
details: { reason_code: "lease_conflict" },
|
|
1666
|
+
loopId,
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
const projectionFailure = projectionErrorResult(error);
|
|
1670
|
+
if (projectionFailure)
|
|
1671
|
+
return projectionFailure;
|
|
1672
|
+
const stateFailure = stateErrorResult(error);
|
|
1673
|
+
if (stateFailure)
|
|
1674
|
+
return stateFailure;
|
|
1675
|
+
throw error;
|
|
1144
1676
|
}
|
|
1145
1677
|
if (!result) {
|
|
1146
1678
|
return {
|
|
1147
1679
|
error: "state_unavailable",
|
|
1148
1680
|
errorMessage: `no saved session found for loop "${loopId}"`,
|
|
1681
|
+
loopId,
|
|
1149
1682
|
};
|
|
1150
1683
|
}
|
|
1151
|
-
return { ...result };
|
|
1684
|
+
return { ...result, loopId };
|
|
1152
1685
|
},
|
|
1153
1686
|
async loopforge_gate_check(mgr, input) {
|
|
1154
1687
|
// v3.7.1: hidden when disabled — direct calls get a stable error.
|
|
1155
1688
|
if (!getPolicy().gate.enabled)
|
|
1156
1689
|
return { error: "gate_disabled" };
|
|
1157
|
-
const sessionId = String(input.sessionId ?? "");
|
|
1158
|
-
const roundId = String(input.roundId ?? "");
|
|
1159
1690
|
const action = input.action;
|
|
1160
|
-
if (!sessionId) {
|
|
1161
|
-
return { error: "invalid_argument", errorMessage: "sessionId is required" };
|
|
1162
|
-
}
|
|
1163
|
-
if (!roundId) {
|
|
1164
|
-
return {
|
|
1165
|
-
error: "round_id_required",
|
|
1166
|
-
errorMessage: "roundId is required — pass the roundId from the latest response",
|
|
1167
|
-
sessionId,
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
1691
|
if (!action || !isRecord(action)) {
|
|
1171
|
-
return { error: "invalid_argument", errorMessage: "action is required"
|
|
1692
|
+
return { error: "invalid_argument", errorMessage: "action is required" };
|
|
1172
1693
|
}
|
|
1173
1694
|
const descriptor = {
|
|
1174
1695
|
description: String(action.description ?? ""),
|
|
@@ -1192,11 +1713,9 @@ export const TOOL_HANDLERS = {
|
|
|
1192
1713
|
return {
|
|
1193
1714
|
error: "invalid_argument",
|
|
1194
1715
|
errorMessage: "action.description is required",
|
|
1195
|
-
sessionId,
|
|
1196
|
-
roundId,
|
|
1197
1716
|
};
|
|
1198
1717
|
}
|
|
1199
|
-
return mgr.checkGate(
|
|
1718
|
+
return mgr.checkGate(input.submissionHandle, descriptor);
|
|
1200
1719
|
},
|
|
1201
1720
|
async loopforge_gate_resolve(mgr, input) {
|
|
1202
1721
|
// v3.7.1: hidden when disabled — direct calls get a stable error.
|