@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.
Files changed (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/README.md +147 -127
  6. package/capabilities/cognitive-routing/SKILL.md +162 -93
  7. package/gemini-extension.json +1 -1
  8. package/package.json +1 -1
  9. package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
  10. package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
  11. package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
  12. package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
  13. package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
  14. package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
  15. package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
  16. package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
  17. package/pi-extension/dist/context-virtualization/runtime.js +29 -7
  18. package/pi-extension/dist/freeflow-context/tool.js +7 -6
  19. package/pi-extension/dist/index.js +36 -15
  20. package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
  21. package/pi-extension/dist/provider-support/astra/history.js +125 -0
  22. package/pi-extension/dist/provider-support/astra/index.js +11 -0
  23. package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
  24. package/pi-extension/dist/provider-support/index.js +4 -0
  25. package/pi-extension/dist/runtime/request-history.js +216 -0
  26. package/pi-extension/dist/runtime/runtime-context.js +29 -10
  27. package/pi-extension/dist/session-sources/events.js +22 -0
  28. package/pi-extension/dist/session-sources/provenance.js +32 -0
  29. package/pi-extension/dist/session-sources/sources.js +51 -7
  30. package/pi-extension/dist/settings/settings-ui.js +247 -47
  31. package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
  32. package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
  33. package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
  34. package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
  35. package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
  36. package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
  37. package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
  38. package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
  39. package/pi-extension/src/context-virtualization/runtime.ts +30 -8
  40. package/pi-extension/src/freeflow-context/tool.ts +7 -6
  41. package/pi-extension/src/index.ts +36 -14
  42. package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
  43. package/pi-extension/src/provider-support/astra/history.ts +145 -0
  44. package/pi-extension/src/provider-support/astra/index.ts +12 -0
  45. package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
  46. package/pi-extension/src/provider-support/index.ts +5 -0
  47. package/pi-extension/src/runtime/request-history.ts +243 -0
  48. package/pi-extension/src/runtime/runtime-context.ts +32 -10
  49. package/pi-extension/src/session-sources/events.ts +22 -0
  50. package/pi-extension/src/session-sources/provenance.ts +39 -0
  51. package/pi-extension/src/session-sources/sources.ts +77 -6
  52. package/pi-extension/src/settings/settings-ui.ts +276 -46
  53. package/pi-extension/tests/cache-reuse/history.test.js +117 -0
  54. package/pi-extension/tests/cache-reuse/native.test.js +91 -0
  55. package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
  56. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +94 -1
  57. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
  58. package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
  59. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
  60. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
  61. package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
  62. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
  63. package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
  64. package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
  65. package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
  66. package/pi-extension/tests/fixtures/routing-native.js +4 -1
  67. package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
  68. package/pi-extension/tests/integration/pi-extension.test.js +508 -29
  69. package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
  70. package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
  71. package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
  72. package/pi-extension/tests/provider-support/astra.test.js +306 -0
  73. package/pi-extension/tests/provider-support/cli.test.js +92 -0
  74. package/pi-extension/tests/provider-support/native.test.js +183 -0
  75. package/plugin.json +1 -1
  76. package/runtime/prompts/cognitive-routing.md +4 -4
  77. package/skills/design-for-depth/SKILL.md +1 -1
  78. package/skills/design-for-depth/references/implementation-decisions.md +3 -3
  79. package/skills/discuss/SKILL.md +2 -2
@@ -0,0 +1,91 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { fixture } from "../fixtures/routing-native.js";
4
+ const profiles = Object.fromEntries(
5
+ ["coordinator", "helper", "executor"].map((p) => [
6
+ p,
7
+ { provider: "openai", model: "gpt-6-astra", thinking: p === "coordinator" ? "high" : "low" },
8
+ ]),
9
+ );
10
+ for (const projection of [false, true])
11
+ test(`native routing preserves per-view prefixes with projection=${projection}`, { timeout: 30000 }, async () => {
12
+ const last = new Map();
13
+ let ref;
14
+ const result = await fixture(
15
+ (n, body, manager) => {
16
+ const runtimeItems = body.input.filter((x) => JSON.stringify(x).includes("# Cognitive Routing Runtime State"));
17
+ const current = JSON.stringify(runtimeItems.at(-1));
18
+ const profile = current.includes("Profile: coordinator") ? "coordinator" : "worker";
19
+ const view = projection ? profile : "ordinary";
20
+ const previous = last.get(view);
21
+ if (previous) {
22
+ assert.deepEqual(body.tools, previous.tools, "fixed routing tool definitions");
23
+ assert.deepEqual(
24
+ body.input.slice(0, previous.input.length),
25
+ previous.input,
26
+ `prior ${view} prefix survives request ${n}`,
27
+ );
28
+ }
29
+ last.set(view, body);
30
+ if (n === 1)
31
+ return [
32
+ {
33
+ name: "freeflow_delegate",
34
+ args: { operation: "assign", worker: "helper", contract: "Read evidence.txt and select its result." },
35
+ },
36
+ ];
37
+ if (n === 2) return [{ name: "read", args: { path: "evidence.txt" } }];
38
+ if (n === 3) {
39
+ ref = "ctx:" + manager.getBranch().find((e) => e.message?.toolName === "read").id;
40
+ return [
41
+ ...(projection ? [{ name: "freeflow_project", args: { operation: "add", refs: [ref] } }] : []),
42
+ { name: "freeflow_return", args: { operation: "submit", report: "Evidence read.", outcome: "completed" } },
43
+ ];
44
+ }
45
+ if (n === 4)
46
+ return [
47
+ {
48
+ name: "freeflow_unit",
49
+ args: { operation: "close", outcome: "accepted", assessment: "Exact evidence observed." },
50
+ },
51
+ ];
52
+ if (n === 5)
53
+ return [
54
+ {
55
+ name: "freeflow_delegate",
56
+ args: { operation: "assign", worker: "executor", contract: "Read evidence.txt and return." },
57
+ },
58
+ ];
59
+ if (n === 6) return [{ name: "read", args: { path: "evidence.txt" } }];
60
+ if (n === 7) {
61
+ const latest = manager
62
+ .getBranch()
63
+ .filter((e) => e.message?.toolName === "read")
64
+ .at(-1);
65
+ return [
66
+ ...(projection
67
+ ? [{ name: "freeflow_project", args: { operation: "add", refs: ["ctx:" + latest.id] } }]
68
+ : []),
69
+ {
70
+ name: "freeflow_return",
71
+ args: { operation: "submit", report: "Executor evidence read.", outcome: "completed" },
72
+ },
73
+ ];
74
+ }
75
+ if (n === 8)
76
+ return [
77
+ {
78
+ name: "freeflow_unit",
79
+ args: { operation: "close", outcome: "accepted", assessment: "Second exact result checked." },
80
+ },
81
+ ];
82
+ return [];
83
+ },
84
+ projection,
85
+ undefined,
86
+ false,
87
+ { cognitiveRouting: { enabled: true, projection, delegation: "both", profiles } },
88
+ );
89
+ assert.equal(result.requests.length, 9);
90
+ assert.ok(result.entries.some((e) => e.customType === "freeflow-request-history-v1"));
91
+ });
@@ -0,0 +1,211 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import {
7
+ createAgentSession,
8
+ DefaultResourceLoader,
9
+ ModelRuntime,
10
+ SessionManager,
11
+ SettingsManager,
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import freeflow from "../../dist/index.js";
14
+ import { response } from "../fixtures/routing-native.js";
15
+
16
+ test(
17
+ "native Freeflow settings retain the exact prefix; archive and restore change only real evidence",
18
+ { timeout: 30000 },
19
+ async () => {
20
+ const root = await mkdtemp(join(tmpdir(), "freeflow-settings-cache-")),
21
+ agentDir = join(root, "agent");
22
+ await mkdir(agentDir);
23
+ await mkdir(join(root, ".freeflow"));
24
+ const config = join(root, ".freeflow/config.json");
25
+ await writeFile(config, JSON.stringify({ enabled: true }));
26
+ await writeFile(join(root, "evidence.txt"), "PRIVATE_RAW_EVIDENCE_716");
27
+ let session, action, ref;
28
+ const requests = [],
29
+ errors = [],
30
+ prior = globalThis.fetch;
31
+ const manager = SessionManager.create(root, join(root, "sessions"));
32
+ globalThis.fetch = async (url, init) => {
33
+ assert.equal(String(url), "https://api.openai.com/v1/responses");
34
+ const body = JSON.parse(String(init.body));
35
+ requests.push(body);
36
+ assert.ok(requests.length <= 20);
37
+ let calls = [];
38
+ if (action === "read") {
39
+ action = undefined;
40
+ calls = [{ name: "read", args: { path: "evidence.txt" } }];
41
+ }
42
+ if (action === "archive") {
43
+ action = undefined;
44
+ calls = [
45
+ {
46
+ name: "freeflow_context",
47
+ args: { operation: "archive", targets: [{ ref, retained: "The completed evidence was checked." }] },
48
+ },
49
+ ];
50
+ }
51
+ if (action === "restore") {
52
+ action = undefined;
53
+ calls = [{ name: "freeflow_context", args: { operation: "restore", refs: [ref] } }];
54
+ }
55
+ if (action === "denied") {
56
+ action = undefined;
57
+ calls = [
58
+ { name: "freeflow_context", args: { operation: "search", query: "private" } },
59
+ { name: "freeflow_delegate", args: { operation: "assign", contract: "Must not execute" } },
60
+ ];
61
+ }
62
+ return response(requests.length, calls, "OK");
63
+ };
64
+ try {
65
+ const runtime = await ModelRuntime.create({
66
+ authPath: join(agentDir, "auth.json"),
67
+ modelsPath: null,
68
+ modelsStorePath: join(agentDir, "models.json"),
69
+ allowModelNetwork: false,
70
+ });
71
+ await runtime.setRuntimeApiKey("openai", "fixture-key");
72
+ const settings = SettingsManager.inMemory({
73
+ transport: "sse",
74
+ compaction: { enabled: false },
75
+ retry: { enabled: false, maxRetries: 0, provider: { maxRetries: 0 } },
76
+ });
77
+ const loader = new DefaultResourceLoader({
78
+ cwd: root,
79
+ agentDir,
80
+ settingsManager: settings,
81
+ noExtensions: true,
82
+ noSkills: true,
83
+ noPromptTemplates: true,
84
+ noThemes: true,
85
+ noContextFiles: true,
86
+ systemPrompt: "Synthetic fixture.",
87
+ extensionFactories: [freeflow],
88
+ });
89
+ await loader.reload();
90
+ ({ session } = await createAgentSession({
91
+ cwd: root,
92
+ agentDir,
93
+ modelRuntime: runtime,
94
+ model: runtime.getModel("openai", "gpt-6-astra"),
95
+ thinkingLevel: "high",
96
+ settingsManager: settings,
97
+ sessionManager: manager,
98
+ resourceLoader: loader,
99
+ }));
100
+ await session.bindExtensions({ mode: "print", onError: (e) => errors.push(e) });
101
+ const prompt = async () => {
102
+ await session.prompt("Continue the synthetic fixture.");
103
+ await session.waitForIdle();
104
+ assert.equal(session.messages.at(-1).stopReason, "stop");
105
+ assert.deepEqual(errors, []);
106
+ return requests.at(-1);
107
+ };
108
+ action = "read";
109
+ await prompt();
110
+ const read = manager.getBranch().find((e) => e.message?.toolName === "read");
111
+ ref = "ctx:" + read.id;
112
+ const original = JSON.stringify(read);
113
+ let previous = requests.at(-1);
114
+ const stable = async () => {
115
+ const body = await prompt();
116
+ assert.deepEqual(body.tools, previous.tools);
117
+ assert.deepEqual(body.input.slice(0, previous.input.length), previous.input);
118
+ previous = body;
119
+ };
120
+ await session.prompt("/freeflow disable");
121
+ await stable();
122
+ action = "denied";
123
+ await stable();
124
+ assert.ok(
125
+ manager
126
+ .getBranch()
127
+ .filter((e) => ["freeflow_context", "freeflow_delegate"].includes(e.message?.toolName))
128
+ .every((e) => e.message.isError),
129
+ );
130
+ assert.ok(!manager.getBranch().some((e) => e.customType === "freeflow-routing-v2"));
131
+ await session.prompt("/freeflow enable");
132
+ await stable();
133
+ await writeFile(config, JSON.stringify({ enabled: true, conversationHistory: true }));
134
+ await session.reload();
135
+ await stable();
136
+ await writeFile(
137
+ config,
138
+ JSON.stringify({ enabled: true, conversationHistory: true, contextVirtualization: true }),
139
+ );
140
+ await session.reload();
141
+ await stable();
142
+ await writeFile(join(root, ".freeflow/local.json"), JSON.stringify({ enabled: true }));
143
+ await session.reload();
144
+ await stable();
145
+ const routing = {
146
+ enabled: true,
147
+ projection: false,
148
+ profiles: {
149
+ coordinator: { provider: "openai", model: "gpt-6-astra", thinking: "high" },
150
+ executor: { provider: "openai", model: "gpt-6-astra", thinking: "low" },
151
+ },
152
+ };
153
+ await writeFile(
154
+ config,
155
+ JSON.stringify({
156
+ enabled: true,
157
+ conversationHistory: true,
158
+ contextVirtualization: true,
159
+ cognitiveRouting: routing,
160
+ }),
161
+ );
162
+ await session.reload();
163
+ await stable();
164
+ await session.prompt("/freeflow profile executor");
165
+ await stable();
166
+ await session.prompt("/freeflow profile auto");
167
+ await stable();
168
+ routing.profiles.executor.thinking = "medium";
169
+ await writeFile(
170
+ config,
171
+ JSON.stringify({
172
+ enabled: true,
173
+ conversationHistory: true,
174
+ contextVirtualization: true,
175
+ cognitiveRouting: routing,
176
+ }),
177
+ );
178
+ await session.reload();
179
+ await stable();
180
+ routing.projection = true;
181
+ await writeFile(config, JSON.stringify({ enabled: true, cognitiveRouting: routing }));
182
+ await session.reload();
183
+ await stable();
184
+ routing.projection = false;
185
+ await writeFile(
186
+ config,
187
+ JSON.stringify({
188
+ enabled: true,
189
+ conversationHistory: true,
190
+ contextVirtualization: true,
191
+ cognitiveRouting: routing,
192
+ }),
193
+ );
194
+ await session.reload();
195
+ await stable();
196
+ action = "archive";
197
+ const archived = await prompt();
198
+ assert.ok(!JSON.stringify(archived).includes("PRIVATE_RAW_EVIDENCE_716"));
199
+ assert.deepEqual(archived.tools, previous.tools);
200
+ action = "restore";
201
+ const restored = await prompt();
202
+ assert.ok(JSON.stringify(restored).includes("PRIVATE_RAW_EVIDENCE_716"));
203
+ assert.deepEqual(restored.tools, previous.tools);
204
+ assert.equal(JSON.stringify(manager.getEntry(read.id)), original, "canonical evidence untouched");
205
+ } finally {
206
+ session?.dispose();
207
+ globalThis.fetch = prior;
208
+ await rm(root, { recursive: true, force: true });
209
+ }
210
+ },
211
+ );
@@ -49,6 +49,92 @@ function prepare(state, entries, messages = entries.map((e) => e.message), view
49
49
  });
50
50
  }
51
51
 
52
+ test("source locators retain native occurrence identity and accept optional adapter labels", () => {
53
+ const state = initialState();
54
+ const assistantWith = (id, name, args) =>
55
+ entry(id, null, assistant("Observed", [{ type: "toolCall", id, name, arguments: args }]));
56
+ const result = (id, toolName) =>
57
+ entry(`${id}-result`, id, {
58
+ role: "toolResult",
59
+ toolCallId: id,
60
+ toolName,
61
+ content: [{ type: "text", text: "RESULT_BODY" }],
62
+ });
63
+ const entries = [
64
+ assistantWith("read-1", "read", { path: "same.txt", offset: 1, limit: 20 }),
65
+ result("read-1", "read"),
66
+ assistantWith("read-2", "read", { path: "same.txt", offset: 1, limit: 20 }),
67
+ result("read-2", "read"),
68
+ assistantWith("bash-1", "bash", { command: `printf "bad${String.fromCharCode(0)}${"x".repeat(220)}"` }),
69
+ result("bash-1", "bash"),
70
+ assistantWith("unknown-1", "write", { path: "out.txt" }),
71
+ result("unknown-1", "write"),
72
+ entry(
73
+ "ambiguous",
74
+ null,
75
+ assistant("Ambiguous", [
76
+ { type: "toolCall", id: "dup", name: "read", arguments: { path: "one.txt" } },
77
+ { type: "toolCall", id: "dup", name: "read", arguments: { path: "two.txt" } },
78
+ ]),
79
+ ),
80
+ entry("ambiguous-result", "ambiguous", {
81
+ role: "toolResult",
82
+ toolCallId: "dup",
83
+ toolName: "read",
84
+ content: [{ type: "text", text: "RESULT_BODY" }],
85
+ }),
86
+ ];
87
+ for (const source of entries)
88
+ state.authors.set(source.id, { profile: "executor", executionId: "e", assignmentId: "a" });
89
+ const sources = new Sources(entries, state);
90
+ assert.deepEqual(sources.locator("ctx:read-1-result"), {
91
+ callRef: "ctx:read-1",
92
+ toolCallId: "read-1",
93
+ });
94
+ assert.notEqual(sources.locator("ctx:read-1-result").callRef, sources.locator("ctx:read-2-result").callRef);
95
+ assert.deepEqual(sources.locator("ctx:bash-1-result"), {
96
+ callRef: "ctx:bash-1",
97
+ toolCallId: "bash-1",
98
+ });
99
+ assert.deepEqual(sources.locator("ctx:unknown-1-result"), {
100
+ callRef: "ctx:unknown-1",
101
+ toolCallId: "unknown-1",
102
+ });
103
+ assert.equal(sources.locator("ctx:ambiguous-result"), undefined);
104
+
105
+ const adapted = new Sources(entries, state, undefined, () => ({
106
+ label: `adapter${String.fromCharCode(0)}${"x".repeat(220)}`,
107
+ }));
108
+ const adaptedLocator = adapted.locator("ctx:bash-1-result");
109
+ assert.equal(adaptedLocator.truncated, true);
110
+ assert.ok(adaptedLocator.label.length <= 160);
111
+ assert.doesNotMatch(adaptedLocator.label, /\u0000/);
112
+ });
113
+
114
+ test("registered projection accepts a known eligible ref without inspection first", async () => {
115
+ await fixture((n, _body, manager) => {
116
+ if (n === 1) return call("freeflow_delegate", { operation: "assign", contract: "Read evidence and return." });
117
+ if (n === 2) return call("read", { path: "evidence.txt" });
118
+ if (n === 3) {
119
+ const ref = `ctx:${manager.getBranch().find((e) => e.message?.toolName === "read").id}`;
120
+ return call("freeflow_project", { operation: "add", refs: [ref] });
121
+ }
122
+ if (n === 4) {
123
+ const projects = manager.getBranch().filter((e) => e.message?.toolName === "freeflow_project");
124
+ assert.equal(projects.length, 1);
125
+ assert.deepEqual(
126
+ projects[0].message.details.items.map((item) => item.status),
127
+ ["added"],
128
+ );
129
+ assert.equal(projects[0].message.details.selected.length, 1);
130
+ return [
131
+ { name: "freeflow_return", args: { operation: "submit", report: "Evidence ready.", outcome: "completed" } },
132
+ ];
133
+ }
134
+ return [];
135
+ });
136
+ });
137
+
52
138
  test("provenance covers old sources and complete exchanges, with stable prefixes and unchanged originals", () => {
53
139
  const state = initialState();
54
140
  const entries = Array.from({ length: 40 }, (_, i) =>
@@ -87,7 +173,7 @@ test("provenance covers old sources and complete exchanges, with stable prefixes
87
173
  assert.ok(annotations.some((m) => m.content.includes("ctx:unknown | producer: unknown/common")));
88
174
  const callIndex = prepared.messages.findIndex((m) => m.role === "assistant" && m.content.some((b) => b.id === "c"));
89
175
  assert.equal(prepared.messages[callIndex + 1].toolCallId, "c", "no annotation splits a call/result exchange");
90
- assert.match(prepared.messages[callIndex - 1].content, /ctx:result.*producer: executor/);
176
+ assert.match(prepared.messages[callIndex + 2].content, /ctx:result.*producer: executor/);
91
177
  const extended = prepare(state, [...entries, entry("later", "result", assistant("Later observation"))]);
92
178
  assert.deepEqual(
93
179
  extended.messages.slice(0, prepared.messages.length - 1),
@@ -129,6 +215,13 @@ test("new control selections are rejected while old selections and mixed assista
129
215
  attempted.unresolved.map((p) => p.code),
130
216
  ["routing_source", "routing_source"],
131
217
  );
218
+ assert.deepEqual(
219
+ attempted.unresolved.map((p) => p.detail),
220
+ [
221
+ "Routing control messages are not task evidence. Use ctx:control#text if its visible assistant text is the intended evidence.",
222
+ "Routing control receipts are not task evidence; saved communication is delivered separately.",
223
+ ],
224
+ );
132
225
  const prior = { ...empty, revision: 1, selected: ["ctx:receipt"] };
133
226
  assert.equal(changeSelection(prior, { operation: "add", refs: ["ctx:receipt"] }, sources, state), prior);
134
227
  state.assignmentId = "a";