@shanepadgett/tau-agent 0.28.0 → 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/ir.ts +6 -0
- package/extensions/explore/ast/languages/typescript.ts +37 -2
- package/extensions/explore/ast/read/hook.ts +2 -34
- package/extensions/explore/ast/read/policy.ts +0 -32
- package/extensions/explore/ast/slice.ts +15 -2
- 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,248 @@
|
|
|
1
|
+
import { sessionEntryToContextMessages, type ContextEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ActiveWorkingMemoryState } from "./state.ts";
|
|
3
|
+
|
|
4
|
+
type ContextMessage = ContextEvent["messages"][number];
|
|
5
|
+
|
|
6
|
+
export interface MemoryUnit {
|
|
7
|
+
ref: string;
|
|
8
|
+
label: string;
|
|
9
|
+
preview: string;
|
|
10
|
+
order: number;
|
|
11
|
+
suborder: number;
|
|
12
|
+
messages: ContextMessage[];
|
|
13
|
+
rowIds: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EXCLUDED_CUSTOM_TYPES = new Set(["tau.autoread", "tau.runtime-context", "tau.working-memory.nudge"]);
|
|
17
|
+
const REFERENCE_CATALOG_TYPE = "tau.working-memory.references";
|
|
18
|
+
|
|
19
|
+
export function buildMemoryCatalog(branch: readonly SessionEntry[]): Map<string, MemoryUnit> {
|
|
20
|
+
const catalog = new Map<string, MemoryUnit>();
|
|
21
|
+
const results = new Map<string, ContextMessage>();
|
|
22
|
+
for (const entry of branch) {
|
|
23
|
+
for (const message of sessionEntryToContextMessages(entry)) {
|
|
24
|
+
if (message.role === "toolResult") results.set(message.toolCallId, message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (let order = 0; order < branch.length; order += 1) {
|
|
29
|
+
const entry = branch[order];
|
|
30
|
+
if (!entry) continue;
|
|
31
|
+
const message = sessionEntryToContextMessages(entry)[0];
|
|
32
|
+
if (!message) continue;
|
|
33
|
+
if (message.role === "assistant") {
|
|
34
|
+
const calls = message.content.filter((block) => block.type === "toolCall");
|
|
35
|
+
const frameworkCall = calls.some((call) => call.name === "working_memory");
|
|
36
|
+
if (frameworkCall) continue;
|
|
37
|
+
const prose = message.content.filter((block) => block.type !== "toolCall");
|
|
38
|
+
if (prose.length > 0) {
|
|
39
|
+
const ref = `m:${entry.id}`;
|
|
40
|
+
catalog.set(ref, {
|
|
41
|
+
ref,
|
|
42
|
+
label: "assistant",
|
|
43
|
+
preview: previewAssistant(prose),
|
|
44
|
+
order,
|
|
45
|
+
suborder: 0,
|
|
46
|
+
messages: [{ ...message, content: prose }],
|
|
47
|
+
rowIds: [],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
for (let index = 0; index < calls.length; index += 1) {
|
|
51
|
+
const call = calls[index];
|
|
52
|
+
if (!call) continue;
|
|
53
|
+
const result = results.get(call.id);
|
|
54
|
+
if (result?.role !== "toolResult" || result.toolName !== call.name) continue;
|
|
55
|
+
const ref = `t:${entry.id}:${index + 1}`;
|
|
56
|
+
catalog.set(ref, {
|
|
57
|
+
ref,
|
|
58
|
+
label: `tool ${call.name}`,
|
|
59
|
+
preview: previewTool(call.arguments),
|
|
60
|
+
order,
|
|
61
|
+
suborder: index + 1,
|
|
62
|
+
messages: [{ ...message, content: [call] }, result],
|
|
63
|
+
rowIds: [call.id],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (message.role === "toolResult") continue;
|
|
69
|
+
if (message.role === "custom" && EXCLUDED_CUSTOM_TYPES.has(message.customType)) continue;
|
|
70
|
+
const ref = `m:${entry.id}`;
|
|
71
|
+
catalog.set(ref, {
|
|
72
|
+
ref,
|
|
73
|
+
label: message.role === "custom" ? message.customType : message.role,
|
|
74
|
+
preview: previewMessage(message),
|
|
75
|
+
order,
|
|
76
|
+
suborder: 0,
|
|
77
|
+
messages: [message],
|
|
78
|
+
rowIds: outlineRowIds(message),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return catalog;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function projectWorkingMemory(
|
|
85
|
+
messages: readonly ContextMessage[],
|
|
86
|
+
state: ActiveWorkingMemoryState,
|
|
87
|
+
branch: readonly SessionEntry[],
|
|
88
|
+
contextEntries: readonly SessionEntry[],
|
|
89
|
+
): ContextMessage[] {
|
|
90
|
+
const catalog = buildMemoryCatalog(branch);
|
|
91
|
+
const refsByMessage = referenceCurrentMessages(messages, contextEntries, catalog);
|
|
92
|
+
if (state.latestAnchorToolCallId === undefined) {
|
|
93
|
+
return addReferenceCatalog(messages, refsByMessage);
|
|
94
|
+
}
|
|
95
|
+
const anchorIndex = findAnchor(messages, state.latestAnchorToolCallId);
|
|
96
|
+
if (anchorIndex < 0) return addReferenceCatalog(messages, refsByMessage);
|
|
97
|
+
|
|
98
|
+
const retained = state.retainedRefs
|
|
99
|
+
.flatMap((ref) => {
|
|
100
|
+
const unit = catalog.get(ref);
|
|
101
|
+
return unit ? [unit] : [];
|
|
102
|
+
})
|
|
103
|
+
.sort((left, right) => left.order - right.order || left.suborder - right.suborder);
|
|
104
|
+
const projected: ContextMessage[] = [];
|
|
105
|
+
const projectedRefs: MemoryUnit[][] = [];
|
|
106
|
+
for (const unit of retained) {
|
|
107
|
+
projected.push(...unit.messages);
|
|
108
|
+
projectedRefs.push(...unit.messages.map((_, index) => (index === unit.messages.length - 1 ? [unit] : [])));
|
|
109
|
+
}
|
|
110
|
+
for (let index = anchorIndex; index < messages.length; index += 1) {
|
|
111
|
+
const message = messages[index];
|
|
112
|
+
if (!message) continue;
|
|
113
|
+
projected.push(index === anchorIndex ? sanitizeAnchor(message, state.latestAnchorToolCallId) : message);
|
|
114
|
+
projectedRefs.push(refsByMessage[index] ?? []);
|
|
115
|
+
}
|
|
116
|
+
return addReferenceCatalog(projected, projectedRefs);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function referenceCurrentMessages(
|
|
120
|
+
messages: readonly ContextMessage[],
|
|
121
|
+
entries: readonly SessionEntry[],
|
|
122
|
+
catalog: ReadonlyMap<string, MemoryUnit>,
|
|
123
|
+
): MemoryUnit[][] {
|
|
124
|
+
const generated = entries.flatMap((entry) =>
|
|
125
|
+
sessionEntryToContextMessages(entry).map((message) => ({ entry, message })),
|
|
126
|
+
);
|
|
127
|
+
if (generated.length !== messages.length) return messages.map(() => []);
|
|
128
|
+
const toolRefs = new Map<string, MemoryUnit[]>();
|
|
129
|
+
for (const unit of catalog.values()) {
|
|
130
|
+
const result = unit.messages.find((message) => message.role === "toolResult");
|
|
131
|
+
if (result?.role !== "toolResult") continue;
|
|
132
|
+
toolRefs.set(result.toolCallId, [...(toolRefs.get(result.toolCallId) ?? []), unit]);
|
|
133
|
+
}
|
|
134
|
+
return generated.map(({ entry, message }, index) => {
|
|
135
|
+
if (message.role === "assistant") {
|
|
136
|
+
const unit = catalog.get(`m:${entry.id}`);
|
|
137
|
+
return unit && messages[index]?.role === "assistant" ? [unit] : [];
|
|
138
|
+
}
|
|
139
|
+
if (message.role === "toolResult") return toolRefs.get(message.toolCallId) ?? [];
|
|
140
|
+
const unit = catalog.get(`m:${entry.id}`);
|
|
141
|
+
return unit ? [unit] : [];
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function addReferenceCatalog(
|
|
146
|
+
messages: readonly ContextMessage[],
|
|
147
|
+
unitsByMessage: readonly MemoryUnit[][],
|
|
148
|
+
): ContextMessage[] {
|
|
149
|
+
const projected: ContextMessage[] = [
|
|
150
|
+
{
|
|
151
|
+
role: "custom",
|
|
152
|
+
customType: REFERENCE_CATALOG_TYPE,
|
|
153
|
+
content:
|
|
154
|
+
"Working-memory references follow their source messages. Use these exact refs in working_memory.keep; do not repeat them in responses.",
|
|
155
|
+
display: false,
|
|
156
|
+
timestamp: 0,
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
160
|
+
const message = messages[index];
|
|
161
|
+
if (!message) continue;
|
|
162
|
+
projected.push(message);
|
|
163
|
+
const unique = new Map((unitsByMessage[index] ?? []).map((unit) => [unit.ref, unit]));
|
|
164
|
+
if (unique.size === 0) continue;
|
|
165
|
+
projected.push({
|
|
166
|
+
role: "custom",
|
|
167
|
+
customType: REFERENCE_CATALOG_TYPE,
|
|
168
|
+
content: [...unique.values()]
|
|
169
|
+
.sort((left, right) => left.order - right.order || left.suborder - right.suborder)
|
|
170
|
+
.map((unit) => `${unit.ref} (${unit.label}): ${unit.preview}`)
|
|
171
|
+
.join("\n"),
|
|
172
|
+
display: false,
|
|
173
|
+
timestamp: 0,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return projected;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function sanitizeAnchor(message: ContextMessage, id: string): ContextMessage {
|
|
180
|
+
if (message.role !== "assistant") return message;
|
|
181
|
+
return {
|
|
182
|
+
...message,
|
|
183
|
+
content: message.content.map((block) =>
|
|
184
|
+
block.type === "toolCall" && block.id === id && block.name === "working_memory"
|
|
185
|
+
? { ...block, arguments: { checkpoint: true } }
|
|
186
|
+
: block,
|
|
187
|
+
),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function findAnchor(messages: readonly ContextMessage[], id: string): number {
|
|
192
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
193
|
+
const message = messages[index];
|
|
194
|
+
if (
|
|
195
|
+
message?.role === "assistant" &&
|
|
196
|
+
message.content.some(
|
|
197
|
+
(block) => block.type === "toolCall" && block.id === id && block.name === "working_memory",
|
|
198
|
+
)
|
|
199
|
+
) {
|
|
200
|
+
return index;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return -1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function previewAssistant(blocks: ReadonlyArray<{ type: string }>): string {
|
|
207
|
+
for (const block of blocks) {
|
|
208
|
+
if (block.type === "text" && "text" in block && typeof block.text === "string") return compact(block.text);
|
|
209
|
+
if (block.type === "thinking" && "thinking" in block && typeof block.thinking === "string") {
|
|
210
|
+
return compact(block.thinking);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function previewTool(args: Record<string, unknown>): string {
|
|
217
|
+
return compact(JSON.stringify(args));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function previewMessage(message: ContextMessage): string {
|
|
221
|
+
if (message.role === "user" || message.role === "custom") {
|
|
222
|
+
if (typeof message.content === "string") return compact(message.content);
|
|
223
|
+
const text = message.content.find((part) => part.type === "text");
|
|
224
|
+
return text?.type === "text" ? compact(text.text) : "";
|
|
225
|
+
}
|
|
226
|
+
if (message.role === "bashExecution") return compact(`${message.command}: ${message.output}`);
|
|
227
|
+
if (message.role === "assistant") return previewAssistant(message.content);
|
|
228
|
+
if (message.role === "toolResult") {
|
|
229
|
+
const text = message.content.find((part) => part.type === "text");
|
|
230
|
+
return text?.type === "text" ? compact(text.text) : "";
|
|
231
|
+
}
|
|
232
|
+
return compact(message.summary);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function outlineRowIds(message: ContextMessage): string[] {
|
|
236
|
+
if (message.role !== "custom" || message.customType !== "tau.explore.outline") return [];
|
|
237
|
+
if (!isRecord(message.details) || typeof message.details.rowId !== "string") return [];
|
|
238
|
+
return [message.details.rowId];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function compact(value: string): string {
|
|
242
|
+
const text = value.replaceAll(/\s+/g, " ").trim();
|
|
243
|
+
return text.length <= 120 ? text : `${text.slice(0, 119)}…`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
247
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
248
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { AgentToolResult, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Marker } from "@shanepadgett/tau-tui";
|
|
4
|
+
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.ts";
|
|
5
|
+
import type { WorkingMemoryInput } from "./checkpoint.ts";
|
|
6
|
+
import { parseWorkingMemoryDetails } from "./state.ts";
|
|
7
|
+
|
|
8
|
+
const MAX_ITEMS = 20;
|
|
9
|
+
const MAX_LINE = 240;
|
|
10
|
+
const MAX_TEXT = 4_000;
|
|
11
|
+
|
|
12
|
+
export type WorkingMemoryNudgeDetails =
|
|
13
|
+
| {
|
|
14
|
+
v: 1;
|
|
15
|
+
kind: "automatic";
|
|
16
|
+
tokens: number;
|
|
17
|
+
boundaryTokens: number;
|
|
18
|
+
reminder: number;
|
|
19
|
+
tier: number;
|
|
20
|
+
tierCount: number;
|
|
21
|
+
anchorToolCallId: string | null;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
v: 1;
|
|
25
|
+
kind: "manual";
|
|
26
|
+
tokens: null;
|
|
27
|
+
boundaryTokens: null;
|
|
28
|
+
reminder: null;
|
|
29
|
+
tier: null;
|
|
30
|
+
tierCount: null;
|
|
31
|
+
anchorToolCallId: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function parseWorkingMemoryNudge(value: unknown): WorkingMemoryNudgeDetails | undefined {
|
|
35
|
+
if (!isRecord(value) || value.v !== 1 || (value.kind !== "automatic" && value.kind !== "manual")) return undefined;
|
|
36
|
+
if (value.anchorToolCallId !== null && !nonempty(value.anchorToolCallId)) return undefined;
|
|
37
|
+
if (value.kind === "manual") {
|
|
38
|
+
if (
|
|
39
|
+
value.tokens !== null ||
|
|
40
|
+
value.boundaryTokens !== null ||
|
|
41
|
+
value.reminder !== null ||
|
|
42
|
+
value.tier !== null ||
|
|
43
|
+
value.tierCount !== null
|
|
44
|
+
) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
v: 1,
|
|
49
|
+
kind: "manual",
|
|
50
|
+
tokens: null,
|
|
51
|
+
boundaryTokens: null,
|
|
52
|
+
reminder: null,
|
|
53
|
+
tier: null,
|
|
54
|
+
tierCount: null,
|
|
55
|
+
anchorToolCallId: value.anchorToolCallId,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
!count(value.tokens, 0) ||
|
|
60
|
+
!count(value.boundaryTokens, 1) ||
|
|
61
|
+
!count(value.reminder, 1) ||
|
|
62
|
+
!count(value.tier, 1, 5) ||
|
|
63
|
+
!count(value.tierCount, 1, 5) ||
|
|
64
|
+
value.boundaryTokens > value.tokens ||
|
|
65
|
+
value.tier > value.tierCount ||
|
|
66
|
+
value.tier !== Math.min(value.reminder, value.tierCount)
|
|
67
|
+
) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
v: 1,
|
|
72
|
+
kind: "automatic",
|
|
73
|
+
tokens: value.tokens,
|
|
74
|
+
boundaryTokens: value.boundaryTokens,
|
|
75
|
+
reminder: value.reminder,
|
|
76
|
+
tier: value.tier,
|
|
77
|
+
tierCount: value.tierCount,
|
|
78
|
+
anchorToolCallId: value.anchorToolCallId,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function renderWorkingMemoryNudge(details: unknown, theme: Theme): Marker | undefined {
|
|
83
|
+
const parsed = parseWorkingMemoryNudge(details);
|
|
84
|
+
if (!parsed) return undefined;
|
|
85
|
+
return new Marker({
|
|
86
|
+
theme,
|
|
87
|
+
state: "muted",
|
|
88
|
+
label: "Memory:",
|
|
89
|
+
parts:
|
|
90
|
+
parsed.kind === "manual" ? ["Reassessment requested."] : [formatTokens(parsed.boundaryTokens), "Reassess."],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function renderWorkingMemoryCall(
|
|
95
|
+
args: WorkingMemoryInput,
|
|
96
|
+
theme: Theme,
|
|
97
|
+
context: {
|
|
98
|
+
rowState: ToolRowStateStore;
|
|
99
|
+
rowId: string;
|
|
100
|
+
invalidate: () => void;
|
|
101
|
+
lastComponent: unknown;
|
|
102
|
+
},
|
|
103
|
+
): Text {
|
|
104
|
+
context.rowState.watch(context.rowId, context.invalidate);
|
|
105
|
+
const component = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
106
|
+
const kept = Array.isArray(args.keep) ? args.keep.length : 0;
|
|
107
|
+
const outlined = Array.isArray(args.outlineFiles) ? args.outlineFiles.length : 0;
|
|
108
|
+
const deferred = Array.isArray(args.deferFiles) ? args.deferFiles.length : 0;
|
|
109
|
+
component.setText(
|
|
110
|
+
`${formatToolRowTitle(context.rowState, context.rowId, "working_memory", theme)} ${theme.fg("muted", `${kept} kept · ${outlined} outline · ${deferred} deferred`)}`,
|
|
111
|
+
);
|
|
112
|
+
return component;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function renderWorkingMemoryResult(
|
|
116
|
+
result: AgentToolResult<unknown>,
|
|
117
|
+
expanded: boolean,
|
|
118
|
+
theme: Theme,
|
|
119
|
+
lastComponent: unknown,
|
|
120
|
+
): Text {
|
|
121
|
+
const component = lastComponent instanceof Text ? lastComponent : new Text("", 0, 0);
|
|
122
|
+
const details = parseWorkingMemoryDetails(result.details);
|
|
123
|
+
if (!details) {
|
|
124
|
+
component.setText(
|
|
125
|
+
theme.fg("warning", bounded(firstText(result) || "working_memory returned invalid details", 1_000)),
|
|
126
|
+
);
|
|
127
|
+
return component;
|
|
128
|
+
}
|
|
129
|
+
let text = theme.fg(
|
|
130
|
+
details.warnings.length === 0 ? "success" : "warning",
|
|
131
|
+
`Checkpoint · kept ${details.retainedRefs.length} · outlined ${details.outlinedFiles.length} · deferred ${details.deferredFiles.length} · removed ${details.removedUnits}${details.warnings.length === 0 ? "" : ` · warnings ${details.warnings.length}`}`,
|
|
132
|
+
);
|
|
133
|
+
if (expanded) {
|
|
134
|
+
const lines = [
|
|
135
|
+
bounded(firstText(result), 1_500),
|
|
136
|
+
...details.retainedLabels.map((item) => `${item.ref} ${item.label}: ${item.preview}`),
|
|
137
|
+
...details.outlinedFiles.map((file) => `outlined: ${file.path}`),
|
|
138
|
+
...details.deferredFiles.map((file) => `deferred: ${file.path} — ${file.reason}; when ${file.relevantWhen}`),
|
|
139
|
+
...details.warnings.map((warning) => `warning: ${warning}`),
|
|
140
|
+
].filter(Boolean);
|
|
141
|
+
let remaining = MAX_TEXT - visibleWidth(text);
|
|
142
|
+
for (const line of lines.slice(0, MAX_ITEMS)) {
|
|
143
|
+
if (remaining <= 1) break;
|
|
144
|
+
const styled = theme.fg("dim", bounded(line, MAX_LINE));
|
|
145
|
+
text += `\n${truncateToWidth(styled, remaining - 1, "…")}`;
|
|
146
|
+
remaining -= 1 + Math.min(visibleWidth(styled), remaining - 1);
|
|
147
|
+
}
|
|
148
|
+
if (lines.length > MAX_ITEMS && remaining > 10)
|
|
149
|
+
text += `\n${theme.fg("dim", `… ${lines.length - MAX_ITEMS} more`)}`;
|
|
150
|
+
}
|
|
151
|
+
component.setText(text);
|
|
152
|
+
return component;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function firstText(result: AgentToolResult<unknown>): string {
|
|
156
|
+
for (const part of result.content) if (part.type === "text") return part.text;
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function bounded(text: string, maximum: number): string {
|
|
161
|
+
return text.length <= maximum ? text : `${text.slice(0, maximum - 1)}…`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatTokens(tokens: number): string {
|
|
165
|
+
return tokens < 1_000 ? `${tokens}` : `${Math.round(tokens / 1_000)}k`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function count(value: unknown, minimum: number, maximum = Number.MAX_SAFE_INTEGER): value is number {
|
|
169
|
+
return Number.isSafeInteger(value) && (value as number) >= minimum && (value as number) <= maximum;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function nonempty(value: unknown): value is string {
|
|
173
|
+
return typeof value === "string" && value.length > 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
177
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
178
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_NUDGE_INSTRUCTIONS: [string, ...string[]] = [
|
|
5
|
+
"Reassess working memory. Continue coherent exploration when its evidence remains useful; otherwise prune known dead ends, obsolete outputs, and other context with no expected value.",
|
|
6
|
+
"Context is materially larger. Prune stale or bulky irrelevant evidence when safe, but keep active working evidence that would otherwise need to be reread.",
|
|
7
|
+
"Strongly reassess before more broad work. Remove accumulated waste and carry useful information at cheapest sufficient fidelity without scrubbing the active working set.",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export default defineTauExtensionSettings({
|
|
11
|
+
key: "workingMemory",
|
|
12
|
+
defaults: {
|
|
13
|
+
enabled: true as boolean,
|
|
14
|
+
nudgeEveryTokens: 40_000 as number,
|
|
15
|
+
nudgeInstructions: DEFAULT_NUDGE_INSTRUCTIONS,
|
|
16
|
+
},
|
|
17
|
+
schema: Type.Object(
|
|
18
|
+
{
|
|
19
|
+
enabled: Type.Optional(Type.Boolean({ default: true, description: "Enable working-memory checkpoints." })),
|
|
20
|
+
nudgeEveryTokens: Type.Optional(
|
|
21
|
+
Type.Integer({
|
|
22
|
+
default: 40_000,
|
|
23
|
+
minimum: 1,
|
|
24
|
+
description: "Active-context token interval between advisory working-memory reminders.",
|
|
25
|
+
}),
|
|
26
|
+
),
|
|
27
|
+
nudgeInstructions: Type.Optional(
|
|
28
|
+
Type.Array(Type.String({ minLength: 1, maxLength: 2_000 }), {
|
|
29
|
+
default: DEFAULT_NUDGE_INSTRUCTIONS,
|
|
30
|
+
minItems: 1,
|
|
31
|
+
maxItems: 5,
|
|
32
|
+
description: "Ordered advisory working-memory instructions. Later reminders repeat final instruction.",
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
{ additionalProperties: false },
|
|
37
|
+
),
|
|
38
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const WORKING_MEMORY_TOOL = "working_memory";
|
|
4
|
+
|
|
5
|
+
export interface DeferredFile {
|
|
6
|
+
path: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
relevantWhen: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RetainedLabel {
|
|
12
|
+
ref: string;
|
|
13
|
+
label: string;
|
|
14
|
+
preview: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkingMemoryCheckpointDetailsV1 {
|
|
18
|
+
v: 1;
|
|
19
|
+
anchorToolCallId: string;
|
|
20
|
+
retainedRefs: string[];
|
|
21
|
+
retainedLabels: RetainedLabel[];
|
|
22
|
+
prunedRowIds: string[];
|
|
23
|
+
outlinedFiles: Array<{ path: string; rowId: string }>;
|
|
24
|
+
deferredFiles: DeferredFile[];
|
|
25
|
+
removedUnits: number;
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ActiveWorkingMemoryState {
|
|
30
|
+
latestAnchorToolCallId: string | undefined;
|
|
31
|
+
retainedRefs: readonly string[];
|
|
32
|
+
prunedRowIds: ReadonlySet<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseWorkingMemoryDetails(value: unknown): WorkingMemoryCheckpointDetailsV1 | undefined {
|
|
36
|
+
if (!isRecord(value) || value.v !== 1 || !nonempty(value.anchorToolCallId)) return undefined;
|
|
37
|
+
const keys = [
|
|
38
|
+
"v",
|
|
39
|
+
"anchorToolCallId",
|
|
40
|
+
"retainedRefs",
|
|
41
|
+
"retainedLabels",
|
|
42
|
+
"prunedRowIds",
|
|
43
|
+
"outlinedFiles",
|
|
44
|
+
"deferredFiles",
|
|
45
|
+
"removedUnits",
|
|
46
|
+
"warnings",
|
|
47
|
+
];
|
|
48
|
+
if (Object.keys(value).length !== keys.length || !keys.every((key) => Object.hasOwn(value, key))) return undefined;
|
|
49
|
+
const retainedRefs = uniqueStrings(value.retainedRefs);
|
|
50
|
+
const prunedRowIds = uniqueStrings(value.prunedRowIds);
|
|
51
|
+
const warnings = strings(value.warnings);
|
|
52
|
+
const retainedLabels = parseRetainedLabels(value.retainedLabels);
|
|
53
|
+
const outlinedFiles = parseOutlinedFiles(value.outlinedFiles);
|
|
54
|
+
const deferredFiles = parseDeferredFiles(value.deferredFiles);
|
|
55
|
+
if (
|
|
56
|
+
!retainedRefs ||
|
|
57
|
+
!prunedRowIds ||
|
|
58
|
+
!warnings ||
|
|
59
|
+
!retainedLabels ||
|
|
60
|
+
retainedLabels.length !== retainedRefs.length ||
|
|
61
|
+
retainedLabels.some((label, index) => label.ref !== retainedRefs[index]) ||
|
|
62
|
+
!outlinedFiles ||
|
|
63
|
+
!deferredFiles ||
|
|
64
|
+
!Number.isSafeInteger(value.removedUnits) ||
|
|
65
|
+
(value.removedUnits as number) < 0
|
|
66
|
+
) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
v: 1,
|
|
71
|
+
anchorToolCallId: value.anchorToolCallId,
|
|
72
|
+
retainedRefs,
|
|
73
|
+
retainedLabels,
|
|
74
|
+
prunedRowIds,
|
|
75
|
+
outlinedFiles,
|
|
76
|
+
deferredFiles,
|
|
77
|
+
removedUnits: value.removedUnits as number,
|
|
78
|
+
warnings,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function replayWorkingMemoryState(branch: readonly SessionEntry[], enabled: boolean): ActiveWorkingMemoryState {
|
|
83
|
+
let prunedRowIds = new Set<string>();
|
|
84
|
+
let latestAnchorToolCallId: string | undefined;
|
|
85
|
+
let retainedRefs: readonly string[] = [];
|
|
86
|
+
if (enabled) {
|
|
87
|
+
for (const entry of branch) {
|
|
88
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
|
|
89
|
+
if (entry.message.toolName !== WORKING_MEMORY_TOOL) continue;
|
|
90
|
+
const details = parseWorkingMemoryDetails(entry.message.details);
|
|
91
|
+
if (!details || details.anchorToolCallId !== entry.message.toolCallId) continue;
|
|
92
|
+
prunedRowIds = new Set(details.prunedRowIds);
|
|
93
|
+
latestAnchorToolCallId = details.anchorToolCallId;
|
|
94
|
+
retainedRefs = details.retainedRefs;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { latestAnchorToolCallId, retainedRefs, prunedRowIds };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseRetainedLabels(value: unknown): RetainedLabel[] | undefined {
|
|
101
|
+
if (!Array.isArray(value)) return undefined;
|
|
102
|
+
const labels: RetainedLabel[] = [];
|
|
103
|
+
for (const item of value) {
|
|
104
|
+
if (!isRecord(item) || !nonempty(item.ref) || !nonempty(item.label) || typeof item.preview !== "string") {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
labels.push({ ref: item.ref, label: item.label, preview: item.preview });
|
|
108
|
+
}
|
|
109
|
+
return labels;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseOutlinedFiles(value: unknown): Array<{ path: string; rowId: string }> | undefined {
|
|
113
|
+
if (!Array.isArray(value)) return undefined;
|
|
114
|
+
const files: Array<{ path: string; rowId: string }> = [];
|
|
115
|
+
for (const item of value) {
|
|
116
|
+
if (!isRecord(item) || !nonempty(item.path) || !nonempty(item.rowId)) return undefined;
|
|
117
|
+
files.push({ path: item.path, rowId: item.rowId });
|
|
118
|
+
}
|
|
119
|
+
return files;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseDeferredFiles(value: unknown): DeferredFile[] | undefined {
|
|
123
|
+
if (!Array.isArray(value)) return undefined;
|
|
124
|
+
const files: DeferredFile[] = [];
|
|
125
|
+
for (const item of value) {
|
|
126
|
+
if (!isRecord(item) || !nonempty(item.path) || !nonempty(item.reason) || !nonempty(item.relevantWhen)) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
files.push({ path: item.path, reason: item.reason, relevantWhen: item.relevantWhen });
|
|
130
|
+
}
|
|
131
|
+
return files;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function uniqueStrings(value: unknown): string[] | undefined {
|
|
135
|
+
const parsed = strings(value, true);
|
|
136
|
+
if (!parsed || new Set(parsed).size !== parsed.length) return undefined;
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function strings(value: unknown, requireNonempty = false): string[] | undefined {
|
|
141
|
+
if (!Array.isArray(value)) return undefined;
|
|
142
|
+
if (!value.every((item) => typeof item === "string" && (!requireNonempty || item.length > 0))) return undefined;
|
|
143
|
+
return [...value];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function nonempty(value: unknown): value is string {
|
|
147
|
+
return typeof value === "string" && value.length > 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
151
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
152
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shanepadgett/tau-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Tau is a custom agentic harness built with pi extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@ast-grep/wasm": "0.45.0",
|
|
38
|
-
"@shanepadgett/tau-tui": "0.
|
|
38
|
+
"@shanepadgett/tau-tui": "0.29.0",
|
|
39
39
|
"@vscode/tree-sitter-wasm": "0.3.1",
|
|
40
40
|
"image-size": "2.0.2",
|
|
41
41
|
"smol-toml": "1.7.0",
|