@shanepadgett/tau-agent 0.26.0 → 0.28.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/appshot/README.md +1 -1
- package/extensions/appshot/index.ts +5 -11
- package/extensions/context/sync.ts +1 -9
- package/extensions/context-pruning/README.md +2 -2
- package/extensions/context-pruning/index.ts +58 -57
- package/extensions/context-pruning/prune.ts +2 -2
- package/extensions/context-pruning/render.ts +51 -39
- package/extensions/context-pruning/settings.ts +4 -5
- package/extensions/explore/README.md +13 -14
- package/extensions/explore/ast/adapter.ts +97 -0
- package/extensions/explore/ast/engine.ts +470 -0
- package/extensions/explore/ast/format/ast-search.ts +97 -0
- package/extensions/explore/ast/format/composite.ts +26 -0
- package/extensions/explore/ast/format/context.ts +66 -0
- package/extensions/explore/ast/format/deps.ts +86 -0
- package/extensions/explore/ast/format/discover.ts +85 -0
- package/extensions/explore/ast/format/impact.ts +105 -0
- package/extensions/explore/ast/format/outline.ts +66 -0
- package/extensions/explore/ast/format/relationships.ts +81 -0
- package/extensions/explore/ast/format/show.ts +38 -0
- package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
- package/extensions/explore/ast/grammars/manifest.json +41 -0
- package/extensions/explore/ast/grammars/manifest.ts +99 -0
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/grammars/swift.wasm +0 -0
- package/extensions/explore/ast/graph/file-graph.ts +360 -0
- package/extensions/explore/ast/graph/relationships.ts +597 -0
- package/extensions/explore/ast/identity.ts +119 -0
- package/extensions/explore/ast/ir.ts +130 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
- package/extensions/explore/ast/languages/csharp.ts +493 -0
- package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
- package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
- package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
- package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
- package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
- package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
- package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
- package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
- package/extensions/explore/ast/languages/go.ts +545 -0
- package/extensions/explore/ast/languages/java.ts +412 -0
- package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
- package/extensions/explore/ast/languages/kotlin.ts +611 -0
- package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
- package/extensions/explore/ast/languages/odin.ts +407 -0
- package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
- package/extensions/explore/ast/languages/rust.ts +506 -0
- package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
- package/extensions/explore/ast/languages/swift.ts +466 -0
- package/extensions/explore/ast/languages/tree.ts +220 -0
- package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
- package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
- package/extensions/explore/ast/languages/typescript.ts +716 -0
- package/extensions/explore/ast/markdown.ts +145 -0
- package/extensions/explore/ast/package-surface.ts +42 -0
- package/extensions/explore/ast/queries/ast-search.ts +306 -0
- package/extensions/explore/ast/queries/composite-target.ts +77 -0
- package/extensions/explore/ast/queries/context.ts +406 -0
- package/extensions/explore/ast/queries/discover.ts +427 -0
- package/extensions/explore/ast/queries/impact.ts +250 -0
- package/extensions/explore/ast/queries/outline.ts +203 -0
- package/extensions/explore/ast/queries/show.ts +262 -0
- package/extensions/explore/ast/query.ts +48 -0
- package/extensions/explore/ast/read/hook.ts +125 -0
- package/extensions/explore/ast/read/policy.ts +55 -0
- package/extensions/explore/ast/registry.ts +120 -0
- package/extensions/explore/ast/scan.ts +76 -0
- package/extensions/explore/ast/slice.ts +31 -0
- package/extensions/explore/ast/tools/ast-search.ts +138 -0
- package/extensions/explore/ast/tools/context.ts +126 -0
- package/extensions/explore/ast/tools/deps.ts +72 -0
- package/extensions/explore/ast/tools/discover.ts +208 -0
- package/extensions/explore/ast/tools/impact.ts +135 -0
- package/extensions/explore/ast/tools/outline.ts +200 -0
- package/extensions/explore/ast/tools/relationships.ts +160 -0
- package/extensions/explore/ast/tools/render.ts +121 -0
- package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
- package/extensions/explore/ast/tools/show.ts +116 -0
- package/extensions/explore/guidance.ts +52 -0
- package/extensions/explore/index.ts +91 -45
- package/extensions/explore/read/autoread.ts +118 -0
- package/extensions/explore/settings.ts +64 -0
- package/extensions/explore/traverse.ts +260 -86
- package/extensions/handoff/index.ts +1 -1
- package/extensions/patch/executor.ts +95 -1
- package/extensions/patch/index.ts +1 -0
- package/extensions/publish/index.ts +18 -85
- package/extensions/publish/publish-ui.ts +0 -4
- package/extensions/soul/README.md +7 -4
- package/extensions/soul/index.ts +11 -5
- package/extensions/soul/prompt.ts +30 -14
- package/extensions/soul/settings.ts +7 -2
- package/extensions/subagent/agents/context-sync.md +6 -10
- package/extensions/subagent/agents/review.md +44 -24
- package/extensions/subagent/agents/scout.md +53 -34
- package/extensions/subagent/run.ts +1 -1
- package/extensions/tau-help/help.md +2 -2
- package/package.json +6 -5
- package/schemas/tau.schema.json +51 -6
- package/{extensions/explore → shared}/autoread.ts +21 -7
- package/shared/bounded-text-result.ts +304 -0
- package/shared/events.ts +1 -0
- package/shared/full-file-knowledge.ts +49 -0
- package/shared/temporary-output-store.ts +275 -0
- package/extensions/explore/ast-tools.ts +0 -828
- package/extensions/explore/ast-worker.ts +0 -356
- package/extensions/explore/find.ts +0 -147
- package/extensions/explore/full-file-knowledge.ts +0 -234
- package/extensions/explore/grep.ts +0 -749
- package/extensions/explore/limits.ts +0 -22
- package/extensions/explore/ls.ts +0 -113
- package/extensions/explore/path-display.ts +0 -41
- package/extensions/explore/path-tree.ts +0 -150
- package/extensions/explore/read-cache.ts +0 -273
- package/extensions/explore/read-snapshots.ts +0 -57
- package/extensions/explore/read-stats-panel.ts +0 -154
- package/extensions/explore/read-stats.ts +0 -120
- package/extensions/explore/read.ts +0 -335
- package/extensions/explore/result.ts +0 -29
- package/native-bin/darwin-arm64/tau-ast +0 -0
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export function normalizeNonNegativeInteger(value: number | undefined, fallback: number): number {
|
|
2
|
-
if (value === undefined) return fallback;
|
|
3
|
-
if (!Number.isFinite(value)) return fallback;
|
|
4
|
-
return Math.max(0, Math.floor(value));
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function normalizeCountLimit(value: number | undefined, fallback: number): number {
|
|
8
|
-
if (value === undefined) return fallback;
|
|
9
|
-
if (!Number.isFinite(value)) return fallback;
|
|
10
|
-
return Math.max(1, Math.floor(value));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function divideBudget(limit: number, count: number): number[] {
|
|
14
|
-
if (count <= 0) return [];
|
|
15
|
-
const base = Math.max(1, Math.floor(limit / count));
|
|
16
|
-
let remainder = Math.max(0, limit - base * count);
|
|
17
|
-
return Array.from({ length: count }, () => {
|
|
18
|
-
const extra = remainder > 0 ? 1 : 0;
|
|
19
|
-
if (remainder > 0) remainder -= 1;
|
|
20
|
-
return base + extra;
|
|
21
|
-
});
|
|
22
|
-
}
|
package/extensions/explore/ls.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { defineTool, formatSize } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import { type Static, Type } from "typebox";
|
|
4
|
-
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
5
|
-
import { divideBudget, normalizeCountLimit, normalizeNonNegativeInteger } from "./limits.ts";
|
|
6
|
-
import { formatPathForDisplay, pathResolutionError, resolveExplorePath, stripLeadingAt } from "./path-display.ts";
|
|
7
|
-
import { type PathTreeEntry, renderPathTree } from "./path-tree.ts";
|
|
8
|
-
import { createExploreTextResult, type ExploreTextDetails, expandedExploreText } from "./result.ts";
|
|
9
|
-
import { collectPaths, type TraversalEntry } from "./traverse.ts";
|
|
10
|
-
|
|
11
|
-
const lsParams = Type.Object(
|
|
12
|
-
{
|
|
13
|
-
paths: Type.Optional(Type.Array(Type.String())),
|
|
14
|
-
depth: Type.Optional(Type.Number()),
|
|
15
|
-
limit: Type.Optional(Type.Number()),
|
|
16
|
-
all: Type.Optional(Type.Boolean()),
|
|
17
|
-
long: Type.Optional(Type.Boolean()),
|
|
18
|
-
},
|
|
19
|
-
{ additionalProperties: false },
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
type LsParams = Static<typeof lsParams>;
|
|
23
|
-
|
|
24
|
-
function formatTime(date: Date): string {
|
|
25
|
-
return date.toISOString().slice(0, 10);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function entryMetadata(entry: TraversalEntry, long: boolean): string | undefined {
|
|
29
|
-
if (!long) return undefined;
|
|
30
|
-
const size = entry.type === "dir" ? "dir" : formatSize(entry.stats.size);
|
|
31
|
-
return `${size} ${formatTime(entry.stats.mtime)}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function pathTreeEntry(entry: TraversalEntry, long: boolean): PathTreeEntry {
|
|
35
|
-
return {
|
|
36
|
-
displayPath: entry.displayPath,
|
|
37
|
-
type: entry.type,
|
|
38
|
-
metadata: entryMetadata(entry, long),
|
|
39
|
-
empty: entry.empty,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function renderCallSummary(args: LsParams | undefined): string {
|
|
44
|
-
const paths = args?.paths && args.paths.length > 0 ? args.paths.map(stripLeadingAt).join(" ") : ".";
|
|
45
|
-
const depth = normalizeNonNegativeInteger(args?.depth, 1);
|
|
46
|
-
const limit = normalizeCountLimit(args?.limit, 100);
|
|
47
|
-
const flags = [args?.all ? "all" : "", args?.long ? "long" : ""].filter(Boolean).join(" ");
|
|
48
|
-
return `paths=${paths} depth=${depth} limit=${limit}${flags ? ` ${flags}` : ""}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function createLsTool(rowState: ToolRowStateStore) {
|
|
52
|
-
return defineTool<typeof lsParams, ExploreTextDetails | undefined>({
|
|
53
|
-
name: "ls",
|
|
54
|
-
label: "ls",
|
|
55
|
-
description: "List files and directories with compact path output.",
|
|
56
|
-
promptSnippet: "List files and directories",
|
|
57
|
-
parameters: lsParams,
|
|
58
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
59
|
-
const requestedPaths = params.paths && params.paths.length > 0 ? params.paths : ["."];
|
|
60
|
-
const depth = normalizeNonNegativeInteger(params.depth, 1);
|
|
61
|
-
const limit = normalizeCountLimit(params.limit, 100);
|
|
62
|
-
const budgets = divideBudget(limit, requestedPaths.length);
|
|
63
|
-
const all = params.all === true;
|
|
64
|
-
const humanParts: string[] = [];
|
|
65
|
-
const agentParts: string[] = [];
|
|
66
|
-
|
|
67
|
-
for (let i = 0; i < requestedPaths.length; i += 1) {
|
|
68
|
-
const input = requestedPaths[i] ?? ".";
|
|
69
|
-
const root = resolveExplorePath(ctx.cwd, input);
|
|
70
|
-
const entries = await collectPaths({
|
|
71
|
-
cwd: ctx.cwd,
|
|
72
|
-
root,
|
|
73
|
-
maxDepth: depth,
|
|
74
|
-
includeRoot: true,
|
|
75
|
-
includeHidden: all,
|
|
76
|
-
includeIgnored: all,
|
|
77
|
-
includeNoise: all,
|
|
78
|
-
}).catch((error: unknown) => {
|
|
79
|
-
throw pathResolutionError(error, input);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const budget = budgets[i] ?? limit;
|
|
83
|
-
const shown = entries.slice(0, budget);
|
|
84
|
-
const omitted = entries.length - shown.length;
|
|
85
|
-
const notice = omitted > 0 ? `… omitted ${omitted} entries (limit ${budget})` : undefined;
|
|
86
|
-
const rootPath = formatPathForDisplay(root, ctx.cwd);
|
|
87
|
-
const tree = renderPathTree(
|
|
88
|
-
shown.map((entry) => pathTreeEntry(entry, params.long === true)),
|
|
89
|
-
{
|
|
90
|
-
rootPath,
|
|
91
|
-
omissionNotice: notice,
|
|
92
|
-
},
|
|
93
|
-
);
|
|
94
|
-
humanParts.push(tree.humanText);
|
|
95
|
-
agentParts.push(tree.agentText);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return createExploreTextResult(agentParts.join("\n\n"), humanParts.join("\n\n"));
|
|
99
|
-
},
|
|
100
|
-
renderCall(args, theme, context) {
|
|
101
|
-
rowState.watch(context.toolCallId, context.invalidate);
|
|
102
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
103
|
-
const title = formatToolRowTitle(rowState, context.toolCallId, "ls", theme);
|
|
104
|
-
text.setText(`${title} ${theme.fg("muted", renderCallSummary(args))}`);
|
|
105
|
-
return text;
|
|
106
|
-
},
|
|
107
|
-
renderResult(result, _options, _theme, context) {
|
|
108
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
109
|
-
text.setText(context.expanded ? expandedExploreText(result) : "");
|
|
110
|
-
return text;
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
-
|
|
3
|
-
export function stripLeadingAt(value: string): string {
|
|
4
|
-
return value.startsWith("@") ? value.slice(1) : value;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function toSlashPath(value: string): string {
|
|
8
|
-
return value.replace(/\\/g, "/");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function resolveExplorePath(cwd: string, input: string): string {
|
|
12
|
-
const path = stripLeadingAt(input);
|
|
13
|
-
if (path.length === 0) return resolve(cwd);
|
|
14
|
-
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function pathResolutionError(error: unknown, input: string): Error {
|
|
18
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
19
|
-
return new Error(`Path not found: ${stripLeadingAt(input)}`);
|
|
20
|
-
}
|
|
21
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function isWithinPath(parent: string, child: string): boolean {
|
|
25
|
-
const rel = relative(resolve(parent), resolve(child));
|
|
26
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function formatPathForDisplay(absolutePath: string, cwd: string): string {
|
|
30
|
-
const resolvedCwd = resolve(cwd);
|
|
31
|
-
const resolvedPath = resolve(absolutePath);
|
|
32
|
-
const rel = relative(resolvedCwd, resolvedPath);
|
|
33
|
-
if (rel === "") return ".";
|
|
34
|
-
if (!rel.startsWith("..") && !isAbsolute(rel)) return toSlashPath(rel);
|
|
35
|
-
return toSlashPath(resolvedPath);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function relativeSlash(from: string, to: string): string {
|
|
39
|
-
const rel = relative(resolve(from), resolve(to));
|
|
40
|
-
return rel === "" ? "." : toSlashPath(rel);
|
|
41
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
export type PathTreeEntryType = "file" | "dir";
|
|
2
|
-
|
|
3
|
-
export interface PathTreeEntry {
|
|
4
|
-
displayPath: string;
|
|
5
|
-
type: PathTreeEntryType;
|
|
6
|
-
metadata?: string;
|
|
7
|
-
empty?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface PathTreeRenderOptions {
|
|
11
|
-
rootPath?: string;
|
|
12
|
-
omissionNotice?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface PathTreeRender {
|
|
16
|
-
humanText: string;
|
|
17
|
-
agentText: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface TreeNode {
|
|
21
|
-
name: string;
|
|
22
|
-
type: PathTreeEntryType;
|
|
23
|
-
metadata?: string;
|
|
24
|
-
empty?: boolean;
|
|
25
|
-
children: Map<string, TreeNode>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function splitDisplayPath(displayPath: string): string[] {
|
|
29
|
-
if (displayPath === ".") return [];
|
|
30
|
-
if (displayPath === "/") return ["/"];
|
|
31
|
-
if (displayPath.startsWith("/")) return ["/", ...displayPath.slice(1).split("/").filter(Boolean)];
|
|
32
|
-
return displayPath.split("/").filter(Boolean);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isUnderRoot(rootPath: string, displayPath: string): boolean {
|
|
36
|
-
if (rootPath === "/") return displayPath.startsWith("/");
|
|
37
|
-
if (rootPath === ".") return !displayPath.startsWith("/");
|
|
38
|
-
return displayPath === rootPath || displayPath.startsWith(`${rootPath}/`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function relativeToRoot(rootPath: string, displayPath: string): string {
|
|
42
|
-
if (rootPath === "/") return displayPath === "/" ? "" : displayPath.slice(1);
|
|
43
|
-
if (rootPath === ".") return displayPath === "." ? "" : displayPath;
|
|
44
|
-
if (displayPath === rootPath) return "";
|
|
45
|
-
return displayPath.slice(rootPath.length + 1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function entryLabel(node: TreeNode): string {
|
|
49
|
-
const name = node.name === "/" ? "/" : node.type === "dir" ? `${node.name}/` : node.name;
|
|
50
|
-
return node.metadata ? `${name} ${node.metadata}` : name;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function sortChildren(node: TreeNode): TreeNode[] {
|
|
54
|
-
return [...node.children.values()].sort((a, b) => {
|
|
55
|
-
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
56
|
-
return a.name.localeCompare(b.name);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function createNode(name: string, type: PathTreeEntryType): TreeNode {
|
|
61
|
-
return { name, type, children: new Map() };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function insert(root: TreeNode, relativePath: string, entry: PathTreeEntry): void {
|
|
65
|
-
const parts = splitDisplayPath(relativePath);
|
|
66
|
-
if (parts.length === 0) {
|
|
67
|
-
root.type = entry.type;
|
|
68
|
-
root.metadata = entry.metadata;
|
|
69
|
-
root.empty = entry.empty;
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let current = root;
|
|
74
|
-
for (let i = 0; i < parts.length; i += 1) {
|
|
75
|
-
const part = parts[i];
|
|
76
|
-
if (!part) continue;
|
|
77
|
-
const isLeaf = i === parts.length - 1;
|
|
78
|
-
const type = isLeaf ? entry.type : "dir";
|
|
79
|
-
let child = current.children.get(part);
|
|
80
|
-
if (!child) {
|
|
81
|
-
child = createNode(part, type);
|
|
82
|
-
current.children.set(part, child);
|
|
83
|
-
}
|
|
84
|
-
if (isLeaf) {
|
|
85
|
-
child.type = entry.type;
|
|
86
|
-
child.metadata = entry.metadata;
|
|
87
|
-
child.empty = entry.empty;
|
|
88
|
-
}
|
|
89
|
-
current = child;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function renderHumanNode(node: TreeNode, depth: number, lines: string[]): void {
|
|
94
|
-
lines.push(`${" ".repeat(depth)}${entryLabel(node)}`);
|
|
95
|
-
if (node.empty && node.children.size === 0) lines.push(`${" ".repeat(depth + 1)}[empty]`);
|
|
96
|
-
for (const child of sortChildren(node)) renderHumanNode(child, depth + 1, lines);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function renderAgentNode(node: TreeNode, depth: number, lines: string[]): void {
|
|
100
|
-
if (node.type === "file") {
|
|
101
|
-
lines.push(`${" ".repeat(depth)}${entryLabel(node)}`);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const children = sortChildren(node);
|
|
106
|
-
const files = children.filter((child) => child.type === "file").map(entryLabel);
|
|
107
|
-
let line = `${" ".repeat(depth)}${entryLabel(node)}`;
|
|
108
|
-
if (files.length > 0) line += `: ${files.join(", ")}`;
|
|
109
|
-
else if (node.empty && children.length === 0) line += " [empty]";
|
|
110
|
-
lines.push(line);
|
|
111
|
-
|
|
112
|
-
for (const child of children) {
|
|
113
|
-
if (child.type === "dir") renderAgentNode(child, depth + 1, lines);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function buildTree(entries: readonly PathTreeEntry[], rootPath: string | undefined): TreeNode {
|
|
118
|
-
const rootName = rootPath === undefined ? "" : rootPath === "." ? "." : rootPath;
|
|
119
|
-
const root = createNode(rootName, "dir");
|
|
120
|
-
for (const entry of entries) {
|
|
121
|
-
if (rootPath !== undefined && isUnderRoot(rootPath, entry.displayPath)) {
|
|
122
|
-
insert(root, relativeToRoot(rootPath, entry.displayPath), entry);
|
|
123
|
-
} else {
|
|
124
|
-
insert(root, entry.displayPath, entry);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return root;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function appendNotice(lines: string[], notice: string | undefined): void {
|
|
131
|
-
if (notice) lines.push(notice);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function renderPathTree(entries: readonly PathTreeEntry[], options: PathTreeRenderOptions = {}): PathTreeRender {
|
|
135
|
-
const root = buildTree(entries, options.rootPath);
|
|
136
|
-
const humanLines: string[] = [];
|
|
137
|
-
const agentLines: string[] = [];
|
|
138
|
-
|
|
139
|
-
if (options.rootPath === undefined) {
|
|
140
|
-
for (const child of sortChildren(root)) renderHumanNode(child, 0, humanLines);
|
|
141
|
-
for (const child of sortChildren(root)) renderAgentNode(child, 0, agentLines);
|
|
142
|
-
} else {
|
|
143
|
-
renderHumanNode(root, 0, humanLines);
|
|
144
|
-
renderAgentNode(root, 0, agentLines);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
appendNotice(humanLines, options.omissionNotice);
|
|
148
|
-
appendNotice(agentLines, options.omissionNotice);
|
|
149
|
-
return { humanText: humanLines.join("\n"), agentText: agentLines.join("\n") };
|
|
150
|
-
}
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { replayContextPruningState } from "../../shared/context-pruning-state.ts";
|
|
4
|
-
import { applyCompleteFileDiff, COMPLETE_FILE_SCOPE, decodeCompleteFileSource } from "./full-file-knowledge.ts";
|
|
5
|
-
|
|
6
|
-
export type ReadCacheMode = "baseline" | "recovery" | "unchanged" | "diff";
|
|
7
|
-
export type ReadCachePresentation = "plain" | "line-numbered";
|
|
8
|
-
|
|
9
|
-
export interface ReadCacheMetaV1 {
|
|
10
|
-
v: 1;
|
|
11
|
-
pathKey: string;
|
|
12
|
-
scopeKey: string;
|
|
13
|
-
presentation: ReadCachePresentation;
|
|
14
|
-
servedHash: string;
|
|
15
|
-
baseHash?: string;
|
|
16
|
-
mode: ReadCacheMode;
|
|
17
|
-
baselineTokens: number;
|
|
18
|
-
returnedTokens: number;
|
|
19
|
-
totalLines: number;
|
|
20
|
-
summary: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ReadCacheScopeTrust {
|
|
24
|
-
hash: string;
|
|
25
|
-
text?: string;
|
|
26
|
-
rowIds: readonly string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface AcceptedReadCacheRow {
|
|
30
|
-
rowId: string;
|
|
31
|
-
pathKey: string;
|
|
32
|
-
scopeKey: string;
|
|
33
|
-
meta: ReadCacheMetaV1;
|
|
34
|
-
dependencyRowIds: readonly string[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface CompleteFileDependencyChain {
|
|
38
|
-
pathKey: string;
|
|
39
|
-
servedHash: string;
|
|
40
|
-
rowIds: readonly string[];
|
|
41
|
-
sourceText: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ReadCacheReplayResult {
|
|
45
|
-
scopeTrust: ReadonlyMap<string, ReadonlyMap<string, ReadCacheScopeTrust>>;
|
|
46
|
-
acceptedRows: readonly AcceptedReadCacheRow[];
|
|
47
|
-
completeFileChains: ReadonlyMap<string, CompleteFileDependencyChain>;
|
|
48
|
-
failedPatchRecoveryPaths: ReadonlySet<string>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface ReadCacheDecision {
|
|
52
|
-
baseHash?: string;
|
|
53
|
-
baselineText?: string;
|
|
54
|
-
recovery: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface ReadCacheStore {
|
|
58
|
-
decision(ctx: ExtensionContext, pathKey: string, scopeKey: string): ReadCacheDecision;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function createReadCacheStore(): ReadCacheStore {
|
|
62
|
-
return {
|
|
63
|
-
decision(ctx, pathKey, scopeKey) {
|
|
64
|
-
const manager = ctx.sessionManager;
|
|
65
|
-
if (!manager) {
|
|
66
|
-
return { baseHash: undefined, baselineText: undefined, recovery: false };
|
|
67
|
-
}
|
|
68
|
-
const pruning = replayContextPruningState(manager.getBranch());
|
|
69
|
-
const ignoredRowIds = new Set([...pruning.prunedToolCallIds, ...pruning.prunedAutoreadRowIds]);
|
|
70
|
-
for (const id of pruning.retainedToolCallIds) ignoredRowIds.delete(id);
|
|
71
|
-
const state = replayReadCache(manager.buildContextEntries(), ctx.cwd, ignoredRowIds);
|
|
72
|
-
const trust = state.scopeTrust.get(pathKey)?.get(scopeKey);
|
|
73
|
-
return {
|
|
74
|
-
baseHash: trust?.hash,
|
|
75
|
-
baselineText: trust?.text,
|
|
76
|
-
recovery: state.failedPatchRecoveryPaths.has(pathKey),
|
|
77
|
-
};
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function parseReadCacheMeta(value: unknown): ReadCacheMetaV1 | undefined {
|
|
83
|
-
if (!isRecord(value) || value.v !== 1) return undefined;
|
|
84
|
-
if (typeof value.pathKey !== "string" || typeof value.scopeKey !== "string") return undefined;
|
|
85
|
-
if (value.presentation !== "plain" && value.presentation !== "line-numbered") return undefined;
|
|
86
|
-
if (typeof value.servedHash !== "string" || value.servedHash.length === 0) return undefined;
|
|
87
|
-
if (!isReadCacheMode(value.mode)) return undefined;
|
|
88
|
-
if (!isNonNegativeFinite(value.baselineTokens) || !isNonNegativeFinite(value.returnedTokens)) return undefined;
|
|
89
|
-
if (!isNonNegativeFinite(value.totalLines) || typeof value.summary !== "string") return undefined;
|
|
90
|
-
if (value.baseHash !== undefined && typeof value.baseHash !== "string") return undefined;
|
|
91
|
-
if ((value.mode === "unchanged" || value.mode === "diff") && !value.baseHash) return undefined;
|
|
92
|
-
return {
|
|
93
|
-
v: 1,
|
|
94
|
-
pathKey: value.pathKey,
|
|
95
|
-
scopeKey: value.scopeKey,
|
|
96
|
-
presentation: value.presentation,
|
|
97
|
-
servedHash: value.servedHash,
|
|
98
|
-
baseHash: value.baseHash,
|
|
99
|
-
mode: value.mode,
|
|
100
|
-
baselineTokens: value.baselineTokens,
|
|
101
|
-
returnedTokens: value.returnedTokens,
|
|
102
|
-
totalLines: value.totalLines,
|
|
103
|
-
summary: value.summary,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function readMetaFromMessage(message: unknown): ReadCacheMetaV1 | undefined {
|
|
108
|
-
if (!isRecord(message)) return undefined;
|
|
109
|
-
const isReadResult = message.role === "toolResult" && message.toolName === "read";
|
|
110
|
-
const isAutoread = message.customType === "tau.autoread";
|
|
111
|
-
if (!isReadResult && !isAutoread) return undefined;
|
|
112
|
-
if (!isRecord(message.details)) return undefined;
|
|
113
|
-
if (isAutoread && message.details.status !== "read") return undefined;
|
|
114
|
-
return parseReadCacheMeta(message.details.readCache);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function replayReadCache(
|
|
118
|
-
entries: readonly unknown[],
|
|
119
|
-
cwd: string,
|
|
120
|
-
ignoredRowIds: ReadonlySet<string> = new Set(),
|
|
121
|
-
): ReadCacheReplayResult {
|
|
122
|
-
const trust = new Map<string, Map<string, ReadCacheScopeTrust>>();
|
|
123
|
-
const acceptedRows: AcceptedReadCacheRow[] = [];
|
|
124
|
-
const failedPatchRecoveryPaths = new Set<string>();
|
|
125
|
-
for (const entry of entries) {
|
|
126
|
-
if (!isRecord(entry)) continue;
|
|
127
|
-
let message: unknown;
|
|
128
|
-
if (entry.type === "message" && "message" in entry) message = entry.message;
|
|
129
|
-
else if (entry.type === "custom_message") message = entry;
|
|
130
|
-
else continue;
|
|
131
|
-
const parsedMeta = readMetaFromMessage(message);
|
|
132
|
-
if (parsedMeta) {
|
|
133
|
-
const rowId = readRowId(message);
|
|
134
|
-
if (!rowId || ignoredRowIds.has(rowId)) continue;
|
|
135
|
-
const pathKey = resolve(cwd, parsedMeta.pathKey);
|
|
136
|
-
const meta = pathKey === parsedMeta.pathKey ? parsedMeta : { ...parsedMeta, pathKey };
|
|
137
|
-
if (applyReadMeta(trust, failedPatchRecoveryPaths, message, meta, rowId)) {
|
|
138
|
-
const acceptedTrust = trust.get(meta.pathKey)?.get(meta.scopeKey);
|
|
139
|
-
if (acceptedTrust)
|
|
140
|
-
acceptedRows.push({
|
|
141
|
-
rowId,
|
|
142
|
-
pathKey: meta.pathKey,
|
|
143
|
-
scopeKey: meta.scopeKey,
|
|
144
|
-
meta,
|
|
145
|
-
dependencyRowIds: [...acceptedTrust.rowIds],
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
for (const path of failedPatchPaths(message, cwd)) failedPatchRecoveryPaths.add(path);
|
|
151
|
-
}
|
|
152
|
-
const completeFileChains = new Map<string, CompleteFileDependencyChain>();
|
|
153
|
-
for (const [pathKey, scopes] of trust) {
|
|
154
|
-
const complete = scopes.get(COMPLETE_FILE_SCOPE);
|
|
155
|
-
if (!complete || complete.text === undefined) continue;
|
|
156
|
-
completeFileChains.set(pathKey, {
|
|
157
|
-
pathKey,
|
|
158
|
-
servedHash: complete.hash,
|
|
159
|
-
rowIds: [...complete.rowIds],
|
|
160
|
-
sourceText: complete.text,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
return { scopeTrust: trust, acceptedRows, completeFileChains, failedPatchRecoveryPaths };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function applyReadMeta(
|
|
167
|
-
trust: Map<string, Map<string, ReadCacheScopeTrust>>,
|
|
168
|
-
failedPatchRecoveryPaths: Set<string>,
|
|
169
|
-
message: unknown,
|
|
170
|
-
meta: ReadCacheMetaV1,
|
|
171
|
-
rowId: string,
|
|
172
|
-
): boolean {
|
|
173
|
-
const existing = trust.get(meta.pathKey)?.get(meta.scopeKey);
|
|
174
|
-
if (meta.mode === "baseline" || meta.mode === "recovery") {
|
|
175
|
-
let text: string | undefined;
|
|
176
|
-
if (meta.scopeKey === COMPLETE_FILE_SCOPE) {
|
|
177
|
-
if (!isRecord(message)) return false;
|
|
178
|
-
let pathHeader: string | undefined;
|
|
179
|
-
if (message.customType === "tau.autoread") {
|
|
180
|
-
if (!isRecord(message.details)) return false;
|
|
181
|
-
if (typeof message.details.path !== "string" || typeof message.details.cwd !== "string") return false;
|
|
182
|
-
if (resolve(message.details.cwd, message.details.path) !== meta.pathKey) return false;
|
|
183
|
-
pathHeader = message.details.path;
|
|
184
|
-
}
|
|
185
|
-
const decoded = decodeCompleteFileSource({
|
|
186
|
-
content: message.content,
|
|
187
|
-
autoread: message.customType === "tau.autoread",
|
|
188
|
-
...(pathHeader === undefined ? {} : { pathHeader }),
|
|
189
|
-
presentation: meta.presentation,
|
|
190
|
-
servedHash: meta.servedHash,
|
|
191
|
-
});
|
|
192
|
-
if (!decoded.valid || decoded.text === undefined) return false;
|
|
193
|
-
text = decoded.text;
|
|
194
|
-
}
|
|
195
|
-
setTrust(trust, meta.pathKey, meta.scopeKey, { hash: meta.servedHash, text, rowIds: [rowId] });
|
|
196
|
-
failedPatchRecoveryPaths.delete(meta.pathKey);
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
if (!meta.baseHash || existing?.hash !== meta.baseHash) return false;
|
|
200
|
-
if (meta.mode === "unchanged" && meta.servedHash !== meta.baseHash) return false;
|
|
201
|
-
let text = meta.mode === "unchanged" ? existing.text : undefined;
|
|
202
|
-
if (meta.scopeKey === COMPLETE_FILE_SCOPE && meta.mode === "diff") {
|
|
203
|
-
if (existing.text === undefined) return false;
|
|
204
|
-
const decoded = applyCompleteFileDiff({
|
|
205
|
-
content: isRecord(message) ? message.content : undefined,
|
|
206
|
-
baseText: existing.text,
|
|
207
|
-
baseHash: meta.baseHash,
|
|
208
|
-
servedHash: meta.servedHash,
|
|
209
|
-
});
|
|
210
|
-
if (!decoded.valid || decoded.text === undefined) return false;
|
|
211
|
-
text = decoded.text;
|
|
212
|
-
}
|
|
213
|
-
setTrust(trust, meta.pathKey, meta.scopeKey, {
|
|
214
|
-
hash: meta.servedHash,
|
|
215
|
-
text,
|
|
216
|
-
rowIds: [...existing.rowIds, rowId],
|
|
217
|
-
});
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function readRowId(message: unknown): string | undefined {
|
|
222
|
-
if (!isRecord(message)) return undefined;
|
|
223
|
-
if (message.role === "toolResult" && message.toolName === "read") {
|
|
224
|
-
return typeof message.toolCallId === "string" && message.toolCallId.length > 0 ? message.toolCallId : undefined;
|
|
225
|
-
}
|
|
226
|
-
if (message.customType !== "tau.autoread" || !isRecord(message.details)) return undefined;
|
|
227
|
-
return typeof message.details.rowId === "string" && message.details.rowId.length > 0
|
|
228
|
-
? message.details.rowId
|
|
229
|
-
: undefined;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function failedPatchPaths(message: unknown, cwd: string): string[] {
|
|
233
|
-
if (!isRecord(message) || message.role !== "toolResult" || message.toolName !== "patch") return [];
|
|
234
|
-
if (
|
|
235
|
-
!isRecord(message.details) ||
|
|
236
|
-
message.details.status === "completed" ||
|
|
237
|
-
!Array.isArray(message.details.failures)
|
|
238
|
-
) {
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
241
|
-
const paths: string[] = [];
|
|
242
|
-
for (const failure of message.details.failures) {
|
|
243
|
-
if (!isRecord(failure) || typeof failure.path !== "string" || failure.path.trim().length === 0) continue;
|
|
244
|
-
paths.push(resolve(cwd, failure.path));
|
|
245
|
-
}
|
|
246
|
-
return paths;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function setTrust(
|
|
250
|
-
trust: Map<string, Map<string, ReadCacheScopeTrust>>,
|
|
251
|
-
pathKey: string,
|
|
252
|
-
scopeKey: string,
|
|
253
|
-
value: ReadCacheScopeTrust,
|
|
254
|
-
): void {
|
|
255
|
-
let scopes = trust.get(pathKey);
|
|
256
|
-
if (!scopes) {
|
|
257
|
-
scopes = new Map();
|
|
258
|
-
trust.set(pathKey, scopes);
|
|
259
|
-
}
|
|
260
|
-
scopes.set(scopeKey, value);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function isReadCacheMode(value: unknown): value is ReadCacheMode {
|
|
264
|
-
return value === "baseline" || value === "recovery" || value === "unchanged" || value === "diff";
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function isNonNegativeFinite(value: unknown): value is number {
|
|
268
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
272
|
-
return typeof value === "object" && value !== null;
|
|
273
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "./full-file-knowledge.ts";
|
|
2
|
-
|
|
3
|
-
const MAX_TOTAL_BYTES = 16 * 1024 * 1024;
|
|
4
|
-
|
|
5
|
-
interface Snapshot {
|
|
6
|
-
text: string;
|
|
7
|
-
bytes: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ReadSnapshotStore {
|
|
11
|
-
get(hash: string): string | undefined;
|
|
12
|
-
epoch(): number;
|
|
13
|
-
isCurrent(epoch: number): boolean;
|
|
14
|
-
set(hash: string, text: string, bytes: number, epoch: number): boolean;
|
|
15
|
-
clear(): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function createReadSnapshotStore(): ReadSnapshotStore {
|
|
19
|
-
const snapshots = new Map<string, Snapshot>();
|
|
20
|
-
let totalBytes = 0;
|
|
21
|
-
let epoch = 0;
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
get(hash) {
|
|
25
|
-
const snapshot = snapshots.get(hash);
|
|
26
|
-
if (!snapshot) return undefined;
|
|
27
|
-
snapshots.delete(hash);
|
|
28
|
-
snapshots.set(hash, snapshot);
|
|
29
|
-
return snapshot.text;
|
|
30
|
-
},
|
|
31
|
-
epoch() {
|
|
32
|
-
return epoch;
|
|
33
|
-
},
|
|
34
|
-
isCurrent(candidate) {
|
|
35
|
-
return candidate === epoch;
|
|
36
|
-
},
|
|
37
|
-
set(hash, text, bytes, candidateEpoch) {
|
|
38
|
-
if (candidateEpoch !== epoch || bytes > MAX_COMPLETE_FILE_SNAPSHOT_BYTES) return false;
|
|
39
|
-
if (snapshots.has(hash)) return true;
|
|
40
|
-
snapshots.set(hash, { text, bytes });
|
|
41
|
-
totalBytes += bytes;
|
|
42
|
-
while (totalBytes > MAX_TOTAL_BYTES) {
|
|
43
|
-
const oldestHash = snapshots.keys().next().value as string | undefined;
|
|
44
|
-
if (oldestHash === undefined) break;
|
|
45
|
-
const oldest = snapshots.get(oldestHash);
|
|
46
|
-
snapshots.delete(oldestHash);
|
|
47
|
-
totalBytes -= oldest?.bytes ?? 0;
|
|
48
|
-
}
|
|
49
|
-
return true;
|
|
50
|
-
},
|
|
51
|
-
clear() {
|
|
52
|
-
snapshots.clear();
|
|
53
|
-
totalBytes = 0;
|
|
54
|
-
epoch += 1;
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|