@shanepadgett/tau-agent 0.16.0 → 0.18.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 +25 -6
- package/extensions/attention/README.md +1 -0
- package/extensions/attention/index.ts +38 -0
- package/extensions/context/README.md +13 -3
- package/extensions/context/definitions.ts +3 -15
- package/extensions/context/evidence.ts +516 -0
- package/extensions/context/index.ts +114 -116
- package/extensions/context/panel.ts +60 -0
- package/extensions/context/settings.ts +30 -1
- package/extensions/context/sync.ts +171 -686
- package/extensions/context/validation.ts +4 -1
- package/extensions/context/write-scope.ts +109 -0
- package/extensions/context-pruning/README.md +22 -0
- package/extensions/context-pruning/file-evidence.ts +265 -0
- package/extensions/context-pruning/index.ts +379 -0
- package/extensions/context-pruning/projection.ts +108 -0
- package/extensions/context-pruning/prune.ts +346 -0
- package/extensions/context-pruning/render.ts +179 -0
- package/extensions/context-pruning/settings.ts +41 -0
- package/extensions/explore/README.md +3 -1
- package/extensions/explore/autoread.ts +85 -22
- package/extensions/explore/full-file-knowledge.ts +234 -0
- package/extensions/explore/index.ts +2 -3
- package/extensions/explore/read-cache.ts +150 -86
- package/extensions/explore/read-snapshots.ts +17 -4
- package/extensions/explore/read.ts +57 -38
- package/extensions/footer/index.ts +62 -60
- package/extensions/run-summary/index.ts +5 -5
- package/extensions/silent-command-runner/README.md +1 -1
- package/extensions/silent-command-runner/index.ts +45 -28
- package/extensions/soul/prompt.ts +3 -1
- package/extensions/subagent/README.md +21 -5
- package/extensions/subagent/agents/context-sync.md +97 -0
- package/extensions/subagent/agents/{generalist.md → dormant/generalist.md} +6 -0
- package/extensions/subagent/agents/{scout.md → dormant/scout.md} +6 -0
- package/extensions/subagent/agents/review.md +48 -0
- package/extensions/subagent/agents/web-research.md +6 -0
- package/extensions/subagent/agents.ts +15 -2
- package/extensions/subagent/cmux-dashboard.ts +454 -0
- package/extensions/subagent/index.ts +181 -234
- package/extensions/subagent/render.ts +1 -1
- package/extensions/subagent/resume.ts +78 -0
- package/extensions/subagent/run.ts +213 -118
- package/extensions/subagent/runtime.ts +856 -0
- package/extensions/subagent/session-resource.ts +169 -0
- package/extensions/tau-help/help.md +6 -2
- package/extensions/turn-budget/index.ts +8 -36
- package/package.json +2 -2
- package/schemas/tau.schema.json +48 -1
- package/shared/context-pruning-state.ts +364 -0
- package/shared/events.ts +18 -0
- package/shared/model-fallback/index.ts +21 -10
- package/shared/model-fallback/types.ts +5 -3
- package/shared/settings/load.ts +78 -1
- package/shared/tool-row-state.ts +21 -1
|
@@ -1,34 +1,82 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { Type } from "typebox";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { getKeybindings } from "@earendil-works/pi-tui";
|
|
6
5
|
import { emitTauEvent } from "../../shared/events.ts";
|
|
7
6
|
import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
|
|
8
7
|
import { createInjectedContext } from "../../shared/injected-context.ts";
|
|
9
8
|
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
10
|
-
import { ContextPanel } from "./panel.ts";
|
|
9
|
+
import { ContextPanel, ContextSyncStatusPanel } from "./panel.ts";
|
|
11
10
|
import { findProjectRoot, loadContextEntries, type ContextEntry } from "./definitions.ts";
|
|
11
|
+
import { hideContextSyncEvidenceTool, registerContextSyncEvidenceTool } from "./evidence.ts";
|
|
12
12
|
import contextSettings from "./settings.ts";
|
|
13
|
-
import { runContextSync
|
|
13
|
+
import { runContextSync } from "./sync.ts";
|
|
14
14
|
import { formatContextValidationFailure, validateContextCatalog } from "./validation.ts";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
export default function contextExtension(pi: ExtensionAPI): void {
|
|
17
|
+
let settings = contextSettings.defaults;
|
|
18
|
+
let lastValidationFailure: string | undefined;
|
|
19
|
+
let syncCommandRegistered = false;
|
|
20
|
+
|
|
21
|
+
registerContextSyncEvidenceTool(pi);
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
reason: details.reason,
|
|
23
|
-
changedContextFiles: details.changedContextFiles,
|
|
23
|
+
const refreshSettings = async (ctx: { cwd: string; isProjectTrusted(): boolean }) => {
|
|
24
|
+
settings = await loadTauExtensionSettings(ctx, contextSettings);
|
|
25
|
+
if (settings.sync.enabled) registerContextSyncCommand();
|
|
26
|
+
hideContextSyncEvidenceTool(pi);
|
|
24
27
|
};
|
|
25
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(value) }], details };
|
|
26
|
-
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const registerContextSyncCommand = () => {
|
|
30
|
+
if (syncCommandRegistered) return;
|
|
31
|
+
syncCommandRegistered = true;
|
|
32
|
+
pi.registerCommand("context-sync", {
|
|
33
|
+
description: "Synchronize repository context from current Git changes via context-sync subagent",
|
|
34
|
+
handler: async (args, ctx) => {
|
|
35
|
+
if (!(await loadTauExtensionSettings(ctx, contextSettings)).sync.enabled) {
|
|
36
|
+
ctx.ui.notify("Context sync is disabled in settings", "warning");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
|
|
40
|
+
ctx.ui.notify("/context-sync requires a trusted TUI project", "warning");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await ctx.waitForIdle();
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const widget = showContextSyncWidget(ctx);
|
|
46
|
+
const stopListening = ctx.ui.onTerminalInput((data) => {
|
|
47
|
+
if (!getKeybindings().matches(data, "tui.select.cancel")) return;
|
|
48
|
+
if (!controller.signal.aborted) widget.update("Cancelling context sync");
|
|
49
|
+
controller.abort();
|
|
50
|
+
return { consume: true };
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
const result = await runContextSync(pi, ctx, {
|
|
54
|
+
nudge: args.trim() || undefined,
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
onStatus: (status) => {
|
|
57
|
+
widget.update(status);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (controller.signal.aborted) {
|
|
61
|
+
ctx.ui.notify("Context sync cancelled", "info");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const level = result.outcome === "failed" ? "error" : "info";
|
|
65
|
+
ctx.ui.notify(result.summary, level);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
ctx.ui.notify(
|
|
68
|
+
controller.signal.aborted
|
|
69
|
+
? "Context sync cancelled"
|
|
70
|
+
: `Context sync failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
71
|
+
controller.signal.aborted ? "info" : "error",
|
|
72
|
+
);
|
|
73
|
+
} finally {
|
|
74
|
+
stopListening();
|
|
75
|
+
widget.clear();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
};
|
|
32
80
|
|
|
33
81
|
pi.registerCommand("context", {
|
|
34
82
|
description: "Select repository context entries and inject their files",
|
|
@@ -85,98 +133,14 @@ export default function contextExtension(pi: ExtensionAPI): void {
|
|
|
85
133
|
},
|
|
86
134
|
});
|
|
87
135
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
handler: async (args, ctx) => {
|
|
91
|
-
if (args.trim()) {
|
|
92
|
-
ctx.ui.notify("Usage: /context-sync", "warning");
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
|
|
96
|
-
ctx.ui.notify("/context-sync requires a trusted TUI project", "warning");
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
await ctx.waitForIdle();
|
|
100
|
-
ctx.ui.setStatus("context-sync", "synchronizing context");
|
|
101
|
-
try {
|
|
102
|
-
const result = await runContextSync(pi, ctx);
|
|
103
|
-
ctx.ui.notify(result.summary, "info");
|
|
104
|
-
} catch (error) {
|
|
105
|
-
ctx.ui.notify(`Context sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
106
|
-
} finally {
|
|
107
|
-
ctx.ui.setStatus("context-sync", undefined);
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
pi.registerTool(
|
|
113
|
-
defineTool<typeof contextSyncParams, ContextSyncDetails | undefined>({
|
|
114
|
-
name: "context_sync",
|
|
115
|
-
label: "context_sync",
|
|
116
|
-
description:
|
|
117
|
-
"Synchronize repository context membership after context validation reports changed, stale, or unassigned files.",
|
|
118
|
-
parameters: contextSyncParams,
|
|
119
|
-
async execute(id, _params, _signal, onUpdate, ctx) {
|
|
120
|
-
await Promise.all(
|
|
121
|
-
[...activeToolExecutions]
|
|
122
|
-
.filter(([toolCallId, execution]) => toolCallId !== id && execution.toolName !== "context_sync")
|
|
123
|
-
.map(([, execution]) => execution.done),
|
|
124
|
-
);
|
|
125
|
-
return compactResult(
|
|
126
|
-
await runContextSync(pi, ctx, (status) =>
|
|
127
|
-
onUpdate?.({ content: [{ type: "text", text: status }], details: undefined }),
|
|
128
|
-
),
|
|
129
|
-
);
|
|
130
|
-
},
|
|
131
|
-
renderCall(_args, theme, context) {
|
|
132
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
133
|
-
text.setText(theme.fg("toolTitle", "context_sync"));
|
|
134
|
-
return text;
|
|
135
|
-
},
|
|
136
|
-
renderResult(result, options, theme, context) {
|
|
137
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
138
|
-
const details = result.details;
|
|
139
|
-
const output = result.content.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
140
|
-
text.setText(
|
|
141
|
-
options.isPartial
|
|
142
|
-
? theme.fg("dim", output)
|
|
143
|
-
: context.expanded && details
|
|
144
|
-
? [
|
|
145
|
-
details.summary,
|
|
146
|
-
details.reason,
|
|
147
|
-
...details.changes.map((change) =>
|
|
148
|
-
change.action === "set-entry"
|
|
149
|
-
? `${change.action} ${change.tab}/${change.concept}/${change.entry}: ${change.files.join(", ")}`
|
|
150
|
-
: `${change.action} ${change.tab}/${change.concept}/${change.entry}`,
|
|
151
|
-
),
|
|
152
|
-
...details.changedContextFiles,
|
|
153
|
-
].join("\n")
|
|
154
|
-
: (details?.summary ?? theme.fg("error", output)),
|
|
155
|
-
);
|
|
156
|
-
return text;
|
|
157
|
-
},
|
|
158
|
-
}),
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
pi.on("tool_execution_start", (event) => {
|
|
162
|
-
let complete = () => {};
|
|
163
|
-
const done = new Promise<void>((resolve) => {
|
|
164
|
-
complete = resolve;
|
|
165
|
-
});
|
|
166
|
-
activeToolExecutions.set(event.toolCallId, { toolName: event.toolName, done, complete });
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
pi.on("tool_execution_end", (event) => {
|
|
170
|
-
const execution = activeToolExecutions.get(event.toolCallId);
|
|
171
|
-
activeToolExecutions.delete(event.toolCallId);
|
|
172
|
-
execution?.complete();
|
|
173
|
-
});
|
|
136
|
+
// Default on: register immediately so /context-sync exists before session_start in tests and early UI.
|
|
137
|
+
if (settings.sync.enabled) registerContextSyncCommand();
|
|
174
138
|
|
|
175
139
|
pi.on("session_start", async (_event, ctx) => {
|
|
176
|
-
|
|
140
|
+
await refreshSettings(ctx);
|
|
177
141
|
});
|
|
178
142
|
pi.on("agent_start", async (_event, ctx) => {
|
|
179
|
-
|
|
143
|
+
await refreshSettings(ctx);
|
|
180
144
|
});
|
|
181
145
|
pi.on("agent_end", async (event, ctx) => {
|
|
182
146
|
if (
|
|
@@ -191,7 +155,8 @@ export default function contextExtension(pi: ExtensionAPI): void {
|
|
|
191
155
|
)
|
|
192
156
|
)
|
|
193
157
|
return;
|
|
194
|
-
|
|
158
|
+
settings = await loadTauExtensionSettings(ctx, contextSettings);
|
|
159
|
+
if (!settings.sync.enabled || !settings.validation.enabled || !ctx.isProjectTrusted()) {
|
|
195
160
|
lastValidationFailure = undefined;
|
|
196
161
|
return;
|
|
197
162
|
}
|
|
@@ -200,25 +165,58 @@ export default function contextExtension(pi: ExtensionAPI): void {
|
|
|
200
165
|
const git = createGitRunner(pi, ctx);
|
|
201
166
|
if (!(await loadRepoStatus(git))) return;
|
|
202
167
|
const failure = formatContextValidationFailure(
|
|
203
|
-
await validateContextCatalog(git, root,
|
|
168
|
+
await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
|
|
204
169
|
);
|
|
205
170
|
if (!failure) {
|
|
206
171
|
lastValidationFailure = undefined;
|
|
207
172
|
return;
|
|
208
173
|
}
|
|
174
|
+
// Same unresolved failure fingerprint: do not re-spawn until the dirty/catalog signal changes.
|
|
209
175
|
if (failure === lastValidationFailure) return;
|
|
210
176
|
lastValidationFailure = failure;
|
|
211
|
-
ctx.ui.notify("Context catalog validation failed", "error");
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
177
|
+
ctx.ui.notify("Context catalog validation failed; running context-sync", "error");
|
|
178
|
+
const widget = showContextSyncWidget(ctx);
|
|
179
|
+
try {
|
|
180
|
+
const result = await runContextSync(pi, ctx, {
|
|
181
|
+
onStatus: (status) => {
|
|
182
|
+
widget.update(status);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const afterFailure = formatContextValidationFailure(
|
|
186
|
+
await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
|
|
187
|
+
);
|
|
188
|
+
if (result.outcome === "failed" || afterFailure) {
|
|
189
|
+
lastValidationFailure = afterFailure ?? `${failure}\n${result.reason}`;
|
|
190
|
+
ctx.ui.notify(
|
|
191
|
+
result.outcome === "failed" ? result.summary : "Context catalog still invalid after context-sync",
|
|
192
|
+
"error",
|
|
193
|
+
);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
lastValidationFailure = undefined;
|
|
197
|
+
ctx.ui.notify(result.summary, "info");
|
|
198
|
+
} finally {
|
|
199
|
+
widget.clear();
|
|
200
|
+
}
|
|
216
201
|
} catch (error) {
|
|
217
202
|
ctx.ui.notify(`Context validation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
218
203
|
}
|
|
219
204
|
});
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function showContextSyncWidget(ctx: ExtensionContext): { update(status: string): void; clear(): void } {
|
|
208
|
+
let widget: ContextSyncStatusPanel | undefined;
|
|
209
|
+
ctx.ui.setWidget("context-sync", (tui, theme) => {
|
|
210
|
+
widget = new ContextSyncStatusPanel(tui, theme, "Synchronizing repository context");
|
|
211
|
+
return widget;
|
|
223
212
|
});
|
|
213
|
+
return {
|
|
214
|
+
update(status) {
|
|
215
|
+
widget?.update(status);
|
|
216
|
+
},
|
|
217
|
+
clear() {
|
|
218
|
+
ctx.ui.setWidget("context-sync", undefined);
|
|
219
|
+
widget = undefined;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
224
222
|
}
|
|
@@ -187,3 +187,63 @@ export class ContextPanel implements Component {
|
|
|
187
187
|
this.tui.requestRender();
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
+
|
|
191
|
+
export class ContextSyncStatusPanel implements Component {
|
|
192
|
+
private readonly tui: TUI;
|
|
193
|
+
private readonly panel: ToolPanel;
|
|
194
|
+
private readonly config: ToolPanelConfig;
|
|
195
|
+
private readonly startedAt = Date.now();
|
|
196
|
+
private readonly timer: ReturnType<typeof setInterval>;
|
|
197
|
+
private lines: string[] = [];
|
|
198
|
+
private lineCount = 0;
|
|
199
|
+
|
|
200
|
+
constructor(tui: TUI, theme: Theme, status: string) {
|
|
201
|
+
this.tui = tui;
|
|
202
|
+
this.config = {
|
|
203
|
+
title: "Context sync",
|
|
204
|
+
body: {
|
|
205
|
+
render: (width) => {
|
|
206
|
+
const visible =
|
|
207
|
+
this.lineCount > 5
|
|
208
|
+
? [`${this.lineCount - 4} earlier lines omitted`, ...this.lines.slice(-4)]
|
|
209
|
+
: this.lines;
|
|
210
|
+
return visible.map((line, index) =>
|
|
211
|
+
truncateToWidth(theme.fg(index === 0 && this.lineCount > 5 ? "dim" : "muted", line), width, "…"),
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
invalidate: () => {},
|
|
215
|
+
},
|
|
216
|
+
footer: { kind: "hints", hints: [] },
|
|
217
|
+
border: "horizontal",
|
|
218
|
+
};
|
|
219
|
+
this.panel = new ToolPanel(theme, this.config);
|
|
220
|
+
this.update(status);
|
|
221
|
+
this.timer = setInterval(() => this.tui.requestRender(), 1_000);
|
|
222
|
+
this.timer.unref();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
update(status: string): void {
|
|
226
|
+
const added = status
|
|
227
|
+
.split(/\r?\n/)
|
|
228
|
+
.map((line) => line.trim())
|
|
229
|
+
.filter(Boolean);
|
|
230
|
+
this.lineCount += added.length;
|
|
231
|
+
this.lines = [...this.lines, ...added].slice(-5);
|
|
232
|
+
this.tui.requestRender();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
render(width: number): string[] {
|
|
236
|
+
const elapsed = Math.floor((Date.now() - this.startedAt) / 1_000);
|
|
237
|
+
this.config.title =
|
|
238
|
+
elapsed < 60 ? `Context sync · ${elapsed}s` : `Context sync · ${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
|
|
239
|
+
return this.panel.render(width);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
invalidate(): void {
|
|
243
|
+
this.panel.invalidate();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
dispose(): void {
|
|
247
|
+
clearInterval(this.timer);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -4,6 +4,10 @@ import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
|
|
|
4
4
|
export default defineTauExtensionSettings({
|
|
5
5
|
key: "context",
|
|
6
6
|
defaults: {
|
|
7
|
+
sync: {
|
|
8
|
+
enabled: true as boolean,
|
|
9
|
+
automation: true as boolean,
|
|
10
|
+
},
|
|
7
11
|
validation: {
|
|
8
12
|
enabled: false as boolean,
|
|
9
13
|
ignoreGlobs: [] as string[],
|
|
@@ -11,11 +15,36 @@ export default defineTauExtensionSettings({
|
|
|
11
15
|
},
|
|
12
16
|
schema: Type.Object(
|
|
13
17
|
{
|
|
18
|
+
sync: Type.Optional(
|
|
19
|
+
Type.Object(
|
|
20
|
+
{
|
|
21
|
+
enabled: Type.Optional(
|
|
22
|
+
Type.Boolean({
|
|
23
|
+
default: true,
|
|
24
|
+
description:
|
|
25
|
+
"Master switch for context-sync. When false: no /context-sync, parent cannot call the agent, validation does not auto-run sync.",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
automation: Type.Optional(
|
|
29
|
+
Type.Boolean({
|
|
30
|
+
default: true,
|
|
31
|
+
description:
|
|
32
|
+
"When true with sync.enabled, the coding agent may call context-sync. When false, only manual /context-sync (validation auto-run still allowed if validation.enabled).",
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
{ additionalProperties: false },
|
|
37
|
+
),
|
|
38
|
+
),
|
|
14
39
|
validation: Type.Optional(
|
|
15
40
|
Type.Object(
|
|
16
41
|
{
|
|
17
42
|
enabled: Type.Optional(
|
|
18
|
-
Type.Boolean({
|
|
43
|
+
Type.Boolean({
|
|
44
|
+
default: false,
|
|
45
|
+
description:
|
|
46
|
+
"Validate context membership after agent turns and auto-run context-sync on failure (requires sync.enabled).",
|
|
47
|
+
}),
|
|
19
48
|
),
|
|
20
49
|
ignoreGlobs: Type.Optional(
|
|
21
50
|
Type.Array(Type.String(), {
|