@hassangameryt/freeflow 0.7.2 → 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 +147 -127
- package/capabilities/cognitive-routing/SKILL.md +162 -93
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
- package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
- package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
- package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
- package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
- package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
- package/pi-extension/dist/context-virtualization/runtime.js +29 -7
- package/pi-extension/dist/freeflow-context/tool.js +7 -6
- package/pi-extension/dist/index.js +36 -15
- 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 +29 -10
- package/pi-extension/dist/session-sources/events.js +22 -0
- package/pi-extension/dist/session-sources/provenance.js +32 -0
- package/pi-extension/dist/session-sources/sources.js +51 -7
- package/pi-extension/dist/settings/settings-ui.js +247 -47
- package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
- package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
- package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
- package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
- package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
- package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
- package/pi-extension/src/context-virtualization/runtime.ts +30 -8
- package/pi-extension/src/freeflow-context/tool.ts +7 -6
- package/pi-extension/src/index.ts +36 -14
- 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 +32 -10
- package/pi-extension/src/session-sources/events.ts +22 -0
- package/pi-extension/src/session-sources/provenance.ts +39 -0
- package/pi-extension/src/session-sources/sources.ts +77 -6
- package/pi-extension/src/settings/settings-ui.ts +276 -46
- 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 +94 -1
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
- package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
- package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
- package/pi-extension/tests/fixtures/routing-native.js +4 -1
- package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
- package/pi-extension/tests/integration/pi-extension.test.js +508 -29
- package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
- package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
- 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 -4
- package/skills/design-for-depth/SKILL.md +1 -1
- package/skills/design-for-depth/references/implementation-decisions.md +3 -3
- package/skills/discuss/SKILL.md +2 -2
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { estimateRequest } from "./budget.js";
|
|
2
|
+
import { annotateSources } from "../session-sources/provenance.js";
|
|
3
|
+
import { ROUTING_SCHEMAS } from "./schemas.js";
|
|
2
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
6
|
+
import { relative, resolve as resolvePath } from "node:path";
|
|
3
7
|
import { EventStore } from "../session-sources/events.js";
|
|
4
8
|
import { Sources, bodyHash, isTaskEvidence } from "../session-sources/sources.js";
|
|
5
|
-
import { pairFromProfile } from "./config.js";
|
|
9
|
+
import { pairFromProfile, resolveCognitiveRoutingState } from "./config.js";
|
|
6
10
|
import { prepareView, changeSelection, representationProblems } from "./projection.js";
|
|
7
11
|
import { initialState } from "./state.js";
|
|
8
12
|
import {
|
|
@@ -11,13 +15,16 @@ import {
|
|
|
11
15
|
RoutingError,
|
|
12
16
|
canonical,
|
|
13
17
|
emptySelection,
|
|
18
|
+
isWorkerProfile,
|
|
14
19
|
requireCondition as check,
|
|
15
20
|
samePair,
|
|
21
|
+
workersForDelegation,
|
|
16
22
|
} from "./types.js";
|
|
17
23
|
export const ROUTING_TOOLS = ["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project"];
|
|
18
24
|
const HANDOFF_TOOLS = new Set(["freeflow_delegate", "freeflow_return"]);
|
|
19
25
|
export class RoutingRuntime {
|
|
20
26
|
pi;
|
|
27
|
+
packageRoots;
|
|
21
28
|
store;
|
|
22
29
|
ctx;
|
|
23
30
|
capability;
|
|
@@ -37,8 +44,9 @@ export class RoutingRuntime {
|
|
|
37
44
|
manualHold;
|
|
38
45
|
automaticControl = false;
|
|
39
46
|
sourceCache;
|
|
40
|
-
constructor(pi) {
|
|
47
|
+
constructor(pi, packageRoots = []) {
|
|
41
48
|
this.pi = pi;
|
|
49
|
+
this.packageRoots = packageRoots;
|
|
42
50
|
}
|
|
43
51
|
subject() {
|
|
44
52
|
const branch = this.store?.reader.getBranch() ?? [];
|
|
@@ -70,6 +78,29 @@ export class RoutingRuntime {
|
|
|
70
78
|
? state.resumeBasis.get(assignmentId)
|
|
71
79
|
: state.assignments.get(assignmentId)?.basisUserEntryId;
|
|
72
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
|
+
}
|
|
73
104
|
retireUnbound(reason) {
|
|
74
105
|
for (const execution of this.stateData().executions.values()) {
|
|
75
106
|
if (!execution.assistantEntryId && !execution.interrupted)
|
|
@@ -86,6 +117,7 @@ export class RoutingRuntime {
|
|
|
86
117
|
id: t.id,
|
|
87
118
|
profile: t.profile,
|
|
88
119
|
assignmentId: t.assignmentId,
|
|
120
|
+
recoveryId: t.recoveryId,
|
|
89
121
|
basisUserEntryId: t.basisUserEntryId,
|
|
90
122
|
pair: t.pair,
|
|
91
123
|
resultEntryIds: [],
|
|
@@ -101,7 +133,7 @@ export class RoutingRuntime {
|
|
|
101
133
|
async beforeRun(ctx) {
|
|
102
134
|
this.ctx = ctx;
|
|
103
135
|
if (!this.store || !this.supported() || this.store.blocked || this.error) return;
|
|
104
|
-
if (this.stateData().control === "automatic" && this.stateData().profile
|
|
136
|
+
if (this.stateData().control === "automatic" && isWorkerProfile(this.stateData().profile)) {
|
|
105
137
|
const result = await this.control("coordinator", false);
|
|
106
138
|
check(result.status !== "blocked", "reconciliation_required", result.reason);
|
|
107
139
|
}
|
|
@@ -147,6 +179,7 @@ export class RoutingRuntime {
|
|
|
147
179
|
return {
|
|
148
180
|
effective: this.supported() && state.control !== "inactive" && !blocked,
|
|
149
181
|
activeProfile: state.profile,
|
|
182
|
+
delegation: this.capability?.delegation ?? "executor",
|
|
150
183
|
controlMode: state.control === "manual" ? `manual-${state.profile}` : state.control,
|
|
151
184
|
runtimeStatus: blocked ? "blocked" : this.supported() && state.control !== "inactive" ? "active" : "inactive",
|
|
152
185
|
runtimeReason: blocked ?? (this.suppressed ? "startup_selection" : this.capability?.blockingReason.message),
|
|
@@ -189,11 +222,36 @@ export class RoutingRuntime {
|
|
|
189
222
|
if (profile) check(state.profile === profile && this.turn?.profile === profile, "wrong_profile");
|
|
190
223
|
if (state.profile) check(samePair(this.observed(), this.profilePair(state.profile)), "configuration_mismatch");
|
|
191
224
|
}
|
|
192
|
-
|
|
225
|
+
configuredProfilePair(profile) {
|
|
193
226
|
const configured = this.capability?.profiles[profile];
|
|
194
227
|
check(configured, "profile_missing");
|
|
195
228
|
return pairFromProfile(configured);
|
|
196
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
|
+
}
|
|
197
255
|
model(profile) {
|
|
198
256
|
const pair = this.profilePair(profile);
|
|
199
257
|
const model = this.ctx?.modelRegistry?.find(pair.provider, pair.modelId);
|
|
@@ -205,6 +263,8 @@ export class RoutingRuntime {
|
|
|
205
263
|
this.ctx = ctx;
|
|
206
264
|
this.capability = capability;
|
|
207
265
|
this.turn = undefined;
|
|
266
|
+
this.manualHold = undefined;
|
|
267
|
+
this.automaticControl = false;
|
|
208
268
|
this.messages = [];
|
|
209
269
|
this.sourceCache = undefined;
|
|
210
270
|
this.receipts.clear();
|
|
@@ -214,7 +274,7 @@ export class RoutingRuntime {
|
|
|
214
274
|
this.error = undefined;
|
|
215
275
|
this.projectionError = undefined;
|
|
216
276
|
this.operations = Promise.resolve();
|
|
217
|
-
this.targetSignature = canonical(capability.profiles);
|
|
277
|
+
this.targetSignature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
|
|
218
278
|
this.suppressed = startup && process.argv.some((a) => /^(--model|--thinking)(=|$)/.test(a));
|
|
219
279
|
this.store = new EventStore(this.pi, ctx.sessionManager);
|
|
220
280
|
const subject = this.subject();
|
|
@@ -225,6 +285,7 @@ export class RoutingRuntime {
|
|
|
225
285
|
const state = this.stateData();
|
|
226
286
|
this.manualHold = state.control === "manual" ? state.profile : undefined;
|
|
227
287
|
this.automaticControl = !state.events.size || state.control === "automatic";
|
|
288
|
+
await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
|
|
228
289
|
this.retireUnbound("Session rebind found an unfinished execution; no task effects replayed.");
|
|
229
290
|
if (!state.events.size) {
|
|
230
291
|
this.append({
|
|
@@ -265,7 +326,7 @@ export class RoutingRuntime {
|
|
|
265
326
|
async refresh(ctx, capability) {
|
|
266
327
|
this.ctx = ctx;
|
|
267
328
|
const was = this.capability?.effective === true;
|
|
268
|
-
const signature = canonical(capability.profiles);
|
|
329
|
+
const signature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
|
|
269
330
|
if (this.capability && (was !== capability.effective || signature !== this.targetSignature)) this.revision++;
|
|
270
331
|
this.capability = capability;
|
|
271
332
|
if (!capability.effective) {
|
|
@@ -297,7 +358,8 @@ export class RoutingRuntime {
|
|
|
297
358
|
} else if (this.targetSignature && signature !== this.targetSignature) {
|
|
298
359
|
this.error = "configuration_changed: reconcile profile configuration before automatic execution";
|
|
299
360
|
}
|
|
300
|
-
if (canonical(this.capability?.profiles) === signature)
|
|
361
|
+
if (canonical({ delegation: this.capability?.delegation, profiles: this.capability?.profiles }) === signature)
|
|
362
|
+
this.targetSignature = signature;
|
|
301
363
|
}
|
|
302
364
|
async ancestryChanged(ctx, navigation = true) {
|
|
303
365
|
this.revision++;
|
|
@@ -313,8 +375,9 @@ export class RoutingRuntime {
|
|
|
313
375
|
await this.store.reconcile();
|
|
314
376
|
this.guard(subject);
|
|
315
377
|
this.error = undefined;
|
|
316
|
-
this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
|
|
317
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.");
|
|
318
381
|
if (this.manualHold) {
|
|
319
382
|
this.append({
|
|
320
383
|
type: "control",
|
|
@@ -322,6 +385,7 @@ export class RoutingRuntime {
|
|
|
322
385
|
profile: this.manualHold,
|
|
323
386
|
reason: "Current explicit manual hold survives navigation",
|
|
324
387
|
});
|
|
388
|
+
await this.applyPair(this.profilePair(this.manualHold));
|
|
325
389
|
} else if (
|
|
326
390
|
(this.automaticControl || !state.events.size) &&
|
|
327
391
|
(navigation || !samePair(this.observed(), this.profilePair(state.profile ?? "coordinator")))
|
|
@@ -403,6 +467,12 @@ export class RoutingRuntime {
|
|
|
403
467
|
return this.enqueue(async () => {
|
|
404
468
|
try {
|
|
405
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
|
+
);
|
|
406
476
|
await this.store.reconcile();
|
|
407
477
|
this.guard(subject);
|
|
408
478
|
this.error = undefined;
|
|
@@ -440,7 +510,8 @@ export class RoutingRuntime {
|
|
|
440
510
|
this.automaticControl = !manual;
|
|
441
511
|
return { status: manual ? "active" : "automatic" };
|
|
442
512
|
} catch (error) {
|
|
443
|
-
if (this.current(subject)
|
|
513
|
+
if (this.current(subject) && !(error instanceof RoutingError && error.code === "worker_disabled"))
|
|
514
|
+
this.mark(error);
|
|
444
515
|
return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
|
|
445
516
|
}
|
|
446
517
|
});
|
|
@@ -448,6 +519,118 @@ export class RoutingRuntime {
|
|
|
448
519
|
setManualProfile(profile, _mechanism) {
|
|
449
520
|
return this.control(profile, true);
|
|
450
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
|
+
}
|
|
451
634
|
setAutomaticControl(_mechanism) {
|
|
452
635
|
return this.control("coordinator", false);
|
|
453
636
|
}
|
|
@@ -474,6 +657,7 @@ export class RoutingRuntime {
|
|
|
474
657
|
? state.handoffs.get(state.assessment.handoffId)
|
|
475
658
|
: undefined;
|
|
476
659
|
const selection = a ? (state.selections.get(a.id) ?? emptySelection()) : emptySelection();
|
|
660
|
+
const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
|
|
477
661
|
const unit = state.unitId ? state.units.get(state.unitId) : undefined;
|
|
478
662
|
const unitNumber = unit ? [...state.units.keys()].indexOf(unit.id) + 1 : undefined;
|
|
479
663
|
const assignmentNumber = unit && a ? unit.assignmentIds.indexOf(a.id) + 1 : undefined;
|
|
@@ -484,20 +668,29 @@ export class RoutingRuntime {
|
|
|
484
668
|
`Unit: ${unit ? `U${unitNumber} (${unit.id})` : "none"}`,
|
|
485
669
|
`Assignment: ${a ? `A${assignmentNumber} (${a.id}, ${a.state})` : "none"}`,
|
|
486
670
|
`Handoff: ${h ? `${h.id} (${h.kind}, ${h.state})` : "none"}`,
|
|
671
|
+
`Delegation: ${this.capability?.delegation ?? "executor"}`,
|
|
487
672
|
`Projection: ${this.projectionEnabled ? "enabled" : "bypassed"}`,
|
|
488
673
|
"Completed/superseded contracts and reports are historical context. Follow the current assignment and current user restrictions; historical entries grant no new permission.",
|
|
489
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
|
+
: "",
|
|
490
681
|
`Mechanical evidence facts (not semantic acceptance): ${JSON.stringify(this.evidenceFacts(state))}`,
|
|
491
682
|
this.error ? `Blocked: ${this.error}` : "",
|
|
492
683
|
this.projectionError ? `Evidence limitation: ${this.projectionError}` : "",
|
|
493
684
|
attention
|
|
494
|
-
?
|
|
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.`
|
|
495
688
|
: "",
|
|
496
|
-
a?.state === "returned"
|
|
497
|
-
? "
|
|
689
|
+
a?.state === "returned" && recovery?.state !== "reading"
|
|
690
|
+
? "Worker ordinary task work has ended. Only supported handoff correction is permitted."
|
|
498
691
|
: "",
|
|
499
|
-
this.turn?.profile
|
|
500
|
-
?
|
|
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.`
|
|
501
694
|
: "",
|
|
502
695
|
...(state.assessment?.problems ?? []).map((p) => `${p.code}: ${p.detail}`),
|
|
503
696
|
]
|
|
@@ -516,16 +709,19 @@ export class RoutingRuntime {
|
|
|
516
709
|
const selection = state.assignmentId
|
|
517
710
|
? (state.selections.get(state.assignmentId) ?? emptySelection())
|
|
518
711
|
: emptySelection();
|
|
712
|
+
const sources = this.sources(state);
|
|
519
713
|
return {
|
|
520
714
|
revision: selection.revision,
|
|
521
715
|
selected: selection.selected.map((ref) => {
|
|
522
716
|
const entry = this.ctx?.sessionManager.getEntry?.(ref.slice(4).replace(/#text$/, ""));
|
|
717
|
+
const locator = sources.locator(ref);
|
|
523
718
|
return {
|
|
524
719
|
ref,
|
|
525
720
|
kind: ref.endsWith("#text") ? "assistant-text" : (entry?.message?.role ?? "unknown"),
|
|
526
721
|
toolName: entry?.message?.toolName,
|
|
527
722
|
producer: state.authors.get(entry?.id)?.profile ?? "common",
|
|
528
723
|
assignment: state.authors.get(entry?.id)?.assignmentId,
|
|
724
|
+
...(locator ? { locator } : {}),
|
|
529
725
|
};
|
|
530
726
|
}),
|
|
531
727
|
unresolved: selection.unresolved,
|
|
@@ -547,18 +743,9 @@ export class RoutingRuntime {
|
|
|
547
743
|
pair: this.profilePair(profile),
|
|
548
744
|
systemPrompt: this.ctx.getSystemPrompt?.() ?? "",
|
|
549
745
|
tools: (this.pi.getAllTools?.() ?? [])
|
|
550
|
-
.filter((tool) =>
|
|
551
|
-
ROUTING_TOOLS.includes(tool.name)
|
|
552
|
-
? tool.name === "freeflow_unit" ||
|
|
553
|
-
(profile === "coordinator"
|
|
554
|
-
? tool.name === "freeflow_delegate"
|
|
555
|
-
: tool.name === "freeflow_return" || (tool.name === "freeflow_project" && this.projectionEnabled))
|
|
556
|
-
: this.pi.getActiveTools?.().includes(tool.name),
|
|
557
|
-
)
|
|
746
|
+
.filter((tool) => this.pi.getActiveTools?.().includes(tool.name))
|
|
558
747
|
.map((tool) =>
|
|
559
|
-
ROUTING_TOOLS.includes(tool.name)
|
|
560
|
-
? { ...tool, parameters: schemaForProfile(tool.name, profile === "coordinator") }
|
|
561
|
-
: tool,
|
|
748
|
+
ROUTING_TOOLS.includes(tool.name) ? { ...tool, parameters: ROUTING_SCHEMAS[tool.name] } : tool,
|
|
562
749
|
),
|
|
563
750
|
runtimeMessage: this.runtimeMessage(
|
|
564
751
|
state,
|
|
@@ -569,6 +756,35 @@ export class RoutingRuntime {
|
|
|
569
756
|
restoring,
|
|
570
757
|
});
|
|
571
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
|
+
}
|
|
572
788
|
async context(ctx, incoming) {
|
|
573
789
|
this.ctx = ctx;
|
|
574
790
|
const input = incoming.filter(
|
|
@@ -579,10 +795,10 @@ export class RoutingRuntime {
|
|
|
579
795
|
),
|
|
580
796
|
);
|
|
581
797
|
this.messages = input;
|
|
582
|
-
if (!this.supported() || !this.store) return input;
|
|
798
|
+
if (!this.supported() || !this.store) return this.ordinaryContext(input);
|
|
583
799
|
try {
|
|
584
800
|
const state = this.stateData();
|
|
585
|
-
if (state.control !== "automatic") return input;
|
|
801
|
+
if (state.control !== "automatic") return this.ordinaryContext(input);
|
|
586
802
|
check(!this.error && !this.store.blocked && state.profile, "routing_blocked", this.error ?? this.store.blocked);
|
|
587
803
|
check(samePair(this.observed(), this.profilePair(state.profile)), "prepared_pair_mismatch");
|
|
588
804
|
const sources = this.sources(state),
|
|
@@ -598,16 +814,19 @@ export class RoutingRuntime {
|
|
|
598
814
|
},
|
|
599
815
|
JSON.stringify(["attention", state.assessment.handoffId, user]),
|
|
600
816
|
);
|
|
601
|
-
// New input reaching an already prepared
|
|
817
|
+
// New input reaching an already prepared worker request cannot be rerouted by changing only the live model.
|
|
602
818
|
const outstanding = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
819
|
+
const worker = outstanding ? this.assignedWorker(state, outstanding.id) : undefined;
|
|
603
820
|
const interrupted =
|
|
604
|
-
state.profile
|
|
821
|
+
isWorkerProfile(state.profile) &&
|
|
822
|
+
(state.profile !== worker || user !== (outstanding ? this.workerBasis(state, outstanding.id) : undefined));
|
|
605
823
|
if (!this.turn || this.turn.bound) {
|
|
606
824
|
this.receipts.clear();
|
|
607
825
|
const execution = {
|
|
608
826
|
id: randomUUID(),
|
|
609
827
|
profile: state.profile,
|
|
610
828
|
assignmentId: state.assignmentId,
|
|
829
|
+
recoveryId: state.recoveryId,
|
|
611
830
|
basisUserEntryId: user,
|
|
612
831
|
pair: this.observed(),
|
|
613
832
|
resultEntryIds: [],
|
|
@@ -617,6 +836,7 @@ export class RoutingRuntime {
|
|
|
617
836
|
id: execution.id,
|
|
618
837
|
profile: execution.profile,
|
|
619
838
|
assignmentId: execution.assignmentId,
|
|
839
|
+
recoveryId: execution.recoveryId,
|
|
620
840
|
basisUserEntryId: user,
|
|
621
841
|
before,
|
|
622
842
|
pair: execution.pair,
|
|
@@ -639,12 +859,12 @@ export class RoutingRuntime {
|
|
|
639
859
|
check(prepared.ready, "context_unavailable", prepared.problems.map((p) => p.detail).join("; "));
|
|
640
860
|
const known = this.stateData().exposure;
|
|
641
861
|
const added = prepared.fullSources.filter((s) => known.get(s.ref) !== s.hash);
|
|
642
|
-
if (this.turn.profile
|
|
862
|
+
if (isWorkerProfile(this.turn.profile))
|
|
643
863
|
for (let i = 0; i < added.length; i += 128)
|
|
644
864
|
this.append({
|
|
645
865
|
type: "sources-exposed",
|
|
646
866
|
executionId: this.turn.id,
|
|
647
|
-
view:
|
|
867
|
+
view: this.turn.profile,
|
|
648
868
|
sources: added.slice(i, i + 128).map((s) => ({ ref: s.ref, bodyHash: s.hash })),
|
|
649
869
|
});
|
|
650
870
|
if (interrupted)
|
|
@@ -661,7 +881,7 @@ export class RoutingRuntime {
|
|
|
661
881
|
} catch (error) {
|
|
662
882
|
this.mark(error);
|
|
663
883
|
ctx.abort?.();
|
|
664
|
-
// Do not send an accidental full
|
|
884
|
+
// Do not send an accidental full worker history on a projection failure.
|
|
665
885
|
return [
|
|
666
886
|
{
|
|
667
887
|
role: "custom",
|
|
@@ -669,7 +889,7 @@ export class RoutingRuntime {
|
|
|
669
889
|
content: `Automatic request blocked: ${this.error}`,
|
|
670
890
|
display: false,
|
|
671
891
|
timestamp: 0,
|
|
672
|
-
details: { routingInstance: this.token },
|
|
892
|
+
details: { routingInstance: this.token, routingRequestBlocked: true },
|
|
673
893
|
},
|
|
674
894
|
];
|
|
675
895
|
}
|
|
@@ -680,6 +900,57 @@ export class RoutingRuntime {
|
|
|
680
900
|
contextOperation(name, input) {
|
|
681
901
|
return name === "freeflow_context" && ["archive", "restore", "search", "retrieve"].includes(input?.operation);
|
|
682
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
|
+
}
|
|
683
954
|
batch(callId, name) {
|
|
684
955
|
check(this.turn?.message, "batch_unavailable");
|
|
685
956
|
const matches = this.ctx.sessionManager
|
|
@@ -699,14 +970,16 @@ export class RoutingRuntime {
|
|
|
699
970
|
this.turn.sourceFingerprint = fingerprint;
|
|
700
971
|
this.turn.message = structuredClone(matches[0].message);
|
|
701
972
|
const calls = (this.turn.message.content ?? []).filter((b) => b.type === "toolCall");
|
|
702
|
-
const handoffs = calls.filter((b) =>
|
|
973
|
+
const handoffs = calls.filter((b) => this.handoffOperation(b.name, b.arguments));
|
|
703
974
|
check(
|
|
704
975
|
!handoffs.length ||
|
|
705
976
|
(handoffs.length === 1 &&
|
|
706
|
-
|
|
977
|
+
this.handoffOperation(calls.at(-1)?.name, calls.at(-1)?.arguments) &&
|
|
707
978
|
calls.every(
|
|
708
979
|
(b) =>
|
|
709
|
-
|
|
980
|
+
this.handoffOperation(b.name, b.arguments) ||
|
|
981
|
+
b.name === "freeflow_project" ||
|
|
982
|
+
this.contextOperation(b.name, b.arguments),
|
|
710
983
|
)),
|
|
711
984
|
"invalid_handoff_batch",
|
|
712
985
|
"A handoff must be last and cannot accompany ordinary task tools.",
|
|
@@ -730,14 +1003,21 @@ export class RoutingRuntime {
|
|
|
730
1003
|
this.batch(event.toolCallId, name);
|
|
731
1004
|
check(!this.error && !this.store?.blocked, "routing_blocked");
|
|
732
1005
|
this.openTurn();
|
|
733
|
-
if (this.turn?.profile
|
|
1006
|
+
if (isWorkerProfile(this.turn?.profile) && !isRouting && !this.contextOperation(name, event.input)) {
|
|
734
1007
|
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
735
|
-
|
|
1008
|
+
const worker = a ? this.assignedWorker(state, a.id) : undefined;
|
|
1009
|
+
const ordinary =
|
|
736
1010
|
a?.state === "outstanding" &&
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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");
|
|
741
1021
|
}
|
|
742
1022
|
return undefined;
|
|
743
1023
|
} catch (error) {
|
|
@@ -757,8 +1037,14 @@ export class RoutingRuntime {
|
|
|
757
1037
|
this.ctx = ctx;
|
|
758
1038
|
try {
|
|
759
1039
|
check(!signal?.aborted, "cancelled");
|
|
760
|
-
if (name === "freeflow_unit" && input.operation === "inspect")
|
|
761
|
-
|
|
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);
|
|
762
1048
|
const op = this.callOperation(callId, name);
|
|
763
1049
|
const cached = this.receipts.get(op);
|
|
764
1050
|
if (cached) {
|
|
@@ -773,7 +1059,7 @@ export class RoutingRuntime {
|
|
|
773
1059
|
: name === "freeflow_project"
|
|
774
1060
|
? this.project(input, op)
|
|
775
1061
|
: name === "freeflow_unit"
|
|
776
|
-
? this.unit(input, op)
|
|
1062
|
+
? this.unit(input, callId, op)
|
|
777
1063
|
: undefined;
|
|
778
1064
|
if (value !== undefined) {
|
|
779
1065
|
this.receipts.set(op, { input: canonical(input), value: structuredClone(value) });
|
|
@@ -792,7 +1078,13 @@ export class RoutingRuntime {
|
|
|
792
1078
|
? undefined
|
|
793
1079
|
: name === "freeflow_delegate" || name === "freeflow_unit"
|
|
794
1080
|
? "coordinator"
|
|
795
|
-
:
|
|
1081
|
+
: (() => {
|
|
1082
|
+
try {
|
|
1083
|
+
return this.assignedWorker(this.stateData());
|
|
1084
|
+
} catch {
|
|
1085
|
+
return undefined;
|
|
1086
|
+
}
|
|
1087
|
+
})(),
|
|
796
1088
|
recoveryAction:
|
|
797
1089
|
error instanceof RoutingError &&
|
|
798
1090
|
[
|
|
@@ -813,26 +1105,48 @@ export class RoutingRuntime {
|
|
|
813
1105
|
return [...this.stateData().events.values()].find(
|
|
814
1106
|
(e) =>
|
|
815
1107
|
e.operationId === op &&
|
|
816
|
-
[
|
|
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),
|
|
817
1116
|
);
|
|
818
1117
|
}
|
|
819
1118
|
delegate(input, callId, op) {
|
|
820
1119
|
const prior = this.duplicate(op);
|
|
821
1120
|
if (prior) {
|
|
822
1121
|
check(
|
|
823
|
-
prior.data.type === "delegate-accepted" &&
|
|
1122
|
+
prior.data.type === "delegate-accepted" &&
|
|
1123
|
+
prior.data.assignment.contract === input.contract &&
|
|
1124
|
+
(input.worker === undefined || prior.data.handoff.to === input.worker),
|
|
824
1125
|
"operation_conflict",
|
|
825
1126
|
);
|
|
826
1127
|
return { status: "accepted", handoff: prior.data.handoff.id, unchanged: true };
|
|
827
1128
|
}
|
|
828
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.`);
|
|
829
1140
|
const unit = state.unitId
|
|
830
1141
|
? state.units.get(state.unitId)
|
|
831
1142
|
: { id: randomUUID(), objective: input.contract, state: "open", assignmentIds: [] };
|
|
832
1143
|
const old = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
833
1144
|
if (input.operation === "replace") check(old?.state === "outstanding" && !this.applying, "no_quiescent_assignment");
|
|
834
1145
|
else
|
|
835
|
-
check(
|
|
1146
|
+
check(
|
|
1147
|
+
input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId && !state.recoveryId,
|
|
1148
|
+
"assignment_outstanding",
|
|
1149
|
+
);
|
|
836
1150
|
const assignmentId = randomUUID(),
|
|
837
1151
|
id = randomUUID();
|
|
838
1152
|
const assignment = {
|
|
@@ -849,7 +1163,7 @@ export class RoutingRuntime {
|
|
|
849
1163
|
assignmentId,
|
|
850
1164
|
text: input.contract,
|
|
851
1165
|
from: "coordinator",
|
|
852
|
-
to:
|
|
1166
|
+
to: worker,
|
|
853
1167
|
executionId: this.turn.id,
|
|
854
1168
|
toolCallId: callId,
|
|
855
1169
|
basisUserEntryId: this.turn.basisUserEntryId,
|
|
@@ -879,6 +1193,7 @@ export class RoutingRuntime {
|
|
|
879
1193
|
status: "accepted",
|
|
880
1194
|
unit: unit.id,
|
|
881
1195
|
assignment: assignmentId,
|
|
1196
|
+
worker,
|
|
882
1197
|
handoff: id,
|
|
883
1198
|
transition: "pending",
|
|
884
1199
|
contract: input.contract,
|
|
@@ -897,25 +1212,28 @@ export class RoutingRuntime {
|
|
|
897
1212
|
const state = this.stateData(),
|
|
898
1213
|
a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
899
1214
|
check(a, "assignment_missing");
|
|
900
|
-
const
|
|
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;
|
|
901
1221
|
const selection = state.selections.get(a.id) ?? emptySelection();
|
|
902
1222
|
const duplicate = this.duplicate(op);
|
|
903
1223
|
if (duplicate) {
|
|
904
1224
|
if (input.operation === "retry") check(duplicate.data.type === "handoff-retry-requested", "operation_conflict");
|
|
905
|
-
else
|
|
1225
|
+
else {
|
|
1226
|
+
const expected = input.operation === "supplement" ? "recovery-supplement-accepted" : "return-accepted";
|
|
906
1227
|
check(
|
|
907
|
-
duplicate.data.type ===
|
|
1228
|
+
duplicate.data.type === expected &&
|
|
908
1229
|
duplicate.data.handoff.text === input.report &&
|
|
909
1230
|
duplicate.data.handoff.outcome === input.outcome &&
|
|
910
1231
|
canonical(duplicate.data.handoff.limitations) === canonical(input.limitations ?? []),
|
|
911
1232
|
"operation_conflict",
|
|
912
1233
|
);
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
unchanged: true,
|
|
917
|
-
handoff: duplicate.data.handoffId ?? duplicate.data.handoff.id,
|
|
918
|
-
};
|
|
1234
|
+
}
|
|
1235
|
+
const handoff = duplicate.data.handoffId ?? duplicate.data.handoff.id;
|
|
1236
|
+
return { ...this.returnReadiness(handoff), unchanged: true };
|
|
919
1237
|
}
|
|
920
1238
|
if (input.operation === "retry") {
|
|
921
1239
|
check(saved && ["blocked", "pending"].includes(saved.state), "saved_return_missing");
|
|
@@ -931,25 +1249,55 @@ export class RoutingRuntime {
|
|
|
931
1249
|
},
|
|
932
1250
|
op,
|
|
933
1251
|
);
|
|
934
|
-
return {
|
|
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);
|
|
935
1282
|
}
|
|
936
1283
|
check(input.operation === "submit", "invalid_return_operation");
|
|
937
1284
|
check(
|
|
938
|
-
a.state === "outstanding" ||
|
|
1285
|
+
a.state === "outstanding" ||
|
|
1286
|
+
(a.state === "returned" && original && ["pending", "blocked"].includes(original.state)),
|
|
939
1287
|
"assignment_task_ended",
|
|
940
1288
|
);
|
|
941
1289
|
const h = {
|
|
942
|
-
id:
|
|
1290
|
+
id: original?.id ?? randomUUID(),
|
|
943
1291
|
kind: "return",
|
|
944
1292
|
assignmentId: a.id,
|
|
945
1293
|
text: input.report,
|
|
946
|
-
from:
|
|
1294
|
+
from: worker,
|
|
947
1295
|
to: "coordinator",
|
|
948
1296
|
executionId: this.turn.id,
|
|
949
1297
|
toolCallId: callId,
|
|
950
1298
|
basisUserEntryId: this.turn.basisUserEntryId,
|
|
951
1299
|
state: "pending",
|
|
952
|
-
reportRevision: (
|
|
1300
|
+
reportRevision: (original?.reportRevision ?? 0) + 1,
|
|
953
1301
|
outcome: input.outcome,
|
|
954
1302
|
limitations: input.limitations ?? [],
|
|
955
1303
|
};
|
|
@@ -957,16 +1305,25 @@ export class RoutingRuntime {
|
|
|
957
1305
|
return this.returnReadiness(h.id);
|
|
958
1306
|
}
|
|
959
1307
|
returnReadiness(id) {
|
|
960
|
-
const
|
|
1308
|
+
const state = this.stateData();
|
|
1309
|
+
const h = state.handoffs.get(id);
|
|
961
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;
|
|
962
1315
|
return {
|
|
963
1316
|
status: "accepted",
|
|
964
|
-
reportSaved:
|
|
1317
|
+
reportSaved: !isSupplement,
|
|
1318
|
+
supplementSaved: isSupplement,
|
|
965
1319
|
handoff: id,
|
|
966
|
-
|
|
1320
|
+
...(recovery ? { recovery: recovery.id } : {}),
|
|
1321
|
+
...(isSupplement ? { supplementRevision: h.reportRevision } : { reportRevision: h.reportRevision }),
|
|
967
1322
|
assignmentRef: `assignment:${h.assignmentId}`,
|
|
968
|
-
|
|
969
|
-
|
|
1323
|
+
...(isSupplement
|
|
1324
|
+
? { supplementRef: `supplement:${recovery?.id ?? "unknown"}:${h.reportRevision}` }
|
|
1325
|
+
: { reportRef: `report:${h.id}:${h.reportRevision}` }),
|
|
1326
|
+
producer: h.from,
|
|
970
1327
|
outcome: h.outcome,
|
|
971
1328
|
limitations: h.limitations,
|
|
972
1329
|
ready: prepared.ready,
|
|
@@ -977,8 +1334,8 @@ export class RoutingRuntime {
|
|
|
977
1334
|
finalExchangePending: true,
|
|
978
1335
|
provisional:
|
|
979
1336
|
"The finalized handoff exchange, target configuration and budget are revalidated at turn_end; this is not delivery evidence.",
|
|
980
|
-
report: h.text,
|
|
981
|
-
evidence: this.evidenceFacts(
|
|
1337
|
+
...(isSupplement ? { supplement: h.text } : { report: h.text }),
|
|
1338
|
+
evidence: this.evidenceFacts(state),
|
|
982
1339
|
};
|
|
983
1340
|
}
|
|
984
1341
|
project(input, op) {
|
|
@@ -1013,7 +1370,7 @@ export class RoutingRuntime {
|
|
|
1013
1370
|
[...sources.byRef.values()]
|
|
1014
1371
|
.filter(
|
|
1015
1372
|
(s) =>
|
|
1016
|
-
s.producer
|
|
1373
|
+
isWorkerProfile(s.producer) &&
|
|
1017
1374
|
(isTaskEvidence(s) || (scope === "selected" && next.selected.includes(s.ref))) &&
|
|
1018
1375
|
(scope === "selected"
|
|
1019
1376
|
? next.selected.includes(s.ref)
|
|
@@ -1027,12 +1384,14 @@ export class RoutingRuntime {
|
|
|
1027
1384
|
s.message.content?.some((b) => b.type === "text" && b.text?.trim())),
|
|
1028
1385
|
)
|
|
1029
1386
|
.map((s) => {
|
|
1387
|
+
const locator = sources.locator(s.ref);
|
|
1030
1388
|
return {
|
|
1031
1389
|
ref: s.ref,
|
|
1032
1390
|
kind: s.original ? "assistant-text" : s.message.role,
|
|
1033
1391
|
producer: s.producer,
|
|
1034
1392
|
assignment: s.assignmentId,
|
|
1035
1393
|
toolName: s.message.toolName,
|
|
1394
|
+
...(locator ? { locator } : {}),
|
|
1036
1395
|
active: s.active,
|
|
1037
1396
|
selected: next.selected.includes(s.ref),
|
|
1038
1397
|
retainedSelection: next.selected.includes(s.ref) && !isTaskEvidence(s),
|
|
@@ -1057,7 +1416,7 @@ export class RoutingRuntime {
|
|
|
1057
1416
|
},
|
|
1058
1417
|
otherAssignments: [...sources.byRef.values()].filter(
|
|
1059
1418
|
(s) =>
|
|
1060
|
-
s.producer
|
|
1419
|
+
isWorkerProfile(s.producer) && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
|
|
1061
1420
|
).length,
|
|
1062
1421
|
}),
|
|
1063
1422
|
);
|
|
@@ -1109,10 +1468,40 @@ export class RoutingRuntime {
|
|
|
1109
1468
|
estimate: { tokens: prepared.estimatedTokens, method: prepared.estimateMethod },
|
|
1110
1469
|
};
|
|
1111
1470
|
}
|
|
1112
|
-
unit(input, op) {
|
|
1471
|
+
unit(input, callId, op) {
|
|
1113
1472
|
const state = this.stateData();
|
|
1114
1473
|
const prior = [...state.events.values()].find((e) => e.operationId === op);
|
|
1115
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
|
+
}
|
|
1116
1505
|
check(
|
|
1117
1506
|
(input.operation === "assess" && prior.data.type === "assessment-resumed") ||
|
|
1118
1507
|
(input.operation === "close" &&
|
|
@@ -1123,18 +1512,95 @@ export class RoutingRuntime {
|
|
|
1123
1512
|
);
|
|
1124
1513
|
return { status: "unchanged", operation: input.operation };
|
|
1125
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
|
+
}
|
|
1126
1591
|
if (input.operation === "assess") {
|
|
1127
1592
|
check(state.assessment, "assessment_missing");
|
|
1593
|
+
check(!state.recoveryId, "recovery_outstanding");
|
|
1128
1594
|
if (state.assessment.view === "active") return { status: "unchanged", ready: true };
|
|
1129
1595
|
const prepared = this.prepared("coordinator", this.messages, state.assessment.handoffId, true);
|
|
1130
1596
|
if (!prepared.ready) return { status: "suspended", ready: false, problems: prepared.problems };
|
|
1131
|
-
check(prepared.reservation, "reservation_missing");
|
|
1597
|
+
if (this.projectionEnabled) check(prepared.reservation, "reservation_missing");
|
|
1132
1598
|
this.append(
|
|
1133
1599
|
{
|
|
1134
1600
|
type: "assessment-resumed",
|
|
1135
1601
|
handoffId: state.assessment.handoffId,
|
|
1136
1602
|
basisUserEntryId: this.turn.basisUserEntryId,
|
|
1137
|
-
reservation: prepared.reservation,
|
|
1603
|
+
...(prepared.reservation ? { reservation: prepared.reservation } : {}),
|
|
1138
1604
|
},
|
|
1139
1605
|
op,
|
|
1140
1606
|
);
|
|
@@ -1149,6 +1615,19 @@ export class RoutingRuntime {
|
|
|
1149
1615
|
check(input.operation === "close" && state.unitId, "unit_missing");
|
|
1150
1616
|
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1151
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
|
+
}
|
|
1152
1631
|
this.append(
|
|
1153
1632
|
{
|
|
1154
1633
|
type: "unit-closed",
|
|
@@ -1231,6 +1710,7 @@ export class RoutingRuntime {
|
|
|
1231
1710
|
[...state.assignments.values()].reverse().map((a) => {
|
|
1232
1711
|
const u = state.units.get(a.unitId),
|
|
1233
1712
|
h = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1713
|
+
const delegate = state.handoffs.get(a.delegateHandoffId);
|
|
1234
1714
|
return {
|
|
1235
1715
|
ref: `assignment:${a.id}`,
|
|
1236
1716
|
unitRef: `unit:${a.unitId}`,
|
|
@@ -1242,8 +1722,8 @@ export class RoutingRuntime {
|
|
|
1242
1722
|
summary: a.contract.slice(0, 160),
|
|
1243
1723
|
reportAvailable: !!h,
|
|
1244
1724
|
outcome: h?.outcome,
|
|
1245
|
-
from:
|
|
1246
|
-
to:
|
|
1725
|
+
from: delegate?.from,
|
|
1726
|
+
to: delegate?.to,
|
|
1247
1727
|
};
|
|
1248
1728
|
});
|
|
1249
1729
|
const page = this.page("work-history", rows, input.cursor, input.limit ?? 20);
|
|
@@ -1262,9 +1742,9 @@ export class RoutingRuntime {
|
|
|
1262
1742
|
"Use a ref returned by history inspection.",
|
|
1263
1743
|
);
|
|
1264
1744
|
check(
|
|
1265
|
-
/^(?:(?:unit|assignment):[^:\s]+|report:[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
|
|
1745
|
+
/^(?:(?:unit|assignment|recovery):[^:\s]+|(?:report|supplement):[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
|
|
1266
1746
|
"invalid_work_ref",
|
|
1267
|
-
"Use
|
|
1747
|
+
"Use assignment, report, recovery, or supplement refs returned by a receipt or inspection, unchanged. A bare handoff ID is not a work ref.",
|
|
1268
1748
|
);
|
|
1269
1749
|
if (input.ref.startsWith("unit:")) {
|
|
1270
1750
|
const u = state.units.get(input.ref.slice(5));
|
|
@@ -1287,6 +1767,63 @@ export class RoutingRuntime {
|
|
|
1287
1767
|
historical: true,
|
|
1288
1768
|
};
|
|
1289
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
|
+
}
|
|
1290
1827
|
let savedRevision;
|
|
1291
1828
|
if (input.ref.startsWith("report:")) {
|
|
1292
1829
|
const match = /^report:([^:]+):([0-9]+)$/.exec(input.ref);
|
|
@@ -1335,8 +1872,23 @@ export class RoutingRuntime {
|
|
|
1335
1872
|
return {
|
|
1336
1873
|
...this.state(),
|
|
1337
1874
|
unit: unit ? { id: unit.id, state: unit.state, assignments: unit.assignmentIds.length } : null,
|
|
1338
|
-
assignment: a ? { id: a.id, state: a.state } : null,
|
|
1875
|
+
assignment: a ? { id: a.id, state: a.state, worker: this.assignedWorker(state, a.id) } : null,
|
|
1339
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,
|
|
1340
1892
|
assessment: state.assessment
|
|
1341
1893
|
? { handoffId: state.assessment.handoffId, view: state.assessment.view, problems: state.assessment.problems }
|
|
1342
1894
|
: null,
|
|
@@ -1423,23 +1975,29 @@ export class RoutingRuntime {
|
|
|
1423
1975
|
"source_exchange_incomplete",
|
|
1424
1976
|
"The saved handoff has an incomplete native exchange; reconcile or explicitly dispose of it.",
|
|
1425
1977
|
);
|
|
1426
|
-
|
|
1978
|
+
let attentionProblems = [];
|
|
1979
|
+
if (["return", "recovery-return"].includes(h.kind) && this.projectionEnabled) {
|
|
1427
1980
|
const prepared = this.prepared("coordinator", inputs, h.id);
|
|
1428
1981
|
if (!prepared.ready) {
|
|
1982
|
+
const attentionFallback = h.kind === "recovery-return" && h.outcome !== "completed";
|
|
1429
1983
|
this.projectionError = prepared.problems.map((p) => p.code).join(", ");
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
.
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
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 });
|
|
1440
2000
|
}
|
|
1441
|
-
check(prepared.reservation, "reservation_missing");
|
|
1442
|
-
this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
|
|
1443
2001
|
}
|
|
1444
2002
|
try {
|
|
1445
2003
|
const current = this.stateData();
|
|
@@ -1449,6 +2007,28 @@ export class RoutingRuntime {
|
|
|
1449
2007
|
this.append({ type: "handoff-state", handoffId: h.id, state: "configured", observedPair: this.observed() });
|
|
1450
2008
|
const completed = this.stateData();
|
|
1451
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
|
+
}
|
|
1452
2032
|
if (h.kind === "return" && completed.assessment?.view === "suspended" && reservation) {
|
|
1453
2033
|
this.append({
|
|
1454
2034
|
type: "assessment-resumed",
|
|
@@ -1457,7 +2037,7 @@ export class RoutingRuntime {
|
|
|
1457
2037
|
reservation,
|
|
1458
2038
|
});
|
|
1459
2039
|
}
|
|
1460
|
-
this.projectionError = undefined;
|
|
2040
|
+
if (!attentionProblems.length) this.projectionError = undefined;
|
|
1461
2041
|
} catch (error) {
|
|
1462
2042
|
if (this.current(subject) && !this.store?.blocked && !this.error && this.stateData().pendingId === h.id) {
|
|
1463
2043
|
this.append({
|
|
@@ -1467,7 +2047,7 @@ export class RoutingRuntime {
|
|
|
1467
2047
|
reason: error instanceof Error ? error.message.slice(0, 4096) : "configuration_failed",
|
|
1468
2048
|
});
|
|
1469
2049
|
this.ctx.ui?.notify?.(
|
|
1470
|
-
`Couldn’t switch to ${h.to
|
|
2050
|
+
`Couldn’t switch to ${h.to.charAt(0).toUpperCase()}${h.to.slice(1)} — ${["return", "recovery-return"].includes(h.kind) ? "report" : "assignment"} saved.`,
|
|
1471
2051
|
"warning",
|
|
1472
2052
|
);
|
|
1473
2053
|
} else throw error;
|
|
@@ -1493,7 +2073,7 @@ export class RoutingRuntime {
|
|
|
1493
2073
|
check(
|
|
1494
2074
|
!ctx.hasPendingMessages?.(),
|
|
1495
2075
|
"pending_input",
|
|
1496
|
-
"Queued input must be delivered and reconciled before resuming
|
|
2076
|
+
"Queued input must be delivered and reconciled before resuming a worker.",
|
|
1497
2077
|
);
|
|
1498
2078
|
const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
|
|
1499
2079
|
if (pending) {
|
|
@@ -1503,17 +2083,18 @@ export class RoutingRuntime {
|
|
|
1503
2083
|
this.guard(subject);
|
|
1504
2084
|
if (this.stateData().pendingId) {
|
|
1505
2085
|
check(
|
|
1506
|
-
|
|
2086
|
+
["return", "recovery-return"].includes(pending.kind) && this.projectionError,
|
|
1507
2087
|
"handoff_still_blocked",
|
|
1508
2088
|
"Saved transfer is still blocked; reconcile its configuration before retrying.",
|
|
1509
2089
|
);
|
|
2090
|
+
const worker = this.assignedWorker(this.stateData(), pending.assignmentId);
|
|
1510
2091
|
this.append({
|
|
1511
2092
|
type: "control",
|
|
1512
2093
|
control: "automatic",
|
|
1513
|
-
profile:
|
|
2094
|
+
profile: worker,
|
|
1514
2095
|
reason: "Explicit resume of saved-return evidence correction only; ordinary assignment work remains ended",
|
|
1515
2096
|
});
|
|
1516
|
-
await this.applyPair(this.profilePair(
|
|
2097
|
+
await this.applyPair(this.profilePair(worker));
|
|
1517
2098
|
}
|
|
1518
2099
|
} else {
|
|
1519
2100
|
const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
@@ -1523,7 +2104,22 @@ export class RoutingRuntime {
|
|
|
1523
2104
|
(x) =>
|
|
1524
2105
|
x.profile === "coordinator" && x.basisUserEntryId === user && x.assistantEntryId && x.outcome === "completed",
|
|
1525
2106
|
);
|
|
1526
|
-
|
|
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") {
|
|
1527
2123
|
check(
|
|
1528
2124
|
user === this.taskBasis(state, assignment.id) || coordinatorSawInput,
|
|
1529
2125
|
"input_unreconciled",
|
|
@@ -1533,10 +2129,10 @@ export class RoutingRuntime {
|
|
|
1533
2129
|
this.append({
|
|
1534
2130
|
type: "control",
|
|
1535
2131
|
control: "automatic",
|
|
1536
|
-
profile:
|
|
2132
|
+
profile: worker,
|
|
1537
2133
|
reason: "Explicit user resume of the unchanged assignment; current restrictions remain applicable",
|
|
1538
2134
|
});
|
|
1539
|
-
await this.applyPair(this.profilePair(
|
|
2135
|
+
await this.applyPair(this.profilePair(worker));
|
|
1540
2136
|
} else await this.applyPair(this.profilePair("coordinator"));
|
|
1541
2137
|
}
|
|
1542
2138
|
this.guard(subject);
|
|
@@ -1573,7 +2169,7 @@ export class RoutingRuntime {
|
|
|
1573
2169
|
check(
|
|
1574
2170
|
words.length === 2 && [...PROFILES, "auto"].includes(words[1]),
|
|
1575
2171
|
"invalid_profile_command",
|
|
1576
|
-
"Use /freeflow profile coordinator|executor|auto|history, or /freeflow resume.",
|
|
2172
|
+
"Use /freeflow profile coordinator|helper|executor|auto|history, or /freeflow resume.",
|
|
1577
2173
|
);
|
|
1578
2174
|
const result = words[1] === "auto" ? await this.setAutomaticControl() : await this.setManualProfile(words[1]);
|
|
1579
2175
|
ctx.ui.notify(JSON.stringify(result), result.status === "blocked" ? "warning" : "info");
|