@orion-agents/orion-code 0.1.0 → 0.1.2
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 +107 -67
- package/README.zh-CN.md +192 -217
- package/dist/cli.js +23 -21
- package/dist/cli.js.map +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +397 -212
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/target-command.d.ts.map +1 -1
- package/dist/commands/target-command.js +43 -9
- package/dist/commands/target-command.js.map +1 -1
- package/dist/commands/types.d.ts +33 -0
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js +5 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +2 -2
- package/dist/core/agent.js.map +1 -1
- package/dist/framework/external-assertion.d.ts +65 -0
- package/dist/framework/external-assertion.d.ts.map +1 -0
- package/dist/framework/external-assertion.js +571 -0
- package/dist/framework/external-assertion.js.map +1 -0
- package/dist/framework/index.d.ts +6 -4
- package/dist/framework/index.d.ts.map +1 -1
- package/dist/framework/index.js +4 -1
- package/dist/framework/index.js.map +1 -1
- package/dist/framework/query.d.ts +18 -1
- package/dist/framework/query.d.ts.map +1 -1
- package/dist/framework/query.js +65 -28
- package/dist/framework/query.js.map +1 -1
- package/dist/framework/tool-scheduler.d.ts +2 -0
- package/dist/framework/tool-scheduler.d.ts.map +1 -1
- package/dist/framework/tool-scheduler.js +23 -9
- package/dist/framework/tool-scheduler.js.map +1 -1
- package/dist/framework/tool-serializer.d.ts.map +1 -1
- package/dist/framework/tool-serializer.js +20 -4
- package/dist/framework/tool-serializer.js.map +1 -1
- package/dist/framework/tool.d.ts +11 -5
- package/dist/framework/tool.d.ts.map +1 -1
- package/dist/framework/tool.js.map +1 -1
- package/dist/harness/context-harness.d.ts.map +1 -1
- package/dist/harness/context-harness.js +4 -3
- package/dist/harness/context-harness.js.map +1 -1
- package/dist/harness/drift-guard.d.ts.map +1 -1
- package/dist/harness/drift-guard.js +14 -5
- package/dist/harness/drift-guard.js.map +1 -1
- package/dist/harness/safety.d.ts.map +1 -1
- package/dist/harness/safety.js +19 -3
- package/dist/harness/safety.js.map +1 -1
- package/dist/init.d.ts +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +115 -34
- package/dist/init.js.map +1 -1
- package/dist/ink-ui/screens/ReplScreen.d.ts.map +1 -1
- package/dist/ink-ui/screens/ReplScreen.js +4 -1
- package/dist/ink-ui/screens/ReplScreen.js.map +1 -1
- package/dist/memory/storage.d.ts.map +1 -1
- package/dist/memory/storage.js +1 -0
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +11 -5
- package/dist/memory/store.js.map +1 -1
- package/dist/migration/command.d.ts.map +1 -1
- package/dist/migration/command.js +6 -1
- package/dist/migration/command.js.map +1 -1
- package/dist/migration/migrate.d.ts.map +1 -1
- package/dist/migration/migrate.js +4 -1
- package/dist/migration/migrate.js.map +1 -1
- package/dist/print-ui/launch.d.ts +4 -0
- package/dist/print-ui/launch.d.ts.map +1 -1
- package/dist/print-ui/launch.js +102 -9
- package/dist/print-ui/launch.js.map +1 -1
- package/dist/product/identity.d.ts +2 -2
- package/dist/product/identity.d.ts.map +1 -1
- package/dist/product/identity.js +2 -2
- package/dist/product/identity.js.map +1 -1
- package/dist/product/index.d.ts +2 -1
- package/dist/product/index.d.ts.map +1 -1
- package/dist/product/index.js +5 -1
- package/dist/product/index.js.map +1 -1
- package/dist/product/version.d.ts +12 -0
- package/dist/product/version.d.ts.map +1 -0
- package/dist/product/version.js +31 -0
- package/dist/product/version.js.map +1 -0
- package/dist/runtime/agent-runtime-controller.d.ts +49 -0
- package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-controller.js +1048 -81
- package/dist/runtime/agent-runtime-controller.js.map +1 -1
- package/dist/runtime/agent-runtime-protocol.d.ts +14 -2
- package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-protocol.js +18 -0
- package/dist/runtime/agent-runtime-protocol.js.map +1 -1
- package/dist/runtime/chat-controller.d.ts +6 -0
- package/dist/runtime/chat-controller.d.ts.map +1 -1
- package/dist/runtime/chat-controller.js +226 -20
- package/dist/runtime/chat-controller.js.map +1 -1
- package/dist/runtime/goals/accounting.d.ts +28 -0
- package/dist/runtime/goals/accounting.d.ts.map +1 -1
- package/dist/runtime/goals/accounting.js +69 -0
- package/dist/runtime/goals/accounting.js.map +1 -1
- package/dist/runtime/goals/completion-audit.d.ts +13 -8
- package/dist/runtime/goals/completion-audit.d.ts.map +1 -1
- package/dist/runtime/goals/completion-audit.js +598 -16
- package/dist/runtime/goals/completion-audit.js.map +1 -1
- package/dist/runtime/goals/coordinator.d.ts +44 -5
- package/dist/runtime/goals/coordinator.d.ts.map +1 -1
- package/dist/runtime/goals/coordinator.js +1124 -58
- package/dist/runtime/goals/coordinator.js.map +1 -1
- package/dist/runtime/goals/evidence.d.ts +37 -0
- package/dist/runtime/goals/evidence.d.ts.map +1 -0
- package/dist/runtime/goals/evidence.js +300 -0
- package/dist/runtime/goals/evidence.js.map +1 -0
- package/dist/runtime/goals/presentation.d.ts +4 -0
- package/dist/runtime/goals/presentation.d.ts.map +1 -0
- package/dist/runtime/goals/presentation.js +50 -0
- package/dist/runtime/goals/presentation.js.map +1 -0
- package/dist/runtime/goals/prompt.d.ts.map +1 -1
- package/dist/runtime/goals/prompt.js +38 -8
- package/dist/runtime/goals/prompt.js.map +1 -1
- package/dist/runtime/goals/tools.d.ts +18 -1
- package/dist/runtime/goals/tools.d.ts.map +1 -1
- package/dist/runtime/goals/tools.js +308 -28
- package/dist/runtime/goals/tools.js.map +1 -1
- package/dist/runtime/goals/types.d.ts +241 -7
- package/dist/runtime/goals/types.d.ts.map +1 -1
- package/dist/runtime/goals/types.js +8 -2
- package/dist/runtime/goals/types.js.map +1 -1
- package/dist/runtime/subagents/child-executor-guard.d.ts +12 -4
- package/dist/runtime/subagents/child-executor-guard.d.ts.map +1 -1
- package/dist/runtime/subagents/child-executor-guard.js +397 -61
- package/dist/runtime/subagents/child-executor-guard.js.map +1 -1
- package/dist/runtime/subagents/presets.d.ts +4 -5
- package/dist/runtime/subagents/presets.d.ts.map +1 -1
- package/dist/runtime/subagents/presets.js +18 -33
- package/dist/runtime/subagents/presets.js.map +1 -1
- package/dist/runtime/subagents/production.d.ts +3 -1
- package/dist/runtime/subagents/production.d.ts.map +1 -1
- package/dist/runtime/subagents/production.js +37 -7
- package/dist/runtime/subagents/production.js.map +1 -1
- package/dist/runtime/subagents/runner.d.ts.map +1 -1
- package/dist/runtime/subagents/runner.js +24 -3
- package/dist/runtime/subagents/runner.js.map +1 -1
- package/dist/runtime/subagents/runtime-integration.d.ts +3 -0
- package/dist/runtime/subagents/runtime-integration.d.ts.map +1 -1
- package/dist/runtime/subagents/runtime-integration.js +18 -7
- package/dist/runtime/subagents/runtime-integration.js.map +1 -1
- package/dist/runtime/subagents/supervisor.d.ts +4 -4
- package/dist/runtime/subagents/supervisor.d.ts.map +1 -1
- package/dist/runtime/subagents/supervisor.js +39 -19
- package/dist/runtime/subagents/supervisor.js.map +1 -1
- package/dist/runtime/subagents/types.d.ts +7 -0
- package/dist/runtime/subagents/types.d.ts.map +1 -1
- package/dist/runtime/subagents/types.js +12 -1
- package/dist/runtime/subagents/types.js.map +1 -1
- package/dist/runtime/ui-events.d.ts +10 -0
- package/dist/runtime/ui-events.d.ts.map +1 -1
- package/dist/runtime/ui-events.js +5 -5
- package/dist/runtime/ui-events.js.map +1 -1
- package/dist/runtime/ui-view-model.d.ts +3 -3
- package/dist/runtime/ui-view-model.d.ts.map +1 -1
- package/dist/runtime/ui-view-model.js +11 -4
- package/dist/runtime/ui-view-model.js.map +1 -1
- package/dist/services/config.d.ts +18 -7
- package/dist/services/config.d.ts.map +1 -1
- package/dist/services/config.js +79 -57
- package/dist/services/config.js.map +1 -1
- package/dist/services/doctor.d.ts.map +1 -1
- package/dist/services/doctor.js +164 -18
- package/dist/services/doctor.js.map +1 -1
- package/dist/services/global-config.d.ts +1 -1
- package/dist/services/global-config.d.ts.map +1 -1
- package/dist/services/goal-storage.d.ts +9 -4
- package/dist/services/goal-storage.d.ts.map +1 -1
- package/dist/services/goal-storage.js +1113 -75
- package/dist/services/goal-storage.js.map +1 -1
- package/dist/services/llm.d.ts +19 -0
- package/dist/services/llm.d.ts.map +1 -1
- package/dist/services/llm.js +153 -20
- package/dist/services/llm.js.map +1 -1
- package/dist/services/model-registry.d.ts +1 -1
- package/dist/services/model-registry.d.ts.map +1 -1
- package/dist/services/model-registry.js +69 -19
- package/dist/services/model-registry.js.map +1 -1
- package/dist/services/provider-resilience/coordinator.d.ts +2 -1
- package/dist/services/provider-resilience/coordinator.d.ts.map +1 -1
- package/dist/services/provider-resilience/coordinator.js +7 -6
- package/dist/services/provider-resilience/coordinator.js.map +1 -1
- package/dist/services/provider-resilience/error-classifier.d.ts.map +1 -1
- package/dist/services/provider-resilience/error-classifier.js +67 -17
- package/dist/services/provider-resilience/error-classifier.js.map +1 -1
- package/dist/services/provider-resilience/types.d.ts +1 -1
- package/dist/services/provider-resilience/types.d.ts.map +1 -1
- package/dist/services/provider-resilience/types.js.map +1 -1
- package/dist/services/redaction.d.ts.map +1 -1
- package/dist/services/redaction.js +9 -1
- package/dist/services/redaction.js.map +1 -1
- package/dist/services/session-storage.d.ts +12 -1
- package/dist/services/session-storage.d.ts.map +1 -1
- package/dist/services/session-storage.js +23 -7
- package/dist/services/session-storage.js.map +1 -1
- package/dist/services/storage-maintenance.d.ts +73 -3
- package/dist/services/storage-maintenance.d.ts.map +1 -1
- package/dist/services/storage-maintenance.js +771 -104
- package/dist/services/storage-maintenance.js.map +1 -1
- package/dist/services/task-manager.d.ts.map +1 -1
- package/dist/services/task-manager.js +2 -2
- package/dist/services/task-manager.js.map +1 -1
- package/dist/services/web-search-adapters.d.ts.map +1 -1
- package/dist/services/web-search-adapters.js +89 -49
- package/dist/services/web-search-adapters.js.map +1 -1
- package/dist/services/web-search-mcp.d.ts.map +1 -1
- package/dist/services/web-search-mcp.js +34 -37
- package/dist/services/web-search-mcp.js.map +1 -1
- package/dist/services/workspace-state.d.ts +4 -0
- package/dist/services/workspace-state.d.ts.map +1 -1
- package/dist/services/workspace-state.js +30 -9
- package/dist/services/workspace-state.js.map +1 -1
- package/dist/terminal-ui/completion.js +1 -1
- package/dist/terminal-ui/completion.js.map +1 -1
- package/dist/terminal-ui/launch.d.ts +3 -0
- package/dist/terminal-ui/launch.d.ts.map +1 -1
- package/dist/terminal-ui/launch.js +26 -98
- package/dist/terminal-ui/launch.js.map +1 -1
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +275 -27
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +29 -10
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mcp.js +20 -12
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/web.d.ts.map +1 -1
- package/dist/tools/web.js +38 -14
- package/dist/tools/web.js.map +1 -1
- package/dist/tui-ui/layout.d.ts.map +1 -1
- package/dist/tui-ui/layout.js +36 -23
- package/dist/tui-ui/layout.js.map +1 -1
- package/dist/tui-ui/pickers.d.ts.map +1 -1
- package/dist/tui-ui/pickers.js +5 -4
- package/dist/tui-ui/pickers.js.map +1 -1
- package/dist/tui-ui/runner.d.ts +1 -0
- package/dist/tui-ui/runner.d.ts.map +1 -1
- package/dist/tui-ui/runner.js +15 -17
- package/dist/tui-ui/runner.js.map +1 -1
- package/dist/tui-ui/state.d.ts +6 -0
- package/dist/tui-ui/state.d.ts.map +1 -1
- package/dist/tui-ui/state.js +61 -30
- package/dist/tui-ui/state.js.map +1 -1
- package/package.json +14 -9
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AgentRuntimeController = void 0;
|
|
4
|
+
exports.goalProviderError = goalProviderError;
|
|
5
|
+
exports.goalTurnMadeProgress = goalTurnMadeProgress;
|
|
4
6
|
const parser_1 = require("../commands/parser");
|
|
5
7
|
const target_command_1 = require("../commands/target-command");
|
|
6
8
|
const agent_runtime_protocol_1 = require("./agent-runtime-protocol");
|
|
@@ -8,10 +10,46 @@ const agent_status_1 = require("./agent-status");
|
|
|
8
10
|
const chat_controller_1 = require("./chat-controller");
|
|
9
11
|
const ui_events_1 = require("./ui-events");
|
|
10
12
|
const turn_controller_1 = require("./turn-controller");
|
|
13
|
+
const tools_1 = require("./goals/tools");
|
|
14
|
+
const accounting_1 = require("./goals/accounting");
|
|
15
|
+
const crypto_1 = require("crypto");
|
|
16
|
+
const workspace_state_1 = require("../services/workspace-state");
|
|
17
|
+
const redaction_1 = require("../services/redaction");
|
|
18
|
+
const evidence_1 = require("./goals/evidence");
|
|
19
|
+
const session_storage_1 = require("../services/session-storage");
|
|
20
|
+
const external_assertion_1 = require("../framework/external-assertion");
|
|
11
21
|
function isExitInput(input) {
|
|
12
22
|
const parsed = (0, parser_1.parseInput)(input.trim());
|
|
13
23
|
return parsed.isCommand && ['exit', 'quit', 'q'].includes(parsed.name);
|
|
14
24
|
}
|
|
25
|
+
function goalProviderError(finishReason, errorType) {
|
|
26
|
+
if (finishReason !== 'failed' || !errorType)
|
|
27
|
+
return undefined;
|
|
28
|
+
switch (errorType) {
|
|
29
|
+
case 'quota_or_credit_exhausted':
|
|
30
|
+
return { kind: 'usage_limit', retryable: false };
|
|
31
|
+
case 'rate_limit':
|
|
32
|
+
return { kind: 'rate_limit', retryable: true };
|
|
33
|
+
case 'provider_busy':
|
|
34
|
+
return { kind: 'provider_busy', retryable: true };
|
|
35
|
+
case 'auth_failed':
|
|
36
|
+
return { kind: 'auth', retryable: false };
|
|
37
|
+
case 'connect_timeout':
|
|
38
|
+
case 'read_timeout':
|
|
39
|
+
case 'connection_reset':
|
|
40
|
+
case 'network_error':
|
|
41
|
+
return { kind: 'network', retryable: true };
|
|
42
|
+
default:
|
|
43
|
+
return { kind: 'unknown', retryable: false };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function goalTurnMadeProgress(input) {
|
|
47
|
+
const passedEvidence = input.evidenceRecords?.some(record => record.result === 'passed') ?? false;
|
|
48
|
+
const workspaceChanged = Boolean(input.workspaceFingerprintBefore &&
|
|
49
|
+
input.workspaceFingerprintAfter &&
|
|
50
|
+
input.workspaceFingerprintBefore !== input.workspaceFingerprintAfter);
|
|
51
|
+
return passedEvidence || workspaceChanged;
|
|
52
|
+
}
|
|
15
53
|
function submittedEntry(input) {
|
|
16
54
|
const parsed = (0, parser_1.parseInput)(input.trim());
|
|
17
55
|
return {
|
|
@@ -41,9 +79,7 @@ class AgentRuntimeController {
|
|
|
41
79
|
/** v0.2.24: set the goal coordinator for /target mode. */
|
|
42
80
|
setGoalCoordinator(coord) {
|
|
43
81
|
this.goalCoordinator = coord;
|
|
44
|
-
|
|
45
|
-
const { setGoalToolCoordinator } = require('./goals/tools');
|
|
46
|
-
setGoalToolCoordinator(coord);
|
|
82
|
+
this.goalCoordinatorSessionId = coord.boundSessionId;
|
|
47
83
|
// Wire goal prompt injection into the chat controller.
|
|
48
84
|
if ('setGoalCoordinator' in this.runner) {
|
|
49
85
|
this.runner.setGoalCoordinator(coord);
|
|
@@ -54,20 +90,228 @@ class AgentRuntimeController {
|
|
|
54
90
|
this.pendingPermissions = new Map();
|
|
55
91
|
this.activeRun = null;
|
|
56
92
|
this.stopping = false;
|
|
93
|
+
this.continuationScheduleEpoch = 0;
|
|
94
|
+
/** Conservative reservations for every provider attempt in the current root turn. */
|
|
95
|
+
this.goalProviderReservedTokens = 0;
|
|
57
96
|
this.nextPermissionRequestId = 1;
|
|
58
97
|
/** v0.2.24: optional goal coordinator for /target mode. */
|
|
59
98
|
this.goalCoordinator = null;
|
|
99
|
+
this.goalCoordinatorSessionId = null;
|
|
60
100
|
if (!options.events && !options.eventSink) {
|
|
61
101
|
throw new Error('AgentRuntimeController requires either events or eventSink');
|
|
62
102
|
}
|
|
63
103
|
this.turnController = new turn_controller_1.TurnController(options);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
104
|
+
const downstream = options.eventSink ?? (0, agent_runtime_protocol_1.createAgentRuntimeEventSinkFromUiEvents)(options.events);
|
|
105
|
+
this.eventSink = {
|
|
106
|
+
emit: event => {
|
|
107
|
+
this.captureGoalEvidence(event);
|
|
108
|
+
const result = downstream.emit(event);
|
|
109
|
+
if (event.type === 'session_restored') {
|
|
110
|
+
this.restoreGoalForSession(event.event.sessionId, event.event.projectPath);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const events = (0, agent_runtime_protocol_1.createUiEventSinkFromAgentRuntimeEvents)(this.eventSink);
|
|
116
|
+
this.runner =
|
|
117
|
+
options.runner ?? new chat_controller_1.AgentChatController(options.runtime, events, this.createChatOptions());
|
|
67
118
|
}
|
|
68
119
|
hasActiveTurn() {
|
|
69
120
|
return this.turnController.hasActiveTurn();
|
|
70
121
|
}
|
|
122
|
+
/** v0.1.1: shared /target command handling for all renderers. */
|
|
123
|
+
handleTargetInput(rawInput) {
|
|
124
|
+
const parsed = (0, target_command_1.parseTargetCommand)(rawInput);
|
|
125
|
+
if (!parsed.ok) {
|
|
126
|
+
this.emitAppend({
|
|
127
|
+
role: 'error',
|
|
128
|
+
title: 'target',
|
|
129
|
+
content: parsed.error,
|
|
130
|
+
errorLayer: 'runtime',
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
handled: true,
|
|
134
|
+
statusText: parsed.error,
|
|
135
|
+
runtimeResult: { type: 'command_handled' },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const input = parsed.input;
|
|
139
|
+
const coord = this.ensureGoalCoordinator(input.action !== 'show');
|
|
140
|
+
if (!coord) {
|
|
141
|
+
const statusText = 'Target unavailable: no active session.';
|
|
142
|
+
this.emitAppend({
|
|
143
|
+
role: 'error',
|
|
144
|
+
title: 'target',
|
|
145
|
+
content: statusText,
|
|
146
|
+
errorLayer: 'session',
|
|
147
|
+
});
|
|
148
|
+
return { handled: true, statusText, runtimeResult: { type: 'command_handled' } };
|
|
149
|
+
}
|
|
150
|
+
if (this.turnController.hasActiveTurn() && !['pause', 'show', 'clear'].includes(input.action)) {
|
|
151
|
+
const statusText = 'Target command ignored while the agent is running. Use /target pause or interrupt first.';
|
|
152
|
+
this.emitStatus(statusText);
|
|
153
|
+
return { handled: true, statusText, runtimeResult: { type: 'command_ignored' } };
|
|
154
|
+
}
|
|
155
|
+
let success = true;
|
|
156
|
+
let error;
|
|
157
|
+
let executionRevoked = false;
|
|
158
|
+
const previousGoalId = coord.goal?.goalId;
|
|
159
|
+
try {
|
|
160
|
+
switch (input.action) {
|
|
161
|
+
case 'show':
|
|
162
|
+
break;
|
|
163
|
+
case 'create': {
|
|
164
|
+
const result = coord.create(input.payload?.objective ?? '');
|
|
165
|
+
success = result.ok;
|
|
166
|
+
if (!result.ok)
|
|
167
|
+
error = result.error;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'pause':
|
|
171
|
+
if (coord.isActive) {
|
|
172
|
+
this.abortGoalOwnedExecution();
|
|
173
|
+
executionRevoked = true;
|
|
174
|
+
}
|
|
175
|
+
success = coord.pause();
|
|
176
|
+
if (!success)
|
|
177
|
+
error = 'Target is not active.';
|
|
178
|
+
break;
|
|
179
|
+
case 'resume':
|
|
180
|
+
success = coord.resume({
|
|
181
|
+
confirmBoundary: true,
|
|
182
|
+
expectedGoalId: coord.goal?.goalId,
|
|
183
|
+
expectedRevision: coord.goal?.revision,
|
|
184
|
+
});
|
|
185
|
+
if (!success)
|
|
186
|
+
error = 'Target cannot be resumed from its current state.';
|
|
187
|
+
break;
|
|
188
|
+
case 'confirm':
|
|
189
|
+
success = coord.confirmCriterion(input.payload?.criterionId ?? '');
|
|
190
|
+
if (!success) {
|
|
191
|
+
error = 'Criterion cannot be confirmed. It must exist and require user evidence.';
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
case 'edit':
|
|
195
|
+
success = coord.edit(input.payload?.objective ?? '');
|
|
196
|
+
if (!success)
|
|
197
|
+
error = 'Target objective could not be updated.';
|
|
198
|
+
break;
|
|
199
|
+
case 'replace':
|
|
200
|
+
success = coord.replace(input.payload?.objective ?? '');
|
|
201
|
+
if (!success) {
|
|
202
|
+
error = 'Target could not be replaced.';
|
|
203
|
+
if (input.payload?.objective?.trim()) {
|
|
204
|
+
error = this.failClosedGoalMutation(coord, input.action, new Error(error), executionRevoked, previousGoalId);
|
|
205
|
+
executionRevoked = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
case 'set_budget':
|
|
210
|
+
success = coord.setBudget(input.payload?.tokenBudget ?? null);
|
|
211
|
+
if (!success)
|
|
212
|
+
error = 'Create or resume a target before setting a budget.';
|
|
213
|
+
break;
|
|
214
|
+
case 'clear':
|
|
215
|
+
if (!input.payload?.confirmed) {
|
|
216
|
+
success = false;
|
|
217
|
+
error = 'Clearing a target requires confirmation: /target clear --yes';
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const hadGoal = coord.goal !== null;
|
|
221
|
+
if (hadGoal) {
|
|
222
|
+
this.abortGoalOwnedExecution();
|
|
223
|
+
executionRevoked = true;
|
|
224
|
+
}
|
|
225
|
+
success = coord.clear();
|
|
226
|
+
if (hadGoal && !success) {
|
|
227
|
+
error = this.failClosedGoalMutation(coord, input.action, new Error('Target clear did not remove the active Goal.'), executionRevoked, previousGoalId);
|
|
228
|
+
}
|
|
229
|
+
if (!success && !hadGoal)
|
|
230
|
+
error = 'No target exists to clear.';
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (cause) {
|
|
236
|
+
success = false;
|
|
237
|
+
error =
|
|
238
|
+
input.action === 'show'
|
|
239
|
+
? cause instanceof Error
|
|
240
|
+
? cause.message
|
|
241
|
+
: String(cause)
|
|
242
|
+
: this.failClosedGoalMutation(coord, input.action, cause, executionRevoked, previousGoalId);
|
|
243
|
+
}
|
|
244
|
+
if (!success && error) {
|
|
245
|
+
this.emitAppend({ role: 'error', title: 'target', content: error, errorLayer: 'runtime' });
|
|
246
|
+
}
|
|
247
|
+
if (success &&
|
|
248
|
+
input.action === 'resume' &&
|
|
249
|
+
coord.goal?.status === 'active' &&
|
|
250
|
+
coord.goal.tokenBudget !== undefined) {
|
|
251
|
+
const resumedBudget = (0, accounting_1.budgetPreflight)(coord.goal.tokensUsed, coord.goal.tokenBudget, 0);
|
|
252
|
+
if (!resumedBudget.available) {
|
|
253
|
+
try {
|
|
254
|
+
coord.limitBudget(resumedBudget.reason ?? 'Token budget unavailable after resuming the target.');
|
|
255
|
+
}
|
|
256
|
+
catch (cause) {
|
|
257
|
+
success = false;
|
|
258
|
+
error = this.failClosedGoalMutation(coord, 'resume_budget_stop', cause, executionRevoked, previousGoalId);
|
|
259
|
+
this.emitGoalMutationError(error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (success) {
|
|
264
|
+
const { updateSessionGoalBinding } = require('../services/session-storage');
|
|
265
|
+
updateSessionGoalBinding(coord.boundSessionId, coord.goal);
|
|
266
|
+
if (input.action === 'clear' && previousGoalId) {
|
|
267
|
+
this.emitGoalEvent({ type: 'goal_cleared', goalId: previousGoalId, reason: 'user_clear' });
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const snapshot = coord.snapshot();
|
|
271
|
+
if (snapshot) {
|
|
272
|
+
this.emitGoalEvent({
|
|
273
|
+
type: 'goal_updated',
|
|
274
|
+
goal: snapshot,
|
|
275
|
+
reason: `target_${input.action}`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const statusText = this.formatTargetStatus(coord);
|
|
281
|
+
this.emitAppend({ role: 'system', title: 'target', content: statusText });
|
|
282
|
+
if (success &&
|
|
283
|
+
(input.action === 'create' || input.action === 'resume' || input.action === 'replace') &&
|
|
284
|
+
coord.isActive) {
|
|
285
|
+
const req = coord.buildContinuationRequest();
|
|
286
|
+
if (req) {
|
|
287
|
+
const runtimeResult = this.submitGoalContinuation(req, `target_${input.action}`, true);
|
|
288
|
+
return { handled: true, statusText, runtimeResult };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { handled: true, statusText, runtimeResult: { type: 'command_handled' } };
|
|
292
|
+
}
|
|
293
|
+
/** v0.1.1: shared /target intercept check for all renderers. */
|
|
294
|
+
canInterceptTargetCommand(input, duringActiveTurn) {
|
|
295
|
+
if (!(0, target_command_1.isTargetCommand)(input))
|
|
296
|
+
return false;
|
|
297
|
+
const parsed = (0, target_command_1.parseTargetCommand)(input);
|
|
298
|
+
if (!parsed.ok)
|
|
299
|
+
return true; // intercept to show error
|
|
300
|
+
// During active turn, only allow non-mutating actions.
|
|
301
|
+
if (duringActiveTurn) {
|
|
302
|
+
return ['pause', 'show', 'clear'].includes(parsed.input.action);
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
/** v0.1.1: emit clear_view event through the renderer protocol. */
|
|
307
|
+
emitClearView() {
|
|
308
|
+
this.eventSink.emit({ type: 'clear_view' });
|
|
309
|
+
}
|
|
310
|
+
/** v0.1.1: emit shutdown_requested event through the renderer protocol. */
|
|
311
|
+
emitShutdownRequested(reason) {
|
|
312
|
+
this.deferGoalContinuation(reason ?? 'shutdown requested', true);
|
|
313
|
+
this.eventSink.emit({ type: 'shutdown_requested', reason });
|
|
314
|
+
}
|
|
71
315
|
setVerificationState(state) {
|
|
72
316
|
this.turnController.setVerificationState(state);
|
|
73
317
|
}
|
|
@@ -99,27 +343,76 @@ class AgentRuntimeController {
|
|
|
99
343
|
if (!submitted)
|
|
100
344
|
return { type: 'empty' };
|
|
101
345
|
if (isExitInput(submitted)) {
|
|
346
|
+
this.emitShutdownRequested('user request');
|
|
102
347
|
return { type: 'exit_requested' };
|
|
103
348
|
}
|
|
349
|
+
if ((0, target_command_1.isTargetCommand)(submitted)) {
|
|
350
|
+
return this.handleTargetInput(submitted).runtimeResult;
|
|
351
|
+
}
|
|
352
|
+
const parsedInput = (0, parser_1.parseInput)(submitted);
|
|
353
|
+
if (parsedInput.isCommand && parsedInput.name === 'clear') {
|
|
354
|
+
this.emitClearView();
|
|
355
|
+
this.emitStatus('View cleared. Conversation context is preserved.');
|
|
356
|
+
return { type: 'command_handled' };
|
|
357
|
+
}
|
|
104
358
|
if (this.turnController.hasActiveTurn()) {
|
|
105
359
|
const parsed = (0, parser_1.parseInput)(submitted);
|
|
106
360
|
if (parsed.isCommand) {
|
|
107
|
-
this.emitStatus(this.options.commandWhileRunningStatus ??
|
|
361
|
+
this.emitStatus(this.options.commandWhileRunningStatus ??
|
|
362
|
+
'Command ignored while agent is running. Press Ctrl+C to interrupt first.');
|
|
108
363
|
return { type: 'command_ignored' };
|
|
109
364
|
}
|
|
110
365
|
// v0.2.26: user steering input during active goal — update constraints
|
|
111
366
|
// without replacing the root objective.
|
|
112
367
|
const gc = this.goalCoordinator;
|
|
113
368
|
if (gc?.goal && !(0, target_command_1.isTargetCommand)(submitted)) {
|
|
114
|
-
|
|
115
|
-
|
|
369
|
+
const steeringGoalId = gc.goal.goalId;
|
|
370
|
+
try {
|
|
371
|
+
gc.addConstraint(submitted);
|
|
372
|
+
}
|
|
373
|
+
catch (cause) {
|
|
374
|
+
const message = this.failClosedGoalMutation(gc, 'steering', cause, false, steeringGoalId);
|
|
375
|
+
this.emitAppend({
|
|
376
|
+
role: 'error',
|
|
377
|
+
title: 'target steering',
|
|
378
|
+
content: message,
|
|
379
|
+
errorLayer: 'runtime',
|
|
380
|
+
});
|
|
381
|
+
this.emitStatus(message);
|
|
382
|
+
return { type: 'command_ignored' };
|
|
383
|
+
}
|
|
116
384
|
}
|
|
117
385
|
this.turnController.clearExitIntent();
|
|
118
386
|
this.turnController.requestRevision(submitted);
|
|
119
387
|
this.emitStatus(this.options.revisionStatus ?? 'Revision received. Interrupting current response...');
|
|
120
388
|
return { type: 'revision_requested' };
|
|
121
389
|
}
|
|
122
|
-
this.
|
|
390
|
+
const session = this.options.runtime.getSession() ?? this.options.runtime.ensureSession();
|
|
391
|
+
const coord = this.ensureGoalCoordinator(false);
|
|
392
|
+
const goal = coord?.goal?.status === 'active' ? coord.goal : undefined;
|
|
393
|
+
const request = {
|
|
394
|
+
inputKind: 'user',
|
|
395
|
+
text: submitted,
|
|
396
|
+
sessionId: session?.id ?? 'pending-session',
|
|
397
|
+
goal: goal
|
|
398
|
+
? {
|
|
399
|
+
goalId: goal.goalId,
|
|
400
|
+
revision: goal.revision,
|
|
401
|
+
continuationIndex: goal.continuationCount,
|
|
402
|
+
}
|
|
403
|
+
: undefined,
|
|
404
|
+
persistAsUserMessage: true,
|
|
405
|
+
echoToTranscript: true,
|
|
406
|
+
generation: coord?.generation ?? 0,
|
|
407
|
+
};
|
|
408
|
+
return this.submitTurnRequest(request);
|
|
409
|
+
}
|
|
410
|
+
submitTurnRequest(request) {
|
|
411
|
+
if (this.turnController.hasActiveTurn())
|
|
412
|
+
return { type: 'command_ignored' };
|
|
413
|
+
if (!this.requestIsCurrent(request))
|
|
414
|
+
return { type: 'command_ignored' };
|
|
415
|
+
this.activeRun = this.runTurn(request)
|
|
123
416
|
.catch(error => {
|
|
124
417
|
this.handleRunLoopError(error);
|
|
125
418
|
})
|
|
@@ -130,58 +423,326 @@ class AgentRuntimeController {
|
|
|
130
423
|
});
|
|
131
424
|
return { type: 'started' };
|
|
132
425
|
}
|
|
426
|
+
submitGoalContinuation(request, reason, emitScheduled) {
|
|
427
|
+
const goalId = request.goal?.goalId;
|
|
428
|
+
if (emitScheduled && goalId) {
|
|
429
|
+
this.emitGoalEvent({ type: 'goal_continuation', goalId, phase: 'scheduled', reason });
|
|
430
|
+
}
|
|
431
|
+
const result = this.submitTurnRequest(request);
|
|
432
|
+
if (goalId) {
|
|
433
|
+
this.emitGoalEvent({
|
|
434
|
+
type: 'goal_continuation',
|
|
435
|
+
goalId,
|
|
436
|
+
phase: result.type === 'started' ? 'started' : 'deferred',
|
|
437
|
+
reason: result.type === 'started' ? reason : `request rejected: ${reason}`,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
133
442
|
// --- v0.2.26: goal continuation scheduling ---
|
|
134
443
|
scheduleGoalContinuation() {
|
|
135
444
|
const coord = this.goalCoordinator;
|
|
136
445
|
if (!coord || !coord.isActive || coord.goal?.status !== 'active')
|
|
137
446
|
return;
|
|
138
|
-
if (!coord.canContinue)
|
|
139
|
-
|
|
447
|
+
if (!coord.canContinue) {
|
|
448
|
+
this.emitGoalEvent({
|
|
449
|
+
type: 'goal_continuation',
|
|
450
|
+
goalId: coord.goal.goalId,
|
|
451
|
+
phase: 'deferred',
|
|
452
|
+
reason: 'coordinator is not eligible to continue',
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const scheduleEpoch = ++this.continuationScheduleEpoch;
|
|
457
|
+
const scheduledGoalId = coord.goal.goalId;
|
|
140
458
|
// Defer to next tick to let the current turn's events settle.
|
|
141
459
|
setImmediate(() => {
|
|
142
|
-
if (
|
|
460
|
+
if (scheduleEpoch !== this.continuationScheduleEpoch) {
|
|
461
|
+
this.emitGoalEvent({
|
|
462
|
+
type: 'goal_continuation',
|
|
463
|
+
goalId: scheduledGoalId,
|
|
464
|
+
phase: 'deferred',
|
|
465
|
+
reason: 'scheduled continuation was invalidated',
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (this.stopping ||
|
|
470
|
+
this.turnController.hasActiveTurn() ||
|
|
471
|
+
this.pendingPermissions.size > 0) {
|
|
472
|
+
this.emitGoalEvent({
|
|
473
|
+
type: 'goal_continuation',
|
|
474
|
+
goalId: scheduledGoalId,
|
|
475
|
+
phase: 'deferred',
|
|
476
|
+
reason: this.stopping
|
|
477
|
+
? 'runtime is stopping'
|
|
478
|
+
: this.pendingPermissions.size > 0
|
|
479
|
+
? 'tool permission is pending'
|
|
480
|
+
: 'another turn is active',
|
|
481
|
+
});
|
|
143
482
|
return;
|
|
144
|
-
|
|
145
|
-
|
|
483
|
+
}
|
|
484
|
+
if (!coord.isActive || coord.goal?.status !== 'active') {
|
|
485
|
+
this.emitGoalEvent({
|
|
486
|
+
type: 'goal_continuation',
|
|
487
|
+
goalId: scheduledGoalId,
|
|
488
|
+
phase: 'deferred',
|
|
489
|
+
reason: 'goal is no longer active',
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
146
493
|
const req = coord.buildContinuationRequest();
|
|
147
494
|
if (req) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
495
|
+
const projected = coord.goal?.lastTurn?.totalTokens ?? 0;
|
|
496
|
+
const preflight = (0, accounting_1.budgetPreflight)(coord.goal?.tokensUsed ?? 0, coord.goal?.tokenBudget, projected);
|
|
497
|
+
if (!preflight.available) {
|
|
498
|
+
try {
|
|
499
|
+
coord.limitBudget(preflight.reason ?? 'Token budget unavailable for another turn.');
|
|
500
|
+
}
|
|
501
|
+
catch (cause) {
|
|
502
|
+
const message = this.failClosedGoalMutation(coord, 'budget_stop', cause, false, scheduledGoalId);
|
|
503
|
+
this.emitGoalMutationError(message);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
this.emitGoalEvent({
|
|
507
|
+
type: 'goal_continuation',
|
|
508
|
+
goalId: coord.goal.goalId,
|
|
509
|
+
phase: 'stopped',
|
|
510
|
+
reason: preflight.reason ?? 'budget preflight failed',
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
this.submitGoalContinuation(req, `continuation ${req.goal.continuationIndex}`, true);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
this.emitGoalEvent({
|
|
518
|
+
type: 'goal_continuation',
|
|
519
|
+
goalId: scheduledGoalId,
|
|
520
|
+
phase: 'deferred',
|
|
521
|
+
reason: 'coordinator did not produce a continuation request',
|
|
152
522
|
});
|
|
153
523
|
}
|
|
154
524
|
});
|
|
155
525
|
}
|
|
156
526
|
/** v0.2.26: finalize goal turn with usage data from the last loop. */
|
|
157
|
-
finalizeGoalTurn(turnId) {
|
|
527
|
+
finalizeGoalTurn(turnId, request, toolContext, startedAt, workspaceFingerprintBefore, turnAborted) {
|
|
158
528
|
const coord = this.goalCoordinator;
|
|
159
|
-
|
|
529
|
+
const requestGoal = request.goal;
|
|
530
|
+
const currentSessionId = this.options.runtime.getSession()?.id;
|
|
531
|
+
if (!coord?.goal ||
|
|
532
|
+
!requestGoal ||
|
|
533
|
+
currentSessionId !== request.sessionId ||
|
|
534
|
+
coord.boundSessionId !== request.sessionId ||
|
|
535
|
+
coord.generation !== request.generation ||
|
|
536
|
+
coord.goal.goalId !== requestGoal.goalId) {
|
|
537
|
+
if (request.inputKind === 'goal_continuation' && requestGoal) {
|
|
538
|
+
this.emitGoalEvent({
|
|
539
|
+
type: 'goal_continuation',
|
|
540
|
+
goalId: requestGoal.goalId,
|
|
541
|
+
phase: 'deferred',
|
|
542
|
+
reason: 'turn outcome rejected because its session, goal, revision, or generation is stale',
|
|
543
|
+
});
|
|
544
|
+
}
|
|
160
545
|
return;
|
|
546
|
+
}
|
|
161
547
|
const usage = this.options.runtime.store.getSnapshot().tokenUsage;
|
|
162
548
|
const loopStats = this.options.runtime.store.getSnapshot().lastLoopStats;
|
|
549
|
+
const subagentPromptTokens = loopStats?.subagentPromptTokens ?? 0;
|
|
550
|
+
const subagentCompletionTokens = loopStats?.subagentCompletionTokens ?? 0;
|
|
551
|
+
const subagentTokens = loopStats?.subagentTotalTokens ?? 0;
|
|
552
|
+
const workspaceFingerprintAfter = (0, workspace_state_1.captureWorkspaceFingerprint)(this.activeProjectPath());
|
|
553
|
+
const workspaceChanged = Boolean(workspaceFingerprintBefore &&
|
|
554
|
+
workspaceFingerprintAfter &&
|
|
555
|
+
workspaceFingerprintBefore !== workspaceFingerprintAfter);
|
|
556
|
+
const usageAccountingComplete = loopStats
|
|
557
|
+
?.usageAccountingComplete !== false;
|
|
163
558
|
const outcome = {
|
|
164
559
|
turnId: String(turnId),
|
|
165
|
-
sessionId:
|
|
166
|
-
goalId:
|
|
167
|
-
goalRevision:
|
|
168
|
-
|
|
560
|
+
sessionId: request.sessionId,
|
|
561
|
+
goalId: requestGoal.goalId,
|
|
562
|
+
goalRevision: requestGoal.revision,
|
|
563
|
+
goalGeneration: request.generation,
|
|
564
|
+
startedAt,
|
|
169
565
|
endedAt: Date.now(),
|
|
170
566
|
finishReason: loopStats?.finishReason ?? 'unknown',
|
|
171
567
|
usage: {
|
|
172
|
-
promptTokens: usage?.promptTokens ?? 0,
|
|
173
|
-
completionTokens: usage?.completionTokens ?? 0,
|
|
174
|
-
subagentTokens
|
|
175
|
-
totalTokens:
|
|
568
|
+
promptTokens: Math.max(0, (usage?.promptTokens ?? 0) - subagentPromptTokens),
|
|
569
|
+
completionTokens: Math.max(0, (usage?.completionTokens ?? 0) - subagentCompletionTokens),
|
|
570
|
+
subagentTokens,
|
|
571
|
+
totalTokens: usage !== null
|
|
572
|
+
? (usage?.promptTokens ?? 0) + (usage?.completionTokens ?? 0)
|
|
573
|
+
: subagentTokens,
|
|
176
574
|
},
|
|
177
|
-
madeProgress:
|
|
575
|
+
madeProgress: goalTurnMadeProgress({
|
|
576
|
+
evidenceRecords: toolContext?.evidenceRecords,
|
|
577
|
+
pendingPlanUpdate: toolContext?.pendingPlanUpdate,
|
|
578
|
+
workspaceFingerprintBefore,
|
|
579
|
+
workspaceFingerprintAfter,
|
|
580
|
+
}),
|
|
581
|
+
workspaceChanged,
|
|
582
|
+
providerError: goalProviderError(loopStats?.finishReason, loopStats?.providerLastRetryErrorType),
|
|
583
|
+
blocker: toolContext?.pendingBlocker,
|
|
584
|
+
pendingTerminalRequest: toolContext?.pendingTerminalRequest,
|
|
585
|
+
pendingPlanUpdate: toolContext?.pendingPlanUpdate,
|
|
586
|
+
evidenceRecords: toolContext?.evidenceRecords,
|
|
587
|
+
workspaceFingerprint: workspaceFingerprintAfter,
|
|
588
|
+
evidenceRefs: toolContext?.evidenceRecords.map(record => record.id),
|
|
589
|
+
verificationSummary: this.verificationSummary(loopStats),
|
|
590
|
+
usageComplete: usageAccountingComplete &&
|
|
591
|
+
usage !== null &&
|
|
592
|
+
loopStats?.finishReason !== 'failed' &&
|
|
593
|
+
(loopStats?.providerRetryCount ?? 0) === 0 &&
|
|
594
|
+
(loopStats?.providerFallbackCount ?? 0) === 0,
|
|
178
595
|
};
|
|
179
|
-
coord.
|
|
596
|
+
const semanticOutcomeIsCurrent = coord.goal.status === 'active' &&
|
|
597
|
+
!turnAborted &&
|
|
598
|
+
coord.goal.revision === requestGoal.revision;
|
|
599
|
+
if (!semanticOutcomeIsCurrent) {
|
|
600
|
+
const accountingOnlyEligible = turnAborted || coord.goal.status === 'paused' || coord.goal.revision > requestGoal.revision;
|
|
601
|
+
if (!accountingOnlyEligible) {
|
|
602
|
+
if (request.inputKind === 'goal_continuation') {
|
|
603
|
+
this.emitGoalEvent({
|
|
604
|
+
type: 'goal_continuation',
|
|
605
|
+
goalId: requestGoal.goalId,
|
|
606
|
+
phase: 'deferred',
|
|
607
|
+
reason: 'turn outcome rejected because the Goal lifecycle no longer accepts this turn',
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
// accountStaleTurn is the coordinator's accounting-only, idempotent
|
|
613
|
+
// entry point. An interrupt can leave the Goal revision unchanged, so
|
|
614
|
+
// freeze this outcome immediately before the current revision without
|
|
615
|
+
// admitting any semantic payload from the cancelled turn.
|
|
616
|
+
const accountingOutcome = {
|
|
617
|
+
...outcome,
|
|
618
|
+
goalRevision: Math.min(outcome.goalRevision ?? requestGoal.revision, coord.goal.revision - 1),
|
|
619
|
+
};
|
|
620
|
+
let accounted = false;
|
|
621
|
+
try {
|
|
622
|
+
const alreadyAccounted = coord.goal.lastTurn?.turnId === String(turnId);
|
|
623
|
+
accounted = alreadyAccounted ? false : coord.accountStaleTurn(accountingOutcome);
|
|
624
|
+
// Missing/partial provider usage is fail-closed. Known lower-bound
|
|
625
|
+
// tokens have already been retained by accountStaleTurn.
|
|
626
|
+
if (outcome.usageComplete === false && coord.goal?.status === 'active') {
|
|
627
|
+
coord.pause();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
this.failClosedGoalPersistence(coord, requestGoal.goalId, error, 'stale_accounting');
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const snapshot = coord.snapshot();
|
|
635
|
+
if (accounted && snapshot) {
|
|
636
|
+
this.emitGoalEvent({
|
|
637
|
+
type: 'goal_updated',
|
|
638
|
+
goal: snapshot,
|
|
639
|
+
reason: 'interrupted or stale turn usage accounted without semantic finalization',
|
|
640
|
+
});
|
|
641
|
+
this.recordGoalTraceState(turnId, request, snapshot);
|
|
642
|
+
}
|
|
643
|
+
if (request.inputKind === 'goal_continuation') {
|
|
644
|
+
this.emitGoalEvent({
|
|
645
|
+
type: 'goal_continuation',
|
|
646
|
+
goalId: requestGoal.goalId,
|
|
647
|
+
phase: 'deferred',
|
|
648
|
+
reason: 'turn semantic outcome rejected; incurred usage was accounted without continuation',
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
// Live steering intentionally bumps the Goal revision before the aborted
|
|
654
|
+
// provider turn settles. The semantic payload is stale, but the provider
|
|
655
|
+
// usage and elapsed time were still incurred and must be counted once.
|
|
656
|
+
const planRevisionBefore = coord.goal.contract?.planSnapshot?.revision;
|
|
657
|
+
try {
|
|
658
|
+
coord.finalizeTurn(outcome);
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
this.failClosedGoalPersistence(coord, requestGoal.goalId, error, 'turn_finalize');
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const snapshot = coord.snapshot();
|
|
665
|
+
if (snapshot) {
|
|
666
|
+
this.recordGoalTraceState(turnId, request, snapshot);
|
|
667
|
+
if (coord.goal?.contract?.planSnapshot &&
|
|
668
|
+
coord.goal.contract.planSnapshot.revision !== planRevisionBefore) {
|
|
669
|
+
const plan = coord.goal.contract.planSnapshot;
|
|
670
|
+
this.emitGoalEvent({
|
|
671
|
+
type: 'goal_plan_updated',
|
|
672
|
+
goalId: snapshot.goalId,
|
|
673
|
+
planRevision: plan.revision,
|
|
674
|
+
phase: plan.phase,
|
|
675
|
+
nextAction: plan.nextAction,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
let terminalEvent;
|
|
679
|
+
if (snapshot.status === 'complete' && coord.goal?.completionAudit) {
|
|
680
|
+
terminalEvent = {
|
|
681
|
+
type: 'goal_completed',
|
|
682
|
+
goal: snapshot,
|
|
683
|
+
audit: coord.goal.completionAudit,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
else if (coord.goal?.completionAudit && !coord.goal.completionAudit.passed) {
|
|
687
|
+
terminalEvent = {
|
|
688
|
+
type: 'goal_audit_failed',
|
|
689
|
+
goalId: snapshot.goalId,
|
|
690
|
+
audit: 'completion',
|
|
691
|
+
summary: coord.goal.completionAudit.remainingRequirements.join(' '),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
else if (toolContext?.pendingTerminalRequest?.requestedStatus === 'blocked' &&
|
|
695
|
+
snapshot.status !== 'blocked') {
|
|
696
|
+
const blocker = coord.goal?.blocker;
|
|
697
|
+
terminalEvent = {
|
|
698
|
+
type: 'goal_audit_failed',
|
|
699
|
+
goalId: snapshot.goalId,
|
|
700
|
+
audit: 'blocked',
|
|
701
|
+
summary: blocker
|
|
702
|
+
? `Blocker is not terminal: ${blocker.consecutiveTurns}/3 consecutive turns; no-progress ${coord.goal?.noProgressCount ?? 0}/3.`
|
|
703
|
+
: 'Blocked request did not include a valid non-retryable user, permission, or external-state blocker.',
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
// Project the fresh snapshot first, then the specific audit/completion
|
|
707
|
+
// event. This keeps renderer state current without a generic update
|
|
708
|
+
// overwriting the user-facing terminal result.
|
|
709
|
+
this.emitGoalEvent({ type: 'goal_updated', goal: snapshot, reason: 'turn_finalized' });
|
|
710
|
+
if (terminalEvent)
|
|
711
|
+
this.emitGoalEvent(terminalEvent);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
failClosedGoalPersistence(coord, goalId, error, operation) {
|
|
715
|
+
const message = this.failClosedGoalMutation(coord, operation, error, false, goalId);
|
|
716
|
+
this.emitAppend({
|
|
717
|
+
role: 'error',
|
|
718
|
+
title: 'goal persistence',
|
|
719
|
+
content: message,
|
|
720
|
+
errorLayer: 'session',
|
|
721
|
+
});
|
|
722
|
+
this.emitStatus(message);
|
|
723
|
+
}
|
|
724
|
+
recordGoalTraceState(turnId, request, snapshot) {
|
|
725
|
+
if (!request.goal)
|
|
726
|
+
return;
|
|
727
|
+
const trace = (0, session_storage_1.appendSessionTraceEvent)(request.sessionId, {
|
|
728
|
+
turnId: String(turnId),
|
|
729
|
+
type: 'goal_state',
|
|
730
|
+
goalId: request.goal.goalId,
|
|
731
|
+
goalRevision: request.goal.revision,
|
|
732
|
+
goalInputKind: request.inputKind,
|
|
733
|
+
goalStopReason: snapshot.stopReason ?? (snapshot.status === 'complete' ? 'completed' : 'continue'),
|
|
734
|
+
note: `status=${snapshot.status}; snapshotRevision=${snapshot.revision}`,
|
|
735
|
+
});
|
|
736
|
+
if (trace)
|
|
737
|
+
this.eventSink.emit({ type: 'trace_event_recorded', event: trace });
|
|
180
738
|
}
|
|
181
739
|
interrupt() {
|
|
182
|
-
const shouldExit = this.turnController.registerExitIntent();
|
|
183
740
|
// v0.2.26: pause active goal on interrupt to prevent immediate restart.
|
|
184
|
-
this.
|
|
741
|
+
this.deferGoalContinuation('user interrupt', true);
|
|
742
|
+
// Register after any synchronous persistence recovery. A lock wait can be
|
|
743
|
+
// longer than the confirmation window, but must not consume the user's
|
|
744
|
+
// first Ctrl+C before this call even returns.
|
|
745
|
+
const shouldExit = this.turnController.registerExitIntent();
|
|
185
746
|
if (this.turnController.hasActiveTurn()) {
|
|
186
747
|
this.turnController.interruptActiveTurn();
|
|
187
748
|
if (shouldExit)
|
|
@@ -196,6 +757,7 @@ class AgentRuntimeController {
|
|
|
196
757
|
}
|
|
197
758
|
async stopActiveTurn() {
|
|
198
759
|
this.stopping = true;
|
|
760
|
+
this.deferGoalContinuation('runtime stopping');
|
|
199
761
|
this.turnController.interruptActiveTurn();
|
|
200
762
|
this.rejectPendingPermissions();
|
|
201
763
|
if (this.activeRun) {
|
|
@@ -204,24 +766,136 @@ class AgentRuntimeController {
|
|
|
204
766
|
// Reset stopping flag so subsequent turns can execute.
|
|
205
767
|
this.stopping = false;
|
|
206
768
|
}
|
|
769
|
+
deferGoalContinuation(reason, preserveExitIntent = false) {
|
|
770
|
+
this.continuationScheduleEpoch += 1;
|
|
771
|
+
const coord = this.goalCoordinator;
|
|
772
|
+
const goalId = coord?.goal?.status === 'active' ? coord.goal.goalId : undefined;
|
|
773
|
+
if (!coord || !goalId)
|
|
774
|
+
return;
|
|
775
|
+
try {
|
|
776
|
+
coord.deferContinuation();
|
|
777
|
+
}
|
|
778
|
+
catch (cause) {
|
|
779
|
+
const message = this.failClosedGoalMutation(coord, 'pause', cause, false, goalId, preserveExitIntent);
|
|
780
|
+
this.emitGoalMutationError(message);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
this.emitGoalEvent({
|
|
784
|
+
type: 'goal_continuation',
|
|
785
|
+
goalId,
|
|
786
|
+
phase: 'deferred',
|
|
787
|
+
reason,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
/** Revoke all Goal-owned execution before mutating a lifecycle boundary. */
|
|
791
|
+
abortGoalOwnedExecution(preserveExitIntent = false) {
|
|
792
|
+
this.continuationScheduleEpoch += 1;
|
|
793
|
+
if (!preserveExitIntent)
|
|
794
|
+
this.turnController.clearExitIntent();
|
|
795
|
+
this.turnController.interruptActiveTurn();
|
|
796
|
+
this.rejectPendingPermissions();
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Contain a failed Goal mutation without writing the restored disk state back.
|
|
800
|
+
* The coordinator advances its generation so every in-flight request becomes
|
|
801
|
+
* stale, then preserves complete or deleted disk authority as-is.
|
|
802
|
+
*/
|
|
803
|
+
failClosedGoalMutation(coord, operation, cause, executionAlreadyRevoked = false, goalIdBeforeMutation, preserveExitIntent = false) {
|
|
804
|
+
if (!executionAlreadyRevoked)
|
|
805
|
+
this.abortGoalOwnedExecution(preserveExitIntent);
|
|
806
|
+
const detail = (0, redaction_1.redactTraceText)(cause instanceof Error ? cause.message : String(cause)).slice(0, 600);
|
|
807
|
+
const operationSubject = operation === 'steering'
|
|
808
|
+
? 'Steering'
|
|
809
|
+
: operation === 'turn_finalize'
|
|
810
|
+
? 'Goal turn finalization'
|
|
811
|
+
: operation === 'stale_accounting'
|
|
812
|
+
? 'Stale Goal accounting'
|
|
813
|
+
: `Target ${operation}`;
|
|
814
|
+
const goalIdBeforeRecovery = coord.goal?.goalId ?? goalIdBeforeMutation;
|
|
815
|
+
const failureReason = `${operationSubject} was not saved; Goal-owned execution was revoked fail-closed. ${detail}`;
|
|
816
|
+
coord.failClosedAfterPersistenceError(failureReason);
|
|
817
|
+
const snapshot = coord.snapshot();
|
|
818
|
+
const message = !snapshot
|
|
819
|
+
? `${operationSubject} was not saved; disk authority reports that the Goal was deleted. No continuation was started. ${detail}`
|
|
820
|
+
: coord.goal?.status === 'complete'
|
|
821
|
+
? `${operationSubject} was not saved; the restored completed Goal remains terminal. No continuation was started. ${detail}`
|
|
822
|
+
: `${operationSubject} was not saved; execution was paused fail-closed. Resolve the storage error, then use /target resume. ${detail}`;
|
|
823
|
+
if (snapshot) {
|
|
824
|
+
this.emitGoalEvent({
|
|
825
|
+
type: 'goal_updated',
|
|
826
|
+
goal: snapshot,
|
|
827
|
+
reason: coord.goal?.status === 'complete'
|
|
828
|
+
? `target_${operation} persistence failed; completed disk authority preserved`
|
|
829
|
+
: `target_${operation} persistence failed; continuation paused fail-closed`,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
else if (goalIdBeforeRecovery) {
|
|
833
|
+
this.emitGoalEvent({
|
|
834
|
+
type: 'goal_cleared',
|
|
835
|
+
goalId: goalIdBeforeRecovery,
|
|
836
|
+
reason: `target_${operation} failed after the Goal was deleted`,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const deferredGoalId = snapshot?.goalId ?? goalIdBeforeRecovery;
|
|
840
|
+
if (deferredGoalId) {
|
|
841
|
+
this.emitGoalEvent({
|
|
842
|
+
type: 'goal_continuation',
|
|
843
|
+
goalId: deferredGoalId,
|
|
844
|
+
phase: 'deferred',
|
|
845
|
+
reason: `target_${operation} persistence failed`,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
return message;
|
|
849
|
+
}
|
|
207
850
|
waitForIdle() {
|
|
208
851
|
return this.activeRun ?? Promise.resolve();
|
|
209
852
|
}
|
|
210
|
-
async runTurn(
|
|
211
|
-
let
|
|
212
|
-
while (
|
|
213
|
-
|
|
853
|
+
async runTurn(firstRequest) {
|
|
854
|
+
let nextRequest = firstRequest;
|
|
855
|
+
while (nextRequest && !this.stopping) {
|
|
856
|
+
this.goalProviderReservedTokens = 0;
|
|
857
|
+
if (!this.requestIsCurrent(nextRequest))
|
|
858
|
+
break;
|
|
859
|
+
const request = nextRequest;
|
|
860
|
+
const nextInput = request.text?.trim() ||
|
|
861
|
+
'Continue pursuing the active goal from its persisted plan and evidence.';
|
|
862
|
+
if ((this.options.echoSubmittedInput ?? true) && request.echoToTranscript) {
|
|
214
863
|
this.emitAppend(submittedEntry(nextInput));
|
|
215
864
|
}
|
|
216
865
|
this.options.beforeTurn?.(nextInput);
|
|
217
866
|
const turn = this.turnController.beginTurn(nextInput);
|
|
867
|
+
const startedAt = Date.now();
|
|
868
|
+
const workspaceFingerprintBefore = (0, workspace_state_1.captureWorkspaceFingerprint)(this.activeProjectPath());
|
|
869
|
+
const coord = this.ensureGoalCoordinator(false);
|
|
870
|
+
const toolContext = coord
|
|
871
|
+
? { coordinator: coord, request, turnId: String(turn.id), evidenceRecords: [] }
|
|
872
|
+
: undefined;
|
|
218
873
|
this.options.runtime.store.setProcessing(true);
|
|
219
874
|
this.emitProcessing(true);
|
|
220
875
|
const runningStatus = statusText(this.options.runningStatus, nextInput);
|
|
221
876
|
if (runningStatus)
|
|
222
877
|
this.emitStatus(runningStatus);
|
|
223
878
|
try {
|
|
224
|
-
|
|
879
|
+
const execute = () => {
|
|
880
|
+
if (this.runner.runRequest) {
|
|
881
|
+
return this.runner.runRequest(request, {
|
|
882
|
+
abortSignal: turn.abortSignal,
|
|
883
|
+
turnId: turn.id,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return this.runner.runInput(nextInput, {
|
|
887
|
+
abortSignal: turn.abortSignal,
|
|
888
|
+
turnId: turn.id,
|
|
889
|
+
persistAsUserMessage: request.persistAsUserMessage,
|
|
890
|
+
inputKind: request.inputKind,
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
if (toolContext) {
|
|
894
|
+
await (0, tools_1.runWithGoalToolContext)(toolContext, execute);
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
await execute();
|
|
898
|
+
}
|
|
225
899
|
}
|
|
226
900
|
catch (error) {
|
|
227
901
|
if (!turn.abortSignal.aborted) {
|
|
@@ -237,13 +911,35 @@ class AgentRuntimeController {
|
|
|
237
911
|
finally {
|
|
238
912
|
const revision = this.turnController.finishTurn(turn.id);
|
|
239
913
|
// v0.2.26: finalize goal turn with usage data.
|
|
240
|
-
this.finalizeGoalTurn(turn.id);
|
|
914
|
+
this.finalizeGoalTurn(turn.id, request, toolContext, startedAt, workspaceFingerprintBefore, turn.abortSignal.aborted);
|
|
241
915
|
if (revision?.trim()) {
|
|
242
|
-
this.
|
|
243
|
-
|
|
916
|
+
const currentCoord = this.ensureGoalCoordinator(false);
|
|
917
|
+
const currentGoal = currentCoord?.goal?.status === 'active' ? currentCoord.goal : undefined;
|
|
918
|
+
if (request.goal && currentCoord?.goal && !currentGoal) {
|
|
919
|
+
this.emitStatus('Latest instruction was saved, but the Goal is paused because usage accounting is incomplete. Use /target resume after reviewing the budget.');
|
|
920
|
+
nextRequest = undefined;
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
this.emitStatus(this.options.restartingStatus ?? 'Restarting with latest instruction...');
|
|
924
|
+
nextRequest = {
|
|
925
|
+
inputKind: 'revision',
|
|
926
|
+
text: revision,
|
|
927
|
+
sessionId: this.options.runtime.getSession()?.id ?? request.sessionId,
|
|
928
|
+
goal: currentGoal
|
|
929
|
+
? {
|
|
930
|
+
goalId: currentGoal.goalId,
|
|
931
|
+
revision: currentGoal.revision,
|
|
932
|
+
continuationIndex: currentGoal.continuationCount,
|
|
933
|
+
}
|
|
934
|
+
: undefined,
|
|
935
|
+
persistAsUserMessage: true,
|
|
936
|
+
echoToTranscript: true,
|
|
937
|
+
generation: currentCoord?.generation ?? 0,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
244
940
|
}
|
|
245
941
|
else {
|
|
246
|
-
|
|
942
|
+
nextRequest = undefined;
|
|
247
943
|
}
|
|
248
944
|
}
|
|
249
945
|
}
|
|
@@ -264,7 +960,11 @@ class AgentRuntimeController {
|
|
|
264
960
|
}
|
|
265
961
|
else {
|
|
266
962
|
const message = error instanceof Error ? error.message : String(error);
|
|
267
|
-
this.emitAppend({
|
|
963
|
+
this.emitAppend({
|
|
964
|
+
role: 'error',
|
|
965
|
+
content: `[RUNTIME] Error: ${message}`,
|
|
966
|
+
errorLayer: 'runtime',
|
|
967
|
+
});
|
|
268
968
|
}
|
|
269
969
|
this.options.runtime.store.setProcessing(false);
|
|
270
970
|
this.emitProcessing(false);
|
|
@@ -284,6 +984,131 @@ class AgentRuntimeController {
|
|
|
284
984
|
emitProcessing(processing) {
|
|
285
985
|
this.eventSink.emit({ type: 'processing_changed', processing });
|
|
286
986
|
}
|
|
987
|
+
emitGoalEvent(event) {
|
|
988
|
+
this.eventSink.emit({ type: 'goal_event', event });
|
|
989
|
+
}
|
|
990
|
+
requestIsCurrent(request) {
|
|
991
|
+
const activeSessionId = this.options.runtime.getSession()?.id;
|
|
992
|
+
if (activeSessionId && request.sessionId !== activeSessionId)
|
|
993
|
+
return false;
|
|
994
|
+
if (!request.goal)
|
|
995
|
+
return true;
|
|
996
|
+
const coord = this.goalCoordinator;
|
|
997
|
+
const goal = coord?.goal;
|
|
998
|
+
return Boolean(coord &&
|
|
999
|
+
goal &&
|
|
1000
|
+
coord.boundSessionId === request.sessionId &&
|
|
1001
|
+
coord.generation === request.generation &&
|
|
1002
|
+
goal.goalId === request.goal.goalId &&
|
|
1003
|
+
goal.revision === request.goal.revision &&
|
|
1004
|
+
goal.status === 'active');
|
|
1005
|
+
}
|
|
1006
|
+
verificationSummary(loopStats) {
|
|
1007
|
+
if (!loopStats)
|
|
1008
|
+
return 'No loop statistics were recorded.';
|
|
1009
|
+
const passed = loopStats.verificationPassedCommands ?? [];
|
|
1010
|
+
const failed = loopStats.verificationFailedCommands ?? [];
|
|
1011
|
+
const missing = loopStats.verificationMissingCommands ?? [];
|
|
1012
|
+
return [
|
|
1013
|
+
`finish=${loopStats.finishReason}`,
|
|
1014
|
+
passed.length ? `passed=${passed.join(', ')}` : '',
|
|
1015
|
+
failed.length ? `failed=${failed.join(', ')}` : '',
|
|
1016
|
+
missing.length ? `missing=${missing.join(', ')}` : '',
|
|
1017
|
+
]
|
|
1018
|
+
.filter(Boolean)
|
|
1019
|
+
.join('; ');
|
|
1020
|
+
}
|
|
1021
|
+
captureGoalEvidence(event) {
|
|
1022
|
+
if (event.type !== 'tool_finished')
|
|
1023
|
+
return;
|
|
1024
|
+
const context = (0, tools_1.currentGoalToolContext)();
|
|
1025
|
+
const goal = context?.coordinator.goal;
|
|
1026
|
+
if (!context || !goal || goal.status !== 'active')
|
|
1027
|
+
return;
|
|
1028
|
+
const capturedAt = Date.now();
|
|
1029
|
+
const claimedAssertion = event.event.externalAssertion;
|
|
1030
|
+
const assertion = claimedAssertion &&
|
|
1031
|
+
(0, external_assertion_1.externalAssertionMatchesInvocation)({
|
|
1032
|
+
assertion: claimedAssertion,
|
|
1033
|
+
name: event.event.name,
|
|
1034
|
+
args: event.event.args,
|
|
1035
|
+
success: event.event.success,
|
|
1036
|
+
skipped: event.event.skipped,
|
|
1037
|
+
now: capturedAt,
|
|
1038
|
+
})
|
|
1039
|
+
? claimedAssertion
|
|
1040
|
+
: undefined;
|
|
1041
|
+
const kind = claimedAssertion
|
|
1042
|
+
? 'external'
|
|
1043
|
+
: (0, evidence_1.classifyGoalEvidenceKind)(event.event.name, event.event.args);
|
|
1044
|
+
if (!kind)
|
|
1045
|
+
return;
|
|
1046
|
+
const classifiedResult = (0, evidence_1.classifyGoalEvidenceResult)({
|
|
1047
|
+
kind,
|
|
1048
|
+
success: event.event.success,
|
|
1049
|
+
skipped: event.event.skipped,
|
|
1050
|
+
summary: event.event.summary,
|
|
1051
|
+
error: event.event.error,
|
|
1052
|
+
});
|
|
1053
|
+
// A successful transport plus display text is never sufficient proof of
|
|
1054
|
+
// an external mutation/state. Only runtime-derived typed assertions may
|
|
1055
|
+
// promote successful external tool output to passed evidence. Explicit
|
|
1056
|
+
// tool failures remain failed so the ledger still records negative proof.
|
|
1057
|
+
const result = assertion
|
|
1058
|
+
? assertion.status
|
|
1059
|
+
: kind === 'external' && classifiedResult === 'passed'
|
|
1060
|
+
? 'inconclusive'
|
|
1061
|
+
: classifiedResult;
|
|
1062
|
+
const assertionSubject = assertion
|
|
1063
|
+
? [
|
|
1064
|
+
`external ${assertion.action} ${assertion.status}`,
|
|
1065
|
+
`provider=${assertion.provider}`,
|
|
1066
|
+
`target=${assertion.target}`,
|
|
1067
|
+
assertion.observedValue ? `observed=${assertion.observedValue}` : '',
|
|
1068
|
+
]
|
|
1069
|
+
.filter(Boolean)
|
|
1070
|
+
.join(' ')
|
|
1071
|
+
: undefined;
|
|
1072
|
+
const record = {
|
|
1073
|
+
id: `evidence:${(0, crypto_1.randomUUID)()}`,
|
|
1074
|
+
goalId: goal.goalId,
|
|
1075
|
+
goalRevision: goal.revision,
|
|
1076
|
+
objectiveRevision: goal.contract?.objectiveRevision ?? 0,
|
|
1077
|
+
turnId: context.turnId,
|
|
1078
|
+
kind,
|
|
1079
|
+
subject: (0, redaction_1.redactTraceText)(assertionSubject || event.event.summary || event.event.error || event.event.name).slice(0, 512),
|
|
1080
|
+
result,
|
|
1081
|
+
sourceRef: `tool:${event.event.callId}:${event.event.name}`,
|
|
1082
|
+
capturedAt,
|
|
1083
|
+
workspaceFingerprint: (0, workspace_state_1.captureWorkspaceFingerprint)(this.activeProjectPath()),
|
|
1084
|
+
expiresAt: kind === 'external' ? capturedAt + 5 * 60000 : undefined,
|
|
1085
|
+
...(assertion ? { externalAssertion: assertion } : {}),
|
|
1086
|
+
redacted: true,
|
|
1087
|
+
};
|
|
1088
|
+
context.evidenceRecords.push(record);
|
|
1089
|
+
this.emitGoalEvent({
|
|
1090
|
+
type: 'goal_evidence_recorded',
|
|
1091
|
+
goalId: goal.goalId,
|
|
1092
|
+
evidence: {
|
|
1093
|
+
id: record.id,
|
|
1094
|
+
kind: record.kind,
|
|
1095
|
+
result: record.result,
|
|
1096
|
+
subject: record.subject,
|
|
1097
|
+
},
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
restoreGoalForSession(sessionId, projectPath) {
|
|
1101
|
+
// A continuation queued by the previously active session must not survive a
|
|
1102
|
+
// session switch. It captured the old coordinator and would otherwise be
|
|
1103
|
+
// able to start after /resume has rebound the runtime.
|
|
1104
|
+
this.continuationScheduleEpoch += 1;
|
|
1105
|
+
this.goalCoordinator = null;
|
|
1106
|
+
this.goalCoordinatorSessionId = null;
|
|
1107
|
+
const coord = this.bindGoalCoordinator(sessionId, true, projectPath);
|
|
1108
|
+
const snapshot = coord?.snapshot();
|
|
1109
|
+
if (snapshot)
|
|
1110
|
+
this.emitGoalEvent({ type: 'goal_restored', goal: snapshot });
|
|
1111
|
+
}
|
|
287
1112
|
async requestToolPermission(request) {
|
|
288
1113
|
if (request.abortSignal?.aborted || this.stopping)
|
|
289
1114
|
return false;
|
|
@@ -313,46 +1138,146 @@ class AgentRuntimeController {
|
|
|
313
1138
|
const coord = this.goalCoordinator;
|
|
314
1139
|
if (!coord)
|
|
315
1140
|
return { type: 'empty' };
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!obj)
|
|
1141
|
+
let executionRevoked = false;
|
|
1142
|
+
const goalIdBeforeMutation = coord.goal?.goalId;
|
|
1143
|
+
try {
|
|
1144
|
+
switch (input.action) {
|
|
1145
|
+
case 'show':
|
|
1146
|
+
// Just shows status — already handled by the renderer format function.
|
|
323
1147
|
return { type: 'empty' };
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
coord.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return { type: 'interrupted' };
|
|
348
|
-
case 'clear':
|
|
349
|
-
if (input.payload?.confirmed) {
|
|
350
|
-
coord.clear();
|
|
1148
|
+
case 'create': {
|
|
1149
|
+
const obj = input.payload?.objective;
|
|
1150
|
+
if (!obj)
|
|
1151
|
+
return { type: 'empty' };
|
|
1152
|
+
const result = coord.create(obj);
|
|
1153
|
+
return { type: result.ok ? 'interrupted' : 'empty' };
|
|
1154
|
+
}
|
|
1155
|
+
case 'pause':
|
|
1156
|
+
if (coord.isActive) {
|
|
1157
|
+
this.abortGoalOwnedExecution();
|
|
1158
|
+
executionRevoked = true;
|
|
1159
|
+
}
|
|
1160
|
+
return { type: coord.pause() ? 'interrupted' : 'empty' };
|
|
1161
|
+
case 'resume':
|
|
1162
|
+
return { type: coord.resume() ? 'interrupted' : 'empty' };
|
|
1163
|
+
case 'confirm':
|
|
1164
|
+
coord.confirmCriterion(input.payload?.criterionId ?? '');
|
|
1165
|
+
return { type: 'interrupted' };
|
|
1166
|
+
case 'edit': {
|
|
1167
|
+
const obj = input.payload?.objective;
|
|
1168
|
+
if (obj)
|
|
1169
|
+
coord.edit(obj);
|
|
1170
|
+
return { type: 'interrupted' };
|
|
351
1171
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1172
|
+
case 'replace': {
|
|
1173
|
+
const obj = input.payload?.objective;
|
|
1174
|
+
if (obj && !coord.replace(obj)) {
|
|
1175
|
+
const message = this.failClosedGoalMutation(coord, input.action, new Error('Target could not be replaced.'), executionRevoked, goalIdBeforeMutation);
|
|
1176
|
+
this.emitGoalMutationError(message);
|
|
1177
|
+
}
|
|
1178
|
+
return { type: 'interrupted' };
|
|
1179
|
+
}
|
|
1180
|
+
case 'set_budget':
|
|
1181
|
+
coord.setBudget(input.payload?.tokenBudget ?? null);
|
|
1182
|
+
return { type: 'interrupted' };
|
|
1183
|
+
case 'clear':
|
|
1184
|
+
if (input.payload?.confirmed && coord.goal !== null) {
|
|
1185
|
+
this.abortGoalOwnedExecution();
|
|
1186
|
+
executionRevoked = true;
|
|
1187
|
+
if (!coord.clear()) {
|
|
1188
|
+
const message = this.failClosedGoalMutation(coord, input.action, new Error('Target clear did not remove the active Goal.'), executionRevoked, goalIdBeforeMutation);
|
|
1189
|
+
this.emitGoalMutationError(message);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return { type: 'interrupted' };
|
|
1193
|
+
default:
|
|
1194
|
+
return { type: 'empty' };
|
|
1195
|
+
}
|
|
355
1196
|
}
|
|
1197
|
+
catch (cause) {
|
|
1198
|
+
const message = this.failClosedGoalMutation(coord, input.action, cause, executionRevoked, goalIdBeforeMutation);
|
|
1199
|
+
this.emitGoalMutationError(message);
|
|
1200
|
+
return { type: 'interrupted' };
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
emitGoalMutationError(message) {
|
|
1204
|
+
this.emitAppend({
|
|
1205
|
+
role: 'error',
|
|
1206
|
+
title: 'target',
|
|
1207
|
+
content: message,
|
|
1208
|
+
errorLayer: 'runtime',
|
|
1209
|
+
});
|
|
1210
|
+
this.emitStatus(message);
|
|
1211
|
+
}
|
|
1212
|
+
ensureGoalCoordinator(ensureSession) {
|
|
1213
|
+
if (ensureSession) {
|
|
1214
|
+
this.options.runtime.ensureSession();
|
|
1215
|
+
}
|
|
1216
|
+
const session = this.options.runtime.getSession();
|
|
1217
|
+
const sessionId = session?.id;
|
|
1218
|
+
if (!sessionId)
|
|
1219
|
+
return this.goalCoordinator;
|
|
1220
|
+
if (this.goalCoordinator && this.goalCoordinatorSessionId === sessionId) {
|
|
1221
|
+
return this.goalCoordinator;
|
|
1222
|
+
}
|
|
1223
|
+
return this.bindGoalCoordinator(sessionId, true, session.projectPath || this.options.runtime.cwd);
|
|
1224
|
+
}
|
|
1225
|
+
bindGoalCoordinator(sessionId, recoverActive, projectPath) {
|
|
1226
|
+
const { GoalCoordinator } = require('./goals/coordinator');
|
|
1227
|
+
const coord = new GoalCoordinator(projectPath, sessionId);
|
|
1228
|
+
coord.load(recoverActive);
|
|
1229
|
+
if (coord.lastLoadIssue) {
|
|
1230
|
+
this.emitAppend({
|
|
1231
|
+
role: 'error',
|
|
1232
|
+
title: 'target recovery',
|
|
1233
|
+
content: `${coord.lastLoadIssue.code}: ${coord.lastLoadIssue.message}. Run orion doctor for recovery guidance.`,
|
|
1234
|
+
errorLayer: 'runtime',
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
this.goalCoordinator = coord;
|
|
1238
|
+
this.goalCoordinatorSessionId = sessionId;
|
|
1239
|
+
if ('setGoalCoordinator' in this.runner) {
|
|
1240
|
+
this.runner.setGoalCoordinator(coord);
|
|
1241
|
+
}
|
|
1242
|
+
return coord;
|
|
1243
|
+
}
|
|
1244
|
+
activeProjectPath() {
|
|
1245
|
+
return this.options.runtime.getSession()?.projectPath || this.options.runtime.cwd;
|
|
1246
|
+
}
|
|
1247
|
+
formatTargetStatus(coord) {
|
|
1248
|
+
const goal = coord.goal;
|
|
1249
|
+
if (!goal) {
|
|
1250
|
+
return 'Target: no active goal. Use /target <objective> to create one.';
|
|
1251
|
+
}
|
|
1252
|
+
const objective = goal.objective.length > 60 ? `${goal.objective.slice(0, 57)}...` : goal.objective;
|
|
1253
|
+
const tokens = goal.tokensUsed >= 1000 ? `${(goal.tokensUsed / 1000).toFixed(1)}K` : String(goal.tokensUsed);
|
|
1254
|
+
const lines = [
|
|
1255
|
+
`Target: [${goal.status}] ${objective} | ${goal.continuationCount} turns | ${tokens} tokens${goal.tokenBudget ? ` | budget ${tokens}/${goal.tokenBudget}` : ''}`,
|
|
1256
|
+
];
|
|
1257
|
+
const contract = goal.contract;
|
|
1258
|
+
const plan = contract?.planSnapshot;
|
|
1259
|
+
if (plan) {
|
|
1260
|
+
lines.push(`Plan: r${plan.revision} ${plan.phase}${plan.nextAction ? ` | next: ${plan.nextAction}` : ''}`);
|
|
1261
|
+
}
|
|
1262
|
+
if (contract?.successCriteria.length) {
|
|
1263
|
+
const passed = contract.successCriteria.filter(criterion => criterion.status === 'passed').length;
|
|
1264
|
+
const failed = contract.successCriteria.filter(criterion => criterion.status === 'failed').length;
|
|
1265
|
+
const stale = contract.successCriteria.filter(criterion => criterion.status === 'stale').length;
|
|
1266
|
+
const pending = contract.successCriteria.length - passed - failed - stale;
|
|
1267
|
+
lines.push(`Criteria: ${pending} pending | ${passed}/${contract.successCriteria.length} passed | ${failed} failed | ${stale} stale`);
|
|
1268
|
+
}
|
|
1269
|
+
if (goal.completionAudit?.remainingRequirements.length) {
|
|
1270
|
+
lines.push(`Audit: ${goal.completionAudit.remainingRequirements.slice(0, 3).join(' | ')}`);
|
|
1271
|
+
}
|
|
1272
|
+
if (goal.stopReason)
|
|
1273
|
+
lines.push(`Resume: ${goal.stopReason.message}`);
|
|
1274
|
+
const recentEvidence = (goal.evidenceLedger ?? []).slice(-3);
|
|
1275
|
+
if (recentEvidence.length > 0) {
|
|
1276
|
+
lines.push(`Evidence: ${recentEvidence
|
|
1277
|
+
.map(item => `${item.id} ${item.kind}/${item.result} ${item.subject}`)
|
|
1278
|
+
.join(' | ')}`);
|
|
1279
|
+
}
|
|
1280
|
+
return lines.join('\n');
|
|
356
1281
|
}
|
|
357
1282
|
recordPermissionDecision(requestId, approved) {
|
|
358
1283
|
const resolve = this.pendingPermissions.get(requestId);
|
|
@@ -383,7 +1308,9 @@ class AgentRuntimeController {
|
|
|
383
1308
|
};
|
|
384
1309
|
chatOptions.uiCapabilities = uiCapabilities;
|
|
385
1310
|
chatOptions.uiRenderer = chatOptions.uiRenderer ?? resolvedRenderer;
|
|
386
|
-
chatOptions.onVerificationStateChange =
|
|
1311
|
+
chatOptions.onVerificationStateChange =
|
|
1312
|
+
chatOptions.onVerificationStateChange ??
|
|
1313
|
+
(state => this.turnController.setVerificationState(state));
|
|
387
1314
|
if (this.options.useRuntimeToolPermissions && !chatOptions.confirmToolUse) {
|
|
388
1315
|
chatOptions.confirmToolUse = request => this.requestToolPermission(request);
|
|
389
1316
|
}
|
|
@@ -410,6 +1337,46 @@ class AgentRuntimeController {
|
|
|
410
1337
|
});
|
|
411
1338
|
};
|
|
412
1339
|
}
|
|
1340
|
+
const configuredPreflight = chatOptions.beforeProviderRequest;
|
|
1341
|
+
chatOptions.beforeProviderRequest = async (context) => {
|
|
1342
|
+
if (configuredPreflight) {
|
|
1343
|
+
const configuredDecision = await configuredPreflight(context);
|
|
1344
|
+
if (!configuredDecision.available)
|
|
1345
|
+
return configuredDecision;
|
|
1346
|
+
}
|
|
1347
|
+
const coord = this.goalCoordinator;
|
|
1348
|
+
const goal = coord?.goal;
|
|
1349
|
+
if (!coord || !goal || goal.status !== 'active' || !goal.tokenBudget) {
|
|
1350
|
+
return { available: true };
|
|
1351
|
+
}
|
|
1352
|
+
const projectedTokens = Math.max(1, context.estimatedPromptTokens, goal.lastTurn?.totalTokens ?? 0);
|
|
1353
|
+
const decision = (0, accounting_1.budgetPreflight)(goal.tokensUsed + this.goalProviderReservedTokens, goal.tokenBudget, projectedTokens);
|
|
1354
|
+
if (!decision.available) {
|
|
1355
|
+
try {
|
|
1356
|
+
coord.limitBudget(decision.reason ?? 'Token budget unavailable for provider request.');
|
|
1357
|
+
}
|
|
1358
|
+
catch (cause) {
|
|
1359
|
+
const message = this.failClosedGoalMutation(coord, 'provider_budget_stop', cause, false, goal.goalId);
|
|
1360
|
+
this.emitGoalMutationError(message);
|
|
1361
|
+
return { available: false, reason: message };
|
|
1362
|
+
}
|
|
1363
|
+
const snapshot = coord.snapshot();
|
|
1364
|
+
if (snapshot) {
|
|
1365
|
+
this.emitGoalEvent({
|
|
1366
|
+
type: 'goal_updated',
|
|
1367
|
+
goal: snapshot,
|
|
1368
|
+
reason: `provider request ${context.operation} attempt ${context.attempt} rejected by token budget`,
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
return decision;
|
|
1372
|
+
}
|
|
1373
|
+
// Reserve the projected cost before the network call. Failed retries and
|
|
1374
|
+
// fallbacks keep their reservation because providers may charge attempts
|
|
1375
|
+
// that fail before returning usage. Actual successful usage is persisted
|
|
1376
|
+
// at turn finalization; reservations reset only when the next root turn starts.
|
|
1377
|
+
this.goalProviderReservedTokens += projectedTokens;
|
|
1378
|
+
return decision;
|
|
1379
|
+
};
|
|
413
1380
|
return Object.keys(chatOptions).length > 0 ? chatOptions : undefined;
|
|
414
1381
|
}
|
|
415
1382
|
}
|