@shanepadgett/tau-agent 0.33.0 → 0.34.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/context.md +29 -20
- package/docs/extending-tau-agent.md +63 -24
- package/extensions/appshot/index.ts +112 -111
- package/extensions/aside/README.md +7 -0
- package/extensions/aside/index.ts +168 -0
- package/extensions/aside/panel.ts +83 -0
- package/extensions/branch/index.ts +38 -21
- package/extensions/cache-diagnostics/index.ts +70 -58
- package/extensions/checkpoint/README.md +9 -0
- package/extensions/checkpoint/checkpoint-budget.ts +79 -0
- package/extensions/checkpoint/checkpoint.ts +277 -0
- package/extensions/checkpoint/index.ts +102 -0
- package/extensions/checkpoint/messages.ts +169 -0
- package/extensions/checkpoint/prompt.ts +24 -0
- package/extensions/checkpoint/settings.ts +27 -0
- package/extensions/commit/commit-effort.ts +3 -3
- package/extensions/context/README.md +12 -8
- package/extensions/context/definitions.ts +178 -55
- package/extensions/context/index.ts +192 -160
- package/extensions/context/panel.ts +57 -37
- package/extensions/context/sync.ts +139 -89
- package/extensions/context/validation.ts +37 -25
- package/extensions/effort/README.md +1 -1
- package/extensions/effort/index.ts +3 -3
- package/extensions/effort/state.ts +3 -18
- package/extensions/explore/guidance.ts +6 -47
- package/extensions/explore/index.ts +38 -53
- package/extensions/explore/{ast/read → read}/hook.ts +12 -7
- package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
- package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
- package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
- package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
- package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
- package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
- package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
- package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
- package/extensions/explore/tools/target-params.ts +9 -0
- package/extensions/footer/index.ts +75 -56
- package/extensions/handoff/index.ts +8 -30
- package/extensions/image-gen/index.ts +46 -40
- package/extensions/patch/executor.ts +190 -135
- package/extensions/patch/matcher.ts +82 -48
- package/extensions/patch/parser.ts +149 -105
- package/extensions/patch/render.ts +32 -39
- package/extensions/patch/summary.ts +30 -26
- package/extensions/ready/README.md +7 -0
- package/extensions/ready/index.ts +58 -0
- package/extensions/ready/model.ts +128 -0
- package/extensions/ready/packs.ts +272 -0
- package/extensions/ready/render.ts +445 -0
- package/extensions/ready/scan.ts +760 -0
- package/extensions/reference/panel.ts +28 -16
- package/extensions/review/index.ts +63 -51
- package/extensions/review/panel.ts +5 -34
- package/extensions/runtime-context/context.ts +30 -26
- package/extensions/script-runner/README.md +2 -2
- package/extensions/script-runner/index.ts +130 -93
- package/extensions/silent-command-runner/index.ts +38 -23
- package/extensions/subagent/agents/context-sync.md +185 -33
- package/extensions/subagent/agents/scout.md +2 -2
- package/extensions/subagent/agents.ts +137 -84
- package/extensions/subagent/cmux-dashboard.ts +39 -30
- package/extensions/subagent/index.ts +79 -59
- package/extensions/subagent/resume.ts +1 -1
- package/extensions/subagent/run.ts +304 -193
- package/extensions/subagent/runtime.ts +781 -343
- package/extensions/tau/index.ts +47 -38
- package/extensions/tau-help/help.md +14 -6
- package/extensions/tool-loader/README.md +4 -2
- package/extensions/tool-loader/index.ts +77 -52
- package/extensions/web/index.ts +6 -3
- package/extensions/web/webfetch.ts +114 -86
- package/package.json +3 -3
- package/schemas/tau.schema.json +20 -33
- package/shared/bounded-text-result.ts +1 -0
- package/shared/checkpoint-visibility.ts +9 -0
- package/shared/events.ts +28 -10
- package/shared/isolated-session.ts +71 -29
- package/shared/model-effort.ts +9 -19
- package/shared/model-fallback/index.ts +66 -41
- package/shared/settings/load.ts +45 -38
- package/shared/temporary-output-store.ts +16 -10
- package/{extensions/explore → src}/ast/adapter.ts +11 -2
- package/{extensions/explore → src}/ast/engine.ts +54 -38
- package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
- package/{extensions/explore → src}/ast/format/context.ts +3 -6
- package/src/ast/format/deps.ts +94 -0
- package/{extensions/explore → src}/ast/format/discover.ts +2 -2
- package/{extensions/explore → src}/ast/format/impact.ts +9 -11
- package/{extensions/explore → src}/ast/format/outline.ts +2 -2
- package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
- package/{extensions/explore → src}/ast/format/show.ts +1 -1
- package/src/ast/format/targeted-result.ts +20 -0
- package/src/ast/grammars/c_sharp.wasm +0 -0
- package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
- package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
- package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
- package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
- package/{extensions/explore → src}/ast/ir.ts +6 -0
- package/src/ast/languages/csharp-file-deps.ts +90 -0
- package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
- package/src/ast/languages/dotted-index.ts +131 -0
- package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
- package/src/ast/languages/fixtures/sample.cs +53 -0
- package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
- package/src/ast/languages/fixtures/sample.go +37 -0
- package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
- package/src/ast/languages/fixtures/sample.java +40 -0
- package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
- package/src/ast/languages/fixtures/sample.kt +31 -0
- package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
- package/src/ast/languages/fixtures/sample.md +23 -0
- package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
- package/src/ast/languages/fixtures/sample.rs +54 -0
- package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
- package/src/ast/languages/fixtures/sample.swift +45 -0
- package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
- package/src/ast/languages/fixtures/sample.ts +45 -0
- package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
- package/src/ast/languages/fixtures/sample.tsx +36 -0
- package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
- package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
- package/{extensions/explore → src}/ast/languages/go.ts +21 -3
- package/{extensions/explore → src}/ast/languages/java.ts +5 -38
- package/src/ast/languages/jvm-file-deps.ts +135 -0
- package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
- package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
- package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
- package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
- package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
- package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
- package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
- package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
- package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
- package/{extensions/explore → src}/ast/markdown.ts +72 -47
- package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
- package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
- package/{extensions/explore → src}/ast/queries/context.ts +116 -80
- package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
- package/src/ast/queries/impact.ts +291 -0
- package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
- package/{extensions/explore → src}/ast/queries/show.ts +46 -42
- package/{extensions/explore → src}/ast/query.ts +1 -1
- package/{extensions/explore → src}/ast/registry.ts +1 -2
- package/{extensions/explore → src}/ast/scan.ts +1 -1
- package/src/ast/session.ts +50 -0
- package/{extensions/explore → src}/ast/slice.ts +8 -0
- package/{extensions/explore → src/ast}/traverse.ts +32 -20
- package/src/file-injection/index.ts +547 -0
- package/src/image-generation/client.ts +50 -31
- package/src/image-generation/index.ts +177 -134
- package/src/index.ts +2 -0
- package/src/tool-loading/index.ts +58 -0
- package/extensions/context/evidence.ts +0 -517
- package/extensions/context/projection.ts +0 -141
- package/extensions/context/state.ts +0 -30
- package/extensions/context/write-scope.ts +0 -109
- package/extensions/explore/ast/format/deps.ts +0 -86
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
- package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
- package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
- package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
- package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
- package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
- package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
- package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
- package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
- package/extensions/explore/ast/languages/odin.ts +0 -407
- package/extensions/explore/ast/queries/impact.ts +0 -250
- package/extensions/explore/outline-injection.ts +0 -151
- package/extensions/explore/read/autoread.ts +0 -118
- package/extensions/working-memory/README.md +0 -17
- package/extensions/working-memory/checkpoint.ts +0 -242
- package/extensions/working-memory/index.ts +0 -341
- package/extensions/working-memory/memory.ts +0 -235
- package/extensions/working-memory/render.ts +0 -185
- package/extensions/working-memory/settings.ts +0 -38
- package/extensions/working-memory/state.ts +0 -158
- package/shared/autoread.ts +0 -234
- package/shared/context-messages.ts +0 -19
- package/shared/outline-injection.ts +0 -56
- /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
- /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
- /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
- /package/{extensions/explore → src}/ast/identity.ts +0 -0
- /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
- /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { bindingHint, bindingsHint, ScrollableMarkdown, ToolPanel } from "@shanepadgett/tau-tui";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
type Component,
|
|
5
|
+
type KeybindingsManager,
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
type TUI,
|
|
8
|
+
wrapTextWithAnsi,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
|
|
11
|
+
const PREVIEW_LINES = 5;
|
|
12
|
+
const PREVIEW_CHARACTERS = 2_000;
|
|
13
|
+
|
|
14
|
+
export interface AsideResult {
|
|
15
|
+
question: string;
|
|
16
|
+
answer: string;
|
|
17
|
+
context: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class AsideWidget implements Component {
|
|
21
|
+
private readonly panel: ToolPanel;
|
|
22
|
+
|
|
23
|
+
constructor(theme: Theme, question: string) {
|
|
24
|
+
this.panel = new ToolPanel(theme, {
|
|
25
|
+
title: "Aside · Thinking…",
|
|
26
|
+
body: {
|
|
27
|
+
render: (width) =>
|
|
28
|
+
wrapTextWithAnsi(theme.fg("muted", question.slice(0, PREVIEW_CHARACTERS)), width)
|
|
29
|
+
.slice(0, PREVIEW_LINES)
|
|
30
|
+
.map((line) => truncateToWidth(line, width, "")),
|
|
31
|
+
invalidate() {},
|
|
32
|
+
},
|
|
33
|
+
footer: { kind: "hints", hints: [] },
|
|
34
|
+
border: "horizontal",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
render(width: number): string[] {
|
|
39
|
+
return this.panel.render(width);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
invalidate(): void {
|
|
43
|
+
this.panel.invalidate();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class AsideResultPanel implements Component {
|
|
48
|
+
private readonly panel: ToolPanel;
|
|
49
|
+
private readonly body: ScrollableMarkdown;
|
|
50
|
+
|
|
51
|
+
constructor(tui: TUI, theme: Theme, keys: KeybindingsManager, result: AsideResult, done: () => void) {
|
|
52
|
+
this.body = new ScrollableMarkdown(tui, result.answer, 8);
|
|
53
|
+
this.panel = new ToolPanel(theme, {
|
|
54
|
+
title: "Aside",
|
|
55
|
+
secondary: `${result.context} · ${result.question.slice(0, 240)}`,
|
|
56
|
+
body: this.body,
|
|
57
|
+
footer: {
|
|
58
|
+
kind: "hints",
|
|
59
|
+
hints: [
|
|
60
|
+
bindingsHint(["tui.select.up", "tui.select.down"], "scroll"),
|
|
61
|
+
bindingHint("tui.select.confirm", "close"),
|
|
62
|
+
bindingHint("tui.select.cancel", "close"),
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
border: "box",
|
|
66
|
+
});
|
|
67
|
+
this.handleInput = (data) => {
|
|
68
|
+
if (keys.matches(data, "tui.select.up")) this.body.scroll(-1);
|
|
69
|
+
else if (keys.matches(data, "tui.select.down")) this.body.scroll(1);
|
|
70
|
+
else if (keys.matches(data, "tui.select.confirm") || keys.matches(data, "tui.select.cancel")) done();
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleInput: (data: string) => void;
|
|
75
|
+
|
|
76
|
+
render(width: number): string[] {
|
|
77
|
+
return this.panel.render(width);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
invalidate(): void {
|
|
81
|
+
this.panel.invalidate();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -14,7 +14,7 @@ const LIST_BRANCH_REFS_ARGS = [
|
|
|
14
14
|
"refs/remotes",
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
function normalizeBranchName(name: string): string {
|
|
18
18
|
return name
|
|
19
19
|
.toLowerCase()
|
|
20
20
|
.replace(/[^a-z0-9]+/g, "-")
|
|
@@ -108,38 +108,55 @@ async function loadBranchChoices(git: GitRunner, root: string): Promise<BranchCh
|
|
|
108
108
|
return parseBranchChoices(await git.run(LIST_BRANCH_REFS_ARGS, { cwd: root }));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
interface ParsedRef {
|
|
112
|
+
ref: string;
|
|
113
|
+
updatedAt: number;
|
|
114
|
+
current: boolean;
|
|
115
|
+
symbolic: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseRefLines(output: string): ParsedRef[] {
|
|
119
|
+
return output
|
|
113
120
|
.split("\n")
|
|
114
121
|
.filter(Boolean)
|
|
115
122
|
.map((line) => {
|
|
116
123
|
const [ref = "", seconds = "0", head = "", symref = ""] = line.split("\0");
|
|
117
124
|
return { ref, updatedAt: Number(seconds) * 1000, current: head.trim() === "*", symbolic: Boolean(symref) };
|
|
118
125
|
});
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function localChoice(ref: ParsedRef): BranchChoice | undefined {
|
|
129
|
+
const name = ref.ref.slice("refs/heads/".length);
|
|
130
|
+
if (!name || ref.current) return undefined;
|
|
131
|
+
return { id: `local:${name}`, kind: "local", label: name, name, updatedAt: ref.updatedAt };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function remoteChoice(ref: ParsedRef, localNames: ReadonlySet<string>): BranchChoice | undefined {
|
|
135
|
+
if (ref.symbolic) return undefined;
|
|
136
|
+
const upstream = ref.ref.slice("refs/remotes/".length);
|
|
137
|
+
const separator = upstream.indexOf("/");
|
|
138
|
+
if (separator < 1) return undefined;
|
|
139
|
+
const name = upstream.slice(separator + 1);
|
|
140
|
+
if (!name || name === "HEAD" || localNames.has(name)) return undefined;
|
|
141
|
+
const label = localNames.has(upstream) ? `${upstream} (remote)` : upstream;
|
|
142
|
+
return { id: `remote:${upstream}`, kind: "remote", label, name, upstream, updatedAt: ref.updatedAt };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseBranchChoices(output: string): BranchChoice[] {
|
|
146
|
+
const refs = parseRefLines(output);
|
|
121
147
|
const localNames = new Set(
|
|
122
|
-
refs.filter(({ ref }) => ref.startsWith(
|
|
148
|
+
refs.filter(({ ref }) => ref.startsWith("refs/heads/")).map(({ ref }) => ref.slice("refs/heads/".length)),
|
|
123
149
|
);
|
|
124
150
|
const choices: BranchChoice[] = [];
|
|
125
|
-
|
|
126
151
|
for (const ref of refs) {
|
|
127
|
-
if (ref.ref.startsWith(
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
choices.push({ id: `local:${name}`, kind: "local", label: name, name, updatedAt: ref.updatedAt });
|
|
152
|
+
if (ref.ref.startsWith("refs/heads/")) {
|
|
153
|
+
const choice = localChoice(ref);
|
|
154
|
+
if (choice) choices.push(choice);
|
|
131
155
|
continue;
|
|
132
156
|
}
|
|
133
|
-
if (!ref.ref.startsWith(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const separator = upstream.indexOf("/");
|
|
137
|
-
if (separator < 1) continue;
|
|
138
|
-
const name = upstream.slice(separator + 1);
|
|
139
|
-
if (!name || name === "HEAD" || localNames.has(name)) continue;
|
|
140
|
-
const label = localNames.has(upstream) ? `${upstream} (remote)` : upstream;
|
|
141
|
-
choices.push({ id: `remote:${upstream}`, kind: "remote", label, name, upstream, updatedAt: ref.updatedAt });
|
|
157
|
+
if (!ref.ref.startsWith("refs/remotes/")) continue;
|
|
158
|
+
const choice = remoteChoice(ref, localNames);
|
|
159
|
+
if (choice) choices.push(choice);
|
|
142
160
|
}
|
|
143
|
-
|
|
144
161
|
return choices.sort((left, right) => right.updatedAt - left.updatedAt || left.label.localeCompare(right.label));
|
|
145
162
|
}
|
|
@@ -179,6 +179,72 @@ export default function cacheDiagnosticsExtension(pi: ExtensionAPI): void {
|
|
|
179
179
|
cacheActivitySeen = false;
|
|
180
180
|
};
|
|
181
181
|
|
|
182
|
+
const purgeExpiredFiles = async (): Promise<void> => {
|
|
183
|
+
const cutoff = Date.now() - RETENTION_MS;
|
|
184
|
+
for (const parent of [directory, reportsDirectory]) {
|
|
185
|
+
for (const entry of await readdir(parent, { withFileTypes: true })) {
|
|
186
|
+
if (
|
|
187
|
+
!entry.isFile() ||
|
|
188
|
+
(!entry.name.endsWith(".jsonl") && !entry.name.endsWith(".json") && !entry.name.endsWith(".tmp"))
|
|
189
|
+
)
|
|
190
|
+
continue;
|
|
191
|
+
const path = join(parent, entry.name);
|
|
192
|
+
if ((await stat(path)).mtimeMs < cutoff) await unlink(path);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const restoreRecordsFromLog = async (path: string): Promise<Array<Record<string, unknown>>> => {
|
|
198
|
+
const persisted = await readRecentLogRecords(path);
|
|
199
|
+
requests = keepRecent(
|
|
200
|
+
persisted
|
|
201
|
+
.filter((record) => record.kind === "request" && record.version === 2)
|
|
202
|
+
.map((record) => record as unknown as RequestRecord),
|
|
203
|
+
);
|
|
204
|
+
responses = keepRecent(
|
|
205
|
+
persisted
|
|
206
|
+
.filter((record) => record.kind === "response" && record.version === 2)
|
|
207
|
+
.map((record) => record as unknown as ResponseRecord),
|
|
208
|
+
);
|
|
209
|
+
results = keepRecent(
|
|
210
|
+
persisted
|
|
211
|
+
.filter((record) => record.kind === "result" && record.version === 2)
|
|
212
|
+
.map((record) => record as unknown as ResultRecord),
|
|
213
|
+
);
|
|
214
|
+
markers = keepRecent(
|
|
215
|
+
persisted
|
|
216
|
+
.filter((record) => record.kind === "marker" && record.version === 2)
|
|
217
|
+
.map((record) => record as unknown as MarkerRecord),
|
|
218
|
+
);
|
|
219
|
+
return persisted;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const restoreComparisonBaseline = (persisted: Array<Record<string, unknown>>): void => {
|
|
223
|
+
let latestModelSelectIndex = -1;
|
|
224
|
+
for (let index = persisted.length - 1; index >= 0; index -= 1) {
|
|
225
|
+
const record = persisted[index];
|
|
226
|
+
if (record?.kind !== "marker" || record.version !== 2 || record.name !== "model-select") continue;
|
|
227
|
+
latestModelSelectIndex = index;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
const comparisonResults = persisted
|
|
231
|
+
.slice(latestModelSelectIndex + 1)
|
|
232
|
+
.filter((record) => record.kind === "result" && record.version === 2)
|
|
233
|
+
.map((record) => record as unknown as ResultRecord);
|
|
234
|
+
const latestPromotedResult = [...comparisonResults].reverse().find((result) => result.baselinePromoted);
|
|
235
|
+
const latestRequest = latestPromotedResult
|
|
236
|
+
? requests.find((request) => request.id === latestPromotedResult.id)
|
|
237
|
+
: undefined;
|
|
238
|
+
if (!latestPromotedResult || !latestRequest) return;
|
|
239
|
+
previousFingerprint = payloadFingerprintFromRequest(latestRequest);
|
|
240
|
+
previousRequestId = latestRequest.id;
|
|
241
|
+
previousPromptState = latestRequest.promptState;
|
|
242
|
+
previousPromptTokens = latestPromotedResult.usage.promptTokens;
|
|
243
|
+
cacheActivitySeen = comparisonResults.some(
|
|
244
|
+
(result) => result.baselinePromoted && result.usage.cacheRead + result.usage.cacheWrite > 0,
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
182
248
|
pi.registerCommand("cache-debug", {
|
|
183
249
|
description: "Write a bounded prompt-cache diagnostic report for this session",
|
|
184
250
|
async handler(_args, ctx) {
|
|
@@ -246,63 +312,9 @@ export default function cacheDiagnosticsExtension(pi: ExtensionAPI): void {
|
|
|
246
312
|
resetComparison();
|
|
247
313
|
await mkdir(reportsDirectory, { recursive: true });
|
|
248
314
|
logFile = join(directory, `${sessionId.replaceAll(/[^a-zA-Z0-9_-]/g, "_")}.jsonl`);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
!entry.isFile() ||
|
|
254
|
-
(!entry.name.endsWith(".jsonl") && !entry.name.endsWith(".json") && !entry.name.endsWith(".tmp"))
|
|
255
|
-
)
|
|
256
|
-
continue;
|
|
257
|
-
const path = join(parent, entry.name);
|
|
258
|
-
if ((await stat(path)).mtimeMs < cutoff) await unlink(path);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const persisted = await readRecentLogRecords(logFile);
|
|
262
|
-
requests = keepRecent(
|
|
263
|
-
persisted
|
|
264
|
-
.filter((record) => record.kind === "request" && record.version === 2)
|
|
265
|
-
.map((record) => record as unknown as RequestRecord),
|
|
266
|
-
);
|
|
267
|
-
responses = keepRecent(
|
|
268
|
-
persisted
|
|
269
|
-
.filter((record) => record.kind === "response" && record.version === 2)
|
|
270
|
-
.map((record) => record as unknown as ResponseRecord),
|
|
271
|
-
);
|
|
272
|
-
results = keepRecent(
|
|
273
|
-
persisted
|
|
274
|
-
.filter((record) => record.kind === "result" && record.version === 2)
|
|
275
|
-
.map((record) => record as unknown as ResultRecord),
|
|
276
|
-
);
|
|
277
|
-
markers = keepRecent(
|
|
278
|
-
persisted
|
|
279
|
-
.filter((record) => record.kind === "marker" && record.version === 2)
|
|
280
|
-
.map((record) => record as unknown as MarkerRecord),
|
|
281
|
-
);
|
|
282
|
-
let latestModelSelectIndex = -1;
|
|
283
|
-
for (let index = persisted.length - 1; index >= 0; index -= 1) {
|
|
284
|
-
const record = persisted[index];
|
|
285
|
-
if (record?.kind !== "marker" || record.version !== 2 || record.name !== "model-select") continue;
|
|
286
|
-
latestModelSelectIndex = index;
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
const comparisonRecords = persisted.slice(latestModelSelectIndex + 1);
|
|
290
|
-
const comparisonResults = comparisonRecords
|
|
291
|
-
.filter((record) => record.kind === "result" && record.version === 2)
|
|
292
|
-
.map((record) => record as unknown as ResultRecord);
|
|
293
|
-
const latestPromotedResult = [...comparisonResults].reverse().find((result) => result.baselinePromoted);
|
|
294
|
-
const latestRequest = latestPromotedResult
|
|
295
|
-
? requests.find((request) => request.id === latestPromotedResult.id)
|
|
296
|
-
: undefined;
|
|
297
|
-
if (latestPromotedResult && latestRequest) {
|
|
298
|
-
previousFingerprint = payloadFingerprintFromRequest(latestRequest);
|
|
299
|
-
previousRequestId = latestRequest.id;
|
|
300
|
-
previousPromptState = latestRequest.promptState;
|
|
301
|
-
previousPromptTokens = latestPromotedResult.usage.promptTokens;
|
|
302
|
-
cacheActivitySeen = comparisonResults.some(
|
|
303
|
-
(result) => result.baselinePromoted && result.usage.cacheRead + result.usage.cacheWrite > 0,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
315
|
+
await purgeExpiredFiles();
|
|
316
|
+
const persisted = await restoreRecordsFromLog(logFile);
|
|
317
|
+
restoreComparisonBaseline(persisted);
|
|
306
318
|
await appendRecord({
|
|
307
319
|
kind: "runtime",
|
|
308
320
|
version: 2,
|
|
@@ -471,7 +483,7 @@ export default function cacheDiagnosticsExtension(pi: ExtensionAPI): void {
|
|
|
471
483
|
);
|
|
472
484
|
pi.on("session_tree", () => addMarker("session-tree"));
|
|
473
485
|
pi.on("tool_execution_end", (event) => {
|
|
474
|
-
if (event.toolName !== "
|
|
486
|
+
if (event.toolName !== "checkpoint" && event.toolName !== "load_tools") return;
|
|
475
487
|
return addMarker("cache-affecting-tool", { tool: event.toolName, isError: event.isError });
|
|
476
488
|
});
|
|
477
489
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Checkpoint
|
|
2
|
+
|
|
3
|
+
Keeps long-running agent work focused by letting the agent checkpoint durable working context while disposable conversation history is retired. The agent writes a short resume directive that is appended to the hidden continuation message after each checkpoint.
|
|
4
|
+
|
|
5
|
+
Checkpoint rows are hidden by default. Set `extensions.checkpoint.showToolRows` to `true` before `/reload` to watch checkpoint and newly injected-file rows while working. Existing injected-file rows keep their saved display state.
|
|
6
|
+
|
|
7
|
+
Checkpoint nudges the agent at 50% and 75% of `extensions.checkpoint.checkpointTokenLimit`, which defaults to 150,000 context tokens. At the limit, it blocks non-checkpoint tools until the agent checkpoints.
|
|
8
|
+
|
|
9
|
+
After changing this extension, run `/reload` before testing it.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const DEFAULT_CHECKPOINT_TOKEN_LIMIT = 150_000;
|
|
2
|
+
|
|
3
|
+
export type CheckpointBudgetLevel = 0 | 50 | 75 | 100;
|
|
4
|
+
export type CheckpointBudgetNoticeLevel = Exclude<CheckpointBudgetLevel, 0>;
|
|
5
|
+
|
|
6
|
+
export interface CheckpointBudget {
|
|
7
|
+
configure(limit: number): void;
|
|
8
|
+
beginTurn(tokens: number | null): CheckpointBudgetNoticeLevel | undefined;
|
|
9
|
+
finishTurn(tokens: number | null): CheckpointBudgetNoticeLevel | undefined;
|
|
10
|
+
shouldBlockTool(toolName: string, checkpointToolName: string): boolean;
|
|
11
|
+
reset(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createCheckpointBudget(initialLimit = DEFAULT_CHECKPOINT_TOKEN_LIMIT): CheckpointBudget {
|
|
15
|
+
let limit = validateLimit(initialLimit);
|
|
16
|
+
let highestNoticed: CheckpointBudgetLevel = 0;
|
|
17
|
+
let forced = false;
|
|
18
|
+
let baselineTokens: number | null = 0;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
configure(nextLimit: number): void {
|
|
22
|
+
limit = validateLimit(nextLimit);
|
|
23
|
+
highestNoticed = 0;
|
|
24
|
+
forced = false;
|
|
25
|
+
baselineTokens = 0;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
beginTurn(tokens: number | null): CheckpointBudgetNoticeLevel | undefined {
|
|
29
|
+
return observe(tokens);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
finishTurn(tokens: number | null): CheckpointBudgetNoticeLevel | undefined {
|
|
33
|
+
return observe(tokens);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
shouldBlockTool(toolName: string, checkpointToolName: string): boolean {
|
|
37
|
+
return forced && toolName !== checkpointToolName;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
reset(): void {
|
|
41
|
+
highestNoticed = 0;
|
|
42
|
+
forced = false;
|
|
43
|
+
baselineTokens = null;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function observe(tokens: number | null): CheckpointBudgetNoticeLevel | undefined {
|
|
48
|
+
if (tokens === null) return undefined;
|
|
49
|
+
if (baselineTokens === null) {
|
|
50
|
+
baselineTokens = tokens;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (tokens < baselineTokens) {
|
|
54
|
+
baselineTokens = tokens;
|
|
55
|
+
highestNoticed = 0;
|
|
56
|
+
forced = false;
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const level = levelFor(tokens - baselineTokens, limit);
|
|
60
|
+
if (level === 100) forced = true;
|
|
61
|
+
if (level <= highestNoticed) return undefined;
|
|
62
|
+
highestNoticed = level;
|
|
63
|
+
return level === 0 ? undefined : level;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function levelFor(tokens: number, limit: number): CheckpointBudgetLevel {
|
|
68
|
+
if (tokens >= limit) return 100;
|
|
69
|
+
if (tokens >= limit * 0.75) return 75;
|
|
70
|
+
if (tokens >= limit * 0.5) return 50;
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateLimit(limit: number): number {
|
|
75
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
76
|
+
throw new Error("Checkpoint token limit must be a positive safe integer");
|
|
77
|
+
}
|
|
78
|
+
return limit;
|
|
79
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineTool,
|
|
3
|
+
keyText,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
type SessionEntry,
|
|
6
|
+
type SessionMessageEntry,
|
|
7
|
+
type Theme,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { truncateToWidth, type Component, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
10
|
+
import { Type, type Static } from "typebox";
|
|
11
|
+
import { prepareFileInjection } from "@shanepadgett/tau-agent";
|
|
12
|
+
import { areCheckpointRowsVisible } from "../../shared/checkpoint-visibility.ts";
|
|
13
|
+
import type { FileInjectionFile } from "../../src/file-injection/index.ts";
|
|
14
|
+
import { extractConversationText } from "./messages.ts";
|
|
15
|
+
import { formatCheckpointMessage } from "./prompt.ts";
|
|
16
|
+
|
|
17
|
+
export const CHECKPOINT_TOOL = "checkpoint";
|
|
18
|
+
const CONTINUATION_TYPE = "tau.checkpoint";
|
|
19
|
+
const CONTINUATION_PREAMBLE =
|
|
20
|
+
"Continue directly from the checkpoint state and provided files. Trust the provided sources and continue the listed work.";
|
|
21
|
+
const CHECKPOINT_PREVIEW_CHARACTERS = 240;
|
|
22
|
+
const CHECKPOINT_RENDER_CHARACTERS = 24_000;
|
|
23
|
+
const CHECKPOINT_RENDER_LINES = 200;
|
|
24
|
+
const CONVERSATION_MESSAGE_ID_ATTRIBUTE = /^(<(?:user|assistant)-message)\s+id="(?:\\.|[^"\\])*">$/gm;
|
|
25
|
+
|
|
26
|
+
const lineRange = Type.Object(
|
|
27
|
+
{
|
|
28
|
+
startLine: Type.Integer({ minimum: 1 }),
|
|
29
|
+
endLine: Type.Integer({ minimum: 1 }),
|
|
30
|
+
},
|
|
31
|
+
{ additionalProperties: false },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const checkpointFile = Type.Union([
|
|
35
|
+
Type.Object(
|
|
36
|
+
{
|
|
37
|
+
path: Type.String({ minLength: 1 }),
|
|
38
|
+
mode: Type.Literal("read"),
|
|
39
|
+
ranges: Type.Optional(Type.Array(lineRange)),
|
|
40
|
+
},
|
|
41
|
+
{ additionalProperties: false },
|
|
42
|
+
),
|
|
43
|
+
Type.Object(
|
|
44
|
+
{
|
|
45
|
+
path: Type.String({ minLength: 1 }),
|
|
46
|
+
mode: Type.Literal("outline"),
|
|
47
|
+
},
|
|
48
|
+
{ additionalProperties: false },
|
|
49
|
+
),
|
|
50
|
+
Type.Object(
|
|
51
|
+
{
|
|
52
|
+
path: Type.String({ minLength: 1 }),
|
|
53
|
+
mode: Type.Literal("deferred"),
|
|
54
|
+
when: Type.String({ minLength: 1 }),
|
|
55
|
+
},
|
|
56
|
+
{ additionalProperties: false },
|
|
57
|
+
),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const checkpointParams = Type.Object(
|
|
61
|
+
{
|
|
62
|
+
keepMessages: Type.Array(Type.String({ minLength: 1 }), {
|
|
63
|
+
description:
|
|
64
|
+
"Exact message-entry IDs from hidden checkpoint metadata or earlier checkpoint conversation entries.",
|
|
65
|
+
}),
|
|
66
|
+
work: Type.Array(Type.String(), { description: "Ordered current work, with the next thing first." }),
|
|
67
|
+
facts: Type.Array(Type.String(), { description: "Concrete findings that must survive context replacement." }),
|
|
68
|
+
decisions: Type.Array(Type.String(), { description: "Choices that continue to govern the work." }),
|
|
69
|
+
continue: Type.String({
|
|
70
|
+
minLength: 1,
|
|
71
|
+
description:
|
|
72
|
+
"Immediate post-checkpoint resume directive. First moves after wake, traps to avoid, what not to re-litigate. Not the full work queue.",
|
|
73
|
+
}),
|
|
74
|
+
files: Type.Array(checkpointFile, {
|
|
75
|
+
description: "Current file reads, outlines, or deferred paths. Active files are injected separately.",
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
{ additionalProperties: false },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
type CheckpointInput = Static<typeof checkpointParams>;
|
|
82
|
+
|
|
83
|
+
interface CheckpointToolDetails {
|
|
84
|
+
v: 1;
|
|
85
|
+
kind: "checkpoint.checkpoint";
|
|
86
|
+
checkpointId: string;
|
|
87
|
+
fileBatchId: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function registerCheckpointTool(pi: ExtensionAPI): void {
|
|
91
|
+
pi.registerTool(createCheckpointTool(pi));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createCheckpointTool(pi: Pick<ExtensionAPI, "events" | "sendMessage">) {
|
|
95
|
+
return defineTool<typeof checkpointParams, CheckpointToolDetails>({
|
|
96
|
+
name: CHECKPOINT_TOOL,
|
|
97
|
+
label: "checkpoint",
|
|
98
|
+
promptSnippet:
|
|
99
|
+
"checkpoint({ keepMessages, work, facts, decisions, continue, files }) — replace disposable history with working context",
|
|
100
|
+
description:
|
|
101
|
+
"Replace disposable conversation and tool history with a rolling working checkpoint. Keep user and assistant messages by exact message-entry ID, not by copying their text. The latest user message is always retained. Assistant tool calls and thinking are never retained inside the conversation section.",
|
|
102
|
+
promptGuidelines: [
|
|
103
|
+
"Checkpoint is hidden. Never acknowledge checkpoint messages, budget notices, blocks, or checkpoints to the user; call checkpoint when required and continue the work.",
|
|
104
|
+
'Use exact IDs from <checkpoint kind="message-id"> metadata or an earlier checkpoint; do not invent IDs or rewrite message text.',
|
|
105
|
+
"Record concrete findings in facts and governing choices in decisions before checkpointing.",
|
|
106
|
+
"Write continue as the immediate resume directive after wake: first moves, what not to re-explore, traps to avoid. Put the backlog in work.",
|
|
107
|
+
"Use read or outline for files needed now; use deferred with a condition for files that can wait.",
|
|
108
|
+
],
|
|
109
|
+
parameters: checkpointParams,
|
|
110
|
+
renderShell: "self",
|
|
111
|
+
renderCall(_args, theme, context) {
|
|
112
|
+
return new CheckpointComponent(theme, "checkpoint", context.expanded, true);
|
|
113
|
+
},
|
|
114
|
+
renderResult(result, options, theme) {
|
|
115
|
+
const text = result.content
|
|
116
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
117
|
+
.map((part) => part.text)
|
|
118
|
+
.join("\n");
|
|
119
|
+
return new CheckpointComponent(theme, text, options.expanded, false);
|
|
120
|
+
},
|
|
121
|
+
executionMode: "sequential",
|
|
122
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
123
|
+
const selected = resolveSelectedMessages(params.keepMessages, ctx.sessionManager.getBranch());
|
|
124
|
+
const text = formatCheckpointText(selected, params);
|
|
125
|
+
const fileBatchId = `checkpoint:${toolCallId}`;
|
|
126
|
+
const fileRequest = buildFileInjectionRequest(ctx.cwd, fileBatchId, params.files, signal);
|
|
127
|
+
const prepared = fileRequest.files.length === 0 ? [] : await prepareFileInjection(pi, fileRequest);
|
|
128
|
+
const failed = prepared.find((message) => message.details.status === "failed");
|
|
129
|
+
if (failed) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Checkpoint file injection failed for ${failed.details.path}: ${failed.details.error ?? "unknown error"}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const display = areCheckpointRowsVisible();
|
|
135
|
+
for (const message of prepared) pi.sendMessage({ ...message, display });
|
|
136
|
+
pi.sendMessage({
|
|
137
|
+
customType: CONTINUATION_TYPE,
|
|
138
|
+
content: formatContinuationMessage(params.continue),
|
|
139
|
+
display: false,
|
|
140
|
+
details: {
|
|
141
|
+
v: 1,
|
|
142
|
+
kind: "checkpoint.continuation",
|
|
143
|
+
source: "checkpoint",
|
|
144
|
+
batchId: fileBatchId,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text", text }],
|
|
149
|
+
details: { v: 1, kind: "checkpoint.checkpoint", checkpointId: toolCallId, fileBatchId },
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
class CheckpointComponent implements Component {
|
|
156
|
+
private readonly theme: Theme;
|
|
157
|
+
private readonly content: string;
|
|
158
|
+
private readonly expanded: boolean;
|
|
159
|
+
private readonly call: boolean;
|
|
160
|
+
|
|
161
|
+
constructor(theme: Theme, content: string, expanded: boolean, call: boolean) {
|
|
162
|
+
this.theme = theme;
|
|
163
|
+
this.content = content;
|
|
164
|
+
this.expanded = expanded;
|
|
165
|
+
this.call = call;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
render(width: number): string[] {
|
|
169
|
+
if (!areCheckpointRowsVisible()) return [];
|
|
170
|
+
if (this.call) return [truncateToWidth(this.theme.fg("toolTitle", this.theme.bold(CHECKPOINT_TOOL)), width, "…")];
|
|
171
|
+
const displayContent = this.content.replace(CONVERSATION_MESSAGE_ID_ATTRIBUTE, "$1>");
|
|
172
|
+
if (!this.expanded) {
|
|
173
|
+
const preview = displayContent.replace(/\s+/g, " ").trim().slice(0, CHECKPOINT_PREVIEW_CHARACTERS);
|
|
174
|
+
const suffix = displayContent.length > CHECKPOINT_PREVIEW_CHARACTERS ? "…" : "";
|
|
175
|
+
return [
|
|
176
|
+
truncateToWidth(
|
|
177
|
+
`${this.theme.fg("dim", preview + suffix)} ${this.theme.fg("muted", `(${keyText("app.tools.expand")} to expand)`)}`,
|
|
178
|
+
width,
|
|
179
|
+
"…",
|
|
180
|
+
),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const bounded =
|
|
185
|
+
displayContent.length > CHECKPOINT_RENDER_CHARACTERS
|
|
186
|
+
? `${displayContent.slice(0, CHECKPOINT_RENDER_CHARACTERS)}\n…`
|
|
187
|
+
: displayContent;
|
|
188
|
+
return wrapTextWithAnsi(this.theme.fg("dim", bounded), Math.max(1, width))
|
|
189
|
+
.slice(0, CHECKPOINT_RENDER_LINES)
|
|
190
|
+
.map((line) => truncateToWidth(line, width, "…"));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
invalidate(): void {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveSelectedMessages(ids: readonly string[], branch: readonly SessionEntry[]): SessionMessageEntry[] {
|
|
197
|
+
const entriesById = new Map(branch.map((entry) => [entry.id, entry]));
|
|
198
|
+
const selectedIds = new Set(ids);
|
|
199
|
+
const latestUser = [...branch].reverse().find(isUserMessageEntry);
|
|
200
|
+
if (!latestUser) throw new Error("Checkpoint requires a current user message");
|
|
201
|
+
selectedIds.add(latestUser.id);
|
|
202
|
+
|
|
203
|
+
for (const id of selectedIds) {
|
|
204
|
+
const entry = entriesById.get(id);
|
|
205
|
+
if (!entry || !isConversationEntry(entry)) throw new Error(`Unknown user or assistant message ID: ${id}`);
|
|
206
|
+
if (entry.message.role === "assistant" && extractConversationText(entry.message) === "") {
|
|
207
|
+
throw new Error(`Assistant message ID has no text content: ${id}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return branch.filter(
|
|
212
|
+
(entry): entry is SessionMessageEntry => selectedIds.has(entry.id) && isConversationEntry(entry),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function formatCheckpointText(selected: readonly SessionMessageEntry[], params: CheckpointInput): string {
|
|
217
|
+
const conversation = selected
|
|
218
|
+
.map((entry) => {
|
|
219
|
+
const text = extractConversationText(entry.message);
|
|
220
|
+
if (text === undefined) throw new Error(`Message ID is not a user or assistant message: ${entry.id}`);
|
|
221
|
+
const role = entry.message.role === "user" ? "user" : "assistant";
|
|
222
|
+
return `<${role}-message id=${JSON.stringify(entry.id)}>\n${text}\n</${role}-message>`;
|
|
223
|
+
})
|
|
224
|
+
.join("\n\n");
|
|
225
|
+
const deferred = params.files
|
|
226
|
+
.filter(
|
|
227
|
+
(file): file is Extract<CheckpointInput["files"][number], { mode: "deferred" }> => file.mode === "deferred",
|
|
228
|
+
)
|
|
229
|
+
.map((file) => `- ${file.path} — ${file.when}`);
|
|
230
|
+
|
|
231
|
+
return [
|
|
232
|
+
`Conversation:\n${conversation || "(none)"}`,
|
|
233
|
+
`Work:\n${formatList(params.work)}`,
|
|
234
|
+
`Facts:\n${formatList(params.facts)}`,
|
|
235
|
+
`Decisions:\n${formatList(params.decisions)}`,
|
|
236
|
+
`Continue:\n${params.continue}`,
|
|
237
|
+
`Deferred files:\n${deferred.length ? deferred.join("\n") : "(none)"}`,
|
|
238
|
+
].join("\n\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatContinuationMessage(continueNudge: string): string {
|
|
242
|
+
return formatCheckpointMessage("continuation", `${CONTINUATION_PREAMBLE}\n\n${continueNudge}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildFileInjectionRequest(
|
|
246
|
+
cwd: string,
|
|
247
|
+
batchId: string,
|
|
248
|
+
files: CheckpointInput["files"],
|
|
249
|
+
signal: AbortSignal | undefined,
|
|
250
|
+
) {
|
|
251
|
+
const activeFiles: FileInjectionFile[] = [];
|
|
252
|
+
for (const file of files) {
|
|
253
|
+
if (file.mode === "deferred") continue;
|
|
254
|
+
if (file.mode === "outline") {
|
|
255
|
+
activeFiles.push({ path: file.path, mode: "outline" });
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
activeFiles.push({
|
|
259
|
+
path: file.path,
|
|
260
|
+
mode: "full",
|
|
261
|
+
...(file.ranges === undefined ? {} : { ranges: file.ranges }),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return { cwd, source: "checkpoint", batchId, files: activeFiles, signal };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function formatList(items: readonly string[]): string {
|
|
268
|
+
return items.length ? items.map((item) => `- ${item}`).join("\n") : "(none)";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isConversationEntry(entry: SessionEntry): entry is SessionMessageEntry {
|
|
272
|
+
return entry.type === "message" && (entry.message.role === "user" || entry.message.role === "assistant");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isUserMessageEntry(entry: SessionEntry): entry is SessionMessageEntry {
|
|
276
|
+
return entry.type === "message" && entry.message.role === "user";
|
|
277
|
+
}
|