@shanepadgett/tau-agent 0.28.1 → 0.29.0
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/docs/subagents.md +1 -1
- package/extensions/cache-diagnostics/index.ts +1 -1
- package/extensions/explore/README.md +1 -1
- package/extensions/explore/ast/read/hook.ts +2 -34
- package/extensions/explore/ast/read/policy.ts +0 -32
- package/extensions/explore/index.ts +2 -0
- package/extensions/explore/outline-injection.ts +151 -0
- package/extensions/explore/settings.ts +0 -8
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/runtime-context/index.ts +3 -66
- package/extensions/subagent/agents/review.md +25 -18
- package/extensions/subagent/agents/scout.md +10 -0
- package/extensions/tau-help/help.md +2 -2
- package/extensions/working-memory/README.md +9 -0
- package/extensions/working-memory/checkpoint.ts +175 -0
- package/extensions/working-memory/index.ts +320 -0
- package/extensions/working-memory/memory.ts +248 -0
- package/extensions/working-memory/render.ts +178 -0
- package/extensions/working-memory/settings.ts +38 -0
- package/extensions/working-memory/state.ts +152 -0
- package/package.json +2 -2
- package/schemas/tau.schema.json +33 -39
- package/shared/events.ts +5 -0
- package/shared/full-file-knowledge.ts +0 -1
- package/shared/outline-injection.ts +56 -0
- package/extensions/context-pruning/README.md +0 -39
- package/extensions/context-pruning/index.ts +0 -382
- package/extensions/context-pruning/projection.ts +0 -60
- package/extensions/context-pruning/prune.ts +0 -199
- package/extensions/context-pruning/render.ts +0 -251
- package/extensions/context-pruning/settings.ts +0 -39
- package/shared/context-pruning-state.ts +0 -152
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { type ExtensionAPI, type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type Static, Type } from "typebox";
|
|
4
|
+
import { truncateBoundedHead } from "../../shared/bounded-text-result.ts";
|
|
5
|
+
import { requestOutlineInjections, type PreparedOutlineInjection } from "../../shared/outline-injection.ts";
|
|
6
|
+
import { buildMemoryCatalog } from "./memory.ts";
|
|
7
|
+
import { WORKING_MEMORY_TOOL, type DeferredFile, type WorkingMemoryCheckpointDetailsV1 } from "./state.ts";
|
|
8
|
+
|
|
9
|
+
const PATH = Type.String({ minLength: 1, maxLength: 500, pattern: "\\S" });
|
|
10
|
+
|
|
11
|
+
export const workingMemoryParameters = Type.Object(
|
|
12
|
+
{
|
|
13
|
+
continuation: Type.String({ minLength: 1, maxLength: 8_000, pattern: "\\S" }),
|
|
14
|
+
keep: Type.Array(Type.String({ minLength: 3, maxLength: 100 }), { maxItems: 100 }),
|
|
15
|
+
outlineFiles: Type.Array(PATH, { maxItems: 12 }),
|
|
16
|
+
deferFiles: Type.Array(
|
|
17
|
+
Type.Object(
|
|
18
|
+
{
|
|
19
|
+
path: PATH,
|
|
20
|
+
reason: Type.String({ minLength: 1, maxLength: 300, pattern: "\\S" }),
|
|
21
|
+
relevantWhen: Type.String({ minLength: 1, maxLength: 300, pattern: "\\S" }),
|
|
22
|
+
},
|
|
23
|
+
{ additionalProperties: false },
|
|
24
|
+
),
|
|
25
|
+
{ maxItems: 8 },
|
|
26
|
+
),
|
|
27
|
+
},
|
|
28
|
+
{ additionalProperties: false },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export type WorkingMemoryInput = Static<typeof workingMemoryParameters>;
|
|
32
|
+
|
|
33
|
+
interface ExecuteWorkingMemoryOptions {
|
|
34
|
+
pi: Pick<ExtensionAPI, "events">;
|
|
35
|
+
toolCallId: string;
|
|
36
|
+
params: WorkingMemoryInput;
|
|
37
|
+
signal: AbortSignal | undefined;
|
|
38
|
+
ctx: ExtensionContext;
|
|
39
|
+
generation: number;
|
|
40
|
+
currentGeneration(): number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WorkingMemoryExecution {
|
|
44
|
+
result: {
|
|
45
|
+
content: Array<{ type: "text"; text: string }>;
|
|
46
|
+
details: WorkingMemoryCheckpointDetailsV1;
|
|
47
|
+
};
|
|
48
|
+
outlines: PreparedOutlineInjection[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function executeWorkingMemory(options: ExecuteWorkingMemoryOptions): Promise<WorkingMemoryExecution> {
|
|
52
|
+
assertCurrent(options);
|
|
53
|
+
const branch = options.ctx.sessionManager.getBranch();
|
|
54
|
+
const catalog = buildMemoryCatalog(branch);
|
|
55
|
+
const requestedRefs = [...new Set(options.params.keep)];
|
|
56
|
+
const retained = requestedRefs
|
|
57
|
+
.flatMap((ref) => {
|
|
58
|
+
const unit = catalog.get(ref);
|
|
59
|
+
return unit ? [unit] : [];
|
|
60
|
+
})
|
|
61
|
+
.sort((left, right) => left.order - right.order || left.suborder - right.suborder);
|
|
62
|
+
const retainedRefs = retained.map((unit) => unit.ref);
|
|
63
|
+
const warnings = requestedRefs
|
|
64
|
+
.filter((ref) => !catalog.has(ref))
|
|
65
|
+
.map((ref) => `${ref}: memory reference is unavailable and was not retained`);
|
|
66
|
+
|
|
67
|
+
const outlinePaths = dedupePaths(options.params.outlineFiles, options.ctx.cwd);
|
|
68
|
+
const outlineResponse = await requestOutlineInjections(options.pi, {
|
|
69
|
+
cwd: options.ctx.cwd,
|
|
70
|
+
batchId: options.toolCallId,
|
|
71
|
+
paths: outlinePaths,
|
|
72
|
+
signal: options.signal,
|
|
73
|
+
isLifecycleCurrent: () => options.generation === options.currentGeneration(),
|
|
74
|
+
});
|
|
75
|
+
warnings.push(...outlineResponse.warnings.map((warning) => boundedWarning(warning)));
|
|
76
|
+
assertCurrent(options);
|
|
77
|
+
|
|
78
|
+
const outlinedKeys = new Set(
|
|
79
|
+
outlineResponse.messages.map((message) => resolve(options.ctx.cwd, message.details.path)),
|
|
80
|
+
);
|
|
81
|
+
const deferredFiles: DeferredFile[] = [];
|
|
82
|
+
const deferredKeys = new Set<string>();
|
|
83
|
+
for (const file of options.params.deferFiles) {
|
|
84
|
+
const path = normalizePath(file.path);
|
|
85
|
+
const key = resolve(options.ctx.cwd, path);
|
|
86
|
+
if (outlinedKeys.has(key) || deferredKeys.has(key)) continue;
|
|
87
|
+
deferredKeys.add(key);
|
|
88
|
+
deferredFiles.push({ path, reason: file.reason.trim(), relevantWhen: file.relevantWhen.trim() });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const anchorIndex = findAnchorEntry(branch, options.toolCallId);
|
|
92
|
+
const retainedSet = new Set(retainedRefs);
|
|
93
|
+
const preAnchorUnits = [...catalog.values()].filter((unit) => unit.order < anchorIndex);
|
|
94
|
+
const prunedRowIds = [...new Set(preAnchorUnits.flatMap((unit) => (retainedSet.has(unit.ref) ? [] : unit.rowIds)))];
|
|
95
|
+
const details: WorkingMemoryCheckpointDetailsV1 = {
|
|
96
|
+
v: 1,
|
|
97
|
+
anchorToolCallId: options.toolCallId,
|
|
98
|
+
retainedRefs,
|
|
99
|
+
retainedLabels: retained.map((unit) => ({ ref: unit.ref, label: unit.label, preview: unit.preview })),
|
|
100
|
+
prunedRowIds,
|
|
101
|
+
outlinedFiles: outlineResponse.messages.map((message) => ({
|
|
102
|
+
path: message.details.path,
|
|
103
|
+
rowId: message.details.rowId,
|
|
104
|
+
})),
|
|
105
|
+
deferredFiles,
|
|
106
|
+
removedUnits: Math.max(0, preAnchorUnits.length - retained.length),
|
|
107
|
+
warnings,
|
|
108
|
+
};
|
|
109
|
+
const markdown = formatResult(options.params.continuation.trim(), deferredFiles, warnings);
|
|
110
|
+
return {
|
|
111
|
+
result: { content: [{ type: "text", text: truncateBoundedHead(markdown).content }], details },
|
|
112
|
+
outlines: outlineResponse.messages,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatResult(continuation: string, deferred: readonly DeferredFile[], warnings: readonly string[]): string {
|
|
117
|
+
const sections = [`## Continue\n\n${continuation}`];
|
|
118
|
+
if (deferred.length > 0) {
|
|
119
|
+
sections.push(
|
|
120
|
+
`## Deferred files\n\n${deferred
|
|
121
|
+
.map((file) => `- \`${escapeCode(file.path)}\` — ${file.reason} Reconsider when: ${file.relevantWhen}.`)
|
|
122
|
+
.join("\n")}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (warnings.length > 0) sections.push(`## Warnings\n\n${warnings.map((warning) => `- ${warning}`).join("\n")}`);
|
|
126
|
+
return sections.join("\n\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function dedupePaths(paths: readonly string[], cwd: string): string[] {
|
|
130
|
+
const keys = new Set<string>();
|
|
131
|
+
const result: string[] = [];
|
|
132
|
+
for (const raw of paths) {
|
|
133
|
+
const path = normalizePath(raw);
|
|
134
|
+
const key = resolve(cwd, path);
|
|
135
|
+
if (keys.has(key)) continue;
|
|
136
|
+
keys.add(key);
|
|
137
|
+
result.push(path);
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function findAnchorEntry(branch: readonly SessionEntry[], toolCallId: string): number {
|
|
143
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
144
|
+
const entry = branch[index];
|
|
145
|
+
if (
|
|
146
|
+
entry?.type === "message" &&
|
|
147
|
+
entry.message.role === "assistant" &&
|
|
148
|
+
entry.message.content.some(
|
|
149
|
+
(block) => block.type === "toolCall" && block.id === toolCallId && block.name === WORKING_MEMORY_TOOL,
|
|
150
|
+
)
|
|
151
|
+
) {
|
|
152
|
+
return index;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return -1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizePath(path: string): string {
|
|
159
|
+
return path.trim().replace(/^@/, "");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function escapeCode(path: string): string {
|
|
163
|
+
return path.replaceAll("`", "\\`");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function boundedWarning(warning: string): string {
|
|
167
|
+
return warning.length <= 500 ? warning : `${warning.slice(0, 499)}…`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function assertCurrent(options: ExecuteWorkingMemoryOptions): void {
|
|
171
|
+
options.signal?.throwIfAborted();
|
|
172
|
+
if (options.generation !== options.currentGeneration()) {
|
|
173
|
+
throw new Error("Working-memory checkpoint crossed a session lifecycle boundary");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineTool,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
type SessionEntry,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { emitTauEvent, onTauEvent } from "../../shared/events.ts";
|
|
8
|
+
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
9
|
+
import { createToolRowStateStore } from "../../shared/tool-row-state.ts";
|
|
10
|
+
import { executeWorkingMemory, workingMemoryParameters } from "./checkpoint.ts";
|
|
11
|
+
import { projectWorkingMemory } from "./memory.ts";
|
|
12
|
+
import {
|
|
13
|
+
parseWorkingMemoryNudge,
|
|
14
|
+
renderWorkingMemoryCall,
|
|
15
|
+
renderWorkingMemoryNudge,
|
|
16
|
+
renderWorkingMemoryResult,
|
|
17
|
+
type WorkingMemoryNudgeDetails,
|
|
18
|
+
} from "./render.ts";
|
|
19
|
+
import workingMemorySettings from "./settings.ts";
|
|
20
|
+
import { replayWorkingMemoryState, WORKING_MEMORY_TOOL, type WorkingMemoryCheckpointDetailsV1 } from "./state.ts";
|
|
21
|
+
|
|
22
|
+
const NUDGE_TYPE = "tau.working-memory.nudge";
|
|
23
|
+
const BASELINE_TYPE = "tau.working-memory.nudge-baseline";
|
|
24
|
+
const TOOL_DESCRIPTION =
|
|
25
|
+
"Create a selective hard checkpoint for future model context. Keep valuable referenced evidence and complete tool exchanges, carry file structure as outlines, defer conditionally relevant files without reading them, and provide one compact continuation note. Never checkpoint useful evidence merely to reread it.";
|
|
26
|
+
|
|
27
|
+
interface NudgeState {
|
|
28
|
+
anchorToolCallId: string | undefined;
|
|
29
|
+
suppressedThroughTokens: number | undefined;
|
|
30
|
+
highestBoundaryTokens: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function workingMemoryExtension(pi: ExtensionAPI): void {
|
|
34
|
+
let enabled = false;
|
|
35
|
+
let generation = 0;
|
|
36
|
+
let interval = workingMemorySettings.defaults.nudgeEveryTokens;
|
|
37
|
+
let instructions = workingMemorySettings.defaults.nudgeInstructions;
|
|
38
|
+
let toolRegistered = false;
|
|
39
|
+
let commandRegistered = false;
|
|
40
|
+
let visualRows = new Set<string>();
|
|
41
|
+
let nudgeState: NudgeState = { anchorToolCallId: undefined, suppressedThroughTokens: 0, highestBoundaryTokens: 0 };
|
|
42
|
+
const rowState = createToolRowStateStore(pi, "working-memory.tool-row-state");
|
|
43
|
+
|
|
44
|
+
pi.registerMessageRenderer<WorkingMemoryNudgeDetails>(NUDGE_TYPE, (message, _options, theme) =>
|
|
45
|
+
renderWorkingMemoryNudge(message.details, theme),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const pushVisualSnapshot = () => {
|
|
49
|
+
emitTauEvent(pi, "tau:tool-row-state.snapshot", {
|
|
50
|
+
states: [...visualRows].map((rowId) => ({ rowId, state: "pruned" as const })),
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
onTauEvent(
|
|
54
|
+
pi,
|
|
55
|
+
"working-memory.tool-row-state-producer",
|
|
56
|
+
"tau:tool-row-state.snapshot.requested",
|
|
57
|
+
pushVisualSnapshot,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const invalidate = () => {
|
|
61
|
+
generation += 1;
|
|
62
|
+
};
|
|
63
|
+
const setToolActive = (active: boolean) => {
|
|
64
|
+
if (!toolRegistered) return;
|
|
65
|
+
const activeTools = pi.getActiveTools();
|
|
66
|
+
const currentlyActive = activeTools.includes(WORKING_MEMORY_TOOL);
|
|
67
|
+
if (active === currentlyActive) return;
|
|
68
|
+
pi.setActiveTools(
|
|
69
|
+
active ? [...activeTools, WORKING_MEMORY_TOOL] : activeTools.filter((name) => name !== WORKING_MEMORY_TOOL),
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
const syncBranch = (ctx: ExtensionContext) => {
|
|
73
|
+
const branch = ctx.sessionManager.getBranch();
|
|
74
|
+
const state = replayWorkingMemoryState(branch, enabled);
|
|
75
|
+
visualRows = new Set(state.prunedRowIds);
|
|
76
|
+
nudgeState = reconstructNudgeState(branch, state.latestAnchorToolCallId);
|
|
77
|
+
pushVisualSnapshot();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
81
|
+
invalidate();
|
|
82
|
+
enabled = false;
|
|
83
|
+
setToolActive(false);
|
|
84
|
+
visualRows.clear();
|
|
85
|
+
pushVisualSnapshot();
|
|
86
|
+
const current = generation;
|
|
87
|
+
const settings = await loadTauExtensionSettings(ctx, workingMemorySettings);
|
|
88
|
+
if (current !== generation) return;
|
|
89
|
+
enabled = settings.enabled;
|
|
90
|
+
interval = settings.nudgeEveryTokens;
|
|
91
|
+
instructions = settings.nudgeInstructions;
|
|
92
|
+
if (enabled && !toolRegistered) {
|
|
93
|
+
pi.registerTool(
|
|
94
|
+
defineTool<typeof workingMemoryParameters, WorkingMemoryCheckpointDetailsV1>({
|
|
95
|
+
name: WORKING_MEMORY_TOOL,
|
|
96
|
+
label: WORKING_MEMORY_TOOL,
|
|
97
|
+
description: TOOL_DESCRIPTION,
|
|
98
|
+
promptSnippet: "Reassess and selectively checkpoint active working memory",
|
|
99
|
+
promptGuidelines: [
|
|
100
|
+
"Use working_memory when stale evidence has accumulated or a memory reminder asks for reassessment; continue coherent exploration when current evidence remains useful.",
|
|
101
|
+
"A hidden working-memory reference catalog provides keep refs. Message refs preserve conversational evidence; tool refs preserve one complete call/result exchange.",
|
|
102
|
+
"Keep active or expensive evidence, outline files when structure suffices, defer paths whose relevance is conditional, and discard evidence with no expected value.",
|
|
103
|
+
"Everything before working_memory leaves future model context unless selected in keep. Put durable decisions, constraints, unresolved matters, and next action in continuation without duplicating retained evidence.",
|
|
104
|
+
],
|
|
105
|
+
parameters: workingMemoryParameters,
|
|
106
|
+
executionMode: "sequential",
|
|
107
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
108
|
+
const execution = await executeWorkingMemory({
|
|
109
|
+
pi,
|
|
110
|
+
toolCallId,
|
|
111
|
+
params,
|
|
112
|
+
signal,
|
|
113
|
+
ctx,
|
|
114
|
+
generation,
|
|
115
|
+
currentGeneration: () => generation,
|
|
116
|
+
});
|
|
117
|
+
for (const outline of execution.outlines) pi.sendMessage(outline, { deliverAs: "steer" });
|
|
118
|
+
return execution.result;
|
|
119
|
+
},
|
|
120
|
+
renderCall(args, theme, context) {
|
|
121
|
+
return renderWorkingMemoryCall(args, theme, {
|
|
122
|
+
rowState,
|
|
123
|
+
rowId: context.toolCallId,
|
|
124
|
+
invalidate: context.invalidate,
|
|
125
|
+
lastComponent: context.lastComponent,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
renderResult(result, options, theme, context) {
|
|
129
|
+
return renderWorkingMemoryResult(result, options.expanded, theme, context.lastComponent);
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
toolRegistered = true;
|
|
134
|
+
}
|
|
135
|
+
setToolActive(enabled);
|
|
136
|
+
if (enabled && !commandRegistered) {
|
|
137
|
+
pi.registerCommand("prune", {
|
|
138
|
+
description: "Ask agent to reassess working memory and continue",
|
|
139
|
+
async handler(args, commandContext) {
|
|
140
|
+
if (!enabled) {
|
|
141
|
+
commandContext.ui.notify("Working memory is disabled.", "info");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (args.trim().length > 0) {
|
|
145
|
+
commandContext.ui.notify("Usage: /prune", "info");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const anchor = replayWorkingMemoryState(
|
|
149
|
+
commandContext.sessionManager.getBranch(),
|
|
150
|
+
true,
|
|
151
|
+
).latestAnchorToolCallId;
|
|
152
|
+
pi.sendMessage<WorkingMemoryNudgeDetails>(
|
|
153
|
+
{
|
|
154
|
+
customType: NUDGE_TYPE,
|
|
155
|
+
content: manualInstruction(),
|
|
156
|
+
display: true,
|
|
157
|
+
details: {
|
|
158
|
+
v: 1,
|
|
159
|
+
kind: "manual",
|
|
160
|
+
tokens: null,
|
|
161
|
+
boundaryTokens: null,
|
|
162
|
+
reminder: null,
|
|
163
|
+
tier: null,
|
|
164
|
+
tierCount: null,
|
|
165
|
+
anchorToolCallId: anchor ?? null,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
commandRegistered = true;
|
|
173
|
+
}
|
|
174
|
+
syncBranch(ctx);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
178
|
+
if (!enabled) return undefined;
|
|
179
|
+
const usage = ctx.getContextUsage();
|
|
180
|
+
if (!usage || usage.tokens === null || !Number.isFinite(usage.tokens)) return undefined;
|
|
181
|
+
const reminder = Math.floor(Math.max(0, usage.tokens) / interval);
|
|
182
|
+
if (reminder < 1) return undefined;
|
|
183
|
+
const instruction = instructions[Math.min(reminder, instructions.length) - 1] ?? instructions[0];
|
|
184
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${automaticInstruction(instruction)}` };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
pi.on("turn_end", (event, ctx) => {
|
|
188
|
+
if (!enabled || event.toolResults.length === 0) return undefined;
|
|
189
|
+
const usage = ctx.getContextUsage();
|
|
190
|
+
if (!usage || usage.tokens === null || !Number.isFinite(usage.tokens)) return undefined;
|
|
191
|
+
const tokens = Math.max(0, Math.floor(usage.tokens));
|
|
192
|
+
const branch = ctx.sessionManager.getBranch();
|
|
193
|
+
const anchor = replayWorkingMemoryState(branch, true).latestAnchorToolCallId;
|
|
194
|
+
if (anchor !== nudgeState.anchorToolCallId) nudgeState = reconstructNudgeState(branch, anchor);
|
|
195
|
+
if (anchor !== undefined && nudgeState.suppressedThroughTokens === undefined) {
|
|
196
|
+
const floor = Math.floor(tokens / interval) * interval;
|
|
197
|
+
pi.appendEntry(BASELINE_TYPE, { v: 1, anchorToolCallId: anchor, suppressedThroughTokens: floor });
|
|
198
|
+
nudgeState.suppressedThroughTokens = floor;
|
|
199
|
+
nudgeState.highestBoundaryTokens = floor;
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
const reminder = Math.floor(tokens / interval);
|
|
203
|
+
if (reminder < 1) return undefined;
|
|
204
|
+
const boundaryTokens = reminder * interval;
|
|
205
|
+
if (boundaryTokens <= nudgeState.highestBoundaryTokens) return undefined;
|
|
206
|
+
const tier = Math.min(reminder, instructions.length);
|
|
207
|
+
const instruction = instructions[tier - 1] ?? instructions[0];
|
|
208
|
+
pi.sendMessage<WorkingMemoryNudgeDetails>(
|
|
209
|
+
{
|
|
210
|
+
customType: NUDGE_TYPE,
|
|
211
|
+
content: automaticInstruction(instruction),
|
|
212
|
+
display: true,
|
|
213
|
+
details: {
|
|
214
|
+
v: 1,
|
|
215
|
+
kind: "automatic",
|
|
216
|
+
tokens,
|
|
217
|
+
boundaryTokens,
|
|
218
|
+
reminder,
|
|
219
|
+
tier,
|
|
220
|
+
tierCount: instructions.length,
|
|
221
|
+
anchorToolCallId: anchor ?? null,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{ deliverAs: "steer" },
|
|
225
|
+
);
|
|
226
|
+
nudgeState.highestBoundaryTokens = boundaryTokens;
|
|
227
|
+
return undefined;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
pi.on("context", (event, ctx) => {
|
|
231
|
+
if (!enabled) return undefined;
|
|
232
|
+
const branch = ctx.sessionManager.getBranch();
|
|
233
|
+
const state = replayWorkingMemoryState(branch, true);
|
|
234
|
+
const messages = projectWorkingMemory(event.messages, state, branch, ctx.sessionManager.buildContextEntries());
|
|
235
|
+
if (!setsEqual(visualRows, state.prunedRowIds)) {
|
|
236
|
+
visualRows = new Set(state.prunedRowIds);
|
|
237
|
+
pushVisualSnapshot();
|
|
238
|
+
}
|
|
239
|
+
return { messages };
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
243
|
+
invalidate();
|
|
244
|
+
syncBranch(ctx);
|
|
245
|
+
});
|
|
246
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
247
|
+
invalidate();
|
|
248
|
+
syncBranch(ctx);
|
|
249
|
+
});
|
|
250
|
+
pi.on("session_shutdown", () => {
|
|
251
|
+
invalidate();
|
|
252
|
+
enabled = false;
|
|
253
|
+
setToolActive(false);
|
|
254
|
+
visualRows.clear();
|
|
255
|
+
pushVisualSnapshot();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function reconstructNudgeState(branch: readonly SessionEntry[], anchor: string | undefined): NudgeState {
|
|
260
|
+
let suppressedThroughTokens = anchor === undefined ? 0 : undefined;
|
|
261
|
+
let highestBoundaryTokens = 0;
|
|
262
|
+
let anchorResultIndex = -1;
|
|
263
|
+
if (anchor !== undefined) {
|
|
264
|
+
anchorResultIndex = branch.findIndex(
|
|
265
|
+
(entry) =>
|
|
266
|
+
entry.type === "message" &&
|
|
267
|
+
entry.message.role === "toolResult" &&
|
|
268
|
+
entry.message.toolName === WORKING_MEMORY_TOOL &&
|
|
269
|
+
entry.message.toolCallId === anchor,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
for (let index = anchorResultIndex + 1; index < branch.length; index += 1) {
|
|
273
|
+
const entry = branch[index];
|
|
274
|
+
if (!entry) continue;
|
|
275
|
+
if (entry.type === "custom" && entry.customType === BASELINE_TYPE) {
|
|
276
|
+
const baseline = parseBaseline(entry.data);
|
|
277
|
+
if (baseline !== undefined && baseline.anchorToolCallId === anchor) {
|
|
278
|
+
suppressedThroughTokens = baseline.suppressedThroughTokens;
|
|
279
|
+
highestBoundaryTokens = Math.max(highestBoundaryTokens, baseline.suppressedThroughTokens);
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (entry.type !== "custom_message" || entry.customType !== NUDGE_TYPE) continue;
|
|
284
|
+
const details = parseWorkingMemoryNudge(entry.details);
|
|
285
|
+
if (details?.kind !== "automatic" || details.anchorToolCallId !== (anchor ?? null)) continue;
|
|
286
|
+
highestBoundaryTokens = Math.max(highestBoundaryTokens, details.boundaryTokens);
|
|
287
|
+
}
|
|
288
|
+
return { anchorToolCallId: anchor, suppressedThroughTokens, highestBoundaryTokens };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseBaseline(value: unknown): { anchorToolCallId: string; suppressedThroughTokens: number } | undefined {
|
|
292
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
293
|
+
const record = value as Record<string, unknown>;
|
|
294
|
+
if (
|
|
295
|
+
record.v !== 1 ||
|
|
296
|
+
typeof record.anchorToolCallId !== "string" ||
|
|
297
|
+
!Number.isSafeInteger(record.suppressedThroughTokens) ||
|
|
298
|
+
(record.suppressedThroughTokens as number) < 0
|
|
299
|
+
) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
anchorToolCallId: record.anchorToolCallId,
|
|
304
|
+
suppressedThroughTokens: record.suppressedThroughTokens as number,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function automaticInstruction(instruction: string): string {
|
|
309
|
+
return `Internal working-memory instruction. Follow silently without mentioning token counts or context management. ${instruction} Use working_memory only when a selective checkpoint improves future work; never prune useful evidence merely to reread it.`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function manualInstruction(): string {
|
|
313
|
+
return "Internal working-memory instruction. Follow silently. Reassess active evidence, use working_memory if a selective checkpoint improves future work, then continue unfinished work. Do not mention this request.";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function setsEqual(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
|
|
317
|
+
if (left.size !== right.size) return false;
|
|
318
|
+
for (const value of left) if (!right.has(value)) return false;
|
|
319
|
+
return true;
|
|
320
|
+
}
|