@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, type SessionReader } from "../session-sources/events.js";
|
|
4
8
|
import { Sources, bodyHash, textRef, isTaskEvidence, type Source } from "../session-sources/sources.js";
|
|
5
|
-
import { pairFromProfile, type CognitiveRoutingCapabilityState } from "./config.js";
|
|
9
|
+
import { pairFromProfile, resolveCognitiveRoutingState, type CognitiveRoutingCapabilityState } from "./config.js";
|
|
6
10
|
import { prepareView, changeSelection, representationProblems, type PreparedView } from "./projection.js";
|
|
7
11
|
import { initialState } from "./state.js";
|
|
8
12
|
import {
|
|
@@ -12,13 +16,18 @@ import {
|
|
|
12
16
|
RoutingError,
|
|
13
17
|
canonical,
|
|
14
18
|
emptySelection,
|
|
19
|
+
idFor,
|
|
20
|
+
isWorkerProfile,
|
|
15
21
|
requireCondition as check,
|
|
16
22
|
samePair,
|
|
23
|
+
workersForDelegation,
|
|
17
24
|
type Profile,
|
|
25
|
+
type WorkerProfile,
|
|
18
26
|
type View,
|
|
19
27
|
type Pair,
|
|
20
28
|
type State,
|
|
21
29
|
type Handoff,
|
|
30
|
+
type Recovery,
|
|
22
31
|
type NativeEntry,
|
|
23
32
|
type EventData,
|
|
24
33
|
type Execution,
|
|
@@ -31,6 +40,7 @@ interface Turn {
|
|
|
31
40
|
id: string;
|
|
32
41
|
profile: View;
|
|
33
42
|
assignmentId?: string;
|
|
43
|
+
recoveryId?: string;
|
|
34
44
|
basisUserEntryId: string | null;
|
|
35
45
|
before: Set<string>;
|
|
36
46
|
message?: any;
|
|
@@ -77,7 +87,10 @@ export class RoutingRuntime {
|
|
|
77
87
|
private manualHold?: Profile;
|
|
78
88
|
private automaticControl = false;
|
|
79
89
|
private sourceCache?: { entries: NativeEntry[]; authors: number; source: Sources };
|
|
80
|
-
constructor(
|
|
90
|
+
constructor(
|
|
91
|
+
private readonly pi: any,
|
|
92
|
+
private readonly packageRoots: readonly string[] = [],
|
|
93
|
+
) {}
|
|
81
94
|
private subject(): Subject {
|
|
82
95
|
const branch = this.store?.reader.getBranch() ?? [];
|
|
83
96
|
return {
|
|
@@ -108,6 +121,29 @@ export class RoutingRuntime {
|
|
|
108
121
|
? state.resumeBasis.get(assignmentId)
|
|
109
122
|
: state.assignments.get(assignmentId)?.basisUserEntryId;
|
|
110
123
|
}
|
|
124
|
+
private enabledWorkers(): readonly WorkerProfile[] {
|
|
125
|
+
return workersForDelegation(this.capability?.delegation ?? "executor");
|
|
126
|
+
}
|
|
127
|
+
private assignedWorker(state: State, assignmentId = state.assignmentId): WorkerProfile {
|
|
128
|
+
const assignment = assignmentId ? state.assignments.get(assignmentId) : undefined;
|
|
129
|
+
const handoff = assignment ? state.handoffs.get(assignment.delegateHandoffId) : undefined;
|
|
130
|
+
check(
|
|
131
|
+
handoff?.kind === "delegate" && handoff.assignmentId === assignment?.id && isWorkerProfile(handoff.to),
|
|
132
|
+
"assignment_worker_missing",
|
|
133
|
+
"The current assignment has no valid recorded worker.",
|
|
134
|
+
);
|
|
135
|
+
return handoff.to;
|
|
136
|
+
}
|
|
137
|
+
private requiredProfiles(state = this.stateData()): Profile[] {
|
|
138
|
+
const profiles = new Set<Profile>(["coordinator", ...this.enabledWorkers()]);
|
|
139
|
+
if (state.profile) profiles.add(state.profile);
|
|
140
|
+
if (this.manualHold) profiles.add(this.manualHold);
|
|
141
|
+
const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
142
|
+
if (assignment?.state === "outstanding" || state.recoveryId) profiles.add(this.assignedWorker(state));
|
|
143
|
+
const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
|
|
144
|
+
if (pending && isWorkerProfile(pending.to)) profiles.add(pending.to);
|
|
145
|
+
return [...profiles];
|
|
146
|
+
}
|
|
111
147
|
private retireUnbound(reason: string): void {
|
|
112
148
|
for (const execution of this.stateData().executions.values()) {
|
|
113
149
|
if (!execution.assistantEntryId && !execution.interrupted)
|
|
@@ -124,6 +160,7 @@ export class RoutingRuntime {
|
|
|
124
160
|
id: t.id,
|
|
125
161
|
profile: t.profile,
|
|
126
162
|
assignmentId: t.assignmentId,
|
|
163
|
+
recoveryId: t.recoveryId,
|
|
127
164
|
basisUserEntryId: t.basisUserEntryId,
|
|
128
165
|
pair: t.pair,
|
|
129
166
|
resultEntryIds: [],
|
|
@@ -139,7 +176,7 @@ export class RoutingRuntime {
|
|
|
139
176
|
async beforeRun(ctx: any): Promise<void> {
|
|
140
177
|
this.ctx = ctx;
|
|
141
178
|
if (!this.store || !this.supported() || this.store.blocked || this.error) return;
|
|
142
|
-
if (this.stateData().control === "automatic" && this.stateData().profile
|
|
179
|
+
if (this.stateData().control === "automatic" && isWorkerProfile(this.stateData().profile)) {
|
|
143
180
|
const result = await this.control("coordinator", false);
|
|
144
181
|
check(result.status !== "blocked", "reconciliation_required", result.reason);
|
|
145
182
|
}
|
|
@@ -185,6 +222,7 @@ export class RoutingRuntime {
|
|
|
185
222
|
return {
|
|
186
223
|
effective: this.supported() && state.control !== "inactive" && !blocked,
|
|
187
224
|
activeProfile: state.profile,
|
|
225
|
+
delegation: this.capability?.delegation ?? "executor",
|
|
188
226
|
controlMode: state.control === "manual" ? `manual-${state.profile}` : state.control,
|
|
189
227
|
runtimeStatus: blocked
|
|
190
228
|
? ("blocked" as const)
|
|
@@ -231,11 +269,36 @@ export class RoutingRuntime {
|
|
|
231
269
|
if (profile) check(state.profile === profile && this.turn?.profile === profile, "wrong_profile");
|
|
232
270
|
if (state.profile) check(samePair(this.observed(), this.profilePair(state.profile)), "configuration_mismatch");
|
|
233
271
|
}
|
|
234
|
-
private
|
|
272
|
+
private configuredProfilePair(profile: Profile): Pair {
|
|
235
273
|
const configured = this.capability?.profiles[profile];
|
|
236
274
|
check(configured, "profile_missing");
|
|
237
275
|
return pairFromProfile(configured);
|
|
238
276
|
}
|
|
277
|
+
private profilePair(profile: Profile): Pair {
|
|
278
|
+
return this.stateData().profileOverrides.get(profile) ?? this.configuredProfilePair(profile);
|
|
279
|
+
}
|
|
280
|
+
private profilePairs(profiles = this.requiredProfiles()): Partial<Record<Profile, Pair>> {
|
|
281
|
+
return Object.fromEntries(profiles.map((profile) => [profile, this.profilePair(profile)]));
|
|
282
|
+
}
|
|
283
|
+
private async validateProfilePairs(pairs: Partial<Record<Profile, Pair>>): Promise<void> {
|
|
284
|
+
check(pairs.coordinator, "profile_missing", "Configure the coordinator profile.");
|
|
285
|
+
const helper = pairs.helper !== undefined;
|
|
286
|
+
const executor = pairs.executor !== undefined;
|
|
287
|
+
check(helper || executor, "profile_missing", "Configure an enabled worker profile.");
|
|
288
|
+
const delegation = helper && executor ? "both" : helper ? "helper" : "executor";
|
|
289
|
+
const profiles = Object.fromEntries(
|
|
290
|
+
Object.entries(pairs).map(([profile, pair]) => [
|
|
291
|
+
profile,
|
|
292
|
+
{ provider: pair!.provider, model: pair!.modelId, thinking: pair!.thinking },
|
|
293
|
+
]),
|
|
294
|
+
);
|
|
295
|
+
const result = await resolveCognitiveRoutingState(
|
|
296
|
+
{ cognitiveRouting: { enabled: true, delegation, profiles } },
|
|
297
|
+
{},
|
|
298
|
+
this.ctx,
|
|
299
|
+
);
|
|
300
|
+
check(result.effective, result.blockingReason.code, result.blockingReason.message);
|
|
301
|
+
}
|
|
239
302
|
private model(profile: Profile) {
|
|
240
303
|
const pair = this.profilePair(profile);
|
|
241
304
|
const model = this.ctx?.modelRegistry?.find(pair.provider, pair.modelId);
|
|
@@ -247,6 +310,8 @@ export class RoutingRuntime {
|
|
|
247
310
|
this.ctx = ctx;
|
|
248
311
|
this.capability = capability;
|
|
249
312
|
this.turn = undefined;
|
|
313
|
+
this.manualHold = undefined;
|
|
314
|
+
this.automaticControl = false;
|
|
250
315
|
this.messages = [];
|
|
251
316
|
this.sourceCache = undefined;
|
|
252
317
|
this.receipts.clear();
|
|
@@ -256,7 +321,7 @@ export class RoutingRuntime {
|
|
|
256
321
|
this.error = undefined;
|
|
257
322
|
this.projectionError = undefined;
|
|
258
323
|
this.operations = Promise.resolve();
|
|
259
|
-
this.targetSignature = canonical(capability.profiles);
|
|
324
|
+
this.targetSignature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
|
|
260
325
|
this.suppressed = startup && process.argv.some((a) => /^(--model|--thinking)(=|$)/.test(a));
|
|
261
326
|
this.store = new EventStore(this.pi, ctx.sessionManager as SessionReader);
|
|
262
327
|
const subject = this.subject();
|
|
@@ -267,6 +332,7 @@ export class RoutingRuntime {
|
|
|
267
332
|
const state = this.stateData();
|
|
268
333
|
this.manualHold = state.control === "manual" ? state.profile : undefined;
|
|
269
334
|
this.automaticControl = !state.events.size || state.control === "automatic";
|
|
335
|
+
await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
|
|
270
336
|
this.retireUnbound("Session rebind found an unfinished execution; no task effects replayed.");
|
|
271
337
|
if (!state.events.size) {
|
|
272
338
|
this.append({
|
|
@@ -307,7 +373,7 @@ export class RoutingRuntime {
|
|
|
307
373
|
async refresh(ctx: any, capability: CognitiveRoutingCapabilityState): Promise<void> {
|
|
308
374
|
this.ctx = ctx;
|
|
309
375
|
const was = this.capability?.effective === true;
|
|
310
|
-
const signature = canonical(capability.profiles);
|
|
376
|
+
const signature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
|
|
311
377
|
if (this.capability && (was !== capability.effective || signature !== this.targetSignature)) this.revision++;
|
|
312
378
|
this.capability = capability;
|
|
313
379
|
if (!capability.effective) {
|
|
@@ -339,7 +405,8 @@ export class RoutingRuntime {
|
|
|
339
405
|
} else if (this.targetSignature && signature !== this.targetSignature) {
|
|
340
406
|
this.error = "configuration_changed: reconcile profile configuration before automatic execution";
|
|
341
407
|
}
|
|
342
|
-
if (canonical(this.capability?.profiles) === signature)
|
|
408
|
+
if (canonical({ delegation: this.capability?.delegation, profiles: this.capability?.profiles }) === signature)
|
|
409
|
+
this.targetSignature = signature;
|
|
343
410
|
}
|
|
344
411
|
async ancestryChanged(ctx: any, navigation = true): Promise<void> {
|
|
345
412
|
this.revision++;
|
|
@@ -355,8 +422,9 @@ export class RoutingRuntime {
|
|
|
355
422
|
await this.store.reconcile();
|
|
356
423
|
this.guard(subject);
|
|
357
424
|
this.error = undefined;
|
|
358
|
-
this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
|
|
359
425
|
const state = this.stateData();
|
|
426
|
+
await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
|
|
427
|
+
this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
|
|
360
428
|
if (this.manualHold) {
|
|
361
429
|
this.append({
|
|
362
430
|
type: "control",
|
|
@@ -364,6 +432,7 @@ export class RoutingRuntime {
|
|
|
364
432
|
profile: this.manualHold,
|
|
365
433
|
reason: "Current explicit manual hold survives navigation",
|
|
366
434
|
});
|
|
435
|
+
await this.applyPair(this.profilePair(this.manualHold));
|
|
367
436
|
} else if (
|
|
368
437
|
(this.automaticControl || !state.events.size) &&
|
|
369
438
|
(navigation || !samePair(this.observed(), this.profilePair(state.profile ?? "coordinator")))
|
|
@@ -445,6 +514,12 @@ export class RoutingRuntime {
|
|
|
445
514
|
return this.enqueue(async () => {
|
|
446
515
|
try {
|
|
447
516
|
check(this.capability?.effective && this.store, "routing_unavailable");
|
|
517
|
+
if (manual && isWorkerProfile(profile))
|
|
518
|
+
check(
|
|
519
|
+
this.enabledWorkers().includes(profile),
|
|
520
|
+
"worker_disabled",
|
|
521
|
+
`${profile} is not enabled by delegation mode.`,
|
|
522
|
+
);
|
|
448
523
|
await this.store.reconcile();
|
|
449
524
|
this.guard(subject);
|
|
450
525
|
this.error = undefined;
|
|
@@ -482,7 +557,8 @@ export class RoutingRuntime {
|
|
|
482
557
|
this.automaticControl = !manual;
|
|
483
558
|
return { status: manual ? "active" : "automatic" };
|
|
484
559
|
} catch (error) {
|
|
485
|
-
if (this.current(subject)
|
|
560
|
+
if (this.current(subject) && !(error instanceof RoutingError && error.code === "worker_disabled"))
|
|
561
|
+
this.mark(error);
|
|
486
562
|
return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
|
|
487
563
|
}
|
|
488
564
|
});
|
|
@@ -490,6 +566,124 @@ export class RoutingRuntime {
|
|
|
490
566
|
setManualProfile(profile: Profile, _mechanism?: string) {
|
|
491
567
|
return this.control(profile, true);
|
|
492
568
|
}
|
|
569
|
+
sessionProfileOverrides(): Partial<Record<Profile, Pair>> {
|
|
570
|
+
return Object.fromEntries(this.stateData().profileOverrides) as Partial<Record<Profile, Pair>>;
|
|
571
|
+
}
|
|
572
|
+
async setSessionProfileOverride(
|
|
573
|
+
profile: Profile,
|
|
574
|
+
override: Pair | null,
|
|
575
|
+
mechanism = "Session profile override",
|
|
576
|
+
): Promise<{ status: string; reason?: string }> {
|
|
577
|
+
this.revision++;
|
|
578
|
+
const subject = this.subject();
|
|
579
|
+
return this.enqueue(async () => {
|
|
580
|
+
try {
|
|
581
|
+
check(this.capability?.effective && this.store, "routing_unavailable");
|
|
582
|
+
check(
|
|
583
|
+
this.ctx?.isIdle?.() !== false,
|
|
584
|
+
"host_busy",
|
|
585
|
+
"Wait for Pi to become idle before changing session presets.",
|
|
586
|
+
);
|
|
587
|
+
await this.store.reconcile();
|
|
588
|
+
this.guard(subject);
|
|
589
|
+
const state = this.stateData();
|
|
590
|
+
const previous = state.profileOverrides.get(profile);
|
|
591
|
+
if ((override === null && !previous) || (override !== null && samePair(previous, override)))
|
|
592
|
+
return { status: "unchanged" };
|
|
593
|
+
const target = override ?? this.configuredProfilePair(profile);
|
|
594
|
+
const profiles = new Set(this.requiredProfiles(state));
|
|
595
|
+
profiles.add(profile);
|
|
596
|
+
const pairs = this.profilePairs([...profiles]);
|
|
597
|
+
pairs[profile] = target;
|
|
598
|
+
await this.validateProfilePairs(pairs);
|
|
599
|
+
this.append({
|
|
600
|
+
type: "profile-overrides",
|
|
601
|
+
overrides: { [profile]: override },
|
|
602
|
+
reason: mechanism,
|
|
603
|
+
});
|
|
604
|
+
if (state.control !== "inactive" && state.profile === profile) {
|
|
605
|
+
try {
|
|
606
|
+
await this.applyPair(target);
|
|
607
|
+
this.guard(subject);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
|
|
610
|
+
this.append({
|
|
611
|
+
type: "profile-overrides",
|
|
612
|
+
overrides: { [profile]: previous ?? null },
|
|
613
|
+
reason: "Session profile application failed; prior override retained",
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
this.error = undefined;
|
|
619
|
+
return { status: "active" };
|
|
620
|
+
}
|
|
621
|
+
this.error = undefined;
|
|
622
|
+
return { status: "stored" };
|
|
623
|
+
} catch (error) {
|
|
624
|
+
if (this.current(subject)) this.mark(error);
|
|
625
|
+
return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
async resetSessionProfileOverrides(
|
|
630
|
+
mechanism = "Reset session profile overrides",
|
|
631
|
+
): Promise<{ status: string; reason?: string }> {
|
|
632
|
+
this.revision++;
|
|
633
|
+
const subject = this.subject();
|
|
634
|
+
return this.enqueue(async () => {
|
|
635
|
+
try {
|
|
636
|
+
check(this.capability?.effective && this.store, "routing_unavailable");
|
|
637
|
+
check(
|
|
638
|
+
this.ctx?.isIdle?.() !== false,
|
|
639
|
+
"host_busy",
|
|
640
|
+
"Wait for Pi to become idle before resetting session presets.",
|
|
641
|
+
);
|
|
642
|
+
await this.store.reconcile();
|
|
643
|
+
this.guard(subject);
|
|
644
|
+
const state = this.stateData();
|
|
645
|
+
if (state.profileOverrides.size === 0) return { status: "unchanged" };
|
|
646
|
+
const previous = Object.fromEntries(state.profileOverrides) as Partial<Record<Profile, Pair>>;
|
|
647
|
+
const required = this.requiredProfiles(state);
|
|
648
|
+
const targetPairs = Object.fromEntries(
|
|
649
|
+
required.map((profile) => [profile, this.configuredProfilePair(profile)]),
|
|
650
|
+
) as Partial<Record<Profile, Pair>>;
|
|
651
|
+
await this.validateProfilePairs(targetPairs);
|
|
652
|
+
this.append({
|
|
653
|
+
type: "profile-overrides",
|
|
654
|
+
overrides: { coordinator: null, helper: null, executor: null },
|
|
655
|
+
reason: mechanism,
|
|
656
|
+
});
|
|
657
|
+
const active = state.control !== "inactive" ? state.profile : undefined;
|
|
658
|
+
if (active && previous[active]) {
|
|
659
|
+
try {
|
|
660
|
+
await this.applyPair(targetPairs[active]!);
|
|
661
|
+
this.guard(subject);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
|
|
664
|
+
this.append({
|
|
665
|
+
type: "profile-overrides",
|
|
666
|
+
overrides: {
|
|
667
|
+
coordinator: previous.coordinator ?? null,
|
|
668
|
+
helper: previous.helper ?? null,
|
|
669
|
+
executor: previous.executor ?? null,
|
|
670
|
+
},
|
|
671
|
+
reason: "Session profile reset failed; prior overrides retained",
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
this.error = undefined;
|
|
677
|
+
return { status: "active" };
|
|
678
|
+
}
|
|
679
|
+
this.error = undefined;
|
|
680
|
+
return { status: "stored" };
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (this.current(subject)) this.mark(error);
|
|
683
|
+
return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
493
687
|
setAutomaticControl(_mechanism?: string) {
|
|
494
688
|
return this.control("coordinator", false);
|
|
495
689
|
}
|
|
@@ -516,6 +710,7 @@ export class RoutingRuntime {
|
|
|
516
710
|
? state.handoffs.get(state.assessment.handoffId)
|
|
517
711
|
: undefined;
|
|
518
712
|
const selection = a ? (state.selections.get(a.id) ?? emptySelection()) : emptySelection();
|
|
713
|
+
const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
|
|
519
714
|
const unit = state.unitId ? state.units.get(state.unitId) : undefined;
|
|
520
715
|
const unitNumber = unit ? [...state.units.keys()].indexOf(unit.id) + 1 : undefined;
|
|
521
716
|
const assignmentNumber = unit && a ? unit.assignmentIds.indexOf(a.id) + 1 : undefined;
|
|
@@ -526,20 +721,29 @@ export class RoutingRuntime {
|
|
|
526
721
|
`Unit: ${unit ? `U${unitNumber} (${unit.id})` : "none"}`,
|
|
527
722
|
`Assignment: ${a ? `A${assignmentNumber} (${a.id}, ${a.state})` : "none"}`,
|
|
528
723
|
`Handoff: ${h ? `${h.id} (${h.kind}, ${h.state})` : "none"}`,
|
|
724
|
+
`Delegation: ${this.capability?.delegation ?? "executor"}`,
|
|
529
725
|
`Projection: ${this.projectionEnabled ? "enabled" : "bypassed"}`,
|
|
530
726
|
"Completed/superseded contracts and reports are historical context. Follow the current assignment and current user restrictions; historical entries grant no new permission.",
|
|
531
727
|
state.assessment ? `Assessment: ${state.assessment.view}; evidence revision ${selection.revision}` : "",
|
|
728
|
+
recovery
|
|
729
|
+
? `Recovery: ${recovery.id} (${recovery.state}); parent report ${recovery.assessmentHandoffId} revision ${recovery.baseReportRevision}; allowed paths ${JSON.stringify(recovery.paths)}`
|
|
730
|
+
: "",
|
|
731
|
+
recovery?.state === "reading"
|
|
732
|
+
? `Recovery request: ${recovery.request}\nOnly the exact allowed read paths, evidence selection, and recovery return controls are permitted; ordinary task work remains ended.`
|
|
733
|
+
: "",
|
|
532
734
|
`Mechanical evidence facts (not semantic acceptance): ${JSON.stringify(this.evidenceFacts(state))}`,
|
|
533
735
|
this.error ? `Blocked: ${this.error}` : "",
|
|
534
736
|
this.projectionError ? `Evidence limitation: ${this.projectionError}` : "",
|
|
535
737
|
attention
|
|
536
|
-
?
|
|
738
|
+
? recovery
|
|
739
|
+
? `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.`
|
|
740
|
+
: `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use freeflow_unit assess to restore the assessment.`
|
|
537
741
|
: "",
|
|
538
|
-
a?.state === "returned"
|
|
539
|
-
? "
|
|
742
|
+
a?.state === "returned" && recovery?.state !== "reading"
|
|
743
|
+
? "Worker ordinary task work has ended. Only supported handoff correction is permitted."
|
|
540
744
|
: "",
|
|
541
|
-
this.turn?.profile
|
|
542
|
-
?
|
|
745
|
+
isWorkerProfile(this.turn?.profile) && a && this.turn.basisUserEntryId !== this.workerBasis(state, a.id)
|
|
746
|
+
? `Current input differs from the active ${this.turn.profile} responsibility. Account for it; changed direction requires Coordinator attention.`
|
|
543
747
|
: "",
|
|
544
748
|
...(state.assessment?.problems ?? []).map((p) => `${p.code}: ${p.detail}`),
|
|
545
749
|
]
|
|
@@ -558,16 +762,19 @@ export class RoutingRuntime {
|
|
|
558
762
|
const selection = state.assignmentId
|
|
559
763
|
? (state.selections.get(state.assignmentId) ?? emptySelection())
|
|
560
764
|
: emptySelection();
|
|
765
|
+
const sources = this.sources(state);
|
|
561
766
|
return {
|
|
562
767
|
revision: selection.revision,
|
|
563
768
|
selected: selection.selected.map((ref) => {
|
|
564
769
|
const entry = this.ctx?.sessionManager.getEntry?.(ref.slice(4).replace(/#text$/, ""));
|
|
770
|
+
const locator = sources.locator(ref);
|
|
565
771
|
return {
|
|
566
772
|
ref,
|
|
567
773
|
kind: ref.endsWith("#text") ? "assistant-text" : (entry?.message?.role ?? "unknown"),
|
|
568
774
|
toolName: entry?.message?.toolName,
|
|
569
775
|
producer: state.authors.get(entry?.id)?.profile ?? "common",
|
|
570
776
|
assignment: state.authors.get(entry?.id)?.assignmentId,
|
|
777
|
+
...(locator ? { locator } : {}),
|
|
571
778
|
};
|
|
572
779
|
}),
|
|
573
780
|
unresolved: selection.unresolved,
|
|
@@ -589,18 +796,9 @@ export class RoutingRuntime {
|
|
|
589
796
|
pair: this.profilePair(profile),
|
|
590
797
|
systemPrompt: this.ctx.getSystemPrompt?.() ?? "",
|
|
591
798
|
tools: (this.pi.getAllTools?.() ?? [])
|
|
592
|
-
.filter((tool: any) =>
|
|
593
|
-
ROUTING_TOOLS.includes(tool.name)
|
|
594
|
-
? tool.name === "freeflow_unit" ||
|
|
595
|
-
(profile === "coordinator"
|
|
596
|
-
? tool.name === "freeflow_delegate"
|
|
597
|
-
: tool.name === "freeflow_return" || (tool.name === "freeflow_project" && this.projectionEnabled))
|
|
598
|
-
: this.pi.getActiveTools?.().includes(tool.name),
|
|
599
|
-
)
|
|
799
|
+
.filter((tool: any) => this.pi.getActiveTools?.().includes(tool.name))
|
|
600
800
|
.map((tool: any) =>
|
|
601
|
-
ROUTING_TOOLS.includes(tool.name)
|
|
602
|
-
? { ...tool, parameters: schemaForProfile(tool.name, profile === "coordinator") }
|
|
603
|
-
: tool,
|
|
801
|
+
ROUTING_TOOLS.includes(tool.name) ? { ...tool, parameters: ROUTING_SCHEMAS[tool.name] } : tool,
|
|
604
802
|
),
|
|
605
803
|
runtimeMessage: this.runtimeMessage(
|
|
606
804
|
state,
|
|
@@ -611,6 +809,35 @@ export class RoutingRuntime {
|
|
|
611
809
|
restoring,
|
|
612
810
|
});
|
|
613
811
|
}
|
|
812
|
+
budgetNotice(messages: any[], ctx: any): any | undefined {
|
|
813
|
+
if (!this.supported()) return;
|
|
814
|
+
const tools = (this.pi.getAllTools?.() ?? []).filter((tool: any) => this.pi.getActiveTools?.().includes(tool.name));
|
|
815
|
+
const estimate = estimateRequest(ctx.getSystemPrompt?.() ?? "", tools, messages, ctx.model);
|
|
816
|
+
if (!estimate.warnings.length) return;
|
|
817
|
+
return {
|
|
818
|
+
role: "custom",
|
|
819
|
+
customType: "freeflow-routing-budget",
|
|
820
|
+
display: false,
|
|
821
|
+
content: estimate.warnings.map((warning) => warning.detail).join("\n"),
|
|
822
|
+
timestamp: 0,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
private ordinaryContext(input: any[]): any[] {
|
|
826
|
+
try {
|
|
827
|
+
const sources = this.sources(),
|
|
828
|
+
associated = sources.associate(input);
|
|
829
|
+
const mapped = new Map(associated.filter((item) => item.source).map((item) => [item.message, item.source!]));
|
|
830
|
+
return annotateSources(
|
|
831
|
+
input,
|
|
832
|
+
mapped,
|
|
833
|
+
new Set([...mapped.values()].map((source) => source.ref)),
|
|
834
|
+
sources,
|
|
835
|
+
this.token,
|
|
836
|
+
);
|
|
837
|
+
} catch {
|
|
838
|
+
return input;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
614
841
|
async context(ctx: any, incoming: any[]): Promise<any[]> {
|
|
615
842
|
this.ctx = ctx;
|
|
616
843
|
const input = incoming.filter(
|
|
@@ -621,10 +848,10 @@ export class RoutingRuntime {
|
|
|
621
848
|
),
|
|
622
849
|
);
|
|
623
850
|
this.messages = input;
|
|
624
|
-
if (!this.supported() || !this.store) return input;
|
|
851
|
+
if (!this.supported() || !this.store) return this.ordinaryContext(input);
|
|
625
852
|
try {
|
|
626
853
|
const state = this.stateData();
|
|
627
|
-
if (state.control !== "automatic") return input;
|
|
854
|
+
if (state.control !== "automatic") return this.ordinaryContext(input);
|
|
628
855
|
check(!this.error && !this.store.blocked && state.profile, "routing_blocked", this.error ?? this.store.blocked);
|
|
629
856
|
check(samePair(this.observed(), this.profilePair(state.profile!)), "prepared_pair_mismatch");
|
|
630
857
|
const sources = this.sources(state),
|
|
@@ -640,16 +867,19 @@ export class RoutingRuntime {
|
|
|
640
867
|
},
|
|
641
868
|
JSON.stringify(["attention", state.assessment.handoffId, user]),
|
|
642
869
|
);
|
|
643
|
-
// New input reaching an already prepared
|
|
870
|
+
// New input reaching an already prepared worker request cannot be rerouted by changing only the live model.
|
|
644
871
|
const outstanding = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
872
|
+
const worker = outstanding ? this.assignedWorker(state, outstanding.id) : undefined;
|
|
645
873
|
const interrupted =
|
|
646
|
-
state.profile
|
|
874
|
+
isWorkerProfile(state.profile) &&
|
|
875
|
+
(state.profile !== worker || user !== (outstanding ? this.workerBasis(state, outstanding.id) : undefined));
|
|
647
876
|
if (!this.turn || this.turn.bound) {
|
|
648
877
|
this.receipts.clear();
|
|
649
878
|
const execution: Execution = {
|
|
650
879
|
id: randomUUID(),
|
|
651
880
|
profile: state.profile!,
|
|
652
881
|
assignmentId: state.assignmentId,
|
|
882
|
+
recoveryId: state.recoveryId,
|
|
653
883
|
basisUserEntryId: user,
|
|
654
884
|
pair: this.observed()!,
|
|
655
885
|
resultEntryIds: [],
|
|
@@ -659,6 +889,7 @@ export class RoutingRuntime {
|
|
|
659
889
|
id: execution.id,
|
|
660
890
|
profile: execution.profile,
|
|
661
891
|
assignmentId: execution.assignmentId,
|
|
892
|
+
recoveryId: execution.recoveryId,
|
|
662
893
|
basisUserEntryId: user,
|
|
663
894
|
before,
|
|
664
895
|
pair: execution.pair,
|
|
@@ -681,12 +912,12 @@ export class RoutingRuntime {
|
|
|
681
912
|
check(prepared.ready, "context_unavailable", prepared.problems.map((p) => p.detail).join("; "));
|
|
682
913
|
const known = this.stateData().exposure;
|
|
683
914
|
const added = prepared.fullSources.filter((s) => known.get(s.ref) !== s.hash);
|
|
684
|
-
if (this.turn.profile
|
|
915
|
+
if (isWorkerProfile(this.turn.profile))
|
|
685
916
|
for (let i = 0; i < added.length; i += 128)
|
|
686
917
|
this.append({
|
|
687
918
|
type: "sources-exposed",
|
|
688
919
|
executionId: this.turn.id,
|
|
689
|
-
view:
|
|
920
|
+
view: this.turn.profile,
|
|
690
921
|
sources: added.slice(i, i + 128).map((s) => ({ ref: s.ref, bodyHash: s.hash })),
|
|
691
922
|
});
|
|
692
923
|
if (interrupted)
|
|
@@ -703,7 +934,7 @@ export class RoutingRuntime {
|
|
|
703
934
|
} catch (error) {
|
|
704
935
|
this.mark(error);
|
|
705
936
|
ctx.abort?.();
|
|
706
|
-
// Do not send an accidental full
|
|
937
|
+
// Do not send an accidental full worker history on a projection failure.
|
|
707
938
|
return [
|
|
708
939
|
{
|
|
709
940
|
role: "custom",
|
|
@@ -711,7 +942,7 @@ export class RoutingRuntime {
|
|
|
711
942
|
content: `Automatic request blocked: ${this.error}`,
|
|
712
943
|
display: false,
|
|
713
944
|
timestamp: 0,
|
|
714
|
-
details: { routingInstance: this.token },
|
|
945
|
+
details: { routingInstance: this.token, routingRequestBlocked: true },
|
|
715
946
|
},
|
|
716
947
|
];
|
|
717
948
|
}
|
|
@@ -722,6 +953,57 @@ export class RoutingRuntime {
|
|
|
722
953
|
private contextOperation(name: string, input: any): boolean {
|
|
723
954
|
return name === "freeflow_context" && ["archive", "restore", "search", "retrieve"].includes(input?.operation);
|
|
724
955
|
}
|
|
956
|
+
private handoffOperation(name: string, input: any): boolean {
|
|
957
|
+
if (name === "freeflow_delegate") return true;
|
|
958
|
+
if (name === "freeflow_return") return ["submit", "supplement", "retry"].includes(input?.operation);
|
|
959
|
+
return name === "freeflow_unit" && input?.operation === "recover";
|
|
960
|
+
}
|
|
961
|
+
private stableRecoveryPath(path: string): boolean {
|
|
962
|
+
return (
|
|
963
|
+
!path.startsWith("@") &&
|
|
964
|
+
path !== "~" &&
|
|
965
|
+
!path.startsWith("~/") &&
|
|
966
|
+
!path.startsWith("file://") &&
|
|
967
|
+
!/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(path) &&
|
|
968
|
+
!(process.platform === "win32" && /^\/(?:mnt\/|cygdrive\/)?[a-z](?:\/|$)/i.test(path))
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
private canonicalRecoveryPath(path: string): string {
|
|
972
|
+
check(this.stableRecoveryPath(path), "recovery_path_unsupported", `Recovery path spelling is unsupported: ${path}`);
|
|
973
|
+
const requested = resolvePath(this.ctx.cwd, path);
|
|
974
|
+
check(existsSync(requested), "recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
|
|
975
|
+
try {
|
|
976
|
+
return realpathSync(requested);
|
|
977
|
+
} catch {
|
|
978
|
+
throw new RoutingError("recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
private packagedInstruction(path: string): boolean {
|
|
982
|
+
return this.packageRoots.some((root) => {
|
|
983
|
+
const inside = relative(root, path);
|
|
984
|
+
if (!inside || inside.startsWith("..") || resolvePath(root, inside) !== path) return false;
|
|
985
|
+
return (
|
|
986
|
+
/^(?:skills|capabilities)\/[^/]+\/SKILL\.md$/.test(inside) ||
|
|
987
|
+
/^(?:skills|capabilities)\/[^/]+\/references\/[^/]+\.md$/.test(inside)
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
private recoveryReadAllowed(state: State, name: string, input: any): boolean {
|
|
992
|
+
if (name !== "read" || typeof input?.path !== "string" || !state.recoveryId) return false;
|
|
993
|
+
const recovery = state.recoveries.get(state.recoveryId);
|
|
994
|
+
if (!recovery || recovery.state !== "reading" || !this.stableRecoveryPath(input.path)) return false;
|
|
995
|
+
try {
|
|
996
|
+
const path = this.canonicalRecoveryPath(input.path);
|
|
997
|
+
return recovery.paths.includes(path) || this.packagedInstruction(path);
|
|
998
|
+
} catch {
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
private workerBasis(state: State, assignmentId: string): string | null {
|
|
1003
|
+
const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
|
|
1004
|
+
if (recovery?.state === "reading") return state.handoffs.get(recovery.requestHandoffId)?.basisUserEntryId ?? null;
|
|
1005
|
+
return this.taskBasis(state, assignmentId);
|
|
1006
|
+
}
|
|
725
1007
|
private batch(callId: string, name: string): void {
|
|
726
1008
|
check(this.turn?.message, "batch_unavailable");
|
|
727
1009
|
const matches = (this.ctx.sessionManager.getBranch() as NativeEntry[]).filter(
|
|
@@ -739,14 +1021,16 @@ export class RoutingRuntime {
|
|
|
739
1021
|
this.turn.sourceFingerprint = fingerprint;
|
|
740
1022
|
this.turn.message = structuredClone(matches[0].message);
|
|
741
1023
|
const calls = (this.turn.message.content ?? []).filter((b: any) => b.type === "toolCall");
|
|
742
|
-
const handoffs = calls.filter((b: any) =>
|
|
1024
|
+
const handoffs = calls.filter((b: any) => this.handoffOperation(b.name, b.arguments));
|
|
743
1025
|
check(
|
|
744
1026
|
!handoffs.length ||
|
|
745
1027
|
(handoffs.length === 1 &&
|
|
746
|
-
|
|
1028
|
+
this.handoffOperation(calls.at(-1)?.name, calls.at(-1)?.arguments) &&
|
|
747
1029
|
calls.every(
|
|
748
1030
|
(b: any) =>
|
|
749
|
-
|
|
1031
|
+
this.handoffOperation(b.name, b.arguments) ||
|
|
1032
|
+
b.name === "freeflow_project" ||
|
|
1033
|
+
this.contextOperation(b.name, b.arguments),
|
|
750
1034
|
)),
|
|
751
1035
|
"invalid_handoff_batch",
|
|
752
1036
|
"A handoff must be last and cannot accompany ordinary task tools.",
|
|
@@ -770,14 +1054,21 @@ export class RoutingRuntime {
|
|
|
770
1054
|
this.batch(event.toolCallId, name);
|
|
771
1055
|
check(!this.error && !this.store?.blocked, "routing_blocked");
|
|
772
1056
|
this.openTurn();
|
|
773
|
-
if (this.turn?.profile
|
|
1057
|
+
if (isWorkerProfile(this.turn?.profile) && !isRouting && !this.contextOperation(name, event.input)) {
|
|
774
1058
|
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
775
|
-
|
|
1059
|
+
const worker = a ? this.assignedWorker(state, a.id) : undefined;
|
|
1060
|
+
const ordinary =
|
|
776
1061
|
a?.state === "outstanding" &&
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1062
|
+
this.turn.basisUserEntryId === this.taskBasis(state, a.id) &&
|
|
1063
|
+
state.profile === worker &&
|
|
1064
|
+
this.turn.profile === worker;
|
|
1065
|
+
const recovery =
|
|
1066
|
+
a?.state === "returned" &&
|
|
1067
|
+
this.turn.basisUserEntryId === this.workerBasis(state, a.id) &&
|
|
1068
|
+
state.profile === worker &&
|
|
1069
|
+
this.turn.profile === worker &&
|
|
1070
|
+
this.recoveryReadAllowed(state, name, event.input);
|
|
1071
|
+
check(ordinary || recovery, "worker_task_phase_ended");
|
|
781
1072
|
}
|
|
782
1073
|
return undefined;
|
|
783
1074
|
} catch (error) {
|
|
@@ -797,8 +1088,14 @@ export class RoutingRuntime {
|
|
|
797
1088
|
this.ctx = ctx;
|
|
798
1089
|
try {
|
|
799
1090
|
check(!signal?.aborted, "cancelled");
|
|
800
|
-
if (name === "freeflow_unit" && input.operation === "inspect")
|
|
801
|
-
|
|
1091
|
+
if (name === "freeflow_unit" && input.operation === "inspect") {
|
|
1092
|
+
check(this.supported(), "routing_unavailable");
|
|
1093
|
+
return this.result(this.inspectUnit(input));
|
|
1094
|
+
}
|
|
1095
|
+
const state = this.stateData();
|
|
1096
|
+
const expected =
|
|
1097
|
+
name === "freeflow_delegate" || name === "freeflow_unit" ? "coordinator" : this.assignedWorker(state);
|
|
1098
|
+
this.assertAvailable(expected);
|
|
802
1099
|
const op = this.callOperation(callId, name);
|
|
803
1100
|
const cached = this.receipts.get(op);
|
|
804
1101
|
if (cached) {
|
|
@@ -813,7 +1110,7 @@ export class RoutingRuntime {
|
|
|
813
1110
|
: name === "freeflow_project"
|
|
814
1111
|
? this.project(input, op)
|
|
815
1112
|
: name === "freeflow_unit"
|
|
816
|
-
? this.unit(input, op)
|
|
1113
|
+
? this.unit(input, callId, op)
|
|
817
1114
|
: undefined;
|
|
818
1115
|
if (value !== undefined) {
|
|
819
1116
|
this.receipts.set(op, { input: canonical(input), value: structuredClone(value) });
|
|
@@ -832,7 +1129,13 @@ export class RoutingRuntime {
|
|
|
832
1129
|
? undefined
|
|
833
1130
|
: name === "freeflow_delegate" || name === "freeflow_unit"
|
|
834
1131
|
? "coordinator"
|
|
835
|
-
:
|
|
1132
|
+
: (() => {
|
|
1133
|
+
try {
|
|
1134
|
+
return this.assignedWorker(this.stateData());
|
|
1135
|
+
} catch {
|
|
1136
|
+
return undefined;
|
|
1137
|
+
}
|
|
1138
|
+
})(),
|
|
836
1139
|
recoveryAction:
|
|
837
1140
|
error instanceof RoutingError &&
|
|
838
1141
|
[
|
|
@@ -853,26 +1156,48 @@ export class RoutingRuntime {
|
|
|
853
1156
|
return [...this.stateData().events.values()].find(
|
|
854
1157
|
(e) =>
|
|
855
1158
|
e.operationId === op &&
|
|
856
|
-
[
|
|
1159
|
+
[
|
|
1160
|
+
"delegate-accepted",
|
|
1161
|
+
"return-accepted",
|
|
1162
|
+
"recovery-request-accepted",
|
|
1163
|
+
"recovery-supplement-accepted",
|
|
1164
|
+
"recovery-cancelled",
|
|
1165
|
+
"handoff-retry-requested",
|
|
1166
|
+
].includes(e.data.type),
|
|
857
1167
|
);
|
|
858
1168
|
}
|
|
859
1169
|
private delegate(input: any, callId: string, op: string) {
|
|
860
1170
|
const prior = this.duplicate(op);
|
|
861
1171
|
if (prior) {
|
|
862
1172
|
check(
|
|
863
|
-
prior.data.type === "delegate-accepted" &&
|
|
1173
|
+
prior.data.type === "delegate-accepted" &&
|
|
1174
|
+
prior.data.assignment.contract === input.contract &&
|
|
1175
|
+
(input.worker === undefined || prior.data.handoff.to === input.worker),
|
|
864
1176
|
"operation_conflict",
|
|
865
1177
|
);
|
|
866
1178
|
return { status: "accepted", handoff: prior.data.handoff.id, unchanged: true };
|
|
867
1179
|
}
|
|
868
1180
|
const state = this.stateData();
|
|
1181
|
+
const enabled = this.enabledWorkers();
|
|
1182
|
+
let worker: WorkerProfile;
|
|
1183
|
+
if (input.worker === undefined) {
|
|
1184
|
+
check(enabled.length === 1, "worker_required", "Delegation mode 'both' requires choosing helper or executor.");
|
|
1185
|
+
worker = enabled[0]!;
|
|
1186
|
+
} else {
|
|
1187
|
+
check(isWorkerProfile(input.worker), "invalid_worker");
|
|
1188
|
+
worker = input.worker;
|
|
1189
|
+
}
|
|
1190
|
+
check(enabled.includes(worker), "worker_disabled", `${worker} is not enabled by delegation mode.`);
|
|
869
1191
|
const unit = state.unitId
|
|
870
1192
|
? state.units.get(state.unitId)!
|
|
871
1193
|
: { id: randomUUID(), objective: input.contract, state: "open" as const, assignmentIds: [] };
|
|
872
1194
|
const old = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
873
1195
|
if (input.operation === "replace") check(old?.state === "outstanding" && !this.applying, "no_quiescent_assignment");
|
|
874
1196
|
else
|
|
875
|
-
check(
|
|
1197
|
+
check(
|
|
1198
|
+
input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId && !state.recoveryId,
|
|
1199
|
+
"assignment_outstanding",
|
|
1200
|
+
);
|
|
876
1201
|
const assignmentId = randomUUID(),
|
|
877
1202
|
id = randomUUID();
|
|
878
1203
|
const assignment = {
|
|
@@ -889,7 +1214,7 @@ export class RoutingRuntime {
|
|
|
889
1214
|
assignmentId,
|
|
890
1215
|
text: input.contract,
|
|
891
1216
|
from: "coordinator",
|
|
892
|
-
to:
|
|
1217
|
+
to: worker,
|
|
893
1218
|
executionId: this.turn!.id,
|
|
894
1219
|
toolCallId: callId,
|
|
895
1220
|
basisUserEntryId: this.turn!.basisUserEntryId,
|
|
@@ -919,6 +1244,7 @@ export class RoutingRuntime {
|
|
|
919
1244
|
status: "accepted",
|
|
920
1245
|
unit: unit.id,
|
|
921
1246
|
assignment: assignmentId,
|
|
1247
|
+
worker,
|
|
922
1248
|
handoff: id,
|
|
923
1249
|
transition: "pending",
|
|
924
1250
|
contract: input.contract,
|
|
@@ -937,25 +1263,28 @@ export class RoutingRuntime {
|
|
|
937
1263
|
const state = this.stateData(),
|
|
938
1264
|
a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
939
1265
|
check(a, "assignment_missing");
|
|
940
|
-
const
|
|
1266
|
+
const worker = this.assignedWorker(state, a.id);
|
|
1267
|
+
const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
|
|
1268
|
+
const original = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1269
|
+
const supplement = recovery?.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
|
|
1270
|
+
const retryingSupplement = input.operation === "retry" && recovery?.state === "returning";
|
|
1271
|
+
const saved = retryingSupplement || input.operation === "supplement" ? supplement : original;
|
|
941
1272
|
const selection = state.selections.get(a.id) ?? emptySelection();
|
|
942
1273
|
const duplicate = this.duplicate(op);
|
|
943
1274
|
if (duplicate) {
|
|
944
1275
|
if (input.operation === "retry") check(duplicate.data.type === "handoff-retry-requested", "operation_conflict");
|
|
945
|
-
else
|
|
1276
|
+
else {
|
|
1277
|
+
const expected = input.operation === "supplement" ? "recovery-supplement-accepted" : "return-accepted";
|
|
946
1278
|
check(
|
|
947
|
-
duplicate.data.type ===
|
|
1279
|
+
duplicate.data.type === expected &&
|
|
948
1280
|
duplicate.data.handoff.text === input.report &&
|
|
949
1281
|
duplicate.data.handoff.outcome === input.outcome &&
|
|
950
1282
|
canonical(duplicate.data.handoff.limitations) === canonical(input.limitations ?? []),
|
|
951
1283
|
"operation_conflict",
|
|
952
1284
|
);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
unchanged: true,
|
|
957
|
-
handoff: duplicate.data.handoffId ?? duplicate.data.handoff.id,
|
|
958
|
-
};
|
|
1285
|
+
}
|
|
1286
|
+
const handoff = duplicate.data.handoffId ?? duplicate.data.handoff.id;
|
|
1287
|
+
return { ...this.returnReadiness(handoff), unchanged: true };
|
|
959
1288
|
}
|
|
960
1289
|
if (input.operation === "retry") {
|
|
961
1290
|
check(saved && ["blocked", "pending"].includes(saved.state), "saved_return_missing");
|
|
@@ -971,25 +1300,55 @@ export class RoutingRuntime {
|
|
|
971
1300
|
},
|
|
972
1301
|
op,
|
|
973
1302
|
);
|
|
974
|
-
return {
|
|
1303
|
+
return {
|
|
1304
|
+
...this.returnReadiness(saved.id),
|
|
1305
|
+
reportUnchanged: true,
|
|
1306
|
+
supplementUnchanged: saved.kind === "recovery-return",
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
if (input.operation === "supplement") {
|
|
1310
|
+
check(
|
|
1311
|
+
recovery &&
|
|
1312
|
+
["reading", "returning"].includes(recovery.state) &&
|
|
1313
|
+
(!supplement || ["pending", "blocked"].includes(supplement.state)),
|
|
1314
|
+
"recovery_not_returnable",
|
|
1315
|
+
);
|
|
1316
|
+
const h: Handoff = {
|
|
1317
|
+
id: supplement?.id ?? randomUUID(),
|
|
1318
|
+
kind: "recovery-return",
|
|
1319
|
+
assignmentId: a.id,
|
|
1320
|
+
text: input.report,
|
|
1321
|
+
from: worker,
|
|
1322
|
+
to: "coordinator",
|
|
1323
|
+
executionId: this.turn!.id,
|
|
1324
|
+
toolCallId: callId,
|
|
1325
|
+
basisUserEntryId: this.turn!.basisUserEntryId,
|
|
1326
|
+
state: "pending",
|
|
1327
|
+
reportRevision: (supplement?.reportRevision ?? 0) + 1,
|
|
1328
|
+
outcome: input.outcome,
|
|
1329
|
+
limitations: input.limitations ?? [],
|
|
1330
|
+
};
|
|
1331
|
+
this.append({ type: "recovery-supplement-accepted", recoveryId: recovery.id, handoff: h }, op);
|
|
1332
|
+
return this.returnReadiness(h.id);
|
|
975
1333
|
}
|
|
976
1334
|
check(input.operation === "submit", "invalid_return_operation");
|
|
977
1335
|
check(
|
|
978
|
-
a.state === "outstanding" ||
|
|
1336
|
+
a.state === "outstanding" ||
|
|
1337
|
+
(a.state === "returned" && original && ["pending", "blocked"].includes(original.state)),
|
|
979
1338
|
"assignment_task_ended",
|
|
980
1339
|
);
|
|
981
1340
|
const h: Handoff = {
|
|
982
|
-
id:
|
|
1341
|
+
id: original?.id ?? randomUUID(),
|
|
983
1342
|
kind: "return",
|
|
984
1343
|
assignmentId: a.id,
|
|
985
1344
|
text: input.report,
|
|
986
|
-
from:
|
|
1345
|
+
from: worker,
|
|
987
1346
|
to: "coordinator",
|
|
988
1347
|
executionId: this.turn!.id,
|
|
989
1348
|
toolCallId: callId,
|
|
990
1349
|
basisUserEntryId: this.turn!.basisUserEntryId,
|
|
991
1350
|
state: "pending",
|
|
992
|
-
reportRevision: (
|
|
1351
|
+
reportRevision: (original?.reportRevision ?? 0) + 1,
|
|
993
1352
|
outcome: input.outcome,
|
|
994
1353
|
limitations: input.limitations ?? [],
|
|
995
1354
|
};
|
|
@@ -997,16 +1356,25 @@ export class RoutingRuntime {
|
|
|
997
1356
|
return this.returnReadiness(h.id);
|
|
998
1357
|
}
|
|
999
1358
|
private returnReadiness(id: string) {
|
|
1000
|
-
const
|
|
1359
|
+
const state = this.stateData();
|
|
1360
|
+
const h = state.handoffs.get(id)!;
|
|
1001
1361
|
const prepared = this.prepared("coordinator", this.messages, id);
|
|
1362
|
+
const isSupplement = h.kind === "recovery-return";
|
|
1363
|
+
const recovery = isSupplement
|
|
1364
|
+
? [...state.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
|
|
1365
|
+
: undefined;
|
|
1002
1366
|
return {
|
|
1003
1367
|
status: "accepted",
|
|
1004
|
-
reportSaved:
|
|
1368
|
+
reportSaved: !isSupplement,
|
|
1369
|
+
supplementSaved: isSupplement,
|
|
1005
1370
|
handoff: id,
|
|
1006
|
-
|
|
1371
|
+
...(recovery ? { recovery: recovery.id } : {}),
|
|
1372
|
+
...(isSupplement ? { supplementRevision: h.reportRevision } : { reportRevision: h.reportRevision }),
|
|
1007
1373
|
assignmentRef: `assignment:${h.assignmentId}`,
|
|
1008
|
-
|
|
1009
|
-
|
|
1374
|
+
...(isSupplement
|
|
1375
|
+
? { supplementRef: `supplement:${recovery?.id ?? "unknown"}:${h.reportRevision}` }
|
|
1376
|
+
: { reportRef: `report:${h.id}:${h.reportRevision}` }),
|
|
1377
|
+
producer: h.from,
|
|
1010
1378
|
outcome: h.outcome,
|
|
1011
1379
|
limitations: h.limitations,
|
|
1012
1380
|
ready: prepared.ready,
|
|
@@ -1017,8 +1385,8 @@ export class RoutingRuntime {
|
|
|
1017
1385
|
finalExchangePending: true,
|
|
1018
1386
|
provisional:
|
|
1019
1387
|
"The finalized handoff exchange, target configuration and budget are revalidated at turn_end; this is not delivery evidence.",
|
|
1020
|
-
report: h.text,
|
|
1021
|
-
evidence: this.evidenceFacts(
|
|
1388
|
+
...(isSupplement ? { supplement: h.text } : { report: h.text }),
|
|
1389
|
+
evidence: this.evidenceFacts(state),
|
|
1022
1390
|
};
|
|
1023
1391
|
}
|
|
1024
1392
|
private project(input: any, op: string) {
|
|
@@ -1053,7 +1421,7 @@ export class RoutingRuntime {
|
|
|
1053
1421
|
[...sources.byRef.values()]
|
|
1054
1422
|
.filter(
|
|
1055
1423
|
(s) =>
|
|
1056
|
-
s.producer
|
|
1424
|
+
isWorkerProfile(s.producer) &&
|
|
1057
1425
|
(isTaskEvidence(s) || (scope === "selected" && next.selected.includes(s.ref))) &&
|
|
1058
1426
|
(scope === "selected"
|
|
1059
1427
|
? next.selected.includes(s.ref)
|
|
@@ -1067,12 +1435,14 @@ export class RoutingRuntime {
|
|
|
1067
1435
|
s.message.content?.some((b: any) => b.type === "text" && b.text?.trim())),
|
|
1068
1436
|
)
|
|
1069
1437
|
.map((s) => {
|
|
1438
|
+
const locator = sources.locator(s.ref);
|
|
1070
1439
|
return {
|
|
1071
1440
|
ref: s.ref,
|
|
1072
1441
|
kind: s.original ? "assistant-text" : s.message.role,
|
|
1073
1442
|
producer: s.producer,
|
|
1074
1443
|
assignment: s.assignmentId,
|
|
1075
1444
|
toolName: s.message.toolName,
|
|
1445
|
+
...(locator ? { locator } : {}),
|
|
1076
1446
|
active: s.active,
|
|
1077
1447
|
selected: next.selected.includes(s.ref),
|
|
1078
1448
|
retainedSelection: next.selected.includes(s.ref) && !isTaskEvidence(s),
|
|
@@ -1097,7 +1467,7 @@ export class RoutingRuntime {
|
|
|
1097
1467
|
},
|
|
1098
1468
|
otherAssignments: [...sources.byRef.values()].filter(
|
|
1099
1469
|
(s) =>
|
|
1100
|
-
s.producer
|
|
1470
|
+
isWorkerProfile(s.producer) && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
|
|
1101
1471
|
).length,
|
|
1102
1472
|
}),
|
|
1103
1473
|
);
|
|
@@ -1149,10 +1519,40 @@ export class RoutingRuntime {
|
|
|
1149
1519
|
estimate: { tokens: prepared.estimatedTokens, method: prepared.estimateMethod },
|
|
1150
1520
|
};
|
|
1151
1521
|
}
|
|
1152
|
-
private unit(input: any, op: string) {
|
|
1522
|
+
private unit(input: any, callId: string, op: string) {
|
|
1153
1523
|
const state = this.stateData();
|
|
1154
1524
|
const prior = [...state.events.values()].find((e) => e.operationId === op);
|
|
1155
1525
|
if (prior) {
|
|
1526
|
+
if (input.operation === "recover") {
|
|
1527
|
+
check(
|
|
1528
|
+
prior.data.type === "recovery-request-accepted" &&
|
|
1529
|
+
prior.data.recovery.request === input.request &&
|
|
1530
|
+
canonical(prior.data.recovery.requestedPaths) === canonical([...new Set<string>(input.paths ?? [])]),
|
|
1531
|
+
"operation_conflict",
|
|
1532
|
+
);
|
|
1533
|
+
const { recovery, handoff } = prior.data;
|
|
1534
|
+
return {
|
|
1535
|
+
status: "accepted",
|
|
1536
|
+
unchanged: true,
|
|
1537
|
+
recovery: recovery.id,
|
|
1538
|
+
handoff: handoff.id,
|
|
1539
|
+
assignmentRef: `assignment:${recovery.assignmentId}`,
|
|
1540
|
+
reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
|
|
1541
|
+
request: recovery.request,
|
|
1542
|
+
paths: recovery.paths,
|
|
1543
|
+
transition: handoff.state,
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
if (input.operation === "cancel-recovery") {
|
|
1547
|
+
check(prior.data.type === "recovery-cancelled" && prior.data.reason === input.reason, "operation_conflict");
|
|
1548
|
+
return {
|
|
1549
|
+
status: "cancelled",
|
|
1550
|
+
unchanged: true,
|
|
1551
|
+
recovery: prior.data.recoveryId,
|
|
1552
|
+
reason: prior.data.reason,
|
|
1553
|
+
assessment: state.assessment?.handoffId,
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1156
1556
|
check(
|
|
1157
1557
|
(input.operation === "assess" && prior.data.type === "assessment-resumed") ||
|
|
1158
1558
|
(input.operation === "close" &&
|
|
@@ -1163,18 +1563,95 @@ export class RoutingRuntime {
|
|
|
1163
1563
|
);
|
|
1164
1564
|
return { status: "unchanged", operation: input.operation };
|
|
1165
1565
|
}
|
|
1566
|
+
if (input.operation === "recover") {
|
|
1567
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1568
|
+
const base = a?.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1569
|
+
check(
|
|
1570
|
+
state.unitId &&
|
|
1571
|
+
a?.state === "returned" &&
|
|
1572
|
+
base?.kind === "return" &&
|
|
1573
|
+
base.state === "configured" &&
|
|
1574
|
+
state.assessment?.handoffId === base.id &&
|
|
1575
|
+
!state.pendingId &&
|
|
1576
|
+
!state.recoveryId &&
|
|
1577
|
+
!this.applying,
|
|
1578
|
+
"recovery_not_available",
|
|
1579
|
+
);
|
|
1580
|
+
const worker = this.assignedWorker(state, a.id);
|
|
1581
|
+
const id = randomUUID(),
|
|
1582
|
+
handoffId = randomUUID();
|
|
1583
|
+
const paths = [...new Set<string>((input.paths ?? []).map((path: string) => this.canonicalRecoveryPath(path)))];
|
|
1584
|
+
const recovery: Recovery = {
|
|
1585
|
+
id,
|
|
1586
|
+
assignmentId: a.id,
|
|
1587
|
+
assessmentHandoffId: base.id,
|
|
1588
|
+
baseReportRevision: base.reportRevision,
|
|
1589
|
+
request: input.request,
|
|
1590
|
+
requestedPaths: [...new Set<string>(input.paths ?? [])],
|
|
1591
|
+
paths,
|
|
1592
|
+
state: "requested",
|
|
1593
|
+
requestHandoffId: handoffId,
|
|
1594
|
+
supplementRevision: 0,
|
|
1595
|
+
};
|
|
1596
|
+
const handoff: Handoff = {
|
|
1597
|
+
id: handoffId,
|
|
1598
|
+
kind: "recovery-request",
|
|
1599
|
+
assignmentId: a.id,
|
|
1600
|
+
text: input.request,
|
|
1601
|
+
from: "coordinator",
|
|
1602
|
+
to: worker,
|
|
1603
|
+
executionId: this.turn!.id,
|
|
1604
|
+
toolCallId: callId,
|
|
1605
|
+
basisUserEntryId: this.turn!.basisUserEntryId,
|
|
1606
|
+
state: "pending",
|
|
1607
|
+
reportRevision: 0,
|
|
1608
|
+
limitations: [],
|
|
1609
|
+
};
|
|
1610
|
+
this.append({ type: "recovery-request-accepted", recovery, handoff }, op);
|
|
1611
|
+
return {
|
|
1612
|
+
status: "accepted",
|
|
1613
|
+
recovery: id,
|
|
1614
|
+
handoff: handoffId,
|
|
1615
|
+
assignmentRef: `assignment:${a.id}`,
|
|
1616
|
+
reportRef: `report:${base.id}:${base.reportRevision}`,
|
|
1617
|
+
request: input.request,
|
|
1618
|
+
paths,
|
|
1619
|
+
transition: "pending",
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
if (input.operation === "cancel-recovery") {
|
|
1623
|
+
check(state.recoveryId && !this.applying, "recovery_not_cancellable");
|
|
1624
|
+
check(
|
|
1625
|
+
![...state.executions.values()].some(
|
|
1626
|
+
(execution) =>
|
|
1627
|
+
execution.recoveryId === state.recoveryId &&
|
|
1628
|
+
isWorkerProfile(execution.profile) &&
|
|
1629
|
+
!execution.assistantEntryId &&
|
|
1630
|
+
!execution.interrupted,
|
|
1631
|
+
),
|
|
1632
|
+
"recovery_not_cancellable",
|
|
1633
|
+
);
|
|
1634
|
+
this.append({ type: "recovery-cancelled", recoveryId: state.recoveryId, reason: input.reason }, op);
|
|
1635
|
+
return {
|
|
1636
|
+
status: "cancelled",
|
|
1637
|
+
recovery: state.recoveryId,
|
|
1638
|
+
reason: input.reason,
|
|
1639
|
+
assessment: state.assessment?.handoffId,
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1166
1642
|
if (input.operation === "assess") {
|
|
1167
1643
|
check(state.assessment, "assessment_missing");
|
|
1644
|
+
check(!state.recoveryId, "recovery_outstanding");
|
|
1168
1645
|
if (state.assessment.view === "active") return { status: "unchanged", ready: true };
|
|
1169
1646
|
const prepared = this.prepared("coordinator", this.messages, state.assessment.handoffId, true);
|
|
1170
1647
|
if (!prepared.ready) return { status: "suspended", ready: false, problems: prepared.problems };
|
|
1171
|
-
check(prepared.reservation, "reservation_missing");
|
|
1648
|
+
if (this.projectionEnabled) check(prepared.reservation, "reservation_missing");
|
|
1172
1649
|
this.append(
|
|
1173
1650
|
{
|
|
1174
1651
|
type: "assessment-resumed",
|
|
1175
1652
|
handoffId: state.assessment.handoffId,
|
|
1176
1653
|
basisUserEntryId: this.turn!.basisUserEntryId,
|
|
1177
|
-
reservation: prepared.reservation,
|
|
1654
|
+
...(prepared.reservation ? { reservation: prepared.reservation } : {}),
|
|
1178
1655
|
},
|
|
1179
1656
|
op,
|
|
1180
1657
|
);
|
|
@@ -1189,6 +1666,19 @@ export class RoutingRuntime {
|
|
|
1189
1666
|
check(input.operation === "close" && state.unitId, "unit_missing");
|
|
1190
1667
|
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1191
1668
|
check(!this.applying, "not_quiescent");
|
|
1669
|
+
if (state.recoveryId) {
|
|
1670
|
+
check(input.outcome !== "accepted", "unfinished_work");
|
|
1671
|
+
check(
|
|
1672
|
+
![...state.executions.values()].some(
|
|
1673
|
+
(execution) =>
|
|
1674
|
+
execution.recoveryId === state.recoveryId &&
|
|
1675
|
+
isWorkerProfile(execution.profile) &&
|
|
1676
|
+
!execution.assistantEntryId &&
|
|
1677
|
+
!execution.interrupted,
|
|
1678
|
+
),
|
|
1679
|
+
"not_quiescent",
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1192
1682
|
this.append(
|
|
1193
1683
|
{
|
|
1194
1684
|
type: "unit-closed",
|
|
@@ -1279,6 +1769,7 @@ export class RoutingRuntime {
|
|
|
1279
1769
|
[...state.assignments.values()].reverse().map((a) => {
|
|
1280
1770
|
const u = state.units.get(a.unitId),
|
|
1281
1771
|
h = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1772
|
+
const delegate = state.handoffs.get(a.delegateHandoffId);
|
|
1282
1773
|
return {
|
|
1283
1774
|
ref: `assignment:${a.id}`,
|
|
1284
1775
|
unitRef: `unit:${a.unitId}`,
|
|
@@ -1290,8 +1781,8 @@ export class RoutingRuntime {
|
|
|
1290
1781
|
summary: a.contract.slice(0, 160),
|
|
1291
1782
|
reportAvailable: !!h,
|
|
1292
1783
|
outcome: h?.outcome,
|
|
1293
|
-
from:
|
|
1294
|
-
to:
|
|
1784
|
+
from: delegate?.from,
|
|
1785
|
+
to: delegate?.to,
|
|
1295
1786
|
};
|
|
1296
1787
|
});
|
|
1297
1788
|
const page = this.page("work-history", rows, input.cursor, input.limit ?? 20);
|
|
@@ -1310,9 +1801,9 @@ export class RoutingRuntime {
|
|
|
1310
1801
|
"Use a ref returned by history inspection.",
|
|
1311
1802
|
);
|
|
1312
1803
|
check(
|
|
1313
|
-
/^(?:(?:unit|assignment):[^:\s]+|report:[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
|
|
1804
|
+
/^(?:(?:unit|assignment|recovery):[^:\s]+|(?:report|supplement):[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
|
|
1314
1805
|
"invalid_work_ref",
|
|
1315
|
-
"Use
|
|
1806
|
+
"Use assignment, report, recovery, or supplement refs returned by a receipt or inspection, unchanged. A bare handoff ID is not a work ref.",
|
|
1316
1807
|
);
|
|
1317
1808
|
if (input.ref.startsWith("unit:")) {
|
|
1318
1809
|
const u = state.units.get(input.ref.slice(5));
|
|
@@ -1335,6 +1826,63 @@ export class RoutingRuntime {
|
|
|
1335
1826
|
historical: true,
|
|
1336
1827
|
};
|
|
1337
1828
|
}
|
|
1829
|
+
if (input.ref.startsWith("recovery:")) {
|
|
1830
|
+
const recovery = state.recoveries.get(input.ref.slice(9));
|
|
1831
|
+
check(recovery, "work_unavailable", "Recovery is not on current ancestry.");
|
|
1832
|
+
const request = state.handoffs.get(recovery.requestHandoffId);
|
|
1833
|
+
const supplement = recovery.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
|
|
1834
|
+
return {
|
|
1835
|
+
status: "inspected",
|
|
1836
|
+
view,
|
|
1837
|
+
ref: input.ref,
|
|
1838
|
+
historical: recovery.id !== state.recoveryId,
|
|
1839
|
+
recovery: {
|
|
1840
|
+
id: recovery.id,
|
|
1841
|
+
state: recovery.state,
|
|
1842
|
+
assignmentId: recovery.assignmentId,
|
|
1843
|
+
assessmentHandoffId: recovery.assessmentHandoffId,
|
|
1844
|
+
baseReportRevision: recovery.baseReportRevision,
|
|
1845
|
+
request: recovery.request,
|
|
1846
|
+
paths: recovery.paths,
|
|
1847
|
+
requestHandoffState: request?.state,
|
|
1848
|
+
supplementRevision: recovery.supplementRevision,
|
|
1849
|
+
cancellationReason: recovery.cancellationReason,
|
|
1850
|
+
},
|
|
1851
|
+
supplementRef: supplement ? `supplement:${recovery.id}:${supplement.reportRevision}` : undefined,
|
|
1852
|
+
sourceBoundary: "Saved recovery on current ancestry; historical content grants no current permission.",
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
if (input.ref.startsWith("supplement:")) {
|
|
1856
|
+
const match = /^supplement:([^:]+):([0-9]+)$/.exec(input.ref);
|
|
1857
|
+
check(match, "work_unavailable", "Use a supplement ref returned by a receipt or recovery inspection.");
|
|
1858
|
+
const recovery = state.recoveries.get(match[1]);
|
|
1859
|
+
let supplement: Handoff | undefined;
|
|
1860
|
+
for (const event of state.events.values())
|
|
1861
|
+
if (
|
|
1862
|
+
event.data.type === "recovery-supplement-accepted" &&
|
|
1863
|
+
event.data.recoveryId === match[1] &&
|
|
1864
|
+
event.data.handoff.reportRevision === Number(match[2])
|
|
1865
|
+
)
|
|
1866
|
+
supplement = event.data.handoff;
|
|
1867
|
+
check(recovery && supplement, "work_unavailable", "Supplement revision is not on current ancestry.");
|
|
1868
|
+
const base = state.handoffs.get(recovery.assessmentHandoffId);
|
|
1869
|
+
return {
|
|
1870
|
+
status: "inspected",
|
|
1871
|
+
view,
|
|
1872
|
+
ref: input.ref,
|
|
1873
|
+
historical: true,
|
|
1874
|
+
recoveryRef: `recovery:${recovery.id}`,
|
|
1875
|
+
assignment: { id: recovery.assignmentId, state: state.assignments.get(recovery.assignmentId)?.state },
|
|
1876
|
+
reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
|
|
1877
|
+
report: base?.text,
|
|
1878
|
+
supplement: supplement.text,
|
|
1879
|
+
supplementRevision: supplement.reportRevision,
|
|
1880
|
+
outcome: supplement.outcome,
|
|
1881
|
+
limitations: supplement.limitations,
|
|
1882
|
+
sourceBoundary:
|
|
1883
|
+
"Saved supplement revision and original report linkage on current ancestry; historical content grants no current permission.",
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1338
1886
|
let savedRevision: Handoff | undefined;
|
|
1339
1887
|
if (input.ref.startsWith("report:")) {
|
|
1340
1888
|
const match = /^report:([^:]+):([0-9]+)$/.exec(input.ref);
|
|
@@ -1383,8 +1931,23 @@ export class RoutingRuntime {
|
|
|
1383
1931
|
return {
|
|
1384
1932
|
...this.state(),
|
|
1385
1933
|
unit: unit ? { id: unit.id, state: unit.state, assignments: unit.assignmentIds.length } : null,
|
|
1386
|
-
assignment: a ? { id: a.id, state: a.state } : null,
|
|
1934
|
+
assignment: a ? { id: a.id, state: a.state, worker: this.assignedWorker(state, a.id) } : null,
|
|
1387
1935
|
pendingHandoff: state.pendingId ?? null,
|
|
1936
|
+
recovery: state.recoveryId
|
|
1937
|
+
? (() => {
|
|
1938
|
+
const recovery = state.recoveries.get(state.recoveryId!);
|
|
1939
|
+
return recovery
|
|
1940
|
+
? {
|
|
1941
|
+
id: recovery.id,
|
|
1942
|
+
state: recovery.state,
|
|
1943
|
+
assignmentId: recovery.assignmentId,
|
|
1944
|
+
assessmentHandoffId: recovery.assessmentHandoffId,
|
|
1945
|
+
paths: recovery.paths,
|
|
1946
|
+
supplementRevision: recovery.supplementRevision,
|
|
1947
|
+
}
|
|
1948
|
+
: null;
|
|
1949
|
+
})()
|
|
1950
|
+
: null,
|
|
1388
1951
|
assessment: state.assessment
|
|
1389
1952
|
? { handoffId: state.assessment.handoffId, view: state.assessment.view, problems: state.assessment.problems }
|
|
1390
1953
|
: null,
|
|
@@ -1471,23 +2034,29 @@ export class RoutingRuntime {
|
|
|
1471
2034
|
"source_exchange_incomplete",
|
|
1472
2035
|
"The saved handoff has an incomplete native exchange; reconcile or explicitly dispose of it.",
|
|
1473
2036
|
);
|
|
1474
|
-
|
|
2037
|
+
let attentionProblems: Problem[] = [];
|
|
2038
|
+
if (["return", "recovery-return"].includes(h.kind) && this.projectionEnabled) {
|
|
1475
2039
|
const prepared = this.prepared("coordinator", inputs, h.id);
|
|
1476
2040
|
if (!prepared.ready) {
|
|
2041
|
+
const attentionFallback = h.kind === "recovery-return" && h.outcome !== "completed";
|
|
1477
2042
|
this.projectionError = prepared.problems.map((p) => p.code).join(", ");
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
.
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
2043
|
+
if (attentionFallback) attentionProblems = prepared.problems;
|
|
2044
|
+
else {
|
|
2045
|
+
this.append({
|
|
2046
|
+
type: "handoff-state",
|
|
2047
|
+
handoffId: h.id,
|
|
2048
|
+
state: "blocked",
|
|
2049
|
+
reason: prepared.problems
|
|
2050
|
+
.map((p) => p.detail)
|
|
2051
|
+
.join("; ")
|
|
2052
|
+
.slice(0, 4096),
|
|
2053
|
+
});
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
} else {
|
|
2057
|
+
check(prepared.reservation, "reservation_missing");
|
|
2058
|
+
this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
|
|
1488
2059
|
}
|
|
1489
|
-
check(prepared.reservation, "reservation_missing");
|
|
1490
|
-
this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
|
|
1491
2060
|
}
|
|
1492
2061
|
try {
|
|
1493
2062
|
const current = this.stateData();
|
|
@@ -1497,6 +2066,28 @@ export class RoutingRuntime {
|
|
|
1497
2066
|
this.append({ type: "handoff-state", handoffId: h.id, state: "configured", observedPair: this.observed() });
|
|
1498
2067
|
const completed = this.stateData();
|
|
1499
2068
|
const reservation = completed.reservations.get(h.id);
|
|
2069
|
+
const recovery =
|
|
2070
|
+
h.kind === "recovery-return"
|
|
2071
|
+
? [...completed.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
|
|
2072
|
+
: undefined;
|
|
2073
|
+
if (h.kind === "recovery-return" && recovery && completed.assessment?.view === "suspended") {
|
|
2074
|
+
if (attentionProblems.length) {
|
|
2075
|
+
this.append({
|
|
2076
|
+
type: "assessment-suspended",
|
|
2077
|
+
handoffId: recovery.assessmentHandoffId,
|
|
2078
|
+
reason: "delivery-gap",
|
|
2079
|
+
basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
|
|
2080
|
+
problems: attentionProblems,
|
|
2081
|
+
});
|
|
2082
|
+
} else if (completed.assessment.suspensionReason === "recovery") {
|
|
2083
|
+
this.append({
|
|
2084
|
+
type: "assessment-resumed",
|
|
2085
|
+
handoffId: recovery.assessmentHandoffId,
|
|
2086
|
+
basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
|
|
2087
|
+
...(reservation ? { reservation } : {}),
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
1500
2091
|
if (h.kind === "return" && completed.assessment?.view === "suspended" && reservation) {
|
|
1501
2092
|
this.append({
|
|
1502
2093
|
type: "assessment-resumed",
|
|
@@ -1505,7 +2096,7 @@ export class RoutingRuntime {
|
|
|
1505
2096
|
reservation,
|
|
1506
2097
|
});
|
|
1507
2098
|
}
|
|
1508
|
-
this.projectionError = undefined;
|
|
2099
|
+
if (!attentionProblems.length) this.projectionError = undefined;
|
|
1509
2100
|
} catch (error) {
|
|
1510
2101
|
if (this.current(subject) && !this.store?.blocked && !this.error && this.stateData().pendingId === h.id) {
|
|
1511
2102
|
this.append({
|
|
@@ -1515,7 +2106,7 @@ export class RoutingRuntime {
|
|
|
1515
2106
|
reason: error instanceof Error ? error.message.slice(0, 4096) : "configuration_failed",
|
|
1516
2107
|
});
|
|
1517
2108
|
this.ctx.ui?.notify?.(
|
|
1518
|
-
`Couldn’t switch to ${h.to
|
|
2109
|
+
`Couldn’t switch to ${h.to.charAt(0).toUpperCase()}${h.to.slice(1)} — ${["return", "recovery-return"].includes(h.kind) ? "report" : "assignment"} saved.`,
|
|
1519
2110
|
"warning",
|
|
1520
2111
|
);
|
|
1521
2112
|
} else throw error;
|
|
@@ -1541,7 +2132,7 @@ export class RoutingRuntime {
|
|
|
1541
2132
|
check(
|
|
1542
2133
|
!ctx.hasPendingMessages?.(),
|
|
1543
2134
|
"pending_input",
|
|
1544
|
-
"Queued input must be delivered and reconciled before resuming
|
|
2135
|
+
"Queued input must be delivered and reconciled before resuming a worker.",
|
|
1545
2136
|
);
|
|
1546
2137
|
const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
|
|
1547
2138
|
if (pending) {
|
|
@@ -1551,17 +2142,18 @@ export class RoutingRuntime {
|
|
|
1551
2142
|
this.guard(subject);
|
|
1552
2143
|
if (this.stateData().pendingId) {
|
|
1553
2144
|
check(
|
|
1554
|
-
|
|
2145
|
+
["return", "recovery-return"].includes(pending.kind) && this.projectionError,
|
|
1555
2146
|
"handoff_still_blocked",
|
|
1556
2147
|
"Saved transfer is still blocked; reconcile its configuration before retrying.",
|
|
1557
2148
|
);
|
|
2149
|
+
const worker = this.assignedWorker(this.stateData(), pending.assignmentId);
|
|
1558
2150
|
this.append({
|
|
1559
2151
|
type: "control",
|
|
1560
2152
|
control: "automatic",
|
|
1561
|
-
profile:
|
|
2153
|
+
profile: worker,
|
|
1562
2154
|
reason: "Explicit resume of saved-return evidence correction only; ordinary assignment work remains ended",
|
|
1563
2155
|
});
|
|
1564
|
-
await this.applyPair(this.profilePair(
|
|
2156
|
+
await this.applyPair(this.profilePair(worker));
|
|
1565
2157
|
}
|
|
1566
2158
|
} else {
|
|
1567
2159
|
const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
@@ -1571,7 +2163,22 @@ export class RoutingRuntime {
|
|
|
1571
2163
|
(x) =>
|
|
1572
2164
|
x.profile === "coordinator" && x.basisUserEntryId === user && x.assistantEntryId && x.outcome === "completed",
|
|
1573
2165
|
);
|
|
1574
|
-
|
|
2166
|
+
const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
|
|
2167
|
+
const worker = assignment ? this.assignedWorker(state, assignment.id) : undefined;
|
|
2168
|
+
if (assignment?.state === "returned" && recovery?.state === "reading") {
|
|
2169
|
+
check(
|
|
2170
|
+
user === this.workerBasis(state, assignment.id) || coordinatorSawInput,
|
|
2171
|
+
"input_unreconciled",
|
|
2172
|
+
"New input needs Coordinator attention before evidence recovery can resume.",
|
|
2173
|
+
);
|
|
2174
|
+
this.append({
|
|
2175
|
+
type: "control",
|
|
2176
|
+
control: "automatic",
|
|
2177
|
+
profile: worker!,
|
|
2178
|
+
reason: "Explicit user resume of unchanged evidence recovery; ordinary assignment work remains ended",
|
|
2179
|
+
});
|
|
2180
|
+
await this.applyPair(this.profilePair(worker!));
|
|
2181
|
+
} else if (assignment?.state === "outstanding") {
|
|
1575
2182
|
check(
|
|
1576
2183
|
user === this.taskBasis(state, assignment.id) || coordinatorSawInput,
|
|
1577
2184
|
"input_unreconciled",
|
|
@@ -1581,10 +2188,10 @@ export class RoutingRuntime {
|
|
|
1581
2188
|
this.append({
|
|
1582
2189
|
type: "control",
|
|
1583
2190
|
control: "automatic",
|
|
1584
|
-
profile:
|
|
2191
|
+
profile: worker!,
|
|
1585
2192
|
reason: "Explicit user resume of the unchanged assignment; current restrictions remain applicable",
|
|
1586
2193
|
});
|
|
1587
|
-
await this.applyPair(this.profilePair(
|
|
2194
|
+
await this.applyPair(this.profilePair(worker!));
|
|
1588
2195
|
} else await this.applyPair(this.profilePair("coordinator"));
|
|
1589
2196
|
}
|
|
1590
2197
|
this.guard(subject);
|
|
@@ -1621,7 +2228,7 @@ export class RoutingRuntime {
|
|
|
1621
2228
|
check(
|
|
1622
2229
|
words.length === 2 && [...PROFILES, "auto"].includes(words[1]),
|
|
1623
2230
|
"invalid_profile_command",
|
|
1624
|
-
"Use /freeflow profile coordinator|executor|auto|history, or /freeflow resume.",
|
|
2231
|
+
"Use /freeflow profile coordinator|helper|executor|auto|history, or /freeflow resume.",
|
|
1625
2232
|
);
|
|
1626
2233
|
const result =
|
|
1627
2234
|
words[1] === "auto" ? await this.setAutomaticControl() : await this.setManualProfile(words[1] as Profile);
|