@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
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { resolveCognitiveRoutingState } from "../../dist/cognitive-routing-v2/config.js";
|
|
6
|
+
import { RoutingRuntime } from "../../dist/cognitive-routing-v2/runtime.js";
|
|
7
|
+
import { replay } from "../../dist/cognitive-routing-v2/state.js";
|
|
8
|
+
import { fixture } from "../fixtures/routing-native.js";
|
|
9
|
+
|
|
10
|
+
const coordinator = { provider: "openai", model: "gpt-4o", thinking: "off" };
|
|
11
|
+
const worker = { provider: "openai", model: "gpt-4.1-mini", thinking: "off" };
|
|
12
|
+
const alternateExecutor = { provider: "openai", model: "gpt-4.1", thinking: "off" };
|
|
13
|
+
const bothConfig = {
|
|
14
|
+
enabled: true,
|
|
15
|
+
delegation: "both",
|
|
16
|
+
projection: true,
|
|
17
|
+
profiles: { coordinator, helper: worker, executor: worker },
|
|
18
|
+
};
|
|
19
|
+
const helperConfig = {
|
|
20
|
+
enabled: true,
|
|
21
|
+
delegation: "helper",
|
|
22
|
+
projection: true,
|
|
23
|
+
profiles: { coordinator, helper: worker },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function registry(available = ["coordinator", "helper", "executor"]) {
|
|
27
|
+
const models = {
|
|
28
|
+
coordinator: { provider: "test", id: "coordinator", reasoning: true },
|
|
29
|
+
helper: { provider: "test", id: "helper", reasoning: true },
|
|
30
|
+
executor: { provider: "test", id: "executor", reasoning: true },
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
find(_provider, id) {
|
|
34
|
+
return available.includes(id) ? models[id] : undefined;
|
|
35
|
+
},
|
|
36
|
+
async getApiKeyAndHeaders() {
|
|
37
|
+
return { ok: true };
|
|
38
|
+
},
|
|
39
|
+
clampThinkingLevel(_model, level) {
|
|
40
|
+
return level;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const profile = (model) => ({ provider: "test", model, thinking: "off" });
|
|
46
|
+
|
|
47
|
+
test("delegation modes validate only enabled worker presets and preserve the executor default", async () => {
|
|
48
|
+
const legacy = await resolveCognitiveRoutingState(
|
|
49
|
+
{
|
|
50
|
+
cognitiveRouting: {
|
|
51
|
+
enabled: true,
|
|
52
|
+
profiles: {
|
|
53
|
+
coordinator: profile("coordinator"),
|
|
54
|
+
executor: profile("executor"),
|
|
55
|
+
helper: profile("missing-unused-helper"),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{},
|
|
60
|
+
{ modelRegistry: registry() },
|
|
61
|
+
);
|
|
62
|
+
assert.equal(legacy.delegation, "executor");
|
|
63
|
+
assert.equal(legacy.delegationSource, "default");
|
|
64
|
+
assert.equal(legacy.effective, true, "an unavailable unused Helper preset does not block executor-only mode");
|
|
65
|
+
|
|
66
|
+
const helperOnly = await resolveCognitiveRoutingState(
|
|
67
|
+
{
|
|
68
|
+
cognitiveRouting: {
|
|
69
|
+
enabled: true,
|
|
70
|
+
delegation: "helper",
|
|
71
|
+
profiles: { coordinator: profile("coordinator"), helper: profile("helper") },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{},
|
|
75
|
+
{ modelRegistry: registry() },
|
|
76
|
+
);
|
|
77
|
+
assert.equal(helperOnly.effective, true, "helper-only does not require Executor");
|
|
78
|
+
|
|
79
|
+
const missingBoth = await resolveCognitiveRoutingState(
|
|
80
|
+
{
|
|
81
|
+
cognitiveRouting: {
|
|
82
|
+
enabled: true,
|
|
83
|
+
delegation: "both",
|
|
84
|
+
profiles: { coordinator: profile("coordinator"), helper: profile("helper") },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{},
|
|
88
|
+
{ modelRegistry: registry() },
|
|
89
|
+
);
|
|
90
|
+
assert.equal(missingBoth.effective, false);
|
|
91
|
+
assert.equal(missingBoth.blockingReason.code, "profile_missing");
|
|
92
|
+
assert.match(missingBoth.blockingReason.message, /executor/i);
|
|
93
|
+
|
|
94
|
+
const personalMode = await resolveCognitiveRoutingState(
|
|
95
|
+
{
|
|
96
|
+
cognitiveRouting: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
delegation: "executor",
|
|
99
|
+
profiles: { coordinator: profile("coordinator"), executor: profile("executor") },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
cognitiveRouting: {
|
|
104
|
+
delegation: "helper",
|
|
105
|
+
profiles: { helper: profile("helper") },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{ modelRegistry: registry() },
|
|
109
|
+
);
|
|
110
|
+
assert.equal(personalMode.effective, true);
|
|
111
|
+
assert.equal(personalMode.delegation, "helper");
|
|
112
|
+
assert.equal(personalMode.delegationSource, "personal");
|
|
113
|
+
|
|
114
|
+
const malformedUnused = await resolveCognitiveRoutingState(
|
|
115
|
+
{
|
|
116
|
+
cognitiveRouting: {
|
|
117
|
+
enabled: true,
|
|
118
|
+
profiles: {
|
|
119
|
+
coordinator: profile("coordinator"),
|
|
120
|
+
executor: profile("executor"),
|
|
121
|
+
helper: { provider: "test", model: "helper" },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{},
|
|
126
|
+
{ modelRegistry: registry() },
|
|
127
|
+
);
|
|
128
|
+
assert.equal(malformedUnused.configValid, false, "malformed unused presets remain invalid configuration");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a duplicate delegation operation cannot change its recorded worker", { timeout: 30000 }, async () => {
|
|
132
|
+
const originalInvoke = RoutingRuntime.prototype.invoke;
|
|
133
|
+
let checked = false;
|
|
134
|
+
RoutingRuntime.prototype.invoke = async function (name, id, input, signal, ctx) {
|
|
135
|
+
const result = await originalInvoke.call(this, name, id, input, signal, ctx);
|
|
136
|
+
if (!checked && name === "freeflow_delegate" && input.worker === "helper") {
|
|
137
|
+
checked = true;
|
|
138
|
+
this.receipts.clear();
|
|
139
|
+
const conflict = await originalInvoke.call(this, name, id, { ...input, worker: "executor" }, signal, ctx);
|
|
140
|
+
assert.equal(conflict.details.code, "operation_conflict");
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
const result = await fixture(
|
|
146
|
+
(n) =>
|
|
147
|
+
n === 1
|
|
148
|
+
? [
|
|
149
|
+
{
|
|
150
|
+
name: "freeflow_delegate",
|
|
151
|
+
args: { operation: "assign", worker: "helper", contract: "Keep the accepted Helper identity." },
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
: n === 2
|
|
155
|
+
? [
|
|
156
|
+
{
|
|
157
|
+
name: "freeflow_return",
|
|
158
|
+
args: { operation: "submit", report: "Helper identity retained.", outcome: "completed" },
|
|
159
|
+
},
|
|
160
|
+
]
|
|
161
|
+
: n === 3
|
|
162
|
+
? [
|
|
163
|
+
{
|
|
164
|
+
name: "freeflow_unit",
|
|
165
|
+
args: { operation: "close", outcome: "accepted", assessment: "Worker identity is stable." },
|
|
166
|
+
},
|
|
167
|
+
]
|
|
168
|
+
: [],
|
|
169
|
+
true,
|
|
170
|
+
undefined,
|
|
171
|
+
true,
|
|
172
|
+
{ cognitiveRouting: bothConfig, maxRequests: 5 },
|
|
173
|
+
);
|
|
174
|
+
assert.equal(checked, true);
|
|
175
|
+
assert.equal([...result.state.handoffs.values()].find((handoff) => handoff.kind === "delegate").to, "helper");
|
|
176
|
+
} finally {
|
|
177
|
+
RoutingRuntime.prototype.invoke = originalInvoke;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("both mode requires an explicit worker and shares Helper history with Executor", { timeout: 30000 }, async () => {
|
|
182
|
+
let helperReadId;
|
|
183
|
+
const result = await fixture(
|
|
184
|
+
(n, body, manager) => {
|
|
185
|
+
const wire = JSON.stringify(body);
|
|
186
|
+
if (n === 1) return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Ambiguous worker." } }];
|
|
187
|
+
if (n === 2) {
|
|
188
|
+
assert.equal(replay(manager.getBranch()).assignments.size, 0, "ambiguous delegation appends no assignment");
|
|
189
|
+
return [
|
|
190
|
+
{
|
|
191
|
+
name: "freeflow_delegate",
|
|
192
|
+
args: { operation: "assign", worker: "helper", contract: "Read the marker for later implementation." },
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
if (n === 3) {
|
|
197
|
+
assert.equal(body.model, "gpt-4.1-mini");
|
|
198
|
+
assert.equal(replay(manager.getBranch()).profile, "helper");
|
|
199
|
+
return [{ name: "read", args: { path: "unselected.txt" } }];
|
|
200
|
+
}
|
|
201
|
+
if (n === 4) {
|
|
202
|
+
const read = manager
|
|
203
|
+
.getBranch()
|
|
204
|
+
.filter((entry) => entry.message?.toolName === "read")
|
|
205
|
+
.at(-1);
|
|
206
|
+
helperReadId = read.id;
|
|
207
|
+
assert.equal(read.message.isError, false);
|
|
208
|
+
assert.match(wire, /UNSELECTED_PRIVATE_BODY_93/);
|
|
209
|
+
return [
|
|
210
|
+
{
|
|
211
|
+
name: "freeflow_return",
|
|
212
|
+
args: { operation: "submit", report: "Helper context gathered.", outcome: "completed" },
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
if (n === 5) {
|
|
217
|
+
assert.equal(body.model, "gpt-4o");
|
|
218
|
+
assert.equal(replay(manager.getBranch()).profile, "coordinator");
|
|
219
|
+
assert.doesNotMatch(
|
|
220
|
+
wire,
|
|
221
|
+
/UNSELECTED_PRIVATE_BODY_93/,
|
|
222
|
+
"unselected Helper result stays out of Coordinator view",
|
|
223
|
+
);
|
|
224
|
+
return [
|
|
225
|
+
{
|
|
226
|
+
name: "freeflow_delegate",
|
|
227
|
+
args: { operation: "assign", worker: "executor", contract: "Use the gathered marker and report." },
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
if (n === 6) {
|
|
232
|
+
assert.equal(body.model, "gpt-4.1-mini");
|
|
233
|
+
assert.equal(replay(manager.getBranch()).profile, "executor");
|
|
234
|
+
assert.match(wire, /UNSELECTED_PRIVATE_BODY_93/, "Executor inherits ordinary Helper history");
|
|
235
|
+
assert.equal(
|
|
236
|
+
manager.getBranch().filter((entry) => entry.message?.toolName === "read").length,
|
|
237
|
+
1,
|
|
238
|
+
"Executor does not rerun Helper's read",
|
|
239
|
+
);
|
|
240
|
+
return [
|
|
241
|
+
{
|
|
242
|
+
name: "freeflow_project",
|
|
243
|
+
args: { operation: "add", refs: [`ctx:${helperReadId}`] },
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "freeflow_return",
|
|
247
|
+
args: { operation: "submit", report: "Executor used shared context.", outcome: "completed" },
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
if (n === 7) {
|
|
252
|
+
assert.equal(body.model, "gpt-4o");
|
|
253
|
+
assert.match(wire, /UNSELECTED_PRIVATE_BODY_93/);
|
|
254
|
+
assert.match(wire, /producer: helper/);
|
|
255
|
+
return [
|
|
256
|
+
{
|
|
257
|
+
name: "freeflow_unit",
|
|
258
|
+
args: { operation: "close", outcome: "accepted", assessment: "Shared worker context observed." },
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
return [];
|
|
263
|
+
},
|
|
264
|
+
true,
|
|
265
|
+
undefined,
|
|
266
|
+
true,
|
|
267
|
+
{ cognitiveRouting: bothConfig, maxRequests: 9 },
|
|
268
|
+
);
|
|
269
|
+
assert.equal(result.requests.length, 8);
|
|
270
|
+
assert.equal(result.state.units.values().next().value.state, "closed");
|
|
271
|
+
const delegates = [...result.state.handoffs.values()].filter((handoff) => handoff.kind === "delegate");
|
|
272
|
+
assert.deepEqual(
|
|
273
|
+
delegates.map((handoff) => handoff.to),
|
|
274
|
+
["helper", "executor"],
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test(
|
|
279
|
+
"helper-only rejects Executor delegation and recovers through the recorded Helper",
|
|
280
|
+
{ timeout: 30000 },
|
|
281
|
+
async () => {
|
|
282
|
+
let assignmentId, baseReportId;
|
|
283
|
+
const result = await fixture(
|
|
284
|
+
(n, body, manager) => {
|
|
285
|
+
if (n === 1)
|
|
286
|
+
return [
|
|
287
|
+
{
|
|
288
|
+
name: "freeflow_delegate",
|
|
289
|
+
args: { operation: "assign", worker: "executor", contract: "Disabled target." },
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
if (n === 2) {
|
|
293
|
+
assert.equal(replay(manager.getBranch()).assignments.size, 0);
|
|
294
|
+
return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return then recover." } }];
|
|
295
|
+
}
|
|
296
|
+
if (n === 3) {
|
|
297
|
+
const state = replay(manager.getBranch());
|
|
298
|
+
assignmentId = state.assignmentId;
|
|
299
|
+
assert.equal(state.profile, "helper");
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
name: "freeflow_return",
|
|
303
|
+
args: { operation: "submit", report: "Helper base report.", outcome: "completed" },
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
if (n === 4) {
|
|
308
|
+
const state = replay(manager.getBranch());
|
|
309
|
+
baseReportId = state.assignments.get(assignmentId).returnHandoffId;
|
|
310
|
+
return [
|
|
311
|
+
{
|
|
312
|
+
name: "freeflow_unit",
|
|
313
|
+
args: { operation: "recover", request: "Read the approved recovery file.", paths: ["recovery.txt"] },
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
if (n === 5) {
|
|
318
|
+
assert.equal(replay(manager.getBranch()).profile, "helper");
|
|
319
|
+
return [{ name: "read", args: { path: "recovery.txt" } }];
|
|
320
|
+
}
|
|
321
|
+
if (n === 6)
|
|
322
|
+
return [
|
|
323
|
+
{
|
|
324
|
+
name: "freeflow_return",
|
|
325
|
+
args: { operation: "supplement", report: "Helper recovery supplement.", outcome: "completed" },
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
if (n === 7) {
|
|
329
|
+
const state = replay(manager.getBranch());
|
|
330
|
+
assert.equal(state.assignmentId, assignmentId);
|
|
331
|
+
assert.equal(state.assignments.get(assignmentId).returnHandoffId, baseReportId);
|
|
332
|
+
assert.equal(state.handoffs.get(baseReportId).text, "Helper base report.");
|
|
333
|
+
const completed = [...state.recoveries.values()].at(-1);
|
|
334
|
+
assert.equal(completed.state, "completed");
|
|
335
|
+
assert.equal(state.handoffs.get(completed.requestHandoffId).to, "helper");
|
|
336
|
+
assert.equal(state.handoffs.get(completed.supplementHandoffId).from, "helper");
|
|
337
|
+
return [
|
|
338
|
+
{
|
|
339
|
+
name: "freeflow_unit",
|
|
340
|
+
args: { operation: "close", outcome: "accepted", assessment: "Helper recovery retained lineage." },
|
|
341
|
+
},
|
|
342
|
+
];
|
|
343
|
+
}
|
|
344
|
+
return [];
|
|
345
|
+
},
|
|
346
|
+
true,
|
|
347
|
+
undefined,
|
|
348
|
+
true,
|
|
349
|
+
{ cognitiveRouting: helperConfig, maxRequests: 9 },
|
|
350
|
+
);
|
|
351
|
+
assert.equal(result.requests.length, 8);
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
test("new input routes through Coordinator before the same Helper assignment resumes", { timeout: 30000 }, async () => {
|
|
356
|
+
let assignmentId;
|
|
357
|
+
const result = await fixture(
|
|
358
|
+
(n, body, manager) => {
|
|
359
|
+
if (n === 1)
|
|
360
|
+
return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Keep the Helper task." } }];
|
|
361
|
+
if (n === 2) {
|
|
362
|
+
assignmentId = replay(manager.getBranch()).assignmentId;
|
|
363
|
+
assert.equal(replay(manager.getBranch()).profile, "helper");
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
if (n === 3) {
|
|
367
|
+
const state = replay(manager.getBranch());
|
|
368
|
+
assert.equal(body.model, "gpt-4o");
|
|
369
|
+
assert.equal(state.profile, "coordinator");
|
|
370
|
+
assert.equal(state.assignments.get(assignmentId).state, "outstanding");
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
if (n === 4) {
|
|
374
|
+
const state = replay(manager.getBranch());
|
|
375
|
+
assert.equal(state.profile, "helper");
|
|
376
|
+
assert.equal(state.assignmentId, assignmentId);
|
|
377
|
+
assert.doesNotMatch(JSON.stringify(body), /New delivered user input requires Coordinator attention/);
|
|
378
|
+
return [
|
|
379
|
+
{
|
|
380
|
+
name: "freeflow_return",
|
|
381
|
+
args: { operation: "submit", report: "Helper resumed after Coordinator attention.", outcome: "completed" },
|
|
382
|
+
},
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
return [];
|
|
386
|
+
},
|
|
387
|
+
true,
|
|
388
|
+
async ({ session }) => {
|
|
389
|
+
await session.prompt("Consider this new direction before continuing.");
|
|
390
|
+
await session.waitForIdle();
|
|
391
|
+
await session.prompt("/freeflow resume");
|
|
392
|
+
await session.waitForIdle();
|
|
393
|
+
},
|
|
394
|
+
true,
|
|
395
|
+
{ cognitiveRouting: helperConfig, maxRequests: 6 },
|
|
396
|
+
);
|
|
397
|
+
assert.equal(result.requests.length, 5);
|
|
398
|
+
assert.equal(result.state.assignments.get(assignmentId).state, "returned");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("an accepted Helper assignment survives a later executor-only mode change", { timeout: 30000 }, async () => {
|
|
402
|
+
let assignmentId;
|
|
403
|
+
const result = await fixture(
|
|
404
|
+
(n, body, manager) => {
|
|
405
|
+
if (n === 1)
|
|
406
|
+
return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Keep this Helper assignment." } }];
|
|
407
|
+
if (n === 2) {
|
|
408
|
+
assignmentId = replay(manager.getBranch()).assignmentId;
|
|
409
|
+
assert.equal(replay(manager.getBranch()).profile, "helper");
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
if (n === 3) {
|
|
413
|
+
const state = replay(manager.getBranch());
|
|
414
|
+
assert.equal(state.assignmentId, assignmentId);
|
|
415
|
+
assert.equal(state.profile, "helper");
|
|
416
|
+
assert.equal(body.model, "gpt-4.1-mini");
|
|
417
|
+
assert.notEqual(body.model, alternateExecutor.model, "resume does not use the newly enabled Executor pair");
|
|
418
|
+
return [
|
|
419
|
+
{
|
|
420
|
+
name: "freeflow_return",
|
|
421
|
+
args: { operation: "submit", report: "Original Helper assignment resumed.", outcome: "completed" },
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
if (n === 4)
|
|
426
|
+
return [
|
|
427
|
+
{
|
|
428
|
+
name: "freeflow_unit",
|
|
429
|
+
args: { operation: "close", outcome: "accepted", assessment: "Mode change did not retarget work." },
|
|
430
|
+
},
|
|
431
|
+
];
|
|
432
|
+
return [];
|
|
433
|
+
},
|
|
434
|
+
true,
|
|
435
|
+
async ({ session, cwd }) => {
|
|
436
|
+
await writeFile(
|
|
437
|
+
join(cwd, ".freeflow/config.json"),
|
|
438
|
+
JSON.stringify({
|
|
439
|
+
cognitiveRouting: {
|
|
440
|
+
enabled: true,
|
|
441
|
+
delegation: "executor",
|
|
442
|
+
projection: true,
|
|
443
|
+
profiles: { coordinator, helper: worker, executor: alternateExecutor },
|
|
444
|
+
},
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
await session.reload();
|
|
448
|
+
await session.prompt("/freeflow resume");
|
|
449
|
+
await session.waitForIdle();
|
|
450
|
+
},
|
|
451
|
+
true,
|
|
452
|
+
{ cognitiveRouting: helperConfig, maxRequests: 6 },
|
|
453
|
+
);
|
|
454
|
+
assert.equal(result.requests.length, 5);
|
|
455
|
+
assert.equal(result.state.assignments.get(assignmentId).state, "returned");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test(
|
|
459
|
+
"an outstanding Helper assignment fails closed when its preset becomes unavailable",
|
|
460
|
+
{ timeout: 30000 },
|
|
461
|
+
async () => {
|
|
462
|
+
let assignmentId, delegateHandoffId;
|
|
463
|
+
const result = await fixture(
|
|
464
|
+
(n, body, manager) => {
|
|
465
|
+
if (n === 1)
|
|
466
|
+
return [
|
|
467
|
+
{ name: "freeflow_delegate", args: { operation: "assign", contract: "Retain unavailable Helper work." } },
|
|
468
|
+
];
|
|
469
|
+
if (n === 2) {
|
|
470
|
+
const state = replay(manager.getBranch());
|
|
471
|
+
assignmentId = state.assignmentId;
|
|
472
|
+
delegateHandoffId = state.assignments.get(assignmentId).delegateHandoffId;
|
|
473
|
+
assert.equal(state.profile, "helper");
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
assert.fail(`blocked continuation unexpectedly reached the provider with ${body.model}`);
|
|
477
|
+
},
|
|
478
|
+
true,
|
|
479
|
+
async ({ session, cwd, requests, notices }) => {
|
|
480
|
+
await writeFile(
|
|
481
|
+
join(cwd, ".freeflow/config.json"),
|
|
482
|
+
JSON.stringify({
|
|
483
|
+
cognitiveRouting: {
|
|
484
|
+
enabled: true,
|
|
485
|
+
delegation: "executor",
|
|
486
|
+
projection: true,
|
|
487
|
+
profiles: { coordinator, executor: alternateExecutor },
|
|
488
|
+
},
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
await session.reload();
|
|
492
|
+
const requestCount = requests.length;
|
|
493
|
+
await session.prompt("/freeflow resume");
|
|
494
|
+
await session.waitForIdle();
|
|
495
|
+
assert.equal(requests.length, requestCount, "blocked resume makes no provider request");
|
|
496
|
+
assert.equal(session.model.id, "gpt-4.1-mini", "no fallback setter applies the Executor pair");
|
|
497
|
+
assert.match(JSON.stringify(notices), /profile_missing|helper/i);
|
|
498
|
+
},
|
|
499
|
+
true,
|
|
500
|
+
{ cognitiveRouting: helperConfig, maxRequests: 4 },
|
|
501
|
+
);
|
|
502
|
+
assert.equal(result.requests.length, 2);
|
|
503
|
+
assert.equal(result.state.assignmentId, assignmentId);
|
|
504
|
+
assert.equal(result.state.assignments.get(assignmentId).state, "outstanding");
|
|
505
|
+
assert.equal(result.state.assignments.get(assignmentId).delegateHandoffId, delegateHandoffId);
|
|
506
|
+
assert.equal(result.state.handoffs.get(delegateHandoffId).to, "helper");
|
|
507
|
+
assert.equal(
|
|
508
|
+
[...result.state.handoffs.values()].some((handoff) => handoff.kind === "return"),
|
|
509
|
+
false,
|
|
510
|
+
);
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
test("compaction and explicit resume preserve a Helper assignment", { timeout: 30000 }, async () => {
|
|
515
|
+
let assignmentId, basis;
|
|
516
|
+
const result = await fixture(
|
|
517
|
+
(n, body, manager) => {
|
|
518
|
+
if (n === 1)
|
|
519
|
+
return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Preserve Helper work." } }];
|
|
520
|
+
if (n === 2) {
|
|
521
|
+
const state = replay(manager.getBranch());
|
|
522
|
+
assignmentId = state.assignmentId;
|
|
523
|
+
basis = state.assignments.get(assignmentId).basisUserEntryId;
|
|
524
|
+
return [{ name: "read", args: { path: "evidence.txt" } }];
|
|
525
|
+
}
|
|
526
|
+
if (n === 4) {
|
|
527
|
+
const state = replay(manager.getBranch());
|
|
528
|
+
assert.equal(state.profile, "helper");
|
|
529
|
+
assert.equal(state.assignmentId, assignmentId);
|
|
530
|
+
assert.equal(state.resumeBasis.get(assignmentId), basis);
|
|
531
|
+
assert.doesNotMatch(JSON.stringify(body), /New delivered user input requires Coordinator attention/);
|
|
532
|
+
return [
|
|
533
|
+
{
|
|
534
|
+
name: "freeflow_return",
|
|
535
|
+
args: { operation: "submit", report: "Helper resumed after compaction.", outcome: "completed" },
|
|
536
|
+
},
|
|
537
|
+
];
|
|
538
|
+
}
|
|
539
|
+
return [];
|
|
540
|
+
},
|
|
541
|
+
true,
|
|
542
|
+
async ({ session, manager }) => {
|
|
543
|
+
await session.compact();
|
|
544
|
+
assert.equal(
|
|
545
|
+
manager.buildSessionContext().messages.some((message) => message.role === "user"),
|
|
546
|
+
false,
|
|
547
|
+
);
|
|
548
|
+
await session.reload();
|
|
549
|
+
await session.prompt("/freeflow resume");
|
|
550
|
+
await session.waitForIdle();
|
|
551
|
+
},
|
|
552
|
+
true,
|
|
553
|
+
{ cognitiveRouting: helperConfig, maxRequests: 6 },
|
|
554
|
+
);
|
|
555
|
+
assert.equal(result.requests.length, 5);
|
|
556
|
+
assert.equal(result.state.assignments.get(assignmentId).state, "returned");
|
|
557
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
@@ -184,7 +185,7 @@ test("native projection omits an unselected sibling body but preserves its excha
|
|
|
184
185
|
assert.equal(body.model, "gpt-4o");
|
|
185
186
|
assert.match(JSON.stringify(body), /EXACT_EVIDENCE_BODY_81/);
|
|
186
187
|
assert.doesNotMatch(JSON.stringify(body), /UNSELECTED_PRIVATE_BODY_93/);
|
|
187
|
-
assert.match(JSON.stringify(body), /
|
|
188
|
+
assert.match(JSON.stringify(body), /Worker result omitted/);
|
|
188
189
|
return [];
|
|
189
190
|
},
|
|
190
191
|
true,
|
|
@@ -203,6 +204,38 @@ test("native projection omits an unselected sibling body but preserves its excha
|
|
|
203
204
|
);
|
|
204
205
|
});
|
|
205
206
|
|
|
207
|
+
test("native pre-prompt reload and profile changes preserve unflushed routing state", { timeout: 30000 }, async () => {
|
|
208
|
+
await fixture(
|
|
209
|
+
() => [],
|
|
210
|
+
false,
|
|
211
|
+
async ({ session, manager, notices }) => {
|
|
212
|
+
assert.equal(existsSync(manager.getSessionFile()), true, "first assistant flushes the session file");
|
|
213
|
+
await session.reload();
|
|
214
|
+
await session.prompt("/freeflow profile executor");
|
|
215
|
+
await session.waitForIdle();
|
|
216
|
+
assert.equal(JSON.parse(notices.at(-1)[0]).status, "active");
|
|
217
|
+
},
|
|
218
|
+
true,
|
|
219
|
+
{
|
|
220
|
+
beforePrompt: async ({ session, manager, notices }) => {
|
|
221
|
+
const file = manager.getSessionFile();
|
|
222
|
+
assert.equal(existsSync(file), false, "new session is still unflushed after startup bind");
|
|
223
|
+
await session.reload();
|
|
224
|
+
assert.equal(existsSync(file), false, "reload must not require a pre-flush session file");
|
|
225
|
+
await session.prompt("/freeflow profile executor");
|
|
226
|
+
await session.waitForIdle();
|
|
227
|
+
assert.equal(JSON.parse(notices.at(-1)[0]).status, "active");
|
|
228
|
+
assert.equal(existsSync(file), false, "profile control remains in memory before first assistant");
|
|
229
|
+
await session.reload();
|
|
230
|
+
assert.equal(existsSync(file), false, "profile-triggered reload remains pre-flush");
|
|
231
|
+
await session.prompt("/freeflow profile coordinator");
|
|
232
|
+
await session.waitForIdle();
|
|
233
|
+
assert.equal(JSON.parse(notices.at(-1)[0]).status, "active");
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
206
239
|
test("bundled CLI loads the package entrypoint and completes the real routing loop", { timeout: 30000 }, async () => {
|
|
207
240
|
const { spawn } = await import("node:child_process");
|
|
208
241
|
const { fileURLToPath } = await import("node:url");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { mkdtemp, readFile, writeFile, rename, mkdir, rm } from "node:fs/promises";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
@@ -58,6 +59,60 @@ test("fresh no-v2 baseline validates ancestry before allowing automatic effects"
|
|
|
58
59
|
}
|
|
59
60
|
});
|
|
60
61
|
|
|
62
|
+
test("pre-flush native routing state reconciles from the in-memory branch", async () => {
|
|
63
|
+
const dir = await mkdtemp(join(tmpdir(), "freeflow-preflush-routing-"));
|
|
64
|
+
try {
|
|
65
|
+
const manager = SessionManager.create(dir, join(dir, "sessions"));
|
|
66
|
+
const pi = { appendEntry: (type, data) => manager.appendCustomEntry(type, data) };
|
|
67
|
+
const store = new EventStore(pi, manager);
|
|
68
|
+
await store.reconcile();
|
|
69
|
+
const event = store.make({ type: "control", control: "automatic", profile: "coordinator", reason: "fixture" });
|
|
70
|
+
store.append(event);
|
|
71
|
+
assert.equal(existsSync(manager.getSessionFile()), false, "Pi has not flushed a new session yet");
|
|
72
|
+
|
|
73
|
+
const rebound = new EventStore(pi, manager);
|
|
74
|
+
await rebound.reconcile();
|
|
75
|
+
assert.equal(rebound.blocked, undefined);
|
|
76
|
+
assert.equal(rebound.state().control, "automatic");
|
|
77
|
+
assert.equal(rebound.state().profile, "coordinator");
|
|
78
|
+
assert.equal(rebound.append(event).eventId, event.eventId, "reconciled occurrence is reusable");
|
|
79
|
+
assert.equal(
|
|
80
|
+
manager.getEntries().filter((entry) => entry.type === "custom" && entry.customType === "freeflow-routing-v2")
|
|
81
|
+
.length,
|
|
82
|
+
1,
|
|
83
|
+
);
|
|
84
|
+
} finally {
|
|
85
|
+
await rm(dir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("pre-flush uncertain append still requires persisted readback", async () => {
|
|
90
|
+
const dir = await mkdtemp(join(tmpdir(), "freeflow-preflush-uncertain-"));
|
|
91
|
+
try {
|
|
92
|
+
const manager = SessionManager.create(dir, join(dir, "sessions"));
|
|
93
|
+
const store = new EventStore(
|
|
94
|
+
{
|
|
95
|
+
appendEntry: (type, data) => {
|
|
96
|
+
manager.appendCustomEntry(type, data);
|
|
97
|
+
throw new Error("fixture append failure");
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
manager,
|
|
101
|
+
);
|
|
102
|
+
await store.reconcile();
|
|
103
|
+
const event = store.make({ type: "control", control: "automatic", profile: "coordinator", reason: "fixture" });
|
|
104
|
+
assert.throws(
|
|
105
|
+
() => store.append(event),
|
|
106
|
+
(error) => error.code === "acknowledgment_uncertain",
|
|
107
|
+
);
|
|
108
|
+
assert.equal(existsSync(manager.getSessionFile()), false);
|
|
109
|
+
await assert.rejects(store.reconcile(), (error) => error.code === "read_failed" && /ENOENT/.test(error.message));
|
|
110
|
+
assert.ok(store.blocked, "uncertain pre-flush state remains blocked without persisted acknowledgment");
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(dir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
61
116
|
test("snapshot does not repair a missing newline and enforces its read limit", async () => {
|
|
62
117
|
const dir = await mkdtemp(join(tmpdir(), "freeflow-snapshot-"));
|
|
63
118
|
try {
|