@shanepadgett/tau-agent 0.16.0 → 0.17.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 +90 -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,66 +1,109 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
1
2
|
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Type } from "typebox";
|
|
4
|
+
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
3
5
|
import { createToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
4
|
-
import
|
|
6
|
+
import contextSettings from "../context/settings.ts";
|
|
7
|
+
import { discoverAgents, type AgentDiscovery } from "./agents.ts";
|
|
8
|
+
import { createCmuxDashboard, type CmuxDashboard, type DashboardOrphan } from "./cmux-dashboard.ts";
|
|
5
9
|
import { renderSubagentCall, renderSubagentResult } from "./render.ts";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
disposeSubagentThread,
|
|
9
|
-
extensionPathsForTools,
|
|
10
|
-
FifoGate,
|
|
11
|
-
runSubagentTurn,
|
|
12
|
-
type SubagentDetails,
|
|
13
|
-
type SubagentThread,
|
|
14
|
-
} from "./run.ts";
|
|
10
|
+
import type { SubagentDetails } from "./run.ts";
|
|
11
|
+
import { failedToolResult, SubagentRuntime } from "./runtime.ts";
|
|
15
12
|
|
|
16
13
|
const params = Type.Union([
|
|
17
14
|
Type.Object(
|
|
18
|
-
{
|
|
15
|
+
{
|
|
16
|
+
agent: Type.String({ minLength: 1 }),
|
|
17
|
+
task: Type.String({ minLength: 1 }),
|
|
18
|
+
files: Type.Optional(
|
|
19
|
+
Type.Array(Type.String({ minLength: 1 }), {
|
|
20
|
+
description: "Files to autoread into the child's context before this turn",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
},
|
|
19
24
|
{ additionalProperties: false },
|
|
20
25
|
),
|
|
21
26
|
Type.Object(
|
|
22
|
-
{
|
|
27
|
+
{
|
|
28
|
+
thread: Type.String({ minLength: 1 }),
|
|
29
|
+
task: Type.String({ minLength: 1 }),
|
|
30
|
+
files: Type.Optional(
|
|
31
|
+
Type.Array(Type.String({ minLength: 1 }), {
|
|
32
|
+
description: "Files to autoread into the child's context before this turn",
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
},
|
|
23
36
|
{ additionalProperties: false },
|
|
24
37
|
),
|
|
25
38
|
]);
|
|
26
39
|
|
|
27
|
-
const MAX_RETAINED_THREADS = 16;
|
|
28
|
-
|
|
29
|
-
function emptyDetails(
|
|
30
|
-
agent: string,
|
|
31
|
-
task: string,
|
|
32
|
-
status: "waiting" | "failed" | "aborted",
|
|
33
|
-
phase: "discovery" | "queue" | "startup",
|
|
34
|
-
model: string,
|
|
35
|
-
thinkingLevel: string,
|
|
36
|
-
threadId: string | undefined,
|
|
37
|
-
error: string | undefined,
|
|
38
|
-
): SubagentDetails {
|
|
39
|
-
return {
|
|
40
|
-
agent,
|
|
41
|
-
...(threadId === undefined ? {} : { threadId }),
|
|
42
|
-
status,
|
|
43
|
-
phase,
|
|
44
|
-
task,
|
|
45
|
-
model,
|
|
46
|
-
thinkingLevel,
|
|
47
|
-
toolCalls: 0,
|
|
48
|
-
actions: [],
|
|
49
|
-
omittedActions: 0,
|
|
50
|
-
omittedErrors: 0,
|
|
51
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
52
|
-
durationMs: 0,
|
|
53
|
-
...(error === undefined ? {} : { error }),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
40
|
export default function subagentExtension(pi: ExtensionAPI): void {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const
|
|
41
|
+
const runtime = new SubagentRuntime(pi);
|
|
42
|
+
let failureNotify: ((message: string) => void) | undefined;
|
|
43
|
+
const orphans: DashboardOrphan[] = [];
|
|
44
|
+
const retryOrphans = async () => {
|
|
45
|
+
if (orphans.length === 0) return;
|
|
46
|
+
const pending = orphans.splice(0, orphans.length);
|
|
47
|
+
for (const orphan of pending) {
|
|
48
|
+
if (orphan.surfaceId && orphan.workspaceId) {
|
|
49
|
+
const result = await pi
|
|
50
|
+
.exec(
|
|
51
|
+
"cmux",
|
|
52
|
+
[
|
|
53
|
+
"--json",
|
|
54
|
+
"--id-format",
|
|
55
|
+
"both",
|
|
56
|
+
"rpc",
|
|
57
|
+
"surface.close",
|
|
58
|
+
JSON.stringify({ workspace_id: orphan.workspaceId, surface_id: orphan.surfaceId }),
|
|
59
|
+
],
|
|
60
|
+
{ timeout: 2500 },
|
|
61
|
+
)
|
|
62
|
+
.catch(() => ({ stdout: "", stderr: "close failed", code: 1, killed: false }));
|
|
63
|
+
const closed =
|
|
64
|
+
(result.code === 0 && !result.killed) ||
|
|
65
|
+
/not found|unknown surface|no such/i.test(`${result.stderr}\n${result.stdout}`);
|
|
66
|
+
if (!closed) {
|
|
67
|
+
orphans.push(orphan);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Known-closed or never had a surface id after ambiguous open: drop directory only when closed/absent.
|
|
72
|
+
// Ambiguous no-id orphans stay on disk; keep tracking so we do not open more without awareness.
|
|
73
|
+
if (orphan.surfaceId) {
|
|
74
|
+
await rm(orphan.directory, { recursive: true, force: true }).catch(() => undefined);
|
|
75
|
+
} else {
|
|
76
|
+
orphans.push(orphan);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const makeDashboard = () =>
|
|
81
|
+
createCmuxDashboard({
|
|
82
|
+
exec: (command, args, options) => pi.exec(command, args, options),
|
|
83
|
+
canOpen: () => orphans.length === 0,
|
|
84
|
+
notify: (message) => {
|
|
85
|
+
try {
|
|
86
|
+
failureNotify?.(message);
|
|
87
|
+
} catch {
|
|
88
|
+
// ignore
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
let dashboard: CmuxDashboard = makeDashboard();
|
|
93
|
+
let unsubscribeDashboard = runtime.subscribe((snapshot) => {
|
|
94
|
+
dashboard.onSnapshot(snapshot);
|
|
95
|
+
});
|
|
96
|
+
const replaceDashboard = async () => {
|
|
97
|
+
unsubscribeDashboard();
|
|
98
|
+
const leftover = await dashboard.shutdown();
|
|
99
|
+
orphans.push(...leftover);
|
|
100
|
+
await retryOrphans();
|
|
101
|
+
dashboard = makeDashboard();
|
|
102
|
+
unsubscribeDashboard = runtime.subscribe((snapshot) => {
|
|
103
|
+
dashboard.onSnapshot(snapshot);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
61
106
|
const fingerprints = new Map<string, string>();
|
|
62
|
-
const runtimeWarnings = new Set<string>();
|
|
63
|
-
let nextThreadId = 1;
|
|
64
107
|
const rowState = createToolRowStateStore(pi, "subagent.tool-row-state");
|
|
65
108
|
const warn = (discovery: AgentDiscovery, ctx: ExtensionContext) => {
|
|
66
109
|
const current = new Set<string>();
|
|
@@ -79,25 +122,23 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
79
122
|
}
|
|
80
123
|
for (const path of fingerprints.keys()) if (!current.has(path)) fingerprints.delete(path);
|
|
81
124
|
};
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
125
|
+
const parentVisibleAgents = async (ctx: ExtensionContext, discovery: AgentDiscovery) => {
|
|
126
|
+
const sync = (await loadTauExtensionSettings(ctx, contextSettings)).sync;
|
|
127
|
+
const parentVisible = sync.enabled && sync.automation;
|
|
128
|
+
return [...discovery.agents.values()]
|
|
129
|
+
.filter((agent) => agent.name !== "context-sync" || parentVisible)
|
|
130
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
86
131
|
};
|
|
132
|
+
|
|
87
133
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
88
134
|
if (!pi.getActiveTools().includes("subagent")) return undefined;
|
|
89
135
|
const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
|
|
90
136
|
warn(discovery, ctx);
|
|
91
|
-
const lines =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.sort((a, b) => a.id.localeCompare(b.id))
|
|
97
|
-
.map((thread) => {
|
|
98
|
-
const task = thread.initialTask.replace(/\s+/g, " ").trim();
|
|
99
|
-
return `- ${thread.id} (${thread.definition.name}): ${task.length <= 160 ? task : `${task.slice(0, 159)}…`}`;
|
|
100
|
-
});
|
|
137
|
+
const lines = (await parentVisibleAgents(ctx, discovery)).map((agent) => `- ${agent.name}: ${agent.description}`);
|
|
138
|
+
const activeThreads = runtime.listThreads(ctx.cwd).map((thread) => {
|
|
139
|
+
const task = thread.initialTask.replace(/\s+/g, " ").trim();
|
|
140
|
+
return `- ${thread.id} (${thread.displayName} · ${thread.definition.name}): ${task.length <= 160 ? task : `${task.slice(0, 159)}…`}`;
|
|
141
|
+
});
|
|
101
142
|
const threadSection = activeThreads.length ? `\n\nActive reusable threads:\n${activeThreads.join("\n")}` : "";
|
|
102
143
|
const prompt = `## Subagents
|
|
103
144
|
Use \`subagent\` when an available agent matches a focused part of the task.
|
|
@@ -107,6 +148,8 @@ ${lines.join("\n")}
|
|
|
107
148
|
|
|
108
149
|
Start a fresh thread with \`agent\` and \`task\`. Continue an existing thread with \`thread\` and \`task\`. Reuse a thread when feedback or follow-up work depends on its prior reads and reasoning. Start fresh for unrelated work or when its context is stale or oversized.${threadSection}
|
|
109
150
|
|
|
151
|
+
Pass \`files\` when exact relevant files are already known. Tau autoreads current line-numbered snapshots into that child turn before it starts.
|
|
152
|
+
|
|
110
153
|
Delegate one focused task per call. Children do not inherit parent messages. Include exact absolute reference paths when a child must inspect a repository outside the current working directory.`;
|
|
111
154
|
return { systemPrompt: `${event.systemPrompt}\n\n${prompt}` };
|
|
112
155
|
});
|
|
@@ -115,182 +158,82 @@ Delegate one focused task per call. Children do not inherit parent messages. Inc
|
|
|
115
158
|
name: "subagent",
|
|
116
159
|
label: "Subagent",
|
|
117
160
|
description:
|
|
118
|
-
"Start a focused isolated child agent or continue a retained child thread. Output is limited to 50KB or 2,000 lines.",
|
|
161
|
+
"Start a focused isolated child agent or continue a retained child thread. Known files can be autoread into the child turn. Output is limited to 50KB or 2,000 lines.",
|
|
119
162
|
promptSnippet: "Start a focused child agent or continue a retained child thread",
|
|
120
163
|
parameters: params,
|
|
121
164
|
executionMode: "parallel",
|
|
122
165
|
async execute(_id, raw, signal, onUpdate, ctx) {
|
|
123
166
|
const task = raw.task.trim();
|
|
167
|
+
const files = [...new Set((raw.files ?? []).map((path) => path.trim()))];
|
|
124
168
|
const continuing = "thread" in raw;
|
|
125
169
|
const parentModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable";
|
|
126
170
|
const parentThinking = pi.getThinkingLevel();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
171
|
+
const agent = continuing ? raw.thread.trim() : raw.agent.trim();
|
|
172
|
+
const threadKey = continuing ? raw.thread.trim() : undefined;
|
|
173
|
+
|
|
174
|
+
failureNotify = (message) => {
|
|
175
|
+
ctx.ui.notify(message, "warning");
|
|
176
|
+
};
|
|
177
|
+
dashboard.setInteractive(ctx.mode === "tui" && ctx.hasUI);
|
|
178
|
+
|
|
179
|
+
if (!task || !agent || files.some((path) => !path)) {
|
|
132
180
|
const error = continuing
|
|
133
|
-
? "Subagent continuation requires non-empty thread and
|
|
134
|
-
: "Subagent input requires non-empty agent and
|
|
135
|
-
return
|
|
136
|
-
content: [{ type: "text", text: error }],
|
|
137
|
-
details: emptyDetails(agent, task, "failed", "queue", parentModel, parentThinking, threadId, error),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
if (continuing) {
|
|
141
|
-
thread = threads.get(agent);
|
|
142
|
-
if (!thread || thread.cwd !== ctx.cwd) {
|
|
143
|
-
const names =
|
|
144
|
-
[...threads.values()]
|
|
145
|
-
.filter((item) => item.cwd === ctx.cwd)
|
|
146
|
-
.map((item) => item.id)
|
|
147
|
-
.sort()
|
|
148
|
-
.join(", ") || "none";
|
|
149
|
-
const error = `Subagent thread ${agent} is unavailable. Active threads: ${names}`;
|
|
150
|
-
return {
|
|
151
|
-
content: [{ type: "text", text: error }],
|
|
152
|
-
details: emptyDetails(
|
|
153
|
-
agent,
|
|
154
|
-
task,
|
|
155
|
-
"failed",
|
|
156
|
-
"discovery",
|
|
157
|
-
parentModel,
|
|
158
|
-
parentThinking,
|
|
159
|
-
agent,
|
|
160
|
-
error,
|
|
161
|
-
),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
agent = thread.definition.name;
|
|
165
|
-
threadId = thread.id;
|
|
166
|
-
definition = thread.definition;
|
|
167
|
-
} else {
|
|
168
|
-
const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
|
|
169
|
-
warn(discovery, ctx);
|
|
170
|
-
definition = discovery.agents.get(agent);
|
|
171
|
-
const invalid = discovery.invalid.get(agent);
|
|
172
|
-
if (!definition) {
|
|
173
|
-
const reason = invalid?.map((item) => item.reason).join("; ") ?? "unknown agent";
|
|
174
|
-
const names = [...discovery.agents.keys()].sort().join(", ") || "none";
|
|
175
|
-
const error = `Agent ${agent} discovery failed: ${reason}. Runnable agents: ${names}`;
|
|
176
|
-
return {
|
|
177
|
-
content: [{ type: "text", text: error }],
|
|
178
|
-
details: emptyDetails(
|
|
179
|
-
agent,
|
|
180
|
-
task,
|
|
181
|
-
"failed",
|
|
182
|
-
"discovery",
|
|
183
|
-
parentModel,
|
|
184
|
-
parentThinking,
|
|
185
|
-
undefined,
|
|
186
|
-
error,
|
|
187
|
-
),
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
threadId = `thread-${nextThreadId++}`;
|
|
181
|
+
? "Subagent continuation requires non-empty thread, task, and file paths"
|
|
182
|
+
: "Subagent input requires non-empty agent, task, and file paths";
|
|
183
|
+
return failedToolResult(agent, task, "queue", parentModel, parentThinking, error, threadKey);
|
|
191
184
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
await disposeSubagentThread(evicted);
|
|
185
|
+
|
|
186
|
+
return runtime.execute({
|
|
187
|
+
agent,
|
|
188
|
+
task,
|
|
189
|
+
files,
|
|
190
|
+
continuing,
|
|
191
|
+
threadKey,
|
|
192
|
+
ctx,
|
|
193
|
+
parentModel,
|
|
194
|
+
parentThinking,
|
|
195
|
+
signal,
|
|
196
|
+
onUpdate: (details) =>
|
|
197
|
+
onUpdate?.({
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: details.currentActivity ?? details.response ?? `${details.agent}: ${details.status}`,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
details,
|
|
205
|
+
}),
|
|
206
|
+
resolveFreshDefinition: async () => {
|
|
207
|
+
const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
|
|
208
|
+
warn(discovery, ctx);
|
|
209
|
+
const definition = discovery.agents.get(agent);
|
|
210
|
+
const invalid = discovery.invalid.get(agent);
|
|
211
|
+
if (!definition) {
|
|
212
|
+
const reason = invalid?.map((item) => item.reason).join("; ") ?? "unknown agent";
|
|
213
|
+
const names =
|
|
214
|
+
(await parentVisibleAgents(ctx, discovery)).map((item) => item.name).join(", ") || "none";
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
phase: "discovery",
|
|
218
|
+
error: `Agent ${agent} discovery failed: ${reason}. Runnable agents: ${names}`,
|
|
219
|
+
};
|
|
228
220
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
reservedThread = thread;
|
|
246
|
-
thread.pendingTurns += 1;
|
|
247
|
-
}
|
|
248
|
-
if (!thread) throw new Error("Subagent thread startup failed");
|
|
249
|
-
const result = await runSubagentTurn({
|
|
250
|
-
thread,
|
|
251
|
-
task,
|
|
252
|
-
initial: thread.turns === 0,
|
|
253
|
-
signal: combined,
|
|
254
|
-
onUpdate: (details) =>
|
|
255
|
-
onUpdate?.({
|
|
256
|
-
content: [
|
|
257
|
-
{
|
|
258
|
-
type: "text",
|
|
259
|
-
text: details.currentActivity ?? details.response ?? `${agent}: ${details.status}`,
|
|
260
|
-
},
|
|
261
|
-
],
|
|
262
|
-
details,
|
|
263
|
-
}),
|
|
264
|
-
});
|
|
265
|
-
return {
|
|
266
|
-
content: [
|
|
267
|
-
{
|
|
268
|
-
type: "text",
|
|
269
|
-
text: `Thread: ${thread.id}\nReuse with subagent({ thread: "${thread.id}", task: "..." })\n\n${result.content}`,
|
|
270
|
-
},
|
|
271
|
-
],
|
|
272
|
-
details: result.details,
|
|
273
|
-
};
|
|
274
|
-
} catch (error) {
|
|
275
|
-
const message = error instanceof Error ? error.message : `Agent ${agent} ${phase} failed`;
|
|
276
|
-
const status = combined.aborted ? "aborted" : "failed";
|
|
277
|
-
const details = emptyDetails(
|
|
278
|
-
agent,
|
|
279
|
-
task,
|
|
280
|
-
status,
|
|
281
|
-
phase,
|
|
282
|
-
thread?.model ?? parentModel,
|
|
283
|
-
thread?.thinkingLevel ?? parentThinking,
|
|
284
|
-
thread?.id ?? threadId,
|
|
285
|
-
message,
|
|
286
|
-
);
|
|
287
|
-
return { content: [{ type: "text", text: message }], details };
|
|
288
|
-
} finally {
|
|
289
|
-
if (reservedThread) reservedThread.pendingTurns -= 1;
|
|
290
|
-
releaseGlobal?.();
|
|
291
|
-
releaseThread?.();
|
|
292
|
-
controllers.delete(controller);
|
|
293
|
-
}
|
|
221
|
+
if (definition.name === "context-sync") {
|
|
222
|
+
const sync = (await loadTauExtensionSettings(ctx, contextSettings)).sync;
|
|
223
|
+
if (!sync.enabled) {
|
|
224
|
+
return { ok: false, phase: "discovery", error: "Context sync is disabled in settings." };
|
|
225
|
+
}
|
|
226
|
+
if (!sync.automation) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
phase: "discovery",
|
|
230
|
+
error: "Context-sync automation is disabled. Use /context-sync or enable extensions.context.sync.automation.",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { ok: true, definition };
|
|
235
|
+
},
|
|
236
|
+
});
|
|
294
237
|
},
|
|
295
238
|
renderCall(args, theme, context) {
|
|
296
239
|
return renderSubagentCall(args, theme, {
|
|
@@ -318,16 +261,20 @@ Delegate one focused task per call. Children do not inherit parent messages. Inc
|
|
|
318
261
|
if (details?.status === "failed" || details?.status === "aborted") return { isError: true };
|
|
319
262
|
});
|
|
320
263
|
pi.on("session_start", async () => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
await disposeThreads();
|
|
324
|
-
nextThreadId = 1;
|
|
264
|
+
await runtime.reset();
|
|
265
|
+
await replaceDashboard();
|
|
325
266
|
rowState.clear();
|
|
326
|
-
|
|
267
|
+
fingerprints.clear();
|
|
268
|
+
failureNotify = undefined;
|
|
327
269
|
});
|
|
328
270
|
pi.on("session_shutdown", async () => {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
await
|
|
271
|
+
unsubscribeDashboard();
|
|
272
|
+
await runtime.shutdown();
|
|
273
|
+
const leftover = await dashboard.shutdown();
|
|
274
|
+
orphans.push(...leftover);
|
|
275
|
+
await retryOrphans();
|
|
276
|
+
rowState.clear();
|
|
277
|
+
fingerprints.clear();
|
|
278
|
+
failureNotify = undefined;
|
|
332
279
|
});
|
|
333
280
|
}
|
|
@@ -47,7 +47,7 @@ export function renderSubagentResult(
|
|
|
47
47
|
text.setText("");
|
|
48
48
|
return text;
|
|
49
49
|
}
|
|
50
|
-
const identity =
|
|
50
|
+
const identity = `${details.displayName} (${details.agent})`;
|
|
51
51
|
const header = `${title(theme, context.rowState, context.rowId, context.invalidate)} ${theme.fg("accent", identity)} ${theme.fg("muted", `$${details.usage.cost.toFixed(4)} · ${(details.durationMs / 1000).toFixed(1)}s · ${details.toolCalls} tools`)}`;
|
|
52
52
|
if (!expanded) {
|
|
53
53
|
text.setText(`${header} ${theme.fg("muted", details.task.replace(/\s+/g, " ").trim())}`);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AgentDefinition } from "./agents.ts";
|
|
2
|
+
|
|
3
|
+
const RESUME_OPTIONAL_HISTORY_TOKENS = 20_000;
|
|
4
|
+
|
|
5
|
+
export type RetainedTurnOutcome = "completed" | "failed" | "aborted";
|
|
6
|
+
|
|
7
|
+
export interface RetainedTurnRecord {
|
|
8
|
+
task: string;
|
|
9
|
+
outcome: RetainedTurnOutcome;
|
|
10
|
+
terminalText: string;
|
|
11
|
+
files: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SubagentResumeState {
|
|
15
|
+
records: readonly RetainedTurnRecord[];
|
|
16
|
+
relevantPaths: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function emptySubagentResumeState(): SubagentResumeState {
|
|
20
|
+
return { records: [], relevantPaths: [] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function retainSubagentTurn(state: SubagentResumeState, record: RetainedTurnRecord): SubagentResumeState {
|
|
24
|
+
const paths = new Set(state.relevantPaths);
|
|
25
|
+
for (const path of record.files) paths.add(path);
|
|
26
|
+
return {
|
|
27
|
+
records: [...state.records, { ...record, files: [...record.files] }],
|
|
28
|
+
relevantPaths: [...paths],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function selectRetainedTurns(
|
|
33
|
+
state: SubagentResumeState,
|
|
34
|
+
optionalTokenBudget = RESUME_OPTIONAL_HISTORY_TOKENS,
|
|
35
|
+
): { records: readonly RetainedTurnRecord[]; omitted: number } {
|
|
36
|
+
const initial = state.records[0];
|
|
37
|
+
if (!initial) return { records: [], omitted: 0 };
|
|
38
|
+
let used = 0;
|
|
39
|
+
const selectedIndexes = new Set<number>();
|
|
40
|
+
for (let index = state.records.length - 1; index >= 1; index -= 1) {
|
|
41
|
+
const record = state.records[index];
|
|
42
|
+
if (!record) continue;
|
|
43
|
+
const tokens = Math.ceil(JSON.stringify(record).length / 4);
|
|
44
|
+
if (used + tokens > optionalTokenBudget) continue;
|
|
45
|
+
used += tokens;
|
|
46
|
+
selectedIndexes.add(index);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
records: [initial, ...state.records.slice(1).filter((_record, index) => selectedIndexes.has(index + 1))],
|
|
50
|
+
omitted: state.records.length - 1 - selectedIndexes.size,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildColdResumePrompt(options: {
|
|
55
|
+
definition: AgentDefinition;
|
|
56
|
+
state: SubagentResumeState;
|
|
57
|
+
followUp: string;
|
|
58
|
+
hasAutoreadFiles: boolean;
|
|
59
|
+
}): string {
|
|
60
|
+
const selected = selectRetainedTurns(options.state);
|
|
61
|
+
const data = JSON.stringify({
|
|
62
|
+
version: 1,
|
|
63
|
+
retainedTurns: selected.records,
|
|
64
|
+
omittedRetainedTurns: selected.omitted,
|
|
65
|
+
relevantPaths: options.state.relevantPaths,
|
|
66
|
+
parentFollowUp: options.followUp,
|
|
67
|
+
});
|
|
68
|
+
return `You are an isolated delegated child agent resuming prior delegated work. Stay within the delegated task and return only the requested result.${options.hasAutoreadFiles ? " Parent-supplied autoread files are included as line-numbered context for this turn." : ""}
|
|
69
|
+
|
|
70
|
+
## Agent instructions
|
|
71
|
+
${options.definition.prompt}
|
|
72
|
+
|
|
73
|
+
The JSON object between the fixed markers is prior work data. Its retained terminal text is exact. Earlier file contents, tool calls, tool results, intermediate responses, and thinking are absent. Relevant paths are a manifest only; read current source before relying on it.
|
|
74
|
+
|
|
75
|
+
<tau-subagent-resume-json>
|
|
76
|
+
${data}
|
|
77
|
+
</tau-subagent-resume-json>`;
|
|
78
|
+
}
|