@hassangameryt/freeflow 0.7.1 → 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 +153 -128
- package/capabilities/cognitive-routing/SKILL.md +126 -274
- 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 +136 -0
- package/pi-extension/dist/cognitive-routing-v2/projection.js +330 -0
- package/pi-extension/dist/cognitive-routing-v2/render.js +252 -0
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +2181 -0
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +173 -0
- package/pi-extension/dist/cognitive-routing-v2/state.js +828 -0
- package/pi-extension/dist/cognitive-routing-v2/tools.js +60 -0
- package/pi-extension/dist/cognitive-routing-v2/types.js +48 -0
- package/pi-extension/dist/context-virtualization/runtime.js +29 -7
- package/pi-extension/dist/freeflow-context/tool.js +12 -6
- package/pi-extension/dist/index.js +302 -626
- 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 +85 -16
- package/pi-extension/dist/session-sources/events.js +217 -0
- package/pi-extension/dist/session-sources/provenance.js +32 -0
- package/pi-extension/dist/session-sources/read-only-session.js +148 -0
- package/pi-extension/dist/session-sources/sources.js +277 -0
- package/pi-extension/dist/settings/settings-ui.js +274 -147
- package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
- package/pi-extension/src/cognitive-routing-v2/config.ts +173 -0
- package/pi-extension/src/cognitive-routing-v2/projection.ts +375 -0
- package/pi-extension/src/cognitive-routing-v2/render.ts +277 -0
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +2241 -0
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +180 -0
- package/pi-extension/src/cognitive-routing-v2/state.ts +839 -0
- package/pi-extension/src/cognitive-routing-v2/tools.ts +62 -0
- package/pi-extension/src/cognitive-routing-v2/types.ts +254 -0
- package/pi-extension/src/context-virtualization/runtime.ts +30 -8
- package/pi-extension/src/freeflow-context/tool.ts +12 -6
- package/pi-extension/src/index.ts +308 -675
- 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 +100 -17
- package/pi-extension/src/session-sources/events.ts +230 -0
- package/pi-extension/src/session-sources/provenance.ts +39 -0
- package/pi-extension/src/session-sources/read-only-session.ts +170 -0
- package/pi-extension/src/session-sources/sources.ts +334 -0
- package/pi-extension/src/settings/settings-ui.ts +311 -158
- 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 +577 -0
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +474 -0
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +300 -0
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +1034 -0
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +244 -0
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +423 -0
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +586 -0
- package/pi-extension/tests/context-virtualization/extension.test.js +33 -7
- package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
- package/pi-extension/tests/fixtures/routing-native.js +191 -0
- package/pi-extension/tests/integration/mode-free-core.test.js +6 -4
- package/pi-extension/tests/integration/pi-extension.test.js +655 -59
- package/pi-extension/tests/integration/prompt-architecture.test.js +127 -30
- package/pi-extension/tests/integration/unaffected-regressions.test.js +78 -11
- 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 -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,586 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { initialState, reduce, parseRoutingEvent } from "../../dist/cognitive-routing-v2/state.js";
|
|
4
|
+
const pair = { provider: "fixture", modelId: "model", thinking: "off" };
|
|
5
|
+
function machine() {
|
|
6
|
+
let s = initialState(),
|
|
7
|
+
n = 0;
|
|
8
|
+
return {
|
|
9
|
+
get state() {
|
|
10
|
+
return s;
|
|
11
|
+
},
|
|
12
|
+
event(data) {
|
|
13
|
+
return {
|
|
14
|
+
version: 2,
|
|
15
|
+
eventId: `event-${++n}`,
|
|
16
|
+
operationId: `op-${n}`,
|
|
17
|
+
stepId: data.type,
|
|
18
|
+
recordedSessionId: "session",
|
|
19
|
+
data,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
apply(data) {
|
|
23
|
+
const e = this.event(data);
|
|
24
|
+
s = reduce(s, e);
|
|
25
|
+
return e;
|
|
26
|
+
},
|
|
27
|
+
set(e) {
|
|
28
|
+
s = reduce(s, e);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const control = (profile) => ({ type: "control", control: "automatic", profile, reason: "fixture control" });
|
|
33
|
+
const opened = (id, profile, assignmentId) => ({
|
|
34
|
+
type: "execution-opened",
|
|
35
|
+
execution: { id, profile, assignmentId, basisUserEntryId: "user", pair, resultEntryIds: [] },
|
|
36
|
+
});
|
|
37
|
+
const bound = (id) => ({
|
|
38
|
+
type: "execution-bound",
|
|
39
|
+
executionId: id,
|
|
40
|
+
assistantEntryId: `assistant-${id}`,
|
|
41
|
+
resultEntryIds: [],
|
|
42
|
+
outcome: "completed",
|
|
43
|
+
});
|
|
44
|
+
function delegated(id = "a1", unitId = "u1", assignmentIds = [id], executionId = "e1", replacement) {
|
|
45
|
+
const hid = `d-${id}`;
|
|
46
|
+
return {
|
|
47
|
+
type: "delegate-accepted",
|
|
48
|
+
unit: { id: unitId, objective: "fixture objective", state: "open", assignmentIds },
|
|
49
|
+
assignment: {
|
|
50
|
+
id,
|
|
51
|
+
unitId,
|
|
52
|
+
contract: "fixture contract",
|
|
53
|
+
basisUserEntryId: "user",
|
|
54
|
+
state: "outstanding",
|
|
55
|
+
delegateHandoffId: hid,
|
|
56
|
+
},
|
|
57
|
+
handoff: {
|
|
58
|
+
id: hid,
|
|
59
|
+
kind: "delegate",
|
|
60
|
+
assignmentId: id,
|
|
61
|
+
text: "fixture contract",
|
|
62
|
+
from: "coordinator",
|
|
63
|
+
to: "executor",
|
|
64
|
+
executionId,
|
|
65
|
+
toolCallId: `call-${executionId}`,
|
|
66
|
+
basisUserEntryId: "user",
|
|
67
|
+
state: "pending",
|
|
68
|
+
reportRevision: 0,
|
|
69
|
+
limitations: [],
|
|
70
|
+
},
|
|
71
|
+
...(replacement ? { replacement } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function returned(report = "exact report", revision = 1) {
|
|
75
|
+
return {
|
|
76
|
+
type: "return-accepted",
|
|
77
|
+
handoff: {
|
|
78
|
+
id: "return",
|
|
79
|
+
kind: "return",
|
|
80
|
+
assignmentId: "a1",
|
|
81
|
+
text: report,
|
|
82
|
+
from: "executor",
|
|
83
|
+
to: "coordinator",
|
|
84
|
+
executionId: "e2",
|
|
85
|
+
toolCallId: "return-call",
|
|
86
|
+
basisUserEntryId: "user",
|
|
87
|
+
state: "pending",
|
|
88
|
+
reportRevision: revision,
|
|
89
|
+
outcome: "partial",
|
|
90
|
+
limitations: ["uncertainty retained"],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function started() {
|
|
95
|
+
const m = machine();
|
|
96
|
+
m.apply(control("coordinator"));
|
|
97
|
+
m.apply(opened("e1", "coordinator"));
|
|
98
|
+
m.apply(delegated());
|
|
99
|
+
m.apply(bound("e1"));
|
|
100
|
+
m.apply({ type: "handoff-state", handoffId: "d-a1", state: "configured", observedPair: pair });
|
|
101
|
+
return m;
|
|
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
|
+
}
|
|
161
|
+
const reservation = (revision) => ({
|
|
162
|
+
id: "reservation",
|
|
163
|
+
selectionRevision: revision,
|
|
164
|
+
receiver: "coordinator",
|
|
165
|
+
target: pair,
|
|
166
|
+
qualification: "fixture",
|
|
167
|
+
sources: [],
|
|
168
|
+
maximumInputTokens: 10000,
|
|
169
|
+
outputReserve: 1000,
|
|
170
|
+
estimateMethod: "fixture",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("event identity and operation idempotency reject changed payloads", () => {
|
|
174
|
+
const m = machine(),
|
|
175
|
+
e = m.apply(control("coordinator"));
|
|
176
|
+
const before = m.state;
|
|
177
|
+
m.set(e);
|
|
178
|
+
assert.equal(m.state, before);
|
|
179
|
+
assert.throws(
|
|
180
|
+
() => m.set({ ...e, data: control("executor") }),
|
|
181
|
+
(e) => e.code === "event_identity_conflict",
|
|
182
|
+
);
|
|
183
|
+
assert.throws(
|
|
184
|
+
() => m.set({ ...e, eventId: "other", data: control("executor") }),
|
|
185
|
+
(e) => e.code === "operation_conflict",
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
test("closed nested schemas reject malformed optional handoff and source fields", () => {
|
|
189
|
+
const m = machine();
|
|
190
|
+
for (const patch of [{ reason: {} }, { outcome: 123 }, { unexpected: "field" }]) {
|
|
191
|
+
const data = delegated();
|
|
192
|
+
Object.assign(data.handoff, patch);
|
|
193
|
+
assert.throws(
|
|
194
|
+
() => parseRoutingEvent(m.event(data)),
|
|
195
|
+
(e) => e.code === "invalid_assignment",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const data = opened("e", "executor", "a");
|
|
199
|
+
data.execution.pair = { ...data.execution.pair, extra: "invalid" };
|
|
200
|
+
assert.throws(
|
|
201
|
+
() => parseRoutingEvent(m.event(data)),
|
|
202
|
+
(e) => e.code === "invalid_execution",
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
test("prototype names are bounded malformed events, not raw decoder errors", () => {
|
|
206
|
+
for (const type of ["constructor", "__proto__", "toString", "unknown"])
|
|
207
|
+
assert.throws(
|
|
208
|
+
() =>
|
|
209
|
+
parseRoutingEvent({
|
|
210
|
+
version: 2,
|
|
211
|
+
eventId: "e",
|
|
212
|
+
operationId: "o",
|
|
213
|
+
stepId: "s",
|
|
214
|
+
recordedSessionId: "session",
|
|
215
|
+
data: { type },
|
|
216
|
+
}),
|
|
217
|
+
(e) => e.name === "RoutingError" && e.code === "invalid_event_type",
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
test("outstanding assignments cannot be implicitly replaced", () => {
|
|
221
|
+
const m = started();
|
|
222
|
+
assert.throws(
|
|
223
|
+
() => m.apply(delegated("a2", "u1", ["a1", "a2"])),
|
|
224
|
+
(e) => e.code === "wrong_control",
|
|
225
|
+
);
|
|
226
|
+
m.apply(control("coordinator"));
|
|
227
|
+
assert.throws(
|
|
228
|
+
() => m.apply(delegated("a2", "u1", ["a1", "a2"])),
|
|
229
|
+
(e) => e.code === "assignment_outstanding",
|
|
230
|
+
);
|
|
231
|
+
assert.equal(m.state.assignments.size, 1);
|
|
232
|
+
});
|
|
233
|
+
test("replacement retains old assignment and requires old execution to be resolved", () => {
|
|
234
|
+
const m = started();
|
|
235
|
+
m.apply(opened("e2", "executor", "a1"));
|
|
236
|
+
m.apply(control("coordinator"));
|
|
237
|
+
m.apply(opened("e3", "coordinator", "a1"));
|
|
238
|
+
const replacement = delegated("a2", "u1", ["a1", "a2"], "e3", {
|
|
239
|
+
assignmentId: "a1",
|
|
240
|
+
reason: "current direction changed",
|
|
241
|
+
});
|
|
242
|
+
assert.throws(
|
|
243
|
+
() => m.apply(replacement),
|
|
244
|
+
(e) => e.code === "unresolved_worker_execution",
|
|
245
|
+
);
|
|
246
|
+
m.apply(bound("e2"));
|
|
247
|
+
m.apply(replacement);
|
|
248
|
+
assert.equal(m.state.unitId, "u1");
|
|
249
|
+
assert.equal(m.state.assignments.get("a1").state, "superseded");
|
|
250
|
+
assert.equal(m.state.assignments.get("a1").contract, "fixture contract");
|
|
251
|
+
assert.equal(m.state.assignmentId, "a2");
|
|
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
|
+
|
|
391
|
+
test("saved report revisions and retries preserve exact earlier events", () => {
|
|
392
|
+
const m = started();
|
|
393
|
+
m.apply(opened("e2", "executor", "a1"));
|
|
394
|
+
const first = m.apply(returned());
|
|
395
|
+
m.apply({ type: "handoff-state", handoffId: "return", state: "blocked", reason: "fixture gap" });
|
|
396
|
+
m.apply({
|
|
397
|
+
type: "handoff-retry-requested",
|
|
398
|
+
handoffId: "return",
|
|
399
|
+
attemptId: "retry",
|
|
400
|
+
executionId: "e2",
|
|
401
|
+
toolCallId: "retry-call",
|
|
402
|
+
reportRevision: 1,
|
|
403
|
+
selectionRevision: 0,
|
|
404
|
+
});
|
|
405
|
+
assert.equal(m.state.handoffs.get("return").text, "exact report");
|
|
406
|
+
assert.equal(m.state.handoffs.get("return").reportRevision, 1);
|
|
407
|
+
m.apply(returned("revised exact report", 2));
|
|
408
|
+
assert.equal(m.state.handoffs.get("return").text, "revised exact report");
|
|
409
|
+
assert.equal([...m.state.events.values()].find((e) => e.eventId === first.eventId).data.handoff.text, "exact report");
|
|
410
|
+
assert.equal(m.state.assignments.size, 1);
|
|
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
|
+
|
|
492
|
+
test("assessment resumption rejects stale selection and preserves suspension on failure", () => {
|
|
493
|
+
const m = started();
|
|
494
|
+
m.apply(opened("e2", "executor", "a1"));
|
|
495
|
+
m.apply(returned());
|
|
496
|
+
m.apply({
|
|
497
|
+
type: "selection-changed",
|
|
498
|
+
assignmentId: "a1",
|
|
499
|
+
selection: { revision: 1, selected: [], unresolved: [], withdrawals: [] },
|
|
500
|
+
});
|
|
501
|
+
m.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
|
|
502
|
+
m.apply({
|
|
503
|
+
type: "assessment-suspended",
|
|
504
|
+
handoffId: "return",
|
|
505
|
+
reason: "user-attention",
|
|
506
|
+
basisUserEntryId: "new-user",
|
|
507
|
+
problems: [],
|
|
508
|
+
});
|
|
509
|
+
assert.throws(
|
|
510
|
+
() =>
|
|
511
|
+
m.apply({
|
|
512
|
+
type: "assessment-resumed",
|
|
513
|
+
handoffId: "return",
|
|
514
|
+
basisUserEntryId: "new-user",
|
|
515
|
+
reservation: reservation(0),
|
|
516
|
+
}),
|
|
517
|
+
(e) => e.code === "stale_selection",
|
|
518
|
+
);
|
|
519
|
+
assert.equal(m.state.assessment.view, "suspended");
|
|
520
|
+
m.apply({
|
|
521
|
+
type: "assessment-resumed",
|
|
522
|
+
handoffId: "return",
|
|
523
|
+
basisUserEntryId: "new-user",
|
|
524
|
+
reservation: reservation(1),
|
|
525
|
+
});
|
|
526
|
+
assert.equal(m.state.assessment.view, "active");
|
|
527
|
+
});
|
|
528
|
+
test("accepted closure cannot conceal work; later work creates a new unit", () => {
|
|
529
|
+
const m = started();
|
|
530
|
+
m.apply(control("coordinator"));
|
|
531
|
+
assert.throws(
|
|
532
|
+
() => m.apply({ type: "unit-closed", unitId: "u1", outcome: "accepted", assessment: "incorrect" }),
|
|
533
|
+
(e) => e.code === "unfinished_work",
|
|
534
|
+
);
|
|
535
|
+
m.apply({
|
|
536
|
+
type: "unit-closed",
|
|
537
|
+
unitId: "u1",
|
|
538
|
+
outcome: "cancelled",
|
|
539
|
+
assessment: "user stopped work",
|
|
540
|
+
supersededAssignmentId: "a1",
|
|
541
|
+
});
|
|
542
|
+
assert.equal(m.state.units.get("u1").state, "closed");
|
|
543
|
+
m.apply(opened("e3", "coordinator"));
|
|
544
|
+
assert.throws(
|
|
545
|
+
() => m.apply(delegated("a2", "u1", ["a2"], "e3")),
|
|
546
|
+
(e) => e.code === "closed_unit_reused",
|
|
547
|
+
);
|
|
548
|
+
m.apply(delegated("a2", "u2", ["a2"], "e3"));
|
|
549
|
+
assert.equal(m.state.unitId, "u2");
|
|
550
|
+
assert.equal(m.state.units.size, 2);
|
|
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
|
+
});
|
|
@@ -169,6 +169,31 @@ function renderComponent(component) {
|
|
|
169
169
|
return component.render(120).join("\\n");
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
test("disabled Freeflow Context retains valid full operation schemas", async () => {
|
|
173
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-context-disabled-schema-"));
|
|
174
|
+
try {
|
|
175
|
+
await writeConfig(cwd, {});
|
|
176
|
+
const harness = createHarness(cwd, []);
|
|
177
|
+
await harness.handlers.get("session_start")({ reason: "startup" }, harness.ctx);
|
|
178
|
+
|
|
179
|
+
const contextTool = harness.tools.find((tool) => tool.name === "freeflow_context");
|
|
180
|
+
assert.ok(contextTool);
|
|
181
|
+
assert.equal(contextTool.parameters.type, "object");
|
|
182
|
+
assert.deepEqual(
|
|
183
|
+
contextTool.parameters.oneOf.map((v) => v.properties.operation.const),
|
|
184
|
+
["archive", "restore", "search", "retrieve"],
|
|
185
|
+
);
|
|
186
|
+
const denied = await contextTool.execute(
|
|
187
|
+
"disabled",
|
|
188
|
+
{ operation: "archive", targets: [{ ref: "ctx:tool-1" }] },
|
|
189
|
+
undefined,
|
|
190
|
+
);
|
|
191
|
+
assert.equal(denied.details.result.status, "rejected");
|
|
192
|
+
} finally {
|
|
193
|
+
await rm(cwd, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
172
197
|
test("enabled Context Virtualization registers, projects, archives, restores, and reports through commands", async () => {
|
|
173
198
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-context-extension-"));
|
|
174
199
|
try {
|
|
@@ -189,7 +214,7 @@ test("enabled Context Virtualization registers, projects, archives, restores, an
|
|
|
189
214
|
|
|
190
215
|
const contextTool = harness.tools.find((tool) => tool.name === "freeflow_context");
|
|
191
216
|
assert.ok(contextTool);
|
|
192
|
-
assert.
|
|
217
|
+
assert.match(contextTool.description, /conversation history/i);
|
|
193
218
|
const staleSearch = await contextTool.execute(
|
|
194
219
|
"context-stale-search",
|
|
195
220
|
{ operation: "search", query: "not enabled" },
|
|
@@ -275,7 +300,7 @@ test("enabled Context Virtualization registers, projects, archives, restores, an
|
|
|
275
300
|
}
|
|
276
301
|
});
|
|
277
302
|
|
|
278
|
-
test("enabled Conversation History
|
|
303
|
+
test("enabled Conversation History permits search and retrieve within the stable catalog", async () => {
|
|
279
304
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-conversation-history-extension-"));
|
|
280
305
|
try {
|
|
281
306
|
await writeConfig(cwd, { conversationHistory: true });
|
|
@@ -287,7 +312,7 @@ test("enabled Conversation History exposes only search and retrieve operations",
|
|
|
287
312
|
assert.ok(contextTool);
|
|
288
313
|
assert.deepEqual(
|
|
289
314
|
contextTool.parameters.oneOf.map((variant) => variant.properties.operation.const),
|
|
290
|
-
["search", "retrieve"],
|
|
315
|
+
["archive", "restore", "search", "retrieve"],
|
|
291
316
|
);
|
|
292
317
|
assert.equal(typeof contextTool.renderCall, "function");
|
|
293
318
|
assert.equal(typeof contextTool.renderResult, "function");
|
|
@@ -349,7 +374,7 @@ test("enabled Conversation History exposes only search and retrieve operations",
|
|
|
349
374
|
assert.match(renderComponent(expandedResult), /ctx:render-source/);
|
|
350
375
|
assert.match(renderComponent(expandedResult), /database timeout was recovered/);
|
|
351
376
|
assert.match(contextTool.description, /hidden conversation history/i);
|
|
352
|
-
assert.
|
|
377
|
+
assert.match(contextTool.description, /future context projections/i);
|
|
353
378
|
const before = await harness.handlers.get("before_agent_start")({ systemPrompt: "base" }, harness.ctx);
|
|
354
379
|
assert.match(before.systemPrompt, /## Conversation History Cue/);
|
|
355
380
|
assert.doesNotMatch(before.systemPrompt, /# Conversation History$/m);
|
|
@@ -728,20 +753,21 @@ test("visible invalid sources do not make an empty hidden corpus partial", async
|
|
|
728
753
|
}
|
|
729
754
|
});
|
|
730
755
|
|
|
731
|
-
test("disabled Context Virtualization
|
|
756
|
+
test("disabled Context Virtualization blocks execution while preserving definitions and reference labels", async () => {
|
|
732
757
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-context-disabled-"));
|
|
733
758
|
try {
|
|
734
759
|
await writeConfig(cwd, false);
|
|
735
760
|
const harness = createHarness(cwd, [toolResultEntry()]);
|
|
736
761
|
await harness.handlers.get("session_start")({ reason: "startup" }, harness.ctx);
|
|
737
762
|
|
|
738
|
-
assert.ok(
|
|
763
|
+
assert.ok(harness.activeToolNames().includes("freeflow_context"));
|
|
739
764
|
const contextResult = await harness.handlers.get("context")(
|
|
740
765
|
{ messages: [harness.entries[0].message] },
|
|
741
766
|
harness.ctx,
|
|
742
767
|
);
|
|
743
768
|
assert.ok(contextResult);
|
|
744
|
-
assert.deepEqual(contextResult.messages[0], harness.entries[0].message);
|
|
769
|
+
assert.deepEqual(contextResult.messages[0].content.slice(0, -1), harness.entries[0].message.content);
|
|
770
|
+
assert.match(contextResult.messages[0].content.at(-1).text, /context-ref:/);
|
|
745
771
|
assert.equal(contextResult.messages.at(-1).customType, "freeflow-runtime-state");
|
|
746
772
|
} finally {
|
|
747
773
|
await rm(cwd, { recursive: true, force: true });
|