@hassangameryt/freeflow 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +25 -0
- package/README.md +57 -26
- package/capabilities/cognitive-routing/SKILL.md +94 -311
- package/com.github.copilot/hooks/hooks.json +11 -0
- package/command-surface.json +7 -1
- package/gemini-extension.json +5 -0
- package/hooks/adapters/claude-session-start.mjs +16 -0
- package/hooks/adapters/codex-session-start.mjs +16 -0
- package/hooks/adapters/copilot-session-start.mjs +13 -0
- package/hooks/adapters/cursor-session-start.mjs +13 -0
- package/hooks/adapters/gemini-session-start.mjs +15 -0
- package/hooks/claude/hooks.json +18 -0
- package/hooks/codex/hooks.json +18 -0
- package/hooks/cursor/hooks.json +9 -0
- package/hooks/freeflow-runtime-context.mjs +3 -215
- package/hooks/hooks.json +4 -5
- package/hooks/shared/runtime-context.mjs +210 -0
- package/hooks/tests/check-host-adapters.test.mjs +82 -0
- package/hooks/tests/check-runtime-context-hook.sh +34 -3
- package/package.json +20 -6
- package/pi-extension/dist/cognitive-routing-v2/budget.js +42 -0
- package/pi-extension/dist/cognitive-routing-v2/config.js +120 -0
- package/pi-extension/dist/cognitive-routing-v2/projection.js +321 -0
- package/pi-extension/dist/cognitive-routing-v2/render.js +231 -0
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +1585 -0
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +107 -0
- package/pi-extension/dist/cognitive-routing-v2/state.js +612 -0
- package/pi-extension/dist/cognitive-routing-v2/tools.js +91 -0
- package/pi-extension/dist/cognitive-routing-v2/types.js +44 -0
- package/pi-extension/dist/freeflow-context/tool.js +5 -0
- package/pi-extension/dist/index.js +279 -624
- package/pi-extension/dist/runtime/runtime-context.js +63 -13
- package/pi-extension/dist/session-sources/events.js +195 -0
- package/pi-extension/dist/session-sources/read-only-session.js +148 -0
- package/pi-extension/dist/session-sources/sources.js +233 -0
- package/pi-extension/dist/settings/settings-ui.js +44 -117
- package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
- package/pi-extension/src/cognitive-routing-v2/config.ts +143 -0
- package/pi-extension/src/cognitive-routing-v2/projection.ts +365 -0
- package/pi-extension/src/cognitive-routing-v2/render.ts +256 -0
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +1634 -0
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +109 -0
- package/pi-extension/src/cognitive-routing-v2/state.ts +622 -0
- package/pi-extension/src/cognitive-routing-v2/tools.ts +93 -0
- package/pi-extension/src/cognitive-routing-v2/types.ts +222 -0
- package/pi-extension/src/freeflow-context/tool.ts +5 -0
- package/pi-extension/src/index.ts +285 -674
- package/pi-extension/src/runtime/runtime-context.ts +75 -14
- package/pi-extension/src/session-sources/events.ts +208 -0
- package/pi-extension/src/session-sources/read-only-session.ts +170 -0
- package/pi-extension/src/session-sources/sources.ts +263 -0
- package/pi-extension/src/settings/settings-ui.ts +54 -131
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +484 -0
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +441 -0
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +245 -0
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +236 -0
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +230 -0
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +153 -0
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +275 -0
- package/pi-extension/tests/context-virtualization/extension.test.js +16 -0
- package/pi-extension/tests/fixtures/routing-native.js +188 -0
- package/pi-extension/tests/integration/mode-free-core.test.js +3 -2
- package/pi-extension/tests/integration/pi-extension.test.js +154 -37
- package/pi-extension/tests/integration/prompt-architecture.test.js +87 -6
- package/pi-extension/tests/integration/unaffected-regressions.test.js +77 -10
- package/plugin.json +33 -0
- 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,233 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { canonical, idFor, refFor } from "../cognitive-routing-v2/types.js";
|
|
4
|
+
export const bodyHash = (value) => createHash("sha256").update(canonical(value)).digest("hex");
|
|
5
|
+
export const textRef = (ref) => `${ref}#text`;
|
|
6
|
+
const routingTools = new Set(["freeflow_delegate", "freeflow_return", "freeflow_project", "freeflow_unit"]);
|
|
7
|
+
// Discovery and new selections share this policy. Existing saved selections keep
|
|
8
|
+
// their original delivery semantics; mixed assistant messages offer visible text.
|
|
9
|
+
export function isTaskEvidence(source) {
|
|
10
|
+
if (source.original) return true;
|
|
11
|
+
if (source.message.role === "toolResult") return !routingTools.has(source.message.toolName);
|
|
12
|
+
return (
|
|
13
|
+
source.message.role === "assistant" &&
|
|
14
|
+
!source.message.content?.some((b) => b.type === "toolCall" && routingTools.has(b.name)) &&
|
|
15
|
+
source.message.content?.some((b) => b.type === "text" && b.text?.trim())
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const callKey = (id, name) => JSON.stringify([id, name]);
|
|
19
|
+
export class Sources {
|
|
20
|
+
byRef = new Map();
|
|
21
|
+
ambiguous = new Set();
|
|
22
|
+
byBody = new Map();
|
|
23
|
+
exchanges = new Map();
|
|
24
|
+
owners = new Map();
|
|
25
|
+
tail;
|
|
26
|
+
associated;
|
|
27
|
+
entries = [];
|
|
28
|
+
activeIds;
|
|
29
|
+
constructor(entries, state, activeIds) {
|
|
30
|
+
this.refresh(entries, state, activeIds);
|
|
31
|
+
}
|
|
32
|
+
refresh(entries, state, activeIds) {
|
|
33
|
+
const prefix = this.entries.length <= entries.length && this.entries.every((e, i) => e === entries[i]);
|
|
34
|
+
const start = prefix ? this.entries.length : 0;
|
|
35
|
+
this.associated = undefined;
|
|
36
|
+
if (!prefix) {
|
|
37
|
+
this.byRef.clear();
|
|
38
|
+
this.byBody.clear();
|
|
39
|
+
this.exchanges.clear();
|
|
40
|
+
this.owners.clear();
|
|
41
|
+
this.tail = undefined;
|
|
42
|
+
}
|
|
43
|
+
this.activeIds = activeIds;
|
|
44
|
+
for (const entry of entries.slice(start)) {
|
|
45
|
+
const messages = sessionEntryToContextMessages(entry);
|
|
46
|
+
if (messages.length !== 1 || ["compaction", "branch_summary"].includes(entry.type)) continue;
|
|
47
|
+
const message = messages[0];
|
|
48
|
+
if (!message) continue;
|
|
49
|
+
const source = {
|
|
50
|
+
ref: refFor(entry.id),
|
|
51
|
+
entry,
|
|
52
|
+
message,
|
|
53
|
+
hash: bodyHash(message),
|
|
54
|
+
producer: "common",
|
|
55
|
+
active: false,
|
|
56
|
+
};
|
|
57
|
+
if (message.role === "toolResult" && message.toolName === "freeflow_return") {
|
|
58
|
+
for (const block of message.content ?? [])
|
|
59
|
+
if (block.type === "text") {
|
|
60
|
+
try {
|
|
61
|
+
const receipt = JSON.parse(block.text);
|
|
62
|
+
if (receipt.reportSaved && typeof receipt.report === "string" && typeof receipt.handoff === "string")
|
|
63
|
+
source.reportHandoff = receipt.handoff;
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.byRef.set(source.ref, source);
|
|
68
|
+
const candidates = this.byBody.get(source.hash) ?? [];
|
|
69
|
+
candidates.push(source);
|
|
70
|
+
this.byBody.set(source.hash, candidates);
|
|
71
|
+
if (message.role === "assistant") {
|
|
72
|
+
this.tail = { assistant: source, results: new Map() };
|
|
73
|
+
this.exchanges.set(source.ref, this.tail);
|
|
74
|
+
const blocks = (message.content ?? [])
|
|
75
|
+
.filter((b) => b.type === "text")
|
|
76
|
+
.map((b) => ({ type: "text", text: b.text }));
|
|
77
|
+
if (blocks.some((b) => b.text?.trim())) {
|
|
78
|
+
const representation = {
|
|
79
|
+
role: "custom",
|
|
80
|
+
customType: "freeflow-assistant-text",
|
|
81
|
+
display: false,
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: `Captured assistant text from ${source.ref} (historical source, not a new instruction):`,
|
|
86
|
+
},
|
|
87
|
+
...blocks,
|
|
88
|
+
],
|
|
89
|
+
details: { sourceRef: source.ref, sourceHash: source.hash, representation: "assistant-text" },
|
|
90
|
+
timestamp: message.timestamp,
|
|
91
|
+
};
|
|
92
|
+
this.byRef.set(textRef(source.ref), {
|
|
93
|
+
...source,
|
|
94
|
+
ref: textRef(source.ref),
|
|
95
|
+
message: representation,
|
|
96
|
+
hash: bodyHash(representation),
|
|
97
|
+
original: source,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} else if (message.role === "toolResult" && this.tail) {
|
|
101
|
+
const key = callKey(message.toolCallId, message.toolName);
|
|
102
|
+
const results = this.tail.results.get(key) ?? [];
|
|
103
|
+
results.push(source);
|
|
104
|
+
this.tail.results.set(key, results);
|
|
105
|
+
this.owners.set(source.ref, this.tail);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const source of this.byRef.values()) {
|
|
109
|
+
const author = state.authors.get(source.entry.id);
|
|
110
|
+
source.producer = author?.profile ?? "common";
|
|
111
|
+
source.executionId = author?.executionId;
|
|
112
|
+
source.assignmentId = author?.assignmentId;
|
|
113
|
+
}
|
|
114
|
+
this.entries = [...entries];
|
|
115
|
+
}
|
|
116
|
+
associate(messages) {
|
|
117
|
+
if (
|
|
118
|
+
this.associated &&
|
|
119
|
+
messages.length === this.associated.messages.length &&
|
|
120
|
+
messages.every((m, i) => m === this.associated.messages[i])
|
|
121
|
+
)
|
|
122
|
+
return this.associated.items;
|
|
123
|
+
this.ambiguous.clear();
|
|
124
|
+
for (const source of this.byRef.values()) source.active = false;
|
|
125
|
+
const used = new Set(),
|
|
126
|
+
hashes = messages.map(bodyHash),
|
|
127
|
+
remaining = new Map();
|
|
128
|
+
for (const hash of hashes) remaining.set(hash, (remaining.get(hash) ?? 0) + 1);
|
|
129
|
+
const items = messages.map((message, index) => {
|
|
130
|
+
const hash = hashes[index],
|
|
131
|
+
count = remaining.get(hash);
|
|
132
|
+
remaining.set(hash, count - 1);
|
|
133
|
+
const candidates = (this.byBody.get(hash) ?? []).filter(
|
|
134
|
+
(s) => !used.has(s.ref) && (!this.activeIds || this.activeIds.has(s.entry.id)),
|
|
135
|
+
);
|
|
136
|
+
const exact = candidates.find((s) => s.message === message);
|
|
137
|
+
const source = exact ?? (candidates.length === 1 || candidates.length === count ? candidates[0] : undefined);
|
|
138
|
+
if (!source) {
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
this.ambiguous.add(candidate.ref);
|
|
141
|
+
this.ambiguous.add(textRef(candidate.ref));
|
|
142
|
+
}
|
|
143
|
+
return { message };
|
|
144
|
+
}
|
|
145
|
+
used.add(source.ref);
|
|
146
|
+
source.active = true;
|
|
147
|
+
const text = this.byRef.get(textRef(source.ref));
|
|
148
|
+
if (text) text.active = true;
|
|
149
|
+
return { message, source };
|
|
150
|
+
});
|
|
151
|
+
this.associated = { messages: [...messages], items };
|
|
152
|
+
return items;
|
|
153
|
+
}
|
|
154
|
+
eligible(ref, state) {
|
|
155
|
+
const source = this.byRef.get(ref);
|
|
156
|
+
if (!idFor(ref) || !source)
|
|
157
|
+
return { ref, code: "source_unavailable", detail: "Exact source is unavailable on current native ancestry." };
|
|
158
|
+
const original = source.original ?? source;
|
|
159
|
+
if (bodyHash(original.message) !== original.hash || bodyHash(source.message) !== source.hash)
|
|
160
|
+
return {
|
|
161
|
+
ref,
|
|
162
|
+
code: "source_changed",
|
|
163
|
+
detail: "The captured source changed; reconcile evidence before delivery.",
|
|
164
|
+
};
|
|
165
|
+
if (source.producer !== "executor")
|
|
166
|
+
return { ref, code: "source_origin", detail: "Selection requires observed Executor attribution." };
|
|
167
|
+
if (state.exposure.get(original.ref) !== original.hash && state.exposure.get(source.ref) !== source.hash)
|
|
168
|
+
return {
|
|
169
|
+
ref,
|
|
170
|
+
code: "source_unexposed",
|
|
171
|
+
detail: "The captured body has not been fully exposed to Executor on this ancestry.",
|
|
172
|
+
};
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
selectionProblem(ref, state) {
|
|
176
|
+
const problem = this.eligible(ref, state);
|
|
177
|
+
if (problem) return problem;
|
|
178
|
+
const source = this.byRef.get(ref);
|
|
179
|
+
if (!isTaskEvidence(source))
|
|
180
|
+
return {
|
|
181
|
+
ref,
|
|
182
|
+
code: "routing_source",
|
|
183
|
+
detail: this.byRef.has(textRef(ref))
|
|
184
|
+
? `Routing control messages are not task evidence. Use ${textRef(ref)} if its visible assistant text is the intended evidence.`
|
|
185
|
+
: "Routing control receipts are not task evidence; saved communication is delivered separately.",
|
|
186
|
+
};
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
exchange(source) {
|
|
190
|
+
if (source.original) return { sources: [source], problems: [] };
|
|
191
|
+
if (!["assistant", "toolResult"].includes(source.message.role)) return { sources: [source], problems: [] };
|
|
192
|
+
const group = source.message.role === "assistant" ? this.exchanges.get(source.ref) : this.owners.get(source.ref);
|
|
193
|
+
const calls = group?.assistant.message.content?.filter((b) => b.type === "toolCall") ?? [];
|
|
194
|
+
if (
|
|
195
|
+
!group ||
|
|
196
|
+
(source.message.role === "toolResult" &&
|
|
197
|
+
!calls.some((c) => c.id === source.message.toolCallId && c.name === source.message.toolName))
|
|
198
|
+
)
|
|
199
|
+
return {
|
|
200
|
+
sources: [],
|
|
201
|
+
problems: [
|
|
202
|
+
{ ref: source.ref, code: "ambiguous_exchange", detail: "Result has no unique native assistant call." },
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
const sources = [group.assistant],
|
|
206
|
+
problems = [];
|
|
207
|
+
if (new Set(calls.map((c) => c.id)).size !== calls.length)
|
|
208
|
+
problems.push({
|
|
209
|
+
ref: group.assistant.ref,
|
|
210
|
+
code: "duplicate_call_id",
|
|
211
|
+
detail: "One native assistant exchange repeats a tool-call identity.",
|
|
212
|
+
});
|
|
213
|
+
for (const call of calls) {
|
|
214
|
+
const results = group.results.get(callKey(call.id, call.name)) ?? [];
|
|
215
|
+
if (results.length !== 1)
|
|
216
|
+
problems.push({
|
|
217
|
+
ref: group.assistant.ref,
|
|
218
|
+
code: "incomplete_exchange",
|
|
219
|
+
detail: `Native call ${call.id} does not have exactly one captured result.`,
|
|
220
|
+
});
|
|
221
|
+
else sources.push(results[0]);
|
|
222
|
+
}
|
|
223
|
+
return { sources, problems };
|
|
224
|
+
}
|
|
225
|
+
ordered(sources) {
|
|
226
|
+
const ids = new Set([...sources].map((s) => s.ref));
|
|
227
|
+
return this.entries.flatMap((entry) =>
|
|
228
|
+
[this.byRef.get(refFor(entry.id)), this.byRef.get(textRef(refFor(entry.id)))].filter(
|
|
229
|
+
(s) => !!s && ids.has(s.ref),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
setSessionCoreOverride,
|
|
12
12
|
} from "../runtime/runtime-context.js";
|
|
13
13
|
import { PiSettingsComponent } from "./settings-tui.js";
|
|
14
|
-
import { supportsPiSessionModelStateApi } from "../cognitive-routing/pi-session-control.js";
|
|
15
14
|
import { isPiFlowHost } from "../runtime/runtime-identity.js";
|
|
16
15
|
const DEFAULT_FREEFLOW_ENABLED = true;
|
|
17
16
|
const DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED = false;
|
|
@@ -102,7 +101,7 @@ function effectiveItemValue(item) {
|
|
|
102
101
|
function formatCoreValue(value) {
|
|
103
102
|
if (typeof value === "boolean") return booleanValue(value);
|
|
104
103
|
if (isRecord(value) && typeof value.provider === "string" && typeof value.model === "string") {
|
|
105
|
-
const effort = typeof value.
|
|
104
|
+
const effort = typeof value.thinking === "string" ? ` · ${value.thinking}` : "";
|
|
106
105
|
return `${value.provider}/${value.model}${effort}`;
|
|
107
106
|
}
|
|
108
107
|
return String(value ?? "");
|
|
@@ -291,10 +290,10 @@ function sessionFreeflowItems(state, cognitiveRoutingController) {
|
|
|
291
290
|
conversationHistoryItem.inactive = freeflowInactive;
|
|
292
291
|
const cognitiveRoutingState = cognitiveRoutingController?.state();
|
|
293
292
|
const cognitiveRoutingProfile =
|
|
294
|
-
cognitiveRoutingState?.controlMode === "manual-
|
|
295
|
-
? "
|
|
296
|
-
: cognitiveRoutingState?.controlMode === "manual-
|
|
297
|
-
? "
|
|
293
|
+
cognitiveRoutingState?.controlMode === "manual-executor"
|
|
294
|
+
? "executor"
|
|
295
|
+
: cognitiveRoutingState?.controlMode === "manual-coordinator"
|
|
296
|
+
? "coordinator"
|
|
298
297
|
: "auto";
|
|
299
298
|
const cognitiveRoutingItem = state.cognitiveRouting
|
|
300
299
|
? {
|
|
@@ -303,16 +302,16 @@ function sessionFreeflowItems(state, cognitiveRoutingController) {
|
|
|
303
302
|
description: "Hold a profile manually or release the hold for automatic model control.",
|
|
304
303
|
kind: "enum",
|
|
305
304
|
value: cognitiveRoutingProfile,
|
|
306
|
-
values: ["auto", "
|
|
305
|
+
values: ["auto", "executor", "coordinator"],
|
|
307
306
|
valueLabels: {
|
|
308
307
|
auto: "automatic",
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
executor: "manual · executor",
|
|
309
|
+
coordinator: "manual · coordinator",
|
|
311
310
|
},
|
|
312
311
|
valueDescriptions: {
|
|
313
|
-
auto: "Release the manual hold
|
|
314
|
-
|
|
315
|
-
|
|
312
|
+
auto: "Release the manual hold and return to Coordinator reconciliation.",
|
|
313
|
+
executor: "Hold the executor profile until /freeflow profile auto.",
|
|
314
|
+
coordinator: "Hold the coordinator profile until /freeflow profile auto.",
|
|
316
315
|
},
|
|
317
316
|
inactive: freeflowInactive || !state.cognitiveRouting.effective || cognitiveRoutingController === undefined,
|
|
318
317
|
runtimeInactive: state.cognitiveRouting.blockingReason?.code === "runtime_disabled",
|
|
@@ -363,8 +362,8 @@ function isCognitiveRoutingProfile(value) {
|
|
|
363
362
|
isRecord(value) &&
|
|
364
363
|
typeof value.provider === "string" &&
|
|
365
364
|
typeof value.model === "string" &&
|
|
366
|
-
typeof value.
|
|
367
|
-
COGNITIVE_ROUTING_THINKING_LEVELS.includes(value.
|
|
365
|
+
typeof value.thinking === "string" &&
|
|
366
|
+
COGNITIVE_ROUTING_THINKING_LEVELS.includes(value.thinking)
|
|
368
367
|
);
|
|
369
368
|
}
|
|
370
369
|
function cognitiveRoutingThinkingLevels(model, registry) {
|
|
@@ -487,8 +486,7 @@ function createCognitiveRoutingProfileWizard(currentValue, models, allowInherit)
|
|
|
487
486
|
}
|
|
488
487
|
const model = selected;
|
|
489
488
|
if (!model?.key || model.thinkingLevels.length === 0) return undefined;
|
|
490
|
-
const currentEffort =
|
|
491
|
-
currentProfile && currentModelKey === model.key ? currentProfile.thinkingLevel : undefined;
|
|
489
|
+
const currentEffort = currentProfile && currentModelKey === model.key ? currentProfile.thinking : undefined;
|
|
492
490
|
return {
|
|
493
491
|
title: `Choose effort · ${model.label}`,
|
|
494
492
|
choices: model.thinkingLevels.map((level) => ({
|
|
@@ -514,7 +512,7 @@ function createCognitiveRoutingProfileWizard(currentValue, models, allowInherit)
|
|
|
514
512
|
return {
|
|
515
513
|
provider: model.provider,
|
|
516
514
|
model: model.model,
|
|
517
|
-
|
|
515
|
+
thinking: selectedValues[1],
|
|
518
516
|
};
|
|
519
517
|
},
|
|
520
518
|
};
|
|
@@ -550,90 +548,13 @@ function cognitiveRoutingProfileItem(options) {
|
|
|
550
548
|
wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope === "local"),
|
|
551
549
|
};
|
|
552
550
|
}
|
|
553
|
-
function cognitiveRoutingSessionStartItem(options) {
|
|
554
|
-
const path = ["cognitiveRouting", "sessionStart", options.setting];
|
|
555
|
-
const defaultValue = options.setting === "control" ? "automatic" : "reasoning";
|
|
556
|
-
const values = options.setting === "control" ? ["automatic", "manual"] : ["standard", "reasoning"];
|
|
557
|
-
const labels = Object.fromEntries(values.map((value) => [value, value]));
|
|
558
|
-
const descriptions =
|
|
559
|
-
options.setting === "control"
|
|
560
|
-
? {
|
|
561
|
-
automatic: "Start the new session in Reasoning; automatic delegation may use Standard during execution.",
|
|
562
|
-
manual: "Hold the selected profile for the new session until the user changes it.",
|
|
563
|
-
}
|
|
564
|
-
: {
|
|
565
|
-
standard: "Start a new manual session with the configured Standard preset.",
|
|
566
|
-
reasoning: "Start a new manual session with the configured Reasoning preset.",
|
|
567
|
-
};
|
|
568
|
-
const repositoryValue = getPath(options.rawConfig, path);
|
|
569
|
-
const localValue = getPath(options.localConfig, path);
|
|
570
|
-
const isValidValue = (value) => values.includes(value);
|
|
571
|
-
const repositorySetting = isValidValue(repositoryValue) ? repositoryValue : undefined;
|
|
572
|
-
const localSetting = isValidValue(localValue) ? localValue : undefined;
|
|
573
|
-
const inheritedValue = repositorySetting ?? defaultValue;
|
|
574
|
-
const inheritedSource = repositorySetting ? "repository" : "builtin";
|
|
575
|
-
const configuredSource =
|
|
576
|
-
options.capabilityState?.sessionStartSources?.[options.setting] ?? (repositorySetting ? "repository" : undefined);
|
|
577
|
-
const effectiveSessionStartControl =
|
|
578
|
-
options.capabilityState?.sessionStart?.control ??
|
|
579
|
-
getPath(options.localConfig, ["cognitiveRouting", "sessionStart", "control"]) ??
|
|
580
|
-
getPath(options.rawConfig, ["cognitiveRouting", "sessionStart", "control"]) ??
|
|
581
|
-
"automatic";
|
|
582
|
-
const effectiveValue =
|
|
583
|
-
options.setting === "profile" && effectiveSessionStartControl === "automatic"
|
|
584
|
-
? "reasoning"
|
|
585
|
-
: (options.capabilityState?.sessionStart?.[options.setting] ?? inheritedValue);
|
|
586
|
-
const effectiveSource = cognitiveRoutingSettingsSource(configuredSource);
|
|
587
|
-
const label = options.setting === "control" ? "Session start control" : "Manual session-start profile";
|
|
588
|
-
const description =
|
|
589
|
-
options.setting === "control"
|
|
590
|
-
? "Choose automatic or manual control for new sessions only; changing this does not alter the active session."
|
|
591
|
-
: "Choose the profile for manual session starts; automatic sessions always start in Reasoning.";
|
|
592
|
-
const item =
|
|
593
|
-
options.scope === "local"
|
|
594
|
-
? {
|
|
595
|
-
id: `freeflow.cognitiveRouting.sessionStart.${options.setting}`,
|
|
596
|
-
label,
|
|
597
|
-
description: `${description} Choose inherit to use the repository value; use /freeflow settings repo to edit shared defaults.`,
|
|
598
|
-
path,
|
|
599
|
-
kind: "enum",
|
|
600
|
-
value: localSetting ?? LOCAL_INHERIT,
|
|
601
|
-
values: [LOCAL_INHERIT, ...values],
|
|
602
|
-
valueLabels: { inherit: "Inherit repository", ...labels },
|
|
603
|
-
valueDescriptions: {
|
|
604
|
-
inherit: `Use ${inheritedValue} from ${inheritedSource}.`,
|
|
605
|
-
...descriptions,
|
|
606
|
-
},
|
|
607
|
-
format: (value) => String(value),
|
|
608
|
-
configScope: "local",
|
|
609
|
-
configValues: { inherit: undefined },
|
|
610
|
-
effectiveValue,
|
|
611
|
-
effectiveSource,
|
|
612
|
-
inheritedValue,
|
|
613
|
-
inheritedSource,
|
|
614
|
-
}
|
|
615
|
-
: {
|
|
616
|
-
id: `freeflow.cognitiveRouting.sessionStart.${options.setting}`,
|
|
617
|
-
label,
|
|
618
|
-
description: `${description} This edits shared .freeflow/config.json.`,
|
|
619
|
-
path,
|
|
620
|
-
kind: "enum",
|
|
621
|
-
value: inheritedValue,
|
|
622
|
-
values,
|
|
623
|
-
valueLabels: labels,
|
|
624
|
-
valueDescriptions: descriptions,
|
|
625
|
-
format: (value) => String(value),
|
|
626
|
-
defaultValue,
|
|
627
|
-
configScope: "repository",
|
|
628
|
-
effectiveValue,
|
|
629
|
-
effectiveSource,
|
|
630
|
-
localOverrideValue: localSetting,
|
|
631
|
-
};
|
|
632
|
-
item.displaySuffix = coreDisplaySuffix(item);
|
|
633
|
-
return item;
|
|
634
|
-
}
|
|
635
551
|
function isCognitiveRoutingRuntimeAvailable(pi) {
|
|
636
|
-
return
|
|
552
|
+
return (
|
|
553
|
+
!isPiFlowHost(pi?.host) &&
|
|
554
|
+
typeof pi?.appendEntry === "function" &&
|
|
555
|
+
typeof pi?.setModel === "function" &&
|
|
556
|
+
typeof pi?.setThinkingLevel === "function"
|
|
557
|
+
);
|
|
637
558
|
}
|
|
638
559
|
function freeflowItems(rawConfig, options = {}) {
|
|
639
560
|
const scope = options.scope ?? "repository";
|
|
@@ -689,7 +610,7 @@ function freeflowItems(rawConfig, options = {}) {
|
|
|
689
610
|
label: "Enabled",
|
|
690
611
|
description: cognitiveRoutingRuntimeDisabled
|
|
691
612
|
? "Requires a host model-state control API; configuration is read-only on this host."
|
|
692
|
-
: "Allow Cognitive Routing to
|
|
613
|
+
: "Allow Cognitive Routing to manage explicit assignments and request native profile transitions.",
|
|
693
614
|
path: ["cognitiveRouting", "enabled"],
|
|
694
615
|
effectiveValue: cognitiveRoutingState?.enabled ?? false,
|
|
695
616
|
effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.enabledSource),
|
|
@@ -697,7 +618,22 @@ function freeflowItems(rawConfig, options = {}) {
|
|
|
697
618
|
});
|
|
698
619
|
cognitiveRoutingEnabledItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
|
|
699
620
|
cognitiveRoutingEnabledItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
|
|
700
|
-
const
|
|
621
|
+
const cognitiveRoutingProjectionItem = createScopedBooleanItem({
|
|
622
|
+
scope,
|
|
623
|
+
rawConfig,
|
|
624
|
+
localConfig,
|
|
625
|
+
id: "freeflow.cognitiveRouting.projection",
|
|
626
|
+
label: "Context projection",
|
|
627
|
+
description:
|
|
628
|
+
"Select Executor evidence for Coordinator; disable projection to keep ordinary Pi context in both profiles.",
|
|
629
|
+
path: ["cognitiveRouting", "projection"],
|
|
630
|
+
effectiveValue: cognitiveRoutingState?.projection ?? false,
|
|
631
|
+
effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.projectionSource),
|
|
632
|
+
defaultValue: false,
|
|
633
|
+
});
|
|
634
|
+
cognitiveRoutingProjectionItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
|
|
635
|
+
cognitiveRoutingProjectionItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
|
|
636
|
+
const cognitiveRoutingProfiles = ["executor", "coordinator"].map((name) =>
|
|
701
637
|
cognitiveRoutingProfileItem({
|
|
702
638
|
name: name,
|
|
703
639
|
scope,
|
|
@@ -707,16 +643,7 @@ function freeflowItems(rawConfig, options = {}) {
|
|
|
707
643
|
ctx: options.ctx,
|
|
708
644
|
}),
|
|
709
645
|
);
|
|
710
|
-
const
|
|
711
|
-
cognitiveRoutingSessionStartItem({
|
|
712
|
-
setting,
|
|
713
|
-
scope,
|
|
714
|
-
rawConfig,
|
|
715
|
-
localConfig,
|
|
716
|
-
capabilityState: cognitiveRoutingState,
|
|
717
|
-
}),
|
|
718
|
-
);
|
|
719
|
-
for (const item of [...cognitiveRoutingProfiles, ...cognitiveRoutingSessionStart]) {
|
|
646
|
+
for (const item of cognitiveRoutingProfiles) {
|
|
720
647
|
item.inactive ||= freeflowInactive || cognitiveRoutingRuntimeDisabled;
|
|
721
648
|
item.runtimeInactive = cognitiveRoutingRuntimeDisabled;
|
|
722
649
|
}
|
|
@@ -734,12 +661,12 @@ function freeflowItems(rawConfig, options = {}) {
|
|
|
734
661
|
label: "Cognitive Routing",
|
|
735
662
|
description: cognitiveRoutingRuntimeDisabled
|
|
736
663
|
? "Cognitive Routing configuration is visible for inspection but requires a host model-state control API."
|
|
737
|
-
: "Configure the
|
|
664
|
+
: "Configure the Executor and Coordinator profiles and choose whether Freeflow may manage model state for this repository.",
|
|
738
665
|
kind: "group",
|
|
739
666
|
value: cognitiveRoutingRuntimeDisabled ? false : (cognitiveRoutingState?.enabled ?? false),
|
|
740
667
|
inactive: freeflowInactive,
|
|
741
668
|
displaySuffix: cognitiveRoutingStatus,
|
|
742
|
-
children: [cognitiveRoutingEnabledItem, ...cognitiveRoutingProfiles,
|
|
669
|
+
children: [cognitiveRoutingEnabledItem, ...cognitiveRoutingProfiles, cognitiveRoutingProjectionItem],
|
|
743
670
|
};
|
|
744
671
|
})();
|
|
745
672
|
return [
|
|
@@ -834,7 +761,7 @@ async function updateConfig(cwd, item, value, scope = item.configScope ?? "repos
|
|
|
834
761
|
await writeFile(join(cwd, ".freeflow/config.json"), `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
835
762
|
}
|
|
836
763
|
function isCognitiveRoutingProfileItem(item) {
|
|
837
|
-
return item.id === "freeflow.cognitiveRouting.
|
|
764
|
+
return item.id === "freeflow.cognitiveRouting.executor" || item.id === "freeflow.cognitiveRouting.coordinator";
|
|
838
765
|
}
|
|
839
766
|
function valueForDisplay(item) {
|
|
840
767
|
let value;
|
|
@@ -875,8 +802,8 @@ function refreshSettingsDerivedState(items) {
|
|
|
875
802
|
const cognitiveRoutingGroup = findSettingsItem(items, "freeflow.cognitiveRouting");
|
|
876
803
|
const cognitiveRoutingEnabledItem = findSettingsItem(items, "freeflow.cognitiveRouting.enabled");
|
|
877
804
|
const cognitiveRoutingProfiles = [
|
|
878
|
-
findSettingsItem(items, "freeflow.cognitiveRouting.
|
|
879
|
-
findSettingsItem(items, "freeflow.cognitiveRouting.
|
|
805
|
+
findSettingsItem(items, "freeflow.cognitiveRouting.executor"),
|
|
806
|
+
findSettingsItem(items, "freeflow.cognitiveRouting.coordinator"),
|
|
880
807
|
];
|
|
881
808
|
if (cognitiveRoutingGroup && cognitiveRoutingEnabledItem) {
|
|
882
809
|
const enabled = effectiveItemValue(cognitiveRoutingEnabledItem) === true;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// A local planning estimate, not a provider token count or an output allocation.
|
|
2
|
+
// In particular, base64 and native usage/metadata are not model text.
|
|
3
|
+
export function estimateRequest(systemPrompt: string, tools: readonly any[], messages: readonly any[], model: any) {
|
|
4
|
+
const text = (value: string) => Math.ceil(value.length / 3);
|
|
5
|
+
const content = (value: any): number =>
|
|
6
|
+
typeof value === "string"
|
|
7
|
+
? text(value)
|
|
8
|
+
: Array.isArray(value)
|
|
9
|
+
? value.reduce(
|
|
10
|
+
(sum, b) =>
|
|
11
|
+
sum +
|
|
12
|
+
(b.type === "image"
|
|
13
|
+
? 1200
|
|
14
|
+
: b.type === "toolCall"
|
|
15
|
+
? text(b.name + JSON.stringify(b.arguments ?? {}))
|
|
16
|
+
: b.type === "thinking"
|
|
17
|
+
? text(b.thinking ?? "")
|
|
18
|
+
: text(b.text ?? "")),
|
|
19
|
+
0,
|
|
20
|
+
)
|
|
21
|
+
: 0;
|
|
22
|
+
const estimatedTokens =
|
|
23
|
+
text(systemPrompt) +
|
|
24
|
+
text(JSON.stringify(tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })))) +
|
|
25
|
+
messages.reduce((sum, m) => sum + 8 + content(m.content), 0);
|
|
26
|
+
const window = model?.contextWindow ?? 0;
|
|
27
|
+
const outputReserve = Math.min(4096, Math.max(1, Math.floor(window / 10)));
|
|
28
|
+
const maximumInputTokens = Math.max(0, window - outputReserve);
|
|
29
|
+
const estimateMethod =
|
|
30
|
+
"Planning estimate: text characters / 3, 1200 tokens/image, schemas and message framing; output headroom is not the provider allocation";
|
|
31
|
+
const warnings =
|
|
32
|
+
estimatedTokens > maximumInputTokens
|
|
33
|
+
? [
|
|
34
|
+
{
|
|
35
|
+
ref: "",
|
|
36
|
+
code: "budget_estimate",
|
|
37
|
+
detail: `Estimated input ${estimatedTokens} exceeds planning allowance ${maximumInputTokens}. Actual output allocation and image tokenization are provider-dependent; fit is unconfirmed. Use native compaction or revise explicitly if the provider rejects the request.`,
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
: [];
|
|
41
|
+
return { estimatedTokens, maximumInputTokens, outputReserve, estimateMethod, warnings };
|
|
42
|
+
}
|