sortie-dogs 0.3.21 → 0.4.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 +1 -1
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/plugin/fast-lane.d.ts +21 -0
- package/dist/plugin/fast-lane.js +230 -0
- package/dist/plugin/index.js +113 -26
- package/dist/plugin/task-result-repair.d.ts +1 -0
- package/dist/plugin/task-result-repair.js +2 -2
- package/dist/runtime-assets.d.ts +10 -10
- package/dist/runtime-assets.js +133 -221
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
20
20
|
|
|
21
21
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
22
22
|
|
|
23
|
-
Release: [v0.
|
|
23
|
+
Release: [v0.4.0](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.4.0)
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export declare const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.3.7-fast-lane-v1";
|
|
6
6
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export const RUNTIME_ASSET_VERSION = "0.3.7-fast-lane-v1";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const BACKLOG_DRAIN_CAPABILITY = "sortie_enable_backlog_drain";
|
|
2
|
+
export type FastLaneDenialCode = "TURN_STATE_REQUIRED" | "ROLE_FORBIDDEN" | "WORKER_LIMIT" | "SCOUT_GAP_REQUIRED" | "SCOUT_LIMIT" | "SCOUT_TOO_LATE" | "REVIEW_EVIDENCE_REQUIRED" | "REVIEW_PHASE_INVALID" | "REVIEW_LIMIT" | "ADVISOR_TRIGGER_REQUIRED" | "ADVISOR_LIMIT" | "CONSULTATION_RETRY_INVALID" | "CONSULTATION_RETRY_UNAUTHORIZED" | "CONSULTATION_RETRY_MISMATCH" | "BACKLOG_DRAIN_INVALID" | "BACKLOG_DRAIN_TOO_LATE" | "MANUAL_COMPACTION_FORBIDDEN";
|
|
3
|
+
export declare class FastLaneDeniedError extends Error {
|
|
4
|
+
readonly code: FastLaneDenialCode;
|
|
5
|
+
constructor(code: FastLaneDenialCode);
|
|
6
|
+
}
|
|
7
|
+
export interface FastLaneToolOptions {
|
|
8
|
+
readonly consultationFallbackAuthorized?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare class FastLaneController {
|
|
11
|
+
private readonly sessions;
|
|
12
|
+
private setSession;
|
|
13
|
+
beginTurn(sessionID: string, synthetic: boolean): void;
|
|
14
|
+
forget(sessionID: string): void;
|
|
15
|
+
manualCompactionForbidden(sessionID: string): boolean;
|
|
16
|
+
backlogDrainEnabled(sessionID: string): boolean;
|
|
17
|
+
backlogContinuationAllowed(sessionID: string): boolean;
|
|
18
|
+
enableBacklogDrain(sessionID: string, maxUnits: number): void;
|
|
19
|
+
continuationQueued(sessionID: string): void;
|
|
20
|
+
beforeTool(sessionID: string, tool: string, args: unknown, options?: FastLaneToolOptions): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { isSourceReviewRiskTag, STRATEGY_TRIGGERS } from "../core/consultation.js";
|
|
2
|
+
const MAX_SESSIONS = 256;
|
|
3
|
+
const GAP_CODES = new Set(["manifest", "validation", "owner-risk"]);
|
|
4
|
+
const STRATEGY_TRIGGER_SET = new Set(STRATEGY_TRIGGERS);
|
|
5
|
+
const MANUAL_COMPACTION_TOOLS = new Set(["compact_and_continue", "sortie_compact_and_continue"]);
|
|
6
|
+
export const BACKLOG_DRAIN_CAPABILITY = "sortie_enable_backlog_drain";
|
|
7
|
+
export class FastLaneDeniedError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code) {
|
|
10
|
+
super(`SORTIE_FAST_LANE_DENIED: ${code}`);
|
|
11
|
+
this.name = "FastLaneDeniedError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function taskArgument(args, key) {
|
|
16
|
+
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
17
|
+
return undefined;
|
|
18
|
+
const value = args[key];
|
|
19
|
+
return typeof value === "string" ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
function lineValue(prompt, key) {
|
|
22
|
+
const values = [...prompt.matchAll(new RegExp(`^\\s*${key}\\s*:\\s*(.+?)\\s*$`, "gmu"))];
|
|
23
|
+
return values.length === 1 ? values[0][1] : undefined;
|
|
24
|
+
}
|
|
25
|
+
function hasReviewEvidence(prompt) {
|
|
26
|
+
if (lineValue(prompt, "canonical_validation_exit") !== "0")
|
|
27
|
+
return false;
|
|
28
|
+
const rawTags = lineValue(prompt, "risk_tags");
|
|
29
|
+
if (rawTags === undefined || !/^\[[^\[\]]+\]$/u.test(rawTags))
|
|
30
|
+
return false;
|
|
31
|
+
const tags = rawTags.slice(1, -1).split(",").map((tag) => tag.trim());
|
|
32
|
+
return tags.length > 0 && tags.every((tag) => tag.length > 0) &&
|
|
33
|
+
new Set(tags).size === tags.length && tags.every(isSourceReviewRiskTag);
|
|
34
|
+
}
|
|
35
|
+
function freshState() {
|
|
36
|
+
return {
|
|
37
|
+
advisorDispatches: 0,
|
|
38
|
+
backlogDrain: false,
|
|
39
|
+
continuationPending: false,
|
|
40
|
+
initialReviewDispatches: 0,
|
|
41
|
+
verificationReviewDispatches: 0,
|
|
42
|
+
scoutDispatches: 0,
|
|
43
|
+
totalWorkerDispatches: 0,
|
|
44
|
+
workerLimit: 1,
|
|
45
|
+
workerDispatches: 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function lockedState() {
|
|
49
|
+
return {
|
|
50
|
+
advisorDispatches: 2,
|
|
51
|
+
backlogDrain: false,
|
|
52
|
+
continuationPending: false,
|
|
53
|
+
initialReviewDispatches: 2,
|
|
54
|
+
verificationReviewDispatches: 2,
|
|
55
|
+
scoutDispatches: 1,
|
|
56
|
+
totalWorkerDispatches: 1,
|
|
57
|
+
workerLimit: 1,
|
|
58
|
+
workerDispatches: 1,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function fallbackBasis(prompt) {
|
|
62
|
+
return prompt.replace(/\r\n?/gu, "\n")
|
|
63
|
+
.split("\n")
|
|
64
|
+
.filter((line) => !/^\s*fallback_retry\s*:\s*true\s*$/u.test(line))
|
|
65
|
+
.join("\n")
|
|
66
|
+
.trimEnd();
|
|
67
|
+
}
|
|
68
|
+
export class FastLaneController {
|
|
69
|
+
sessions = new Map();
|
|
70
|
+
setSession(sessionID, state) {
|
|
71
|
+
this.sessions.delete(sessionID);
|
|
72
|
+
this.sessions.set(sessionID, state);
|
|
73
|
+
while (this.sessions.size > MAX_SESSIONS)
|
|
74
|
+
this.sessions.delete(this.sessions.keys().next().value);
|
|
75
|
+
}
|
|
76
|
+
beginTurn(sessionID, synthetic) {
|
|
77
|
+
if (synthetic) {
|
|
78
|
+
const state = this.sessions.get(sessionID);
|
|
79
|
+
if (state === undefined) {
|
|
80
|
+
this.setSession(sessionID, lockedState());
|
|
81
|
+
}
|
|
82
|
+
else if (state.backlogDrain && state.continuationPending) {
|
|
83
|
+
state.advisorDispatches = 0;
|
|
84
|
+
delete state.advisorPrompt;
|
|
85
|
+
state.continuationPending = false;
|
|
86
|
+
state.initialReviewDispatches = 0;
|
|
87
|
+
delete state.initialReviewPrompt;
|
|
88
|
+
state.verificationReviewDispatches = 0;
|
|
89
|
+
delete state.verificationReviewPrompt;
|
|
90
|
+
state.scoutDispatches = 0;
|
|
91
|
+
state.workerDispatches = 0;
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.setSession(sessionID, freshState());
|
|
96
|
+
}
|
|
97
|
+
forget(sessionID) {
|
|
98
|
+
this.sessions.delete(sessionID);
|
|
99
|
+
}
|
|
100
|
+
manualCompactionForbidden(sessionID) {
|
|
101
|
+
const state = this.sessions.get(sessionID);
|
|
102
|
+
return state !== undefined && state.workerDispatches > 0 &&
|
|
103
|
+
(!state.backlogDrain || state.totalWorkerDispatches >= state.workerLimit);
|
|
104
|
+
}
|
|
105
|
+
backlogDrainEnabled(sessionID) {
|
|
106
|
+
return this.sessions.get(sessionID)?.backlogDrain === true;
|
|
107
|
+
}
|
|
108
|
+
backlogContinuationAllowed(sessionID) {
|
|
109
|
+
const state = this.sessions.get(sessionID);
|
|
110
|
+
return state?.backlogDrain === true && state.workerDispatches === 1 &&
|
|
111
|
+
state.totalWorkerDispatches < state.workerLimit && !state.continuationPending;
|
|
112
|
+
}
|
|
113
|
+
enableBacklogDrain(sessionID, maxUnits) {
|
|
114
|
+
const state = this.sessions.get(sessionID);
|
|
115
|
+
if (state === undefined)
|
|
116
|
+
throw new FastLaneDeniedError("TURN_STATE_REQUIRED");
|
|
117
|
+
if (!Number.isInteger(maxUnits) || maxUnits < 4 || maxUnits > 11) {
|
|
118
|
+
throw new FastLaneDeniedError("BACKLOG_DRAIN_INVALID");
|
|
119
|
+
}
|
|
120
|
+
if (state.backlogDrain || state.totalWorkerDispatches > 0) {
|
|
121
|
+
throw new FastLaneDeniedError("BACKLOG_DRAIN_TOO_LATE");
|
|
122
|
+
}
|
|
123
|
+
state.backlogDrain = true;
|
|
124
|
+
state.workerLimit = maxUnits;
|
|
125
|
+
}
|
|
126
|
+
continuationQueued(sessionID) {
|
|
127
|
+
const state = this.sessions.get(sessionID);
|
|
128
|
+
if (this.backlogContinuationAllowed(sessionID) && state !== undefined) {
|
|
129
|
+
state.continuationPending = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
beforeTool(sessionID, tool, args, options = {}) {
|
|
133
|
+
const state = this.sessions.get(sessionID);
|
|
134
|
+
if (state === undefined) {
|
|
135
|
+
if (tool === "task" || MANUAL_COMPACTION_TOOLS.has(tool)) {
|
|
136
|
+
throw new FastLaneDeniedError("TURN_STATE_REQUIRED");
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (MANUAL_COMPACTION_TOOLS.has(tool)) {
|
|
141
|
+
if (!this.backlogContinuationAllowed(sessionID)) {
|
|
142
|
+
throw new FastLaneDeniedError("MANUAL_COMPACTION_FORBIDDEN");
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (tool !== "task")
|
|
147
|
+
return;
|
|
148
|
+
const role = taskArgument(args, "subagent_type");
|
|
149
|
+
const prompt = taskArgument(args, "prompt") ?? "";
|
|
150
|
+
if (role === "dog-worker") {
|
|
151
|
+
if (state.workerDispatches >= 1 || state.totalWorkerDispatches >= state.workerLimit) {
|
|
152
|
+
throw new FastLaneDeniedError("WORKER_LIMIT");
|
|
153
|
+
}
|
|
154
|
+
state.workerDispatches += 1;
|
|
155
|
+
state.totalWorkerDispatches += 1;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (role === "dog-scout") {
|
|
159
|
+
if (state.workerDispatches > 0)
|
|
160
|
+
throw new FastLaneDeniedError("SCOUT_TOO_LATE");
|
|
161
|
+
const gap = lineValue(prompt, "missing_evidence_code");
|
|
162
|
+
if (gap === undefined || !GAP_CODES.has(gap))
|
|
163
|
+
throw new FastLaneDeniedError("SCOUT_GAP_REQUIRED");
|
|
164
|
+
if (state.scoutDispatches >= 1)
|
|
165
|
+
throw new FastLaneDeniedError("SCOUT_LIMIT");
|
|
166
|
+
state.scoutDispatches += 1;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (role === "dog-reviewer") {
|
|
170
|
+
if (!hasReviewEvidence(prompt))
|
|
171
|
+
throw new FastLaneDeniedError("REVIEW_EVIDENCE_REQUIRED");
|
|
172
|
+
const phase = lineValue(prompt, "review_phase");
|
|
173
|
+
if (phase !== "initial" && phase !== "verification") {
|
|
174
|
+
throw new FastLaneDeniedError("REVIEW_PHASE_INVALID");
|
|
175
|
+
}
|
|
176
|
+
const retry = lineValue(prompt, "fallback_retry");
|
|
177
|
+
const count = phase === "initial" ? state.initialReviewDispatches : state.verificationReviewDispatches;
|
|
178
|
+
const previousPrompt = phase === "initial" ? state.initialReviewPrompt : state.verificationReviewPrompt;
|
|
179
|
+
if (phase === "initial" && state.verificationReviewDispatches > 0) {
|
|
180
|
+
throw new FastLaneDeniedError("REVIEW_PHASE_INVALID");
|
|
181
|
+
}
|
|
182
|
+
if (phase === "verification" && state.initialReviewDispatches === 0) {
|
|
183
|
+
throw new FastLaneDeniedError("REVIEW_PHASE_INVALID");
|
|
184
|
+
}
|
|
185
|
+
if ((count === 0 && retry !== undefined) || (count === 1 && retry !== "true")) {
|
|
186
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_INVALID");
|
|
187
|
+
}
|
|
188
|
+
if (count === 1 && options.consultationFallbackAuthorized !== true) {
|
|
189
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_UNAUTHORIZED");
|
|
190
|
+
}
|
|
191
|
+
if (count === 1 && previousPrompt !== fallbackBasis(prompt)) {
|
|
192
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_MISMATCH");
|
|
193
|
+
}
|
|
194
|
+
if (count >= 2)
|
|
195
|
+
throw new FastLaneDeniedError("REVIEW_LIMIT");
|
|
196
|
+
if (phase === "initial") {
|
|
197
|
+
state.initialReviewPrompt ??= fallbackBasis(prompt);
|
|
198
|
+
state.initialReviewDispatches += 1;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
state.verificationReviewPrompt ??= fallbackBasis(prompt);
|
|
202
|
+
state.verificationReviewDispatches += 1;
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (role === "dog-advisor") {
|
|
207
|
+
const trigger = lineValue(prompt, "strategy_trigger");
|
|
208
|
+
if (trigger === undefined || !STRATEGY_TRIGGER_SET.has(trigger)) {
|
|
209
|
+
throw new FastLaneDeniedError("ADVISOR_TRIGGER_REQUIRED");
|
|
210
|
+
}
|
|
211
|
+
const retry = lineValue(prompt, "fallback_retry");
|
|
212
|
+
if ((state.advisorDispatches === 0 && retry !== undefined) ||
|
|
213
|
+
(state.advisorDispatches === 1 && retry !== "true")) {
|
|
214
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_INVALID");
|
|
215
|
+
}
|
|
216
|
+
if (state.advisorDispatches === 1 && options.consultationFallbackAuthorized !== true) {
|
|
217
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_UNAUTHORIZED");
|
|
218
|
+
}
|
|
219
|
+
if (state.advisorDispatches === 1 && state.advisorPrompt !== fallbackBasis(prompt)) {
|
|
220
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_MISMATCH");
|
|
221
|
+
}
|
|
222
|
+
if (state.advisorDispatches >= 2)
|
|
223
|
+
throw new FastLaneDeniedError("ADVISOR_LIMIT");
|
|
224
|
+
state.advisorPrompt ??= fallbackBasis(prompt);
|
|
225
|
+
state.advisorDispatches += 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
throw new FastLaneDeniedError("ROLE_FORBIDDEN");
|
|
229
|
+
}
|
|
230
|
+
}
|
package/dist/plugin/index.js
CHANGED
|
@@ -6,10 +6,11 @@ import { normalizeRelativePath, RelativePathError } from "../core/path.js";
|
|
|
6
6
|
import { validateManifest } from "../core/validate-manifest.js";
|
|
7
7
|
import { safeSchemaPointer, validateHandoffSchema, validateOperationManifestSchema, } from "../core/validate-schema.js";
|
|
8
8
|
import { DEFAULT_PLUGIN_OPTIONS, resolvePluginConfiguration, resolvePluginConfigurationSourcesWithGlobal, } from "./config.js";
|
|
9
|
-
import { CONTINUATION_CAPABILITY, createContinuationHooks, } from "./continuation.js";
|
|
9
|
+
import { CONTINUATION_CAPABILITY, CONTINUATION_MARKER, ROLLOVER_MARKER, createContinuationHooks, } from "./continuation.js";
|
|
10
10
|
import { WriteDeniedError, canonicalManifestReadScopes, canonicalManifestWriteScopes, createProjectPaths, createWriteGate, describeUnclassifiedCommand, isGitMutation, isKnownReadOnlyTool, isRemoteMutation, normalizeCommand, resolveProjectRoot, safePath, writeScopesOverlap, } from "./gate.js";
|
|
11
|
+
import { BACKLOG_DRAIN_CAPABILITY, FastLaneController } from "./fast-lane.js";
|
|
11
12
|
import { createModelRoutingHook, } from "./model-routing-hook.js";
|
|
12
|
-
import { createTaskResultRepairHook, markConsultationFallbackRetry, } from "./task-result-repair.js";
|
|
13
|
+
import { createTaskResultRepairHook, markConsultationFallbackRetry, taskChildSessionID, } from "./task-result-repair.js";
|
|
13
14
|
import { configRoot, nearestPackageVersion, reflectionEnabled, ReflectionError, ReflectionStore } from "../reflection/index.js";
|
|
14
15
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024 };
|
|
15
16
|
const INSPECTION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
@@ -592,6 +593,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
592
593
|
const bindingOperations = new Set();
|
|
593
594
|
const activeSessions = new Map();
|
|
594
595
|
const coordinatorRoots = new Map();
|
|
596
|
+
const coordinatorTaskCalls = new Map();
|
|
595
597
|
const reflectionOwnedRoots = new Set();
|
|
596
598
|
const reflectionClosingRoots = new Set();
|
|
597
599
|
const reflectionInFlight = new Map();
|
|
@@ -602,6 +604,34 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
602
604
|
const sessionRoots = new Map();
|
|
603
605
|
const consultationRetries = new Map();
|
|
604
606
|
const taskResultRepair = createTaskResultRepairHook(input.client);
|
|
607
|
+
const fastLane = new FastLaneController();
|
|
608
|
+
function beginCoordinatorTask(sessionID, callID) {
|
|
609
|
+
const calls = coordinatorTaskCalls.get(sessionID) ?? new Set();
|
|
610
|
+
calls.add(callID);
|
|
611
|
+
coordinatorTaskCalls.set(sessionID, calls);
|
|
612
|
+
}
|
|
613
|
+
function finishCoordinatorTask(sessionID, callID) {
|
|
614
|
+
if (sessionID === undefined || callID === undefined)
|
|
615
|
+
return;
|
|
616
|
+
const calls = coordinatorTaskCalls.get(sessionID);
|
|
617
|
+
if (calls === undefined)
|
|
618
|
+
return;
|
|
619
|
+
calls.delete(callID);
|
|
620
|
+
if (calls.size === 0)
|
|
621
|
+
coordinatorTaskCalls.delete(sessionID);
|
|
622
|
+
}
|
|
623
|
+
function childHasInFlightParentTask(sessionID) {
|
|
624
|
+
const parentID = sessionParents.get(sessionID);
|
|
625
|
+
return parentID !== undefined && (coordinatorTaskCalls.get(parentID)?.size ?? 0) > 0;
|
|
626
|
+
}
|
|
627
|
+
function abortCoordinatorTasks(sessionID) {
|
|
628
|
+
if (!coordinatorTaskCalls.delete(sessionID))
|
|
629
|
+
return;
|
|
630
|
+
for (const [childID, parentID] of [...sessionParents]) {
|
|
631
|
+
if (parentID === sessionID)
|
|
632
|
+
evictSession(childID);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
605
635
|
function hasSessionEnforcementState(sessionID) {
|
|
606
636
|
return sessionAuthorizations.has(sessionID) || bindingPins.has(sessionID);
|
|
607
637
|
}
|
|
@@ -1465,7 +1495,20 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1465
1495
|
async execute(_args, context) {
|
|
1466
1496
|
// A configuration failure must not remove the loop; the shipped default still resolves.
|
|
1467
1497
|
await ensureLoaded().catch(() => undefined);
|
|
1468
|
-
|
|
1498
|
+
const result = await continuation.tool.execute({}, context);
|
|
1499
|
+
if (result === "SORTIE_COMPACT_AND_CONTINUE_QUEUED") {
|
|
1500
|
+
fastLane.continuationQueued(context.sessionID);
|
|
1501
|
+
}
|
|
1502
|
+
return result;
|
|
1503
|
+
},
|
|
1504
|
+
}),
|
|
1505
|
+
[BACKLOG_DRAIN_CAPABILITY]: defineTool({
|
|
1506
|
+
description: "Enable one explicit bounded backlog drain before its first worker dispatch.",
|
|
1507
|
+
args: { max_units: defineTool.schema.string() },
|
|
1508
|
+
async execute(args, context) {
|
|
1509
|
+
const maxUnits = Number(args.max_units);
|
|
1510
|
+
fastLane.enableBacklogDrain(context.sessionID, maxUnits);
|
|
1511
|
+
return JSON.stringify({ status: "enabled", max_units: maxUnits });
|
|
1469
1512
|
},
|
|
1470
1513
|
}),
|
|
1471
1514
|
...(reflectionStartup ? {
|
|
@@ -1506,7 +1549,19 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1506
1549
|
} : {}),
|
|
1507
1550
|
},
|
|
1508
1551
|
"experimental.text.complete": async (textInput, textOutput) => {
|
|
1552
|
+
const backlogMarker = fastLane.backlogContinuationAllowed(textInput.sessionID) &&
|
|
1553
|
+
textOutput.text.includes(CONTINUATION_MARKER);
|
|
1554
|
+
if (fastLane.manualCompactionForbidden(textInput.sessionID)) {
|
|
1555
|
+
textOutput.text = textOutput.text
|
|
1556
|
+
.replaceAll(ROLLOVER_MARKER, "")
|
|
1557
|
+
.replaceAll(CONTINUATION_MARKER, "")
|
|
1558
|
+
.trimEnd();
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1509
1561
|
await continuation.textComplete(textInput, textOutput);
|
|
1562
|
+
if (backlogMarker && continuation.blocksTool(textInput.sessionID)) {
|
|
1563
|
+
fastLane.continuationQueued(textInput.sessionID);
|
|
1564
|
+
}
|
|
1510
1565
|
},
|
|
1511
1566
|
"experimental.session.compacting": async (compactInput, compactOutput) => {
|
|
1512
1567
|
await continuation.sessionCompacting(compactInput, compactOutput);
|
|
@@ -1516,10 +1571,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1516
1571
|
},
|
|
1517
1572
|
"chat.message": async (chatInput, output) => {
|
|
1518
1573
|
const parentID = chatParentID(chatInput);
|
|
1574
|
+
const synthetic = output.parts.some((part) => isRecord(part) && part.synthetic === true);
|
|
1519
1575
|
if (parentID !== undefined)
|
|
1520
1576
|
rememberParent(chatInput.sessionID, parentID);
|
|
1521
1577
|
const coordinatorOrigin = chatInput.agent === COORDINATOR_AGENT || output.message.agent === COORDINATOR_AGENT;
|
|
1522
1578
|
if (coordinatorOrigin) {
|
|
1579
|
+
fastLane.beginTurn(chatInput.sessionID, synthetic);
|
|
1523
1580
|
releaseSessionEnforcement(chatInput.sessionID);
|
|
1524
1581
|
await rememberCoordinatorRoot(chatInput.sessionID);
|
|
1525
1582
|
}
|
|
@@ -1566,31 +1623,36 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1566
1623
|
}
|
|
1567
1624
|
}
|
|
1568
1625
|
if (coordinatorOrigin) {
|
|
1569
|
-
const synthetic = output.parts.some((part) => isRecord(part) && part.synthetic === true);
|
|
1570
1626
|
continuation.observeModel(chatInput.sessionID, output.message.model, synthetic);
|
|
1571
1627
|
}
|
|
1572
1628
|
},
|
|
1573
|
-
|
|
1574
|
-
|
|
1629
|
+
"experimental.chat.system.transform": async (transformInput, transformOutput) => {
|
|
1630
|
+
if (fastLane.manualCompactionForbidden(transformInput.sessionID)) {
|
|
1631
|
+
transformOutput.system = [...(transformOutput.system ?? []),
|
|
1632
|
+
"SORTIE_FAST_LANE_TERMINAL\nA worker was already dispatched in this normal lane. " +
|
|
1633
|
+
"Do not call a compaction capability or emit a continuation marker. " +
|
|
1634
|
+
"After any required risk-based review or coordinator-owned finalization, return the terminal report and stop."];
|
|
1635
|
+
}
|
|
1636
|
+
if (!reflectionStartup || !(await beginReflection(transformInput.sessionID)))
|
|
1637
|
+
return;
|
|
1638
|
+
const config = reflectionConfiguration;
|
|
1639
|
+
try {
|
|
1640
|
+
if (!config)
|
|
1575
1641
|
return;
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
finally {
|
|
1591
|
-
endReflection(transformInput.sessionID);
|
|
1592
|
-
}
|
|
1593
|
-
} } : {}),
|
|
1642
|
+
const heading = "SORTIE_PROCESS_REFLECTIONS";
|
|
1643
|
+
const buckets = ["run", "project", "global"]
|
|
1644
|
+
.filter((layer) => config.layers[layer])
|
|
1645
|
+
.map((layer) => ({ layer, ...(layer === "global" ? {} : { run: transformInput.sessionID }) }));
|
|
1646
|
+
const budget = Math.max(0, config.maxInjectedTokens - Buffer.byteLength(`${heading}\n`, "utf8"));
|
|
1647
|
+
const text = await reflectionStore.injectBuckets(buckets, config.maxInjectedEntries, budget, reflectionVersion);
|
|
1648
|
+
if (text)
|
|
1649
|
+
transformOutput.system = [...(transformOutput.system ?? []), `${heading}\n${text}`];
|
|
1650
|
+
}
|
|
1651
|
+
catch { /* reflection is strictly non-invasive */ }
|
|
1652
|
+
finally {
|
|
1653
|
+
endReflection(transformInput.sessionID);
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1594
1656
|
"permission.ask": async (permission) => {
|
|
1595
1657
|
if (permission.permission !== "edit")
|
|
1596
1658
|
return;
|
|
@@ -1626,6 +1688,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1626
1688
|
* erases an answer the worker already produced and the coordinator re-dispatches the same work.
|
|
1627
1689
|
*/
|
|
1628
1690
|
"tool.execute.after": async (toolInput, output) => {
|
|
1691
|
+
const completedChildSessionID = toolInput.tool === "task" ? taskChildSessionID(output) : undefined;
|
|
1629
1692
|
try {
|
|
1630
1693
|
const repair = await taskResultRepair(toolInput, output);
|
|
1631
1694
|
if (repair.kind === "unrecoverable-empty" && toolInput.sessionID !== undefined) {
|
|
@@ -1643,6 +1706,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1643
1706
|
}
|
|
1644
1707
|
finally {
|
|
1645
1708
|
activeSessions.get(toolInput.sessionID ?? "")?.inFlightCalls.delete(toolInput.callID ?? "");
|
|
1709
|
+
if (toolInput.tool === "task")
|
|
1710
|
+
finishCoordinatorTask(toolInput.sessionID, toolInput.callID);
|
|
1711
|
+
if (completedChildSessionID !== undefined)
|
|
1712
|
+
evictSession(completedChildSessionID);
|
|
1646
1713
|
}
|
|
1647
1714
|
},
|
|
1648
1715
|
"tool.execute.before": async (toolInput, output) => {
|
|
@@ -1650,6 +1717,18 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1650
1717
|
if (continuation.blocksTool(toolInput.sessionID)) {
|
|
1651
1718
|
throw new Error("SORTIE_ROLLOVER_PENDING: stop this turn and wait for compaction");
|
|
1652
1719
|
}
|
|
1720
|
+
const taskRole = isRecord(output.args) && typeof output.args.subagent_type === "string"
|
|
1721
|
+
? output.args.subagent_type
|
|
1722
|
+
: undefined;
|
|
1723
|
+
const role = consultationAgent(taskRole);
|
|
1724
|
+
const consultationFallbackAuthorized = role !== undefined &&
|
|
1725
|
+
consultationRetries.get(consultationRetryKey(toolInput.sessionID, role))?.phase === "pending";
|
|
1726
|
+
fastLane.beforeTool(toolInput.sessionID, toolInput.tool, output.args, {
|
|
1727
|
+
consultationFallbackAuthorized,
|
|
1728
|
+
});
|
|
1729
|
+
if (toolInput.tool === "task" && taskRole === "dog-worker") {
|
|
1730
|
+
beginCoordinatorTask(toolInput.sessionID, toolInput.callID);
|
|
1731
|
+
}
|
|
1653
1732
|
return;
|
|
1654
1733
|
}
|
|
1655
1734
|
const status = activeSessionStatus(toolInput.sessionID);
|
|
@@ -1747,6 +1826,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1747
1826
|
return;
|
|
1748
1827
|
}
|
|
1749
1828
|
if (event.type === "session.deleted") {
|
|
1829
|
+
fastLane.forget(eventSessionID);
|
|
1830
|
+
abortCoordinatorTasks(eventSessionID);
|
|
1750
1831
|
if (reflectionStore !== undefined && reflectionConfiguration?.layers.run && reflectionOwnedRoots.has(eventSessionID)) {
|
|
1751
1832
|
reflectionClosingRoots.add(eventSessionID);
|
|
1752
1833
|
await waitForReflections(eventSessionID);
|
|
@@ -1785,18 +1866,24 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1785
1866
|
await continuation.sessionCompacted(eventSessionID);
|
|
1786
1867
|
if (event.type === "session.idle")
|
|
1787
1868
|
await continuation.sessionIdle(eventSessionID);
|
|
1869
|
+
if (event.type === "session.idle" && isCoordinatorSession(eventSessionID)) {
|
|
1870
|
+
abortCoordinatorTasks(eventSessionID);
|
|
1871
|
+
}
|
|
1788
1872
|
if (!isActiveSession(eventSessionID))
|
|
1789
1873
|
return;
|
|
1790
1874
|
if (event.type !== "session.idle")
|
|
1791
1875
|
touchActiveSession(eventSessionID);
|
|
1792
1876
|
if (event.type === "session.idle" && eventSessionID !== undefined) {
|
|
1793
1877
|
activeSessions.get(eventSessionID)?.inFlightCalls.clear();
|
|
1878
|
+
if (childHasInFlightParentTask(eventSessionID)) {
|
|
1879
|
+
touchActiveSession(eventSessionID);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1794
1882
|
const authorization = sessionAuthorizations.get(eventSessionID);
|
|
1795
1883
|
if (authorization === undefined)
|
|
1796
1884
|
return;
|
|
1797
1885
|
try {
|
|
1798
|
-
// Idle
|
|
1799
|
-
// worker must bind again, so a completed serial worker cannot block later work.
|
|
1886
|
+
// Idle is the abnormal-exit fallback when the parent Task completion hook never arrives.
|
|
1800
1887
|
await inspect(authorization.handoffPath, eventSessionID);
|
|
1801
1888
|
}
|
|
1802
1889
|
catch {
|
|
@@ -56,6 +56,7 @@ export type TaskResultRepairOutcome = {
|
|
|
56
56
|
export declare const CONSULTATION_FALLBACK_RETRY_MARKER = "SORTIE_CONSULTATION_FALLBACK_RETRY";
|
|
57
57
|
/** Assistant text the child actually produced, ignoring the empty tail that caused the defect. */
|
|
58
58
|
export declare function lastAssistantText(messages: readonly SessionMessage[]): string | undefined;
|
|
59
|
+
export declare function taskChildSessionID(output: TaskToolExecuteOutput): string | undefined;
|
|
59
60
|
/**
|
|
60
61
|
* Repair only the exact defect: a completed `task` call whose result body is empty while the child
|
|
61
62
|
* session holds real assistant text. Everything else is left byte-identical.
|
|
@@ -36,7 +36,7 @@ export function lastAssistantText(messages) {
|
|
|
36
36
|
}
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
|
-
function
|
|
39
|
+
export function taskChildSessionID(output) {
|
|
40
40
|
const metadata = output.metadata;
|
|
41
41
|
if (metadata === null || typeof metadata !== "object")
|
|
42
42
|
return undefined;
|
|
@@ -72,7 +72,7 @@ export function createTaskResultRepairHook(client) {
|
|
|
72
72
|
const match = emptyResultMatch(output);
|
|
73
73
|
if (match === undefined)
|
|
74
74
|
return unchanged;
|
|
75
|
-
const sessionID =
|
|
75
|
+
const sessionID = taskChildSessionID(output);
|
|
76
76
|
if (sessionID === undefined)
|
|
77
77
|
return unchanged;
|
|
78
78
|
let recovered;
|