sortie-dogs 0.13.21 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -15
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/cli/codex-mission.js +6 -1
- package/dist/codex/app-server.d.ts +8 -0
- package/dist/codex/app-server.js +37 -14
- package/dist/codex/mission-session.d.ts +7 -0
- package/dist/codex/mission-session.js +29 -4
- package/dist/core/goal-bound.d.ts +6 -0
- package/dist/core/initialize.js +5 -3
- package/dist/core/operator-mission.d.ts +5 -1
- package/dist/core/operator-mission.js +5 -0
- package/dist/core/operator-runtime.d.ts +8 -0
- package/dist/core/operator-runtime.js +33 -0
- package/dist/plugin/index.js +74 -17
- package/dist/plugin/mission-review.d.ts +2 -0
- package/dist/plugin/mission-review.js +10 -0
- package/dist/plugin/profiled.js +94 -8
- package/dist/plugin/runtime-bridge.d.ts +12 -0
- package/dist/plugin/tool-schema.js +5 -3
- package/dist/plugin/unit-usage.d.ts +1 -1
- package/dist/plugin/unit-usage.js +5 -3
- package/dist/runtime-mission-assets.js +26 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,13 +19,13 @@ evidence-backed result, not more agents for their own sake.
|
|
|
19
19
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
22
|
-
**Current release: [v0.
|
|
23
|
-
· [Release notes](docs/release-v0.
|
|
22
|
+
**Current release: [v0.14.0](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.14.0)**
|
|
23
|
+
· [Release notes](docs/release-v0.14.0.md) · [Execution vision](docs/v0.14.0-execution-vision.md)
|
|
24
24
|
|
|
25
25
|
[Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
|
|
26
26
|
[Measured results](#measured-results) · [Configuration](#configuration) · [Documentation](#documentation)
|
|
27
27
|
|
|
28
|
-
> **Beta:** the
|
|
28
|
+
> **Beta:** the package is still stabilizing before 1.0. Both host integrations are
|
|
29
29
|
> implemented; Codex Missions have completed implementation, validation, correction and acceptance.
|
|
30
30
|
> [Host-specific execution boundaries](docs/codex.md#native-permissions-and-approval) remain explicit.
|
|
31
31
|
|
|
@@ -35,9 +35,9 @@ evidence-backed result, not more agents for their own sake.
|
|
|
35
35
|
|
|
36
36
|
- **Autonomy:** retain the original request across delegation, correction, compaction and restart.
|
|
37
37
|
Finish in-request work through the existing host authority instead of routine approval round trips.
|
|
38
|
-
- **Efficiency:**
|
|
39
|
-
Coordinator
|
|
40
|
-
|
|
38
|
+
- **Efficiency:** Operator recommends an initial author from the instruction and known context.
|
|
39
|
+
Coordinator can implement substantial work directly or delegate remaining work as execution makes
|
|
40
|
+
that useful. Continue the same unit with its edits, valid evidence and cumulative budget.
|
|
41
41
|
- **Visibility:** show real commands, exits, timing, model routes, review disposition and final
|
|
42
42
|
acceptance. Unknown execution or cost stays unknown, not an invented success or zero.
|
|
43
43
|
- **Quality:** meaningful formal checks and risk-based review establish completion, not model prose.
|
|
@@ -57,7 +57,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
|
|
|
57
57
|
available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
-
npm install --save-dev sortie-dogs@0.
|
|
60
|
+
npm install --save-dev sortie-dogs@0.14.0
|
|
61
61
|
npx --no-install sortie-dogs codex init .
|
|
62
62
|
```
|
|
63
63
|
|
|
@@ -86,7 +86,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
|
|
|
86
86
|
Use **OpenCode V2**. In the target project:
|
|
87
87
|
|
|
88
88
|
```sh
|
|
89
|
-
npm install --save-dev sortie-dogs@0.
|
|
89
|
+
npm install --save-dev sortie-dogs@0.14.0
|
|
90
90
|
npx --no-install sortie-dogs init .
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -108,14 +108,16 @@ v0.10 is installed. [OpenCode setup and configuration](docs/configuration.md) ·
|
|
|
108
108
|
|
|
109
109
|
```text
|
|
110
110
|
Known unit: User → Operator → Worker → Validation → Review / Correction → Acceptance
|
|
111
|
-
|
|
111
|
+
Direct: User → Operator → Coordinator → Validation → Review / Correction → Acceptance
|
|
112
|
+
Handoff: Coordinator → Worker (same unit, retained edits/context/checks/budget)
|
|
112
113
|
```
|
|
113
114
|
|
|
114
|
-
1. **Operator** saves original requirements
|
|
115
|
+
1. **Operator** saves original requirements, recommends an initial executor from already-known
|
|
116
|
+
context, and owns user decisions and final acceptance. No routing-only investigation is required.
|
|
115
117
|
2. **Worker** investigates, implements, runs meaningful checks and performs requested Git delivery
|
|
116
118
|
in the same useful unit. A known unit/check can go directly to Worker without a planning handoff.
|
|
117
|
-
3. **Coordinator
|
|
118
|
-
|
|
119
|
+
3. **Coordinator** starts useful work, implements and validates directly when efficient, and
|
|
120
|
+
delegates remaining work when useful. The Operator recommendation is advisory, not an execution lock.
|
|
119
121
|
4. **Review** is initially independent for high-risk changes; low-risk skips are explicit.
|
|
120
122
|
Findings can be corrected and validated by the same Reviewer, followed by recorded author
|
|
121
123
|
self-recheck. That is not a second independent PASS.
|
|
@@ -193,7 +195,7 @@ known expense. Official local evaluation and leaderboard acceptance are separate
|
|
|
193
195
|
**8/23 (34.8%)**, versus v0.12.25's 7/23; all seven prior resolutions retained, no empty patches or
|
|
194
196
|
official evaluation errors. [Fixed conditions, stopped-run accounting and costs](docs/benchmarks/swebench-lite-history.md#v0131-dev23-2026-09-30).
|
|
195
197
|
|
|
196
|
-
All scores belong to their fixed candidates, **not v0.
|
|
198
|
+
All scores belong to their fixed candidates, **not v0.14.0**. SWE-bench remains an optional
|
|
197
199
|
measurement, not a release gate. Different budgets, models and conditions are not controlled comparisons.
|
|
198
200
|
|
|
199
201
|
## Configuration
|
|
@@ -216,7 +218,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
216
218
|
|
|
217
219
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
218
220
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
219
|
-
The current Mission asset marker is `0.
|
|
221
|
+
The current Mission asset marker is `0.14.0-autonomous-routing-v1`; the unchanged Codex skill marker is
|
|
220
222
|
`0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
|
|
221
223
|
|
|
222
224
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -232,7 +234,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
232
234
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
233
235
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
234
236
|
[Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
|
|
235
|
-
- **Releases:** [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
237
|
+
- **Releases:** [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
236
238
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
237
239
|
|
|
238
240
|
## Community
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export declare const V010_RUNTIME_ASSET_VERSION = "0.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.14.0-autonomous-routing-v1";
|
|
7
7
|
export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
8
8
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export const V010_RUNTIME_ASSET_VERSION = "0.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.14.0-autonomous-routing-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
@@ -47,7 +47,7 @@ export async function runCodexMissionCommand(argv) {
|
|
|
47
47
|
const result = await adapter.run(prompt);
|
|
48
48
|
process.stdout.write(`${JSON.stringify({ status: result.accepted ? "succeeded" : "incomplete", root_session_id: result.rootSessionID,
|
|
49
49
|
thread_id: result.turn.threadID, turn_id: result.turn.turnID, native_status: result.turn.status,
|
|
50
|
-
response: result.turn.finalResponse, sessions: result.sessions, cost: null })}\n`);
|
|
50
|
+
response: result.turn.finalResponse, lifecycle: result.lifecycle, sessions: result.sessions, cost: null })}\n`);
|
|
51
51
|
return signalExit ?? (result.accepted ? 0 : 1);
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
@@ -66,6 +66,11 @@ export async function runCodexMissionCommand(argv) {
|
|
|
66
66
|
}
|
|
67
67
|
/** Bounded user-facing progress; do not dump control packets or hidden reasoning. */
|
|
68
68
|
export function codexMissionProgress(event) {
|
|
69
|
+
if (event.method === "turn/completed")
|
|
70
|
+
return { thread_id: event.threadId, phase: "native-turn-terminal", turn_id: event.params.turn?.id,
|
|
71
|
+
native_status: event.params.turn?.status };
|
|
72
|
+
if (event.method === "sortie/continuation")
|
|
73
|
+
return { thread_id: event.threadId, turn_id: event.params.turnId, phase: event.params.phase, calls: event.params.calls };
|
|
69
74
|
if (event.method === "sortie/modelRoute")
|
|
70
75
|
return { thread_id: event.threadId, phase: "model-route", agent: event.params.agent, model: event.params.model,
|
|
71
76
|
effort: event.params.effort, service_tier: event.params.serviceTier };
|
|
@@ -46,6 +46,14 @@ export interface CodexTurnResult {
|
|
|
46
46
|
readonly finalResponse?: string;
|
|
47
47
|
readonly items: readonly JsonObject[];
|
|
48
48
|
readonly usage?: JsonObject;
|
|
49
|
+
/** Host tools still running at native turn completion, drained without resending them. */
|
|
50
|
+
readonly deferredToolResults?: readonly CodexDeferredToolResult[];
|
|
51
|
+
}
|
|
52
|
+
export interface CodexDeferredToolResult {
|
|
53
|
+
readonly callID: string;
|
|
54
|
+
readonly tool: string;
|
|
55
|
+
readonly success: boolean;
|
|
56
|
+
readonly output: string;
|
|
49
57
|
}
|
|
50
58
|
export interface CodexAuthenticationState {
|
|
51
59
|
readonly type: string | null;
|
package/dist/codex/app-server.js
CHANGED
|
@@ -188,7 +188,7 @@ export class CodexAppServerHost {
|
|
|
188
188
|
if (!turnID)
|
|
189
189
|
throw new CodexHostError("protocol-invalid-response", "turn/start returned no turn id.");
|
|
190
190
|
return new Promise((resolve, reject) => {
|
|
191
|
-
this.active = { threadID, turnID, items: [], observer: Promise.resolve(), onEvent: options.onEvent, resolve, reject };
|
|
191
|
+
this.active = { threadID, turnID, items: [], observer: Promise.resolve(), tools: new Set(), onEvent: options.onEvent, resolve, reject };
|
|
192
192
|
const backlog = this.notificationBacklog.splice(0);
|
|
193
193
|
void backlog.reduce((prior, event) => prior.then(() => this.notification(event.method, event.params)), Promise.resolve())
|
|
194
194
|
.catch(error => { this.active?.reject(error); this.active = undefined; });
|
|
@@ -289,20 +289,34 @@ export class CodexAppServerHost {
|
|
|
289
289
|
const params = object(message.params) ? message.params : {};
|
|
290
290
|
if (method === "item/tool/call") {
|
|
291
291
|
const active = this.active;
|
|
292
|
-
|
|
292
|
+
if (!active || active.completing || params.threadId !== active.threadID || params.turnId !== active.turnID ||
|
|
293
|
+
(params.namespace != null) || typeof params.callId !== "string" || typeof params.tool !== "string" || !this.options.dynamicTool) {
|
|
294
|
+
this.transport.send({ id: message.id, result: { contentItems: [{ type: "inputText",
|
|
295
|
+
text: "Dynamic tool call does not belong to an active Sortie turn." }], success: false } });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const call = { threadId: active.threadID, turnId: active.turnID, callId: params.callId, tool: params.tool, arguments: params.arguments };
|
|
299
|
+
const pending = Promise.resolve().then(async () => {
|
|
300
|
+
let output, success;
|
|
301
|
+
try {
|
|
302
|
+
output = await this.options.dynamicTool(call);
|
|
303
|
+
success = true;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
output = error instanceof Error ? error.message : "Dynamic tool failed.";
|
|
307
|
+
success = false;
|
|
308
|
+
}
|
|
309
|
+
if (!this.closed && !this.fault)
|
|
310
|
+
this.transport.send({ id: message.id, result: { contentItems: [{ type: "inputText", text: output }], success } });
|
|
311
|
+
return { callID: call.callId, tool: call.tool, success, output };
|
|
312
|
+
});
|
|
313
|
+
active.tools.add(pending);
|
|
293
314
|
try {
|
|
294
|
-
|
|
295
|
-
(params.namespace != null) || typeof params.callId !== "string" || typeof params.tool !== "string" || !this.options.dynamicTool)
|
|
296
|
-
throw new Error("Dynamic tool call does not belong to an active Sortie turn.");
|
|
297
|
-
const output = await this.options.dynamicTool({ threadId: active.threadID, turnId: active.turnID,
|
|
298
|
-
callId: params.callId, tool: params.tool, arguments: params.arguments });
|
|
299
|
-
result = { contentItems: [{ type: "inputText", text: output }], success: true };
|
|
315
|
+
await pending;
|
|
300
316
|
}
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
finally {
|
|
318
|
+
active.tools.delete(pending);
|
|
303
319
|
}
|
|
304
|
-
if (!this.closed)
|
|
305
|
-
this.transport.send({ id: message.id, result });
|
|
306
320
|
return;
|
|
307
321
|
}
|
|
308
322
|
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") {
|
|
@@ -374,14 +388,23 @@ export class CodexAppServerHost {
|
|
|
374
388
|
// requests such as interrupt(), whose response must be read by this pump.
|
|
375
389
|
// Keep the completing turn active until observers settle so close() can
|
|
376
390
|
// reject it and a later turn cannot receive an interrupt meant for it.
|
|
377
|
-
|
|
391
|
+
const deferred = status === "completed" ? [...active.tools] : [];
|
|
392
|
+
// Native response completion is not host execution completion. Keep pumping
|
|
393
|
+
// command responses and child Tasks; explicit close/transport loss can still
|
|
394
|
+
// reject this turn. Failed/interrupted native turns never auto-continue.
|
|
395
|
+
void Promise.all([active.observer, Promise.all(deferred)]).then(([, tools]) => {
|
|
378
396
|
if (this.active !== active)
|
|
379
397
|
return;
|
|
380
398
|
this.active = undefined;
|
|
381
399
|
if (active.observerError)
|
|
382
400
|
active.reject(new CodexHostError("request-failed", "Codex turn event observer failed.", active.observerError));
|
|
383
401
|
else
|
|
384
|
-
active.resolve(result);
|
|
402
|
+
active.resolve({ ...result, ...(tools.length ? { deferredToolResults: tools } : {}) });
|
|
403
|
+
}).catch(error => {
|
|
404
|
+
if (this.active !== active)
|
|
405
|
+
return;
|
|
406
|
+
this.active = undefined;
|
|
407
|
+
active.reject(error);
|
|
385
408
|
});
|
|
386
409
|
}
|
|
387
410
|
}
|
|
@@ -67,6 +67,12 @@ export declare class CodexMissionSession {
|
|
|
67
67
|
rootSessionID: string;
|
|
68
68
|
turn: CodexTurnResult;
|
|
69
69
|
accepted: boolean;
|
|
70
|
+
lifecycle: {
|
|
71
|
+
root_turn_terminal: boolean;
|
|
72
|
+
owned_tasks_settled: boolean;
|
|
73
|
+
mission_phase: string | null;
|
|
74
|
+
mission_accepted: boolean;
|
|
75
|
+
};
|
|
70
76
|
sessions: readonly JsonObject[];
|
|
71
77
|
}>;
|
|
72
78
|
private stopHosts;
|
|
@@ -85,6 +91,7 @@ export declare class CodexMissionSession {
|
|
|
85
91
|
private restoreSession;
|
|
86
92
|
private tools;
|
|
87
93
|
private prompt;
|
|
94
|
+
private promptTurn;
|
|
88
95
|
private execute;
|
|
89
96
|
}
|
|
90
97
|
export {};
|
|
@@ -72,8 +72,11 @@ export class CodexMissionSession {
|
|
|
72
72
|
const previous = await new OperatorMissionRuntime(this.directory, profile).read(session.id);
|
|
73
73
|
const turn = await this.prompt(session, prompt);
|
|
74
74
|
const mission = await new OperatorMissionRuntime(this.directory, profile).read(session.id);
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const ownedTasksSettled = [...this.sessions.values()].every(item => !["running", "unknown"].includes(item.outcome));
|
|
76
|
+
const accepted = turn.status === "completed" && ownedTasksSettled && mission?.phase === "completed" &&
|
|
77
|
+
(previous?.phase !== "completed" || previous.id !== mission.id);
|
|
78
|
+
return { rootSessionID: session.id, turn, accepted, lifecycle: { root_turn_terminal: ["completed", "failed", "interrupted", "cancelled"].includes(turn.status),
|
|
79
|
+
owned_tasks_settled: ownedTasksSettled, mission_phase: mission?.phase ?? null, mission_accepted: accepted }, sessions: [...this.sessions.values()].map(item => ({ thread_id: item.id, agent: item.agent, model: item.model, usage: item.usage ?? null, usage_scope: "native-thread-cumulative", cost: null })) };
|
|
77
80
|
}
|
|
78
81
|
async stopHosts() {
|
|
79
82
|
this.closed = true;
|
|
@@ -459,11 +462,33 @@ export class CodexMissionSession {
|
|
|
459
462
|
return tools;
|
|
460
463
|
}
|
|
461
464
|
async prompt(session, prompt, onDispatch) {
|
|
465
|
+
let result = await this.promptTurn(session, prompt, onDispatch);
|
|
466
|
+
while (result.status === "completed" && result.deferredToolResults?.length) {
|
|
467
|
+
if (this.closed)
|
|
468
|
+
throw new Error("Codex Mission adapter is closed.");
|
|
469
|
+
const mission = await new OperatorMissionRuntime(this.directory, profile).read(this.rootOf(session));
|
|
470
|
+
if (!mission || ["completed", "cancelled"].includes(mission.phase))
|
|
471
|
+
break;
|
|
472
|
+
await this.claimOwner(this.rootOf(session));
|
|
473
|
+
await this.options.onEvent?.({ method: "sortie/continuation", threadId: session.id,
|
|
474
|
+
params: { turnId: result.turnID, phase: "owned-tools-settled", calls: result.deferredToolResults.map(item => ({ callId: item.callID, tool: item.tool, success: item.success })) } });
|
|
475
|
+
result = await this.promptTurn(session, "The previous native response ended while existing host tools were still running. Those exact calls have now returned:\n" +
|
|
476
|
+
JSON.stringify(result.deferredToolResults) +
|
|
477
|
+
"\nContinue this same Mission from these results through the remaining validation, delivery, review and root acceptance. " +
|
|
478
|
+
"Do not resend the original request or redispatch the completed Tasks. Preserve failures, ownership and cumulative budget; use existing Mission status for the next action.", undefined, true);
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
async promptTurn(session, prompt, onDispatch, continuation = false) {
|
|
462
483
|
const startedAt = Date.now();
|
|
463
484
|
const messageID = randomUUID();
|
|
464
485
|
const message = { id: messageID, agent: session.agent, model: { ...session.model } };
|
|
465
|
-
const parts = [{ type: "text", text: prompt }];
|
|
466
|
-
|
|
486
|
+
const parts = [{ type: "text", text: prompt, ...(continuation ? { synthetic: true } : {}) }];
|
|
487
|
+
// Only this host's drained results can continue an already-admitted execution.
|
|
488
|
+
// Reclaiming that Task with the result prompt rejects Workers' exact handoff;
|
|
489
|
+
// treating it as a real root request also grants spurious user-turn authority.
|
|
490
|
+
if (!continuation)
|
|
491
|
+
await this.hooks["chat.message"]?.({ sessionID: session.id, messageID, agent: session.agent }, { message, parts });
|
|
467
492
|
// Shared Task routing may select a model; explicit adapter overrides still govern every native turn.
|
|
468
493
|
if (message.model.providerID !== "openai")
|
|
469
494
|
throw new Error("Codex Mission cannot use a non-OpenAI model route.");
|
|
@@ -204,6 +204,12 @@ export type GoalFlightEvent = (GoalEventBase & {
|
|
|
204
204
|
readonly cost_usd: number | null;
|
|
205
205
|
readonly native_session_id?: string;
|
|
206
206
|
readonly native_started_at?: string;
|
|
207
|
+
/** Multiple actual authors of one handed-off unit; missing usage stays unknown. */
|
|
208
|
+
readonly native_usage_segments?: readonly {
|
|
209
|
+
readonly sessionID: string;
|
|
210
|
+
readonly startedAt: string;
|
|
211
|
+
readonly endedAt?: string;
|
|
212
|
+
}[];
|
|
207
213
|
/** Host-native validation identity retained in this same goal ledger; never model-authored evidence. */
|
|
208
214
|
readonly native_validation_observations?: readonly {
|
|
209
215
|
readonly thread_id: string;
|
package/dist/core/initialize.js
CHANGED
|
@@ -136,10 +136,12 @@ function classifyVersionTransition(installedValue, currentValue, v010Restore = f
|
|
|
136
136
|
const adjacentPreOneLine = installed.major === 0 && current.major === 0 &&
|
|
137
137
|
current.minor === installed.minor + 1;
|
|
138
138
|
// v0.11 used a different runtime. The v010 asset migration restores the 0.10
|
|
139
|
-
// preview into the 0.12
|
|
139
|
+
// preview into the 0.12–0.14 Mission line without selecting a v0.11 execution path.
|
|
140
140
|
const restoredPreview = v010Restore && installed.major === 0 && installed.minor === 10 &&
|
|
141
|
-
current.major === 0 &&
|
|
142
|
-
|
|
141
|
+
current.major === 0 && [12, 13, 14].includes(current.minor);
|
|
142
|
+
const retainedMission = v010Restore && installed.major === 0 && installed.minor === 12 &&
|
|
143
|
+
current.major === 0 && current.minor === 14;
|
|
144
|
+
return sameLine || adjacentPreOneLine || restoredPreview || retainedMission ? "compatible-update" : "incompatible";
|
|
143
145
|
}
|
|
144
146
|
async function metadata(path) {
|
|
145
147
|
try {
|
|
@@ -62,7 +62,7 @@ export interface MissionAttempt {
|
|
|
62
62
|
/** Fingerprint of the settled scoped candidate against which a later Rescue is proposed. */
|
|
63
63
|
candidateID?: string;
|
|
64
64
|
kind: "implementation" | "normal_remediation" | "astra_rescue" | "reviewer_correction" | "direct_execution";
|
|
65
|
-
status: "pending" | "dispatched" | "succeeded" | "failed" | "cancelled" | "unconfirmed";
|
|
65
|
+
status: "pending" | "dispatched" | "succeeded" | "failed" | "cancelled" | "unconfirmed" | "handed-off";
|
|
66
66
|
callID?: string;
|
|
67
67
|
childSessionID?: string;
|
|
68
68
|
dispatchFingerprint?: string;
|
|
@@ -217,6 +217,10 @@ export interface OperatorMission {
|
|
|
217
217
|
context?: MissionContext[];
|
|
218
218
|
/** Shared public context/working notes; observations, never extra requirements or acceptance. */
|
|
219
219
|
continuity?: MissionContinuity;
|
|
220
|
+
/** Operator recommendation from already-known context; not a requirement or execution lock. */
|
|
221
|
+
preferredExecutor?: "coordinator" | "worker";
|
|
222
|
+
/** Known unit facts passed to the Coordinator without pre-starting a Worker. */
|
|
223
|
+
suggestedUnit?: Record<string, unknown>;
|
|
220
224
|
/** Explicit continue deliveries, keyed by the original real turn. */
|
|
221
225
|
steering?: {
|
|
222
226
|
requestID: string;
|
|
@@ -328,6 +328,7 @@ export class OperatorMissionRuntime {
|
|
|
328
328
|
}
|
|
329
329
|
}));
|
|
330
330
|
return { original_requests: state.requests, requirements: state.requirements, prohibited_write: state.prohibitedWrite ?? [],
|
|
331
|
+
...(state.preferredExecutor ? { preferred_executor: state.preferredExecutor } : {}),
|
|
331
332
|
launch_conditions: state.launchConditions ?? [],
|
|
332
333
|
conversation: sources.some(source => source.session_id === state.root && source.entries > 0 && !("snapshot_available" in source)) ? [] : state.context ?? [],
|
|
333
334
|
continuity: { notes: state.continuity?.notes, sources, progress: state.progress,
|
|
@@ -709,6 +710,9 @@ export class OperatorMissionRuntime {
|
|
|
709
710
|
return [`mission_id: ${state.id}`, `project_root: ${this.projectRoot}`, "Use the user's language below for all replies and Task titles.",
|
|
710
711
|
"Own investigation, implementation, formal validation, corrections and Worker/Scout/Advisor/independent Reviewer dispatch.",
|
|
711
712
|
"Use plan_units with executor=self for direct work in this session, or delegate promptly when useful. finish_direct_unit records native checks without a Worker handoff. No proposal/approval phase; root alone accepts completion.",
|
|
713
|
+
"Start useful work from the supplied context. Reconsider the recommendation through normal execution, not a separate routing investigation. You may implement substantial work here or delegate remaining work without asking Operator again; explicit user choices still govern.",
|
|
714
|
+
...(state.preferredExecutor ? [`Operator recommendation (advisory): ${state.preferredExecutor}. This is not an immutable requirement or a model override.`] : []),
|
|
715
|
+
...(state.suggestedUnit ? [`Known unit facts (reuse or adapt during actual work): ${JSON.stringify(state.suggestedUnit)}`] : []),
|
|
712
716
|
"Escalate only a completion candidate, a user-only decision, or an extension of original requirements/budget. Unit progress is published without stopping you.",
|
|
713
717
|
"Requirements:", ...state.requirements.map(item => `${item.id}: ${item.text}`),
|
|
714
718
|
`Confirmed launch conditions (fixed limits, not consumption or remaining budget): ${JSON.stringify(state.launchConditions ?? [])}`,
|
|
@@ -932,6 +936,7 @@ export function missionPacket(mission, run, includeWorkQuantity = true) {
|
|
|
932
936
|
normal_remediation_used: unit.normalRemediationUsed ?? false,
|
|
933
937
|
...(unit.failure ? { failure: { outcome: unit.failure.outcome, exit_code: unit.failure.exitCode } } : {}),
|
|
934
938
|
...(unit.dispatchDenial ? { dispatch_denial: unit.dispatchDenial } : {}) })) } : {}),
|
|
939
|
+
...(mission.preferredExecutor ? { preferred_executor: mission.preferredExecutor } : {}),
|
|
935
940
|
next_action: mission.phase === "completed" ? "Mission completed. Report the accepted result and retained review gaps; no further dispatch or completion call is needed."
|
|
936
941
|
: mission.phase === "submitted" && mission.submission?.status === "ready"
|
|
937
942
|
? "Operator: use acceptance_summary to compare the submitted candidate with the verbatim original requests and actual evidence, inspect concrete gaps only, then complete_mission if satisfied. Do not routinely search archives or reread all source. Historical PASS is not current PASS. Report remaining evidence gaps; they are not a review PASS."
|
|
@@ -141,6 +141,12 @@ interface UnitState {
|
|
|
141
141
|
validationCwd?: Readonly<Record<string, string>>;
|
|
142
142
|
validationRegisteredAt?: Readonly<Record<string, string>>;
|
|
143
143
|
};
|
|
144
|
+
/** Actual direct author segments retained when the same unit is handed to a Worker. */
|
|
145
|
+
directHandoffs?: (NonNullable<UnitState["directExecution"]> & {
|
|
146
|
+
callID: string;
|
|
147
|
+
handedOffAt: string;
|
|
148
|
+
settledCallID?: string;
|
|
149
|
+
})[];
|
|
144
150
|
resultClass: string | null;
|
|
145
151
|
failure?: SerialDispatchSettlement["failure"];
|
|
146
152
|
normalRemediationUsed?: boolean;
|
|
@@ -372,6 +378,8 @@ export declare class OperatorRuntime {
|
|
|
372
378
|
private nextOnce;
|
|
373
379
|
admitWorker(root: string, actor: string, callID: string, args: unknown): Promise<OperatorTask>;
|
|
374
380
|
admitDirect(root: string, actor: string, callID: string): Promise<OperatorState>;
|
|
381
|
+
/** Yield an unfinished ordinary direct unit; keep its contract, reservation and source history. */
|
|
382
|
+
handoffDirect(root: string, actor: string, context: Record<string, unknown>): Promise<OperatorState>;
|
|
375
383
|
/** Continue an admitted independent Review in-place; no second native Task or prompt. */
|
|
376
384
|
admitReviewerDirect(root: string, author: string, callID: string, promptID: string): Promise<OperatorState>;
|
|
377
385
|
private admitWorkerOnce;
|
|
@@ -1454,6 +1454,37 @@ export class OperatorRuntime {
|
|
|
1454
1454
|
return state;
|
|
1455
1455
|
});
|
|
1456
1456
|
}
|
|
1457
|
+
/** Yield an unfinished ordinary direct unit; keep its contract, reservation and source history. */
|
|
1458
|
+
handoffDirect(root, actor, context) {
|
|
1459
|
+
return this.serial(root, async () => {
|
|
1460
|
+
const state = await this.required(root);
|
|
1461
|
+
const unit = state.units.find(item => item.status !== "succeeded");
|
|
1462
|
+
if (actor !== (state.operatorSessionID ?? root) || state.phase !== "running" || !unit ||
|
|
1463
|
+
unit.status !== "running" || unit.directExecution?.actor !== actor || !unit.callID ||
|
|
1464
|
+
unit.directExecution.finishedAt || unit.reviewerCorrection || unit.repairValidation || unit.terminalRescue) {
|
|
1465
|
+
throw new Error("mission-direct-handoff-unavailable");
|
|
1466
|
+
}
|
|
1467
|
+
await this.verifyControls(unit);
|
|
1468
|
+
const segment = { ...structuredClone(unit.directExecution), callID: unit.callID, handedOffAt: new Date().toISOString() };
|
|
1469
|
+
// Keep the original controls immutable. Publish continuation context and the new
|
|
1470
|
+
// Task reference together in the atomic run save, so a crash has no half-updated pins.
|
|
1471
|
+
const continuation = { actor, unit_id: unit.unit.id, at: segment.handedOffAt,
|
|
1472
|
+
instruction: "Continue existing changes and remaining action. The authoritative handoff retains the exact original requirements. Reuse current source-bound checks; inspect missing facts only. Handoff itself is not validation.",
|
|
1473
|
+
continuity: context.continuity,
|
|
1474
|
+
checks: segment.checks.map(({ callID, command, directory, exitCode, startedAt, endedAt }) => ({ callID, command, directory, exitCode, startedAt, endedAt })),
|
|
1475
|
+
validation_cwd: segment.validationCwd ?? unit.unit.validation_cwd, details_ref: this.statePath(root) };
|
|
1476
|
+
unit.task = { ...unit.task, prompt: `${unit.task.prompt.replace(/\ndirect_handoff: .+$/mu, "")}\ndirect_handoff: ${JSON.stringify(continuation)}` };
|
|
1477
|
+
(unit.directHandoffs ??= []).push(segment);
|
|
1478
|
+
delete unit.directExecution;
|
|
1479
|
+
unit.status = "pending";
|
|
1480
|
+
unit.childSessionID = null;
|
|
1481
|
+
state.units[state.units.indexOf(unit)] = { ...unit,
|
|
1482
|
+
unit: segment.validationCwd ? { ...unit.unit, validation_cwd: segment.validationCwd } : unit.unit };
|
|
1483
|
+
// Keep the original call as the reservation anchor until Worker admission replaces it.
|
|
1484
|
+
await this.save(state);
|
|
1485
|
+
return state;
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1457
1488
|
/** Continue an admitted independent Review in-place; no second native Task or prompt. */
|
|
1458
1489
|
admitReviewerDirect(root, author, callID, promptID) {
|
|
1459
1490
|
return this.serial(root, async () => {
|
|
@@ -1848,6 +1879,8 @@ export class OperatorRuntime {
|
|
|
1848
1879
|
else
|
|
1849
1880
|
delete unit.failure;
|
|
1850
1881
|
unit.status = result.disposition;
|
|
1882
|
+
for (const segment of unit.directHandoffs ?? [])
|
|
1883
|
+
segment.settledCallID ??= result.callID;
|
|
1851
1884
|
if (unit.directExecution)
|
|
1852
1885
|
unit.directExecution.finishedAt = new Date().toISOString();
|
|
1853
1886
|
if (unit.terminalRescue) {
|
package/dist/plugin/index.js
CHANGED
|
@@ -1553,17 +1553,26 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1553
1553
|
});
|
|
1554
1554
|
}
|
|
1555
1555
|
}
|
|
1556
|
-
async function nativeUnitCost(child, startedAt) {
|
|
1556
|
+
async function nativeUnitCost(child, startedAt, endedAt) {
|
|
1557
1557
|
if (!child || !input.client?.session?.messages)
|
|
1558
1558
|
return null;
|
|
1559
1559
|
try {
|
|
1560
1560
|
const response = await input.client.session.messages({ path: { id: child }, query: { directory: input.directory } });
|
|
1561
|
-
return isRecord(response) && Array.isArray(response.data) ? settledUnitUsage(response.data, startedAt) : null;
|
|
1561
|
+
return isRecord(response) && Array.isArray(response.data) ? settledUnitUsage(response.data, startedAt, endedAt) : null;
|
|
1562
1562
|
}
|
|
1563
1563
|
catch {
|
|
1564
1564
|
return null;
|
|
1565
1565
|
}
|
|
1566
1566
|
}
|
|
1567
|
+
async function unitUsageSegments(root, taskID, callID, child) {
|
|
1568
|
+
const origin = await input.runtimeBridge?.missionDispatchOrigin?.(root, taskID, callID);
|
|
1569
|
+
return origin ? [...origin.segments, ...(child ? [{ sessionID: child,
|
|
1570
|
+
startedAt: origin.segments.at(-1).endedAt }] : [])] : undefined;
|
|
1571
|
+
}
|
|
1572
|
+
async function priceUsageSegments(segments) {
|
|
1573
|
+
const costs = await Promise.all(segments.map(segment => nativeUnitCost(segment.sessionID, segment.startedAt, segment.endedAt)));
|
|
1574
|
+
return costs.length && costs.every((cost) => cost !== null) ? costs.reduce((sum, cost) => sum + cost, 0) : null;
|
|
1575
|
+
}
|
|
1567
1576
|
async function reconcileUnitUsage(root) {
|
|
1568
1577
|
const ledger = await goalLedger(root), snapshot = await ledger.readGoal();
|
|
1569
1578
|
const reconciled = new Set(snapshot.records.flatMap(({ event }) => event.kind === "unit.usage-reconciled" ? [event.reservation_id] : []));
|
|
@@ -1571,7 +1580,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1571
1580
|
if (event.kind !== "unit.settled" || event.goal_id !== snapshot.state.goal_id || event.cost_usd !== null ||
|
|
1572
1581
|
!event.native_session_id || reconciled.has(event.reservation_id))
|
|
1573
1582
|
continue;
|
|
1574
|
-
const cost = await
|
|
1583
|
+
const cost = event.native_usage_segments ? await priceUsageSegments(event.native_usage_segments)
|
|
1584
|
+
: await nativeUnitCost(event.native_session_id, event.native_started_at);
|
|
1575
1585
|
if (cost === null)
|
|
1576
1586
|
continue;
|
|
1577
1587
|
await ledger.appendGoal({ kind: "unit.usage-reconciled", at: new Date().toISOString(), goal_id: event.goal_id,
|
|
@@ -1594,8 +1604,9 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1594
1604
|
const pending = state.outstanding_reservations;
|
|
1595
1605
|
for (const reservation of pending) {
|
|
1596
1606
|
const dispatch = await input.runtimeBridge?.recoverMissionDispatch?.(root, reservation.unit_id);
|
|
1607
|
+
const origin = dispatch && await input.runtimeBridge?.missionDispatchOrigin?.(root, reservation.unit_id, dispatch.callID);
|
|
1597
1608
|
if (!dispatch || !state.goal_id || goalFingerprint({ goal_id: state.goal_id, unit_id: reservation.unit_id,
|
|
1598
|
-
call_id: dispatch.callID }) !== reservation.reservation_id)
|
|
1609
|
+
call_id: origin?.callID ?? dispatch.callID }) !== reservation.reservation_id)
|
|
1599
1610
|
continue;
|
|
1600
1611
|
if (!dispatch.cancelled && dispatch.nativeOutcome === "completed" && goalReservations.get(dispatch.callID)?.reservationID === reservation.reservation_id) {
|
|
1601
1612
|
// Reuse host-observed command exits/bindings when still present. A terminal outcome alone
|
|
@@ -1616,13 +1627,16 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1616
1627
|
}
|
|
1617
1628
|
const startedAt = (await ledger.readGoal()).records.find(({ event }) => event.kind === "dispatch.reserved" &&
|
|
1618
1629
|
event.reservation_id === reservation.reservation_id)?.event.at;
|
|
1630
|
+
const usageSegments = await unitUsageSegments(root, reservation.unit_id, dispatch.callID, dispatch.childSessionID);
|
|
1619
1631
|
await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(),
|
|
1620
1632
|
reservation_id: reservation.reservation_id,
|
|
1621
1633
|
receipt_id: goalFingerprint({ recovered_host_task: dispatch.callID, reservation: reservation.reservation_id }),
|
|
1622
1634
|
goal_id: state.goal_id, unit_id: reservation.unit_id,
|
|
1623
1635
|
disposition: dispatch.cancelled ? "cancelled" : "failed", result_class: dispatch.cancelled ? "interrupted" : "process-defect",
|
|
1624
1636
|
progress_fingerprint: null, evidence: [], elapsed_ms: null,
|
|
1625
|
-
cost_usd: await nativeUnitCost(dispatch.childSessionID, startedAt),
|
|
1637
|
+
cost_usd: usageSegments ? await priceUsageSegments(usageSegments) : await nativeUnitCost(dispatch.childSessionID, startedAt),
|
|
1638
|
+
...(usageSegments ? { native_usage_segments: usageSegments,
|
|
1639
|
+
native_session_id: dispatch.childSessionID ?? usageSegments.at(-1)?.sessionID } : {}),
|
|
1626
1640
|
...(dispatch.childSessionID ? { native_session_id: dispatch.childSessionID, native_started_at: startedAt } : {}) });
|
|
1627
1641
|
// A missed after hook closes execution, but cannot turn unobserved validation into acceptance.
|
|
1628
1642
|
await input.runtimeBridge?.onSerialSettlement?.({ rootSessionID: root, callID: dispatch.callID,
|
|
@@ -2317,13 +2331,28 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2317
2331
|
if (state.phase === "terminal" || state.receipt !== null) {
|
|
2318
2332
|
throw new Error("SORTIE_GOAL_CONTROL_DENIED: terminal");
|
|
2319
2333
|
}
|
|
2334
|
+
const inheritedTaskID = handoffValue(handoffEntries(prompt), ["task_id"]);
|
|
2335
|
+
const origin = inheritedTaskID && await input.runtimeBridge?.missionDispatchOrigin?.(goalRoot(sessionID), inheritedTaskID, callID);
|
|
2336
|
+
const retainHandoffReservation = () => {
|
|
2337
|
+
if (!origin)
|
|
2338
|
+
return;
|
|
2339
|
+
const reservationID = goalFingerprint({ goal_id: state.goal_id, unit_id: inheritedTaskID, call_id: origin.callID });
|
|
2340
|
+
const retained = state.outstanding_reservations.find(item => item.reservation_id === reservationID && item.unit_id === inheritedTaskID);
|
|
2341
|
+
if (!retained)
|
|
2342
|
+
throw new Error("mission-handoff-reservation-unavailable");
|
|
2343
|
+
for (const [previousCall, reservation] of goalReservations) {
|
|
2344
|
+
if (reservation.reservationID === reservationID)
|
|
2345
|
+
goalReservations.delete(previousCall);
|
|
2346
|
+
}
|
|
2347
|
+
goalReservations.set(callID, { root: goalRoot(sessionID), reservationID, unitID: inheritedTaskID, started: Date.parse(origin.startedAt) });
|
|
2348
|
+
};
|
|
2320
2349
|
if (state.replan_required && !state.replan_used) {
|
|
2321
2350
|
state = await ledger.appendGoal({ kind: "goal.replanned", at: new Date().toISOString(),
|
|
2322
2351
|
goal_id: state.goal_id, revision: state.revision, reason: "no-progress" });
|
|
2323
2352
|
}
|
|
2324
2353
|
// No-progress is diagnostic, not another implicit execution budget. Useful
|
|
2325
2354
|
// repair can continue within the same explicit unit/time/cost limits below.
|
|
2326
|
-
if (state.budget !== null && state.consumed_units + state.outstanding_reservations.length >= state.budget.max_units) {
|
|
2355
|
+
if (!origin && state.budget !== null && state.consumed_units + state.outstanding_reservations.length >= state.budget.max_units) {
|
|
2327
2356
|
await terminalGoal(sessionID, "stop_budget", "stopped");
|
|
2328
2357
|
throw budgetDenied("units");
|
|
2329
2358
|
}
|
|
@@ -2337,6 +2366,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2337
2366
|
await terminalGoal(sessionID, "stop_budget", "stopped");
|
|
2338
2367
|
throw budgetDenied("cost_usd");
|
|
2339
2368
|
}
|
|
2369
|
+
if (origin) {
|
|
2370
|
+
retainHandoffReservation();
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2340
2373
|
if (state.goal_id === null)
|
|
2341
2374
|
return;
|
|
2342
2375
|
const unitID = handoffValue(handoffEntries(prompt), ["task_id"]) ?? callID;
|
|
@@ -2400,7 +2433,16 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2400
2433
|
...execution, immutableRef: execution.immutableRef, endedAt: execution.endedAt, fresh: execution.fresh,
|
|
2401
2434
|
exitCode: execution.exitCode, outcome: execution.outcome
|
|
2402
2435
|
}, state, reservation.unitID));
|
|
2403
|
-
const
|
|
2436
|
+
const handedChecks = await input.runtimeBridge?.missionHandoffChecks?.(reservation.root, reservation.unitID, callID) ?? [];
|
|
2437
|
+
const inherited = handedChecks.flatMap(check => {
|
|
2438
|
+
const laterFailure = [...hostGoalExecutions.values()].some(execution => execution.root === reservation.root &&
|
|
2439
|
+
execution.sessionID === childSessionID && execution.endedAt && execution.outcome !== "pass" && execution.outcome !== "skip" &&
|
|
2440
|
+
Date.parse(execution.startedAt) >= Date.parse(check.startedAt));
|
|
2441
|
+
return laterFailure ? [] : observedHostGoalEvidence({ ...check, owner: "coordinator", outcome: "pass",
|
|
2442
|
+
immutableRef: goalFingerprint({ direct_check: check }),
|
|
2443
|
+
correction: { taskID: reservation.unitID, dispatchCallID: check.dispatchCallID, commands: check.command } }, state, reservation.unitID);
|
|
2444
|
+
});
|
|
2445
|
+
const hostEvidence = [...new Map((directEvidence ?? [...observedEvidence, ...inherited]).map(entry => [entry.evidence_id, entry])).values()];
|
|
2404
2446
|
// A worker may return after a user scope epoch changed. Settle its spend/reservation, but only
|
|
2405
2447
|
// adopt observations still bound to the current accepted source/candidate/criterion contract.
|
|
2406
2448
|
const acceptedEvidence = hostEvidence.filter((entry) => validGoalEvidence(entry, state) &&
|
|
@@ -2432,13 +2474,16 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2432
2474
|
const processDefect = !correctionChecks?.failure && failedAcceptanceExecution === undefined && (childSessionID === undefined || hostBindingDefect ||
|
|
2433
2475
|
goalValidationDefects.has(childSessionID) || !validated);
|
|
2434
2476
|
const resultClass = validated ? "acceptance" : interrupted ? "interrupted" : processDefect ? "process-defect" : "acceptance";
|
|
2477
|
+
const usageSegments = await unitUsageSegments(reservation.root, reservation.unitID, callID, childSessionID);
|
|
2435
2478
|
await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(),
|
|
2436
2479
|
reservation_id: reservation.reservationID, receipt_id: goalFingerprint({ call_id: callID, output: outputText.slice(0, 2048) }),
|
|
2437
2480
|
goal_id: state.goal_id, unit_id: reservation.unitID,
|
|
2438
2481
|
disposition: validated ? "succeeded" : interrupted ? "cancelled" : "failed", result_class: resultClass,
|
|
2439
2482
|
progress_fingerprint: progress && validated ? goalFingerprint(settlementEvidence) : null,
|
|
2440
2483
|
evidence: settlementEvidence, elapsed_ms: Math.max(0, Date.now() - reservation.started),
|
|
2441
|
-
cost_usd: await nativeUnitCost(childSessionID, new Date(reservation.started).toISOString()),
|
|
2484
|
+
cost_usd: usageSegments ? await priceUsageSegments(usageSegments) : await nativeUnitCost(childSessionID, new Date(reservation.started).toISOString()),
|
|
2485
|
+
...(usageSegments ? { native_usage_segments: usageSegments,
|
|
2486
|
+
native_session_id: childSessionID ?? usageSegments.at(-1)?.sessionID } : {}),
|
|
2442
2487
|
...(childSessionID ? { native_session_id: childSessionID, native_started_at: new Date(reservation.started).toISOString() } : {}) });
|
|
2443
2488
|
const result = {
|
|
2444
2489
|
rootSessionID: reservation.root, callID, unitID: reservation.unitID,
|
|
@@ -2484,9 +2529,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2484
2529
|
}
|
|
2485
2530
|
if (request.direct) {
|
|
2486
2531
|
const direct = request.direct;
|
|
2532
|
+
const origin = await input.runtimeBridge?.missionDispatchOrigin?.(root, request.unitID, direct.callID);
|
|
2487
2533
|
const defect = [...snapshot.records].reverse().map(record => record.event).find(event => event.kind === "unit.settled" &&
|
|
2488
2534
|
event.unit_id === request.unitID && event.evidence.length === 0 && event.native_session_id === request.childSessionID &&
|
|
2489
|
-
event.reservation_id === goalFingerprint({ goal_id: goal.goal_id, unit_id: request.unitID, call_id: direct.callID }) &&
|
|
2535
|
+
event.reservation_id === goalFingerprint({ goal_id: goal.goal_id, unit_id: request.unitID, call_id: origin?.callID ?? direct.callID }) &&
|
|
2490
2536
|
event.receipt_id === goalFingerprint({ call_id: direct.callID,
|
|
2491
2537
|
output: "Host-observed direct unit finished; controller session remains active." }));
|
|
2492
2538
|
if (defect?.kind !== "unit.settled" || Math.abs(Date.parse(defect.at) - Date.parse(direct.finishedAt)) > 1000) {
|
|
@@ -8287,7 +8333,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
8287
8333
|
cost_status: state.consumed_cost_usd === null ? "unknown-usage" : reserved > 0 ? "in-flight-not-final" : "settled",
|
|
8288
8334
|
cost_source: "native-usage-price-table",
|
|
8289
8335
|
cost_scope: "worker-only", campaign_remaining_usd: null,
|
|
8290
|
-
cost_note: "Implementation token-price estimates from complete native usage; scoped Reviewer corrections use this same Worker-unit ledger. Missing requests remain unknown and are reconciled on later status. Excludes orchestration/read-only review and external campaign spend. Zero settled cost does not mean free execution." };
|
|
8336
|
+
cost_note: "Implementation token-price estimates from complete native usage; direct-author segments and scoped Reviewer corrections use this same Worker-unit ledger. Missing requests remain unknown and are reconciled on later status. Excludes orchestration/read-only review and external campaign spend. Zero settled cost does not mean free execution." };
|
|
8291
8337
|
return snapshot;
|
|
8292
8338
|
},
|
|
8293
8339
|
extendMissionUnitBudget: (root, maxUnits) => serializeChatTransition(root, async () => {
|
|
@@ -8353,12 +8399,17 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
8353
8399
|
activateSession(actor);
|
|
8354
8400
|
if (!goalReservations.has(direct.callID)) {
|
|
8355
8401
|
const snapshot = await (await goalLedger(root)).readGoal();
|
|
8356
|
-
const
|
|
8357
|
-
if (
|
|
8358
|
-
goalReservations.set(direct.callID, { root, reservationID, unitID: direct.taskID, started: Date.parse(direct.startedAt) });
|
|
8359
|
-
}
|
|
8360
|
-
else
|
|
8402
|
+
const origin = await input.runtimeBridge?.missionDispatchOrigin?.(root, direct.taskID, direct.callID);
|
|
8403
|
+
if (origin)
|
|
8361
8404
|
await reserveGoalDispatch(root, direct.callID, prompt);
|
|
8405
|
+
else {
|
|
8406
|
+
const reservationID = goalFingerprint({ goal_id: snapshot.state.goal_id, unit_id: direct.taskID, call_id: direct.callID });
|
|
8407
|
+
if (snapshot.state.outstanding_reservations.some(item => item.reservation_id === reservationID)) {
|
|
8408
|
+
goalReservations.set(direct.callID, { root, reservationID, unitID: direct.taskID, started: Date.parse(direct.startedAt) });
|
|
8409
|
+
}
|
|
8410
|
+
else
|
|
8411
|
+
await reserveGoalDispatch(root, direct.callID, prompt);
|
|
8412
|
+
}
|
|
8362
8413
|
}
|
|
8363
8414
|
const binding = await input.runtimeBridge?.missionReadBinding?.(root, actor, handoffPath);
|
|
8364
8415
|
if (!binding)
|
|
@@ -8395,6 +8446,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
8395
8446
|
await releaseWriteGate(actor);
|
|
8396
8447
|
releaseSessionEnforcement(actor);
|
|
8397
8448
|
},
|
|
8449
|
+
assertDirectUnitQuiescent: async (actor) => {
|
|
8450
|
+
if (activeSessions.get(actor)?.inFlightCalls.size)
|
|
8451
|
+
throw new Error("mission-direct-tools-running");
|
|
8452
|
+
},
|
|
8398
8453
|
failDirectUnit: async (root, actor) => {
|
|
8399
8454
|
const direct = await input.runtimeBridge?.directMissionExecution?.(actor);
|
|
8400
8455
|
if (!direct || direct.root !== root)
|
|
@@ -8404,7 +8459,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
8404
8459
|
// Cold terminal recovery can happen before the actor's next tool/context hook.
|
|
8405
8460
|
if (!goalReservations.has(direct.callID)) {
|
|
8406
8461
|
const snapshot = await (await goalLedger(root)).readGoal();
|
|
8407
|
-
const
|
|
8462
|
+
const origin = await input.runtimeBridge?.missionDispatchOrigin?.(root, direct.taskID, direct.callID);
|
|
8463
|
+
const reservationID = goalFingerprint({ goal_id: snapshot.state.goal_id, unit_id: direct.taskID, call_id: origin?.callID ?? direct.callID });
|
|
8408
8464
|
if (snapshot.state.outstanding_reservations.some(item => item.reservation_id === reservationID) ||
|
|
8409
8465
|
snapshot.records.some(({ event }) => event.kind === "unit.settled" && event.reservation_id === reservationID)) {
|
|
8410
8466
|
goalReservations.set(direct.callID, { root, reservationID, unitID: direct.taskID, started: Date.parse(direct.startedAt) });
|
|
@@ -8449,7 +8505,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
8449
8505
|
missionWorkerTerminal: async (root, terminal, writeScopes) => {
|
|
8450
8506
|
const childSessionID = terminal.childSessionID;
|
|
8451
8507
|
const snapshot = await (await goalLedger(root)).readGoal();
|
|
8452
|
-
const
|
|
8508
|
+
const origin = await input.runtimeBridge?.missionDispatchOrigin?.(root, terminal.taskID, terminal.callID);
|
|
8509
|
+
const reservationID = goalFingerprint({ goal_id: snapshot.state.goal_id, unit_id: terminal.taskID, call_id: origin?.callID ?? terminal.callID });
|
|
8453
8510
|
if (snapshot.state.outstanding_reservations.some(item => item.reservation_id === reservationID) ||
|
|
8454
8511
|
!snapshot.records.some(({ event }) => event.kind === "unit.settled" && event.goal_id === snapshot.state.goal_id &&
|
|
8455
8512
|
event.unit_id === terminal.taskID && event.reservation_id === reservationID)) {
|
|
@@ -115,6 +115,8 @@ export declare function reviewerCorrectionValidationFresh(validation: readonly s
|
|
|
115
115
|
}[];
|
|
116
116
|
nextOccurrence?: number;
|
|
117
117
|
}>;
|
|
118
|
+
/** Preserve a current ordered prefix while the next author executes the remaining recipe. */
|
|
119
|
+
export declare function directHandoffValidationChecks(...args: Parameters<typeof reviewerCorrectionValidationFresh>): Promise<readonly ReviewerCorrectionCheck[]>;
|
|
118
120
|
/** Summary-only native reader: unavailable API/error is not a successfully observed empty history. */
|
|
119
121
|
export declare function observedMissionValidationSummary(validation: readonly string[], childSessionID: string, read?: () => Promise<unknown>, notBefore?: number, directory?: string, directories?: Readonly<Record<string, string>>): Promise<Record<string, unknown>>;
|
|
120
122
|
export declare function missionReviewBaseline(directory: string): Promise<string | undefined>;
|
|
@@ -268,6 +268,16 @@ export async function reviewerCorrectionValidationFresh(validation, child, histo
|
|
|
268
268
|
}
|
|
269
269
|
return checked;
|
|
270
270
|
}
|
|
271
|
+
/** Preserve a current ordered prefix while the next author executes the remaining recipe. */
|
|
272
|
+
export async function directHandoffValidationChecks(...args) {
|
|
273
|
+
const [validation, child, history, started, checks, root, directories, registeredAt] = args;
|
|
274
|
+
for (let count = validation.length; count > 0; count--) {
|
|
275
|
+
const result = await reviewerCorrectionValidationFresh(validation.slice(0, count), child, history, started, checks, root, directories, registeredAt);
|
|
276
|
+
if (result.ready)
|
|
277
|
+
return checks.filter(check => result.matched?.some(item => item.callID === check.callID));
|
|
278
|
+
}
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
271
281
|
/** Summary-only native reader: unavailable API/error is not a successfully observed empty history. */
|
|
272
282
|
export async function observedMissionValidationSummary(validation, childSessionID, read, notBefore, directory, directories) {
|
|
273
283
|
if (!read)
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -18,7 +18,7 @@ import { publishMissionProgress } from "./mission-progress.js";
|
|
|
18
18
|
import { formatWorkQuantity, planWorkQuantity } from "../core/work-quantity.js";
|
|
19
19
|
import { cachedWorkProgress, forgetWorkSession, observeActorWork, workRootForActor } from "../core/work-quantity-observations.js";
|
|
20
20
|
import { serializeOperatorStatus } from "./operator-status-output.js";
|
|
21
|
-
import { completedMissionReviewPrompts, initialMissionReviewPrompt, missionDeliveryObservation, missionReviewBaseline, missionReviewSource, observedMissionValidation, observedMissionValidationSummary, missionReviewValidation, reviewerCorrectionValidation, reviewerCorrectionValidationFresh, validationDirectoryMatches } from "./mission-review.js";
|
|
21
|
+
import { completedMissionReviewPrompts, initialMissionReviewPrompt, missionDeliveryObservation, missionReviewBaseline, missionReviewSource, observedMissionValidation, observedMissionValidationSummary, missionReviewValidation, reviewerCorrectionValidation, reviewerCorrectionValidationFresh, directHandoffValidationChecks, validationDirectoryMatches } from "./mission-review.js";
|
|
22
22
|
import { missionLocations, missionLocationPacket } from "./mission-location.js";
|
|
23
23
|
import { prepareValidationScratch } from "./validation-scratch.js";
|
|
24
24
|
import { SOURCE_REVIEW_RISK_TAGS } from "../core/consultation.js";
|
|
@@ -610,6 +610,28 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
610
610
|
return direct && taskID && direct.unit.callID ? { root: direct.root, taskID, callID: direct.unit.callID,
|
|
611
611
|
startedAt: direct.unit.directExecution.startedAt } : undefined;
|
|
612
612
|
},
|
|
613
|
+
missionDispatchOrigin: async (root, taskID, callID) => {
|
|
614
|
+
const state = await operators.read(root);
|
|
615
|
+
const unit = state?.units.find(item => item.callID === callID && /^task_id: (.+)$/mu.exec(item.task.prompt)?.[1] === taskID);
|
|
616
|
+
const segments = unit?.directHandoffs?.filter(item => !item.settledCallID || item.settledCallID === callID);
|
|
617
|
+
const first = segments?.[0];
|
|
618
|
+
return first ? { callID: first.callID, startedAt: first.startedAt,
|
|
619
|
+
segments: segments.map(item => ({ sessionID: item.actor, startedAt: item.startedAt, endedAt: item.handedOffAt })) } : undefined;
|
|
620
|
+
},
|
|
621
|
+
missionHandoffChecks: async (root, taskID, callID) => {
|
|
622
|
+
const run = await operators.read(root);
|
|
623
|
+
const unit = run?.units.find(item => item.callID === callID && /^task_id: (.+)$/mu.exec(item.task.prompt)?.[1] === taskID);
|
|
624
|
+
const prior = unit?.directHandoffs?.filter(item => !item.settledCallID || item.settledCallID === callID).at(-1);
|
|
625
|
+
if (!unit || !prior)
|
|
626
|
+
return [];
|
|
627
|
+
try {
|
|
628
|
+
const history = await reviewMessages(prior.actor, prior.startedAt, true);
|
|
629
|
+
return await directHandoffValidationChecks(unit.unit.validation, prior.actor, history, Date.parse(prior.startedAt), prior.checks, input.directory, prior.validationCwd ?? unit.unit.validation_cwd, prior.validationRegisteredAt);
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
},
|
|
613
635
|
reviewerCorrectionValidationMembers: async (child, command, directory) => {
|
|
614
636
|
const correction = await reviewerCorrection(child) ?? await directExecution(child);
|
|
615
637
|
if (!correction)
|
|
@@ -654,6 +676,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
654
676
|
const unit = run?.units.find(item => /^task_id: (.+)$/m.exec(item.task.prompt)?.[1] === taskID);
|
|
655
677
|
if (!run || !unit?.callID)
|
|
656
678
|
return undefined;
|
|
679
|
+
if (run.phase === "cancelled" && unit.directHandoffs?.length && !unit.childSessionID) {
|
|
680
|
+
return { callID: unit.callID, cancelled: true };
|
|
681
|
+
}
|
|
657
682
|
// Direct work has no child Task terminal. Its explicit host-observed finish owns
|
|
658
683
|
// settlement; never mistake a prior parent response for this unit's completion.
|
|
659
684
|
if (unit.directExecution && run.phase !== "cancelled")
|
|
@@ -1150,7 +1175,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1150
1175
|
`and verifies old children before preparing a Worker. Otherwise obtain the user's scope decision.` };
|
|
1151
1176
|
if (mission.coordinator === null && mission.runID === null && !mission.dispatchOpen && mission.phase === "open") {
|
|
1152
1177
|
return { ...packet, task: missions.task(mission),
|
|
1153
|
-
next_action:
|
|
1178
|
+
next_action: initialMissionAction(mission) };
|
|
1154
1179
|
}
|
|
1155
1180
|
if (mission.coordinator === null && mission.runID === run?.runID && !mission.dispatchOpen &&
|
|
1156
1181
|
run?.phase === "awaiting-acceptance" && mission.kind === "operation" && !missionExecutionComplete(mission)) {
|
|
@@ -2199,6 +2224,44 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2199
2224
|
executor_session_id: actor, validation: direct.unit.unit.validation,
|
|
2200
2225
|
next_action: `Implement/correct and run the declared checks here, in this same session. Call ${finishDirectUnit} after the requested delivery boundary. No Worker or handoff read is needed. Retain the original requirements; independent Review and Operator acceptance still follow.` });
|
|
2201
2226
|
}
|
|
2227
|
+
async function handoffDirect(root, actor, callID, notes) {
|
|
2228
|
+
const direct = await directExecution(actor);
|
|
2229
|
+
if (direct) {
|
|
2230
|
+
await control.assertDirectUnitQuiescent(actor);
|
|
2231
|
+
const history = await reviewMessages(actor, direct.unit.directExecution.startedAt, true);
|
|
2232
|
+
const calls = new Map();
|
|
2233
|
+
for (const message of history)
|
|
2234
|
+
if (record(message.info) && message.info.sessionID === actor && Array.isArray(message.parts)) {
|
|
2235
|
+
for (const part of message.parts)
|
|
2236
|
+
if (record(part) && part.type === "tool" && typeof part.callID === "string" && part.callID !== callID)
|
|
2237
|
+
calls.set(part.callID, part);
|
|
2238
|
+
}
|
|
2239
|
+
if ([...calls.values()].some(part => !record(part.state) || !["completed", "error"].includes(String(part.state.status)) ||
|
|
2240
|
+
record(part.state.metadata) && ["running", "pending", "unknown"].includes(String(part.state.metadata.status)))) {
|
|
2241
|
+
return JSON.stringify({ status: "direct-handoff-awaits-tools", next_action: "Finish the existing native command before handing off this unit; retain its result and continue here or delegate afterwards." });
|
|
2242
|
+
}
|
|
2243
|
+
await activateDirect(actor);
|
|
2244
|
+
const mission = await missions.retainContext(root, actor, history, notes);
|
|
2245
|
+
await operators.handoffDirect(root, actor, await missions.handoffContext(mission));
|
|
2246
|
+
}
|
|
2247
|
+
const run = await operators.required(root);
|
|
2248
|
+
const unit = run.units.find(item => item.status !== "succeeded");
|
|
2249
|
+
const last = unit?.directHandoffs?.at(-1);
|
|
2250
|
+
if (actor !== (run.operatorSessionID ?? root) || !last || last.actor !== actor)
|
|
2251
|
+
throw new Error("mission-direct-handoff-unavailable");
|
|
2252
|
+
// Idempotently finish projection/release after a restart between the durable handoff and response.
|
|
2253
|
+
await missions.update(root, mission => {
|
|
2254
|
+
const previous = mission.attempts?.find(item => item.callID === last.callID);
|
|
2255
|
+
if (previous?.status === "dispatched")
|
|
2256
|
+
previous.status = "handed-off";
|
|
2257
|
+
});
|
|
2258
|
+
await control.releaseDirectUnit(actor);
|
|
2259
|
+
const next = await operators.next(root, actor);
|
|
2260
|
+
return JSON.stringify({ ...next,
|
|
2261
|
+
handoff: { from: actor, unit_id: unit.unit.id, retained_checks: last.checks.length },
|
|
2262
|
+
next_action: unit.status === "pending" ? "Dispatch the returned Worker once to continue this same unit. Existing edits, context, checks and reservation are retained. The Worker owns remaining implementation and validation; handoff is not completion."
|
|
2263
|
+
: "The handed-off unit is already admitted. Continue its existing owner; do not dispatch another Worker." });
|
|
2264
|
+
}
|
|
2202
2265
|
tools[startDirectUnit] = { description: "Execute the next already-planned ordinary unit in this Coordinator/Operator session, including a correction planned with executor=self. Native edits and declared checks become formal evidence without a Worker, handoff read or approval. Never replace an active writer; completed correction history does not restrict this choice.",
|
|
2203
2266
|
args: {}, execute: async (_args, context) => {
|
|
2204
2267
|
const { root } = await missionAuthority(context.sessionID);
|
|
@@ -2269,12 +2332,19 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2269
2332
|
description: "For an operation: the actual run/grade commands, not a preflight or NO_START check. Host observes their native shell completion separately from successful exits; no handwritten proof file is needed. For run-once/result collection, validate the collected result without rerunning the operation.", "x-sortie-optional": true };
|
|
2270
2333
|
const handoffContextSchema = { type: "string", "x-sortie-optional": true,
|
|
2271
2334
|
description: "Optional current working notes: intent/rationale, known environment and paths, completed setup/checks with results, failed approaches and remaining work. Reuse known facts with source references; no required form or new approval. Public history and Mission observations are also supplied automatically. Omit when already sufficient; do not recopy the original request." };
|
|
2272
|
-
|
|
2335
|
+
function initialMissionAction(mission) {
|
|
2336
|
+
return mission.preferredExecutor === "coordinator"
|
|
2337
|
+
? "Dispatch this Coordinator Task now with the supplied recommendation/context. Coordinator owns substantial implementation and can use plan_units executor=self or delegate remaining work when useful. Start useful work, not a route-selection investigation."
|
|
2338
|
+
: "Use the recommendation and already-known context: when a meaningful formal check and useful unit are already known, plan_units can dispatch its Worker directly. Otherwise dispatch this Coordinator Task now; it may implement here or delegate during actual work. Do not investigate merely to decide the route or discover a check for Operator planning.";
|
|
2339
|
+
}
|
|
2340
|
+
tools[startMission] = { description: `Operator: save current requirements and recommend preferred_executor from the original instruction and already-known context, without a routing investigation. The host retains the original request verbatim. Clear bounded work normally suits Worker; coupled design, ambiguous requirements or cross-system integration normally suit Coordinator direct execution. Follow explicit user choices. Coordinator may revise this advisory recommendation during useful work, not a mandatory reassessment. With known unit facts and meaningful formal checks, include unit now (and execution for an operation): worker/default can dispatch its Worker directly; coordinator retains these facts for its own execution choice. Unknown checks are resolved during actual work, not an Operator preflight. Investigation/edit/check/requested commit stay with the chosen author before applicable Review. ${MISSION_GIT_SCOPE} intent=replace follows an actual user requirement change, retaining unchanged constraints and cumulative spend; for mission-source-reconciliation-required use the saved requirements when they reflect that change. intent=new is separate work in another location.`,
|
|
2273
2341
|
args: { requirements: { ...stringList, minItems: 1, maxItems: 64 },
|
|
2274
2342
|
work_quantities: { type: ["array", "object", "string", "number", "boolean", "null"], items: {}, additionalProperties: true, "x-sortie-optional": true,
|
|
2275
2343
|
minLength: 0, maxLength: 65535, minItems: 0, maxItems: 4096,
|
|
2276
2344
|
description: "Optional display-only workload array: stable id, kind, relative_weight, bindings.paths/commands, checkpoints. Reuse this ordinary plan; never add a planning/report turn for progress. Invalid shapes/estimates fall back without changing execution." },
|
|
2277
|
-
|
|
2345
|
+
preferred_executor: { type: "string", enum: ["coordinator", "worker"], "x-sortie-optional": true,
|
|
2346
|
+
description: "Initial recommendation from the instruction and already-known context. Coordinator may adapt through actual work without approval or a separate investigation. coordinator returns Coordinator even with supplied unit facts; worker uses the direct Worker route when those facts are ready. Not a model override or immutable requirement." },
|
|
2347
|
+
unit: { ...missionUnitSchema, description: "Include already-known unit facts without investigating to fill them. worker/default returns its configured Worker directly; coordinator passes these facts to Coordinator without starting a Worker.", "x-sortie-optional": true },
|
|
2278
2348
|
execution: missionExecutionSchema,
|
|
2279
2349
|
handoff_context: handoffContextSchema,
|
|
2280
2350
|
confirmed_conditions: conditionsSchema,
|
|
@@ -2282,6 +2352,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2282
2352
|
kind: { type: "string", enum: ["implementation", "operation"], description: "Use operation for running an existing benchmark, command or procedure. The host records its actual execution separately from setup and checks.", "x-sortie-optional": true },
|
|
2283
2353
|
intent: { type: "string", enum: ["", "continue", "new", "replace"], "x-sortie-optional": true } }, execute: async (args, context) => {
|
|
2284
2354
|
await requireRoot(context.sessionID);
|
|
2355
|
+
if (args.preferred_executor !== undefined && !["coordinator", "worker"].includes(args.preferred_executor))
|
|
2356
|
+
throw new Error("mission-preferred-executor-invalid");
|
|
2285
2357
|
if (args.intent !== undefined && !["", "continue", "new", "replace"].includes(args.intent))
|
|
2286
2358
|
throw new Error("mission-intent-invalid");
|
|
2287
2359
|
return serializeDispatchTransition(context.sessionID, async () => {
|
|
@@ -2329,6 +2401,14 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2329
2401
|
...(args.intent === "replace" && cancelled?.phase === "cancelled" ? { cancelledRunID: cancelled.runID } : {}),
|
|
2330
2402
|
});
|
|
2331
2403
|
mission = await missions.retainContext(context.sessionID, context.sessionID, history, args.handoff_context);
|
|
2404
|
+
if (args.preferred_executor !== undefined)
|
|
2405
|
+
mission = await missions.update(context.sessionID, state => {
|
|
2406
|
+
state.preferredExecutor = args.preferred_executor;
|
|
2407
|
+
});
|
|
2408
|
+
const suggestedUnit = args.unit;
|
|
2409
|
+
if (mission.preferredExecutor === "coordinator" && record(suggestedUnit) && mission.runID === null && !mission.dispatchOpen) {
|
|
2410
|
+
mission = await missions.update(context.sessionID, state => { state.suggestedUnit = structuredClone(suggestedUnit); });
|
|
2411
|
+
}
|
|
2332
2412
|
const prohibited = args.prohibited_write;
|
|
2333
2413
|
if (prohibited !== undefined) {
|
|
2334
2414
|
if (!Array.isArray(prohibited) || !prohibited.every(path => typeof path === "string"))
|
|
@@ -2367,7 +2447,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2367
2447
|
if (baseline)
|
|
2368
2448
|
mission = await missions.update(context.sessionID, state => { state.reviewBaseline = baseline; });
|
|
2369
2449
|
}
|
|
2370
|
-
if (args.unit !== undefined && mission.runID === null && !mission.dispatchOpen &&
|
|
2450
|
+
if (mission.preferredExecutor !== "coordinator" && args.unit !== undefined && mission.runID === null && !mission.dispatchOpen &&
|
|
2371
2451
|
(mission.kind !== "operation" || args.execution !== undefined || mission.execution)) {
|
|
2372
2452
|
const planned = JSON.parse(await declareMissionUnits(context.sessionID, context.sessionID, mission, [args.unit], undefined, args.execution, context.callID, args.handoff_context, args.work_quantities));
|
|
2373
2453
|
return JSON.stringify({ ...locationObservation(context.sessionID), mission_id: mission.id, requirements: mission.requirements, ...planned });
|
|
@@ -2380,7 +2460,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2380
2460
|
return JSON.stringify({ ...locationObservation(context.sessionID), ...(mission.dispatchOpen
|
|
2381
2461
|
? missionDispatchPacket(mission, await operators.read(context.sessionID))
|
|
2382
2462
|
: { mission_id: mission.id, requirements: mission.requirements, task: missions.task(mission),
|
|
2383
|
-
|
|
2463
|
+
...(mission.preferredExecutor ? { preferred_executor: mission.preferredExecutor } : {}),
|
|
2464
|
+
next_action: initialMissionAction(mission) }) });
|
|
2384
2465
|
});
|
|
2385
2466
|
} };
|
|
2386
2467
|
tools[skipMissionConsultation] = { description: "Coordinator: durably record why a concrete optional Advisor/Scout consultation is unnecessary. This is observation only: it adds no approval, consultation requirement, or dispatch gate.",
|
|
@@ -2539,6 +2620,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2539
2620
|
const recoveryErrors = [];
|
|
2540
2621
|
const addOwner = (actor, window) => owners.set(actor, [...(owners.get(actor) ?? []), window]);
|
|
2541
2622
|
for (const unit of run.units) {
|
|
2623
|
+
for (const prior of unit.directHandoffs ?? [])
|
|
2624
|
+
addOwner(prior.actor, { notBefore: prior.startedAt, notAfter: prior.handedOffAt });
|
|
2542
2625
|
if (unit.directExecution?.actor) {
|
|
2543
2626
|
addOwner(unit.directExecution.actor, { notBefore: unit.directExecution.startedAt,
|
|
2544
2627
|
...(unit.directExecution.finishedAt ? { notAfter: unit.directExecution.finishedAt } : {}) });
|
|
@@ -2936,17 +3019,20 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2936
3019
|
: next);
|
|
2937
3020
|
}
|
|
2938
3021
|
tools[planUnits] = { description: `Coordinator or single-unit Fast-lane Operator: declare useful work with estimated read/write scope and meaningful formal checks. executor=self starts implementation/correction and formal validation HERE without a Worker or handoff read; otherwise dispatch the returned configured Worker promptly. Reuse useful author context by default; legitimate Worker/direct handoff remains available after terminal work, including correction. Retain findings, actual authorship and passed input-bound checks; do not rediscover or revalidate unchanged work merely for a new actor. ${MISSION_GIT_SCOPE} Native scope reconciliation/expand_unit retain host permissions and explicit path prohibitions. Objective is the target or corrective delta: aim for 2000 characters; the full original request/public reproduction is supplied separately, not copied here. Oversized unit instructions are retained verbatim in handoff Mission context, not rejected for another planning round. Keep every requirement covered. Final validation proves the unit; empty or dummy checks do not qualify. Diagnostics need no registration. write: [] is read-only; dir/** and native absolute paths support actual outputs. reason replans settled work, or ends your own quiescent direct unit without acceptance to correct its declaration, in the same requirements/budget; same-scope failed validation uses retry_mission_unit. Completion history is not a correction-owner lock or an acceptance prerequisite.`,
|
|
2939
|
-
args: { units: { type: "array", minItems: 1, maxItems: 32, items: missionUnitSchema
|
|
3022
|
+
args: { units: { type: "array", minItems: 1, maxItems: 32, items: missionUnitSchema, "x-sortie-optional": true,
|
|
3023
|
+
description: "Useful work units. Omit only with executor=worker to hand off your currently running direct unit, retaining its contract and existing work." },
|
|
2940
3024
|
work_quantities: tools[startMission].args.work_quantities,
|
|
2941
3025
|
execution: missionExecutionSchema,
|
|
2942
3026
|
handoff_context: handoffContextSchema,
|
|
2943
3027
|
reason: { type: "string", "x-sortie-optional": true },
|
|
2944
|
-
executor: { type: "string", enum: ["worker", "self"], description: "Use self to implement/correct and formally validate here without
|
|
3028
|
+
executor: { type: "string", enum: ["worker", "self"], description: "Use self to implement/correct and formally validate here. Use worker without units to yield the current direct unit, carrying remaining action in handoff_context; the same unit, edits, checks and budget continue. With units, worker/default retains configured Worker routing.", "x-sortie-optional": true } }, execute: async (args, context) => {
|
|
2945
3029
|
const { root, mission } = await missionAuthority(context.sessionID);
|
|
2946
3030
|
if (args.executor !== undefined && !["self", "worker"].includes(args.executor))
|
|
2947
3031
|
throw new Error("mission-executor-invalid");
|
|
2948
3032
|
try {
|
|
2949
3033
|
return await serializeDispatchTransition(root, async () => {
|
|
3034
|
+
if (args.executor === "worker" && args.units === undefined)
|
|
3035
|
+
return handoffDirect(root, context.sessionID, context.callID, args.handoff_context);
|
|
2950
3036
|
const planned = await declareMissionUnits(root, context.sessionID, mission, args.units, args.reason, args.execution, context.callID, args.handoff_context, args.work_quantities);
|
|
2951
3037
|
return args.executor === "self" && record(JSON.parse(planned).task) ? startDirect(root, context.sessionID) : planned;
|
|
2952
3038
|
});
|
|
@@ -89,6 +89,17 @@ export interface RuntimeBridge {
|
|
|
89
89
|
callID: string;
|
|
90
90
|
startedAt: string;
|
|
91
91
|
} | undefined>;
|
|
92
|
+
/** The same unit's original reservation and completed direct-author usage segments. */
|
|
93
|
+
missionDispatchOrigin?(rootSessionID: string, taskID: string, callID: string): Promise<{
|
|
94
|
+
callID: string;
|
|
95
|
+
startedAt: string;
|
|
96
|
+
segments: readonly {
|
|
97
|
+
sessionID: string;
|
|
98
|
+
startedAt: string;
|
|
99
|
+
endedAt: string;
|
|
100
|
+
}[];
|
|
101
|
+
} | undefined>;
|
|
102
|
+
missionHandoffChecks?(rootSessionID: string, taskID: string, callID: string): Promise<readonly ReviewerCorrectionCheck[]>;
|
|
92
103
|
reviewerCorrectionValidationMembers?(childSessionID: string, command: string, directory?: string): Promise<string[] | undefined>;
|
|
93
104
|
missionValidationDirectoryMatches?(rootSessionID: string, taskID: string, childSessionID: string, commands: readonly string[], directory: string): Promise<boolean>;
|
|
94
105
|
recordReviewerCorrectionCheck?(rootSessionID: string, taskID: string, check: ReviewerCorrectionCheck): Promise<void>;
|
|
@@ -225,6 +236,7 @@ export interface RuntimeBridge {
|
|
|
225
236
|
/** Settle an unfinished direct unit against its actual actor, never a fabricated child Task. */
|
|
226
237
|
failDirectUnit(rootSessionID: string, actorSessionID: string): Promise<void>;
|
|
227
238
|
releaseDirectUnit(actorSessionID: string): Promise<void>;
|
|
239
|
+
assertDirectUnitQuiescent(actorSessionID: string): Promise<void>;
|
|
228
240
|
expandMissionWriteGate(childSessionID: string, paths: readonly string[]): Promise<void>;
|
|
229
241
|
completeRoot(rootSessionID: string, acceptanceFingerprint: string): Promise<{
|
|
230
242
|
status: "succeeded" | "awaiting-evidence";
|
|
@@ -33,11 +33,13 @@ export function toolSchema(args) {
|
|
|
33
33
|
const schema = plainSchema(value);
|
|
34
34
|
if (optionalArgument(value)) {
|
|
35
35
|
schema.description = `${typeof schema.description === "string" ? `${schema.description} ` : ""}Pass an empty string to omit this argument.`;
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
if (schema.type === "object")
|
|
36
|
+
// Structured optional arguments need the same omission sentinel we decode;
|
|
37
|
+
// otherwise V2 requires a fabricated object/array even when the tool says to omit it.
|
|
38
|
+
if (schema.type === "object" || schema.type === "array")
|
|
39
39
|
return [name, { anyOf: [schema, { type: "string", enum: [""], minLength: 0, maxLength: 0 }],
|
|
40
40
|
description: schema.description }];
|
|
41
|
+
if (schema.type === "string" && Array.isArray(schema.enum) && !schema.enum.includes(""))
|
|
42
|
+
schema.enum = [...schema.enum, ""];
|
|
41
43
|
}
|
|
42
44
|
return [name, schema];
|
|
43
45
|
})),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Price complete native requests; absent/interrupted usage is unknown, never a fabricated zero. */
|
|
2
|
-
export declare function settledUnitUsage(messages: readonly unknown[], startedAt?: string): number | null;
|
|
2
|
+
export declare function settledUnitUsage(messages: readonly unknown[], startedAt?: string, endedAt?: string): number | null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { estimateModelUsageCost } from "./model-cost.js";
|
|
2
2
|
const record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
3
3
|
/** Price complete native requests; absent/interrupted usage is unknown, never a fabricated zero. */
|
|
4
|
-
export function settledUnitUsage(messages, startedAt) {
|
|
4
|
+
export function settledUnitUsage(messages, startedAt, endedAt) {
|
|
5
5
|
let cost = 0, requests = 0;
|
|
6
6
|
const seen = new Set();
|
|
7
7
|
for (const message of messages) {
|
|
@@ -12,10 +12,12 @@ export function settledUnitUsage(messages, startedAt) {
|
|
|
12
12
|
continue;
|
|
13
13
|
seen.add(info.id);
|
|
14
14
|
const time = record(info.time) ? info.time : undefined;
|
|
15
|
-
if (startedAt) {
|
|
15
|
+
if (startedAt || endedAt) {
|
|
16
16
|
if (typeof time?.created !== "number")
|
|
17
17
|
return null;
|
|
18
|
-
if (time.created < Date.parse(startedAt))
|
|
18
|
+
if (startedAt && time.created < Date.parse(startedAt))
|
|
19
|
+
continue;
|
|
20
|
+
if (endedAt && time.created >= Date.parse(endedAt))
|
|
19
21
|
continue;
|
|
20
22
|
}
|
|
21
23
|
if (typeof time?.completed !== "number")
|
|
@@ -161,20 +161,25 @@ ${MISSION_DEPENDENCY_WORKFLOW}
|
|
|
161
161
|
${profile.toolPrefix}cancel_operator with reason: "plain" to stop its owned children, then
|
|
162
162
|
${profile.toolPrefix}start_mission with intent: "replace" and the saved requirements. The cancelled
|
|
163
163
|
Mission is archived; cumulative spend is retained. Dispatch only the returned Coordinator Task.
|
|
164
|
-
2.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
2. Recommend preferred_executor in start_mission from the original instruction and already-known context.
|
|
165
|
+
Respect explicit user choices. Clear bounded implementation or known procedures normally suit worker;
|
|
166
|
+
coupled design, ambiguous requirements and cross-system integration normally suit coordinator.
|
|
167
|
+
Do not read/search/run diagnostics merely to classify difficulty or fill an Operator planning form.
|
|
168
|
+
A short reason in handoff_context is enough. This recommendation is advisory, not an immutable requirement.
|
|
169
|
+
Coordinator starts useful work immediately and may implement substantially itself or delegate remaining work
|
|
170
|
+
as execution makes that useful, without a separate reassessment or your approval.
|
|
171
|
+
If meaningful formal checks and unit facts are already known, include unit (and execution for a known operation).
|
|
172
|
+
worker/default receives its configured Worker directly; coordinator receives these facts without starting a Worker.
|
|
173
|
+
Unknown checks are discovered during useful author work, not a route-selection preflight.
|
|
174
|
+
Source investigation, shell/environment checks, fix design and output inventory belong with that author.
|
|
169
175
|
Use estimated read/write paths; native scope reconciliation and expand_unit cover actual outputs.
|
|
170
176
|
Write objective as the target or corrective delta: aim for 2000 characters.
|
|
171
177
|
The Worker reads the full original request natively from its handoff once, including exact public reproduction
|
|
172
178
|
inputs, paths and failures; do not copy that request into objective. Preserve it, not a summary.
|
|
173
|
-
Do not list speculative write paths or unrelated test suites as a precaution.
|
|
174
|
-
duration and
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
Investigation, edits, formal checks and any requested commit stay in that Worker before independent Review;
|
|
179
|
+
Do not list speculative write paths or unrelated test suites as a precaution. File count, instruction length,
|
|
180
|
+
duration and availability of a check do not alone determine difficulty. Dispatch the returned
|
|
181
|
+
${profileAgent(profile, "dog-operator")} Task when Coordinator is selected or unit facts are not yet known.
|
|
182
|
+
Investigation, edits, formal checks and any requested commit stay with the chosen author before independent Review;
|
|
178
183
|
do not invent a review-before-commit gate or a commit-only handoff. Preserve explicit user ordering.
|
|
179
184
|
${MISSION_GIT_SCOPE}
|
|
180
185
|
3. After a direct Worker succeeds, use its recorded result and inspect only missing source or evidence
|
|
@@ -470,11 +475,17 @@ ${controls(profile, ["plan_units", "start_direct_unit", "finish_direct_unit", "o
|
|
|
470
475
|
You are Coordinator. Own most of the practical work and dispatch within the saved original request.
|
|
471
476
|
You retain practical implementation authority: investigate, edit, correct, run formal checks and deliver
|
|
472
477
|
in this SAME session when your existing context makes that efficient. You are not a dispatch-only role.
|
|
478
|
+
Use the Operator's preferred_executor as a starting recommendation. Start useful work with available context;
|
|
479
|
+
do not investigate merely to confirm the recommendation. Adapt through actual work, honoring explicit user choices.
|
|
473
480
|
Use plan_units with executor: "self" for direct work, or start_direct_unit for an already prepared unit.
|
|
474
481
|
The host binds your native check results to the candidate; finish_direct_unit closes that work without
|
|
475
482
|
a Worker handoff. Independent Review and Operator final acceptance still follow. This is not limited to
|
|
476
483
|
micro-edits. Delegate to the configured Worker when that reduces work/cost or provides useful separation.
|
|
477
484
|
There is no proposal/approval/contract-repair round trip in this route.
|
|
485
|
+
To hand unfinished direct work to a Worker, call plan_units with executor: "worker", omit units, and put
|
|
486
|
+
the remaining action/useful observations in handoff_context. The host transfers this same unit after current
|
|
487
|
+
native commands settle, retaining changes, checks and budget. Do not finish it as successful or replan just
|
|
488
|
+
to change its author. Dispatch the returned Task once; Operator recommendation does not prevent this choice.
|
|
478
489
|
Use project-relative paths for repository read/search/shell; the native working directory is project_root.
|
|
479
490
|
Do not reconstruct or prepend the absolute workspace path. Preserve explicitly requested external paths.
|
|
480
491
|
|
|
@@ -495,9 +506,10 @@ For a named release in the one-attempt case-study fixture, pass its --release-re
|
|
|
495
506
|
the v0127 matched profile intentionally pins 0.12.7, not the newest release. If these identities differ,
|
|
496
507
|
select the matching runner/profile before starting rather than changing the pinned comparison or spending an arm.
|
|
497
508
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
509
|
+
Continue directly while your context makes that efficient. Delegate when the remaining work is clearer or
|
|
510
|
+
separation reduces effort; no routing-only investigation or mandatory delegation checkpoint. The configured Worker
|
|
511
|
+
can investigate, design, implement and check its assigned work; do not reduce its capability to mechanical edits.
|
|
512
|
+
When delegating, start useful work without redundant source/shell preparation. Call ${profile.toolPrefix}plan_units with concise units:
|
|
501
513
|
title, objective, read/write file or directory scopes, validation commands, and related requirement_ids
|
|
502
514
|
when splitting multiple requirements across multiple units; a single unit inherits all requirements when
|
|
503
515
|
requirement_ids is omitted. For operation missions, include execution with the actual
|
|
@@ -659,8 +671,7 @@ Use bounded documented setup in ${TOOL_ENVIRONMENT}/; reuse, never delete.
|
|
|
659
671
|
User missing-only: installed/incompatible is not missing.
|
|
660
672
|
Per-call modes: check retained creation/use for captured vs current settings.
|
|
661
673
|
Tests: existing suites/subtests or distinctive regression entry names.
|
|
662
|
-
${WORKER_VALIDATION_WORKFLOW}
|
|
663
|
-
No fake success.
|
|
674
|
+
${WORKER_VALIDATION_WORKFLOW}No fake success.
|
|
664
675
|
Parent: independent Review after return, not before execution; no review-before-commit gate.
|
|
665
676
|
Do not spawn nested subagents, amend, push/publish. Return changes, command/exit/elapsed,
|
|
666
677
|
rerun reasons, unresolved/untested behavior; no proof doc.
|