@hassangameryt/freeflow 0.7.1 → 0.7.3
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +153 -128
- package/capabilities/cognitive-routing/SKILL.md +126 -274
- package/command-surface.json +7 -1
- package/gemini-extension.json +1 -1
- package/package.json +4 -4
- package/pi-extension/dist/cognitive-routing-v2/budget.js +42 -0
- package/pi-extension/dist/cognitive-routing-v2/config.js +136 -0
- package/pi-extension/dist/cognitive-routing-v2/projection.js +330 -0
- package/pi-extension/dist/cognitive-routing-v2/render.js +252 -0
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +2181 -0
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +173 -0
- package/pi-extension/dist/cognitive-routing-v2/state.js +828 -0
- package/pi-extension/dist/cognitive-routing-v2/tools.js +60 -0
- package/pi-extension/dist/cognitive-routing-v2/types.js +48 -0
- package/pi-extension/dist/context-virtualization/runtime.js +29 -7
- package/pi-extension/dist/freeflow-context/tool.js +12 -6
- package/pi-extension/dist/index.js +302 -626
- package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
- package/pi-extension/dist/provider-support/astra/history.js +125 -0
- package/pi-extension/dist/provider-support/astra/index.js +11 -0
- package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
- package/pi-extension/dist/provider-support/index.js +4 -0
- package/pi-extension/dist/runtime/request-history.js +216 -0
- package/pi-extension/dist/runtime/runtime-context.js +85 -16
- package/pi-extension/dist/session-sources/events.js +217 -0
- package/pi-extension/dist/session-sources/provenance.js +32 -0
- package/pi-extension/dist/session-sources/read-only-session.js +148 -0
- package/pi-extension/dist/session-sources/sources.js +277 -0
- package/pi-extension/dist/settings/settings-ui.js +274 -147
- package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
- package/pi-extension/src/cognitive-routing-v2/config.ts +173 -0
- package/pi-extension/src/cognitive-routing-v2/projection.ts +375 -0
- package/pi-extension/src/cognitive-routing-v2/render.ts +277 -0
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +2241 -0
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +180 -0
- package/pi-extension/src/cognitive-routing-v2/state.ts +839 -0
- package/pi-extension/src/cognitive-routing-v2/tools.ts +62 -0
- package/pi-extension/src/cognitive-routing-v2/types.ts +254 -0
- package/pi-extension/src/context-virtualization/runtime.ts +30 -8
- package/pi-extension/src/freeflow-context/tool.ts +12 -6
- package/pi-extension/src/index.ts +308 -675
- package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
- package/pi-extension/src/provider-support/astra/history.ts +145 -0
- package/pi-extension/src/provider-support/astra/index.ts +12 -0
- package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
- package/pi-extension/src/provider-support/index.ts +5 -0
- package/pi-extension/src/runtime/request-history.ts +243 -0
- package/pi-extension/src/runtime/runtime-context.ts +100 -17
- package/pi-extension/src/session-sources/events.ts +230 -0
- package/pi-extension/src/session-sources/provenance.ts +39 -0
- package/pi-extension/src/session-sources/read-only-session.ts +170 -0
- package/pi-extension/src/session-sources/sources.ts +334 -0
- package/pi-extension/src/settings/settings-ui.ts +311 -158
- package/pi-extension/tests/cache-reuse/history.test.js +117 -0
- package/pi-extension/tests/cache-reuse/native.test.js +91 -0
- package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +577 -0
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +474 -0
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +300 -0
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +1034 -0
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +244 -0
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +423 -0
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +586 -0
- package/pi-extension/tests/context-virtualization/extension.test.js +33 -7
- package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
- package/pi-extension/tests/fixtures/routing-native.js +191 -0
- package/pi-extension/tests/integration/mode-free-core.test.js +6 -4
- package/pi-extension/tests/integration/pi-extension.test.js +655 -59
- package/pi-extension/tests/integration/prompt-architecture.test.js +127 -30
- package/pi-extension/tests/integration/unaffected-regressions.test.js +78 -11
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
- package/pi-extension/tests/provider-support/astra.test.js +306 -0
- package/pi-extension/tests/provider-support/cli.test.js +92 -0
- package/pi-extension/tests/provider-support/native.test.js +183 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -5
- package/runtime/prompts/core.md +51 -45
- package/runtime/prompts/interaction-contract.md +5 -5
- package/skills/action-selection/SKILL.md +57 -97
- package/skills/design-for-depth/SKILL.md +58 -193
- package/skills/design-for-depth/references/implementation-decisions.md +76 -0
- package/skills/diagnose-failure/SKILL.md +64 -196
- package/skills/discuss/SKILL.md +64 -123
- package/skills/discuss/references/discussion-continuity.md +7 -3
- package/skills/execute-work/SKILL.md +69 -88
- package/skills/execute-work/references/execute-work-edges.md +9 -3
- package/skills/handoff/SKILL.md +44 -54
- package/skills/handoff/references/handoff-templates.md +15 -6
- package/skills/review-artifact/SKILL.md +80 -127
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
- package/skills/review-work/SKILL.md +65 -115
- package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
- package/skills/simplify-code/SKILL.md +47 -100
- package/skills/track-work/SKILL.md +128 -119
- package/skills/track-work/references/working-record-format.md +14 -2
- package/skills/verify-work/SKILL.md +58 -62
- package/skills/verify-work/references/integration-evidence.md +4 -2
- package/skills/verify-work/references/test-design.md +89 -0
- package/skills/workflow/SKILL.md +73 -119
- package/skills/write-plan/SKILL.md +46 -77
- package/skills/write-plan/references/plan-shapes.md +5 -5
- package/skills/write-spec/SKILL.md +51 -63
- package/skills/write-spec/references/artifact-standards.md +2 -2
- package/skills/write-spec/references/decision-records.md +3 -2
- package/skills/write-spec/references/spec-shapes.md +15 -0
- package/pi-extension/dist/cognitive-routing/commands.js +0 -96
- package/pi-extension/dist/cognitive-routing/config.js +0 -232
- package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
- package/pi-extension/dist/cognitive-routing/history.js +0 -451
- package/pi-extension/dist/cognitive-routing/host.js +0 -188
- package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
- package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
- package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
- package/pi-extension/dist/cognitive-routing/tool.js +0 -212
- package/pi-extension/dist/cognitive-routing/types.js +0 -6
- package/pi-extension/src/cognitive-routing/commands.ts +0 -129
- package/pi-extension/src/cognitive-routing/config.ts +0 -303
- package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
- package/pi-extension/src/cognitive-routing/history.ts +0 -650
- package/pi-extension/src/cognitive-routing/host.ts +0 -266
- package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
- package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
- package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
- package/pi-extension/src/cognitive-routing/tool.ts +0 -273
- package/pi-extension/src/cognitive-routing/types.ts +0 -89
- package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
- package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
- package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
- package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
- package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
- package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
- package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
- package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
- package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
- package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
- package/skills/tdd/SKILL.md +0 -143
- package/skills/tdd/references/test-design.md +0 -96
- /package/pi-extension/tests/{cognitive-routing/host-fixture.js → fixtures/pi-host.js} +0 -0
|
@@ -0,0 +1,2181 @@
|
|
|
1
|
+
import { estimateRequest } from "./budget.js";
|
|
2
|
+
import { annotateSources } from "../session-sources/provenance.js";
|
|
3
|
+
import { ROUTING_SCHEMAS } from "./schemas.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
6
|
+
import { relative, resolve as resolvePath } from "node:path";
|
|
7
|
+
import { EventStore } from "../session-sources/events.js";
|
|
8
|
+
import { Sources, bodyHash, isTaskEvidence } from "../session-sources/sources.js";
|
|
9
|
+
import { pairFromProfile, resolveCognitiveRoutingState } from "./config.js";
|
|
10
|
+
import { prepareView, changeSelection, representationProblems } from "./projection.js";
|
|
11
|
+
import { initialState } from "./state.js";
|
|
12
|
+
import {
|
|
13
|
+
ROUTING_MESSAGE,
|
|
14
|
+
PROFILES,
|
|
15
|
+
RoutingError,
|
|
16
|
+
canonical,
|
|
17
|
+
emptySelection,
|
|
18
|
+
isWorkerProfile,
|
|
19
|
+
requireCondition as check,
|
|
20
|
+
samePair,
|
|
21
|
+
workersForDelegation,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
export const ROUTING_TOOLS = ["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project"];
|
|
24
|
+
const HANDOFF_TOOLS = new Set(["freeflow_delegate", "freeflow_return"]);
|
|
25
|
+
export class RoutingRuntime {
|
|
26
|
+
pi;
|
|
27
|
+
packageRoots;
|
|
28
|
+
store;
|
|
29
|
+
ctx;
|
|
30
|
+
capability;
|
|
31
|
+
token = randomUUID();
|
|
32
|
+
turn;
|
|
33
|
+
messages = [];
|
|
34
|
+
error;
|
|
35
|
+
projectionError;
|
|
36
|
+
suppressed = false;
|
|
37
|
+
applying;
|
|
38
|
+
externalChange = false;
|
|
39
|
+
targetSignature;
|
|
40
|
+
operations = Promise.resolve();
|
|
41
|
+
revision = 0;
|
|
42
|
+
receipts = new Map();
|
|
43
|
+
pages = new Map();
|
|
44
|
+
manualHold;
|
|
45
|
+
automaticControl = false;
|
|
46
|
+
sourceCache;
|
|
47
|
+
constructor(pi, packageRoots = []) {
|
|
48
|
+
this.pi = pi;
|
|
49
|
+
this.packageRoots = packageRoots;
|
|
50
|
+
}
|
|
51
|
+
subject() {
|
|
52
|
+
const branch = this.store?.reader.getBranch() ?? [];
|
|
53
|
+
return {
|
|
54
|
+
token: this.token,
|
|
55
|
+
revision: this.revision,
|
|
56
|
+
store: this.store,
|
|
57
|
+
leafId: branch.at(-1)?.id ?? null,
|
|
58
|
+
userId: branch.filter((e) => e.message?.role === "user").at(-1)?.id ?? null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
current(s) {
|
|
62
|
+
if (s.token !== this.token || s.revision !== this.revision || s.store !== this.store) return false;
|
|
63
|
+
const branch = this.store?.reader.getBranch() ?? [];
|
|
64
|
+
return (
|
|
65
|
+
(!s.leafId || branch.some((e) => e.id === s.leafId)) &&
|
|
66
|
+
(branch.filter((e) => e.message?.role === "user").at(-1)?.id ?? null) === s.userId
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
guard(s) {
|
|
70
|
+
check(
|
|
71
|
+
this.current(s),
|
|
72
|
+
"stale_operation",
|
|
73
|
+
"Routing state changed while the operation was waiting; no further effects applied.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
taskBasis(state, assignmentId) {
|
|
77
|
+
return state.resumeBasis.has(assignmentId)
|
|
78
|
+
? state.resumeBasis.get(assignmentId)
|
|
79
|
+
: state.assignments.get(assignmentId)?.basisUserEntryId;
|
|
80
|
+
}
|
|
81
|
+
enabledWorkers() {
|
|
82
|
+
return workersForDelegation(this.capability?.delegation ?? "executor");
|
|
83
|
+
}
|
|
84
|
+
assignedWorker(state, assignmentId = state.assignmentId) {
|
|
85
|
+
const assignment = assignmentId ? state.assignments.get(assignmentId) : undefined;
|
|
86
|
+
const handoff = assignment ? state.handoffs.get(assignment.delegateHandoffId) : undefined;
|
|
87
|
+
check(
|
|
88
|
+
handoff?.kind === "delegate" && handoff.assignmentId === assignment?.id && isWorkerProfile(handoff.to),
|
|
89
|
+
"assignment_worker_missing",
|
|
90
|
+
"The current assignment has no valid recorded worker.",
|
|
91
|
+
);
|
|
92
|
+
return handoff.to;
|
|
93
|
+
}
|
|
94
|
+
requiredProfiles(state = this.stateData()) {
|
|
95
|
+
const profiles = new Set(["coordinator", ...this.enabledWorkers()]);
|
|
96
|
+
if (state.profile) profiles.add(state.profile);
|
|
97
|
+
if (this.manualHold) profiles.add(this.manualHold);
|
|
98
|
+
const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
99
|
+
if (assignment?.state === "outstanding" || state.recoveryId) profiles.add(this.assignedWorker(state));
|
|
100
|
+
const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
|
|
101
|
+
if (pending && isWorkerProfile(pending.to)) profiles.add(pending.to);
|
|
102
|
+
return [...profiles];
|
|
103
|
+
}
|
|
104
|
+
retireUnbound(reason) {
|
|
105
|
+
for (const execution of this.stateData().executions.values()) {
|
|
106
|
+
if (!execution.assistantEntryId && !execution.interrupted)
|
|
107
|
+
this.append({ type: "execution-interrupted", executionId: execution.id, reason });
|
|
108
|
+
}
|
|
109
|
+
this.turn = undefined;
|
|
110
|
+
}
|
|
111
|
+
openTurn() {
|
|
112
|
+
if (!this.turn || this.turn.opened) return;
|
|
113
|
+
const t = this.turn;
|
|
114
|
+
this.append({
|
|
115
|
+
type: "execution-opened",
|
|
116
|
+
execution: {
|
|
117
|
+
id: t.id,
|
|
118
|
+
profile: t.profile,
|
|
119
|
+
assignmentId: t.assignmentId,
|
|
120
|
+
recoveryId: t.recoveryId,
|
|
121
|
+
basisUserEntryId: t.basisUserEntryId,
|
|
122
|
+
pair: t.pair,
|
|
123
|
+
resultEntryIds: [],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
t.opened = true;
|
|
127
|
+
}
|
|
128
|
+
async settled(ctx) {
|
|
129
|
+
if (!this.store || !this.supported() || this.store.blocked || !ctx.isIdle?.()) return;
|
|
130
|
+
this.ctx = ctx;
|
|
131
|
+
this.retireUnbound("Native run settled without a complete source binding; prior effects remain unresolved.");
|
|
132
|
+
}
|
|
133
|
+
async beforeRun(ctx) {
|
|
134
|
+
this.ctx = ctx;
|
|
135
|
+
if (!this.store || !this.supported() || this.store.blocked || this.error) return;
|
|
136
|
+
if (this.stateData().control === "automatic" && isWorkerProfile(this.stateData().profile)) {
|
|
137
|
+
const result = await this.control("coordinator", false);
|
|
138
|
+
check(result.status !== "blocked", "reconciliation_required", result.reason);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
stateData() {
|
|
142
|
+
return this.store?.state() ?? initialState();
|
|
143
|
+
}
|
|
144
|
+
sources(state = this.stateData()) {
|
|
145
|
+
const entries = this.ctx.sessionManager
|
|
146
|
+
.getBranch()
|
|
147
|
+
.filter((e) => ["message", "custom_message", "compaction", "branch_summary"].includes(e.type));
|
|
148
|
+
const cache = this.sourceCache;
|
|
149
|
+
if (
|
|
150
|
+
cache &&
|
|
151
|
+
cache.authors === state.authors.size &&
|
|
152
|
+
entries.length === cache.entries.length &&
|
|
153
|
+
entries.every((e, i) => e === cache.entries[i])
|
|
154
|
+
)
|
|
155
|
+
return cache.source;
|
|
156
|
+
const active = this.ctx.sessionManager.buildContextEntries?.();
|
|
157
|
+
const activeIds = active ? new Set(active.map((e) => e.id)) : undefined;
|
|
158
|
+
const source = cache?.source ?? new Sources([], state);
|
|
159
|
+
source.refresh(entries, state, activeIds);
|
|
160
|
+
this.sourceCache = { entries, authors: state.authors.size, source };
|
|
161
|
+
return source;
|
|
162
|
+
}
|
|
163
|
+
observed(ctx = this.ctx) {
|
|
164
|
+
return ctx?.model?.provider && ctx?.model?.id && ctx?.thinkingLevel
|
|
165
|
+
? { provider: ctx.model.provider, modelId: ctx.model.id, thinking: ctx.thinkingLevel }
|
|
166
|
+
: undefined;
|
|
167
|
+
}
|
|
168
|
+
supported() {
|
|
169
|
+
return !!this.capability?.effective && !this.suppressed;
|
|
170
|
+
}
|
|
171
|
+
state() {
|
|
172
|
+
let state;
|
|
173
|
+
try {
|
|
174
|
+
state = this.stateData();
|
|
175
|
+
} catch {
|
|
176
|
+
state = initialState();
|
|
177
|
+
}
|
|
178
|
+
const blocked = this.error ?? this.store?.blocked;
|
|
179
|
+
return {
|
|
180
|
+
effective: this.supported() && state.control !== "inactive" && !blocked,
|
|
181
|
+
activeProfile: state.profile,
|
|
182
|
+
delegation: this.capability?.delegation ?? "executor",
|
|
183
|
+
controlMode: state.control === "manual" ? `manual-${state.profile}` : state.control,
|
|
184
|
+
runtimeStatus: blocked ? "blocked" : this.supported() && state.control !== "inactive" ? "active" : "inactive",
|
|
185
|
+
runtimeReason: blocked ?? (this.suppressed ? "startup_selection" : this.capability?.blockingReason.message),
|
|
186
|
+
projectionFailure: this.projectionError,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
get projectionEnabled() {
|
|
190
|
+
return this.supported() && this.capability?.projection === true && this.stateData().control === "automatic";
|
|
191
|
+
}
|
|
192
|
+
append(data, op, step) {
|
|
193
|
+
check(this.store, "routing_unavailable");
|
|
194
|
+
return this.store.append(this.store.make(data, op, step));
|
|
195
|
+
}
|
|
196
|
+
enqueue(action) {
|
|
197
|
+
const token = this.token;
|
|
198
|
+
const result = this.operations.then(() => {
|
|
199
|
+
check(token === this.token, "stale_instance");
|
|
200
|
+
return action();
|
|
201
|
+
});
|
|
202
|
+
this.operations = result.catch(() => {});
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
mark(error) {
|
|
206
|
+
if (this.error && error instanceof RoutingError && error.code === "routing_blocked") return;
|
|
207
|
+
this.error =
|
|
208
|
+
error instanceof RoutingError
|
|
209
|
+
? `${error.code}: ${error.message}`
|
|
210
|
+
: error instanceof Error
|
|
211
|
+
? error.message
|
|
212
|
+
: String(error);
|
|
213
|
+
}
|
|
214
|
+
assertAvailable(profile) {
|
|
215
|
+
check(
|
|
216
|
+
this.supported() && this.store && !this.error && !this.store.blocked,
|
|
217
|
+
"routing_unavailable",
|
|
218
|
+
this.error ?? this.store?.blocked ?? "Routing is inactive.",
|
|
219
|
+
);
|
|
220
|
+
const state = this.stateData();
|
|
221
|
+
check(state.control === "automatic", "manual_control");
|
|
222
|
+
if (profile) check(state.profile === profile && this.turn?.profile === profile, "wrong_profile");
|
|
223
|
+
if (state.profile) check(samePair(this.observed(), this.profilePair(state.profile)), "configuration_mismatch");
|
|
224
|
+
}
|
|
225
|
+
configuredProfilePair(profile) {
|
|
226
|
+
const configured = this.capability?.profiles[profile];
|
|
227
|
+
check(configured, "profile_missing");
|
|
228
|
+
return pairFromProfile(configured);
|
|
229
|
+
}
|
|
230
|
+
profilePair(profile) {
|
|
231
|
+
return this.stateData().profileOverrides.get(profile) ?? this.configuredProfilePair(profile);
|
|
232
|
+
}
|
|
233
|
+
profilePairs(profiles = this.requiredProfiles()) {
|
|
234
|
+
return Object.fromEntries(profiles.map((profile) => [profile, this.profilePair(profile)]));
|
|
235
|
+
}
|
|
236
|
+
async validateProfilePairs(pairs) {
|
|
237
|
+
check(pairs.coordinator, "profile_missing", "Configure the coordinator profile.");
|
|
238
|
+
const helper = pairs.helper !== undefined;
|
|
239
|
+
const executor = pairs.executor !== undefined;
|
|
240
|
+
check(helper || executor, "profile_missing", "Configure an enabled worker profile.");
|
|
241
|
+
const delegation = helper && executor ? "both" : helper ? "helper" : "executor";
|
|
242
|
+
const profiles = Object.fromEntries(
|
|
243
|
+
Object.entries(pairs).map(([profile, pair]) => [
|
|
244
|
+
profile,
|
|
245
|
+
{ provider: pair.provider, model: pair.modelId, thinking: pair.thinking },
|
|
246
|
+
]),
|
|
247
|
+
);
|
|
248
|
+
const result = await resolveCognitiveRoutingState(
|
|
249
|
+
{ cognitiveRouting: { enabled: true, delegation, profiles } },
|
|
250
|
+
{},
|
|
251
|
+
this.ctx,
|
|
252
|
+
);
|
|
253
|
+
check(result.effective, result.blockingReason.code, result.blockingReason.message);
|
|
254
|
+
}
|
|
255
|
+
model(profile) {
|
|
256
|
+
const pair = this.profilePair(profile);
|
|
257
|
+
const model = this.ctx?.modelRegistry?.find(pair.provider, pair.modelId);
|
|
258
|
+
check(model, "profile_unavailable");
|
|
259
|
+
return model;
|
|
260
|
+
}
|
|
261
|
+
async bind(ctx, capability, startup = false) {
|
|
262
|
+
this.token = randomUUID();
|
|
263
|
+
this.ctx = ctx;
|
|
264
|
+
this.capability = capability;
|
|
265
|
+
this.turn = undefined;
|
|
266
|
+
this.manualHold = undefined;
|
|
267
|
+
this.automaticControl = false;
|
|
268
|
+
this.messages = [];
|
|
269
|
+
this.sourceCache = undefined;
|
|
270
|
+
this.receipts.clear();
|
|
271
|
+
this.pages.clear();
|
|
272
|
+
this.applying = undefined;
|
|
273
|
+
this.externalChange = false;
|
|
274
|
+
this.error = undefined;
|
|
275
|
+
this.projectionError = undefined;
|
|
276
|
+
this.operations = Promise.resolve();
|
|
277
|
+
this.targetSignature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
|
|
278
|
+
this.suppressed = startup && process.argv.some((a) => /^(--model|--thinking)(=|$)/.test(a));
|
|
279
|
+
this.store = new EventStore(this.pi, ctx.sessionManager);
|
|
280
|
+
const subject = this.subject();
|
|
281
|
+
if (!capability.effective || this.suppressed) return;
|
|
282
|
+
try {
|
|
283
|
+
await this.store.reconcile();
|
|
284
|
+
this.guard(subject);
|
|
285
|
+
const state = this.stateData();
|
|
286
|
+
this.manualHold = state.control === "manual" ? state.profile : undefined;
|
|
287
|
+
this.automaticControl = !state.events.size || state.control === "automatic";
|
|
288
|
+
await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
|
|
289
|
+
this.retireUnbound("Session rebind found an unfinished execution; no task effects replayed.");
|
|
290
|
+
if (!state.events.size) {
|
|
291
|
+
this.append({
|
|
292
|
+
type: "control",
|
|
293
|
+
control: "automatic",
|
|
294
|
+
profile: "coordinator",
|
|
295
|
+
reason: "Initial routing activation",
|
|
296
|
+
});
|
|
297
|
+
await this.applyPair(this.profilePair("coordinator"));
|
|
298
|
+
} else if (
|
|
299
|
+
state.control === "automatic" &&
|
|
300
|
+
state.profile &&
|
|
301
|
+
!samePair(this.observed(), this.profilePair(state.profile))
|
|
302
|
+
) {
|
|
303
|
+
// Reload reconstructs responsibility; it does not silently reapply the last setter.
|
|
304
|
+
this.append({
|
|
305
|
+
type: "control",
|
|
306
|
+
control: "automatic",
|
|
307
|
+
profile: "coordinator",
|
|
308
|
+
reason: "Rebind reconciles historical responsibility with current host control",
|
|
309
|
+
});
|
|
310
|
+
await this.applyPair(this.profilePair("coordinator"));
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (this.current(subject)) this.mark(error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
unbind() {
|
|
317
|
+
this.token = randomUUID();
|
|
318
|
+
this.store = undefined;
|
|
319
|
+
this.turn = undefined;
|
|
320
|
+
this.messages = [];
|
|
321
|
+
this.ctx = undefined;
|
|
322
|
+
this.sourceCache = undefined;
|
|
323
|
+
this.receipts.clear();
|
|
324
|
+
this.pages.clear();
|
|
325
|
+
}
|
|
326
|
+
async refresh(ctx, capability) {
|
|
327
|
+
this.ctx = ctx;
|
|
328
|
+
const was = this.capability?.effective === true;
|
|
329
|
+
const signature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
|
|
330
|
+
if (this.capability && (was !== capability.effective || signature !== this.targetSignature)) this.revision++;
|
|
331
|
+
this.capability = capability;
|
|
332
|
+
if (!capability.effective) {
|
|
333
|
+
this.turn = undefined;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (!this.store) {
|
|
337
|
+
await this.bind(ctx, capability);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (!was) {
|
|
341
|
+
const subject = this.subject();
|
|
342
|
+
this.suppressed = false;
|
|
343
|
+
try {
|
|
344
|
+
await this.store.reconcile();
|
|
345
|
+
this.guard(subject);
|
|
346
|
+
this.error = undefined;
|
|
347
|
+
this.append({
|
|
348
|
+
type: "control",
|
|
349
|
+
control: "automatic",
|
|
350
|
+
profile: "coordinator",
|
|
351
|
+
reason: "Capability re-enabled; Coordinator reconciliation",
|
|
352
|
+
});
|
|
353
|
+
this.automaticControl = true;
|
|
354
|
+
await this.applyPair(this.profilePair("coordinator"));
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (this.current(subject)) this.mark(error);
|
|
357
|
+
}
|
|
358
|
+
} else if (this.targetSignature && signature !== this.targetSignature) {
|
|
359
|
+
this.error = "configuration_changed: reconcile profile configuration before automatic execution";
|
|
360
|
+
}
|
|
361
|
+
if (canonical({ delegation: this.capability?.delegation, profiles: this.capability?.profiles }) === signature)
|
|
362
|
+
this.targetSignature = signature;
|
|
363
|
+
}
|
|
364
|
+
async ancestryChanged(ctx, navigation = true) {
|
|
365
|
+
this.revision++;
|
|
366
|
+
this.ctx = ctx;
|
|
367
|
+
this.turn = undefined;
|
|
368
|
+
this.messages = [];
|
|
369
|
+
this.sourceCache = undefined;
|
|
370
|
+
this.receipts.clear();
|
|
371
|
+
this.pages.clear();
|
|
372
|
+
if (!this.store || !this.supported()) return;
|
|
373
|
+
const subject = this.subject();
|
|
374
|
+
try {
|
|
375
|
+
await this.store.reconcile();
|
|
376
|
+
this.guard(subject);
|
|
377
|
+
this.error = undefined;
|
|
378
|
+
const state = this.stateData();
|
|
379
|
+
await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
|
|
380
|
+
this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
|
|
381
|
+
if (this.manualHold) {
|
|
382
|
+
this.append({
|
|
383
|
+
type: "control",
|
|
384
|
+
control: "manual",
|
|
385
|
+
profile: this.manualHold,
|
|
386
|
+
reason: "Current explicit manual hold survives navigation",
|
|
387
|
+
});
|
|
388
|
+
await this.applyPair(this.profilePair(this.manualHold));
|
|
389
|
+
} else if (
|
|
390
|
+
(this.automaticControl || !state.events.size) &&
|
|
391
|
+
(navigation || !samePair(this.observed(), this.profilePair(state.profile ?? "coordinator")))
|
|
392
|
+
) {
|
|
393
|
+
this.append({
|
|
394
|
+
type: "control",
|
|
395
|
+
control: "automatic",
|
|
396
|
+
profile: "coordinator",
|
|
397
|
+
reason: "Coordinator reconciles selected native ancestry",
|
|
398
|
+
});
|
|
399
|
+
await this.applyPair(this.profilePair("coordinator"));
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (this.current(subject)) this.mark(error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async nativeChange(ctx) {
|
|
406
|
+
this.ctx = ctx;
|
|
407
|
+
if (!this.supported() || !this.store) return;
|
|
408
|
+
if (this.applying) {
|
|
409
|
+
if (ctx.model?.provider !== this.applying.provider || ctx.model?.id !== this.applying.modelId) {
|
|
410
|
+
this.externalChange = true;
|
|
411
|
+
this.revision++;
|
|
412
|
+
}
|
|
413
|
+
return; // Never await a queue already held by our setter.
|
|
414
|
+
}
|
|
415
|
+
const state = this.stateData();
|
|
416
|
+
if (state.control !== "automatic" || !state.profile || samePair(this.observed(), this.profilePair(state.profile)))
|
|
417
|
+
return;
|
|
418
|
+
try {
|
|
419
|
+
this.revision++;
|
|
420
|
+
this.manualHold = undefined;
|
|
421
|
+
this.automaticControl = false;
|
|
422
|
+
this.append({ type: "control", control: "inactive", reason: "External native model/effort change" });
|
|
423
|
+
} catch (error) {
|
|
424
|
+
this.mark(error);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async applyPair(target) {
|
|
428
|
+
const subject = this.subject(),
|
|
429
|
+
token = this.token,
|
|
430
|
+
prior = this.observed();
|
|
431
|
+
if (samePair(prior, target)) return;
|
|
432
|
+
const model = this.ctx.modelRegistry.find(target.provider, target.modelId);
|
|
433
|
+
check(model, "profile_unavailable");
|
|
434
|
+
const auth = await this.ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
435
|
+
this.guard(subject);
|
|
436
|
+
check(auth?.ok, "profile_unauthenticated");
|
|
437
|
+
this.applying = target;
|
|
438
|
+
let ownedPair = target;
|
|
439
|
+
this.externalChange = false;
|
|
440
|
+
try {
|
|
441
|
+
check(await this.pi.setModel(model), "model_rejected");
|
|
442
|
+
this.guard(subject);
|
|
443
|
+
this.pi.setThinkingLevel(target.thinking);
|
|
444
|
+
check(!this.externalChange && samePair(this.observed(), target), "configuration_mismatch");
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (this.current(subject) && prior && !this.externalChange) {
|
|
447
|
+
const old = this.ctx.modelRegistry.find(prior.provider, prior.modelId);
|
|
448
|
+
try {
|
|
449
|
+
this.applying = prior;
|
|
450
|
+
ownedPair = prior;
|
|
451
|
+
check(old && (await this.pi.setModel(old)), "rollback_failed");
|
|
452
|
+
this.guard(subject);
|
|
453
|
+
this.pi.setThinkingLevel(prior.thinking);
|
|
454
|
+
check(samePair(this.observed(), prior), "rollback_failed");
|
|
455
|
+
} catch {
|
|
456
|
+
if (this.current(subject)) this.error = "configuration_unknown: rollback could not be observed";
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
throw error;
|
|
460
|
+
} finally {
|
|
461
|
+
if (token === this.token && this.applying === ownedPair) this.applying = undefined;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async control(profile, manual) {
|
|
465
|
+
this.revision++;
|
|
466
|
+
const subject = this.subject();
|
|
467
|
+
return this.enqueue(async () => {
|
|
468
|
+
try {
|
|
469
|
+
check(this.capability?.effective && this.store, "routing_unavailable");
|
|
470
|
+
if (manual && isWorkerProfile(profile))
|
|
471
|
+
check(
|
|
472
|
+
this.enabledWorkers().includes(profile),
|
|
473
|
+
"worker_disabled",
|
|
474
|
+
`${profile} is not enabled by delegation mode.`,
|
|
475
|
+
);
|
|
476
|
+
await this.store.reconcile();
|
|
477
|
+
this.guard(subject);
|
|
478
|
+
this.error = undefined;
|
|
479
|
+
this.suppressed = false;
|
|
480
|
+
const previous = this.stateData();
|
|
481
|
+
if (
|
|
482
|
+
previous.control === (manual ? "manual" : "automatic") &&
|
|
483
|
+
previous.profile === profile &&
|
|
484
|
+
samePair(this.observed(), this.profilePair(profile))
|
|
485
|
+
) {
|
|
486
|
+
this.manualHold = manual ? profile : undefined;
|
|
487
|
+
this.automaticControl = !manual;
|
|
488
|
+
return { status: manual ? "active" : "automatic" };
|
|
489
|
+
}
|
|
490
|
+
this.append({
|
|
491
|
+
type: "control",
|
|
492
|
+
control: manual ? "manual" : "automatic",
|
|
493
|
+
profile,
|
|
494
|
+
reason: manual ? "Explicit user manual hold" : "Explicit user automatic release/reconciliation",
|
|
495
|
+
});
|
|
496
|
+
try {
|
|
497
|
+
await this.applyPair(this.profilePair(profile));
|
|
498
|
+
this.guard(subject);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (this.current(subject) && !this.error && !this.store.blocked)
|
|
501
|
+
this.append({
|
|
502
|
+
type: "control",
|
|
503
|
+
control: previous.control,
|
|
504
|
+
profile: previous.profile,
|
|
505
|
+
reason: "Control request failed; prior control retained",
|
|
506
|
+
});
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
this.manualHold = manual ? profile : undefined;
|
|
510
|
+
this.automaticControl = !manual;
|
|
511
|
+
return { status: manual ? "active" : "automatic" };
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (this.current(subject) && !(error instanceof RoutingError && error.code === "worker_disabled"))
|
|
514
|
+
this.mark(error);
|
|
515
|
+
return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
setManualProfile(profile, _mechanism) {
|
|
520
|
+
return this.control(profile, true);
|
|
521
|
+
}
|
|
522
|
+
sessionProfileOverrides() {
|
|
523
|
+
return Object.fromEntries(this.stateData().profileOverrides);
|
|
524
|
+
}
|
|
525
|
+
async setSessionProfileOverride(profile, override, mechanism = "Session profile override") {
|
|
526
|
+
this.revision++;
|
|
527
|
+
const subject = this.subject();
|
|
528
|
+
return this.enqueue(async () => {
|
|
529
|
+
try {
|
|
530
|
+
check(this.capability?.effective && this.store, "routing_unavailable");
|
|
531
|
+
check(
|
|
532
|
+
this.ctx?.isIdle?.() !== false,
|
|
533
|
+
"host_busy",
|
|
534
|
+
"Wait for Pi to become idle before changing session presets.",
|
|
535
|
+
);
|
|
536
|
+
await this.store.reconcile();
|
|
537
|
+
this.guard(subject);
|
|
538
|
+
const state = this.stateData();
|
|
539
|
+
const previous = state.profileOverrides.get(profile);
|
|
540
|
+
if ((override === null && !previous) || (override !== null && samePair(previous, override)))
|
|
541
|
+
return { status: "unchanged" };
|
|
542
|
+
const target = override ?? this.configuredProfilePair(profile);
|
|
543
|
+
const profiles = new Set(this.requiredProfiles(state));
|
|
544
|
+
profiles.add(profile);
|
|
545
|
+
const pairs = this.profilePairs([...profiles]);
|
|
546
|
+
pairs[profile] = target;
|
|
547
|
+
await this.validateProfilePairs(pairs);
|
|
548
|
+
this.append({
|
|
549
|
+
type: "profile-overrides",
|
|
550
|
+
overrides: { [profile]: override },
|
|
551
|
+
reason: mechanism,
|
|
552
|
+
});
|
|
553
|
+
if (state.control !== "inactive" && state.profile === profile) {
|
|
554
|
+
try {
|
|
555
|
+
await this.applyPair(target);
|
|
556
|
+
this.guard(subject);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
|
|
559
|
+
this.append({
|
|
560
|
+
type: "profile-overrides",
|
|
561
|
+
overrides: { [profile]: previous ?? null },
|
|
562
|
+
reason: "Session profile application failed; prior override retained",
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
this.error = undefined;
|
|
568
|
+
return { status: "active" };
|
|
569
|
+
}
|
|
570
|
+
this.error = undefined;
|
|
571
|
+
return { status: "stored" };
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (this.current(subject)) this.mark(error);
|
|
574
|
+
return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
async resetSessionProfileOverrides(mechanism = "Reset session profile overrides") {
|
|
579
|
+
this.revision++;
|
|
580
|
+
const subject = this.subject();
|
|
581
|
+
return this.enqueue(async () => {
|
|
582
|
+
try {
|
|
583
|
+
check(this.capability?.effective && this.store, "routing_unavailable");
|
|
584
|
+
check(
|
|
585
|
+
this.ctx?.isIdle?.() !== false,
|
|
586
|
+
"host_busy",
|
|
587
|
+
"Wait for Pi to become idle before resetting session presets.",
|
|
588
|
+
);
|
|
589
|
+
await this.store.reconcile();
|
|
590
|
+
this.guard(subject);
|
|
591
|
+
const state = this.stateData();
|
|
592
|
+
if (state.profileOverrides.size === 0) return { status: "unchanged" };
|
|
593
|
+
const previous = Object.fromEntries(state.profileOverrides);
|
|
594
|
+
const required = this.requiredProfiles(state);
|
|
595
|
+
const targetPairs = Object.fromEntries(
|
|
596
|
+
required.map((profile) => [profile, this.configuredProfilePair(profile)]),
|
|
597
|
+
);
|
|
598
|
+
await this.validateProfilePairs(targetPairs);
|
|
599
|
+
this.append({
|
|
600
|
+
type: "profile-overrides",
|
|
601
|
+
overrides: { coordinator: null, helper: null, executor: null },
|
|
602
|
+
reason: mechanism,
|
|
603
|
+
});
|
|
604
|
+
const active = state.control !== "inactive" ? state.profile : undefined;
|
|
605
|
+
if (active && previous[active]) {
|
|
606
|
+
try {
|
|
607
|
+
await this.applyPair(targetPairs[active]);
|
|
608
|
+
this.guard(subject);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
|
|
611
|
+
this.append({
|
|
612
|
+
type: "profile-overrides",
|
|
613
|
+
overrides: {
|
|
614
|
+
coordinator: previous.coordinator ?? null,
|
|
615
|
+
helper: previous.helper ?? null,
|
|
616
|
+
executor: previous.executor ?? null,
|
|
617
|
+
},
|
|
618
|
+
reason: "Session profile reset failed; prior overrides retained",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
this.error = undefined;
|
|
624
|
+
return { status: "active" };
|
|
625
|
+
}
|
|
626
|
+
this.error = undefined;
|
|
627
|
+
return { status: "stored" };
|
|
628
|
+
} catch (error) {
|
|
629
|
+
if (this.current(subject)) this.mark(error);
|
|
630
|
+
return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
setAutomaticControl(_mechanism) {
|
|
635
|
+
return this.control("coordinator", false);
|
|
636
|
+
}
|
|
637
|
+
lastDeliveredUser(sources, messages) {
|
|
638
|
+
const delivered = new Set(
|
|
639
|
+
sources.associate(messages).flatMap((x) => (x.source?.message.role === "user" ? [x.source.entry.id] : [])),
|
|
640
|
+
);
|
|
641
|
+
// Compaction may remove every user message from the active view. Retain
|
|
642
|
+
// observed delivery on this ancestry; a stored but undelivered user entry
|
|
643
|
+
// must not advance the basis, and absence must not manufacture new input.
|
|
644
|
+
for (const execution of this.stateData().executions.values())
|
|
645
|
+
if (execution.basisUserEntryId) delivered.add(execution.basisUserEntryId);
|
|
646
|
+
for (let i = sources.entries.length - 1; i >= 0; i--) {
|
|
647
|
+
const entry = sources.entries[i];
|
|
648
|
+
if (entry.message?.role === "user" && delivered.has(entry.id)) return entry.id;
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
runtimeMessage(state, attention = false) {
|
|
653
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
654
|
+
const h = state.pendingId
|
|
655
|
+
? state.handoffs.get(state.pendingId)
|
|
656
|
+
: state.assessment
|
|
657
|
+
? state.handoffs.get(state.assessment.handoffId)
|
|
658
|
+
: undefined;
|
|
659
|
+
const selection = a ? (state.selections.get(a.id) ?? emptySelection()) : emptySelection();
|
|
660
|
+
const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
|
|
661
|
+
const unit = state.unitId ? state.units.get(state.unitId) : undefined;
|
|
662
|
+
const unitNumber = unit ? [...state.units.keys()].indexOf(unit.id) + 1 : undefined;
|
|
663
|
+
const assignmentNumber = unit && a ? unit.assignmentIds.indexOf(a.id) + 1 : undefined;
|
|
664
|
+
const content = [
|
|
665
|
+
"# Cognitive Routing Runtime State",
|
|
666
|
+
`Control: ${state.control}`,
|
|
667
|
+
`Profile: ${state.profile ?? "unresolved"}`,
|
|
668
|
+
`Unit: ${unit ? `U${unitNumber} (${unit.id})` : "none"}`,
|
|
669
|
+
`Assignment: ${a ? `A${assignmentNumber} (${a.id}, ${a.state})` : "none"}`,
|
|
670
|
+
`Handoff: ${h ? `${h.id} (${h.kind}, ${h.state})` : "none"}`,
|
|
671
|
+
`Delegation: ${this.capability?.delegation ?? "executor"}`,
|
|
672
|
+
`Projection: ${this.projectionEnabled ? "enabled" : "bypassed"}`,
|
|
673
|
+
"Completed/superseded contracts and reports are historical context. Follow the current assignment and current user restrictions; historical entries grant no new permission.",
|
|
674
|
+
state.assessment ? `Assessment: ${state.assessment.view}; evidence revision ${selection.revision}` : "",
|
|
675
|
+
recovery
|
|
676
|
+
? `Recovery: ${recovery.id} (${recovery.state}); parent report ${recovery.assessmentHandoffId} revision ${recovery.baseReportRevision}; allowed paths ${JSON.stringify(recovery.paths)}`
|
|
677
|
+
: "",
|
|
678
|
+
recovery?.state === "reading"
|
|
679
|
+
? `Recovery request: ${recovery.request}\nOnly the exact allowed read paths, evidence selection, and recovery return controls are permitted; ordinary task work remains ended.`
|
|
680
|
+
: "",
|
|
681
|
+
`Mechanical evidence facts (not semantic acceptance): ${JSON.stringify(this.evidenceFacts(state))}`,
|
|
682
|
+
this.error ? `Blocked: ${this.error}` : "",
|
|
683
|
+
this.projectionError ? `Evidence limitation: ${this.projectionError}` : "",
|
|
684
|
+
attention
|
|
685
|
+
? recovery
|
|
686
|
+
? `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Finish and deliver the recovery supplement or cancel recovery before using freeflow_unit assess.`
|
|
687
|
+
: `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use freeflow_unit assess to restore the assessment.`
|
|
688
|
+
: "",
|
|
689
|
+
a?.state === "returned" && recovery?.state !== "reading"
|
|
690
|
+
? "Worker ordinary task work has ended. Only supported handoff correction is permitted."
|
|
691
|
+
: "",
|
|
692
|
+
isWorkerProfile(this.turn?.profile) && a && this.turn.basisUserEntryId !== this.workerBasis(state, a.id)
|
|
693
|
+
? `Current input differs from the active ${this.turn.profile} responsibility. Account for it; changed direction requires Coordinator attention.`
|
|
694
|
+
: "",
|
|
695
|
+
...(state.assessment?.problems ?? []).map((p) => `${p.code}: ${p.detail}`),
|
|
696
|
+
]
|
|
697
|
+
.filter(Boolean)
|
|
698
|
+
.join("\n");
|
|
699
|
+
return {
|
|
700
|
+
role: "custom",
|
|
701
|
+
customType: ROUTING_MESSAGE,
|
|
702
|
+
content,
|
|
703
|
+
display: false,
|
|
704
|
+
timestamp: 0,
|
|
705
|
+
details: { routingInstance: this.token },
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
evidenceFacts(state) {
|
|
709
|
+
const selection = state.assignmentId
|
|
710
|
+
? (state.selections.get(state.assignmentId) ?? emptySelection())
|
|
711
|
+
: emptySelection();
|
|
712
|
+
const sources = this.sources(state);
|
|
713
|
+
return {
|
|
714
|
+
revision: selection.revision,
|
|
715
|
+
selected: selection.selected.map((ref) => {
|
|
716
|
+
const entry = this.ctx?.sessionManager.getEntry?.(ref.slice(4).replace(/#text$/, ""));
|
|
717
|
+
const locator = sources.locator(ref);
|
|
718
|
+
return {
|
|
719
|
+
ref,
|
|
720
|
+
kind: ref.endsWith("#text") ? "assistant-text" : (entry?.message?.role ?? "unknown"),
|
|
721
|
+
toolName: entry?.message?.toolName,
|
|
722
|
+
producer: state.authors.get(entry?.id)?.profile ?? "common",
|
|
723
|
+
assignment: state.authors.get(entry?.id)?.assignmentId,
|
|
724
|
+
...(locator ? { locator } : {}),
|
|
725
|
+
};
|
|
726
|
+
}),
|
|
727
|
+
unresolved: selection.unresolved,
|
|
728
|
+
withdrawals: selection.withdrawals,
|
|
729
|
+
preparedRevision: state.assessment?.reservation?.selectionRevision,
|
|
730
|
+
limitations: state.assessment?.problems ?? [],
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
prepared(profile, input, handoffId, restoring = false) {
|
|
734
|
+
const state = this.stateData(),
|
|
735
|
+
model = this.model(profile);
|
|
736
|
+
return prepareView({
|
|
737
|
+
messages: input,
|
|
738
|
+
sources: this.sources(state),
|
|
739
|
+
state,
|
|
740
|
+
view: profile,
|
|
741
|
+
projection: this.projectionEnabled,
|
|
742
|
+
model,
|
|
743
|
+
pair: this.profilePair(profile),
|
|
744
|
+
systemPrompt: this.ctx.getSystemPrompt?.() ?? "",
|
|
745
|
+
tools: (this.pi.getAllTools?.() ?? [])
|
|
746
|
+
.filter((tool) => this.pi.getActiveTools?.().includes(tool.name))
|
|
747
|
+
.map((tool) =>
|
|
748
|
+
ROUTING_TOOLS.includes(tool.name) ? { ...tool, parameters: ROUTING_SCHEMAS[tool.name] } : tool,
|
|
749
|
+
),
|
|
750
|
+
runtimeMessage: this.runtimeMessage(
|
|
751
|
+
state,
|
|
752
|
+
profile === "coordinator" && state.assessment?.view === "suspended" && !restoring && !handoffId,
|
|
753
|
+
),
|
|
754
|
+
instance: this.token,
|
|
755
|
+
preparingReturn: handoffId,
|
|
756
|
+
restoring,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
budgetNotice(messages, ctx) {
|
|
760
|
+
if (!this.supported()) return;
|
|
761
|
+
const tools = (this.pi.getAllTools?.() ?? []).filter((tool) => this.pi.getActiveTools?.().includes(tool.name));
|
|
762
|
+
const estimate = estimateRequest(ctx.getSystemPrompt?.() ?? "", tools, messages, ctx.model);
|
|
763
|
+
if (!estimate.warnings.length) return;
|
|
764
|
+
return {
|
|
765
|
+
role: "custom",
|
|
766
|
+
customType: "freeflow-routing-budget",
|
|
767
|
+
display: false,
|
|
768
|
+
content: estimate.warnings.map((warning) => warning.detail).join("\n"),
|
|
769
|
+
timestamp: 0,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
ordinaryContext(input) {
|
|
773
|
+
try {
|
|
774
|
+
const sources = this.sources(),
|
|
775
|
+
associated = sources.associate(input);
|
|
776
|
+
const mapped = new Map(associated.filter((item) => item.source).map((item) => [item.message, item.source]));
|
|
777
|
+
return annotateSources(
|
|
778
|
+
input,
|
|
779
|
+
mapped,
|
|
780
|
+
new Set([...mapped.values()].map((source) => source.ref)),
|
|
781
|
+
sources,
|
|
782
|
+
this.token,
|
|
783
|
+
);
|
|
784
|
+
} catch {
|
|
785
|
+
return input;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
async context(ctx, incoming) {
|
|
789
|
+
this.ctx = ctx;
|
|
790
|
+
const input = incoming.filter(
|
|
791
|
+
(m) =>
|
|
792
|
+
!(
|
|
793
|
+
m?.details?.routingInstance === this.token &&
|
|
794
|
+
[ROUTING_MESSAGE, "freeflow-routing-v2-refs"].includes(m.customType)
|
|
795
|
+
),
|
|
796
|
+
);
|
|
797
|
+
this.messages = input;
|
|
798
|
+
if (!this.supported() || !this.store) return this.ordinaryContext(input);
|
|
799
|
+
try {
|
|
800
|
+
const state = this.stateData();
|
|
801
|
+
if (state.control !== "automatic") return this.ordinaryContext(input);
|
|
802
|
+
check(!this.error && !this.store.blocked && state.profile, "routing_blocked", this.error ?? this.store.blocked);
|
|
803
|
+
check(samePair(this.observed(), this.profilePair(state.profile)), "prepared_pair_mismatch");
|
|
804
|
+
const sources = this.sources(state),
|
|
805
|
+
user = this.lastDeliveredUser(sources, input);
|
|
806
|
+
if (state.assessment && user && user !== state.assessment.basisUserEntryId)
|
|
807
|
+
this.append(
|
|
808
|
+
{
|
|
809
|
+
type: "assessment-suspended",
|
|
810
|
+
handoffId: state.assessment.handoffId,
|
|
811
|
+
reason: "user-attention",
|
|
812
|
+
basisUserEntryId: user,
|
|
813
|
+
problems: [],
|
|
814
|
+
},
|
|
815
|
+
JSON.stringify(["attention", state.assessment.handoffId, user]),
|
|
816
|
+
);
|
|
817
|
+
// New input reaching an already prepared worker request cannot be rerouted by changing only the live model.
|
|
818
|
+
const outstanding = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
819
|
+
const worker = outstanding ? this.assignedWorker(state, outstanding.id) : undefined;
|
|
820
|
+
const interrupted =
|
|
821
|
+
isWorkerProfile(state.profile) &&
|
|
822
|
+
(state.profile !== worker || user !== (outstanding ? this.workerBasis(state, outstanding.id) : undefined));
|
|
823
|
+
if (!this.turn || this.turn.bound) {
|
|
824
|
+
this.receipts.clear();
|
|
825
|
+
const execution = {
|
|
826
|
+
id: randomUUID(),
|
|
827
|
+
profile: state.profile,
|
|
828
|
+
assignmentId: state.assignmentId,
|
|
829
|
+
recoveryId: state.recoveryId,
|
|
830
|
+
basisUserEntryId: user,
|
|
831
|
+
pair: this.observed(),
|
|
832
|
+
resultEntryIds: [],
|
|
833
|
+
};
|
|
834
|
+
const before = new Set(ctx.sessionManager.getBranch().map((e) => e.id));
|
|
835
|
+
this.turn = {
|
|
836
|
+
id: execution.id,
|
|
837
|
+
profile: execution.profile,
|
|
838
|
+
assignmentId: execution.assignmentId,
|
|
839
|
+
recoveryId: execution.recoveryId,
|
|
840
|
+
basisUserEntryId: user,
|
|
841
|
+
before,
|
|
842
|
+
pair: execution.pair,
|
|
843
|
+
};
|
|
844
|
+
this.openTurn();
|
|
845
|
+
}
|
|
846
|
+
let prepared = this.prepared(state.profile, input);
|
|
847
|
+
if (!prepared.ready && state.profile === "coordinator" && this.stateData().assessment) {
|
|
848
|
+
const assessment = this.stateData().assessment;
|
|
849
|
+
this.append({
|
|
850
|
+
type: "assessment-suspended",
|
|
851
|
+
handoffId: assessment.handoffId,
|
|
852
|
+
reason: "delivery-gap",
|
|
853
|
+
basisUserEntryId: user,
|
|
854
|
+
problems: prepared.problems,
|
|
855
|
+
});
|
|
856
|
+
this.projectionError = prepared.problems.map((p) => p.code).join(", ");
|
|
857
|
+
prepared = this.prepared("coordinator", input);
|
|
858
|
+
}
|
|
859
|
+
check(prepared.ready, "context_unavailable", prepared.problems.map((p) => p.detail).join("; "));
|
|
860
|
+
const known = this.stateData().exposure;
|
|
861
|
+
const added = prepared.fullSources.filter((s) => known.get(s.ref) !== s.hash);
|
|
862
|
+
if (isWorkerProfile(this.turn.profile))
|
|
863
|
+
for (let i = 0; i < added.length; i += 128)
|
|
864
|
+
this.append({
|
|
865
|
+
type: "sources-exposed",
|
|
866
|
+
executionId: this.turn.id,
|
|
867
|
+
view: this.turn.profile,
|
|
868
|
+
sources: added.slice(i, i + 128).map((s) => ({ ref: s.ref, bodyHash: s.hash })),
|
|
869
|
+
});
|
|
870
|
+
if (interrupted)
|
|
871
|
+
prepared.messages.push({
|
|
872
|
+
role: "custom",
|
|
873
|
+
customType: ROUTING_MESSAGE,
|
|
874
|
+
display: false,
|
|
875
|
+
timestamp: 0,
|
|
876
|
+
details: { routingInstance: this.token },
|
|
877
|
+
content:
|
|
878
|
+
"New delivered user input requires Coordinator attention. Do not run ordinary task tools; return the current partial result.",
|
|
879
|
+
});
|
|
880
|
+
return prepared.messages;
|
|
881
|
+
} catch (error) {
|
|
882
|
+
this.mark(error);
|
|
883
|
+
ctx.abort?.();
|
|
884
|
+
// Do not send an accidental full worker history on a projection failure.
|
|
885
|
+
return [
|
|
886
|
+
{
|
|
887
|
+
role: "custom",
|
|
888
|
+
customType: ROUTING_MESSAGE,
|
|
889
|
+
content: `Automatic request blocked: ${this.error}`,
|
|
890
|
+
display: false,
|
|
891
|
+
timestamp: 0,
|
|
892
|
+
details: { routingInstance: this.token, routingRequestBlocked: true },
|
|
893
|
+
},
|
|
894
|
+
];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
messageEnd(message) {
|
|
898
|
+
if (message?.role === "assistant" && this.turn && !this.turn.bound) this.turn.message = structuredClone(message);
|
|
899
|
+
}
|
|
900
|
+
contextOperation(name, input) {
|
|
901
|
+
return name === "freeflow_context" && ["archive", "restore", "search", "retrieve"].includes(input?.operation);
|
|
902
|
+
}
|
|
903
|
+
handoffOperation(name, input) {
|
|
904
|
+
if (name === "freeflow_delegate") return true;
|
|
905
|
+
if (name === "freeflow_return") return ["submit", "supplement", "retry"].includes(input?.operation);
|
|
906
|
+
return name === "freeflow_unit" && input?.operation === "recover";
|
|
907
|
+
}
|
|
908
|
+
stableRecoveryPath(path) {
|
|
909
|
+
return (
|
|
910
|
+
!path.startsWith("@") &&
|
|
911
|
+
path !== "~" &&
|
|
912
|
+
!path.startsWith("~/") &&
|
|
913
|
+
!path.startsWith("file://") &&
|
|
914
|
+
!/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(path) &&
|
|
915
|
+
!(process.platform === "win32" && /^\/(?:mnt\/|cygdrive\/)?[a-z](?:\/|$)/i.test(path))
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
canonicalRecoveryPath(path) {
|
|
919
|
+
check(this.stableRecoveryPath(path), "recovery_path_unsupported", `Recovery path spelling is unsupported: ${path}`);
|
|
920
|
+
const requested = resolvePath(this.ctx.cwd, path);
|
|
921
|
+
check(existsSync(requested), "recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
|
|
922
|
+
try {
|
|
923
|
+
return realpathSync(requested);
|
|
924
|
+
} catch {
|
|
925
|
+
throw new RoutingError("recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
packagedInstruction(path) {
|
|
929
|
+
return this.packageRoots.some((root) => {
|
|
930
|
+
const inside = relative(root, path);
|
|
931
|
+
if (!inside || inside.startsWith("..") || resolvePath(root, inside) !== path) return false;
|
|
932
|
+
return (
|
|
933
|
+
/^(?:skills|capabilities)\/[^/]+\/SKILL\.md$/.test(inside) ||
|
|
934
|
+
/^(?:skills|capabilities)\/[^/]+\/references\/[^/]+\.md$/.test(inside)
|
|
935
|
+
);
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
recoveryReadAllowed(state, name, input) {
|
|
939
|
+
if (name !== "read" || typeof input?.path !== "string" || !state.recoveryId) return false;
|
|
940
|
+
const recovery = state.recoveries.get(state.recoveryId);
|
|
941
|
+
if (!recovery || recovery.state !== "reading" || !this.stableRecoveryPath(input.path)) return false;
|
|
942
|
+
try {
|
|
943
|
+
const path = this.canonicalRecoveryPath(input.path);
|
|
944
|
+
return recovery.paths.includes(path) || this.packagedInstruction(path);
|
|
945
|
+
} catch {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
workerBasis(state, assignmentId) {
|
|
950
|
+
const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
|
|
951
|
+
if (recovery?.state === "reading") return state.handoffs.get(recovery.requestHandoffId)?.basisUserEntryId ?? null;
|
|
952
|
+
return this.taskBasis(state, assignmentId);
|
|
953
|
+
}
|
|
954
|
+
batch(callId, name) {
|
|
955
|
+
check(this.turn?.message, "batch_unavailable");
|
|
956
|
+
const matches = this.ctx.sessionManager
|
|
957
|
+
.getBranch()
|
|
958
|
+
.filter(
|
|
959
|
+
(e) =>
|
|
960
|
+
!this.turn.before.has(e.id) &&
|
|
961
|
+
e.type === "message" &&
|
|
962
|
+
e.message?.role === "assistant" &&
|
|
963
|
+
e.message.content?.some((b) => b.type === "toolCall" && b.id === callId),
|
|
964
|
+
);
|
|
965
|
+
check(matches.length === 1, "batch_source_changed");
|
|
966
|
+
const fingerprint = bodyHash(matches[0].message);
|
|
967
|
+
check(!this.turn.sourceFingerprint || this.turn.sourceFingerprint === fingerprint, "batch_source_changed");
|
|
968
|
+
// Later message_end handlers may legitimately replace the message. The first
|
|
969
|
+
// preflight freezes the actually persisted batch before any of its tools act.
|
|
970
|
+
this.turn.sourceFingerprint = fingerprint;
|
|
971
|
+
this.turn.message = structuredClone(matches[0].message);
|
|
972
|
+
const calls = (this.turn.message.content ?? []).filter((b) => b.type === "toolCall");
|
|
973
|
+
const handoffs = calls.filter((b) => this.handoffOperation(b.name, b.arguments));
|
|
974
|
+
check(
|
|
975
|
+
!handoffs.length ||
|
|
976
|
+
(handoffs.length === 1 &&
|
|
977
|
+
this.handoffOperation(calls.at(-1)?.name, calls.at(-1)?.arguments) &&
|
|
978
|
+
calls.every(
|
|
979
|
+
(b) =>
|
|
980
|
+
this.handoffOperation(b.name, b.arguments) ||
|
|
981
|
+
b.name === "freeflow_project" ||
|
|
982
|
+
this.contextOperation(b.name, b.arguments),
|
|
983
|
+
)),
|
|
984
|
+
"invalid_handoff_batch",
|
|
985
|
+
"A handoff must be last and cannot accompany ordinary task tools.",
|
|
986
|
+
);
|
|
987
|
+
check(
|
|
988
|
+
calls.some((b) => b.id === callId && b.name === name),
|
|
989
|
+
"call_not_in_batch",
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
preflight(event, ctx) {
|
|
993
|
+
this.ctx = ctx;
|
|
994
|
+
const name = event.toolName,
|
|
995
|
+
isRouting = ROUTING_TOOLS.includes(name);
|
|
996
|
+
if (!this.supported()) return isRouting ? { block: true, reason: "Routing is unavailable." } : undefined;
|
|
997
|
+
try {
|
|
998
|
+
const state = this.stateData();
|
|
999
|
+
if (state.control === "manual")
|
|
1000
|
+
return isRouting && name !== "freeflow_unit" ? { block: true, reason: "Manual control is active." } : undefined;
|
|
1001
|
+
if (state.control !== "automatic")
|
|
1002
|
+
return isRouting ? { block: true, reason: "Automatic routing is inactive." } : undefined;
|
|
1003
|
+
this.batch(event.toolCallId, name);
|
|
1004
|
+
check(!this.error && !this.store?.blocked, "routing_blocked");
|
|
1005
|
+
this.openTurn();
|
|
1006
|
+
if (isWorkerProfile(this.turn?.profile) && !isRouting && !this.contextOperation(name, event.input)) {
|
|
1007
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1008
|
+
const worker = a ? this.assignedWorker(state, a.id) : undefined;
|
|
1009
|
+
const ordinary =
|
|
1010
|
+
a?.state === "outstanding" &&
|
|
1011
|
+
this.turn.basisUserEntryId === this.taskBasis(state, a.id) &&
|
|
1012
|
+
state.profile === worker &&
|
|
1013
|
+
this.turn.profile === worker;
|
|
1014
|
+
const recovery =
|
|
1015
|
+
a?.state === "returned" &&
|
|
1016
|
+
this.turn.basisUserEntryId === this.workerBasis(state, a.id) &&
|
|
1017
|
+
state.profile === worker &&
|
|
1018
|
+
this.turn.profile === worker &&
|
|
1019
|
+
this.recoveryReadAllowed(state, name, event.input);
|
|
1020
|
+
check(ordinary || recovery, "worker_task_phase_ended");
|
|
1021
|
+
}
|
|
1022
|
+
return undefined;
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
return { block: true, reason: error instanceof Error ? error.message : String(error) };
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
callOperation(callId, name) {
|
|
1028
|
+
check(this.turn && this.store, "execution_missing");
|
|
1029
|
+
this.batch(callId, name);
|
|
1030
|
+
return JSON.stringify([this.store.reader.getSessionId(), this.turn.id, callId, name]);
|
|
1031
|
+
}
|
|
1032
|
+
result(value) {
|
|
1033
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }], details: value };
|
|
1034
|
+
}
|
|
1035
|
+
async invoke(name, callId, input, signal, ctx) {
|
|
1036
|
+
return this.enqueue(async () => {
|
|
1037
|
+
this.ctx = ctx;
|
|
1038
|
+
try {
|
|
1039
|
+
check(!signal?.aborted, "cancelled");
|
|
1040
|
+
if (name === "freeflow_unit" && input.operation === "inspect") {
|
|
1041
|
+
check(this.supported(), "routing_unavailable");
|
|
1042
|
+
return this.result(this.inspectUnit(input));
|
|
1043
|
+
}
|
|
1044
|
+
const state = this.stateData();
|
|
1045
|
+
const expected =
|
|
1046
|
+
name === "freeflow_delegate" || name === "freeflow_unit" ? "coordinator" : this.assignedWorker(state);
|
|
1047
|
+
this.assertAvailable(expected);
|
|
1048
|
+
const op = this.callOperation(callId, name);
|
|
1049
|
+
const cached = this.receipts.get(op);
|
|
1050
|
+
if (cached) {
|
|
1051
|
+
check(cached.input === canonical(input), "operation_conflict");
|
|
1052
|
+
return this.result(structuredClone(cached.value));
|
|
1053
|
+
}
|
|
1054
|
+
const value =
|
|
1055
|
+
name === "freeflow_delegate"
|
|
1056
|
+
? this.delegate(input, callId, op)
|
|
1057
|
+
: name === "freeflow_return"
|
|
1058
|
+
? this.returnReport(input, callId, op)
|
|
1059
|
+
: name === "freeflow_project"
|
|
1060
|
+
? this.project(input, op)
|
|
1061
|
+
: name === "freeflow_unit"
|
|
1062
|
+
? this.unit(input, callId, op)
|
|
1063
|
+
: undefined;
|
|
1064
|
+
if (value !== undefined) {
|
|
1065
|
+
this.receipts.set(op, { input: canonical(input), value: structuredClone(value) });
|
|
1066
|
+
return this.result(value);
|
|
1067
|
+
}
|
|
1068
|
+
throw new RoutingError("unknown_tool", name);
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
return this.result({
|
|
1071
|
+
status: "blocked",
|
|
1072
|
+
code: error instanceof RoutingError ? error.code : "operation_failed",
|
|
1073
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1074
|
+
problems: error instanceof RoutingError ? error.problems : [],
|
|
1075
|
+
observed: this.observed(),
|
|
1076
|
+
expectedProfile:
|
|
1077
|
+
name === "freeflow_unit" && input.operation === "inspect"
|
|
1078
|
+
? undefined
|
|
1079
|
+
: name === "freeflow_delegate" || name === "freeflow_unit"
|
|
1080
|
+
? "coordinator"
|
|
1081
|
+
: (() => {
|
|
1082
|
+
try {
|
|
1083
|
+
return this.assignedWorker(this.stateData());
|
|
1084
|
+
} catch {
|
|
1085
|
+
return undefined;
|
|
1086
|
+
}
|
|
1087
|
+
})(),
|
|
1088
|
+
recoveryAction:
|
|
1089
|
+
error instanceof RoutingError &&
|
|
1090
|
+
[
|
|
1091
|
+
"cursor_expired",
|
|
1092
|
+
"invalid_cursor",
|
|
1093
|
+
"work_ref_required",
|
|
1094
|
+
"invalid_work_ref",
|
|
1095
|
+
"work_unavailable",
|
|
1096
|
+
"invalid_inspection",
|
|
1097
|
+
].includes(error.code)
|
|
1098
|
+
? "Inspect again without a cursor and use a returned work ref. Lookup recovery does not require a new assignment."
|
|
1099
|
+
: "Preserve saved work. Use freeflow_unit inspect; reconcile current input/control before retrying the named operation.",
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
duplicate(op) {
|
|
1105
|
+
return [...this.stateData().events.values()].find(
|
|
1106
|
+
(e) =>
|
|
1107
|
+
e.operationId === op &&
|
|
1108
|
+
[
|
|
1109
|
+
"delegate-accepted",
|
|
1110
|
+
"return-accepted",
|
|
1111
|
+
"recovery-request-accepted",
|
|
1112
|
+
"recovery-supplement-accepted",
|
|
1113
|
+
"recovery-cancelled",
|
|
1114
|
+
"handoff-retry-requested",
|
|
1115
|
+
].includes(e.data.type),
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
delegate(input, callId, op) {
|
|
1119
|
+
const prior = this.duplicate(op);
|
|
1120
|
+
if (prior) {
|
|
1121
|
+
check(
|
|
1122
|
+
prior.data.type === "delegate-accepted" &&
|
|
1123
|
+
prior.data.assignment.contract === input.contract &&
|
|
1124
|
+
(input.worker === undefined || prior.data.handoff.to === input.worker),
|
|
1125
|
+
"operation_conflict",
|
|
1126
|
+
);
|
|
1127
|
+
return { status: "accepted", handoff: prior.data.handoff.id, unchanged: true };
|
|
1128
|
+
}
|
|
1129
|
+
const state = this.stateData();
|
|
1130
|
+
const enabled = this.enabledWorkers();
|
|
1131
|
+
let worker;
|
|
1132
|
+
if (input.worker === undefined) {
|
|
1133
|
+
check(enabled.length === 1, "worker_required", "Delegation mode 'both' requires choosing helper or executor.");
|
|
1134
|
+
worker = enabled[0];
|
|
1135
|
+
} else {
|
|
1136
|
+
check(isWorkerProfile(input.worker), "invalid_worker");
|
|
1137
|
+
worker = input.worker;
|
|
1138
|
+
}
|
|
1139
|
+
check(enabled.includes(worker), "worker_disabled", `${worker} is not enabled by delegation mode.`);
|
|
1140
|
+
const unit = state.unitId
|
|
1141
|
+
? state.units.get(state.unitId)
|
|
1142
|
+
: { id: randomUUID(), objective: input.contract, state: "open", assignmentIds: [] };
|
|
1143
|
+
const old = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1144
|
+
if (input.operation === "replace") check(old?.state === "outstanding" && !this.applying, "no_quiescent_assignment");
|
|
1145
|
+
else
|
|
1146
|
+
check(
|
|
1147
|
+
input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId && !state.recoveryId,
|
|
1148
|
+
"assignment_outstanding",
|
|
1149
|
+
);
|
|
1150
|
+
const assignmentId = randomUUID(),
|
|
1151
|
+
id = randomUUID();
|
|
1152
|
+
const assignment = {
|
|
1153
|
+
id: assignmentId,
|
|
1154
|
+
unitId: unit.id,
|
|
1155
|
+
contract: input.contract,
|
|
1156
|
+
state: "outstanding",
|
|
1157
|
+
delegateHandoffId: id,
|
|
1158
|
+
basisUserEntryId: this.turn.basisUserEntryId,
|
|
1159
|
+
};
|
|
1160
|
+
const h = {
|
|
1161
|
+
id,
|
|
1162
|
+
kind: "delegate",
|
|
1163
|
+
assignmentId,
|
|
1164
|
+
text: input.contract,
|
|
1165
|
+
from: "coordinator",
|
|
1166
|
+
to: worker,
|
|
1167
|
+
executionId: this.turn.id,
|
|
1168
|
+
toolCallId: callId,
|
|
1169
|
+
basisUserEntryId: this.turn.basisUserEntryId,
|
|
1170
|
+
state: "pending",
|
|
1171
|
+
reportRevision: 0,
|
|
1172
|
+
limitations: [],
|
|
1173
|
+
};
|
|
1174
|
+
this.append(
|
|
1175
|
+
{
|
|
1176
|
+
type: "delegate-accepted",
|
|
1177
|
+
unit: { ...unit, assignmentIds: [...unit.assignmentIds, assignmentId] },
|
|
1178
|
+
assignment,
|
|
1179
|
+
handoff: h,
|
|
1180
|
+
...(input.operation === "replace"
|
|
1181
|
+
? {
|
|
1182
|
+
replacement: {
|
|
1183
|
+
assignmentId: old.id,
|
|
1184
|
+
reason: input.reason,
|
|
1185
|
+
...(state.pendingId ? { supersededHandoffId: state.pendingId } : {}),
|
|
1186
|
+
},
|
|
1187
|
+
}
|
|
1188
|
+
: {}),
|
|
1189
|
+
},
|
|
1190
|
+
op,
|
|
1191
|
+
);
|
|
1192
|
+
return {
|
|
1193
|
+
status: "accepted",
|
|
1194
|
+
unit: unit.id,
|
|
1195
|
+
assignment: assignmentId,
|
|
1196
|
+
worker,
|
|
1197
|
+
handoff: id,
|
|
1198
|
+
transition: "pending",
|
|
1199
|
+
contract: input.contract,
|
|
1200
|
+
reason: input.reason,
|
|
1201
|
+
createdUnit: !state.unitId,
|
|
1202
|
+
...(old && input.operation === "replace"
|
|
1203
|
+
? {
|
|
1204
|
+
replacedAssignment: old.id,
|
|
1205
|
+
continuity:
|
|
1206
|
+
"Prior effects and evidence remain on native history. Reconcile uncertain effects before dependent work.",
|
|
1207
|
+
}
|
|
1208
|
+
: {}),
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
returnReport(input, callId, op) {
|
|
1212
|
+
const state = this.stateData(),
|
|
1213
|
+
a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1214
|
+
check(a, "assignment_missing");
|
|
1215
|
+
const worker = this.assignedWorker(state, a.id);
|
|
1216
|
+
const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
|
|
1217
|
+
const original = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1218
|
+
const supplement = recovery?.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
|
|
1219
|
+
const retryingSupplement = input.operation === "retry" && recovery?.state === "returning";
|
|
1220
|
+
const saved = retryingSupplement || input.operation === "supplement" ? supplement : original;
|
|
1221
|
+
const selection = state.selections.get(a.id) ?? emptySelection();
|
|
1222
|
+
const duplicate = this.duplicate(op);
|
|
1223
|
+
if (duplicate) {
|
|
1224
|
+
if (input.operation === "retry") check(duplicate.data.type === "handoff-retry-requested", "operation_conflict");
|
|
1225
|
+
else {
|
|
1226
|
+
const expected = input.operation === "supplement" ? "recovery-supplement-accepted" : "return-accepted";
|
|
1227
|
+
check(
|
|
1228
|
+
duplicate.data.type === expected &&
|
|
1229
|
+
duplicate.data.handoff.text === input.report &&
|
|
1230
|
+
duplicate.data.handoff.outcome === input.outcome &&
|
|
1231
|
+
canonical(duplicate.data.handoff.limitations) === canonical(input.limitations ?? []),
|
|
1232
|
+
"operation_conflict",
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
const handoff = duplicate.data.handoffId ?? duplicate.data.handoff.id;
|
|
1236
|
+
return { ...this.returnReadiness(handoff), unchanged: true };
|
|
1237
|
+
}
|
|
1238
|
+
if (input.operation === "retry") {
|
|
1239
|
+
check(saved && ["blocked", "pending"].includes(saved.state), "saved_return_missing");
|
|
1240
|
+
this.append(
|
|
1241
|
+
{
|
|
1242
|
+
type: "handoff-retry-requested",
|
|
1243
|
+
handoffId: saved.id,
|
|
1244
|
+
attemptId: randomUUID(),
|
|
1245
|
+
executionId: this.turn.id,
|
|
1246
|
+
toolCallId: callId,
|
|
1247
|
+
reportRevision: saved.reportRevision,
|
|
1248
|
+
selectionRevision: selection.revision,
|
|
1249
|
+
},
|
|
1250
|
+
op,
|
|
1251
|
+
);
|
|
1252
|
+
return {
|
|
1253
|
+
...this.returnReadiness(saved.id),
|
|
1254
|
+
reportUnchanged: true,
|
|
1255
|
+
supplementUnchanged: saved.kind === "recovery-return",
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
if (input.operation === "supplement") {
|
|
1259
|
+
check(
|
|
1260
|
+
recovery &&
|
|
1261
|
+
["reading", "returning"].includes(recovery.state) &&
|
|
1262
|
+
(!supplement || ["pending", "blocked"].includes(supplement.state)),
|
|
1263
|
+
"recovery_not_returnable",
|
|
1264
|
+
);
|
|
1265
|
+
const h = {
|
|
1266
|
+
id: supplement?.id ?? randomUUID(),
|
|
1267
|
+
kind: "recovery-return",
|
|
1268
|
+
assignmentId: a.id,
|
|
1269
|
+
text: input.report,
|
|
1270
|
+
from: worker,
|
|
1271
|
+
to: "coordinator",
|
|
1272
|
+
executionId: this.turn.id,
|
|
1273
|
+
toolCallId: callId,
|
|
1274
|
+
basisUserEntryId: this.turn.basisUserEntryId,
|
|
1275
|
+
state: "pending",
|
|
1276
|
+
reportRevision: (supplement?.reportRevision ?? 0) + 1,
|
|
1277
|
+
outcome: input.outcome,
|
|
1278
|
+
limitations: input.limitations ?? [],
|
|
1279
|
+
};
|
|
1280
|
+
this.append({ type: "recovery-supplement-accepted", recoveryId: recovery.id, handoff: h }, op);
|
|
1281
|
+
return this.returnReadiness(h.id);
|
|
1282
|
+
}
|
|
1283
|
+
check(input.operation === "submit", "invalid_return_operation");
|
|
1284
|
+
check(
|
|
1285
|
+
a.state === "outstanding" ||
|
|
1286
|
+
(a.state === "returned" && original && ["pending", "blocked"].includes(original.state)),
|
|
1287
|
+
"assignment_task_ended",
|
|
1288
|
+
);
|
|
1289
|
+
const h = {
|
|
1290
|
+
id: original?.id ?? randomUUID(),
|
|
1291
|
+
kind: "return",
|
|
1292
|
+
assignmentId: a.id,
|
|
1293
|
+
text: input.report,
|
|
1294
|
+
from: worker,
|
|
1295
|
+
to: "coordinator",
|
|
1296
|
+
executionId: this.turn.id,
|
|
1297
|
+
toolCallId: callId,
|
|
1298
|
+
basisUserEntryId: this.turn.basisUserEntryId,
|
|
1299
|
+
state: "pending",
|
|
1300
|
+
reportRevision: (original?.reportRevision ?? 0) + 1,
|
|
1301
|
+
outcome: input.outcome,
|
|
1302
|
+
limitations: input.limitations ?? [],
|
|
1303
|
+
};
|
|
1304
|
+
this.append({ type: "return-accepted", handoff: h }, op);
|
|
1305
|
+
return this.returnReadiness(h.id);
|
|
1306
|
+
}
|
|
1307
|
+
returnReadiness(id) {
|
|
1308
|
+
const state = this.stateData();
|
|
1309
|
+
const h = state.handoffs.get(id);
|
|
1310
|
+
const prepared = this.prepared("coordinator", this.messages, id);
|
|
1311
|
+
const isSupplement = h.kind === "recovery-return";
|
|
1312
|
+
const recovery = isSupplement
|
|
1313
|
+
? [...state.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
|
|
1314
|
+
: undefined;
|
|
1315
|
+
return {
|
|
1316
|
+
status: "accepted",
|
|
1317
|
+
reportSaved: !isSupplement,
|
|
1318
|
+
supplementSaved: isSupplement,
|
|
1319
|
+
handoff: id,
|
|
1320
|
+
...(recovery ? { recovery: recovery.id } : {}),
|
|
1321
|
+
...(isSupplement ? { supplementRevision: h.reportRevision } : { reportRevision: h.reportRevision }),
|
|
1322
|
+
assignmentRef: `assignment:${h.assignmentId}`,
|
|
1323
|
+
...(isSupplement
|
|
1324
|
+
? { supplementRef: `supplement:${recovery?.id ?? "unknown"}:${h.reportRevision}` }
|
|
1325
|
+
: { reportRef: `report:${h.id}:${h.reportRevision}` }),
|
|
1326
|
+
producer: h.from,
|
|
1327
|
+
outcome: h.outcome,
|
|
1328
|
+
limitations: h.limitations,
|
|
1329
|
+
ready: prepared.ready,
|
|
1330
|
+
transition: prepared.ready ? "pending" : "blocked",
|
|
1331
|
+
problems: prepared.problems,
|
|
1332
|
+
warnings: prepared.warnings,
|
|
1333
|
+
stage: "completed-input preparation",
|
|
1334
|
+
finalExchangePending: true,
|
|
1335
|
+
provisional:
|
|
1336
|
+
"The finalized handoff exchange, target configuration and budget are revalidated at turn_end; this is not delivery evidence.",
|
|
1337
|
+
...(isSupplement ? { supplement: h.text } : { report: h.text }),
|
|
1338
|
+
evidence: this.evidenceFacts(state),
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
project(input, op) {
|
|
1342
|
+
check(this.projectionEnabled, "projection_disabled");
|
|
1343
|
+
const state = this.stateData(),
|
|
1344
|
+
a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1345
|
+
check(a, "assignment_missing");
|
|
1346
|
+
const sources = this.sources(state);
|
|
1347
|
+
sources.associate(this.messages);
|
|
1348
|
+
const selection = state.selections.get(a.id) ?? emptySelection();
|
|
1349
|
+
let next = selection;
|
|
1350
|
+
if (input.operation !== "inspect") {
|
|
1351
|
+
check(["add", "remove"].includes(input.operation), "invalid_projection_operation");
|
|
1352
|
+
next = changeSelection(selection, input, sources, state);
|
|
1353
|
+
if (next !== selection) this.append({ type: "selection-changed", assignmentId: a.id, selection: next }, op);
|
|
1354
|
+
}
|
|
1355
|
+
const prepared = this.prepared("coordinator", this.messages, a.returnHandoffId);
|
|
1356
|
+
const scope = input.scope ?? "assignment";
|
|
1357
|
+
let page;
|
|
1358
|
+
if (input.operation === "inspect") {
|
|
1359
|
+
const model = this.model("coordinator");
|
|
1360
|
+
const checksFor = (s) => {
|
|
1361
|
+
const eligibility = sources.selectionProblem(s.ref, state);
|
|
1362
|
+
const limitations = [
|
|
1363
|
+
...(eligibility ? [eligibility] : []),
|
|
1364
|
+
...representationProblems(s, model),
|
|
1365
|
+
...sources.exchange(s).problems,
|
|
1366
|
+
];
|
|
1367
|
+
return { eligible: !eligibility, targetReady: !limitations.length, limitations };
|
|
1368
|
+
};
|
|
1369
|
+
const rows = () =>
|
|
1370
|
+
[...sources.byRef.values()]
|
|
1371
|
+
.filter(
|
|
1372
|
+
(s) =>
|
|
1373
|
+
isWorkerProfile(s.producer) &&
|
|
1374
|
+
(isTaskEvidence(s) || (scope === "selected" && next.selected.includes(s.ref))) &&
|
|
1375
|
+
(scope === "selected"
|
|
1376
|
+
? next.selected.includes(s.ref)
|
|
1377
|
+
: scope === "assignment"
|
|
1378
|
+
? s.assignmentId === a.id
|
|
1379
|
+
: scope === "active"
|
|
1380
|
+
? s.active
|
|
1381
|
+
: true) &&
|
|
1382
|
+
(s.original ||
|
|
1383
|
+
s.message.role === "toolResult" ||
|
|
1384
|
+
s.message.content?.some((b) => b.type === "text" && b.text?.trim())),
|
|
1385
|
+
)
|
|
1386
|
+
.map((s) => {
|
|
1387
|
+
const locator = sources.locator(s.ref);
|
|
1388
|
+
return {
|
|
1389
|
+
ref: s.ref,
|
|
1390
|
+
kind: s.original ? "assistant-text" : s.message.role,
|
|
1391
|
+
producer: s.producer,
|
|
1392
|
+
assignment: s.assignmentId,
|
|
1393
|
+
toolName: s.message.toolName,
|
|
1394
|
+
...(locator ? { locator } : {}),
|
|
1395
|
+
active: s.active,
|
|
1396
|
+
selected: next.selected.includes(s.ref),
|
|
1397
|
+
retainedSelection: next.selected.includes(s.ref) && !isTaskEvidence(s),
|
|
1398
|
+
...checksFor(s),
|
|
1399
|
+
};
|
|
1400
|
+
})
|
|
1401
|
+
// Discovery offers usable candidates. Saved selections must remain
|
|
1402
|
+
// inspectable even when a later source or target check fails.
|
|
1403
|
+
.filter((row) => scope === "selected" || row.targetReady);
|
|
1404
|
+
page = this.page(
|
|
1405
|
+
`evidence:${scope}`,
|
|
1406
|
+
rows,
|
|
1407
|
+
input.cursor,
|
|
1408
|
+
30,
|
|
1409
|
+
scope === "assignment" || scope === "selected" ? a.id : undefined,
|
|
1410
|
+
scope === "active",
|
|
1411
|
+
(rows) => ({
|
|
1412
|
+
scopeCounts: {
|
|
1413
|
+
candidates: rows.length,
|
|
1414
|
+
eligible: rows.filter((r) => r.eligible).length,
|
|
1415
|
+
targetReady: rows.filter((r) => r.targetReady).length,
|
|
1416
|
+
},
|
|
1417
|
+
otherAssignments: [...sources.byRef.values()].filter(
|
|
1418
|
+
(s) =>
|
|
1419
|
+
isWorkerProfile(s.producer) && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
|
|
1420
|
+
).length,
|
|
1421
|
+
}),
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
status: input.operation === "inspect" || next === selection ? "unchanged" : "saved",
|
|
1426
|
+
...(page
|
|
1427
|
+
? {
|
|
1428
|
+
scope,
|
|
1429
|
+
count: page.count,
|
|
1430
|
+
returned: page.items.length,
|
|
1431
|
+
candidates: page.items,
|
|
1432
|
+
nextCursor: page.nextCursor,
|
|
1433
|
+
...page.summary,
|
|
1434
|
+
pageCounts: {
|
|
1435
|
+
candidates: page.items.length,
|
|
1436
|
+
eligible: page.items.filter((r) => r.eligible).length,
|
|
1437
|
+
targetReady: page.items.filter((r) => r.targetReady).length,
|
|
1438
|
+
},
|
|
1439
|
+
countsBasis:
|
|
1440
|
+
"Inspection snapshot; page counts describe returned candidates, scope counts describe all candidates in this scope.",
|
|
1441
|
+
historyHint: page.summary.otherAssignments
|
|
1442
|
+
? "Previously exposed task evidence exists in other assignments; inspect scope history when needed."
|
|
1443
|
+
: undefined,
|
|
1444
|
+
}
|
|
1445
|
+
: {}),
|
|
1446
|
+
revision: next.revision,
|
|
1447
|
+
selected: next.selected,
|
|
1448
|
+
selectedCount: next.selected.length,
|
|
1449
|
+
unresolved: next.unresolved,
|
|
1450
|
+
ready: !next.unresolved.length && prepared.ready,
|
|
1451
|
+
problems: prepared.problems,
|
|
1452
|
+
warnings: prepared.warnings,
|
|
1453
|
+
evidence: this.evidenceFacts(this.stateData()),
|
|
1454
|
+
items: (input.refs ? [...new Set(input.refs)] : []).map((ref) => ({
|
|
1455
|
+
ref,
|
|
1456
|
+
status: next.unresolved.some((p) => p.ref === ref)
|
|
1457
|
+
? "rejected"
|
|
1458
|
+
: input.operation === "remove"
|
|
1459
|
+
? selection.selected.includes(ref)
|
|
1460
|
+
? "removed"
|
|
1461
|
+
: selection.unresolved.some((p) => p.ref === ref)
|
|
1462
|
+
? "withdrawn"
|
|
1463
|
+
: "unchanged"
|
|
1464
|
+
: selection.selected.includes(ref)
|
|
1465
|
+
? "already_selected"
|
|
1466
|
+
: "added",
|
|
1467
|
+
})),
|
|
1468
|
+
estimate: { tokens: prepared.estimatedTokens, method: prepared.estimateMethod },
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
unit(input, callId, op) {
|
|
1472
|
+
const state = this.stateData();
|
|
1473
|
+
const prior = [...state.events.values()].find((e) => e.operationId === op);
|
|
1474
|
+
if (prior) {
|
|
1475
|
+
if (input.operation === "recover") {
|
|
1476
|
+
check(
|
|
1477
|
+
prior.data.type === "recovery-request-accepted" &&
|
|
1478
|
+
prior.data.recovery.request === input.request &&
|
|
1479
|
+
canonical(prior.data.recovery.requestedPaths) === canonical([...new Set(input.paths ?? [])]),
|
|
1480
|
+
"operation_conflict",
|
|
1481
|
+
);
|
|
1482
|
+
const { recovery, handoff } = prior.data;
|
|
1483
|
+
return {
|
|
1484
|
+
status: "accepted",
|
|
1485
|
+
unchanged: true,
|
|
1486
|
+
recovery: recovery.id,
|
|
1487
|
+
handoff: handoff.id,
|
|
1488
|
+
assignmentRef: `assignment:${recovery.assignmentId}`,
|
|
1489
|
+
reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
|
|
1490
|
+
request: recovery.request,
|
|
1491
|
+
paths: recovery.paths,
|
|
1492
|
+
transition: handoff.state,
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
if (input.operation === "cancel-recovery") {
|
|
1496
|
+
check(prior.data.type === "recovery-cancelled" && prior.data.reason === input.reason, "operation_conflict");
|
|
1497
|
+
return {
|
|
1498
|
+
status: "cancelled",
|
|
1499
|
+
unchanged: true,
|
|
1500
|
+
recovery: prior.data.recoveryId,
|
|
1501
|
+
reason: prior.data.reason,
|
|
1502
|
+
assessment: state.assessment?.handoffId,
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
check(
|
|
1506
|
+
(input.operation === "assess" && prior.data.type === "assessment-resumed") ||
|
|
1507
|
+
(input.operation === "close" &&
|
|
1508
|
+
prior.data.type === "unit-closed" &&
|
|
1509
|
+
prior.data.outcome === input.outcome &&
|
|
1510
|
+
prior.data.assessment === input.assessment),
|
|
1511
|
+
"operation_conflict",
|
|
1512
|
+
);
|
|
1513
|
+
return { status: "unchanged", operation: input.operation };
|
|
1514
|
+
}
|
|
1515
|
+
if (input.operation === "recover") {
|
|
1516
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1517
|
+
const base = a?.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1518
|
+
check(
|
|
1519
|
+
state.unitId &&
|
|
1520
|
+
a?.state === "returned" &&
|
|
1521
|
+
base?.kind === "return" &&
|
|
1522
|
+
base.state === "configured" &&
|
|
1523
|
+
state.assessment?.handoffId === base.id &&
|
|
1524
|
+
!state.pendingId &&
|
|
1525
|
+
!state.recoveryId &&
|
|
1526
|
+
!this.applying,
|
|
1527
|
+
"recovery_not_available",
|
|
1528
|
+
);
|
|
1529
|
+
const worker = this.assignedWorker(state, a.id);
|
|
1530
|
+
const id = randomUUID(),
|
|
1531
|
+
handoffId = randomUUID();
|
|
1532
|
+
const paths = [...new Set((input.paths ?? []).map((path) => this.canonicalRecoveryPath(path)))];
|
|
1533
|
+
const recovery = {
|
|
1534
|
+
id,
|
|
1535
|
+
assignmentId: a.id,
|
|
1536
|
+
assessmentHandoffId: base.id,
|
|
1537
|
+
baseReportRevision: base.reportRevision,
|
|
1538
|
+
request: input.request,
|
|
1539
|
+
requestedPaths: [...new Set(input.paths ?? [])],
|
|
1540
|
+
paths,
|
|
1541
|
+
state: "requested",
|
|
1542
|
+
requestHandoffId: handoffId,
|
|
1543
|
+
supplementRevision: 0,
|
|
1544
|
+
};
|
|
1545
|
+
const handoff = {
|
|
1546
|
+
id: handoffId,
|
|
1547
|
+
kind: "recovery-request",
|
|
1548
|
+
assignmentId: a.id,
|
|
1549
|
+
text: input.request,
|
|
1550
|
+
from: "coordinator",
|
|
1551
|
+
to: worker,
|
|
1552
|
+
executionId: this.turn.id,
|
|
1553
|
+
toolCallId: callId,
|
|
1554
|
+
basisUserEntryId: this.turn.basisUserEntryId,
|
|
1555
|
+
state: "pending",
|
|
1556
|
+
reportRevision: 0,
|
|
1557
|
+
limitations: [],
|
|
1558
|
+
};
|
|
1559
|
+
this.append({ type: "recovery-request-accepted", recovery, handoff }, op);
|
|
1560
|
+
return {
|
|
1561
|
+
status: "accepted",
|
|
1562
|
+
recovery: id,
|
|
1563
|
+
handoff: handoffId,
|
|
1564
|
+
assignmentRef: `assignment:${a.id}`,
|
|
1565
|
+
reportRef: `report:${base.id}:${base.reportRevision}`,
|
|
1566
|
+
request: input.request,
|
|
1567
|
+
paths,
|
|
1568
|
+
transition: "pending",
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
if (input.operation === "cancel-recovery") {
|
|
1572
|
+
check(state.recoveryId && !this.applying, "recovery_not_cancellable");
|
|
1573
|
+
check(
|
|
1574
|
+
![...state.executions.values()].some(
|
|
1575
|
+
(execution) =>
|
|
1576
|
+
execution.recoveryId === state.recoveryId &&
|
|
1577
|
+
isWorkerProfile(execution.profile) &&
|
|
1578
|
+
!execution.assistantEntryId &&
|
|
1579
|
+
!execution.interrupted,
|
|
1580
|
+
),
|
|
1581
|
+
"recovery_not_cancellable",
|
|
1582
|
+
);
|
|
1583
|
+
this.append({ type: "recovery-cancelled", recoveryId: state.recoveryId, reason: input.reason }, op);
|
|
1584
|
+
return {
|
|
1585
|
+
status: "cancelled",
|
|
1586
|
+
recovery: state.recoveryId,
|
|
1587
|
+
reason: input.reason,
|
|
1588
|
+
assessment: state.assessment?.handoffId,
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
if (input.operation === "assess") {
|
|
1592
|
+
check(state.assessment, "assessment_missing");
|
|
1593
|
+
check(!state.recoveryId, "recovery_outstanding");
|
|
1594
|
+
if (state.assessment.view === "active") return { status: "unchanged", ready: true };
|
|
1595
|
+
const prepared = this.prepared("coordinator", this.messages, state.assessment.handoffId, true);
|
|
1596
|
+
if (!prepared.ready) return { status: "suspended", ready: false, problems: prepared.problems };
|
|
1597
|
+
if (this.projectionEnabled) check(prepared.reservation, "reservation_missing");
|
|
1598
|
+
this.append(
|
|
1599
|
+
{
|
|
1600
|
+
type: "assessment-resumed",
|
|
1601
|
+
handoffId: state.assessment.handoffId,
|
|
1602
|
+
basisUserEntryId: this.turn.basisUserEntryId,
|
|
1603
|
+
...(prepared.reservation ? { reservation: prepared.reservation } : {}),
|
|
1604
|
+
},
|
|
1605
|
+
op,
|
|
1606
|
+
);
|
|
1607
|
+
this.projectionError = undefined;
|
|
1608
|
+
return {
|
|
1609
|
+
status: "resumed",
|
|
1610
|
+
ready: true,
|
|
1611
|
+
stage: "prepared assessment; next request revalidated",
|
|
1612
|
+
evidence: this.evidenceFacts(this.stateData()),
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
check(input.operation === "close" && state.unitId, "unit_missing");
|
|
1616
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1617
|
+
check(!this.applying, "not_quiescent");
|
|
1618
|
+
if (state.recoveryId) {
|
|
1619
|
+
check(input.outcome !== "accepted", "unfinished_work");
|
|
1620
|
+
check(
|
|
1621
|
+
![...state.executions.values()].some(
|
|
1622
|
+
(execution) =>
|
|
1623
|
+
execution.recoveryId === state.recoveryId &&
|
|
1624
|
+
isWorkerProfile(execution.profile) &&
|
|
1625
|
+
!execution.assistantEntryId &&
|
|
1626
|
+
!execution.interrupted,
|
|
1627
|
+
),
|
|
1628
|
+
"not_quiescent",
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
this.append(
|
|
1632
|
+
{
|
|
1633
|
+
type: "unit-closed",
|
|
1634
|
+
unitId: state.unitId,
|
|
1635
|
+
outcome: input.outcome,
|
|
1636
|
+
assessment: input.assessment,
|
|
1637
|
+
...(a?.state === "outstanding" && input.outcome !== "accepted" ? { supersededAssignmentId: a.id } : {}),
|
|
1638
|
+
},
|
|
1639
|
+
op,
|
|
1640
|
+
);
|
|
1641
|
+
return { status: "closed", unit: state.unitId, outcome: input.outcome, assessment: input.assessment };
|
|
1642
|
+
}
|
|
1643
|
+
page(scope, rows, cursor, size = 30, assignment, active = false, summarize) {
|
|
1644
|
+
const branch = this.ctx.sessionManager.getBranch();
|
|
1645
|
+
const compaction = branch.filter((e) => e.type === "compaction").at(-1)?.id;
|
|
1646
|
+
let key,
|
|
1647
|
+
offset = 0;
|
|
1648
|
+
if (cursor) {
|
|
1649
|
+
const match = /^([a-f0-9-]+):([0-9]+)$/.exec(cursor);
|
|
1650
|
+
check(match, "invalid_cursor", "Use the returned cursor unchanged.");
|
|
1651
|
+
key = match[1];
|
|
1652
|
+
offset = Number(match[2]);
|
|
1653
|
+
const page = this.pages.get(key);
|
|
1654
|
+
check(
|
|
1655
|
+
page &&
|
|
1656
|
+
page.scope === scope &&
|
|
1657
|
+
page.assignment === assignment &&
|
|
1658
|
+
(!page.basis || branch.some((e) => e.id === page.basis)) &&
|
|
1659
|
+
(!active || page.compaction === compaction),
|
|
1660
|
+
"cursor_expired",
|
|
1661
|
+
"History or scope changed. Inspect again without a cursor.",
|
|
1662
|
+
);
|
|
1663
|
+
check(
|
|
1664
|
+
Number.isSafeInteger(offset) && offset >= 0 && offset < page.rows.length && offset % page.size === 0,
|
|
1665
|
+
"invalid_cursor",
|
|
1666
|
+
);
|
|
1667
|
+
} else {
|
|
1668
|
+
key = randomUUID();
|
|
1669
|
+
if (this.pages.size >= 8) this.pages.delete(this.pages.keys().next().value);
|
|
1670
|
+
const items = typeof rows === "function" ? rows() : rows;
|
|
1671
|
+
this.pages.set(key, {
|
|
1672
|
+
scope,
|
|
1673
|
+
basis: branch.at(-1)?.id ?? null,
|
|
1674
|
+
assignment,
|
|
1675
|
+
compaction,
|
|
1676
|
+
rows: items,
|
|
1677
|
+
size,
|
|
1678
|
+
summary: summarize?.(items),
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
const page = this.pages.get(key);
|
|
1682
|
+
return {
|
|
1683
|
+
scope,
|
|
1684
|
+
count: page.rows.length,
|
|
1685
|
+
offset,
|
|
1686
|
+
summary: page.summary,
|
|
1687
|
+
items: page.rows.slice(offset, offset + page.size),
|
|
1688
|
+
nextCursor: offset + page.size < page.rows.length ? `${key}:${offset + page.size}` : undefined,
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
inspectUnit(input) {
|
|
1692
|
+
const state = this.stateData(),
|
|
1693
|
+
view = input.view ?? "current";
|
|
1694
|
+
check(view === "detail" || !input.ref, "invalid_inspection", "A work ref belongs to view detail.");
|
|
1695
|
+
check(
|
|
1696
|
+
view === "history" || (!input.cursor && !input.limit),
|
|
1697
|
+
"invalid_inspection",
|
|
1698
|
+
"Pagination belongs to view history.",
|
|
1699
|
+
);
|
|
1700
|
+
if (view === "current")
|
|
1701
|
+
return {
|
|
1702
|
+
...this.status(),
|
|
1703
|
+
status: "inspected",
|
|
1704
|
+
view,
|
|
1705
|
+
currentRef: state.assignmentId ? `assignment:${state.assignmentId}` : undefined,
|
|
1706
|
+
};
|
|
1707
|
+
if (view === "history") {
|
|
1708
|
+
const numbers = new Map([...state.units.keys()].map((id, i) => [id, i + 1]));
|
|
1709
|
+
const rows = () =>
|
|
1710
|
+
[...state.assignments.values()].reverse().map((a) => {
|
|
1711
|
+
const u = state.units.get(a.unitId),
|
|
1712
|
+
h = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1713
|
+
const delegate = state.handoffs.get(a.delegateHandoffId);
|
|
1714
|
+
return {
|
|
1715
|
+
ref: `assignment:${a.id}`,
|
|
1716
|
+
unitRef: `unit:${a.unitId}`,
|
|
1717
|
+
unitNumber: numbers.get(a.unitId),
|
|
1718
|
+
assignmentNumber: (u?.assignmentIds.indexOf(a.id) ?? 0) + 1,
|
|
1719
|
+
state: a.state,
|
|
1720
|
+
unitState: u?.state,
|
|
1721
|
+
disposition: u?.disposition,
|
|
1722
|
+
summary: a.contract.slice(0, 160),
|
|
1723
|
+
reportAvailable: !!h,
|
|
1724
|
+
outcome: h?.outcome,
|
|
1725
|
+
from: delegate?.from,
|
|
1726
|
+
to: delegate?.to,
|
|
1727
|
+
};
|
|
1728
|
+
});
|
|
1729
|
+
const page = this.page("work-history", rows, input.cursor, input.limit ?? 20);
|
|
1730
|
+
return {
|
|
1731
|
+
status: "inspected",
|
|
1732
|
+
view,
|
|
1733
|
+
history: page.items,
|
|
1734
|
+
count: page.count,
|
|
1735
|
+
returned: page.items.length,
|
|
1736
|
+
nextCursor: page.nextCursor,
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
check(
|
|
1740
|
+
view === "detail" && typeof input.ref === "string",
|
|
1741
|
+
"work_ref_required",
|
|
1742
|
+
"Use a ref returned by history inspection.",
|
|
1743
|
+
);
|
|
1744
|
+
check(
|
|
1745
|
+
/^(?:(?:unit|assignment|recovery):[^:\s]+|(?:report|supplement):[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
|
|
1746
|
+
"invalid_work_ref",
|
|
1747
|
+
"Use assignment, report, recovery, or supplement refs returned by a receipt or inspection, unchanged. A bare handoff ID is not a work ref.",
|
|
1748
|
+
);
|
|
1749
|
+
if (input.ref.startsWith("unit:")) {
|
|
1750
|
+
const u = state.units.get(input.ref.slice(5));
|
|
1751
|
+
check(u, "work_unavailable", "Unit is not on current ancestry.");
|
|
1752
|
+
return {
|
|
1753
|
+
status: "inspected",
|
|
1754
|
+
view,
|
|
1755
|
+
ref: input.ref,
|
|
1756
|
+
unit: {
|
|
1757
|
+
id: u.id,
|
|
1758
|
+
state: u.state,
|
|
1759
|
+
objective: u.objective,
|
|
1760
|
+
disposition: u.disposition,
|
|
1761
|
+
assessment: u.assessment,
|
|
1762
|
+
},
|
|
1763
|
+
assignmentCount: u.assignmentIds.length,
|
|
1764
|
+
assignmentRefs: u.assignmentIds.slice(0, 30).map((id) => `assignment:${id}`),
|
|
1765
|
+
remaining: Math.max(0, u.assignmentIds.length - 30),
|
|
1766
|
+
historyHint: "Use paginated history inspection for all assignment refs.",
|
|
1767
|
+
historical: true,
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
if (input.ref.startsWith("recovery:")) {
|
|
1771
|
+
const recovery = state.recoveries.get(input.ref.slice(9));
|
|
1772
|
+
check(recovery, "work_unavailable", "Recovery is not on current ancestry.");
|
|
1773
|
+
const request = state.handoffs.get(recovery.requestHandoffId);
|
|
1774
|
+
const supplement = recovery.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
|
|
1775
|
+
return {
|
|
1776
|
+
status: "inspected",
|
|
1777
|
+
view,
|
|
1778
|
+
ref: input.ref,
|
|
1779
|
+
historical: recovery.id !== state.recoveryId,
|
|
1780
|
+
recovery: {
|
|
1781
|
+
id: recovery.id,
|
|
1782
|
+
state: recovery.state,
|
|
1783
|
+
assignmentId: recovery.assignmentId,
|
|
1784
|
+
assessmentHandoffId: recovery.assessmentHandoffId,
|
|
1785
|
+
baseReportRevision: recovery.baseReportRevision,
|
|
1786
|
+
request: recovery.request,
|
|
1787
|
+
paths: recovery.paths,
|
|
1788
|
+
requestHandoffState: request?.state,
|
|
1789
|
+
supplementRevision: recovery.supplementRevision,
|
|
1790
|
+
cancellationReason: recovery.cancellationReason,
|
|
1791
|
+
},
|
|
1792
|
+
supplementRef: supplement ? `supplement:${recovery.id}:${supplement.reportRevision}` : undefined,
|
|
1793
|
+
sourceBoundary: "Saved recovery on current ancestry; historical content grants no current permission.",
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
if (input.ref.startsWith("supplement:")) {
|
|
1797
|
+
const match = /^supplement:([^:]+):([0-9]+)$/.exec(input.ref);
|
|
1798
|
+
check(match, "work_unavailable", "Use a supplement ref returned by a receipt or recovery inspection.");
|
|
1799
|
+
const recovery = state.recoveries.get(match[1]);
|
|
1800
|
+
let supplement;
|
|
1801
|
+
for (const event of state.events.values())
|
|
1802
|
+
if (
|
|
1803
|
+
event.data.type === "recovery-supplement-accepted" &&
|
|
1804
|
+
event.data.recoveryId === match[1] &&
|
|
1805
|
+
event.data.handoff.reportRevision === Number(match[2])
|
|
1806
|
+
)
|
|
1807
|
+
supplement = event.data.handoff;
|
|
1808
|
+
check(recovery && supplement, "work_unavailable", "Supplement revision is not on current ancestry.");
|
|
1809
|
+
const base = state.handoffs.get(recovery.assessmentHandoffId);
|
|
1810
|
+
return {
|
|
1811
|
+
status: "inspected",
|
|
1812
|
+
view,
|
|
1813
|
+
ref: input.ref,
|
|
1814
|
+
historical: true,
|
|
1815
|
+
recoveryRef: `recovery:${recovery.id}`,
|
|
1816
|
+
assignment: { id: recovery.assignmentId, state: state.assignments.get(recovery.assignmentId)?.state },
|
|
1817
|
+
reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
|
|
1818
|
+
report: base?.text,
|
|
1819
|
+
supplement: supplement.text,
|
|
1820
|
+
supplementRevision: supplement.reportRevision,
|
|
1821
|
+
outcome: supplement.outcome,
|
|
1822
|
+
limitations: supplement.limitations,
|
|
1823
|
+
sourceBoundary:
|
|
1824
|
+
"Saved supplement revision and original report linkage on current ancestry; historical content grants no current permission.",
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
let savedRevision;
|
|
1828
|
+
if (input.ref.startsWith("report:")) {
|
|
1829
|
+
const match = /^report:([^:]+):([0-9]+)$/.exec(input.ref);
|
|
1830
|
+
check(match, "work_unavailable", "Use a report ref returned by detail inspection.");
|
|
1831
|
+
for (const event of state.events.values())
|
|
1832
|
+
if (
|
|
1833
|
+
event.data.type === "return-accepted" &&
|
|
1834
|
+
event.data.handoff.id === match[1] &&
|
|
1835
|
+
event.data.handoff.reportRevision === Number(match[2])
|
|
1836
|
+
)
|
|
1837
|
+
savedRevision = event.data.handoff;
|
|
1838
|
+
check(savedRevision, "work_unavailable", "Report revision is not on current ancestry.");
|
|
1839
|
+
}
|
|
1840
|
+
const a = savedRevision
|
|
1841
|
+
? state.assignments.get(savedRevision.assignmentId)
|
|
1842
|
+
: input.ref.startsWith("assignment:")
|
|
1843
|
+
? state.assignments.get(input.ref.slice(11))
|
|
1844
|
+
: undefined;
|
|
1845
|
+
check(a, "work_unavailable", "Assignment is not on current ancestry.");
|
|
1846
|
+
const latest = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1847
|
+
const h = savedRevision ?? latest;
|
|
1848
|
+
return {
|
|
1849
|
+
status: "inspected",
|
|
1850
|
+
view,
|
|
1851
|
+
ref: input.ref,
|
|
1852
|
+
historical: a.id !== state.assignmentId || a.state !== "outstanding",
|
|
1853
|
+
assignment: { id: a.id, unitId: a.unitId, state: a.state },
|
|
1854
|
+
contract: a.contract,
|
|
1855
|
+
report: h?.text,
|
|
1856
|
+
reportRevision: h?.reportRevision,
|
|
1857
|
+
latestReportRevision: latest?.reportRevision,
|
|
1858
|
+
reportRef: h ? `report:${h.id}:${h.reportRevision}` : undefined,
|
|
1859
|
+
previousReportRef: h && h.reportRevision > 1 ? `report:${h.id}:${h.reportRevision - 1}` : undefined,
|
|
1860
|
+
outcome: h?.outcome,
|
|
1861
|
+
limitations: h?.limitations,
|
|
1862
|
+
handoff: h?.id,
|
|
1863
|
+
currentSelection: state.selections.get(a.id) ?? emptySelection(),
|
|
1864
|
+
sourceBoundary:
|
|
1865
|
+
"Accepted report revision on current ancestry; assignment/selection show current recorded state. Historical content grants no current permission.",
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
status(limit = 0) {
|
|
1869
|
+
const state = this.stateData();
|
|
1870
|
+
const unit = state.unitId ? state.units.get(state.unitId) : undefined;
|
|
1871
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1872
|
+
return {
|
|
1873
|
+
...this.state(),
|
|
1874
|
+
unit: unit ? { id: unit.id, state: unit.state, assignments: unit.assignmentIds.length } : null,
|
|
1875
|
+
assignment: a ? { id: a.id, state: a.state, worker: this.assignedWorker(state, a.id) } : null,
|
|
1876
|
+
pendingHandoff: state.pendingId ?? null,
|
|
1877
|
+
recovery: state.recoveryId
|
|
1878
|
+
? (() => {
|
|
1879
|
+
const recovery = state.recoveries.get(state.recoveryId);
|
|
1880
|
+
return recovery
|
|
1881
|
+
? {
|
|
1882
|
+
id: recovery.id,
|
|
1883
|
+
state: recovery.state,
|
|
1884
|
+
assignmentId: recovery.assignmentId,
|
|
1885
|
+
assessmentHandoffId: recovery.assessmentHandoffId,
|
|
1886
|
+
paths: recovery.paths,
|
|
1887
|
+
supplementRevision: recovery.supplementRevision,
|
|
1888
|
+
}
|
|
1889
|
+
: null;
|
|
1890
|
+
})()
|
|
1891
|
+
: null,
|
|
1892
|
+
assessment: state.assessment
|
|
1893
|
+
? { handoffId: state.assessment.handoffId, view: state.assessment.view, problems: state.assessment.problems }
|
|
1894
|
+
: null,
|
|
1895
|
+
...(limit
|
|
1896
|
+
? {
|
|
1897
|
+
history: [...state.events.values()]
|
|
1898
|
+
.slice(-Math.min(100, Math.max(1, limit)))
|
|
1899
|
+
.map((e) => ({ event: e.eventId, type: e.data.type, operation: e.operationId })),
|
|
1900
|
+
}
|
|
1901
|
+
: {}),
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
async turnEnd(event, ctx) {
|
|
1905
|
+
this.ctx = ctx;
|
|
1906
|
+
if (!this.turn || this.turn.bound || !this.supported() || !this.store) return;
|
|
1907
|
+
if (!this.turn.opened) {
|
|
1908
|
+
this.turn = undefined;
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
const token = this.token,
|
|
1912
|
+
turn = this.turn;
|
|
1913
|
+
const subject = this.subject();
|
|
1914
|
+
try {
|
|
1915
|
+
const message = event.message ?? turn.message;
|
|
1916
|
+
const candidates = ctx.sessionManager
|
|
1917
|
+
.getBranch()
|
|
1918
|
+
.filter((e) => !turn.before.has(e.id) && e.type === "message" && canonical(e.message) === canonical(message));
|
|
1919
|
+
check(candidates.length === 1, "execution_binding_ambiguous");
|
|
1920
|
+
const assistant = candidates[0];
|
|
1921
|
+
const callIds = new Set((message?.content ?? []).filter((b) => b.type === "toolCall").map((b) => b.id));
|
|
1922
|
+
const results = ctx.sessionManager
|
|
1923
|
+
.getBranch()
|
|
1924
|
+
.filter((e) => !turn.before.has(e.id) && e.message?.role === "toolResult" && callIds.has(e.message.toolCallId));
|
|
1925
|
+
const outcome =
|
|
1926
|
+
message?.stopReason === "error" ? "failed" : message?.stopReason === "aborted" ? "aborted" : "completed";
|
|
1927
|
+
this.append(
|
|
1928
|
+
{
|
|
1929
|
+
type: "execution-bound",
|
|
1930
|
+
executionId: turn.id,
|
|
1931
|
+
assistantEntryId: assistant.id,
|
|
1932
|
+
resultEntryIds: results.map((e) => e.id),
|
|
1933
|
+
outcome,
|
|
1934
|
+
},
|
|
1935
|
+
JSON.stringify(["binding", turn.id]),
|
|
1936
|
+
);
|
|
1937
|
+
turn.bound = assistant.id;
|
|
1938
|
+
const state = this.stateData(),
|
|
1939
|
+
pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
|
|
1940
|
+
if (!pending || pending.state === "superseded") return;
|
|
1941
|
+
const attempt = state.attempts.get(pending.id);
|
|
1942
|
+
if (pending.executionId !== turn.id && attempt?.executionId !== turn.id) return;
|
|
1943
|
+
if (ctx.signal?.aborted || outcome === "aborted" || state.control !== "automatic") return;
|
|
1944
|
+
const inputs = [...this.messages, message, ...results.map((e) => e.message)].filter(Boolean);
|
|
1945
|
+
const latest = this.stateData();
|
|
1946
|
+
const retry = latest.attempts.get(pending.id);
|
|
1947
|
+
if (retry?.executionId === turn.id)
|
|
1948
|
+
check(
|
|
1949
|
+
retry.reportRevision === pending.reportRevision &&
|
|
1950
|
+
retry.selectionRevision === (latest.selections.get(pending.assignmentId)?.revision ?? 0),
|
|
1951
|
+
"stale_retry_revision",
|
|
1952
|
+
);
|
|
1953
|
+
await this.finishHandoff(pending, inputs, token);
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
if (this.current(subject)) {
|
|
1956
|
+
this.mark(error);
|
|
1957
|
+
ctx.abort?.();
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
async finishHandoff(h, inputs, token) {
|
|
1962
|
+
const subject = this.subject();
|
|
1963
|
+
const state = this.stateData();
|
|
1964
|
+
check(state.control === "automatic" && !this.store?.blocked && token === this.token, "transition_ineligible");
|
|
1965
|
+
const execution = state.executions.get(h.executionId);
|
|
1966
|
+
check(
|
|
1967
|
+
execution?.assistantEntryId,
|
|
1968
|
+
"source_turn_incomplete",
|
|
1969
|
+
"Saved communication belongs to an incomplete historical turn. Reconcile its effects and replace or cancel the outstanding work; no tool result is fabricated.",
|
|
1970
|
+
);
|
|
1971
|
+
const source = new Sources(this.ctx.sessionManager.getBranch(), state);
|
|
1972
|
+
const carrier = source.byRef.get(`ctx:${execution.assistantEntryId}`);
|
|
1973
|
+
check(
|
|
1974
|
+
carrier && !source.exchange(carrier).problems.length,
|
|
1975
|
+
"source_exchange_incomplete",
|
|
1976
|
+
"The saved handoff has an incomplete native exchange; reconcile or explicitly dispose of it.",
|
|
1977
|
+
);
|
|
1978
|
+
let attentionProblems = [];
|
|
1979
|
+
if (["return", "recovery-return"].includes(h.kind) && this.projectionEnabled) {
|
|
1980
|
+
const prepared = this.prepared("coordinator", inputs, h.id);
|
|
1981
|
+
if (!prepared.ready) {
|
|
1982
|
+
const attentionFallback = h.kind === "recovery-return" && h.outcome !== "completed";
|
|
1983
|
+
this.projectionError = prepared.problems.map((p) => p.code).join(", ");
|
|
1984
|
+
if (attentionFallback) attentionProblems = prepared.problems;
|
|
1985
|
+
else {
|
|
1986
|
+
this.append({
|
|
1987
|
+
type: "handoff-state",
|
|
1988
|
+
handoffId: h.id,
|
|
1989
|
+
state: "blocked",
|
|
1990
|
+
reason: prepared.problems
|
|
1991
|
+
.map((p) => p.detail)
|
|
1992
|
+
.join("; ")
|
|
1993
|
+
.slice(0, 4096),
|
|
1994
|
+
});
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
} else {
|
|
1998
|
+
check(prepared.reservation, "reservation_missing");
|
|
1999
|
+
this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
try {
|
|
2003
|
+
const current = this.stateData();
|
|
2004
|
+
check(current.pendingId === h.id && current.control === "automatic" && token === this.token, "stale_handoff");
|
|
2005
|
+
await this.applyPair(this.profilePair(h.to));
|
|
2006
|
+
this.guard(subject);
|
|
2007
|
+
this.append({ type: "handoff-state", handoffId: h.id, state: "configured", observedPair: this.observed() });
|
|
2008
|
+
const completed = this.stateData();
|
|
2009
|
+
const reservation = completed.reservations.get(h.id);
|
|
2010
|
+
const recovery =
|
|
2011
|
+
h.kind === "recovery-return"
|
|
2012
|
+
? [...completed.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
|
|
2013
|
+
: undefined;
|
|
2014
|
+
if (h.kind === "recovery-return" && recovery && completed.assessment?.view === "suspended") {
|
|
2015
|
+
if (attentionProblems.length) {
|
|
2016
|
+
this.append({
|
|
2017
|
+
type: "assessment-suspended",
|
|
2018
|
+
handoffId: recovery.assessmentHandoffId,
|
|
2019
|
+
reason: "delivery-gap",
|
|
2020
|
+
basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
|
|
2021
|
+
problems: attentionProblems,
|
|
2022
|
+
});
|
|
2023
|
+
} else if (completed.assessment.suspensionReason === "recovery") {
|
|
2024
|
+
this.append({
|
|
2025
|
+
type: "assessment-resumed",
|
|
2026
|
+
handoffId: recovery.assessmentHandoffId,
|
|
2027
|
+
basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
|
|
2028
|
+
...(reservation ? { reservation } : {}),
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
if (h.kind === "return" && completed.assessment?.view === "suspended" && reservation) {
|
|
2033
|
+
this.append({
|
|
2034
|
+
type: "assessment-resumed",
|
|
2035
|
+
handoffId: h.id,
|
|
2036
|
+
basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
|
|
2037
|
+
reservation,
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
if (!attentionProblems.length) this.projectionError = undefined;
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
if (this.current(subject) && !this.store?.blocked && !this.error && this.stateData().pendingId === h.id) {
|
|
2043
|
+
this.append({
|
|
2044
|
+
type: "handoff-state",
|
|
2045
|
+
handoffId: h.id,
|
|
2046
|
+
state: "blocked",
|
|
2047
|
+
reason: error instanceof Error ? error.message.slice(0, 4096) : "configuration_failed",
|
|
2048
|
+
});
|
|
2049
|
+
this.ctx.ui?.notify?.(
|
|
2050
|
+
`Couldn’t switch to ${h.to.charAt(0).toUpperCase()}${h.to.slice(1)} — ${["return", "recovery-return"].includes(h.kind) ? "report" : "assignment"} saved.`,
|
|
2051
|
+
"warning",
|
|
2052
|
+
);
|
|
2053
|
+
} else throw error;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
async resume(ctx) {
|
|
2057
|
+
this.ctx = ctx;
|
|
2058
|
+
check(ctx.isIdle?.(), "not_idle");
|
|
2059
|
+
check(this.store && this.capability?.effective, "routing_unavailable");
|
|
2060
|
+
this.revision++;
|
|
2061
|
+
const subject = this.subject();
|
|
2062
|
+
return this.enqueue(() => this.resumeCurrent(ctx, subject));
|
|
2063
|
+
}
|
|
2064
|
+
async resumeCurrent(ctx, subject) {
|
|
2065
|
+
this.guard(subject);
|
|
2066
|
+
check(this.store, "routing_unavailable");
|
|
2067
|
+
await this.store.reconcile();
|
|
2068
|
+
this.guard(subject);
|
|
2069
|
+
this.error = undefined;
|
|
2070
|
+
this.retireUnbound("Explicit resume retires an interrupted attempt without claiming its effects completed.");
|
|
2071
|
+
const state = this.stateData();
|
|
2072
|
+
check(state.control === "automatic", "manual_control");
|
|
2073
|
+
check(
|
|
2074
|
+
!ctx.hasPendingMessages?.(),
|
|
2075
|
+
"pending_input",
|
|
2076
|
+
"Queued input must be delivered and reconciled before resuming a worker.",
|
|
2077
|
+
);
|
|
2078
|
+
const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
|
|
2079
|
+
if (pending) {
|
|
2080
|
+
const source = ctx.sessionManager.buildSessionContext?.().messages;
|
|
2081
|
+
check(Array.isArray(source) || this.messages.length, "resume_context_unavailable");
|
|
2082
|
+
await this.finishHandoff(pending, source ?? this.messages, this.token);
|
|
2083
|
+
this.guard(subject);
|
|
2084
|
+
if (this.stateData().pendingId) {
|
|
2085
|
+
check(
|
|
2086
|
+
["return", "recovery-return"].includes(pending.kind) && this.projectionError,
|
|
2087
|
+
"handoff_still_blocked",
|
|
2088
|
+
"Saved transfer is still blocked; reconcile its configuration before retrying.",
|
|
2089
|
+
);
|
|
2090
|
+
const worker = this.assignedWorker(this.stateData(), pending.assignmentId);
|
|
2091
|
+
this.append({
|
|
2092
|
+
type: "control",
|
|
2093
|
+
control: "automatic",
|
|
2094
|
+
profile: worker,
|
|
2095
|
+
reason: "Explicit resume of saved-return evidence correction only; ordinary assignment work remains ended",
|
|
2096
|
+
});
|
|
2097
|
+
await this.applyPair(this.profilePair(worker));
|
|
2098
|
+
}
|
|
2099
|
+
} else {
|
|
2100
|
+
const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
2101
|
+
const input = ctx.sessionManager.buildSessionContext?.().messages ?? this.messages;
|
|
2102
|
+
const user = this.lastDeliveredUser(new Sources(ctx.sessionManager.getBranch(), state), input);
|
|
2103
|
+
const coordinatorSawInput = [...state.executions.values()].some(
|
|
2104
|
+
(x) =>
|
|
2105
|
+
x.profile === "coordinator" && x.basisUserEntryId === user && x.assistantEntryId && x.outcome === "completed",
|
|
2106
|
+
);
|
|
2107
|
+
const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
|
|
2108
|
+
const worker = assignment ? this.assignedWorker(state, assignment.id) : undefined;
|
|
2109
|
+
if (assignment?.state === "returned" && recovery?.state === "reading") {
|
|
2110
|
+
check(
|
|
2111
|
+
user === this.workerBasis(state, assignment.id) || coordinatorSawInput,
|
|
2112
|
+
"input_unreconciled",
|
|
2113
|
+
"New input needs Coordinator attention before evidence recovery can resume.",
|
|
2114
|
+
);
|
|
2115
|
+
this.append({
|
|
2116
|
+
type: "control",
|
|
2117
|
+
control: "automatic",
|
|
2118
|
+
profile: worker,
|
|
2119
|
+
reason: "Explicit user resume of unchanged evidence recovery; ordinary assignment work remains ended",
|
|
2120
|
+
});
|
|
2121
|
+
await this.applyPair(this.profilePair(worker));
|
|
2122
|
+
} else if (assignment?.state === "outstanding") {
|
|
2123
|
+
check(
|
|
2124
|
+
user === this.taskBasis(state, assignment.id) || coordinatorSawInput,
|
|
2125
|
+
"input_unreconciled",
|
|
2126
|
+
"New input needs Coordinator attention before the unchanged assignment can resume.",
|
|
2127
|
+
);
|
|
2128
|
+
this.append({ type: "assignment-resumed", assignmentId: assignment.id, basisUserEntryId: user });
|
|
2129
|
+
this.append({
|
|
2130
|
+
type: "control",
|
|
2131
|
+
control: "automatic",
|
|
2132
|
+
profile: worker,
|
|
2133
|
+
reason: "Explicit user resume of the unchanged assignment; current restrictions remain applicable",
|
|
2134
|
+
});
|
|
2135
|
+
await this.applyPair(this.profilePair(worker));
|
|
2136
|
+
} else await this.applyPair(this.profilePair("coordinator"));
|
|
2137
|
+
}
|
|
2138
|
+
this.guard(subject);
|
|
2139
|
+
this.pi.sendMessage(
|
|
2140
|
+
{
|
|
2141
|
+
customType: "freeflow-routing-v2-resume",
|
|
2142
|
+
content:
|
|
2143
|
+
"Explicit user resume of the saved routing responsibility. Recover the current contract and evidence before continuing.",
|
|
2144
|
+
display: true,
|
|
2145
|
+
},
|
|
2146
|
+
{ triggerTurn: true },
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
async command(args, ctx) {
|
|
2150
|
+
const words = args.trim().split(/\s+/);
|
|
2151
|
+
if (!["profile", "resume"].includes(words[0])) return false;
|
|
2152
|
+
try {
|
|
2153
|
+
if (words[0] === "profile" && words[1] === "history") {
|
|
2154
|
+
check(words.length === 2 || (words.length === 3 && words[2] === "diagnostics"), "invalid_history_command");
|
|
2155
|
+
ctx.ui.notify(
|
|
2156
|
+
JSON.stringify(
|
|
2157
|
+
words[2] === "diagnostics" ? this.status(30) : this.inspectUnit({ view: "history", limit: 30 }),
|
|
2158
|
+
),
|
|
2159
|
+
"info",
|
|
2160
|
+
);
|
|
2161
|
+
return true;
|
|
2162
|
+
}
|
|
2163
|
+
check(ctx.isIdle?.(), "not_idle");
|
|
2164
|
+
this.ctx = ctx;
|
|
2165
|
+
if (words[0] === "resume" && words.length === 1) {
|
|
2166
|
+
await this.resume(ctx);
|
|
2167
|
+
return true;
|
|
2168
|
+
}
|
|
2169
|
+
check(
|
|
2170
|
+
words.length === 2 && [...PROFILES, "auto"].includes(words[1]),
|
|
2171
|
+
"invalid_profile_command",
|
|
2172
|
+
"Use /freeflow profile coordinator|helper|executor|auto|history, or /freeflow resume.",
|
|
2173
|
+
);
|
|
2174
|
+
const result = words[1] === "auto" ? await this.setAutomaticControl() : await this.setManualProfile(words[1]);
|
|
2175
|
+
ctx.ui.notify(JSON.stringify(result), result.status === "blocked" ? "warning" : "info");
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
2178
|
+
}
|
|
2179
|
+
return true;
|
|
2180
|
+
}
|
|
2181
|
+
}
|