@hassangameryt/freeflow 0.7.1 → 0.7.2
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 +28 -23
- package/capabilities/cognitive-routing/SKILL.md +94 -311
- package/command-surface.json +7 -1
- package/gemini-extension.json +1 -1
- package/package.json +4 -4
- package/pi-extension/dist/cognitive-routing-v2/budget.js +42 -0
- package/pi-extension/dist/cognitive-routing-v2/config.js +120 -0
- package/pi-extension/dist/cognitive-routing-v2/projection.js +321 -0
- package/pi-extension/dist/cognitive-routing-v2/render.js +231 -0
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +1585 -0
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +107 -0
- package/pi-extension/dist/cognitive-routing-v2/state.js +612 -0
- package/pi-extension/dist/cognitive-routing-v2/tools.js +91 -0
- package/pi-extension/dist/cognitive-routing-v2/types.js +44 -0
- package/pi-extension/dist/freeflow-context/tool.js +5 -0
- package/pi-extension/dist/index.js +279 -624
- package/pi-extension/dist/runtime/runtime-context.js +63 -13
- package/pi-extension/dist/session-sources/events.js +195 -0
- package/pi-extension/dist/session-sources/read-only-session.js +148 -0
- package/pi-extension/dist/session-sources/sources.js +233 -0
- package/pi-extension/dist/settings/settings-ui.js +44 -117
- package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
- package/pi-extension/src/cognitive-routing-v2/config.ts +143 -0
- package/pi-extension/src/cognitive-routing-v2/projection.ts +365 -0
- package/pi-extension/src/cognitive-routing-v2/render.ts +256 -0
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +1634 -0
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +109 -0
- package/pi-extension/src/cognitive-routing-v2/state.ts +622 -0
- package/pi-extension/src/cognitive-routing-v2/tools.ts +93 -0
- package/pi-extension/src/cognitive-routing-v2/types.ts +222 -0
- package/pi-extension/src/freeflow-context/tool.ts +5 -0
- package/pi-extension/src/index.ts +285 -674
- package/pi-extension/src/runtime/runtime-context.ts +75 -14
- package/pi-extension/src/session-sources/events.ts +208 -0
- package/pi-extension/src/session-sources/read-only-session.ts +170 -0
- package/pi-extension/src/session-sources/sources.ts +263 -0
- package/pi-extension/src/settings/settings-ui.ts +54 -131
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +484 -0
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +441 -0
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +245 -0
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +236 -0
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +230 -0
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +153 -0
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +275 -0
- package/pi-extension/tests/context-virtualization/extension.test.js +16 -0
- package/pi-extension/tests/fixtures/routing-native.js +188 -0
- package/pi-extension/tests/integration/mode-free-core.test.js +3 -2
- package/pi-extension/tests/integration/pi-extension.test.js +154 -37
- package/pi-extension/tests/integration/prompt-architecture.test.js +87 -6
- package/pi-extension/tests/integration/unaffected-regressions.test.js +77 -10
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -5
- package/runtime/prompts/core.md +51 -45
- package/runtime/prompts/interaction-contract.md +5 -5
- package/skills/action-selection/SKILL.md +57 -97
- package/skills/design-for-depth/SKILL.md +58 -193
- package/skills/design-for-depth/references/implementation-decisions.md +76 -0
- package/skills/diagnose-failure/SKILL.md +64 -196
- package/skills/discuss/SKILL.md +64 -123
- package/skills/discuss/references/discussion-continuity.md +7 -3
- package/skills/execute-work/SKILL.md +69 -88
- package/skills/execute-work/references/execute-work-edges.md +9 -3
- package/skills/handoff/SKILL.md +44 -54
- package/skills/handoff/references/handoff-templates.md +15 -6
- package/skills/review-artifact/SKILL.md +80 -127
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
- package/skills/review-work/SKILL.md +65 -115
- package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
- package/skills/simplify-code/SKILL.md +47 -100
- package/skills/track-work/SKILL.md +128 -119
- package/skills/track-work/references/working-record-format.md +14 -2
- package/skills/verify-work/SKILL.md +58 -62
- package/skills/verify-work/references/integration-evidence.md +4 -2
- package/skills/verify-work/references/test-design.md +89 -0
- package/skills/workflow/SKILL.md +73 -119
- package/skills/write-plan/SKILL.md +46 -77
- package/skills/write-plan/references/plan-shapes.md +5 -5
- package/skills/write-spec/SKILL.md +51 -63
- package/skills/write-spec/references/artifact-standards.md +2 -2
- package/skills/write-spec/references/decision-records.md +3 -2
- package/skills/write-spec/references/spec-shapes.md +15 -0
- package/pi-extension/dist/cognitive-routing/commands.js +0 -96
- package/pi-extension/dist/cognitive-routing/config.js +0 -232
- package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
- package/pi-extension/dist/cognitive-routing/history.js +0 -451
- package/pi-extension/dist/cognitive-routing/host.js +0 -188
- package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
- package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
- package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
- package/pi-extension/dist/cognitive-routing/tool.js +0 -212
- package/pi-extension/dist/cognitive-routing/types.js +0 -6
- package/pi-extension/src/cognitive-routing/commands.ts +0 -129
- package/pi-extension/src/cognitive-routing/config.ts +0 -303
- package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
- package/pi-extension/src/cognitive-routing/history.ts +0 -650
- package/pi-extension/src/cognitive-routing/host.ts +0 -266
- package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
- package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
- package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
- package/pi-extension/src/cognitive-routing/tool.ts +0 -273
- package/pi-extension/src/cognitive-routing/types.ts +0 -89
- package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
- package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
- package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
- package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
- package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
- package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
- package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
- package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
- package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
- package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
- package/skills/tdd/SKILL.md +0 -143
- package/skills/tdd/references/test-design.md +0 -96
- /package/pi-extension/tests/{cognitive-routing/host-fixture.js → fixtures/pi-host.js} +0 -0
|
@@ -0,0 +1,1634 @@
|
|
|
1
|
+
import { schemaForProfile } from "./schemas.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { EventStore, type SessionReader } from "../session-sources/events.js";
|
|
4
|
+
import { Sources, bodyHash, textRef, isTaskEvidence, type Source } from "../session-sources/sources.js";
|
|
5
|
+
import { pairFromProfile, type CognitiveRoutingCapabilityState } from "./config.js";
|
|
6
|
+
import { prepareView, changeSelection, representationProblems, type PreparedView } from "./projection.js";
|
|
7
|
+
import { initialState } from "./state.js";
|
|
8
|
+
import {
|
|
9
|
+
ROUTING_ENTRY,
|
|
10
|
+
ROUTING_MESSAGE,
|
|
11
|
+
PROFILES,
|
|
12
|
+
RoutingError,
|
|
13
|
+
canonical,
|
|
14
|
+
emptySelection,
|
|
15
|
+
requireCondition as check,
|
|
16
|
+
samePair,
|
|
17
|
+
type Profile,
|
|
18
|
+
type View,
|
|
19
|
+
type Pair,
|
|
20
|
+
type State,
|
|
21
|
+
type Handoff,
|
|
22
|
+
type NativeEntry,
|
|
23
|
+
type EventData,
|
|
24
|
+
type Execution,
|
|
25
|
+
type Problem,
|
|
26
|
+
} from "./types.js";
|
|
27
|
+
|
|
28
|
+
export const ROUTING_TOOLS = ["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project"] as const;
|
|
29
|
+
const HANDOFF_TOOLS = new Set(["freeflow_delegate", "freeflow_return"]);
|
|
30
|
+
interface Turn {
|
|
31
|
+
id: string;
|
|
32
|
+
profile: View;
|
|
33
|
+
assignmentId?: string;
|
|
34
|
+
basisUserEntryId: string | null;
|
|
35
|
+
before: Set<string>;
|
|
36
|
+
message?: any;
|
|
37
|
+
bound?: string;
|
|
38
|
+
sourceFingerprint?: string;
|
|
39
|
+
opened?: boolean;
|
|
40
|
+
pair: Pair;
|
|
41
|
+
}
|
|
42
|
+
interface Subject {
|
|
43
|
+
token: string;
|
|
44
|
+
revision: number;
|
|
45
|
+
store: EventStore | undefined;
|
|
46
|
+
leafId: string | null;
|
|
47
|
+
userId: string | null;
|
|
48
|
+
}
|
|
49
|
+
export class RoutingRuntime {
|
|
50
|
+
private store?: EventStore;
|
|
51
|
+
private ctx: any;
|
|
52
|
+
private capability?: CognitiveRoutingCapabilityState;
|
|
53
|
+
private token = randomUUID();
|
|
54
|
+
private turn?: Turn;
|
|
55
|
+
private messages: any[] = [];
|
|
56
|
+
private error?: string;
|
|
57
|
+
private projectionError?: string;
|
|
58
|
+
private suppressed = false;
|
|
59
|
+
private applying?: Pair;
|
|
60
|
+
private externalChange = false;
|
|
61
|
+
private targetSignature?: string;
|
|
62
|
+
private operations: Promise<unknown> = Promise.resolve();
|
|
63
|
+
private revision = 0;
|
|
64
|
+
private receipts = new Map<string, { input: string; value: any }>();
|
|
65
|
+
private pages = new Map<
|
|
66
|
+
string,
|
|
67
|
+
{
|
|
68
|
+
scope: string;
|
|
69
|
+
basis: string | null;
|
|
70
|
+
assignment?: string;
|
|
71
|
+
compaction?: string;
|
|
72
|
+
rows: any[];
|
|
73
|
+
size: number;
|
|
74
|
+
summary?: any;
|
|
75
|
+
}
|
|
76
|
+
>();
|
|
77
|
+
private manualHold?: Profile;
|
|
78
|
+
private automaticControl = false;
|
|
79
|
+
private sourceCache?: { entries: NativeEntry[]; authors: number; source: Sources };
|
|
80
|
+
constructor(private readonly pi: any) {}
|
|
81
|
+
private subject(): Subject {
|
|
82
|
+
const branch = this.store?.reader.getBranch() ?? [];
|
|
83
|
+
return {
|
|
84
|
+
token: this.token,
|
|
85
|
+
revision: this.revision,
|
|
86
|
+
store: this.store,
|
|
87
|
+
leafId: branch.at(-1)?.id ?? null,
|
|
88
|
+
userId: branch.filter((e) => e.message?.role === "user").at(-1)?.id ?? null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
private current(s: Subject): boolean {
|
|
92
|
+
if (s.token !== this.token || s.revision !== this.revision || s.store !== this.store) return false;
|
|
93
|
+
const branch = this.store?.reader.getBranch() ?? [];
|
|
94
|
+
return (
|
|
95
|
+
(!s.leafId || branch.some((e) => e.id === s.leafId)) &&
|
|
96
|
+
(branch.filter((e) => e.message?.role === "user").at(-1)?.id ?? null) === s.userId
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
private guard(s: Subject): void {
|
|
100
|
+
check(
|
|
101
|
+
this.current(s),
|
|
102
|
+
"stale_operation",
|
|
103
|
+
"Routing state changed while the operation was waiting; no further effects applied.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
private taskBasis(state: State, assignmentId: string): string | null | undefined {
|
|
107
|
+
return state.resumeBasis.has(assignmentId)
|
|
108
|
+
? state.resumeBasis.get(assignmentId)
|
|
109
|
+
: state.assignments.get(assignmentId)?.basisUserEntryId;
|
|
110
|
+
}
|
|
111
|
+
private retireUnbound(reason: string): void {
|
|
112
|
+
for (const execution of this.stateData().executions.values()) {
|
|
113
|
+
if (!execution.assistantEntryId && !execution.interrupted)
|
|
114
|
+
this.append({ type: "execution-interrupted", executionId: execution.id, reason });
|
|
115
|
+
}
|
|
116
|
+
this.turn = undefined;
|
|
117
|
+
}
|
|
118
|
+
private openTurn(): void {
|
|
119
|
+
if (!this.turn || this.turn.opened) return;
|
|
120
|
+
const t = this.turn;
|
|
121
|
+
this.append({
|
|
122
|
+
type: "execution-opened",
|
|
123
|
+
execution: {
|
|
124
|
+
id: t.id,
|
|
125
|
+
profile: t.profile,
|
|
126
|
+
assignmentId: t.assignmentId,
|
|
127
|
+
basisUserEntryId: t.basisUserEntryId,
|
|
128
|
+
pair: t.pair,
|
|
129
|
+
resultEntryIds: [],
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
t.opened = true;
|
|
133
|
+
}
|
|
134
|
+
async settled(ctx: any): Promise<void> {
|
|
135
|
+
if (!this.store || !this.supported() || this.store.blocked || !ctx.isIdle?.()) return;
|
|
136
|
+
this.ctx = ctx;
|
|
137
|
+
this.retireUnbound("Native run settled without a complete source binding; prior effects remain unresolved.");
|
|
138
|
+
}
|
|
139
|
+
async beforeRun(ctx: any): Promise<void> {
|
|
140
|
+
this.ctx = ctx;
|
|
141
|
+
if (!this.store || !this.supported() || this.store.blocked || this.error) return;
|
|
142
|
+
if (this.stateData().control === "automatic" && this.stateData().profile === "executor") {
|
|
143
|
+
const result = await this.control("coordinator", false);
|
|
144
|
+
check(result.status !== "blocked", "reconciliation_required", result.reason);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
private stateData(): State {
|
|
148
|
+
return this.store?.state() ?? initialState();
|
|
149
|
+
}
|
|
150
|
+
private sources(state = this.stateData()): Sources {
|
|
151
|
+
const entries = (this.ctx.sessionManager.getBranch() as NativeEntry[]).filter((e) =>
|
|
152
|
+
["message", "custom_message", "compaction", "branch_summary"].includes(e.type),
|
|
153
|
+
);
|
|
154
|
+
const cache = this.sourceCache;
|
|
155
|
+
if (
|
|
156
|
+
cache &&
|
|
157
|
+
cache.authors === state.authors.size &&
|
|
158
|
+
entries.length === cache.entries.length &&
|
|
159
|
+
entries.every((e, i) => e === cache.entries[i])
|
|
160
|
+
)
|
|
161
|
+
return cache.source;
|
|
162
|
+
const active = this.ctx.sessionManager.buildContextEntries?.();
|
|
163
|
+
const activeIds = active ? new Set<string>(active.map((e: NativeEntry) => e.id)) : undefined;
|
|
164
|
+
const source = cache?.source ?? new Sources([], state);
|
|
165
|
+
source.refresh(entries, state, activeIds);
|
|
166
|
+
this.sourceCache = { entries, authors: state.authors.size, source };
|
|
167
|
+
return source;
|
|
168
|
+
}
|
|
169
|
+
private observed(ctx = this.ctx): Pair | undefined {
|
|
170
|
+
return ctx?.model?.provider && ctx?.model?.id && ctx?.thinkingLevel
|
|
171
|
+
? { provider: ctx.model.provider, modelId: ctx.model.id, thinking: ctx.thinkingLevel }
|
|
172
|
+
: undefined;
|
|
173
|
+
}
|
|
174
|
+
private supported(): boolean {
|
|
175
|
+
return !!this.capability?.effective && !this.suppressed;
|
|
176
|
+
}
|
|
177
|
+
state() {
|
|
178
|
+
let state: State;
|
|
179
|
+
try {
|
|
180
|
+
state = this.stateData();
|
|
181
|
+
} catch {
|
|
182
|
+
state = initialState();
|
|
183
|
+
}
|
|
184
|
+
const blocked = this.error ?? this.store?.blocked;
|
|
185
|
+
return {
|
|
186
|
+
effective: this.supported() && state.control !== "inactive" && !blocked,
|
|
187
|
+
activeProfile: state.profile,
|
|
188
|
+
controlMode: state.control === "manual" ? `manual-${state.profile}` : state.control,
|
|
189
|
+
runtimeStatus: blocked
|
|
190
|
+
? ("blocked" as const)
|
|
191
|
+
: this.supported() && state.control !== "inactive"
|
|
192
|
+
? ("active" as const)
|
|
193
|
+
: ("inactive" as const),
|
|
194
|
+
runtimeReason: blocked ?? (this.suppressed ? "startup_selection" : this.capability?.blockingReason.message),
|
|
195
|
+
projectionFailure: this.projectionError,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
get projectionEnabled(): boolean {
|
|
199
|
+
return this.supported() && this.capability?.projection === true && this.stateData().control === "automatic";
|
|
200
|
+
}
|
|
201
|
+
private append(data: EventData, op?: string, step?: string) {
|
|
202
|
+
check(this.store, "routing_unavailable");
|
|
203
|
+
return this.store.append(this.store.make(data, op, step));
|
|
204
|
+
}
|
|
205
|
+
private enqueue<T>(action: () => Promise<T> | T): Promise<T> {
|
|
206
|
+
const token = this.token;
|
|
207
|
+
const result = this.operations.then(() => {
|
|
208
|
+
check(token === this.token, "stale_instance");
|
|
209
|
+
return action();
|
|
210
|
+
});
|
|
211
|
+
this.operations = result.catch(() => {});
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
private mark(error: unknown) {
|
|
215
|
+
if (this.error && error instanceof RoutingError && error.code === "routing_blocked") return;
|
|
216
|
+
this.error =
|
|
217
|
+
error instanceof RoutingError
|
|
218
|
+
? `${error.code}: ${error.message}`
|
|
219
|
+
: error instanceof Error
|
|
220
|
+
? error.message
|
|
221
|
+
: String(error);
|
|
222
|
+
}
|
|
223
|
+
private assertAvailable(profile?: Profile) {
|
|
224
|
+
check(
|
|
225
|
+
this.supported() && this.store && !this.error && !this.store.blocked,
|
|
226
|
+
"routing_unavailable",
|
|
227
|
+
this.error ?? this.store?.blocked ?? "Routing is inactive.",
|
|
228
|
+
);
|
|
229
|
+
const state = this.stateData();
|
|
230
|
+
check(state.control === "automatic", "manual_control");
|
|
231
|
+
if (profile) check(state.profile === profile && this.turn?.profile === profile, "wrong_profile");
|
|
232
|
+
if (state.profile) check(samePair(this.observed(), this.profilePair(state.profile)), "configuration_mismatch");
|
|
233
|
+
}
|
|
234
|
+
private profilePair(profile: Profile): Pair {
|
|
235
|
+
const configured = this.capability?.profiles[profile];
|
|
236
|
+
check(configured, "profile_missing");
|
|
237
|
+
return pairFromProfile(configured);
|
|
238
|
+
}
|
|
239
|
+
private model(profile: Profile) {
|
|
240
|
+
const pair = this.profilePair(profile);
|
|
241
|
+
const model = this.ctx?.modelRegistry?.find(pair.provider, pair.modelId);
|
|
242
|
+
check(model, "profile_unavailable");
|
|
243
|
+
return model;
|
|
244
|
+
}
|
|
245
|
+
async bind(ctx: any, capability: CognitiveRoutingCapabilityState, startup = false): Promise<void> {
|
|
246
|
+
this.token = randomUUID();
|
|
247
|
+
this.ctx = ctx;
|
|
248
|
+
this.capability = capability;
|
|
249
|
+
this.turn = undefined;
|
|
250
|
+
this.messages = [];
|
|
251
|
+
this.sourceCache = undefined;
|
|
252
|
+
this.receipts.clear();
|
|
253
|
+
this.pages.clear();
|
|
254
|
+
this.applying = undefined;
|
|
255
|
+
this.externalChange = false;
|
|
256
|
+
this.error = undefined;
|
|
257
|
+
this.projectionError = undefined;
|
|
258
|
+
this.operations = Promise.resolve();
|
|
259
|
+
this.targetSignature = canonical(capability.profiles);
|
|
260
|
+
this.suppressed = startup && process.argv.some((a) => /^(--model|--thinking)(=|$)/.test(a));
|
|
261
|
+
this.store = new EventStore(this.pi, ctx.sessionManager as SessionReader);
|
|
262
|
+
const subject = this.subject();
|
|
263
|
+
if (!capability.effective || this.suppressed) return;
|
|
264
|
+
try {
|
|
265
|
+
await this.store.reconcile();
|
|
266
|
+
this.guard(subject);
|
|
267
|
+
const state = this.stateData();
|
|
268
|
+
this.manualHold = state.control === "manual" ? state.profile : undefined;
|
|
269
|
+
this.automaticControl = !state.events.size || state.control === "automatic";
|
|
270
|
+
this.retireUnbound("Session rebind found an unfinished execution; no task effects replayed.");
|
|
271
|
+
if (!state.events.size) {
|
|
272
|
+
this.append({
|
|
273
|
+
type: "control",
|
|
274
|
+
control: "automatic",
|
|
275
|
+
profile: "coordinator",
|
|
276
|
+
reason: "Initial routing activation",
|
|
277
|
+
});
|
|
278
|
+
await this.applyPair(this.profilePair("coordinator"));
|
|
279
|
+
} else if (
|
|
280
|
+
state.control === "automatic" &&
|
|
281
|
+
state.profile &&
|
|
282
|
+
!samePair(this.observed(), this.profilePair(state.profile))
|
|
283
|
+
) {
|
|
284
|
+
// Reload reconstructs responsibility; it does not silently reapply the last setter.
|
|
285
|
+
this.append({
|
|
286
|
+
type: "control",
|
|
287
|
+
control: "automatic",
|
|
288
|
+
profile: "coordinator",
|
|
289
|
+
reason: "Rebind reconciles historical responsibility with current host control",
|
|
290
|
+
});
|
|
291
|
+
await this.applyPair(this.profilePair("coordinator"));
|
|
292
|
+
}
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (this.current(subject)) this.mark(error);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
unbind(): void {
|
|
298
|
+
this.token = randomUUID();
|
|
299
|
+
this.store = undefined;
|
|
300
|
+
this.turn = undefined;
|
|
301
|
+
this.messages = [];
|
|
302
|
+
this.ctx = undefined;
|
|
303
|
+
this.sourceCache = undefined;
|
|
304
|
+
this.receipts.clear();
|
|
305
|
+
this.pages.clear();
|
|
306
|
+
}
|
|
307
|
+
async refresh(ctx: any, capability: CognitiveRoutingCapabilityState): Promise<void> {
|
|
308
|
+
this.ctx = ctx;
|
|
309
|
+
const was = this.capability?.effective === true;
|
|
310
|
+
const signature = canonical(capability.profiles);
|
|
311
|
+
if (this.capability && (was !== capability.effective || signature !== this.targetSignature)) this.revision++;
|
|
312
|
+
this.capability = capability;
|
|
313
|
+
if (!capability.effective) {
|
|
314
|
+
this.turn = undefined;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (!this.store) {
|
|
318
|
+
await this.bind(ctx, capability);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (!was) {
|
|
322
|
+
const subject = this.subject();
|
|
323
|
+
this.suppressed = false;
|
|
324
|
+
try {
|
|
325
|
+
await this.store.reconcile();
|
|
326
|
+
this.guard(subject);
|
|
327
|
+
this.error = undefined;
|
|
328
|
+
this.append({
|
|
329
|
+
type: "control",
|
|
330
|
+
control: "automatic",
|
|
331
|
+
profile: "coordinator",
|
|
332
|
+
reason: "Capability re-enabled; Coordinator reconciliation",
|
|
333
|
+
});
|
|
334
|
+
this.automaticControl = true;
|
|
335
|
+
await this.applyPair(this.profilePair("coordinator"));
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (this.current(subject)) this.mark(error);
|
|
338
|
+
}
|
|
339
|
+
} else if (this.targetSignature && signature !== this.targetSignature) {
|
|
340
|
+
this.error = "configuration_changed: reconcile profile configuration before automatic execution";
|
|
341
|
+
}
|
|
342
|
+
if (canonical(this.capability?.profiles) === signature) this.targetSignature = signature;
|
|
343
|
+
}
|
|
344
|
+
async ancestryChanged(ctx: any, navigation = true): Promise<void> {
|
|
345
|
+
this.revision++;
|
|
346
|
+
this.ctx = ctx;
|
|
347
|
+
this.turn = undefined;
|
|
348
|
+
this.messages = [];
|
|
349
|
+
this.sourceCache = undefined;
|
|
350
|
+
this.receipts.clear();
|
|
351
|
+
this.pages.clear();
|
|
352
|
+
if (!this.store || !this.supported()) return;
|
|
353
|
+
const subject = this.subject();
|
|
354
|
+
try {
|
|
355
|
+
await this.store.reconcile();
|
|
356
|
+
this.guard(subject);
|
|
357
|
+
this.error = undefined;
|
|
358
|
+
this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
|
|
359
|
+
const state = this.stateData();
|
|
360
|
+
if (this.manualHold) {
|
|
361
|
+
this.append({
|
|
362
|
+
type: "control",
|
|
363
|
+
control: "manual",
|
|
364
|
+
profile: this.manualHold,
|
|
365
|
+
reason: "Current explicit manual hold survives navigation",
|
|
366
|
+
});
|
|
367
|
+
} else if (
|
|
368
|
+
(this.automaticControl || !state.events.size) &&
|
|
369
|
+
(navigation || !samePair(this.observed(), this.profilePair(state.profile ?? "coordinator")))
|
|
370
|
+
) {
|
|
371
|
+
this.append({
|
|
372
|
+
type: "control",
|
|
373
|
+
control: "automatic",
|
|
374
|
+
profile: "coordinator",
|
|
375
|
+
reason: "Coordinator reconciles selected native ancestry",
|
|
376
|
+
});
|
|
377
|
+
await this.applyPair(this.profilePair("coordinator"));
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (this.current(subject)) this.mark(error);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async nativeChange(ctx: any): Promise<void> {
|
|
384
|
+
this.ctx = ctx;
|
|
385
|
+
if (!this.supported() || !this.store) return;
|
|
386
|
+
if (this.applying) {
|
|
387
|
+
if (ctx.model?.provider !== this.applying.provider || ctx.model?.id !== this.applying.modelId) {
|
|
388
|
+
this.externalChange = true;
|
|
389
|
+
this.revision++;
|
|
390
|
+
}
|
|
391
|
+
return; // Never await a queue already held by our setter.
|
|
392
|
+
}
|
|
393
|
+
const state = this.stateData();
|
|
394
|
+
if (state.control !== "automatic" || !state.profile || samePair(this.observed(), this.profilePair(state.profile)))
|
|
395
|
+
return;
|
|
396
|
+
try {
|
|
397
|
+
this.revision++;
|
|
398
|
+
this.manualHold = undefined;
|
|
399
|
+
this.automaticControl = false;
|
|
400
|
+
this.append({ type: "control", control: "inactive", reason: "External native model/effort change" });
|
|
401
|
+
} catch (error) {
|
|
402
|
+
this.mark(error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
private async applyPair(target: Pair): Promise<void> {
|
|
406
|
+
const subject = this.subject(),
|
|
407
|
+
token = this.token,
|
|
408
|
+
prior = this.observed();
|
|
409
|
+
if (samePair(prior, target)) return;
|
|
410
|
+
const model = this.ctx.modelRegistry.find(target.provider, target.modelId);
|
|
411
|
+
check(model, "profile_unavailable");
|
|
412
|
+
const auth = await this.ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
413
|
+
this.guard(subject);
|
|
414
|
+
check(auth?.ok, "profile_unauthenticated");
|
|
415
|
+
this.applying = target;
|
|
416
|
+
let ownedPair = target;
|
|
417
|
+
this.externalChange = false;
|
|
418
|
+
try {
|
|
419
|
+
check(await this.pi.setModel(model), "model_rejected");
|
|
420
|
+
this.guard(subject);
|
|
421
|
+
this.pi.setThinkingLevel(target.thinking);
|
|
422
|
+
check(!this.externalChange && samePair(this.observed(), target), "configuration_mismatch");
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (this.current(subject) && prior && !this.externalChange) {
|
|
425
|
+
const old = this.ctx.modelRegistry.find(prior.provider, prior.modelId);
|
|
426
|
+
try {
|
|
427
|
+
this.applying = prior;
|
|
428
|
+
ownedPair = prior;
|
|
429
|
+
check(old && (await this.pi.setModel(old)), "rollback_failed");
|
|
430
|
+
this.guard(subject);
|
|
431
|
+
this.pi.setThinkingLevel(prior.thinking);
|
|
432
|
+
check(samePair(this.observed(), prior), "rollback_failed");
|
|
433
|
+
} catch {
|
|
434
|
+
if (this.current(subject)) this.error = "configuration_unknown: rollback could not be observed";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
throw error;
|
|
438
|
+
} finally {
|
|
439
|
+
if (token === this.token && this.applying === ownedPair) this.applying = undefined;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
private async control(profile: Profile, manual: boolean): Promise<{ status: string; reason?: string }> {
|
|
443
|
+
this.revision++;
|
|
444
|
+
const subject = this.subject();
|
|
445
|
+
return this.enqueue(async () => {
|
|
446
|
+
try {
|
|
447
|
+
check(this.capability?.effective && this.store, "routing_unavailable");
|
|
448
|
+
await this.store.reconcile();
|
|
449
|
+
this.guard(subject);
|
|
450
|
+
this.error = undefined;
|
|
451
|
+
this.suppressed = false;
|
|
452
|
+
const previous = this.stateData();
|
|
453
|
+
if (
|
|
454
|
+
previous.control === (manual ? "manual" : "automatic") &&
|
|
455
|
+
previous.profile === profile &&
|
|
456
|
+
samePair(this.observed(), this.profilePair(profile))
|
|
457
|
+
) {
|
|
458
|
+
this.manualHold = manual ? profile : undefined;
|
|
459
|
+
this.automaticControl = !manual;
|
|
460
|
+
return { status: manual ? "active" : "automatic" };
|
|
461
|
+
}
|
|
462
|
+
this.append({
|
|
463
|
+
type: "control",
|
|
464
|
+
control: manual ? "manual" : "automatic",
|
|
465
|
+
profile,
|
|
466
|
+
reason: manual ? "Explicit user manual hold" : "Explicit user automatic release/reconciliation",
|
|
467
|
+
});
|
|
468
|
+
try {
|
|
469
|
+
await this.applyPair(this.profilePair(profile));
|
|
470
|
+
this.guard(subject);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if (this.current(subject) && !this.error && !this.store.blocked)
|
|
473
|
+
this.append({
|
|
474
|
+
type: "control",
|
|
475
|
+
control: previous.control,
|
|
476
|
+
profile: previous.profile,
|
|
477
|
+
reason: "Control request failed; prior control retained",
|
|
478
|
+
});
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
this.manualHold = manual ? profile : undefined;
|
|
482
|
+
this.automaticControl = !manual;
|
|
483
|
+
return { status: manual ? "active" : "automatic" };
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (this.current(subject)) this.mark(error);
|
|
486
|
+
return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
setManualProfile(profile: Profile, _mechanism?: string) {
|
|
491
|
+
return this.control(profile, true);
|
|
492
|
+
}
|
|
493
|
+
setAutomaticControl(_mechanism?: string) {
|
|
494
|
+
return this.control("coordinator", false);
|
|
495
|
+
}
|
|
496
|
+
private lastDeliveredUser(sources: Sources, messages: any[]): string | null {
|
|
497
|
+
const delivered = new Set(
|
|
498
|
+
sources.associate(messages).flatMap((x) => (x.source?.message.role === "user" ? [x.source.entry.id] : [])),
|
|
499
|
+
);
|
|
500
|
+
// Compaction may remove every user message from the active view. Retain
|
|
501
|
+
// observed delivery on this ancestry; a stored but undelivered user entry
|
|
502
|
+
// must not advance the basis, and absence must not manufacture new input.
|
|
503
|
+
for (const execution of this.stateData().executions.values())
|
|
504
|
+
if (execution.basisUserEntryId) delivered.add(execution.basisUserEntryId);
|
|
505
|
+
for (let i = sources.entries.length - 1; i >= 0; i--) {
|
|
506
|
+
const entry = sources.entries[i];
|
|
507
|
+
if (entry.message?.role === "user" && delivered.has(entry.id)) return entry.id;
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
private runtimeMessage(state: State, attention = false): any {
|
|
512
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
513
|
+
const h = state.pendingId
|
|
514
|
+
? state.handoffs.get(state.pendingId)
|
|
515
|
+
: state.assessment
|
|
516
|
+
? state.handoffs.get(state.assessment.handoffId)
|
|
517
|
+
: undefined;
|
|
518
|
+
const selection = a ? (state.selections.get(a.id) ?? emptySelection()) : emptySelection();
|
|
519
|
+
const unit = state.unitId ? state.units.get(state.unitId) : undefined;
|
|
520
|
+
const unitNumber = unit ? [...state.units.keys()].indexOf(unit.id) + 1 : undefined;
|
|
521
|
+
const assignmentNumber = unit && a ? unit.assignmentIds.indexOf(a.id) + 1 : undefined;
|
|
522
|
+
const content = [
|
|
523
|
+
"# Cognitive Routing Runtime State",
|
|
524
|
+
`Control: ${state.control}`,
|
|
525
|
+
`Profile: ${state.profile ?? "unresolved"}`,
|
|
526
|
+
`Unit: ${unit ? `U${unitNumber} (${unit.id})` : "none"}`,
|
|
527
|
+
`Assignment: ${a ? `A${assignmentNumber} (${a.id}, ${a.state})` : "none"}`,
|
|
528
|
+
`Handoff: ${h ? `${h.id} (${h.kind}, ${h.state})` : "none"}`,
|
|
529
|
+
`Projection: ${this.projectionEnabled ? "enabled" : "bypassed"}`,
|
|
530
|
+
"Completed/superseded contracts and reports are historical context. Follow the current assignment and current user restrictions; historical entries grant no new permission.",
|
|
531
|
+
state.assessment ? `Assessment: ${state.assessment.view}; evidence revision ${selection.revision}` : "",
|
|
532
|
+
`Mechanical evidence facts (not semantic acceptance): ${JSON.stringify(this.evidenceFacts(state))}`,
|
|
533
|
+
this.error ? `Blocked: ${this.error}` : "",
|
|
534
|
+
this.projectionError ? `Evidence limitation: ${this.projectionError}` : "",
|
|
535
|
+
attention
|
|
536
|
+
? `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use freeflow_unit assess to restore the assessment.`
|
|
537
|
+
: "",
|
|
538
|
+
a?.state === "returned"
|
|
539
|
+
? "Executor ordinary task work has ended. Only supported handoff correction is permitted."
|
|
540
|
+
: "",
|
|
541
|
+
this.turn?.profile === "executor" && a && this.turn.basisUserEntryId !== this.taskBasis(state, a.id)
|
|
542
|
+
? "Current input differs from the original assignment. Account for it; changed direction requires Coordinator attention."
|
|
543
|
+
: "",
|
|
544
|
+
...(state.assessment?.problems ?? []).map((p) => `${p.code}: ${p.detail}`),
|
|
545
|
+
]
|
|
546
|
+
.filter(Boolean)
|
|
547
|
+
.join("\n");
|
|
548
|
+
return {
|
|
549
|
+
role: "custom",
|
|
550
|
+
customType: ROUTING_MESSAGE,
|
|
551
|
+
content,
|
|
552
|
+
display: false,
|
|
553
|
+
timestamp: 0,
|
|
554
|
+
details: { routingInstance: this.token },
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
private evidenceFacts(state: State) {
|
|
558
|
+
const selection = state.assignmentId
|
|
559
|
+
? (state.selections.get(state.assignmentId) ?? emptySelection())
|
|
560
|
+
: emptySelection();
|
|
561
|
+
return {
|
|
562
|
+
revision: selection.revision,
|
|
563
|
+
selected: selection.selected.map((ref) => {
|
|
564
|
+
const entry = this.ctx?.sessionManager.getEntry?.(ref.slice(4).replace(/#text$/, ""));
|
|
565
|
+
return {
|
|
566
|
+
ref,
|
|
567
|
+
kind: ref.endsWith("#text") ? "assistant-text" : (entry?.message?.role ?? "unknown"),
|
|
568
|
+
toolName: entry?.message?.toolName,
|
|
569
|
+
producer: state.authors.get(entry?.id)?.profile ?? "common",
|
|
570
|
+
assignment: state.authors.get(entry?.id)?.assignmentId,
|
|
571
|
+
};
|
|
572
|
+
}),
|
|
573
|
+
unresolved: selection.unresolved,
|
|
574
|
+
withdrawals: selection.withdrawals,
|
|
575
|
+
preparedRevision: state.assessment?.reservation?.selectionRevision,
|
|
576
|
+
limitations: state.assessment?.problems ?? [],
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
private prepared(profile: Profile, input: any[], handoffId?: string, restoring = false): PreparedView {
|
|
580
|
+
const state = this.stateData(),
|
|
581
|
+
model = this.model(profile);
|
|
582
|
+
return prepareView({
|
|
583
|
+
messages: input,
|
|
584
|
+
sources: this.sources(state),
|
|
585
|
+
state,
|
|
586
|
+
view: profile,
|
|
587
|
+
projection: this.projectionEnabled,
|
|
588
|
+
model,
|
|
589
|
+
pair: this.profilePair(profile),
|
|
590
|
+
systemPrompt: this.ctx.getSystemPrompt?.() ?? "",
|
|
591
|
+
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
|
+
)
|
|
600
|
+
.map((tool: any) =>
|
|
601
|
+
ROUTING_TOOLS.includes(tool.name)
|
|
602
|
+
? { ...tool, parameters: schemaForProfile(tool.name, profile === "coordinator") }
|
|
603
|
+
: tool,
|
|
604
|
+
),
|
|
605
|
+
runtimeMessage: this.runtimeMessage(
|
|
606
|
+
state,
|
|
607
|
+
profile === "coordinator" && state.assessment?.view === "suspended" && !restoring && !handoffId,
|
|
608
|
+
),
|
|
609
|
+
instance: this.token,
|
|
610
|
+
preparingReturn: handoffId,
|
|
611
|
+
restoring,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
async context(ctx: any, incoming: any[]): Promise<any[]> {
|
|
615
|
+
this.ctx = ctx;
|
|
616
|
+
const input = incoming.filter(
|
|
617
|
+
(m) =>
|
|
618
|
+
!(
|
|
619
|
+
m?.details?.routingInstance === this.token &&
|
|
620
|
+
[ROUTING_MESSAGE, "freeflow-routing-v2-refs"].includes(m.customType)
|
|
621
|
+
),
|
|
622
|
+
);
|
|
623
|
+
this.messages = input;
|
|
624
|
+
if (!this.supported() || !this.store) return input;
|
|
625
|
+
try {
|
|
626
|
+
const state = this.stateData();
|
|
627
|
+
if (state.control !== "automatic") return input;
|
|
628
|
+
check(!this.error && !this.store.blocked && state.profile, "routing_blocked", this.error ?? this.store.blocked);
|
|
629
|
+
check(samePair(this.observed(), this.profilePair(state.profile!)), "prepared_pair_mismatch");
|
|
630
|
+
const sources = this.sources(state),
|
|
631
|
+
user = this.lastDeliveredUser(sources, input);
|
|
632
|
+
if (state.assessment && user && user !== state.assessment.basisUserEntryId)
|
|
633
|
+
this.append(
|
|
634
|
+
{
|
|
635
|
+
type: "assessment-suspended",
|
|
636
|
+
handoffId: state.assessment.handoffId,
|
|
637
|
+
reason: "user-attention",
|
|
638
|
+
basisUserEntryId: user,
|
|
639
|
+
problems: [],
|
|
640
|
+
},
|
|
641
|
+
JSON.stringify(["attention", state.assessment.handoffId, user]),
|
|
642
|
+
);
|
|
643
|
+
// New input reaching an already prepared Executor request cannot be rerouted by changing only the live model.
|
|
644
|
+
const outstanding = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
645
|
+
const interrupted =
|
|
646
|
+
state.profile === "executor" && user !== (outstanding ? this.taskBasis(state, outstanding.id) : undefined);
|
|
647
|
+
if (!this.turn || this.turn.bound) {
|
|
648
|
+
this.receipts.clear();
|
|
649
|
+
const execution: Execution = {
|
|
650
|
+
id: randomUUID(),
|
|
651
|
+
profile: state.profile!,
|
|
652
|
+
assignmentId: state.assignmentId,
|
|
653
|
+
basisUserEntryId: user,
|
|
654
|
+
pair: this.observed()!,
|
|
655
|
+
resultEntryIds: [],
|
|
656
|
+
};
|
|
657
|
+
const before = new Set((ctx.sessionManager.getBranch() as NativeEntry[]).map((e) => e.id));
|
|
658
|
+
this.turn = {
|
|
659
|
+
id: execution.id,
|
|
660
|
+
profile: execution.profile,
|
|
661
|
+
assignmentId: execution.assignmentId,
|
|
662
|
+
basisUserEntryId: user,
|
|
663
|
+
before,
|
|
664
|
+
pair: execution.pair,
|
|
665
|
+
};
|
|
666
|
+
this.openTurn();
|
|
667
|
+
}
|
|
668
|
+
let prepared = this.prepared(state.profile!, input);
|
|
669
|
+
if (!prepared.ready && state.profile === "coordinator" && this.stateData().assessment) {
|
|
670
|
+
const assessment = this.stateData().assessment!;
|
|
671
|
+
this.append({
|
|
672
|
+
type: "assessment-suspended",
|
|
673
|
+
handoffId: assessment.handoffId,
|
|
674
|
+
reason: "delivery-gap",
|
|
675
|
+
basisUserEntryId: user,
|
|
676
|
+
problems: prepared.problems,
|
|
677
|
+
});
|
|
678
|
+
this.projectionError = prepared.problems.map((p) => p.code).join(", ");
|
|
679
|
+
prepared = this.prepared("coordinator", input);
|
|
680
|
+
}
|
|
681
|
+
check(prepared.ready, "context_unavailable", prepared.problems.map((p) => p.detail).join("; "));
|
|
682
|
+
const known = this.stateData().exposure;
|
|
683
|
+
const added = prepared.fullSources.filter((s) => known.get(s.ref) !== s.hash);
|
|
684
|
+
if (this.turn.profile === "executor")
|
|
685
|
+
for (let i = 0; i < added.length; i += 128)
|
|
686
|
+
this.append({
|
|
687
|
+
type: "sources-exposed",
|
|
688
|
+
executionId: this.turn.id,
|
|
689
|
+
view: "executor",
|
|
690
|
+
sources: added.slice(i, i + 128).map((s) => ({ ref: s.ref, bodyHash: s.hash })),
|
|
691
|
+
});
|
|
692
|
+
if (interrupted)
|
|
693
|
+
prepared.messages.push({
|
|
694
|
+
role: "custom",
|
|
695
|
+
customType: ROUTING_MESSAGE,
|
|
696
|
+
display: false,
|
|
697
|
+
timestamp: 0,
|
|
698
|
+
details: { routingInstance: this.token },
|
|
699
|
+
content:
|
|
700
|
+
"New delivered user input requires Coordinator attention. Do not run ordinary task tools; return the current partial result.",
|
|
701
|
+
});
|
|
702
|
+
return prepared.messages;
|
|
703
|
+
} catch (error) {
|
|
704
|
+
this.mark(error);
|
|
705
|
+
ctx.abort?.();
|
|
706
|
+
// Do not send an accidental full Executor history on a projection failure.
|
|
707
|
+
return [
|
|
708
|
+
{
|
|
709
|
+
role: "custom",
|
|
710
|
+
customType: ROUTING_MESSAGE,
|
|
711
|
+
content: `Automatic request blocked: ${this.error}`,
|
|
712
|
+
display: false,
|
|
713
|
+
timestamp: 0,
|
|
714
|
+
details: { routingInstance: this.token },
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
messageEnd(message: any): void {
|
|
720
|
+
if (message?.role === "assistant" && this.turn && !this.turn.bound) this.turn.message = structuredClone(message);
|
|
721
|
+
}
|
|
722
|
+
private contextOperation(name: string, input: any): boolean {
|
|
723
|
+
return name === "freeflow_context" && ["archive", "restore", "search", "retrieve"].includes(input?.operation);
|
|
724
|
+
}
|
|
725
|
+
private batch(callId: string, name: string): void {
|
|
726
|
+
check(this.turn?.message, "batch_unavailable");
|
|
727
|
+
const matches = (this.ctx.sessionManager.getBranch() as NativeEntry[]).filter(
|
|
728
|
+
(e) =>
|
|
729
|
+
!this.turn!.before.has(e.id) &&
|
|
730
|
+
e.type === "message" &&
|
|
731
|
+
e.message?.role === "assistant" &&
|
|
732
|
+
e.message.content?.some((b: any) => b.type === "toolCall" && b.id === callId),
|
|
733
|
+
);
|
|
734
|
+
check(matches.length === 1, "batch_source_changed");
|
|
735
|
+
const fingerprint = bodyHash(matches[0].message);
|
|
736
|
+
check(!this.turn.sourceFingerprint || this.turn.sourceFingerprint === fingerprint, "batch_source_changed");
|
|
737
|
+
// Later message_end handlers may legitimately replace the message. The first
|
|
738
|
+
// preflight freezes the actually persisted batch before any of its tools act.
|
|
739
|
+
this.turn.sourceFingerprint = fingerprint;
|
|
740
|
+
this.turn.message = structuredClone(matches[0].message);
|
|
741
|
+
const calls = (this.turn.message.content ?? []).filter((b: any) => b.type === "toolCall");
|
|
742
|
+
const handoffs = calls.filter((b: any) => HANDOFF_TOOLS.has(b.name));
|
|
743
|
+
check(
|
|
744
|
+
!handoffs.length ||
|
|
745
|
+
(handoffs.length === 1 &&
|
|
746
|
+
HANDOFF_TOOLS.has(calls.at(-1)?.name) &&
|
|
747
|
+
calls.every(
|
|
748
|
+
(b: any) =>
|
|
749
|
+
HANDOFF_TOOLS.has(b.name) || b.name === "freeflow_project" || this.contextOperation(b.name, b.arguments),
|
|
750
|
+
)),
|
|
751
|
+
"invalid_handoff_batch",
|
|
752
|
+
"A handoff must be last and cannot accompany ordinary task tools.",
|
|
753
|
+
);
|
|
754
|
+
check(
|
|
755
|
+
calls.some((b: any) => b.id === callId && b.name === name),
|
|
756
|
+
"call_not_in_batch",
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
preflight(event: any, ctx: any): any {
|
|
760
|
+
this.ctx = ctx;
|
|
761
|
+
const name = event.toolName,
|
|
762
|
+
isRouting = ROUTING_TOOLS.includes(name);
|
|
763
|
+
if (!this.supported()) return isRouting ? { block: true, reason: "Routing is unavailable." } : undefined;
|
|
764
|
+
try {
|
|
765
|
+
const state = this.stateData();
|
|
766
|
+
if (state.control === "manual")
|
|
767
|
+
return isRouting && name !== "freeflow_unit" ? { block: true, reason: "Manual control is active." } : undefined;
|
|
768
|
+
if (state.control !== "automatic")
|
|
769
|
+
return isRouting ? { block: true, reason: "Automatic routing is inactive." } : undefined;
|
|
770
|
+
this.batch(event.toolCallId, name);
|
|
771
|
+
check(!this.error && !this.store?.blocked, "routing_blocked");
|
|
772
|
+
this.openTurn();
|
|
773
|
+
if (this.turn?.profile === "executor" && !isRouting && !this.contextOperation(name, event.input)) {
|
|
774
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
775
|
+
check(
|
|
776
|
+
a?.state === "outstanding" &&
|
|
777
|
+
this.turn.basisUserEntryId === this.taskBasis(state, a.id) &&
|
|
778
|
+
state.profile === "executor",
|
|
779
|
+
"executor_task_phase_ended",
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
return undefined;
|
|
783
|
+
} catch (error) {
|
|
784
|
+
return { block: true, reason: error instanceof Error ? error.message : String(error) };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
private callOperation(callId: string, name: string): string {
|
|
788
|
+
check(this.turn && this.store, "execution_missing");
|
|
789
|
+
this.batch(callId, name);
|
|
790
|
+
return JSON.stringify([this.store.reader.getSessionId(), this.turn.id, callId, name]);
|
|
791
|
+
}
|
|
792
|
+
private result(value: any): any {
|
|
793
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }], details: value };
|
|
794
|
+
}
|
|
795
|
+
async invoke(name: string, callId: string, input: any, signal: AbortSignal | undefined, ctx: any): Promise<any> {
|
|
796
|
+
return this.enqueue(async () => {
|
|
797
|
+
this.ctx = ctx;
|
|
798
|
+
try {
|
|
799
|
+
check(!signal?.aborted, "cancelled");
|
|
800
|
+
if (name === "freeflow_unit" && input.operation === "inspect") return this.result(this.inspectUnit(input));
|
|
801
|
+
this.assertAvailable(name === "freeflow_delegate" || name === "freeflow_unit" ? "coordinator" : "executor");
|
|
802
|
+
const op = this.callOperation(callId, name);
|
|
803
|
+
const cached = this.receipts.get(op);
|
|
804
|
+
if (cached) {
|
|
805
|
+
check(cached.input === canonical(input), "operation_conflict");
|
|
806
|
+
return this.result(structuredClone(cached.value));
|
|
807
|
+
}
|
|
808
|
+
const value =
|
|
809
|
+
name === "freeflow_delegate"
|
|
810
|
+
? this.delegate(input, callId, op)
|
|
811
|
+
: name === "freeflow_return"
|
|
812
|
+
? this.returnReport(input, callId, op)
|
|
813
|
+
: name === "freeflow_project"
|
|
814
|
+
? this.project(input, op)
|
|
815
|
+
: name === "freeflow_unit"
|
|
816
|
+
? this.unit(input, op)
|
|
817
|
+
: undefined;
|
|
818
|
+
if (value !== undefined) {
|
|
819
|
+
this.receipts.set(op, { input: canonical(input), value: structuredClone(value) });
|
|
820
|
+
return this.result(value);
|
|
821
|
+
}
|
|
822
|
+
throw new RoutingError("unknown_tool", name);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
return this.result({
|
|
825
|
+
status: "blocked",
|
|
826
|
+
code: error instanceof RoutingError ? error.code : "operation_failed",
|
|
827
|
+
message: error instanceof Error ? error.message : String(error),
|
|
828
|
+
problems: error instanceof RoutingError ? error.problems : [],
|
|
829
|
+
observed: this.observed(),
|
|
830
|
+
expectedProfile:
|
|
831
|
+
name === "freeflow_unit" && input.operation === "inspect"
|
|
832
|
+
? undefined
|
|
833
|
+
: name === "freeflow_delegate" || name === "freeflow_unit"
|
|
834
|
+
? "coordinator"
|
|
835
|
+
: "executor",
|
|
836
|
+
recoveryAction:
|
|
837
|
+
error instanceof RoutingError &&
|
|
838
|
+
[
|
|
839
|
+
"cursor_expired",
|
|
840
|
+
"invalid_cursor",
|
|
841
|
+
"work_ref_required",
|
|
842
|
+
"invalid_work_ref",
|
|
843
|
+
"work_unavailable",
|
|
844
|
+
"invalid_inspection",
|
|
845
|
+
].includes(error.code)
|
|
846
|
+
? "Inspect again without a cursor and use a returned work ref. Lookup recovery does not require a new assignment."
|
|
847
|
+
: "Preserve saved work. Use freeflow_unit inspect; reconcile current input/control before retrying the named operation.",
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
private duplicate(op: string): any {
|
|
853
|
+
return [...this.stateData().events.values()].find(
|
|
854
|
+
(e) =>
|
|
855
|
+
e.operationId === op &&
|
|
856
|
+
["delegate-accepted", "return-accepted", "handoff-retry-requested"].includes(e.data.type),
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
private delegate(input: any, callId: string, op: string) {
|
|
860
|
+
const prior = this.duplicate(op);
|
|
861
|
+
if (prior) {
|
|
862
|
+
check(
|
|
863
|
+
prior.data.type === "delegate-accepted" && prior.data.assignment.contract === input.contract,
|
|
864
|
+
"operation_conflict",
|
|
865
|
+
);
|
|
866
|
+
return { status: "accepted", handoff: prior.data.handoff.id, unchanged: true };
|
|
867
|
+
}
|
|
868
|
+
const state = this.stateData();
|
|
869
|
+
const unit = state.unitId
|
|
870
|
+
? state.units.get(state.unitId)!
|
|
871
|
+
: { id: randomUUID(), objective: input.contract, state: "open" as const, assignmentIds: [] };
|
|
872
|
+
const old = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
873
|
+
if (input.operation === "replace") check(old?.state === "outstanding" && !this.applying, "no_quiescent_assignment");
|
|
874
|
+
else
|
|
875
|
+
check(input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId, "assignment_outstanding");
|
|
876
|
+
const assignmentId = randomUUID(),
|
|
877
|
+
id = randomUUID();
|
|
878
|
+
const assignment = {
|
|
879
|
+
id: assignmentId,
|
|
880
|
+
unitId: unit.id,
|
|
881
|
+
contract: input.contract,
|
|
882
|
+
state: "outstanding" as const,
|
|
883
|
+
delegateHandoffId: id,
|
|
884
|
+
basisUserEntryId: this.turn!.basisUserEntryId,
|
|
885
|
+
};
|
|
886
|
+
const h: Handoff = {
|
|
887
|
+
id,
|
|
888
|
+
kind: "delegate",
|
|
889
|
+
assignmentId,
|
|
890
|
+
text: input.contract,
|
|
891
|
+
from: "coordinator",
|
|
892
|
+
to: "executor",
|
|
893
|
+
executionId: this.turn!.id,
|
|
894
|
+
toolCallId: callId,
|
|
895
|
+
basisUserEntryId: this.turn!.basisUserEntryId,
|
|
896
|
+
state: "pending",
|
|
897
|
+
reportRevision: 0,
|
|
898
|
+
limitations: [],
|
|
899
|
+
};
|
|
900
|
+
this.append(
|
|
901
|
+
{
|
|
902
|
+
type: "delegate-accepted",
|
|
903
|
+
unit: { ...unit, assignmentIds: [...unit.assignmentIds, assignmentId] },
|
|
904
|
+
assignment,
|
|
905
|
+
handoff: h,
|
|
906
|
+
...(input.operation === "replace"
|
|
907
|
+
? {
|
|
908
|
+
replacement: {
|
|
909
|
+
assignmentId: old!.id,
|
|
910
|
+
reason: input.reason,
|
|
911
|
+
...(state.pendingId ? { supersededHandoffId: state.pendingId } : {}),
|
|
912
|
+
},
|
|
913
|
+
}
|
|
914
|
+
: {}),
|
|
915
|
+
},
|
|
916
|
+
op,
|
|
917
|
+
);
|
|
918
|
+
return {
|
|
919
|
+
status: "accepted",
|
|
920
|
+
unit: unit.id,
|
|
921
|
+
assignment: assignmentId,
|
|
922
|
+
handoff: id,
|
|
923
|
+
transition: "pending",
|
|
924
|
+
contract: input.contract,
|
|
925
|
+
reason: input.reason,
|
|
926
|
+
createdUnit: !state.unitId,
|
|
927
|
+
...(old && input.operation === "replace"
|
|
928
|
+
? {
|
|
929
|
+
replacedAssignment: old.id,
|
|
930
|
+
continuity:
|
|
931
|
+
"Prior effects and evidence remain on native history. Reconcile uncertain effects before dependent work.",
|
|
932
|
+
}
|
|
933
|
+
: {}),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
private returnReport(input: any, callId: string, op: string) {
|
|
937
|
+
const state = this.stateData(),
|
|
938
|
+
a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
939
|
+
check(a, "assignment_missing");
|
|
940
|
+
const saved = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
941
|
+
const selection = state.selections.get(a.id) ?? emptySelection();
|
|
942
|
+
const duplicate = this.duplicate(op);
|
|
943
|
+
if (duplicate) {
|
|
944
|
+
if (input.operation === "retry") check(duplicate.data.type === "handoff-retry-requested", "operation_conflict");
|
|
945
|
+
else
|
|
946
|
+
check(
|
|
947
|
+
duplicate.data.type === "return-accepted" &&
|
|
948
|
+
duplicate.data.handoff.text === input.report &&
|
|
949
|
+
duplicate.data.handoff.outcome === input.outcome &&
|
|
950
|
+
canonical(duplicate.data.handoff.limitations) === canonical(input.limitations ?? []),
|
|
951
|
+
"operation_conflict",
|
|
952
|
+
);
|
|
953
|
+
return {
|
|
954
|
+
status: "accepted",
|
|
955
|
+
reportSaved: true,
|
|
956
|
+
unchanged: true,
|
|
957
|
+
handoff: duplicate.data.handoffId ?? duplicate.data.handoff.id,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
if (input.operation === "retry") {
|
|
961
|
+
check(saved && ["blocked", "pending"].includes(saved.state), "saved_return_missing");
|
|
962
|
+
this.append(
|
|
963
|
+
{
|
|
964
|
+
type: "handoff-retry-requested",
|
|
965
|
+
handoffId: saved.id,
|
|
966
|
+
attemptId: randomUUID(),
|
|
967
|
+
executionId: this.turn!.id,
|
|
968
|
+
toolCallId: callId,
|
|
969
|
+
reportRevision: saved.reportRevision,
|
|
970
|
+
selectionRevision: selection.revision,
|
|
971
|
+
},
|
|
972
|
+
op,
|
|
973
|
+
);
|
|
974
|
+
return { ...this.returnReadiness(saved.id), reportUnchanged: true };
|
|
975
|
+
}
|
|
976
|
+
check(input.operation === "submit", "invalid_return_operation");
|
|
977
|
+
check(
|
|
978
|
+
a.state === "outstanding" || (a.state === "returned" && saved && ["pending", "blocked"].includes(saved.state)),
|
|
979
|
+
"assignment_task_ended",
|
|
980
|
+
);
|
|
981
|
+
const h: Handoff = {
|
|
982
|
+
id: saved?.id ?? randomUUID(),
|
|
983
|
+
kind: "return",
|
|
984
|
+
assignmentId: a.id,
|
|
985
|
+
text: input.report,
|
|
986
|
+
from: "executor",
|
|
987
|
+
to: "coordinator",
|
|
988
|
+
executionId: this.turn!.id,
|
|
989
|
+
toolCallId: callId,
|
|
990
|
+
basisUserEntryId: this.turn!.basisUserEntryId,
|
|
991
|
+
state: "pending",
|
|
992
|
+
reportRevision: (saved?.reportRevision ?? 0) + 1,
|
|
993
|
+
outcome: input.outcome,
|
|
994
|
+
limitations: input.limitations ?? [],
|
|
995
|
+
};
|
|
996
|
+
this.append({ type: "return-accepted", handoff: h }, op);
|
|
997
|
+
return this.returnReadiness(h.id);
|
|
998
|
+
}
|
|
999
|
+
private returnReadiness(id: string) {
|
|
1000
|
+
const h = this.stateData().handoffs.get(id)!;
|
|
1001
|
+
const prepared = this.prepared("coordinator", this.messages, id);
|
|
1002
|
+
return {
|
|
1003
|
+
status: "accepted",
|
|
1004
|
+
reportSaved: true,
|
|
1005
|
+
handoff: id,
|
|
1006
|
+
reportRevision: h.reportRevision,
|
|
1007
|
+
assignmentRef: `assignment:${h.assignmentId}`,
|
|
1008
|
+
reportRef: `report:${h.id}:${h.reportRevision}`,
|
|
1009
|
+
producer: "executor",
|
|
1010
|
+
outcome: h.outcome,
|
|
1011
|
+
limitations: h.limitations,
|
|
1012
|
+
ready: prepared.ready,
|
|
1013
|
+
transition: prepared.ready ? "pending" : "blocked",
|
|
1014
|
+
problems: prepared.problems,
|
|
1015
|
+
warnings: prepared.warnings,
|
|
1016
|
+
stage: "completed-input preparation",
|
|
1017
|
+
finalExchangePending: true,
|
|
1018
|
+
provisional:
|
|
1019
|
+
"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(this.stateData()),
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
private project(input: any, op: string) {
|
|
1025
|
+
check(this.projectionEnabled, "projection_disabled");
|
|
1026
|
+
const state = this.stateData(),
|
|
1027
|
+
a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1028
|
+
check(a, "assignment_missing");
|
|
1029
|
+
const sources = this.sources(state);
|
|
1030
|
+
sources.associate(this.messages);
|
|
1031
|
+
const selection = state.selections.get(a.id) ?? emptySelection();
|
|
1032
|
+
let next = selection;
|
|
1033
|
+
if (input.operation !== "inspect") {
|
|
1034
|
+
check(["add", "remove"].includes(input.operation), "invalid_projection_operation");
|
|
1035
|
+
next = changeSelection(selection, input, sources, state);
|
|
1036
|
+
if (next !== selection) this.append({ type: "selection-changed", assignmentId: a.id, selection: next }, op);
|
|
1037
|
+
}
|
|
1038
|
+
const prepared = this.prepared("coordinator", this.messages, a.returnHandoffId);
|
|
1039
|
+
const scope = input.scope ?? "assignment";
|
|
1040
|
+
let page: any;
|
|
1041
|
+
if (input.operation === "inspect") {
|
|
1042
|
+
const model = this.model("coordinator");
|
|
1043
|
+
const checksFor = (s: Source) => {
|
|
1044
|
+
const eligibility = sources.selectionProblem(s.ref, state);
|
|
1045
|
+
const limitations = [
|
|
1046
|
+
...(eligibility ? [eligibility] : []),
|
|
1047
|
+
...representationProblems(s, model),
|
|
1048
|
+
...sources.exchange(s).problems,
|
|
1049
|
+
];
|
|
1050
|
+
return { eligible: !eligibility, targetReady: !limitations.length, limitations };
|
|
1051
|
+
};
|
|
1052
|
+
const rows = () =>
|
|
1053
|
+
[...sources.byRef.values()]
|
|
1054
|
+
.filter(
|
|
1055
|
+
(s) =>
|
|
1056
|
+
s.producer === "executor" &&
|
|
1057
|
+
(isTaskEvidence(s) || (scope === "selected" && next.selected.includes(s.ref))) &&
|
|
1058
|
+
(scope === "selected"
|
|
1059
|
+
? next.selected.includes(s.ref)
|
|
1060
|
+
: scope === "assignment"
|
|
1061
|
+
? s.assignmentId === a.id
|
|
1062
|
+
: scope === "active"
|
|
1063
|
+
? s.active
|
|
1064
|
+
: true) &&
|
|
1065
|
+
(s.original ||
|
|
1066
|
+
s.message.role === "toolResult" ||
|
|
1067
|
+
s.message.content?.some((b: any) => b.type === "text" && b.text?.trim())),
|
|
1068
|
+
)
|
|
1069
|
+
.map((s) => {
|
|
1070
|
+
return {
|
|
1071
|
+
ref: s.ref,
|
|
1072
|
+
kind: s.original ? "assistant-text" : s.message.role,
|
|
1073
|
+
producer: s.producer,
|
|
1074
|
+
assignment: s.assignmentId,
|
|
1075
|
+
toolName: s.message.toolName,
|
|
1076
|
+
active: s.active,
|
|
1077
|
+
selected: next.selected.includes(s.ref),
|
|
1078
|
+
retainedSelection: next.selected.includes(s.ref) && !isTaskEvidence(s),
|
|
1079
|
+
...checksFor(s),
|
|
1080
|
+
};
|
|
1081
|
+
})
|
|
1082
|
+
// Discovery offers usable candidates. Saved selections must remain
|
|
1083
|
+
// inspectable even when a later source or target check fails.
|
|
1084
|
+
.filter((row) => scope === "selected" || row.targetReady);
|
|
1085
|
+
page = this.page(
|
|
1086
|
+
`evidence:${scope}`,
|
|
1087
|
+
rows,
|
|
1088
|
+
input.cursor,
|
|
1089
|
+
30,
|
|
1090
|
+
scope === "assignment" || scope === "selected" ? a.id : undefined,
|
|
1091
|
+
scope === "active",
|
|
1092
|
+
(rows) => ({
|
|
1093
|
+
scopeCounts: {
|
|
1094
|
+
candidates: rows.length,
|
|
1095
|
+
eligible: rows.filter((r: any) => r.eligible).length,
|
|
1096
|
+
targetReady: rows.filter((r: any) => r.targetReady).length,
|
|
1097
|
+
},
|
|
1098
|
+
otherAssignments: [...sources.byRef.values()].filter(
|
|
1099
|
+
(s) =>
|
|
1100
|
+
s.producer === "executor" && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
|
|
1101
|
+
).length,
|
|
1102
|
+
}),
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
status: input.operation === "inspect" || next === selection ? "unchanged" : "saved",
|
|
1107
|
+
...(page
|
|
1108
|
+
? {
|
|
1109
|
+
scope,
|
|
1110
|
+
count: page.count,
|
|
1111
|
+
returned: page.items.length,
|
|
1112
|
+
candidates: page.items,
|
|
1113
|
+
nextCursor: page.nextCursor,
|
|
1114
|
+
...page.summary,
|
|
1115
|
+
pageCounts: {
|
|
1116
|
+
candidates: page.items.length,
|
|
1117
|
+
eligible: page.items.filter((r: any) => r.eligible).length,
|
|
1118
|
+
targetReady: page.items.filter((r: any) => r.targetReady).length,
|
|
1119
|
+
},
|
|
1120
|
+
countsBasis:
|
|
1121
|
+
"Inspection snapshot; page counts describe returned candidates, scope counts describe all candidates in this scope.",
|
|
1122
|
+
historyHint: page.summary.otherAssignments
|
|
1123
|
+
? "Previously exposed task evidence exists in other assignments; inspect scope history when needed."
|
|
1124
|
+
: undefined,
|
|
1125
|
+
}
|
|
1126
|
+
: {}),
|
|
1127
|
+
revision: next.revision,
|
|
1128
|
+
selected: next.selected,
|
|
1129
|
+
selectedCount: next.selected.length,
|
|
1130
|
+
unresolved: next.unresolved,
|
|
1131
|
+
ready: !next.unresolved.length && prepared.ready,
|
|
1132
|
+
problems: prepared.problems,
|
|
1133
|
+
warnings: prepared.warnings,
|
|
1134
|
+
evidence: this.evidenceFacts(this.stateData()),
|
|
1135
|
+
items: (input.refs ? [...new Set<string>(input.refs)] : []).map((ref) => ({
|
|
1136
|
+
ref,
|
|
1137
|
+
status: next.unresolved.some((p) => p.ref === ref)
|
|
1138
|
+
? "rejected"
|
|
1139
|
+
: input.operation === "remove"
|
|
1140
|
+
? selection.selected.includes(ref)
|
|
1141
|
+
? "removed"
|
|
1142
|
+
: selection.unresolved.some((p) => p.ref === ref)
|
|
1143
|
+
? "withdrawn"
|
|
1144
|
+
: "unchanged"
|
|
1145
|
+
: selection.selected.includes(ref)
|
|
1146
|
+
? "already_selected"
|
|
1147
|
+
: "added",
|
|
1148
|
+
})),
|
|
1149
|
+
estimate: { tokens: prepared.estimatedTokens, method: prepared.estimateMethod },
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
private unit(input: any, op: string) {
|
|
1153
|
+
const state = this.stateData();
|
|
1154
|
+
const prior = [...state.events.values()].find((e) => e.operationId === op);
|
|
1155
|
+
if (prior) {
|
|
1156
|
+
check(
|
|
1157
|
+
(input.operation === "assess" && prior.data.type === "assessment-resumed") ||
|
|
1158
|
+
(input.operation === "close" &&
|
|
1159
|
+
prior.data.type === "unit-closed" &&
|
|
1160
|
+
prior.data.outcome === input.outcome &&
|
|
1161
|
+
prior.data.assessment === input.assessment),
|
|
1162
|
+
"operation_conflict",
|
|
1163
|
+
);
|
|
1164
|
+
return { status: "unchanged", operation: input.operation };
|
|
1165
|
+
}
|
|
1166
|
+
if (input.operation === "assess") {
|
|
1167
|
+
check(state.assessment, "assessment_missing");
|
|
1168
|
+
if (state.assessment.view === "active") return { status: "unchanged", ready: true };
|
|
1169
|
+
const prepared = this.prepared("coordinator", this.messages, state.assessment.handoffId, true);
|
|
1170
|
+
if (!prepared.ready) return { status: "suspended", ready: false, problems: prepared.problems };
|
|
1171
|
+
check(prepared.reservation, "reservation_missing");
|
|
1172
|
+
this.append(
|
|
1173
|
+
{
|
|
1174
|
+
type: "assessment-resumed",
|
|
1175
|
+
handoffId: state.assessment.handoffId,
|
|
1176
|
+
basisUserEntryId: this.turn!.basisUserEntryId,
|
|
1177
|
+
reservation: prepared.reservation,
|
|
1178
|
+
},
|
|
1179
|
+
op,
|
|
1180
|
+
);
|
|
1181
|
+
this.projectionError = undefined;
|
|
1182
|
+
return {
|
|
1183
|
+
status: "resumed",
|
|
1184
|
+
ready: true,
|
|
1185
|
+
stage: "prepared assessment; next request revalidated",
|
|
1186
|
+
evidence: this.evidenceFacts(this.stateData()),
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
check(input.operation === "close" && state.unitId, "unit_missing");
|
|
1190
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1191
|
+
check(!this.applying, "not_quiescent");
|
|
1192
|
+
this.append(
|
|
1193
|
+
{
|
|
1194
|
+
type: "unit-closed",
|
|
1195
|
+
unitId: state.unitId,
|
|
1196
|
+
outcome: input.outcome,
|
|
1197
|
+
assessment: input.assessment,
|
|
1198
|
+
...(a?.state === "outstanding" && input.outcome !== "accepted" ? { supersededAssignmentId: a.id } : {}),
|
|
1199
|
+
},
|
|
1200
|
+
op,
|
|
1201
|
+
);
|
|
1202
|
+
return { status: "closed", unit: state.unitId, outcome: input.outcome, assessment: input.assessment };
|
|
1203
|
+
}
|
|
1204
|
+
private page(
|
|
1205
|
+
scope: string,
|
|
1206
|
+
rows: any[] | (() => any[]),
|
|
1207
|
+
cursor?: string,
|
|
1208
|
+
size = 30,
|
|
1209
|
+
assignment?: string,
|
|
1210
|
+
active = false,
|
|
1211
|
+
summarize?: (rows: any[]) => any,
|
|
1212
|
+
): any {
|
|
1213
|
+
const branch = this.ctx.sessionManager.getBranch() as NativeEntry[];
|
|
1214
|
+
const compaction = branch.filter((e) => e.type === "compaction").at(-1)?.id;
|
|
1215
|
+
let key: string,
|
|
1216
|
+
offset = 0;
|
|
1217
|
+
if (cursor) {
|
|
1218
|
+
const match = /^([a-f0-9-]+):([0-9]+)$/.exec(cursor);
|
|
1219
|
+
check(match, "invalid_cursor", "Use the returned cursor unchanged.");
|
|
1220
|
+
key = match[1];
|
|
1221
|
+
offset = Number(match[2]);
|
|
1222
|
+
const page = this.pages.get(key);
|
|
1223
|
+
check(
|
|
1224
|
+
page &&
|
|
1225
|
+
page.scope === scope &&
|
|
1226
|
+
page.assignment === assignment &&
|
|
1227
|
+
(!page.basis || branch.some((e) => e.id === page.basis)) &&
|
|
1228
|
+
(!active || page.compaction === compaction),
|
|
1229
|
+
"cursor_expired",
|
|
1230
|
+
"History or scope changed. Inspect again without a cursor.",
|
|
1231
|
+
);
|
|
1232
|
+
check(
|
|
1233
|
+
Number.isSafeInteger(offset) && offset >= 0 && offset < page.rows.length && offset % page.size === 0,
|
|
1234
|
+
"invalid_cursor",
|
|
1235
|
+
);
|
|
1236
|
+
} else {
|
|
1237
|
+
key = randomUUID();
|
|
1238
|
+
if (this.pages.size >= 8) this.pages.delete(this.pages.keys().next().value!);
|
|
1239
|
+
const items = typeof rows === "function" ? rows() : rows;
|
|
1240
|
+
this.pages.set(key, {
|
|
1241
|
+
scope,
|
|
1242
|
+
basis: branch.at(-1)?.id ?? null,
|
|
1243
|
+
assignment,
|
|
1244
|
+
compaction,
|
|
1245
|
+
rows: items,
|
|
1246
|
+
size,
|
|
1247
|
+
summary: summarize?.(items),
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
const page = this.pages.get(key)!;
|
|
1251
|
+
return {
|
|
1252
|
+
scope,
|
|
1253
|
+
count: page.rows.length,
|
|
1254
|
+
offset,
|
|
1255
|
+
summary: page.summary,
|
|
1256
|
+
items: page.rows.slice(offset, offset + page.size),
|
|
1257
|
+
nextCursor: offset + page.size < page.rows.length ? `${key}:${offset + page.size}` : undefined,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
private inspectUnit(input: any): any {
|
|
1261
|
+
const state = this.stateData(),
|
|
1262
|
+
view = input.view ?? "current";
|
|
1263
|
+
check(view === "detail" || !input.ref, "invalid_inspection", "A work ref belongs to view detail.");
|
|
1264
|
+
check(
|
|
1265
|
+
view === "history" || (!input.cursor && !input.limit),
|
|
1266
|
+
"invalid_inspection",
|
|
1267
|
+
"Pagination belongs to view history.",
|
|
1268
|
+
);
|
|
1269
|
+
if (view === "current")
|
|
1270
|
+
return {
|
|
1271
|
+
...this.status(),
|
|
1272
|
+
status: "inspected",
|
|
1273
|
+
view,
|
|
1274
|
+
currentRef: state.assignmentId ? `assignment:${state.assignmentId}` : undefined,
|
|
1275
|
+
};
|
|
1276
|
+
if (view === "history") {
|
|
1277
|
+
const numbers = new Map([...state.units.keys()].map((id, i) => [id, i + 1]));
|
|
1278
|
+
const rows = () =>
|
|
1279
|
+
[...state.assignments.values()].reverse().map((a) => {
|
|
1280
|
+
const u = state.units.get(a.unitId),
|
|
1281
|
+
h = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1282
|
+
return {
|
|
1283
|
+
ref: `assignment:${a.id}`,
|
|
1284
|
+
unitRef: `unit:${a.unitId}`,
|
|
1285
|
+
unitNumber: numbers.get(a.unitId),
|
|
1286
|
+
assignmentNumber: (u?.assignmentIds.indexOf(a.id) ?? 0) + 1,
|
|
1287
|
+
state: a.state,
|
|
1288
|
+
unitState: u?.state,
|
|
1289
|
+
disposition: u?.disposition,
|
|
1290
|
+
summary: a.contract.slice(0, 160),
|
|
1291
|
+
reportAvailable: !!h,
|
|
1292
|
+
outcome: h?.outcome,
|
|
1293
|
+
from: "coordinator",
|
|
1294
|
+
to: "executor",
|
|
1295
|
+
};
|
|
1296
|
+
});
|
|
1297
|
+
const page = this.page("work-history", rows, input.cursor, input.limit ?? 20);
|
|
1298
|
+
return {
|
|
1299
|
+
status: "inspected",
|
|
1300
|
+
view,
|
|
1301
|
+
history: page.items,
|
|
1302
|
+
count: page.count,
|
|
1303
|
+
returned: page.items.length,
|
|
1304
|
+
nextCursor: page.nextCursor,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
check(
|
|
1308
|
+
view === "detail" && typeof input.ref === "string",
|
|
1309
|
+
"work_ref_required",
|
|
1310
|
+
"Use a ref returned by history inspection.",
|
|
1311
|
+
);
|
|
1312
|
+
check(
|
|
1313
|
+
/^(?:(?:unit|assignment):[^:\s]+|report:[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
|
|
1314
|
+
"invalid_work_ref",
|
|
1315
|
+
"Use assignmentRef or reportRef from the return receipt, or a work ref returned by inspection, unchanged. A bare handoff ID is not a work ref.",
|
|
1316
|
+
);
|
|
1317
|
+
if (input.ref.startsWith("unit:")) {
|
|
1318
|
+
const u = state.units.get(input.ref.slice(5));
|
|
1319
|
+
check(u, "work_unavailable", "Unit is not on current ancestry.");
|
|
1320
|
+
return {
|
|
1321
|
+
status: "inspected",
|
|
1322
|
+
view,
|
|
1323
|
+
ref: input.ref,
|
|
1324
|
+
unit: {
|
|
1325
|
+
id: u.id,
|
|
1326
|
+
state: u.state,
|
|
1327
|
+
objective: u.objective,
|
|
1328
|
+
disposition: u.disposition,
|
|
1329
|
+
assessment: u.assessment,
|
|
1330
|
+
},
|
|
1331
|
+
assignmentCount: u.assignmentIds.length,
|
|
1332
|
+
assignmentRefs: u.assignmentIds.slice(0, 30).map((id) => `assignment:${id}`),
|
|
1333
|
+
remaining: Math.max(0, u.assignmentIds.length - 30),
|
|
1334
|
+
historyHint: "Use paginated history inspection for all assignment refs.",
|
|
1335
|
+
historical: true,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
let savedRevision: Handoff | undefined;
|
|
1339
|
+
if (input.ref.startsWith("report:")) {
|
|
1340
|
+
const match = /^report:([^:]+):([0-9]+)$/.exec(input.ref);
|
|
1341
|
+
check(match, "work_unavailable", "Use a report ref returned by detail inspection.");
|
|
1342
|
+
for (const event of state.events.values())
|
|
1343
|
+
if (
|
|
1344
|
+
event.data.type === "return-accepted" &&
|
|
1345
|
+
event.data.handoff.id === match[1] &&
|
|
1346
|
+
event.data.handoff.reportRevision === Number(match[2])
|
|
1347
|
+
)
|
|
1348
|
+
savedRevision = event.data.handoff;
|
|
1349
|
+
check(savedRevision, "work_unavailable", "Report revision is not on current ancestry.");
|
|
1350
|
+
}
|
|
1351
|
+
const a = savedRevision
|
|
1352
|
+
? state.assignments.get(savedRevision.assignmentId)
|
|
1353
|
+
: input.ref.startsWith("assignment:")
|
|
1354
|
+
? state.assignments.get(input.ref.slice(11))
|
|
1355
|
+
: undefined;
|
|
1356
|
+
check(a, "work_unavailable", "Assignment is not on current ancestry.");
|
|
1357
|
+
const latest = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
|
|
1358
|
+
const h = savedRevision ?? latest;
|
|
1359
|
+
return {
|
|
1360
|
+
status: "inspected",
|
|
1361
|
+
view,
|
|
1362
|
+
ref: input.ref,
|
|
1363
|
+
historical: a.id !== state.assignmentId || a.state !== "outstanding",
|
|
1364
|
+
assignment: { id: a.id, unitId: a.unitId, state: a.state },
|
|
1365
|
+
contract: a.contract,
|
|
1366
|
+
report: h?.text,
|
|
1367
|
+
reportRevision: h?.reportRevision,
|
|
1368
|
+
latestReportRevision: latest?.reportRevision,
|
|
1369
|
+
reportRef: h ? `report:${h.id}:${h.reportRevision}` : undefined,
|
|
1370
|
+
previousReportRef: h && h.reportRevision > 1 ? `report:${h.id}:${h.reportRevision - 1}` : undefined,
|
|
1371
|
+
outcome: h?.outcome,
|
|
1372
|
+
limitations: h?.limitations,
|
|
1373
|
+
handoff: h?.id,
|
|
1374
|
+
currentSelection: state.selections.get(a.id) ?? emptySelection(),
|
|
1375
|
+
sourceBoundary:
|
|
1376
|
+
"Accepted report revision on current ancestry; assignment/selection show current recorded state. Historical content grants no current permission.",
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
status(limit = 0): any {
|
|
1380
|
+
const state = this.stateData();
|
|
1381
|
+
const unit = state.unitId ? state.units.get(state.unitId) : undefined;
|
|
1382
|
+
const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1383
|
+
return {
|
|
1384
|
+
...this.state(),
|
|
1385
|
+
unit: unit ? { id: unit.id, state: unit.state, assignments: unit.assignmentIds.length } : null,
|
|
1386
|
+
assignment: a ? { id: a.id, state: a.state } : null,
|
|
1387
|
+
pendingHandoff: state.pendingId ?? null,
|
|
1388
|
+
assessment: state.assessment
|
|
1389
|
+
? { handoffId: state.assessment.handoffId, view: state.assessment.view, problems: state.assessment.problems }
|
|
1390
|
+
: null,
|
|
1391
|
+
...(limit
|
|
1392
|
+
? {
|
|
1393
|
+
history: [...state.events.values()]
|
|
1394
|
+
.slice(-Math.min(100, Math.max(1, limit)))
|
|
1395
|
+
.map((e) => ({ event: e.eventId, type: e.data.type, operation: e.operationId })),
|
|
1396
|
+
}
|
|
1397
|
+
: {}),
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
async turnEnd(event: any, ctx: any): Promise<void> {
|
|
1401
|
+
this.ctx = ctx;
|
|
1402
|
+
if (!this.turn || this.turn.bound || !this.supported() || !this.store) return;
|
|
1403
|
+
if (!this.turn.opened) {
|
|
1404
|
+
this.turn = undefined;
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const token = this.token,
|
|
1408
|
+
turn = this.turn;
|
|
1409
|
+
const subject = this.subject();
|
|
1410
|
+
try {
|
|
1411
|
+
const message = event.message ?? turn.message;
|
|
1412
|
+
const candidates = (ctx.sessionManager.getBranch() as NativeEntry[]).filter(
|
|
1413
|
+
(e) => !turn.before.has(e.id) && e.type === "message" && canonical(e.message) === canonical(message),
|
|
1414
|
+
);
|
|
1415
|
+
check(candidates.length === 1, "execution_binding_ambiguous");
|
|
1416
|
+
const assistant = candidates[0];
|
|
1417
|
+
const callIds = new Set((message?.content ?? []).filter((b: any) => b.type === "toolCall").map((b: any) => b.id));
|
|
1418
|
+
const results = (ctx.sessionManager.getBranch() as NativeEntry[]).filter(
|
|
1419
|
+
(e) => !turn.before.has(e.id) && e.message?.role === "toolResult" && callIds.has(e.message.toolCallId),
|
|
1420
|
+
);
|
|
1421
|
+
const outcome =
|
|
1422
|
+
message?.stopReason === "error" ? "failed" : message?.stopReason === "aborted" ? "aborted" : "completed";
|
|
1423
|
+
this.append(
|
|
1424
|
+
{
|
|
1425
|
+
type: "execution-bound",
|
|
1426
|
+
executionId: turn.id,
|
|
1427
|
+
assistantEntryId: assistant.id,
|
|
1428
|
+
resultEntryIds: results.map((e) => e.id),
|
|
1429
|
+
outcome,
|
|
1430
|
+
},
|
|
1431
|
+
JSON.stringify(["binding", turn.id]),
|
|
1432
|
+
);
|
|
1433
|
+
turn.bound = assistant.id;
|
|
1434
|
+
const state = this.stateData(),
|
|
1435
|
+
pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
|
|
1436
|
+
if (!pending || pending.state === "superseded") return;
|
|
1437
|
+
const attempt = state.attempts.get(pending.id);
|
|
1438
|
+
if (pending.executionId !== turn.id && attempt?.executionId !== turn.id) return;
|
|
1439
|
+
if (ctx.signal?.aborted || outcome === "aborted" || state.control !== "automatic") return;
|
|
1440
|
+
const inputs = [...this.messages, message, ...results.map((e) => e.message)].filter(Boolean);
|
|
1441
|
+
const latest = this.stateData();
|
|
1442
|
+
const retry = latest.attempts.get(pending.id);
|
|
1443
|
+
if (retry?.executionId === turn.id)
|
|
1444
|
+
check(
|
|
1445
|
+
retry.reportRevision === pending.reportRevision &&
|
|
1446
|
+
retry.selectionRevision === (latest.selections.get(pending.assignmentId)?.revision ?? 0),
|
|
1447
|
+
"stale_retry_revision",
|
|
1448
|
+
);
|
|
1449
|
+
await this.finishHandoff(pending, inputs, token);
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
if (this.current(subject)) {
|
|
1452
|
+
this.mark(error);
|
|
1453
|
+
ctx.abort?.();
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
private async finishHandoff(h: Handoff, inputs: any[], token: string): Promise<void> {
|
|
1458
|
+
const subject = this.subject();
|
|
1459
|
+
const state = this.stateData();
|
|
1460
|
+
check(state.control === "automatic" && !this.store?.blocked && token === this.token, "transition_ineligible");
|
|
1461
|
+
const execution = state.executions.get(h.executionId);
|
|
1462
|
+
check(
|
|
1463
|
+
execution?.assistantEntryId,
|
|
1464
|
+
"source_turn_incomplete",
|
|
1465
|
+
"Saved communication belongs to an incomplete historical turn. Reconcile its effects and replace or cancel the outstanding work; no tool result is fabricated.",
|
|
1466
|
+
);
|
|
1467
|
+
const source = new Sources(this.ctx.sessionManager.getBranch(), state);
|
|
1468
|
+
const carrier = source.byRef.get(`ctx:${execution.assistantEntryId}`);
|
|
1469
|
+
check(
|
|
1470
|
+
carrier && !source.exchange(carrier).problems.length,
|
|
1471
|
+
"source_exchange_incomplete",
|
|
1472
|
+
"The saved handoff has an incomplete native exchange; reconcile or explicitly dispose of it.",
|
|
1473
|
+
);
|
|
1474
|
+
if (h.kind === "return" && this.projectionEnabled) {
|
|
1475
|
+
const prepared = this.prepared("coordinator", inputs, h.id);
|
|
1476
|
+
if (!prepared.ready) {
|
|
1477
|
+
this.projectionError = prepared.problems.map((p) => p.code).join(", ");
|
|
1478
|
+
this.append({
|
|
1479
|
+
type: "handoff-state",
|
|
1480
|
+
handoffId: h.id,
|
|
1481
|
+
state: "blocked",
|
|
1482
|
+
reason: prepared.problems
|
|
1483
|
+
.map((p) => p.detail)
|
|
1484
|
+
.join("; ")
|
|
1485
|
+
.slice(0, 4096),
|
|
1486
|
+
});
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
check(prepared.reservation, "reservation_missing");
|
|
1490
|
+
this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
const current = this.stateData();
|
|
1494
|
+
check(current.pendingId === h.id && current.control === "automatic" && token === this.token, "stale_handoff");
|
|
1495
|
+
await this.applyPair(this.profilePair(h.to));
|
|
1496
|
+
this.guard(subject);
|
|
1497
|
+
this.append({ type: "handoff-state", handoffId: h.id, state: "configured", observedPair: this.observed() });
|
|
1498
|
+
const completed = this.stateData();
|
|
1499
|
+
const reservation = completed.reservations.get(h.id);
|
|
1500
|
+
if (h.kind === "return" && completed.assessment?.view === "suspended" && reservation) {
|
|
1501
|
+
this.append({
|
|
1502
|
+
type: "assessment-resumed",
|
|
1503
|
+
handoffId: h.id,
|
|
1504
|
+
basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
|
|
1505
|
+
reservation,
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
this.projectionError = undefined;
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
if (this.current(subject) && !this.store?.blocked && !this.error && this.stateData().pendingId === h.id) {
|
|
1511
|
+
this.append({
|
|
1512
|
+
type: "handoff-state",
|
|
1513
|
+
handoffId: h.id,
|
|
1514
|
+
state: "blocked",
|
|
1515
|
+
reason: error instanceof Error ? error.message.slice(0, 4096) : "configuration_failed",
|
|
1516
|
+
});
|
|
1517
|
+
this.ctx.ui?.notify?.(
|
|
1518
|
+
`Couldn’t switch to ${h.to === "coordinator" ? "Coordinator" : "Executor"} — ${h.kind === "return" ? "report" : "assignment"} saved.`,
|
|
1519
|
+
"warning",
|
|
1520
|
+
);
|
|
1521
|
+
} else throw error;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
async resume(ctx: any): Promise<void> {
|
|
1525
|
+
this.ctx = ctx;
|
|
1526
|
+
check(ctx.isIdle?.(), "not_idle");
|
|
1527
|
+
check(this.store && this.capability?.effective, "routing_unavailable");
|
|
1528
|
+
this.revision++;
|
|
1529
|
+
const subject = this.subject();
|
|
1530
|
+
return this.enqueue(() => this.resumeCurrent(ctx, subject));
|
|
1531
|
+
}
|
|
1532
|
+
private async resumeCurrent(ctx: any, subject: Subject): Promise<void> {
|
|
1533
|
+
this.guard(subject);
|
|
1534
|
+
check(this.store, "routing_unavailable");
|
|
1535
|
+
await this.store.reconcile();
|
|
1536
|
+
this.guard(subject);
|
|
1537
|
+
this.error = undefined;
|
|
1538
|
+
this.retireUnbound("Explicit resume retires an interrupted attempt without claiming its effects completed.");
|
|
1539
|
+
const state = this.stateData();
|
|
1540
|
+
check(state.control === "automatic", "manual_control");
|
|
1541
|
+
check(
|
|
1542
|
+
!ctx.hasPendingMessages?.(),
|
|
1543
|
+
"pending_input",
|
|
1544
|
+
"Queued input must be delivered and reconciled before resuming Executor.",
|
|
1545
|
+
);
|
|
1546
|
+
const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
|
|
1547
|
+
if (pending) {
|
|
1548
|
+
const source = ctx.sessionManager.buildSessionContext?.().messages;
|
|
1549
|
+
check(Array.isArray(source) || this.messages.length, "resume_context_unavailable");
|
|
1550
|
+
await this.finishHandoff(pending, source ?? this.messages, this.token);
|
|
1551
|
+
this.guard(subject);
|
|
1552
|
+
if (this.stateData().pendingId) {
|
|
1553
|
+
check(
|
|
1554
|
+
pending.kind === "return" && this.projectionError,
|
|
1555
|
+
"handoff_still_blocked",
|
|
1556
|
+
"Saved transfer is still blocked; reconcile its configuration before retrying.",
|
|
1557
|
+
);
|
|
1558
|
+
this.append({
|
|
1559
|
+
type: "control",
|
|
1560
|
+
control: "automatic",
|
|
1561
|
+
profile: "executor",
|
|
1562
|
+
reason: "Explicit resume of saved-return evidence correction only; ordinary assignment work remains ended",
|
|
1563
|
+
});
|
|
1564
|
+
await this.applyPair(this.profilePair("executor"));
|
|
1565
|
+
}
|
|
1566
|
+
} else {
|
|
1567
|
+
const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
|
|
1568
|
+
const input = ctx.sessionManager.buildSessionContext?.().messages ?? this.messages;
|
|
1569
|
+
const user = this.lastDeliveredUser(new Sources(ctx.sessionManager.getBranch(), state), input);
|
|
1570
|
+
const coordinatorSawInput = [...state.executions.values()].some(
|
|
1571
|
+
(x) =>
|
|
1572
|
+
x.profile === "coordinator" && x.basisUserEntryId === user && x.assistantEntryId && x.outcome === "completed",
|
|
1573
|
+
);
|
|
1574
|
+
if (assignment?.state === "outstanding") {
|
|
1575
|
+
check(
|
|
1576
|
+
user === this.taskBasis(state, assignment.id) || coordinatorSawInput,
|
|
1577
|
+
"input_unreconciled",
|
|
1578
|
+
"New input needs Coordinator attention before the unchanged assignment can resume.",
|
|
1579
|
+
);
|
|
1580
|
+
this.append({ type: "assignment-resumed", assignmentId: assignment.id, basisUserEntryId: user });
|
|
1581
|
+
this.append({
|
|
1582
|
+
type: "control",
|
|
1583
|
+
control: "automatic",
|
|
1584
|
+
profile: "executor",
|
|
1585
|
+
reason: "Explicit user resume of the unchanged assignment; current restrictions remain applicable",
|
|
1586
|
+
});
|
|
1587
|
+
await this.applyPair(this.profilePair("executor"));
|
|
1588
|
+
} else await this.applyPair(this.profilePair("coordinator"));
|
|
1589
|
+
}
|
|
1590
|
+
this.guard(subject);
|
|
1591
|
+
this.pi.sendMessage(
|
|
1592
|
+
{
|
|
1593
|
+
customType: "freeflow-routing-v2-resume",
|
|
1594
|
+
content:
|
|
1595
|
+
"Explicit user resume of the saved routing responsibility. Recover the current contract and evidence before continuing.",
|
|
1596
|
+
display: true,
|
|
1597
|
+
},
|
|
1598
|
+
{ triggerTurn: true },
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
async command(args: string, ctx: any): Promise<boolean> {
|
|
1602
|
+
const words = args.trim().split(/\s+/);
|
|
1603
|
+
if (!["profile", "resume"].includes(words[0])) return false;
|
|
1604
|
+
try {
|
|
1605
|
+
if (words[0] === "profile" && words[1] === "history") {
|
|
1606
|
+
check(words.length === 2 || (words.length === 3 && words[2] === "diagnostics"), "invalid_history_command");
|
|
1607
|
+
ctx.ui.notify(
|
|
1608
|
+
JSON.stringify(
|
|
1609
|
+
words[2] === "diagnostics" ? this.status(30) : this.inspectUnit({ view: "history", limit: 30 }),
|
|
1610
|
+
),
|
|
1611
|
+
"info",
|
|
1612
|
+
);
|
|
1613
|
+
return true;
|
|
1614
|
+
}
|
|
1615
|
+
check(ctx.isIdle?.(), "not_idle");
|
|
1616
|
+
this.ctx = ctx;
|
|
1617
|
+
if (words[0] === "resume" && words.length === 1) {
|
|
1618
|
+
await this.resume(ctx);
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
check(
|
|
1622
|
+
words.length === 2 && [...PROFILES, "auto"].includes(words[1]),
|
|
1623
|
+
"invalid_profile_command",
|
|
1624
|
+
"Use /freeflow profile coordinator|executor|auto|history, or /freeflow resume.",
|
|
1625
|
+
);
|
|
1626
|
+
const result =
|
|
1627
|
+
words[1] === "auto" ? await this.setAutomaticControl() : await this.setManualProfile(words[1] as Profile);
|
|
1628
|
+
ctx.ui.notify(JSON.stringify(result), result.status === "blocked" ? "warning" : "info");
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
1631
|
+
}
|
|
1632
|
+
return true;
|
|
1633
|
+
}
|
|
1634
|
+
}
|