@hassangameryt/freeflow 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +147 -127
- package/capabilities/cognitive-routing/SKILL.md +162 -93
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
- package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
- package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
- package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
- package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
- package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
- package/pi-extension/dist/context-virtualization/runtime.js +29 -7
- package/pi-extension/dist/freeflow-context/tool.js +7 -6
- package/pi-extension/dist/index.js +36 -15
- package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
- package/pi-extension/dist/provider-support/astra/history.js +125 -0
- package/pi-extension/dist/provider-support/astra/index.js +11 -0
- package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
- package/pi-extension/dist/provider-support/index.js +4 -0
- package/pi-extension/dist/runtime/request-history.js +216 -0
- package/pi-extension/dist/runtime/runtime-context.js +29 -10
- package/pi-extension/dist/session-sources/events.js +22 -0
- package/pi-extension/dist/session-sources/provenance.js +32 -0
- package/pi-extension/dist/session-sources/sources.js +51 -7
- package/pi-extension/dist/settings/settings-ui.js +247 -47
- package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
- package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
- package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
- package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
- package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
- package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
- package/pi-extension/src/context-virtualization/runtime.ts +30 -8
- package/pi-extension/src/freeflow-context/tool.ts +7 -6
- package/pi-extension/src/index.ts +36 -14
- package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
- package/pi-extension/src/provider-support/astra/history.ts +145 -0
- package/pi-extension/src/provider-support/astra/index.ts +12 -0
- package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
- package/pi-extension/src/provider-support/index.ts +5 -0
- package/pi-extension/src/runtime/request-history.ts +243 -0
- package/pi-extension/src/runtime/runtime-context.ts +32 -10
- package/pi-extension/src/session-sources/events.ts +22 -0
- package/pi-extension/src/session-sources/provenance.ts +39 -0
- package/pi-extension/src/session-sources/sources.ts +77 -6
- package/pi-extension/src/settings/settings-ui.ts +276 -46
- package/pi-extension/tests/cache-reuse/history.test.js +117 -0
- package/pi-extension/tests/cache-reuse/native.test.js +91 -0
- package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +94 -1
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
- package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
- package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
- package/pi-extension/tests/fixtures/routing-native.js +4 -1
- package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
- package/pi-extension/tests/integration/pi-extension.test.js +508 -29
- package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
- package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
- package/pi-extension/tests/provider-support/astra.test.js +306 -0
- package/pi-extension/tests/provider-support/cli.test.js +92 -0
- package/pi-extension/tests/provider-support/native.test.js +183 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/skills/design-for-depth/SKILL.md +1 -1
- package/skills/design-for-depth/references/implementation-decisions.md +3 -3
- package/skills/discuss/SKILL.md +2 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { activeReadOnlySessionBranch, readOnlySessionSnapshot } from "../../session-sources/read-only-session.js";
|
|
2
|
+
import { ENTRY_TYPE, parseAttempt } from "./history.js";
|
|
3
|
+
export class SessionState {
|
|
4
|
+
pi;
|
|
5
|
+
reader;
|
|
6
|
+
acknowledged = new Map();
|
|
7
|
+
fault;
|
|
8
|
+
constructor(pi, reader) {
|
|
9
|
+
this.pi = pi;
|
|
10
|
+
this.reader = reader;
|
|
11
|
+
}
|
|
12
|
+
branch() {
|
|
13
|
+
const entries = this.reader.getBranch();
|
|
14
|
+
let parent = null;
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (!entry.id || seen.has(entry.id) || entry.parentId !== parent)
|
|
18
|
+
throw new Error("Invalid Astra session ancestry");
|
|
19
|
+
seen.add(entry.id);
|
|
20
|
+
parent = entry.id;
|
|
21
|
+
}
|
|
22
|
+
if (parent !== this.reader.getLeafId()) throw new Error("Astra session leaf changed");
|
|
23
|
+
return entries;
|
|
24
|
+
}
|
|
25
|
+
async records() {
|
|
26
|
+
if (this.fault) throw new Error(this.fault);
|
|
27
|
+
const branch = this.branch();
|
|
28
|
+
// Model visits are not history replacement. Derive both identities from the
|
|
29
|
+
// selected native ancestry so old v1 records remain readable without rewriting them.
|
|
30
|
+
const boundaries = new Map();
|
|
31
|
+
let epoch = "root",
|
|
32
|
+
legacy = "root",
|
|
33
|
+
model;
|
|
34
|
+
for (const entry of branch) {
|
|
35
|
+
if (entry.type === "compaction" || entry.type === "branch_summary") epoch = legacy = entry.id;
|
|
36
|
+
if (entry.type === "model_change") {
|
|
37
|
+
const next = `${entry.provider}/${entry.modelId}`;
|
|
38
|
+
if (model !== next) legacy = entry.id;
|
|
39
|
+
model = next;
|
|
40
|
+
}
|
|
41
|
+
boundaries.set(entry.id, { epoch, legacy });
|
|
42
|
+
}
|
|
43
|
+
const entries = branch.filter((e) => e.type === "custom" && e.customType === ENTRY_TYPE);
|
|
44
|
+
const records = entries.map((e) => {
|
|
45
|
+
const record = parseAttempt(e.data);
|
|
46
|
+
if (
|
|
47
|
+
record.basis !== e.parentId &&
|
|
48
|
+
!(this.reader.getHeader?.()?.parentSession && !branch.some((entry) => entry.id === record.basis))
|
|
49
|
+
)
|
|
50
|
+
throw new Error("Astra attempt anchor mismatch");
|
|
51
|
+
const boundary = boundaries.get(e.id);
|
|
52
|
+
if (record.generation !== boundary.epoch && record.generation !== boundary.legacy)
|
|
53
|
+
throw new Error("Astra history generation unavailable");
|
|
54
|
+
return { ...record, generation: boundary.epoch };
|
|
55
|
+
});
|
|
56
|
+
if (new Set(records.map((r) => r.id)).size !== records.length) throw new Error("Duplicate Astra attempt");
|
|
57
|
+
const parents = new Map();
|
|
58
|
+
for (const record of records) {
|
|
59
|
+
const parent = record.parent === null ? undefined : parents.get(record.parent);
|
|
60
|
+
if (
|
|
61
|
+
record.parent !== null &&
|
|
62
|
+
(!parent ||
|
|
63
|
+
parent.key !== record.key ||
|
|
64
|
+
parent.generation !== record.generation ||
|
|
65
|
+
parent.envelope !== record.envelope ||
|
|
66
|
+
parent.baseline !== record.baseline ||
|
|
67
|
+
parent.length > record.length)
|
|
68
|
+
)
|
|
69
|
+
throw new Error("Invalid Astra history parent");
|
|
70
|
+
const expected = record.update?.effort ?? parent?.effective ?? record.baseline;
|
|
71
|
+
if (record.effective !== expected || (record.update && record.update.at < (parent?.length ?? 0)))
|
|
72
|
+
throw new Error("Invalid Astra effective effort");
|
|
73
|
+
parents.set(record.id, record);
|
|
74
|
+
}
|
|
75
|
+
const unknown = entries.some((e) => this.acknowledged.get(e.id) !== JSON.stringify(e));
|
|
76
|
+
const file = this.reader.getSessionFile();
|
|
77
|
+
if (unknown && file && branch.some((e) => e.type === "message" && e.message?.role === "assistant")) {
|
|
78
|
+
const sessionId = this.reader.getSessionId(),
|
|
79
|
+
leaf = this.reader.getLeafId();
|
|
80
|
+
const snapshot = await readOnlySessionSnapshot(file);
|
|
81
|
+
const persisted = activeReadOnlySessionBranch(snapshot, leaf);
|
|
82
|
+
if (
|
|
83
|
+
snapshot.sessionId !== sessionId ||
|
|
84
|
+
this.reader.getLeafId() !== leaf ||
|
|
85
|
+
JSON.stringify(persisted) !== JSON.stringify(branch)
|
|
86
|
+
)
|
|
87
|
+
throw new Error("Astra persisted ancestry differs");
|
|
88
|
+
}
|
|
89
|
+
for (const entry of entries) this.acknowledged.set(entry.id, JSON.stringify(entry));
|
|
90
|
+
return records;
|
|
91
|
+
}
|
|
92
|
+
generation() {
|
|
93
|
+
let generation = "root";
|
|
94
|
+
for (const entry of this.branch()) {
|
|
95
|
+
if (entry.type === "compaction" || entry.type === "branch_summary") generation = entry.id;
|
|
96
|
+
}
|
|
97
|
+
return generation;
|
|
98
|
+
}
|
|
99
|
+
append(record) {
|
|
100
|
+
try {
|
|
101
|
+
if (this.reader.getLeafId() !== record.basis) throw new Error("Astra request ancestry changed");
|
|
102
|
+
this.pi.appendEntry(ENTRY_TYPE, record);
|
|
103
|
+
const entry = this.branch().find(
|
|
104
|
+
(e) => e.type === "custom" && e.customType === ENTRY_TYPE && e.data?.id === record.id,
|
|
105
|
+
);
|
|
106
|
+
if (!entry || entry.parentId !== record.basis || JSON.stringify(entry.data) !== JSON.stringify(record))
|
|
107
|
+
throw new Error("Astra append not acknowledged");
|
|
108
|
+
this.acknowledged.set(entry.id, JSON.stringify(entry));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.fault = "Astra append uncertain; native effort retained until session recovery";
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { activeReadOnlySessionBranch, readOnlySessionSnapshot } from "../session-sources/read-only-session.js";
|
|
3
|
+
const ENTRY = "freeflow-request-history-v1";
|
|
4
|
+
const TRANSIENT = new Set([
|
|
5
|
+
"freeflow-runtime-state",
|
|
6
|
+
"freeflow-cognitive-routing-runtime-state",
|
|
7
|
+
"freeflow-routing-v2-state",
|
|
8
|
+
"freeflow-routing-budget",
|
|
9
|
+
"freeflow-routing-communication",
|
|
10
|
+
]);
|
|
11
|
+
const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
12
|
+
function fingerprint(message) {
|
|
13
|
+
const { usage: _usage, timestamp: _timestamp, details: _details, ...body } = message;
|
|
14
|
+
return hash(body);
|
|
15
|
+
}
|
|
16
|
+
function prefixes(messages) {
|
|
17
|
+
const result = [hash([])];
|
|
18
|
+
for (const message of messages) result.push(hash([result.at(-1), fingerprint(message)]));
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
function owned(message) {
|
|
22
|
+
return message?.role === "custom" && TRANSIENT.has(message.customType);
|
|
23
|
+
}
|
|
24
|
+
function parse(value) {
|
|
25
|
+
if (
|
|
26
|
+
!value ||
|
|
27
|
+
value.version !== 1 ||
|
|
28
|
+
typeof value.id !== "string" ||
|
|
29
|
+
typeof value.view !== "string" ||
|
|
30
|
+
typeof value.generation !== "string" ||
|
|
31
|
+
!(value.basis === null || typeof value.basis === "string") ||
|
|
32
|
+
!Number.isSafeInteger(value.length) ||
|
|
33
|
+
value.length < 0 ||
|
|
34
|
+
!/^[a-f0-9]{64}$/.test(value.prefix) ||
|
|
35
|
+
!(value.parent === null || typeof value.parent === "string") ||
|
|
36
|
+
!value.states ||
|
|
37
|
+
typeof value.states !== "object" ||
|
|
38
|
+
Array.isArray(value.states) ||
|
|
39
|
+
!Object.entries(value.states).every(
|
|
40
|
+
([key, val]) => TRANSIENT.has(key) && typeof val === "string" && /^[a-f0-9]{64}$/.test(val),
|
|
41
|
+
) ||
|
|
42
|
+
!Array.isArray(value.retained) ||
|
|
43
|
+
!value.retained.every((x) => typeof x === "string") ||
|
|
44
|
+
!Array.isArray(value.additions) ||
|
|
45
|
+
!value.additions.every((x) => Number.isSafeInteger(x.at) && x.at >= 0 && x.at <= value.length && owned(x.message))
|
|
46
|
+
)
|
|
47
|
+
throw new Error("Invalid Freeflow context history");
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
/** Preserve only Freeflow-generated occurrences. Native content remains owned by Pi and its projectors. */
|
|
51
|
+
export class RequestHistory {
|
|
52
|
+
pi;
|
|
53
|
+
acknowledged = new Map();
|
|
54
|
+
identity;
|
|
55
|
+
fault = false;
|
|
56
|
+
serial = Promise.resolve();
|
|
57
|
+
revision = 0;
|
|
58
|
+
constructor(pi) {
|
|
59
|
+
this.pi = pi;
|
|
60
|
+
}
|
|
61
|
+
reset() {
|
|
62
|
+
this.revision++;
|
|
63
|
+
this.acknowledged.clear();
|
|
64
|
+
this.identity = undefined;
|
|
65
|
+
this.fault = false;
|
|
66
|
+
}
|
|
67
|
+
async assemble(messages, view, ctx, notice) {
|
|
68
|
+
const revision = this.revision;
|
|
69
|
+
const operation = this.serial.then(async () => {
|
|
70
|
+
if (revision !== this.revision) return messages;
|
|
71
|
+
try {
|
|
72
|
+
return await this.prepare(messages, view, ctx, revision, notice);
|
|
73
|
+
} catch {
|
|
74
|
+
try {
|
|
75
|
+
ctx.ui?.setStatus?.("freeflow-cache", "Freeflow context replay unavailable · current view retained");
|
|
76
|
+
} catch {}
|
|
77
|
+
// Keep the already-qualified projected view on failure, never raw hidden history.
|
|
78
|
+
return messages;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
this.serial = operation.catch(() => {});
|
|
82
|
+
return operation;
|
|
83
|
+
}
|
|
84
|
+
async prepare(messages, view, ctx, revision, notice) {
|
|
85
|
+
const reader = ctx.sessionManager;
|
|
86
|
+
if (!reader?.getBranch || !reader.getLeafId || !reader.getSessionId || !this.pi.appendEntry) return messages;
|
|
87
|
+
const identity = `${reader.getSessionId()}:${reader.getSessionFile?.() ?? "memory"}`;
|
|
88
|
+
if (this.identity !== identity) {
|
|
89
|
+
this.acknowledged.clear();
|
|
90
|
+
this.fault = false;
|
|
91
|
+
this.identity = identity;
|
|
92
|
+
}
|
|
93
|
+
if (this.fault) throw new Error("Uncertain Freeflow context append");
|
|
94
|
+
const branch = reader.getBranch(),
|
|
95
|
+
leaf = reader.getLeafId();
|
|
96
|
+
let parent = null,
|
|
97
|
+
generation = "root";
|
|
98
|
+
const ids = new Set();
|
|
99
|
+
for (const entry of branch) {
|
|
100
|
+
if (entry.parentId !== parent || ids.has(entry.id)) throw new Error("Invalid context ancestry");
|
|
101
|
+
parent = entry.id;
|
|
102
|
+
ids.add(entry.id);
|
|
103
|
+
if (entry.type === "compaction" || entry.type === "branch_summary") generation = entry.id;
|
|
104
|
+
}
|
|
105
|
+
if (parent !== leaf) throw new Error("Context leaf changed");
|
|
106
|
+
const entries = branch.filter((e) => e.type === "custom" && e.customType === ENTRY);
|
|
107
|
+
if (
|
|
108
|
+
entries.some((e) => this.acknowledged.get(e.id) !== hash(e)) &&
|
|
109
|
+
reader.getSessionFile?.() &&
|
|
110
|
+
branch.some((e) => e.message?.role === "assistant")
|
|
111
|
+
) {
|
|
112
|
+
const snapshot = await readOnlySessionSnapshot(reader.getSessionFile());
|
|
113
|
+
if (
|
|
114
|
+
snapshot.sessionId !== reader.getSessionId() ||
|
|
115
|
+
JSON.stringify(activeReadOnlySessionBranch(snapshot, leaf)) !== JSON.stringify(branch)
|
|
116
|
+
)
|
|
117
|
+
throw new Error("Context history readback differs");
|
|
118
|
+
}
|
|
119
|
+
if (revision !== this.revision || reader.getLeafId() !== leaf) throw new Error("Context changed while preparing");
|
|
120
|
+
const frames = [],
|
|
121
|
+
byId = new Map();
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const frame = parse(entry.data);
|
|
124
|
+
if (
|
|
125
|
+
(frame.basis !== entry.parentId && !(reader.getHeader?.()?.parentSession && !ids.has(frame.basis))) ||
|
|
126
|
+
byId.has(frame.id)
|
|
127
|
+
)
|
|
128
|
+
throw new Error("Context frame anchor differs");
|
|
129
|
+
const previous = frame.parent ? byId.get(frame.parent) : undefined;
|
|
130
|
+
if (frame.parent && (!previous || previous.generation !== frame.generation || previous.length > frame.length))
|
|
131
|
+
throw new Error("Context frame parent missing");
|
|
132
|
+
frames.push(frame);
|
|
133
|
+
byId.set(frame.id, frame);
|
|
134
|
+
this.acknowledged.set(entry.id, hash(entry));
|
|
135
|
+
}
|
|
136
|
+
const base = messages.filter((m) => !owned(m));
|
|
137
|
+
const current = messages
|
|
138
|
+
.filter(owned)
|
|
139
|
+
.filter((message) => !notice || message.customType !== "freeflow-routing-budget");
|
|
140
|
+
const hashes = prefixes(base);
|
|
141
|
+
const candidates = frames.filter((f) => f.generation === generation && hashes[f.length] === f.prefix);
|
|
142
|
+
const prior = candidates.reduce((best, f) => (!best || f.length >= best.length ? f : best), undefined);
|
|
143
|
+
const states = { ...prior?.states },
|
|
144
|
+
additions = [];
|
|
145
|
+
const seenRetained = new Set(prior?.retained ?? []);
|
|
146
|
+
for (const message of current) {
|
|
147
|
+
const signature = fingerprint(message),
|
|
148
|
+
kind = message.customType;
|
|
149
|
+
if (kind === "freeflow-routing-communication" ? seenRetained.has(signature) : states[kind] === signature)
|
|
150
|
+
continue;
|
|
151
|
+
additions.push({ at: base.length, message: structuredClone(message) });
|
|
152
|
+
if (kind === "freeflow-routing-communication") seenRetained.add(signature);
|
|
153
|
+
else states[kind] = signature;
|
|
154
|
+
}
|
|
155
|
+
const injections = [];
|
|
156
|
+
const chain = [];
|
|
157
|
+
for (let cursor = prior; cursor; cursor = cursor.parent ? byId.get(cursor.parent) : undefined)
|
|
158
|
+
chain.unshift(cursor);
|
|
159
|
+
for (const frame of chain) injections.push(...frame.additions);
|
|
160
|
+
injections.push(...additions);
|
|
161
|
+
const result = [];
|
|
162
|
+
let next = 0;
|
|
163
|
+
for (let i = 0; i <= base.length; i++) {
|
|
164
|
+
while (injections[next]?.at === i) result.push(structuredClone(injections[next++].message));
|
|
165
|
+
if (i < base.length) result.push(base[i]);
|
|
166
|
+
}
|
|
167
|
+
if (next !== injections.length) throw new Error("Context anchor ordering differs");
|
|
168
|
+
if (notice) {
|
|
169
|
+
const warning = notice(result);
|
|
170
|
+
if (warning || states["freeflow-routing-budget"]) {
|
|
171
|
+
const message = warning ?? {
|
|
172
|
+
role: "custom",
|
|
173
|
+
customType: "freeflow-routing-budget",
|
|
174
|
+
display: false,
|
|
175
|
+
content: "Routing budget warning cleared for the current request.",
|
|
176
|
+
timestamp: 0,
|
|
177
|
+
};
|
|
178
|
+
const signature = fingerprint(message);
|
|
179
|
+
if (states[message.customType] !== signature) {
|
|
180
|
+
states[message.customType] = signature;
|
|
181
|
+
additions.push({ at: base.length, message });
|
|
182
|
+
result.push(message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!prior || prior.length !== base.length || additions.length) {
|
|
187
|
+
const frame = {
|
|
188
|
+
version: 1,
|
|
189
|
+
id: randomUUID(),
|
|
190
|
+
basis: leaf,
|
|
191
|
+
view,
|
|
192
|
+
generation,
|
|
193
|
+
length: base.length,
|
|
194
|
+
prefix: hashes.at(-1),
|
|
195
|
+
parent: prior?.id ?? null,
|
|
196
|
+
states,
|
|
197
|
+
retained: [...seenRetained],
|
|
198
|
+
additions,
|
|
199
|
+
};
|
|
200
|
+
try {
|
|
201
|
+
this.pi.appendEntry(ENTRY, frame);
|
|
202
|
+
const entry = reader.getBranch().find((e) => e.customType === ENTRY && e.data?.id === frame.id);
|
|
203
|
+
if (!entry || entry.parentId !== leaf || JSON.stringify(entry.data) !== JSON.stringify(frame))
|
|
204
|
+
throw new Error("Context append not acknowledged");
|
|
205
|
+
this.acknowledged.set(entry.id, hash(entry));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
this.fault = true;
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
ctx.ui?.setStatus?.("freeflow-cache", undefined);
|
|
213
|
+
} catch {}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -50,6 +50,12 @@ export const FREEFLOW_MODEL_SKILL_NAMES = [
|
|
|
50
50
|
"write-skill",
|
|
51
51
|
"write-spec",
|
|
52
52
|
];
|
|
53
|
+
export const STABLE_FREEFLOW_SURFACE = Object.freeze({
|
|
54
|
+
enabled: true,
|
|
55
|
+
cognitiveRouting: { effective: true },
|
|
56
|
+
contextVirtualization: { effective: true },
|
|
57
|
+
conversationHistory: { effective: true },
|
|
58
|
+
});
|
|
53
59
|
export const FREEFLOW_CAPABILITY_SKILL_NAMES = ["cognitive-routing", "context-virtualization", "conversation-history"];
|
|
54
60
|
export function freeflowSkillPath(skillName) {
|
|
55
61
|
return fileURLToPath(new URL(`../../../skills/${skillName}/SKILL.md`, import.meta.url));
|
|
@@ -498,11 +504,7 @@ export function setFreeflowStatus(
|
|
|
498
504
|
active.push(`cognitive blocked · ${cognitiveRoutingRuntime?.runtimeReason ?? "runtime_blocked"}`);
|
|
499
505
|
} else if (cognitiveRoutingActive) {
|
|
500
506
|
const profile = cognitiveRoutingRuntime.activeProfile;
|
|
501
|
-
const control =
|
|
502
|
-
cognitiveRoutingRuntime.controlMode === "manual-executor" ||
|
|
503
|
-
cognitiveRoutingRuntime.controlMode === "manual-coordinator"
|
|
504
|
-
? "manual hold"
|
|
505
|
-
: "automatic";
|
|
507
|
+
const control = String(cognitiveRoutingRuntime.controlMode).startsWith("manual-") ? "manual hold" : "automatic";
|
|
506
508
|
active.push(`${profile} · ${control}`);
|
|
507
509
|
} else if (cognitiveRouting?.enabled === true) {
|
|
508
510
|
if (cognitiveRouting.blockingReason?.code === "runtime_disabled") {
|
|
@@ -537,12 +539,12 @@ export function skillPrompt(skill, args) {
|
|
|
537
539
|
}
|
|
538
540
|
function publicCognitiveRoutingControl(controlMode) {
|
|
539
541
|
if (controlMode === "automatic") return "automatic";
|
|
540
|
-
if (
|
|
542
|
+
if (["manual-helper", "manual-executor", "manual-coordinator"].includes(controlMode)) return "manual";
|
|
541
543
|
return "unavailable";
|
|
542
544
|
}
|
|
543
545
|
function publicCognitiveRoutingProfile(activeProfile, effective) {
|
|
544
546
|
if (effective !== true) return "unavailable";
|
|
545
|
-
return
|
|
547
|
+
return ["helper", "executor", "coordinator"].includes(activeProfile) ? activeProfile : "unavailable";
|
|
546
548
|
}
|
|
547
549
|
function publicCapabilityStatus(capability) {
|
|
548
550
|
if (capability?.effective === true) return "active";
|
|
@@ -558,9 +560,7 @@ function publicCognitiveRoutingStatus(capability, runtime) {
|
|
|
558
560
|
function publicCognitiveRoutingProjectionMode(capability, runtime, projectionFailure) {
|
|
559
561
|
if (capability?.projection !== true) return "disabled";
|
|
560
562
|
if (capability?.effective !== true) return "unavailable";
|
|
561
|
-
if (runtime?.controlMode
|
|
562
|
-
return "manual-bypass";
|
|
563
|
-
}
|
|
563
|
+
if (String(runtime?.controlMode).startsWith("manual-")) return "manual-bypass";
|
|
564
564
|
if (projectionFailure) return "blocked";
|
|
565
565
|
if (runtime?.runtimeStatus === "inactive" || runtime?.runtimeStatus === "blocked") return "unavailable";
|
|
566
566
|
if (runtime?.effective === true && runtime.controlMode === "automatic") return "enabled";
|
|
@@ -612,6 +612,7 @@ export function freeflowRuntimeStateMessage(
|
|
|
612
612
|
"Cognitive Routing:",
|
|
613
613
|
`- Control: \`${control}\``,
|
|
614
614
|
`- Profile: \`${profile}\``,
|
|
615
|
+
`- Delegation: \`${capabilityState?.cognitiveRouting?.delegation ?? "executor"}\``,
|
|
615
616
|
`- Projection: \`${projectionMode}\``,
|
|
616
617
|
].join("\n"),
|
|
617
618
|
display: false,
|
|
@@ -664,6 +665,24 @@ export function withFreeflowRuntimeState(
|
|
|
664
665
|
if (unchanged) return source;
|
|
665
666
|
return insertRuntimeStateBeforeLatestUser(withoutRuntimeState, runtimeState);
|
|
666
667
|
}
|
|
668
|
+
export function stableRuntimeContext(context) {
|
|
669
|
+
const sections = [
|
|
670
|
+
["Freeflow core", context?.corePrompt],
|
|
671
|
+
["Freeflow core", context?.interactionContractPrompt],
|
|
672
|
+
["Cognitive Routing", context?.cognitiveRoutingPrompt],
|
|
673
|
+
["Context Virtualization", context?.contextVirtualizationPrompt],
|
|
674
|
+
["Conversation History", context?.conversationHistoryPrompt],
|
|
675
|
+
];
|
|
676
|
+
return [
|
|
677
|
+
"# Freeflow availability contract",
|
|
678
|
+
"Freeflow keeps a fixed reference catalog of its instructions, skills, and tools to preserve prompt caching. Presence in this catalog does not mean a feature is active or an operation is permitted.",
|
|
679
|
+
"Apply the following guidance and Freeflow skills only when the latest extension-generated Freeflow Runtime State marks the corresponding feature active. When Freeflow is inactive, unavailable, or not configured, its core and capability guidance is dormant; do not bootstrap or follow it merely because it is listed. Explicit user instructions retain their normal authority.",
|
|
680
|
+
"For Cognitive Routing, follow the latest control/profile/responsibility snapshots on this branch. Earlier snapshots and notices are historical; they grant no current permission. Tool permissions are checked by the runtime, even though all definitions remain visible. Never call a disabled operation.",
|
|
681
|
+
...sections
|
|
682
|
+
.filter(([, text]) => isPromptAvailable(text))
|
|
683
|
+
.map(([feature, text]) => `## Reference guidance: ${feature}\n\n${text.trim()}`),
|
|
684
|
+
].join("\n\n");
|
|
685
|
+
}
|
|
667
686
|
export function runtimeContext(freeflowContext, capabilityState) {
|
|
668
687
|
if (
|
|
669
688
|
capabilityState?.configured !== true ||
|
|
@@ -91,6 +91,28 @@ export class EventStore {
|
|
|
91
91
|
this.ready = true;
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
|
+
// Pi defers creating a new session file until its first assistant message.
|
|
95
|
+
// Before that flush boundary, the complete in-memory native session is the
|
|
96
|
+
// only available source; uncertain appends still require persisted readback.
|
|
97
|
+
const preFlush =
|
|
98
|
+
!this.attempted.size &&
|
|
99
|
+
!this.reader.getEntries().some((entry) => entry.type === "message" && entry.message?.role === "assistant");
|
|
100
|
+
if (preFlush) {
|
|
101
|
+
replay(branch);
|
|
102
|
+
const known = new Map();
|
|
103
|
+
for (const entry of branch) {
|
|
104
|
+
if (entry.type === "custom" && entry.customType === ROUTING_ENTRY) {
|
|
105
|
+
const event = parseRoutingEvent(entry.data);
|
|
106
|
+
known.set(eventKey(event), { value: eventValue(event), eventId: event.eventId, entryId: entry.id });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.observed = known;
|
|
110
|
+
this.fault = undefined;
|
|
111
|
+
this.ready = true;
|
|
112
|
+
this.cachedEntries = [];
|
|
113
|
+
this.cachedState = replay([]);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
94
116
|
const path = this.reader.getSessionFile();
|
|
95
117
|
check(path, "persisted_snapshot_unavailable", "Existing or uncertain routing state needs a persisted snapshot.");
|
|
96
118
|
const snapshot = await readOnlySessionSnapshot(path);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isWorkerProfile } from "../cognitive-routing-v2/types.js";
|
|
2
|
+
import { isTaskEvidence, textRef } from "./sources.js";
|
|
3
|
+
export function annotateSources(messages, renderedSources, full, sources, instance) {
|
|
4
|
+
const annotated = [];
|
|
5
|
+
for (let i = 0; i < messages.length;) {
|
|
6
|
+
const group = [messages[i++]];
|
|
7
|
+
while (i < messages.length && messages[i].role === "toolResult") group.push(messages[i++]);
|
|
8
|
+
const rows = group.flatMap((message) => {
|
|
9
|
+
const s = renderedSources.get(message);
|
|
10
|
+
if (!s) return [];
|
|
11
|
+
const representation = full.has(s.ref) ? "full" : "structural only; omitted result bodies are not evidence";
|
|
12
|
+
const selection =
|
|
13
|
+
isWorkerProfile(s.producer) && isTaskEvidence(s) && full.has(s.ref)
|
|
14
|
+
? "task evidence; selection checks apply"
|
|
15
|
+
: "not offered for new evidence selection";
|
|
16
|
+
return [
|
|
17
|
+
`${s.ref} | producer: ${s.producer === "common" ? "unknown/common (no observed routing profile)" : s.producer} | ${s.original ? "assistant-text" : s.message.role}${s.message.toolName ? ` | ${s.message.toolName}` : ""}${s.assignmentId ? ` | assignment: ${s.assignmentId}` : ""} | ${representation} | ${selection}${!s.original && full.has(s.ref) && sources.byRef.has(textRef(s.ref)) ? ` | visible text: ${textRef(s.ref)}` : ""}`,
|
|
18
|
+
];
|
|
19
|
+
});
|
|
20
|
+
annotated.push(...group);
|
|
21
|
+
if (rows.length)
|
|
22
|
+
annotated.push({
|
|
23
|
+
role: "custom",
|
|
24
|
+
customType: "freeflow-routing-v2-refs",
|
|
25
|
+
display: false,
|
|
26
|
+
content: `Source provenance for the preceding message/exchange:\n${rows.join("\n")}`,
|
|
27
|
+
details: { routingInstance: instance },
|
|
28
|
+
timestamp: 0,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return annotated;
|
|
32
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { canonical, idFor, refFor } from "../cognitive-routing-v2/types.js";
|
|
3
|
+
import { canonical, idFor, isWorkerProfile, refFor } from "../cognitive-routing-v2/types.js";
|
|
4
4
|
export const bodyHash = (value) => createHash("sha256").update(canonical(value)).digest("hex");
|
|
5
5
|
export const textRef = (ref) => `${ref}#text`;
|
|
6
6
|
const routingTools = new Set(["freeflow_delegate", "freeflow_return", "freeflow_project", "freeflow_unit"]);
|
|
@@ -16,6 +16,25 @@ export function isTaskEvidence(source) {
|
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
const callKey = (id, name) => JSON.stringify([id, name]);
|
|
19
|
+
const locatorLimit = 160;
|
|
20
|
+
function normalizeLabel(value) {
|
|
21
|
+
if (typeof value !== "string") return undefined;
|
|
22
|
+
const clean = value
|
|
23
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
24
|
+
.replace(/\s+/g, " ")
|
|
25
|
+
.trim();
|
|
26
|
+
if (!clean) return undefined;
|
|
27
|
+
if (clean.length <= locatorLimit) return { label: clean, truncated: false };
|
|
28
|
+
return { label: `${clean.slice(0, locatorLimit - 3)}...`, truncated: true };
|
|
29
|
+
}
|
|
30
|
+
function locatorForCall(assistant, call, locatorProvider) {
|
|
31
|
+
if (typeof assistant.ref !== "string" || typeof call.id !== "string" || typeof call.name !== "string")
|
|
32
|
+
return undefined;
|
|
33
|
+
const locator = { callRef: assistant.ref, toolCallId: call.id };
|
|
34
|
+
const details = locatorProvider?.(assistant, call);
|
|
35
|
+
const label = normalizeLabel(details?.label);
|
|
36
|
+
return label ? { ...locator, ...label, truncated: details?.truncated === true || label.truncated } : locator;
|
|
37
|
+
}
|
|
19
38
|
export class Sources {
|
|
20
39
|
byRef = new Map();
|
|
21
40
|
ambiguous = new Set();
|
|
@@ -26,7 +45,9 @@ export class Sources {
|
|
|
26
45
|
associated;
|
|
27
46
|
entries = [];
|
|
28
47
|
activeIds;
|
|
29
|
-
|
|
48
|
+
locatorProvider;
|
|
49
|
+
constructor(entries, state, activeIds, locatorProvider) {
|
|
50
|
+
this.locatorProvider = locatorProvider;
|
|
30
51
|
this.refresh(entries, state, activeIds);
|
|
31
52
|
}
|
|
32
53
|
refresh(entries, state, activeIds) {
|
|
@@ -59,7 +80,11 @@ export class Sources {
|
|
|
59
80
|
if (block.type === "text") {
|
|
60
81
|
try {
|
|
61
82
|
const receipt = JSON.parse(block.text);
|
|
62
|
-
if (
|
|
83
|
+
if (
|
|
84
|
+
((receipt.reportSaved && typeof receipt.report === "string") ||
|
|
85
|
+
(receipt.supplementSaved && typeof receipt.supplement === "string")) &&
|
|
86
|
+
typeof receipt.handoff === "string"
|
|
87
|
+
)
|
|
63
88
|
source.reportHandoff = receipt.handoff;
|
|
64
89
|
} catch {}
|
|
65
90
|
}
|
|
@@ -123,7 +148,15 @@ export class Sources {
|
|
|
123
148
|
this.ambiguous.clear();
|
|
124
149
|
for (const source of this.byRef.values()) source.active = false;
|
|
125
150
|
const used = new Set(),
|
|
126
|
-
hashes = messages.map(
|
|
151
|
+
hashes = messages.map((message) => {
|
|
152
|
+
const direct = bodyHash(message);
|
|
153
|
+
if (this.byBody.has(direct) || message?.role !== "toolResult" || !Array.isArray(message.content)) return direct;
|
|
154
|
+
const last = message.content.at(-1);
|
|
155
|
+
if (last?.type !== "text") return direct;
|
|
156
|
+
const original = bodyHash({ ...message, content: message.content.slice(0, -1) });
|
|
157
|
+
// Freeflow's stable reference decoration does not change the native evidence body.
|
|
158
|
+
return this.byBody.get(original)?.some((s) => last.text === `[context-ref: ${s.ref}]`) ? original : direct;
|
|
159
|
+
}),
|
|
127
160
|
remaining = new Map();
|
|
128
161
|
for (const hash of hashes) remaining.set(hash, (remaining.get(hash) ?? 0) + 1);
|
|
129
162
|
const items = messages.map((message, index) => {
|
|
@@ -162,13 +195,13 @@ export class Sources {
|
|
|
162
195
|
code: "source_changed",
|
|
163
196
|
detail: "The captured source changed; reconcile evidence before delivery.",
|
|
164
197
|
};
|
|
165
|
-
if (source.producer
|
|
166
|
-
return { ref, code: "source_origin", detail: "Selection requires observed
|
|
198
|
+
if (!isWorkerProfile(source.producer))
|
|
199
|
+
return { ref, code: "source_origin", detail: "Selection requires observed worker attribution." };
|
|
167
200
|
if (state.exposure.get(original.ref) !== original.hash && state.exposure.get(source.ref) !== source.hash)
|
|
168
201
|
return {
|
|
169
202
|
ref,
|
|
170
203
|
code: "source_unexposed",
|
|
171
|
-
detail: "The captured body has not been fully exposed to
|
|
204
|
+
detail: "The captured body has not been fully exposed to a worker on this ancestry.",
|
|
172
205
|
};
|
|
173
206
|
return undefined;
|
|
174
207
|
}
|
|
@@ -222,6 +255,17 @@ export class Sources {
|
|
|
222
255
|
}
|
|
223
256
|
return { sources, problems };
|
|
224
257
|
}
|
|
258
|
+
locator(ref) {
|
|
259
|
+
const source = this.byRef.get(ref);
|
|
260
|
+
if (!source || source.original || source.message.role !== "toolResult") return undefined;
|
|
261
|
+
const group = this.owners.get(source.ref);
|
|
262
|
+
if (!group) return undefined;
|
|
263
|
+
const calls = group.assistant.message.content?.filter((b) => b.type === "toolCall") ?? [];
|
|
264
|
+
const matches = calls.filter(
|
|
265
|
+
(call) => call.id === source.message.toolCallId && call.name === source.message.toolName,
|
|
266
|
+
);
|
|
267
|
+
return matches.length === 1 ? locatorForCall(group.assistant, matches[0], this.locatorProvider) : undefined;
|
|
268
|
+
}
|
|
225
269
|
ordered(sources) {
|
|
226
270
|
const ids = new Set([...sources].map((s) => s.ref));
|
|
227
271
|
return this.entries.flatMap((entry) =>
|