@shanepadgett/tau-agent 0.22.0 → 0.23.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.
@@ -11,7 +11,7 @@ Before calling the tool, the agent writes visible prose containing the durable c
11
11
  The tool accepts three required lists:
12
12
 
13
13
  - `keepToolCalls` retains exact earlier tool exchanges by tool-call ID. Parallel calls remain independently selectable: retaining one call does not retain its siblings.
14
- - `keepFiles` reads each selected file from disk and carries its complete current contents forward as a fresh autoread snapshot. It does not require an earlier complete read.
14
+ - `keepFiles` reads each selected file from disk and carries its complete current contents forward as a fresh autoread snapshot. Each successful snapshot appears as its own autoread marker below the compact checkpoint result. It does not require an earlier complete read.
15
15
  - `deferFiles` carries forward a short advisory note explaining why a file is irrelevant now and when to reconsider it.
16
16
 
17
17
  Duplicate selections are collapsed. A file selected in both `keepFiles` and `deferFiles` is kept. Missing, unreadable, or otherwise unsnapshotable files produce warnings in the successful tool result; they do not block the checkpoint or other file snapshots.
@@ -117,7 +117,7 @@ export default function contextPruningExtension(pi: ExtensionAPI): void {
117
117
  parameters: contextPruneParameters,
118
118
  executionMode: "sequential",
119
119
  async execute(toolCallId, params, signal, _onUpdate, executionContext) {
120
- return executeContextPrune({
120
+ const execution = await executeContextPrune({
121
121
  toolCallId,
122
122
  params,
123
123
  signal,
@@ -125,6 +125,10 @@ export default function contextPruningExtension(pi: ExtensionAPI): void {
125
125
  generation: lifecycleGeneration,
126
126
  currentGeneration: () => lifecycleGeneration,
127
127
  });
128
+ for (const autoread of execution.autoreads) {
129
+ pi.sendMessage(autoread, { deliverAs: "steer" });
130
+ }
131
+ return execution.result;
128
132
  },
129
133
  renderCall(args, theme, context) {
130
134
  return renderContextPruneCall(args, theme, {
@@ -34,11 +34,16 @@ interface ContextPruneExecutionOptions {
34
34
  currentGeneration: () => number;
35
35
  }
36
36
 
37
- interface ContextPruneExecutionResult {
37
+ interface ContextPruneToolResult {
38
38
  content: Array<{ type: "text"; text: string }>;
39
39
  details: ContextPruneDetailsV2;
40
40
  }
41
41
 
42
+ interface ContextPruneExecutionResult {
43
+ result: ContextPruneToolResult;
44
+ autoreads: PreparedAutoreadMessage[];
45
+ }
46
+
42
47
  export async function executeContextPrune(options: ContextPruneExecutionOptions): Promise<ContextPruneExecutionResult> {
43
48
  assertCurrent(options);
44
49
  const retainedToolCallIds = [...new Set(options.params.keepToolCalls.map((selection) => selection.toolCallId))];
@@ -91,8 +96,6 @@ export async function executeContextPrune(options: ContextPruneExecutionOptions)
91
96
  const refreshedFiles = preparedSnapshots.map((snapshot) => ({
92
97
  path: snapshot.details.path,
93
98
  rowId: snapshot.details.rowId,
94
- servedHash: snapshot.details.readCache.servedHash,
95
- autoreadDetails: { ...snapshot.details },
96
99
  }));
97
100
  const details: ContextPruneDetailsV2 = {
98
101
  v: 2,
@@ -110,12 +113,14 @@ export async function executeContextPrune(options: ContextPruneExecutionOptions)
110
113
  ? "Context checkpoint applied. Continue with the next action stated before this call."
111
114
  : `Context checkpoint applied with ${warnings.length} warning${warnings.length === 1 ? "" : "s"}:\n${warnings.map((warning) => `- ${warning}`).join("\n")}\nContinue with the next action stated before this call.`;
112
115
  return {
113
- content: [
114
- { type: "text", text: status },
115
- ...preparedSnapshots.map((snapshot) => ({ type: "text" as const, text: snapshot.content })),
116
- ...(deferredFiles.length === 0 ? [] : [{ type: "text" as const, text: deferredFileText(deferredFiles) }]),
117
- ],
118
- details,
116
+ result: {
117
+ content: [
118
+ { type: "text", text: status },
119
+ ...(deferredFiles.length === 0 ? [] : [{ type: "text" as const, text: deferredFileText(deferredFiles) }]),
120
+ ],
121
+ details,
122
+ },
123
+ autoreads: preparedSnapshots,
119
124
  };
120
125
  }
121
126
 
@@ -128,30 +128,25 @@ export function replayReadCache(
128
128
  if (entry.type === "message" && "message" in entry) message = entry.message;
129
129
  else if (entry.type === "custom_message") message = entry;
130
130
  else continue;
131
- const candidates = carriedFileMessages(message);
132
- let acceptedCarryForward = false;
133
- for (const candidate of candidates ?? [message]) {
134
- const parsedMeta = readMetaFromMessage(candidate);
135
- if (!parsedMeta) continue;
136
- acceptedCarryForward = true;
137
- const candidateRowId = readRowId(candidate);
138
- if (candidateRowId && ignoredRowIds.has(candidateRowId)) continue;
131
+ const parsedMeta = readMetaFromMessage(message);
132
+ if (parsedMeta) {
133
+ const rowId = readRowId(message);
134
+ if (!rowId || ignoredRowIds.has(rowId)) continue;
139
135
  const pathKey = resolve(cwd, parsedMeta.pathKey);
140
136
  const meta = pathKey === parsedMeta.pathKey ? parsedMeta : { ...parsedMeta, pathKey };
141
- if (candidateRowId && applyReadMeta(trust, failedPatchRecoveryPaths, candidate, meta, candidateRowId)) {
137
+ if (applyReadMeta(trust, failedPatchRecoveryPaths, message, meta, rowId)) {
142
138
  const acceptedTrust = trust.get(meta.pathKey)?.get(meta.scopeKey);
143
- if (acceptedTrust) {
139
+ if (acceptedTrust)
144
140
  acceptedRows.push({
145
- rowId: candidateRowId,
141
+ rowId,
146
142
  pathKey: meta.pathKey,
147
143
  scopeKey: meta.scopeKey,
148
144
  meta,
149
145
  dependencyRowIds: [...acceptedTrust.rowIds],
150
146
  });
151
- }
152
147
  }
148
+ continue;
153
149
  }
154
- if (acceptedCarryForward) continue;
155
150
  for (const path of failedPatchPaths(message, cwd)) failedPatchRecoveryPaths.add(path);
156
151
  }
157
152
  const completeFileChains = new Map<string, CompleteFileDependencyChain>();
@@ -168,25 +163,6 @@ export function replayReadCache(
168
163
  return { scopeTrust: trust, acceptedRows, completeFileChains, failedPatchRecoveryPaths };
169
164
  }
170
165
 
171
- function carriedFileMessages(message: unknown): unknown[] | undefined {
172
- if (!isRecord(message) || message.role !== "toolResult" || message.toolName !== "context_prune") return undefined;
173
- if (!isRecord(message.details) || message.details.v !== 2 || !Array.isArray(message.details.refreshedFiles)) {
174
- return undefined;
175
- }
176
- if (!Array.isArray(message.content)) {
177
- return undefined;
178
- }
179
- const messages: unknown[] = [];
180
- for (let index = 0; index < message.details.refreshedFiles.length; index += 1) {
181
- const file = message.details.refreshedFiles[index];
182
- const part = message.content[index + 1];
183
- if (!isRecord(file) || !isRecord(file.autoreadDetails)) continue;
184
- if (!isRecord(part) || part.type !== "text" || typeof part.text !== "string") continue;
185
- messages.push({ customType: "tau.autoread", content: part.text, details: file.autoreadDetails });
186
- }
187
- return messages;
188
- }
189
-
190
166
  function applyReadMeta(
191
167
  trust: Map<string, Map<string, ReadCacheScopeTrust>>,
192
168
  failedPatchRecoveryPaths: Set<string>,
@@ -71,54 +71,82 @@ function cap(text: string | undefined, limit: number): string {
71
71
  return `${normalized.slice(0, limit - 1)}…`;
72
72
  }
73
73
 
74
+ function tableCell(text: string, limit: number): string {
75
+ const compact = cap(text.replace(/\s+/g, " ").trim(), limit);
76
+ return compact.replaceAll("|", "&#124;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
77
+ }
78
+
79
+ function quote(text: string): string {
80
+ return cap(text, 1200)
81
+ .replaceAll("<", "&lt;")
82
+ .replaceAll(">", "&gt;")
83
+ .split("\n")
84
+ .map((line) => `> ${line}`)
85
+ .join("\n");
86
+ }
87
+
88
+ function elapsed(durationMs: number): string {
89
+ if (durationMs < 1000) return "<1s";
90
+ if (durationMs < 60_000) return `${(durationMs / 1000).toFixed(1)}s`;
91
+ const minutes = Math.floor(durationMs / 60_000);
92
+ const seconds = Math.floor((durationMs % 60_000) / 1000);
93
+ return `${minutes}m ${seconds}s`;
94
+ }
95
+
96
+ function dashboardState(status: SubagentInvocationSnapshot["status"]): string {
97
+ switch (status) {
98
+ case "waiting":
99
+ return "queued";
100
+ case "completed":
101
+ return "done";
102
+ case "aborted":
103
+ return "stopped";
104
+ default:
105
+ return status;
106
+ }
107
+ }
108
+
74
109
  export function formatDashboardMarkdown(snapshots: readonly SubagentInvocationSnapshot[]): string {
75
110
  const ordered = [...snapshots].sort(
76
111
  (a, b) => a.startedAt - b.startedAt || a.invocationId.localeCompare(b.invocationId),
77
112
  );
78
- const activeCount = ordered.filter(
79
- (snapshot) => snapshot.status !== "completed" && snapshot.status !== "failed" && snapshot.status !== "aborted",
113
+ const activeCount = ordered.filter((snapshot) =>
114
+ ["waiting", "starting", "running"].includes(snapshot.status),
80
115
  ).length;
81
- const lines = [
82
- "# Subagent dashboard",
83
- "",
84
- `Active: ${activeCount} · Completed: ${ordered.length - activeCount}`,
85
- "",
86
- ];
116
+ const doneCount = ordered.filter((snapshot) => snapshot.status === "completed").length;
117
+ const failedCount = ordered.filter((snapshot) => snapshot.status === "failed").length;
118
+ const stoppedCount = ordered.filter((snapshot) => snapshot.status === "aborted").length;
119
+ const counts = [`${activeCount} active`, `${doneCount} done`];
120
+ if (failedCount > 0) counts.push(`${failedCount} failed`);
121
+ if (stoppedCount > 0) counts.push(`${stoppedCount} stopped`);
122
+ const lines = ["# Subagents", "", counts.join(" · "), ""];
87
123
  if (ordered.length === 0) {
88
- lines.push("_No active invocations._", "");
124
+ lines.push("_No subagents._", "");
89
125
  return lines.join("\n");
90
126
  }
127
+ lines.push("| Agent | State | Request | Last tool | Calls | Time |", "| --- | --- | --- | --- | ---: | ---: |");
91
128
  for (const details of ordered) {
92
- const identity = `${details.displayName} (${details.agent})`;
129
+ const latest = details.actions.at(-1);
130
+ const currentTool = details.currentActivity?.match(/^\S+/)?.[0];
131
+ const lastTool = currentTool ?? latest?.tool ?? "";
93
132
  lines.push(
94
- `## ${identity}`,
95
- "",
96
- `\`${details.status}${details.phase ? ` · ${details.phase}` : ""}\` · inv ${details.invocationId} · tools ${details.toolCalls} · ${(details.durationMs / 1000).toFixed(1)}s`,
97
- `model ${details.model} · thinking ${details.thinkingLevel}`,
133
+ `| ${tableCell(`${details.displayName} (${details.agent})`, 48)} | ${dashboardState(details.status)} | ${tableCell(details.task, 90)} | ${tableCell(lastTool, 32) || "—"} | ${details.toolCalls} | ${elapsed(details.durationMs)} |`,
134
+ );
135
+ }
136
+ lines.push("", "## Inputs", "");
137
+ for (const details of ordered) {
138
+ lines.push(
139
+ `### ${tableCell(details.displayName, 80)}`,
98
140
  "",
99
- "### Task",
141
+ `${tableCell(details.agent, 80)} · ${tableCell(details.invocationId, 80)}`,
100
142
  "",
101
- cap(details.task, 1200) || "_(empty)_",
143
+ quote(details.task) || "> _(empty)_",
102
144
  );
103
- if (details.currentActivity) {
104
- lines.push("", "### Current", "", cap(details.currentActivity, 400));
105
- }
106
- if (details.actions.length > 0) {
107
- lines.push("", "### Actions", "");
108
- for (const action of details.actions.slice(-12)) {
109
- const mark = action.error ? "!" : "·";
110
- lines.push(`- ${mark} ${cap(action.summary, 200)}`);
111
- }
112
- if (details.omittedActions > 0) {
113
- lines.push(`- _${details.omittedActions} earlier actions omitted_`);
114
- }
115
- }
116
- if (details.response) {
117
- lines.push("", "### Response", "", cap(details.response, 3000));
118
- }
119
- if (details.error) {
120
- lines.push("", "### Error", "", cap(details.error, 800));
145
+ if (details.files.length > 0) {
146
+ lines.push("", "**Files**", "");
147
+ for (const file of details.files) lines.push(`- ${tableCell(file, 300)}`);
121
148
  }
149
+ if (details.error) lines.push("", "**Error**", "", quote(details.error));
122
150
  lines.push("");
123
151
  }
124
152
  return lines.join("\n");
@@ -77,6 +77,7 @@ export interface SubagentDetails {
77
77
  export interface SubagentInvocationSnapshot extends SubagentDetails {
78
78
  invocationId: string;
79
79
  startedAt: number;
80
+ files: readonly string[];
80
81
  }
81
82
 
82
83
  export interface SubagentThread {
@@ -243,6 +243,7 @@ export class SubagentRuntime {
243
243
  }),
244
244
  invocationId,
245
245
  startedAt,
246
+ files: [...(options.files ?? [])],
246
247
  };
247
248
  const active: ActiveInvocation = {
248
249
  snapshot: initialSnapshot,
@@ -330,6 +331,7 @@ export class SubagentRuntime {
330
331
  ...details,
331
332
  invocationId,
332
333
  startedAt,
334
+ files: current.snapshot.files,
333
335
  threadId: details.threadId ?? threadId,
334
336
  agent: details.agent || agent,
335
337
  displayName: details.displayName || displayName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "README.md"
35
35
  ],
36
36
  "dependencies": {
37
- "@shanepadgett/tau-tui": "0.22.0",
37
+ "@shanepadgett/tau-tui": "0.23.0",
38
38
  "@toon-format/toon": "2.3.0",
39
39
  "image-size": "2.0.2",
40
40
  "smol-toml": "1.7.0"
@@ -5,8 +5,6 @@ const CONTEXT_PRUNE_TOOL = "context_prune";
5
5
  export interface ContextPruneRefreshedFileV2 {
6
6
  path: string;
7
7
  rowId: string;
8
- servedHash: string;
9
- autoreadDetails: Record<string, unknown>;
10
8
  }
11
9
 
12
10
  export interface ContextPruneDeferredFileV2 {
@@ -116,13 +114,7 @@ function parseRefreshedFiles(value: unknown): ContextPruneRefreshedFileV2[] | un
116
114
  const files: ContextPruneRefreshedFileV2[] = [];
117
115
  for (const item of value) {
118
116
  if (!isRecord(item) || !isNonEmptyString(item.path) || !isNonEmptyString(item.rowId)) return undefined;
119
- if (!isNonEmptyString(item.servedHash) || !isRecord(item.autoreadDetails)) return undefined;
120
- files.push({
121
- path: item.path,
122
- rowId: item.rowId,
123
- servedHash: item.servedHash,
124
- autoreadDetails: { ...item.autoreadDetails },
125
- });
117
+ files.push({ path: item.path, rowId: item.rowId });
126
118
  }
127
119
  return files;
128
120
  }