@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
|
@@ -14,6 +14,12 @@ test("evidence inspection renders metadata and gaps without source previews", ()
|
|
|
14
14
|
kind: "toolResult",
|
|
15
15
|
producer: "executor",
|
|
16
16
|
toolName: "read",
|
|
17
|
+
locator: {
|
|
18
|
+
callRef: "ctx:call-source",
|
|
19
|
+
toolCallId: "call-1",
|
|
20
|
+
label: "read: evidence.txt (offset 1, limit 20)",
|
|
21
|
+
truncated: false,
|
|
22
|
+
},
|
|
17
23
|
assignment: "a1",
|
|
18
24
|
selected: true,
|
|
19
25
|
active: false,
|
|
@@ -30,7 +36,15 @@ test("evidence inspection renders metadata and gaps without source previews", ()
|
|
|
30
36
|
.render(100)
|
|
31
37
|
.join("\n");
|
|
32
38
|
assert.doesNotMatch(rendered, /SOURCE_BODY_MUST_NOT_APPEAR|undefined/);
|
|
33
|
-
for (const value of [
|
|
39
|
+
for (const value of [
|
|
40
|
+
"ctx:old",
|
|
41
|
+
"executor",
|
|
42
|
+
"read",
|
|
43
|
+
"read: evidence.txt (offset 1, limit 20)",
|
|
44
|
+
"call-1 (ctx:call-source)",
|
|
45
|
+
"assignment a1",
|
|
46
|
+
"Required image unsupported",
|
|
47
|
+
])
|
|
34
48
|
assert.ok(rendered.includes(value));
|
|
35
49
|
});
|
|
36
50
|
|
|
@@ -8,10 +8,19 @@ import { RoutingRuntime } from "../../dist/cognitive-routing-v2/runtime.js";
|
|
|
8
8
|
import { EventStore } from "../../dist/session-sources/events.js";
|
|
9
9
|
import { replay } from "../../dist/cognitive-routing-v2/state.js";
|
|
10
10
|
|
|
11
|
+
const modelIds = ["coordinator", "helper", "executor", "coordinator-fast", "helper-fast", "executor-cheap"];
|
|
11
12
|
const models = Object.fromEntries(
|
|
12
|
-
|
|
13
|
+
modelIds.map((id) => [
|
|
13
14
|
id,
|
|
14
|
-
{
|
|
15
|
+
{
|
|
16
|
+
id,
|
|
17
|
+
provider: "fixture",
|
|
18
|
+
api: "fixture",
|
|
19
|
+
input: ["text"],
|
|
20
|
+
reasoning: true,
|
|
21
|
+
contextWindow: 100000,
|
|
22
|
+
maxTokens: 1000,
|
|
23
|
+
},
|
|
15
24
|
]),
|
|
16
25
|
);
|
|
17
26
|
const cap = {
|
|
@@ -19,10 +28,17 @@ const cap = {
|
|
|
19
28
|
enabled: true,
|
|
20
29
|
projection: false,
|
|
21
30
|
profiles: Object.fromEntries(
|
|
22
|
-
|
|
31
|
+
["coordinator", "executor"].map((id) => [id, { provider: "fixture", model: id, thinking: "off" }]),
|
|
23
32
|
),
|
|
24
33
|
blockingReason: { code: "", message: "" },
|
|
25
34
|
};
|
|
35
|
+
const helperCap = {
|
|
36
|
+
...cap,
|
|
37
|
+
delegation: "helper",
|
|
38
|
+
profiles: Object.fromEntries(
|
|
39
|
+
["coordinator", "helper"].map((id) => [id, { provider: "fixture", model: id, thinking: "off" }]),
|
|
40
|
+
),
|
|
41
|
+
};
|
|
26
42
|
async function environment(run) {
|
|
27
43
|
const root = await mkdtemp(join(tmpdir(), "routing-runtime-"));
|
|
28
44
|
let active;
|
|
@@ -41,6 +57,8 @@ async function environment(run) {
|
|
|
41
57
|
sessionManager: manager,
|
|
42
58
|
model: models.coordinator,
|
|
43
59
|
thinkingLevel: "off",
|
|
60
|
+
failNextModelId: undefined,
|
|
61
|
+
failNextThinkingLevel: undefined,
|
|
44
62
|
modelRegistry: { find: (_p, id) => models[id], getApiKeyAndHeaders: async () => ({ ok: true }) },
|
|
45
63
|
getSystemPrompt: () => "",
|
|
46
64
|
isIdle: () => true,
|
|
@@ -50,10 +68,18 @@ async function environment(run) {
|
|
|
50
68
|
const pi = {
|
|
51
69
|
appendEntry: (type, data) => active.sessionManager.appendCustomEntry(type, data),
|
|
52
70
|
setModel: async (model) => {
|
|
71
|
+
if (active.failNextModelId === model.id) {
|
|
72
|
+
active.failNextModelId = undefined;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
53
75
|
active.model = model;
|
|
54
76
|
return true;
|
|
55
77
|
},
|
|
56
78
|
setThinkingLevel: (level) => {
|
|
79
|
+
if (active.failNextThinkingLevel === level) {
|
|
80
|
+
active.failNextThinkingLevel = undefined;
|
|
81
|
+
throw new Error("fixture thinking application failed");
|
|
82
|
+
}
|
|
57
83
|
active.thinkingLevel = level;
|
|
58
84
|
},
|
|
59
85
|
getAllTools: () => [],
|
|
@@ -114,6 +140,32 @@ test("delayed old control cannot change or poison a newly bound session", async
|
|
|
114
140
|
}
|
|
115
141
|
}));
|
|
116
142
|
|
|
143
|
+
for (const [name, firstCapability, heldWorker, secondCapability] of [
|
|
144
|
+
["Helper to Executor", helperCap, "helper", cap],
|
|
145
|
+
["Executor to Helper", cap, "executor", helperCap],
|
|
146
|
+
]) {
|
|
147
|
+
test(`binding a fresh ${name} session does not inherit the prior manual hold`, async () =>
|
|
148
|
+
environment(async ({ runtime, make, activate }) => {
|
|
149
|
+
const first = make(`${name}-first`);
|
|
150
|
+
activate(first);
|
|
151
|
+
await runtime.bind(first, firstCapability);
|
|
152
|
+
assert.equal((await runtime.setManualProfile(heldWorker)).status, "active");
|
|
153
|
+
const firstEntryCount = first.sessionManager.getEntries().length;
|
|
154
|
+
|
|
155
|
+
const second = make(`${name}-second`);
|
|
156
|
+
activate(second);
|
|
157
|
+
await runtime.bind(second, secondCapability);
|
|
158
|
+
|
|
159
|
+
assert.equal(runtime.state().runtimeStatus, "active");
|
|
160
|
+
assert.equal(runtime.state().controlMode, "automatic");
|
|
161
|
+
assert.equal(runtime.state().activeProfile, "coordinator");
|
|
162
|
+
assert.equal(second.model.id, "coordinator");
|
|
163
|
+
assert.equal(first.sessionManager.getEntries().length, firstEntryCount);
|
|
164
|
+
assert.equal(replay(first.sessionManager.getBranch()).control, "manual");
|
|
165
|
+
assert.equal(replay(first.sessionManager.getBranch()).profile, heldWorker);
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
117
169
|
test("settled unbound attempt is retired truthfully and next request has a new identity", async () =>
|
|
118
170
|
environment(async ({ runtime, make, activate }) => {
|
|
119
171
|
const ctx = make("interrupt");
|
|
@@ -151,3 +203,221 @@ test("settled unbound attempt is retired truthfully and next request has a new i
|
|
|
151
203
|
assert.notEqual(executions[1].id, old.id);
|
|
152
204
|
assert.notEqual(executions[1].basisUserEntryId, old.basisUserEntryId);
|
|
153
205
|
}));
|
|
206
|
+
|
|
207
|
+
test("session profile overrides apply only to the selected pair and restore configured pairs", async () =>
|
|
208
|
+
environment(async ({ runtime, make, activate }) => {
|
|
209
|
+
const ctx = make("session-profile");
|
|
210
|
+
activate(ctx);
|
|
211
|
+
await runtime.bind(ctx, cap);
|
|
212
|
+
assert.equal(ctx.model.id, "coordinator");
|
|
213
|
+
|
|
214
|
+
const coordinatorOverride = await runtime.setSessionProfileOverride("coordinator", {
|
|
215
|
+
provider: "fixture",
|
|
216
|
+
modelId: "coordinator-fast",
|
|
217
|
+
thinking: "high",
|
|
218
|
+
});
|
|
219
|
+
assert.equal(coordinatorOverride.status, "active");
|
|
220
|
+
assert.equal(ctx.model.id, "coordinator-fast");
|
|
221
|
+
assert.equal(ctx.thinkingLevel, "high");
|
|
222
|
+
assert.deepEqual(replay(ctx.sessionManager.getBranch()).profileOverrides.get("coordinator"), {
|
|
223
|
+
provider: "fixture",
|
|
224
|
+
modelId: "coordinator-fast",
|
|
225
|
+
thinking: "high",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const executorOverride = await runtime.setSessionProfileOverride("executor", {
|
|
229
|
+
provider: "fixture",
|
|
230
|
+
modelId: "executor-cheap",
|
|
231
|
+
thinking: "low",
|
|
232
|
+
});
|
|
233
|
+
assert.equal(executorOverride.status, "stored");
|
|
234
|
+
assert.equal(ctx.model.id, "coordinator-fast");
|
|
235
|
+
assert.equal(ctx.thinkingLevel, "high");
|
|
236
|
+
|
|
237
|
+
runtime.unbind();
|
|
238
|
+
ctx.model = models.coordinator;
|
|
239
|
+
ctx.thinkingLevel = "off";
|
|
240
|
+
activate(ctx);
|
|
241
|
+
await runtime.bind(ctx, cap);
|
|
242
|
+
assert.equal(ctx.model.id, "coordinator-fast");
|
|
243
|
+
assert.equal(ctx.thinkingLevel, "high");
|
|
244
|
+
|
|
245
|
+
const held = await runtime.setManualProfile("executor");
|
|
246
|
+
assert.equal(held.status, "active");
|
|
247
|
+
assert.equal(ctx.model.id, "executor-cheap");
|
|
248
|
+
assert.equal(ctx.thinkingLevel, "low");
|
|
249
|
+
|
|
250
|
+
ctx.model = models.executor;
|
|
251
|
+
ctx.thinkingLevel = "off";
|
|
252
|
+
await runtime.ancestryChanged(ctx, false);
|
|
253
|
+
assert.equal(ctx.model.id, "executor-cheap");
|
|
254
|
+
assert.equal(ctx.thinkingLevel, "low");
|
|
255
|
+
assert.equal(runtime.state().controlMode, "manual-executor");
|
|
256
|
+
|
|
257
|
+
const inheritedExecutor = await runtime.setSessionProfileOverride("executor", null);
|
|
258
|
+
assert.equal(inheritedExecutor.status, "active");
|
|
259
|
+
assert.equal(ctx.model.id, "executor");
|
|
260
|
+
assert.equal(ctx.thinkingLevel, "off");
|
|
261
|
+
assert.equal(replay(ctx.sessionManager.getBranch()).profileOverrides.has("coordinator"), true);
|
|
262
|
+
assert.equal(replay(ctx.sessionManager.getBranch()).profileOverrides.has("executor"), false);
|
|
263
|
+
|
|
264
|
+
const reset = await runtime.resetSessionProfileOverrides();
|
|
265
|
+
assert.equal(reset.status, "stored");
|
|
266
|
+
assert.equal(ctx.model.id, "executor");
|
|
267
|
+
assert.equal(ctx.thinkingLevel, "off");
|
|
268
|
+
assert.equal(replay(ctx.sessionManager.getBranch()).profileOverrides.size, 0);
|
|
269
|
+
assert.equal(runtime.state().controlMode, "manual-executor");
|
|
270
|
+
|
|
271
|
+
runtime.unbind();
|
|
272
|
+
activate(ctx);
|
|
273
|
+
await runtime.bind(ctx, cap);
|
|
274
|
+
assert.equal(ctx.model.id, "executor");
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
test("Helper session presets apply through the same guarded profile path", async () =>
|
|
278
|
+
environment(async ({ runtime, make, activate }) => {
|
|
279
|
+
const ctx = make("session-helper-profile");
|
|
280
|
+
activate(ctx);
|
|
281
|
+
await runtime.bind(ctx, helperCap);
|
|
282
|
+
const helperOverride = { provider: "fixture", modelId: "helper-fast", thinking: "high" };
|
|
283
|
+
assert.equal((await runtime.setSessionProfileOverride("helper", helperOverride)).status, "stored");
|
|
284
|
+
assert.equal(ctx.model.id, "coordinator");
|
|
285
|
+
assert.equal((await runtime.setManualProfile("helper")).status, "active");
|
|
286
|
+
assert.equal(ctx.model.id, "helper-fast");
|
|
287
|
+
assert.equal(ctx.thinkingLevel, "high");
|
|
288
|
+
assert.deepEqual(replay(ctx.sessionManager.getBranch()).profileOverrides.get("helper"), helperOverride);
|
|
289
|
+
assert.equal((await runtime.resetSessionProfileOverrides()).status, "active");
|
|
290
|
+
assert.equal(ctx.model.id, "helper");
|
|
291
|
+
assert.equal(ctx.thinkingLevel, "off");
|
|
292
|
+
assert.equal(replay(ctx.sessionManager.getBranch()).profileOverrides.size, 0);
|
|
293
|
+
assert.equal(runtime.state().controlMode, "manual-helper");
|
|
294
|
+
}));
|
|
295
|
+
|
|
296
|
+
test("manual control rejects a disabled worker without poisoning routing", async () =>
|
|
297
|
+
environment(async ({ runtime, make, activate }) => {
|
|
298
|
+
const ctx = make("disabled-manual-worker");
|
|
299
|
+
activate(ctx);
|
|
300
|
+
await runtime.bind(ctx, helperCap);
|
|
301
|
+
const result = await runtime.setManualProfile("executor");
|
|
302
|
+
assert.equal(result.status, "blocked");
|
|
303
|
+
assert.match(result.reason, /not enabled/);
|
|
304
|
+
assert.equal(runtime.state().runtimeStatus, "active");
|
|
305
|
+
assert.equal(runtime.state().activeProfile, "coordinator");
|
|
306
|
+
assert.equal(ctx.model.id, "coordinator");
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
test("invalid session profile overrides preserve the native pair and routing state", async () =>
|
|
310
|
+
environment(async ({ runtime, make, activate }) => {
|
|
311
|
+
const ctx = make("session-invalid");
|
|
312
|
+
activate(ctx);
|
|
313
|
+
await runtime.bind(ctx, cap);
|
|
314
|
+
const beforeEntries = ctx.sessionManager.getEntries().length;
|
|
315
|
+
const result = await runtime.setSessionProfileOverride("coordinator", {
|
|
316
|
+
provider: "fixture",
|
|
317
|
+
modelId: "missing-model",
|
|
318
|
+
thinking: "high",
|
|
319
|
+
});
|
|
320
|
+
assert.equal(result.status, "blocked");
|
|
321
|
+
assert.equal(ctx.model.id, "coordinator");
|
|
322
|
+
assert.equal(ctx.thinkingLevel, "off");
|
|
323
|
+
assert.equal(ctx.sessionManager.getEntries().length, beforeEntries);
|
|
324
|
+
assert.equal(replay(ctx.sessionManager.getBranch()).profileOverrides.size, 0);
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
test("rebind rejects a persisted session override when its model is unavailable", async () =>
|
|
328
|
+
await environment(async ({ runtime, make, activate }) => {
|
|
329
|
+
const ctx = make("session-stale");
|
|
330
|
+
activate(ctx);
|
|
331
|
+
await runtime.bind(ctx, cap);
|
|
332
|
+
assert.equal(
|
|
333
|
+
(
|
|
334
|
+
await runtime.setSessionProfileOverride("coordinator", {
|
|
335
|
+
provider: "fixture",
|
|
336
|
+
modelId: "coordinator-fast",
|
|
337
|
+
thinking: "high",
|
|
338
|
+
})
|
|
339
|
+
).status,
|
|
340
|
+
"active",
|
|
341
|
+
);
|
|
342
|
+
runtime.unbind();
|
|
343
|
+
const saved = models["coordinator-fast"];
|
|
344
|
+
delete models["coordinator-fast"];
|
|
345
|
+
try {
|
|
346
|
+
ctx.model = models.coordinator;
|
|
347
|
+
ctx.thinkingLevel = "off";
|
|
348
|
+
activate(ctx);
|
|
349
|
+
await runtime.bind(ctx, cap);
|
|
350
|
+
assert.equal(runtime.state().runtimeStatus, "blocked");
|
|
351
|
+
assert.match(runtime.state().runtimeReason, /profile_unavailable|Model unavailable/);
|
|
352
|
+
} finally {
|
|
353
|
+
models["coordinator-fast"] = saved;
|
|
354
|
+
}
|
|
355
|
+
const corrected = await runtime.setSessionProfileOverride("coordinator", null);
|
|
356
|
+
assert.equal(corrected.status, "active");
|
|
357
|
+
assert.equal(runtime.state().runtimeStatus, "active");
|
|
358
|
+
assert.equal(ctx.model.id, "coordinator");
|
|
359
|
+
assert.equal(replay(ctx.sessionManager.getBranch()).profileOverrides.size, 0);
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
test("failed active session override restores the prior pair and event state", async () =>
|
|
363
|
+
await environment(async ({ runtime, make, activate }) => {
|
|
364
|
+
const ctx = make("session-rollback");
|
|
365
|
+
activate(ctx);
|
|
366
|
+
await runtime.bind(ctx, cap);
|
|
367
|
+
assert.equal(
|
|
368
|
+
(
|
|
369
|
+
await runtime.setSessionProfileOverride("coordinator", {
|
|
370
|
+
provider: "fixture",
|
|
371
|
+
modelId: "coordinator-fast",
|
|
372
|
+
thinking: "high",
|
|
373
|
+
})
|
|
374
|
+
).status,
|
|
375
|
+
"active",
|
|
376
|
+
);
|
|
377
|
+
ctx.failNextModelId = "coordinator";
|
|
378
|
+
const result = await runtime.setSessionProfileOverride("coordinator", null);
|
|
379
|
+
assert.equal(result.status, "blocked");
|
|
380
|
+
assert.equal(ctx.model.id, "coordinator-fast");
|
|
381
|
+
assert.equal(ctx.thinkingLevel, "high");
|
|
382
|
+
assert.deepEqual(replay(ctx.sessionManager.getBranch()).profileOverrides.get("coordinator"), {
|
|
383
|
+
provider: "fixture",
|
|
384
|
+
modelId: "coordinator-fast",
|
|
385
|
+
thinking: "high",
|
|
386
|
+
});
|
|
387
|
+
}));
|
|
388
|
+
|
|
389
|
+
test("active session reset restores the configured pair and control mode", async () =>
|
|
390
|
+
await environment(async ({ runtime, make, activate }) => {
|
|
391
|
+
const ctx = make("session-active-reset");
|
|
392
|
+
activate(ctx);
|
|
393
|
+
await runtime.bind(ctx, cap);
|
|
394
|
+
const override = { provider: "fixture", modelId: "coordinator-fast", thinking: "high" };
|
|
395
|
+
assert.equal((await runtime.setSessionProfileOverride("coordinator", override)).status, "active");
|
|
396
|
+
const result = await runtime.resetSessionProfileOverrides();
|
|
397
|
+
assert.equal(result.status, "active");
|
|
398
|
+
assert.equal(ctx.model.id, "coordinator");
|
|
399
|
+
assert.equal(ctx.thinkingLevel, "off");
|
|
400
|
+
assert.equal(replay(ctx.sessionManager.getBranch()).profileOverrides.size, 0);
|
|
401
|
+
assert.equal(runtime.state().controlMode, "automatic");
|
|
402
|
+
}));
|
|
403
|
+
|
|
404
|
+
for (const operation of ["clear", "reset"]) {
|
|
405
|
+
test(`partial active ${operation} failure restores the prior native pair and override`, async () =>
|
|
406
|
+
await environment(async ({ runtime, make, activate }) => {
|
|
407
|
+
const ctx = make(`session-partial-${operation}`);
|
|
408
|
+
activate(ctx);
|
|
409
|
+
await runtime.bind(ctx, cap);
|
|
410
|
+
const override = { provider: "fixture", modelId: "coordinator-fast", thinking: "high" };
|
|
411
|
+
assert.equal((await runtime.setSessionProfileOverride("coordinator", override)).status, "active");
|
|
412
|
+
ctx.failNextThinkingLevel = "off";
|
|
413
|
+
const result =
|
|
414
|
+
operation === "clear"
|
|
415
|
+
? await runtime.setSessionProfileOverride("coordinator", null)
|
|
416
|
+
: await runtime.resetSessionProfileOverrides();
|
|
417
|
+
assert.equal(result.status, "blocked");
|
|
418
|
+
assert.equal(ctx.model.id, "coordinator-fast");
|
|
419
|
+
assert.equal(ctx.thinkingLevel, "high");
|
|
420
|
+
assert.deepEqual(replay(ctx.sessionManager.getBranch()).profileOverrides.get("coordinator"), override);
|
|
421
|
+
assert.equal(runtime.state().runtimeStatus, "blocked");
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
@@ -100,6 +100,64 @@ function started() {
|
|
|
100
100
|
m.apply({ type: "handoff-state", handoffId: "d-a1", state: "configured", observedPair: pair });
|
|
101
101
|
return m;
|
|
102
102
|
}
|
|
103
|
+
const recoveryRequest = (assignmentId = "a1") => ({
|
|
104
|
+
type: "recovery-request-accepted",
|
|
105
|
+
recovery: {
|
|
106
|
+
id: "recovery",
|
|
107
|
+
assignmentId,
|
|
108
|
+
assessmentHandoffId: "return",
|
|
109
|
+
baseReportRevision: 1,
|
|
110
|
+
request: "recover evidence",
|
|
111
|
+
requestedPaths: ["recovery.txt"],
|
|
112
|
+
paths: ["/tmp/recovery.txt"],
|
|
113
|
+
state: "requested",
|
|
114
|
+
requestHandoffId: "recovery-request",
|
|
115
|
+
supplementRevision: 0,
|
|
116
|
+
},
|
|
117
|
+
handoff: {
|
|
118
|
+
id: "recovery-request",
|
|
119
|
+
kind: "recovery-request",
|
|
120
|
+
assignmentId,
|
|
121
|
+
text: "recover evidence",
|
|
122
|
+
from: "coordinator",
|
|
123
|
+
to: "executor",
|
|
124
|
+
executionId: "e3",
|
|
125
|
+
toolCallId: "recover-call",
|
|
126
|
+
basisUserEntryId: "user",
|
|
127
|
+
state: "pending",
|
|
128
|
+
reportRevision: 0,
|
|
129
|
+
limitations: [],
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const recoverySupplement = (revision = 1) => ({
|
|
133
|
+
type: "recovery-supplement-accepted",
|
|
134
|
+
recoveryId: "recovery",
|
|
135
|
+
handoff: {
|
|
136
|
+
id: "recovery-return",
|
|
137
|
+
kind: "recovery-return",
|
|
138
|
+
assignmentId: "a1",
|
|
139
|
+
text: "recovered evidence",
|
|
140
|
+
from: "executor",
|
|
141
|
+
to: "coordinator",
|
|
142
|
+
executionId: "e4",
|
|
143
|
+
toolCallId: "supplement-call",
|
|
144
|
+
basisUserEntryId: "user",
|
|
145
|
+
state: "pending",
|
|
146
|
+
reportRevision: revision,
|
|
147
|
+
outcome: "completed",
|
|
148
|
+
limitations: [],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
function recoveryReady() {
|
|
152
|
+
const m = started();
|
|
153
|
+
m.apply(opened("e2", "executor", "a1"));
|
|
154
|
+
m.apply(returned());
|
|
155
|
+
m.apply(bound("e2"));
|
|
156
|
+
m.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
|
|
157
|
+
m.apply(opened("e3", "coordinator", "a1"));
|
|
158
|
+
m.apply(recoveryRequest());
|
|
159
|
+
return m;
|
|
160
|
+
}
|
|
103
161
|
const reservation = (revision) => ({
|
|
104
162
|
id: "reservation",
|
|
105
163
|
selectionRevision: revision,
|
|
@@ -183,7 +241,7 @@ test("replacement retains old assignment and requires old execution to be resolv
|
|
|
183
241
|
});
|
|
184
242
|
assert.throws(
|
|
185
243
|
() => m.apply(replacement),
|
|
186
|
-
(e) => e.code === "
|
|
244
|
+
(e) => e.code === "unresolved_worker_execution",
|
|
187
245
|
);
|
|
188
246
|
m.apply(bound("e2"));
|
|
189
247
|
m.apply(replacement);
|
|
@@ -192,6 +250,144 @@ test("replacement retains old assignment and requires old execution to be resolv
|
|
|
192
250
|
assert.equal(m.state.assignments.get("a1").contract, "fixture contract");
|
|
193
251
|
assert.equal(m.state.assignmentId, "a2");
|
|
194
252
|
});
|
|
253
|
+
test("worker identity rejects foreign returns and recovery requests", () => {
|
|
254
|
+
const m = machine();
|
|
255
|
+
m.apply(control("coordinator"));
|
|
256
|
+
m.apply(opened("e1", "coordinator"));
|
|
257
|
+
const helperDelegate = delegated();
|
|
258
|
+
helperDelegate.handoff.to = "helper";
|
|
259
|
+
m.apply(helperDelegate);
|
|
260
|
+
m.apply(bound("e1"));
|
|
261
|
+
m.apply({ type: "handoff-state", handoffId: "d-a1", state: "configured", observedPair: pair });
|
|
262
|
+
m.apply(opened("e2", "helper", "a1"));
|
|
263
|
+
const foreignReturn = returned();
|
|
264
|
+
assert.throws(
|
|
265
|
+
() => m.apply(foreignReturn),
|
|
266
|
+
(error) => error.code === "return_identity_mismatch",
|
|
267
|
+
);
|
|
268
|
+
const helperReturn = returned();
|
|
269
|
+
helperReturn.handoff.from = "helper";
|
|
270
|
+
m.apply(helperReturn);
|
|
271
|
+
m.apply(bound("e2"));
|
|
272
|
+
m.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
|
|
273
|
+
m.apply(opened("e3", "coordinator", "a1"));
|
|
274
|
+
const foreignRecovery = recoveryRequest();
|
|
275
|
+
assert.throws(
|
|
276
|
+
() => m.apply(foreignRecovery),
|
|
277
|
+
(error) => error.code === "recovery_not_available",
|
|
278
|
+
);
|
|
279
|
+
foreignRecovery.handoff.to = "helper";
|
|
280
|
+
m.apply(foreignRecovery);
|
|
281
|
+
assert.equal(m.state.recoveryId, "recovery");
|
|
282
|
+
assert.equal(m.state.handoffs.get("recovery-request").to, "helper");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("wrong-worker return, selection, supplement, and retry events are symmetric and state-preserving", () => {
|
|
286
|
+
const startedFor = (worker) => {
|
|
287
|
+
const m = machine();
|
|
288
|
+
m.apply(control("coordinator"));
|
|
289
|
+
m.apply(opened("e1", "coordinator"));
|
|
290
|
+
const delegation = delegated();
|
|
291
|
+
delegation.handoff.to = worker;
|
|
292
|
+
m.apply(delegation);
|
|
293
|
+
m.apply(bound("e1"));
|
|
294
|
+
m.apply({ type: "handoff-state", handoffId: "d-a1", state: "configured", observedPair: pair });
|
|
295
|
+
return m;
|
|
296
|
+
};
|
|
297
|
+
const returnFrom = (worker) => {
|
|
298
|
+
const value = returned();
|
|
299
|
+
value.handoff.from = worker;
|
|
300
|
+
return value;
|
|
301
|
+
};
|
|
302
|
+
const requestTo = (worker) => {
|
|
303
|
+
const value = recoveryRequest();
|
|
304
|
+
value.handoff.to = worker;
|
|
305
|
+
return value;
|
|
306
|
+
};
|
|
307
|
+
const supplementFrom = (worker) => {
|
|
308
|
+
const value = recoverySupplement();
|
|
309
|
+
value.handoff.from = worker;
|
|
310
|
+
return value;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
for (const [worker, foreign] of [
|
|
314
|
+
["executor", "helper"],
|
|
315
|
+
["helper", "executor"],
|
|
316
|
+
]) {
|
|
317
|
+
const returning = startedFor(worker);
|
|
318
|
+
returning.apply(opened("e2", worker, "a1"));
|
|
319
|
+
const beforeReturn = returning.state;
|
|
320
|
+
assert.throws(
|
|
321
|
+
() => returning.apply(returnFrom(foreign)),
|
|
322
|
+
(error) => error.code === "return_identity_mismatch",
|
|
323
|
+
);
|
|
324
|
+
assert.equal(returning.state, beforeReturn);
|
|
325
|
+
assert.equal(returning.state.assignments.get("a1").state, "outstanding");
|
|
326
|
+
|
|
327
|
+
const selecting = startedFor(worker);
|
|
328
|
+
selecting.apply(control(foreign));
|
|
329
|
+
const beforeSelection = selecting.state;
|
|
330
|
+
assert.throws(
|
|
331
|
+
() =>
|
|
332
|
+
selecting.apply({
|
|
333
|
+
type: "selection-changed",
|
|
334
|
+
assignmentId: "a1",
|
|
335
|
+
selection: { revision: 1, selected: [], unresolved: [], withdrawals: [] },
|
|
336
|
+
}),
|
|
337
|
+
(error) => error.code === "selection_wrong_phase",
|
|
338
|
+
);
|
|
339
|
+
assert.equal(selecting.state, beforeSelection);
|
|
340
|
+
assert.equal(selecting.state.selections.has("a1"), false);
|
|
341
|
+
|
|
342
|
+
const retrying = startedFor(worker);
|
|
343
|
+
retrying.apply(opened("e2", worker, "a1"));
|
|
344
|
+
retrying.apply(returnFrom(worker));
|
|
345
|
+
retrying.apply({ type: "handoff-state", handoffId: "return", state: "blocked", reason: "fixture gap" });
|
|
346
|
+
retrying.apply(control(foreign));
|
|
347
|
+
const beforeRetry = retrying.state;
|
|
348
|
+
assert.throws(
|
|
349
|
+
() =>
|
|
350
|
+
retrying.apply({
|
|
351
|
+
type: "handoff-retry-requested",
|
|
352
|
+
handoffId: "return",
|
|
353
|
+
attemptId: "retry",
|
|
354
|
+
executionId: "e2",
|
|
355
|
+
toolCallId: "retry-call",
|
|
356
|
+
reportRevision: 1,
|
|
357
|
+
selectionRevision: 0,
|
|
358
|
+
}),
|
|
359
|
+
(error) => error.code === "invalid_return_retry",
|
|
360
|
+
);
|
|
361
|
+
assert.equal(retrying.state, beforeRetry);
|
|
362
|
+
assert.equal(retrying.state.handoffs.get("return").state, "blocked");
|
|
363
|
+
assert.equal(retrying.state.attempts.has("return"), false);
|
|
364
|
+
|
|
365
|
+
const supplementing = startedFor(worker);
|
|
366
|
+
supplementing.apply(opened("e2", worker, "a1"));
|
|
367
|
+
supplementing.apply(returnFrom(worker));
|
|
368
|
+
supplementing.apply(bound("e2"));
|
|
369
|
+
supplementing.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
|
|
370
|
+
supplementing.apply(opened("e3", "coordinator", "a1"));
|
|
371
|
+
supplementing.apply(requestTo(worker));
|
|
372
|
+
supplementing.apply(bound("e3"));
|
|
373
|
+
supplementing.apply({
|
|
374
|
+
type: "handoff-state",
|
|
375
|
+
handoffId: "recovery-request",
|
|
376
|
+
state: "configured",
|
|
377
|
+
observedPair: pair,
|
|
378
|
+
});
|
|
379
|
+
supplementing.apply(opened("e4", worker, "a1"));
|
|
380
|
+
const beforeSupplement = supplementing.state;
|
|
381
|
+
assert.throws(
|
|
382
|
+
() => supplementing.apply(supplementFrom(foreign)),
|
|
383
|
+
(error) => error.code === "recovery_not_returnable",
|
|
384
|
+
);
|
|
385
|
+
assert.equal(supplementing.state, beforeSupplement);
|
|
386
|
+
assert.equal(supplementing.state.recoveries.get("recovery").state, "reading");
|
|
387
|
+
assert.equal(supplementing.state.handoffs.has("recovery-return"), false);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
195
391
|
test("saved report revisions and retries preserve exact earlier events", () => {
|
|
196
392
|
const m = started();
|
|
197
393
|
m.apply(opened("e2", "executor", "a1"));
|
|
@@ -213,6 +409,86 @@ test("saved report revisions and retries preserve exact earlier events", () => {
|
|
|
213
409
|
assert.equal([...m.state.events.values()].find((e) => e.eventId === first.eventId).data.handoff.text, "exact report");
|
|
214
410
|
assert.equal(m.state.assignments.size, 1);
|
|
215
411
|
});
|
|
412
|
+
test("recovery transitions preserve the original report and reject foreign identities", () => {
|
|
413
|
+
const ready = () => {
|
|
414
|
+
const m = started();
|
|
415
|
+
m.apply(opened("e2", "executor", "a1"));
|
|
416
|
+
m.apply(returned());
|
|
417
|
+
m.apply(bound("e2"));
|
|
418
|
+
m.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
|
|
419
|
+
m.apply(opened("e3", "coordinator", "a1"));
|
|
420
|
+
return m;
|
|
421
|
+
};
|
|
422
|
+
const foreign = ready();
|
|
423
|
+
assert.throws(
|
|
424
|
+
() => foreign.apply(recoveryRequest("foreign")),
|
|
425
|
+
(error) => error.code === "recovery_not_available",
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const m = ready();
|
|
429
|
+
const base = structuredClone(m.state.handoffs.get("return"));
|
|
430
|
+
const request = m.apply(recoveryRequest());
|
|
431
|
+
m.set(request);
|
|
432
|
+
assert.equal(m.state.recoveries.size, 1, "exact event replay is idempotent");
|
|
433
|
+
m.apply(bound("e3"));
|
|
434
|
+
m.apply({ type: "handoff-state", handoffId: "recovery-request", state: "configured", observedPair: pair });
|
|
435
|
+
assert.equal(m.state.recoveries.get("recovery").state, "reading");
|
|
436
|
+
m.apply(opened("e4", "executor", "a1"));
|
|
437
|
+
m.apply(recoverySupplement());
|
|
438
|
+
assert.equal(m.state.recoveries.get("recovery").state, "returning");
|
|
439
|
+
assert.deepEqual(m.state.handoffs.get("return"), base, "the base report remains immutable");
|
|
440
|
+
assert.throws(
|
|
441
|
+
() => m.apply(recoverySupplement(3)),
|
|
442
|
+
(error) => error.code === "invalid_report_revision",
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("assessment cannot resume while attached recovery remains unsettled", () => {
|
|
447
|
+
const m = recoveryReady();
|
|
448
|
+
const before = m.state;
|
|
449
|
+
assert.throws(
|
|
450
|
+
() =>
|
|
451
|
+
m.apply({
|
|
452
|
+
type: "assessment-resumed",
|
|
453
|
+
handoffId: "return",
|
|
454
|
+
basisUserEntryId: "new-user",
|
|
455
|
+
}),
|
|
456
|
+
(error) => error.code === "recovery_outstanding",
|
|
457
|
+
);
|
|
458
|
+
assert.equal(m.state, before);
|
|
459
|
+
assert.equal(m.state.assessment.view, "suspended");
|
|
460
|
+
assert.equal(m.state.recoveryId, "recovery");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
for (const outcome of ["cancelled", "deferred"]) {
|
|
464
|
+
test(`${outcome} unit closure atomically disposes attached recovery`, () => {
|
|
465
|
+
const m = recoveryReady();
|
|
466
|
+
m.apply({ type: "unit-closed", unitId: "u1", outcome, assessment: `${outcome} with recovery` });
|
|
467
|
+
assert.equal(m.state.unitId, undefined);
|
|
468
|
+
assert.equal(m.state.assignmentId, undefined);
|
|
469
|
+
assert.equal(m.state.pendingId, undefined);
|
|
470
|
+
assert.equal(m.state.recoveryId, undefined);
|
|
471
|
+
assert.equal(m.state.recoveries.get("recovery").state, "cancelled");
|
|
472
|
+
assert.equal(m.state.recoveries.get("recovery").cancellationReason, `${outcome} with recovery`);
|
|
473
|
+
assert.equal(m.state.handoffs.get("recovery-request").state, "superseded");
|
|
474
|
+
assert.equal(m.state.handoffs.get("return").text, "exact report");
|
|
475
|
+
m.apply(opened(`e-${outcome}`, "coordinator"));
|
|
476
|
+
m.apply(delegated(`a-${outcome}`, `u-${outcome}`, [`a-${outcome}`], `e-${outcome}`));
|
|
477
|
+
assert.equal(m.state.unitId, `u-${outcome}`);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
test("accepted unit closure rejects attached recovery without changing state", () => {
|
|
482
|
+
const m = recoveryReady();
|
|
483
|
+
const before = m.state;
|
|
484
|
+
assert.throws(
|
|
485
|
+
() => m.apply({ type: "unit-closed", unitId: "u1", outcome: "accepted", assessment: "incorrect" }),
|
|
486
|
+
(error) => error.code === "unfinished_work",
|
|
487
|
+
);
|
|
488
|
+
assert.equal(m.state, before);
|
|
489
|
+
assert.equal(m.state.recoveryId, "recovery");
|
|
490
|
+
});
|
|
491
|
+
|
|
216
492
|
test("assessment resumption rejects stale selection and preserves suspension on failure", () => {
|
|
217
493
|
const m = started();
|
|
218
494
|
m.apply(opened("e2", "executor", "a1"));
|
|
@@ -273,3 +549,38 @@ test("accepted closure cannot conceal work; later work creates a new unit", () =
|
|
|
273
549
|
assert.equal(m.state.unitId, "u2");
|
|
274
550
|
assert.equal(m.state.units.size, 2);
|
|
275
551
|
});
|
|
552
|
+
test("profile override events are complete-pair, branch-replayable session state", () => {
|
|
553
|
+
const m = machine();
|
|
554
|
+
const coordinator = { provider: "fixture", modelId: "coordinator-fast", thinking: "high" };
|
|
555
|
+
const executor = { provider: "fixture", modelId: "executor-cheap", thinking: "low" };
|
|
556
|
+
m.apply({
|
|
557
|
+
type: "profile-overrides",
|
|
558
|
+
overrides: { coordinator },
|
|
559
|
+
reason: "fixture session override",
|
|
560
|
+
});
|
|
561
|
+
assert.deepEqual(m.state.profileOverrides.get("coordinator"), coordinator);
|
|
562
|
+
assert.equal(m.state.profileOverrides.has("executor"), false);
|
|
563
|
+
m.apply({
|
|
564
|
+
type: "profile-overrides",
|
|
565
|
+
overrides: { executor },
|
|
566
|
+
reason: "fixture session override",
|
|
567
|
+
});
|
|
568
|
+
assert.deepEqual(m.state.profileOverrides.get("executor"), executor);
|
|
569
|
+
m.apply({
|
|
570
|
+
type: "profile-overrides",
|
|
571
|
+
overrides: { coordinator: null, executor: null },
|
|
572
|
+
reason: "fixture reset",
|
|
573
|
+
});
|
|
574
|
+
assert.equal(m.state.profileOverrides.size, 0);
|
|
575
|
+
assert.throws(
|
|
576
|
+
() =>
|
|
577
|
+
parseRoutingEvent(
|
|
578
|
+
m.event({
|
|
579
|
+
type: "profile-overrides",
|
|
580
|
+
overrides: { coordinator: { provider: "fixture", modelId: "bad", thinking: "unknown" } },
|
|
581
|
+
reason: "invalid",
|
|
582
|
+
}),
|
|
583
|
+
),
|
|
584
|
+
(e) => e.code === "invalid_profile_overrides",
|
|
585
|
+
);
|
|
586
|
+
});
|