170 lines
6.0 KiB
JavaScript
170 lines
6.0 KiB
JavaScript
/*
|
|||
|
|
* Test-only transcription of the Pi 0.83.0 context algorithms:
|
||
|
|
* https://github.com/earendil-works/pi/blob/v0.83.0/packages/coding-agent/src/core/session-manager.ts
|
||
|
|
* https://github.com/earendil-works/pi/blob/v0.83.0/packages/agent/src/harness/session/session.ts
|
||
|
|
*
|
||
|
|
* MIT License
|
||
|
|
*
|
||
|
|
* Copyright (c) 2025 Mario Zechner
|
||
|
|
*
|
||
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
|
* of this software and associated documentation files (the "Software"), to deal
|
||
|
|
* in the Software without restriction, including without limitation the rights
|
||
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
|
* copies of the Software, and to permit persons to whom the Software is
|
||
|
|
* furnished to do so, subject to the following conditions:
|
||
|
|
*
|
||
|
|
* The above copyright notice and this permission notice shall be included in all
|
||
|
|
* copies or substantial portions of the Software.
|
||
|
|
*
|
||
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
|
|
* SOFTWARE.
|
||
|
|
*/
|
||
|
|
|
||
|
|
export const PI_CONTEXT_ORACLE_VERSION = '0.83.0';
|
||
|
|
|
||
|
|
function buildEntryIndex(entries, byId) {
|
||
|
|
if (byId) return byId;
|
||
|
|
const index = new Map();
|
||
|
|
for (const entry of entries) index.set(entry.id, entry);
|
||
|
|
return index;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildCodingAgentSessionPath(entries, leafId, byId) {
|
||
|
|
const index = buildEntryIndex(entries, byId);
|
||
|
|
let leaf;
|
||
|
|
if (leafId === null) return [];
|
||
|
|
if (leafId) leaf = index.get(leafId);
|
||
|
|
leaf ??= entries[entries.length - 1];
|
||
|
|
if (!leaf) return [];
|
||
|
|
|
||
|
|
const path = [];
|
||
|
|
let current = leaf;
|
||
|
|
while (current) {
|
||
|
|
path.push(current);
|
||
|
|
current = current.parentId ? index.get(current.parentId) : undefined;
|
||
|
|
}
|
||
|
|
path.reverse();
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
|
||
|
|
// pi-agent-core 0.83.0 JsonlSessionStorage.getPathToRootOrCompaction().
|
||
|
|
// The checked fixtures may contain synthetic orphan edges; those terminate the
|
||
|
|
// path just as the coding-agent oracle above does instead of exercising the
|
||
|
|
// storage API's separate invalid-session error.
|
||
|
|
export function buildAgentCoreSessionPath(entries, leafId, byId) {
|
||
|
|
const index = buildEntryIndex(entries, byId);
|
||
|
|
if (leafId === null) return [];
|
||
|
|
let current = leafId ? index.get(leafId) : entries[entries.length - 1];
|
||
|
|
current ??= entries[entries.length - 1];
|
||
|
|
if (!current) return [];
|
||
|
|
|
||
|
|
const path = [];
|
||
|
|
let stopAtEntryId = null;
|
||
|
|
while (current) {
|
||
|
|
path.unshift(current);
|
||
|
|
if (stopAtEntryId !== null && current.id === stopAtEntryId) break;
|
||
|
|
if (current.type === 'compaction') {
|
||
|
|
if (current.retainedTail) break;
|
||
|
|
stopAtEntryId = current.firstKeptEntryId ?? null;
|
||
|
|
}
|
||
|
|
if (!current.parentId) break;
|
||
|
|
current = index.get(current.parentId);
|
||
|
|
}
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildCodingAgentContextEntries(entries, leafId, byId) {
|
||
|
|
const path = buildCodingAgentSessionPath(entries, leafId, byId);
|
||
|
|
let compaction = null;
|
||
|
|
for (const entry of path) {
|
||
|
|
if (entry.type === 'compaction') compaction = entry;
|
||
|
|
}
|
||
|
|
if (!compaction) return path;
|
||
|
|
|
||
|
|
const compactionIndex = path.findIndex(entry => entry.id === compaction.id);
|
||
|
|
if (compactionIndex < 0) return path;
|
||
|
|
|
||
|
|
const contextEntries = [compaction];
|
||
|
|
let foundFirstKept = false;
|
||
|
|
for (let index = 0; index < compactionIndex; index++) {
|
||
|
|
const entry = path[index];
|
||
|
|
if (entry.id === compaction.firstKeptEntryId) foundFirstKept = true;
|
||
|
|
if (foundFirstKept) contextEntries.push(entry);
|
||
|
|
}
|
||
|
|
contextEntries.push(...path.slice(compactionIndex + 1));
|
||
|
|
return contextEntries;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function defaultAgentCoreContextEntryTransform(pathEntries) {
|
||
|
|
let compaction = null;
|
||
|
|
for (const entry of pathEntries) {
|
||
|
|
if (entry.type === 'compaction') compaction = entry;
|
||
|
|
}
|
||
|
|
if (!compaction) return [...pathEntries];
|
||
|
|
|
||
|
|
const entries = [compaction];
|
||
|
|
const compactionIndex = pathEntries.findIndex(entry => (
|
||
|
|
entry.type === 'compaction' && entry.id === compaction.id
|
||
|
|
));
|
||
|
|
if (compaction.retainedTail) {
|
||
|
|
for (let index = compactionIndex + 1; index < pathEntries.length; index++) {
|
||
|
|
entries.push(pathEntries[index]);
|
||
|
|
}
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
if (compaction.firstKeptEntryId) {
|
||
|
|
let foundFirstKept = false;
|
||
|
|
for (let index = 0; index < compactionIndex; index++) {
|
||
|
|
const entry = pathEntries[index];
|
||
|
|
if (entry.id === compaction.firstKeptEntryId) foundFirstKept = true;
|
||
|
|
if (foundFirstKept) entries.push(entry);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (let index = compactionIndex + 1; index < pathEntries.length; index++) {
|
||
|
|
entries.push(pathEntries[index]);
|
||
|
|
}
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function pi083LeafId(entries) {
|
||
|
|
let leafId = null;
|
||
|
|
for (const entry of entries) {
|
||
|
|
leafId = entry.type === 'leaf' ? entry.targetId : entry.id;
|
||
|
|
}
|
||
|
|
return leafId;
|
||
|
|
}
|
||
|
|
|
||
|
|
function evidence(kind, source, role, content) {
|
||
|
|
return JSON.stringify({ kind, source, role, content });
|
||
|
|
}
|
||
|
|
|
||
|
|
// The Pi oracles select the active context entries. Obelisk stores those entries
|
||
|
|
// in durable physical order for its evidence timeline, while retainedTail
|
||
|
|
// messages remain immediately after their owning compaction summary.
|
||
|
|
export function projectCanonicalEvidence(physicalEntries, contextEntries) {
|
||
|
|
const activeIds = new Set(contextEntries.map(entry => entry.id));
|
||
|
|
return physicalEntries.filter(entry => activeIds.has(entry.id)).flatMap((entry) => {
|
||
|
|
if (entry.type === 'message') {
|
||
|
|
return [evidence('message', null, entry.message.role, entry.message.content)];
|
||
|
|
}
|
||
|
|
if (entry.type === 'branch_summary') {
|
||
|
|
return [evidence('summary', 'pi:branch_summary', null, entry.summary)];
|
||
|
|
}
|
||
|
|
if (entry.type === 'compaction') {
|
||
|
|
return [
|
||
|
|
evidence('summary', 'pi:compaction', null, entry.summary),
|
||
|
|
...(entry.retainedTail ?? []).map(message => (
|
||
|
|
evidence('message', null, message.role, message.content)
|
||
|
|
)),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
return [];
|
||
|
|
});
|
||
|
|
}
|