@shanepadgett/tau-agent 0.11.1 → 0.13.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 +11 -0
- package/extensions/attention/README.md +3 -1
- package/extensions/attention/index.ts +12 -0
- package/extensions/publish/README.md +2 -0
- package/extensions/publish/index.ts +12 -1
- package/extensions/subagent/README.md +2 -0
- package/extensions/subagent/index.ts +228 -106
- package/extensions/subagent/render.ts +4 -3
- package/extensions/subagent/run.ts +96 -45
- package/extensions/tau-help/help.md +3 -3
- package/package.json +2 -2
package/docs/subagents.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Tau's `subagent` tool delegates one focused task to an isolated child Pi session. You can add your own agent definitions without writing TypeScript.
|
|
4
4
|
|
|
5
|
+
Each fresh call returns a thread ID. Continue that thread when feedback or follow-up work depends on the child's prior reads and reasoning:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
subagent({ agent: "scout", task: "Trace configuration loading" })
|
|
9
|
+
subagent({ thread: "thread-1", task: "Now check whether this proposed fix covers every caller" })
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Retained threads keep their child conversation and tool results for the current parent session. Start fresh for unrelated work or when earlier context is stale or oversized. Tau keeps up to 16 threads and evicts the least recently used idle thread when needed.
|
|
13
|
+
|
|
5
14
|
## Where definitions live
|
|
6
15
|
|
|
7
16
|
| Scope | Path | Use when |
|
|
@@ -46,8 +55,10 @@ Named tools and configured models must exist in the normally loaded child Pi env
|
|
|
46
55
|
|
|
47
56
|
- Children use the parent's cwd and inherit model/thinking unless the definition overrides them.
|
|
48
57
|
- Children do not receive the parent conversation.
|
|
58
|
+
- Follow-up calls reuse the retained child's conversation, model, thinking level, tools, and cwd.
|
|
49
59
|
- Children load only the extensions that own their declared tools. Unrelated extension hooks do not run in child sessions.
|
|
50
60
|
- At most four children run at once; extra calls wait in order.
|
|
61
|
+
- Calls to the same retained thread run one at a time.
|
|
51
62
|
- Returned text is capped (50 KB / 2,000 lines); full truncated output is saved to a private temp file.
|
|
52
63
|
|
|
53
64
|
## Built-ins
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Attention
|
|
2
2
|
|
|
3
|
-
Sends a terminal-driven attention notification when Tau is ready for input.
|
|
3
|
+
Sends a terminal-driven attention notification when Tau is ready for input, finishes compacting a session, or summarizes an abandoned branch during tree navigation.
|
|
4
4
|
|
|
5
5
|
## Behavior
|
|
6
6
|
|
|
7
7
|
- Emits an attention notification on `agent_end`.
|
|
8
|
+
- Emits an attention notification on `session_compact`.
|
|
9
|
+
- Emits an attention notification on `session_tree` when it includes a branch summary.
|
|
8
10
|
- Listens for shared event `tau:agent.blocked` when Tau is waiting on user input.
|
|
9
11
|
- Uses the terminal or host OS notification path that best fits the current environment.
|
|
10
12
|
|
|
@@ -4,6 +4,8 @@ import { onTauEvent } from "../../shared/events.js";
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_TITLE = "Tau";
|
|
6
6
|
const DEFAULT_BODY = "Ready for input";
|
|
7
|
+
const COMPACTION_BODY = "Compaction complete";
|
|
8
|
+
const BRANCH_SUMMARY_BODY = "Branch summary complete";
|
|
7
9
|
const MACOS_SOUND = "/System/Library/Sounds/Submarine.aiff";
|
|
8
10
|
|
|
9
11
|
function oscText(value: string): string {
|
|
@@ -68,4 +70,14 @@ export default function attentionExtension(pi: ExtensionAPI): void {
|
|
|
68
70
|
if (ctx.mode === "print") return;
|
|
69
71
|
notify({ title: DEFAULT_TITLE, body: DEFAULT_BODY });
|
|
70
72
|
});
|
|
73
|
+
|
|
74
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
75
|
+
if (ctx.mode === "print") return;
|
|
76
|
+
notify({ title: DEFAULT_TITLE, body: COMPACTION_BODY });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
pi.on("session_tree", (event, ctx) => {
|
|
80
|
+
if (ctx.mode === "print" || !event.summaryEntry) return;
|
|
81
|
+
notify({ title: DEFAULT_TITLE, body: BRANCH_SUMMARY_BODY });
|
|
82
|
+
});
|
|
71
83
|
}
|
|
@@ -9,3 +9,5 @@ Create a tagged Tau release and monitor its GitHub Actions npm publish run.
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
`/publish` requires a clean Git working tree, authenticated GitHub CLI, and public approved npm package sources. It recommends a semantic version bump from commits since the previous release tag, asks for confirmation, then pushes a release tag. GitHub Actions publishes the TUI package before the agent package through npm trusted publishing.
|
|
12
|
+
|
|
13
|
+
If publishing fails, the agent runs read-only diagnostics and recommends a solution. It does not apply the fix or retry the release.
|
|
@@ -42,7 +42,18 @@ export default function publishExtension(pi: ExtensionAPI): void {
|
|
|
42
42
|
try {
|
|
43
43
|
await publish(pi, ctx);
|
|
44
44
|
} catch (error) {
|
|
45
|
-
|
|
45
|
+
pi.sendMessage(
|
|
46
|
+
{
|
|
47
|
+
customType: "tau.publish-failure",
|
|
48
|
+
content: [
|
|
49
|
+
"The /publish command failed with this error:",
|
|
50
|
+
errorText(error),
|
|
51
|
+
"Investigate the failure using read-only diagnostics. Determine the likely cause and recommend a solution to the user. Do not change files, git state, package state, or remote state, and do not retry publishing.",
|
|
52
|
+
].join("\n\n"),
|
|
53
|
+
display: false,
|
|
54
|
+
},
|
|
55
|
+
{ triggerTurn: true },
|
|
56
|
+
);
|
|
46
57
|
}
|
|
47
58
|
},
|
|
48
59
|
});
|
|
@@ -12,6 +12,8 @@ Tau includes three built-in agents:
|
|
|
12
12
|
|
|
13
13
|
Ask Tau to delegate a task, or let it call `subagent` with an agent name and task. Children use the parent's current working directory and inherit its model and thinking level unless their definition overrides either value. They do not receive the parent conversation. Tau loads only the extensions that own a child's declared tools, so unrelated extension hooks do not run in child sessions. When a child must inspect another repository, put its exact absolute path in the delegated task.
|
|
14
14
|
|
|
15
|
+
Fresh calls return a thread ID. Tau can send feedback or follow-up work to that thread, preserving the child's conversation, prior reads, and tool results. It starts a fresh thread for unrelated work or when retained context is stale or oversized. Threads live for the current parent session. Tau retains up to 16 and evicts the least recently used idle thread when needed. Calls to one thread run sequentially.
|
|
16
|
+
|
|
15
17
|
## Agent definitions
|
|
16
18
|
|
|
17
19
|
Add Markdown definitions at `~/.pi/agent/tau/agents/*.md` or, in a trusted project, the nearest `.pi/tau/agents/*.md`. Project definitions override user definitions, which override built-ins. Duplicate names in one scope are invalid.
|
|
@@ -1,20 +1,66 @@
|
|
|
1
1
|
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { createToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
4
|
-
import { discoverAgents, type AgentDiscovery } from "./agents.ts";
|
|
4
|
+
import { discoverAgents, type AgentDefinition, type AgentDiscovery } from "./agents.ts";
|
|
5
5
|
import { renderSubagentCall, renderSubagentResult } from "./render.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createSubagentThread,
|
|
8
|
+
disposeSubagentThread,
|
|
9
|
+
extensionPathsForTools,
|
|
10
|
+
FifoGate,
|
|
11
|
+
runSubagentTurn,
|
|
12
|
+
type SubagentDetails,
|
|
13
|
+
type SubagentThread,
|
|
14
|
+
} from "./run.ts";
|
|
7
15
|
|
|
8
|
-
const params = Type.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
const params = Type.Union([
|
|
17
|
+
Type.Object(
|
|
18
|
+
{ agent: Type.String({ minLength: 1 }), task: Type.String({ minLength: 1 }) },
|
|
19
|
+
{ additionalProperties: false },
|
|
20
|
+
),
|
|
21
|
+
Type.Object(
|
|
22
|
+
{ thread: Type.String({ minLength: 1 }), task: Type.String({ minLength: 1 }) },
|
|
23
|
+
{ additionalProperties: false },
|
|
24
|
+
),
|
|
25
|
+
]);
|
|
26
|
+
|
|
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
|
+
}
|
|
12
56
|
|
|
13
57
|
export default function subagentExtension(pi: ExtensionAPI): void {
|
|
14
58
|
const gate = new FifoGate(4);
|
|
15
59
|
const controllers = new Set<AbortController>();
|
|
60
|
+
const threads = new Map<string, SubagentThread>();
|
|
16
61
|
const fingerprints = new Map<string, string>();
|
|
17
62
|
const runtimeWarnings = new Set<string>();
|
|
63
|
+
let nextThreadId = 1;
|
|
18
64
|
const rowState = createToolRowStateStore(pi, "subagent.tool-row-state");
|
|
19
65
|
const warn = (discovery: AgentDiscovery, ctx: ExtensionContext) => {
|
|
20
66
|
const current = new Set<string>();
|
|
@@ -33,6 +79,11 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
33
79
|
}
|
|
34
80
|
for (const path of fingerprints.keys()) if (!current.has(path)) fingerprints.delete(path);
|
|
35
81
|
};
|
|
82
|
+
const disposeThreads = async () => {
|
|
83
|
+
const retained = [...threads.values()];
|
|
84
|
+
threads.clear();
|
|
85
|
+
await Promise.all(retained.map((thread) => disposeSubagentThread(thread)));
|
|
86
|
+
};
|
|
36
87
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
37
88
|
if (!pi.getActiveTools().includes("subagent")) return undefined;
|
|
38
89
|
const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
|
|
@@ -40,7 +91,23 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
40
91
|
const lines = [...discovery.agents.values()]
|
|
41
92
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
42
93
|
.map((agent) => `- ${agent.name}: ${agent.description}`);
|
|
43
|
-
const
|
|
94
|
+
const activeThreads = [...threads.values()]
|
|
95
|
+
.filter((thread) => thread.cwd === ctx.cwd)
|
|
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
|
+
});
|
|
101
|
+
const threadSection = activeThreads.length ? `\n\nActive reusable threads:\n${activeThreads.join("\n")}` : "";
|
|
102
|
+
const prompt = `## Subagents
|
|
103
|
+
Use \`subagent\` when an available agent matches a focused part of the task.
|
|
104
|
+
|
|
105
|
+
Available agents for this turn:
|
|
106
|
+
${lines.join("\n")}
|
|
107
|
+
|
|
108
|
+
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
|
+
|
|
110
|
+
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.`;
|
|
44
111
|
return { systemPrompt: `${event.systemPrompt}\n\n${prompt}` };
|
|
45
112
|
});
|
|
46
113
|
pi.registerTool(
|
|
@@ -48,116 +115,142 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
48
115
|
name: "subagent",
|
|
49
116
|
label: "Subagent",
|
|
50
117
|
description:
|
|
51
|
-
"
|
|
52
|
-
promptSnippet: "
|
|
118
|
+
"Start a focused isolated child agent or continue a retained child thread. Output is limited to 50KB or 2,000 lines.",
|
|
119
|
+
promptSnippet: "Start a focused child agent or continue a retained child thread",
|
|
53
120
|
parameters: params,
|
|
54
121
|
executionMode: "parallel",
|
|
55
122
|
async execute(_id, raw, signal, onUpdate, ctx) {
|
|
56
|
-
const agent = raw.agent.trim();
|
|
57
123
|
const task = raw.task.trim();
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
toolCalls: 0,
|
|
70
|
-
actions: [],
|
|
71
|
-
omittedActions: 0,
|
|
72
|
-
omittedErrors: 0,
|
|
73
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
74
|
-
durationMs: 0,
|
|
75
|
-
error: "Invalid input",
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
|
|
79
|
-
warn(discovery, ctx);
|
|
80
|
-
const definition = discovery.agents.get(agent);
|
|
81
|
-
const invalid = discovery.invalid.get(agent);
|
|
82
|
-
if (!definition) {
|
|
83
|
-
const reason = invalid?.map((item) => item.reason).join("; ") ?? "unknown agent";
|
|
84
|
-
const names = [...discovery.agents.keys()].sort().join(", ") || "none";
|
|
85
|
-
const error = `Agent ${agent} discovery failed: ${reason}. Runnable agents: ${names}`;
|
|
124
|
+
const continuing = "thread" in raw;
|
|
125
|
+
const parentModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable";
|
|
126
|
+
const parentThinking = pi.getThinkingLevel();
|
|
127
|
+
let thread: SubagentThread | undefined;
|
|
128
|
+
let definition: AgentDefinition | undefined;
|
|
129
|
+
let agent = continuing ? raw.thread.trim() : raw.agent.trim();
|
|
130
|
+
let threadId = continuing ? raw.thread.trim() : undefined;
|
|
131
|
+
if (!task || !agent) {
|
|
132
|
+
const error = continuing
|
|
133
|
+
? "Subagent continuation requires non-empty thread and task"
|
|
134
|
+
: "Subagent input requires non-empty agent and task";
|
|
86
135
|
return {
|
|
87
136
|
content: [{ type: "text", text: error }],
|
|
88
|
-
details:
|
|
89
|
-
agent,
|
|
90
|
-
task,
|
|
91
|
-
status: "failed",
|
|
92
|
-
phase: "discovery",
|
|
93
|
-
model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable",
|
|
94
|
-
thinkingLevel,
|
|
95
|
-
toolCalls: 0,
|
|
96
|
-
actions: [],
|
|
97
|
-
omittedActions: 0,
|
|
98
|
-
omittedErrors: 0,
|
|
99
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
100
|
-
durationMs: 0,
|
|
101
|
-
error,
|
|
102
|
-
},
|
|
137
|
+
details: emptyDetails(agent, task, "failed", "queue", parentModel, parentThinking, threadId, error),
|
|
103
138
|
};
|
|
104
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++}`;
|
|
191
|
+
}
|
|
105
192
|
const controller = new AbortController();
|
|
106
193
|
controllers.add(controller);
|
|
107
194
|
const combined = AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]);
|
|
108
|
-
let
|
|
195
|
+
let releaseThread: (() => void) | undefined;
|
|
196
|
+
let releaseGlobal: (() => void) | undefined;
|
|
197
|
+
let reservedThread: SubagentThread | undefined;
|
|
198
|
+
let phase: "queue" | "startup" = "queue";
|
|
109
199
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
try {
|
|
128
|
-
release = await gate.acquire(combined);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
const message = error instanceof Error ? error.message : `Agent ${agent} queue failed`;
|
|
131
|
-
const details: SubagentDetails = {
|
|
132
|
-
agent,
|
|
133
|
-
task,
|
|
134
|
-
status: combined.aborted ? "aborted" : "failed",
|
|
135
|
-
phase: "queue",
|
|
136
|
-
model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable",
|
|
137
|
-
thinkingLevel,
|
|
138
|
-
toolCalls: 0,
|
|
139
|
-
actions: [],
|
|
140
|
-
omittedActions: 0,
|
|
141
|
-
omittedErrors: 0,
|
|
142
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
143
|
-
durationMs: 0,
|
|
144
|
-
error: message,
|
|
145
|
-
};
|
|
146
|
-
return { content: [{ type: "text", text: message }], details };
|
|
200
|
+
const waiting = emptyDetails(
|
|
201
|
+
agent,
|
|
202
|
+
task,
|
|
203
|
+
"waiting",
|
|
204
|
+
"queue",
|
|
205
|
+
thread?.model ?? parentModel,
|
|
206
|
+
thread?.thinkingLevel ?? parentThinking,
|
|
207
|
+
threadId,
|
|
208
|
+
undefined,
|
|
209
|
+
);
|
|
210
|
+
await onUpdate?.({ content: [{ type: "text", text: `${agent}: waiting` }], details: waiting });
|
|
211
|
+
if (thread) {
|
|
212
|
+
reservedThread = thread;
|
|
213
|
+
thread.pendingTurns += 1;
|
|
214
|
+
releaseThread = await thread.turnGate.acquire(combined);
|
|
147
215
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
216
|
+
releaseGlobal = await gate.acquire(combined);
|
|
217
|
+
if (!thread) {
|
|
218
|
+
phase = "startup";
|
|
219
|
+
if (!threadId || !definition) throw new Error("Subagent startup state is incomplete");
|
|
220
|
+
const selectedDefinition = definition;
|
|
221
|
+
if (threads.size >= MAX_RETAINED_THREADS) {
|
|
222
|
+
const evicted = [...threads.values()]
|
|
223
|
+
.filter((item) => item.pendingTurns === 0)
|
|
224
|
+
.sort((a, b) => a.lastUsedAt - b.lastUsedAt)[0];
|
|
225
|
+
if (!evicted) throw new Error("Subagent thread limit reached while all retained threads are busy");
|
|
226
|
+
threads.delete(evicted.id);
|
|
227
|
+
await disposeSubagentThread(evicted);
|
|
228
|
+
}
|
|
229
|
+
thread = await createSubagentThread({
|
|
230
|
+
id: threadId,
|
|
231
|
+
definition: selectedDefinition,
|
|
232
|
+
extensionPaths: extensionPathsForTools(pi, selectedDefinition.tools),
|
|
233
|
+
initialTask: task,
|
|
234
|
+
ctx,
|
|
235
|
+
thinkingLevel: parentThinking,
|
|
236
|
+
signal: combined,
|
|
237
|
+
onWarning: (warning) => {
|
|
238
|
+
const message = `Subagent definition ${selectedDefinition.path}: ${warning}`;
|
|
239
|
+
if (runtimeWarnings.has(message)) return;
|
|
240
|
+
runtimeWarnings.add(message);
|
|
241
|
+
ctx.ui.notify(message, "warning");
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
threads.set(thread.id, thread);
|
|
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,
|
|
151
251
|
task,
|
|
152
|
-
|
|
153
|
-
thinkingLevel,
|
|
252
|
+
initial: thread.turns === 0,
|
|
154
253
|
signal: combined,
|
|
155
|
-
onWarning: (warning) => {
|
|
156
|
-
const message = `Subagent definition ${definition.path}: ${warning}`;
|
|
157
|
-
if (runtimeWarnings.has(message)) return;
|
|
158
|
-
runtimeWarnings.add(message);
|
|
159
|
-
ctx.ui.notify(message, "warning");
|
|
160
|
-
},
|
|
161
254
|
onUpdate: (details) =>
|
|
162
255
|
onUpdate?.({
|
|
163
256
|
content: [
|
|
@@ -169,9 +262,33 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
169
262
|
details,
|
|
170
263
|
}),
|
|
171
264
|
});
|
|
172
|
-
return {
|
|
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 };
|
|
173
288
|
} finally {
|
|
174
|
-
|
|
289
|
+
if (reservedThread) reservedThread.pendingTurns -= 1;
|
|
290
|
+
releaseGlobal?.();
|
|
291
|
+
releaseThread?.();
|
|
175
292
|
controllers.delete(controller);
|
|
176
293
|
}
|
|
177
294
|
},
|
|
@@ -200,12 +317,17 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
200
317
|
const details = event.details as SubagentDetails | undefined;
|
|
201
318
|
if (details?.status === "failed" || details?.status === "aborted") return { isError: true };
|
|
202
319
|
});
|
|
203
|
-
pi.on("session_start", () => {
|
|
320
|
+
pi.on("session_start", async () => {
|
|
321
|
+
for (const controller of controllers) controller.abort();
|
|
322
|
+
controllers.clear();
|
|
323
|
+
await disposeThreads();
|
|
324
|
+
nextThreadId = 1;
|
|
204
325
|
rowState.clear();
|
|
205
326
|
runtimeWarnings.clear();
|
|
206
327
|
});
|
|
207
|
-
pi.on("session_shutdown", () => {
|
|
328
|
+
pi.on("session_shutdown", async () => {
|
|
208
329
|
for (const controller of controllers) controller.abort();
|
|
209
330
|
controllers.clear();
|
|
331
|
+
await disposeThreads();
|
|
210
332
|
});
|
|
211
333
|
}
|
|
@@ -12,7 +12,7 @@ const title = (theme: Theme, rowState: ToolRowStateStore, rowId: string, invalid
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export function renderSubagentCall(
|
|
15
|
-
args: { agent?: string; task?: string },
|
|
15
|
+
args: { agent?: string; thread?: string; task?: string },
|
|
16
16
|
theme: Theme,
|
|
17
17
|
context: {
|
|
18
18
|
executionStarted: boolean;
|
|
@@ -29,7 +29,7 @@ export function renderSubagentCall(
|
|
|
29
29
|
return text;
|
|
30
30
|
}
|
|
31
31
|
const prefix =
|
|
32
|
-
`${title(theme, context.rowState, context.rowId, context.invalidate)} ${theme.fg("accent", args.agent ?? "")}`.trimEnd();
|
|
32
|
+
`${title(theme, context.rowState, context.rowId, context.invalidate)} ${theme.fg("accent", args.agent ?? args.thread ?? "")}`.trimEnd();
|
|
33
33
|
const task = args.task?.replace(/\s+/g, " ").trim();
|
|
34
34
|
text.setText(task ? `${prefix} ${theme.fg("muted", task)}` : prefix);
|
|
35
35
|
return text;
|
|
@@ -47,7 +47,8 @@ export function renderSubagentResult(
|
|
|
47
47
|
text.setText("");
|
|
48
48
|
return text;
|
|
49
49
|
}
|
|
50
|
-
const
|
|
50
|
+
const identity = details.threadId ? `${details.agent} · ${details.threadId}` : details.agent;
|
|
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`)}`;
|
|
51
52
|
if (!expanded) {
|
|
52
53
|
text.setText(`${header} ${theme.fg("muted", details.task.replace(/\s+/g, " ").trim())}`);
|
|
53
54
|
return text;
|
|
@@ -55,6 +55,7 @@ export interface SubagentUsage {
|
|
|
55
55
|
}
|
|
56
56
|
export interface SubagentDetails {
|
|
57
57
|
agent: string;
|
|
58
|
+
threadId?: string;
|
|
58
59
|
status: "waiting" | "running" | "completed" | "failed" | "aborted";
|
|
59
60
|
phase: "discovery" | "queue" | "startup" | "run" | "output";
|
|
60
61
|
completionState?: string;
|
|
@@ -80,6 +81,20 @@ export interface SubagentDetails {
|
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
export interface SubagentThread {
|
|
85
|
+
id: string;
|
|
86
|
+
definition: AgentDefinition;
|
|
87
|
+
session: AgentSession;
|
|
88
|
+
cwd: string;
|
|
89
|
+
model: string;
|
|
90
|
+
thinkingLevel: string;
|
|
91
|
+
initialTask: string;
|
|
92
|
+
turns: number;
|
|
93
|
+
turnGate: FifoGate;
|
|
94
|
+
pendingTurns: number;
|
|
95
|
+
lastUsedAt: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
export function extensionPathsForTools(pi: ExtensionAPI, tools: readonly string[]): string[] {
|
|
84
99
|
const selected = new Set(tools);
|
|
85
100
|
return [
|
|
@@ -154,31 +169,26 @@ function textOf(message: AssistantMessage): string {
|
|
|
154
169
|
.join("");
|
|
155
170
|
}
|
|
156
171
|
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function runSubagent(options: {
|
|
172
|
+
export async function createSubagentThread(options: {
|
|
173
|
+
id: string;
|
|
162
174
|
definition: AgentDefinition;
|
|
163
175
|
extensionPaths: readonly string[];
|
|
164
|
-
|
|
176
|
+
initialTask: string;
|
|
165
177
|
ctx: ExtensionContext;
|
|
166
178
|
thinkingLevel: string;
|
|
167
179
|
signal: AbortSignal;
|
|
168
|
-
onUpdate?: (details: SubagentDetails) => void | Promise<void>;
|
|
169
180
|
onWarning?: (warning: string) => void;
|
|
170
|
-
}): Promise<
|
|
181
|
+
}): Promise<SubagentThread> {
|
|
171
182
|
const {
|
|
183
|
+
id,
|
|
172
184
|
definition,
|
|
173
185
|
extensionPaths,
|
|
174
|
-
|
|
186
|
+
initialTask,
|
|
175
187
|
ctx,
|
|
176
188
|
thinkingLevel: parentThinkingLevel,
|
|
177
189
|
signal,
|
|
178
|
-
onUpdate,
|
|
179
190
|
onWarning,
|
|
180
191
|
} = options;
|
|
181
|
-
const started = Date.now();
|
|
182
192
|
let model = ctx.model;
|
|
183
193
|
let thinkingLevel = parentThinkingLevel;
|
|
184
194
|
if (definition.model) {
|
|
@@ -205,34 +215,7 @@ export async function runSubagent(options: {
|
|
|
205
215
|
else thinkingLevel = definition.thinking;
|
|
206
216
|
}
|
|
207
217
|
const modelName = model ? `${model.provider}/${model.id}` : "unavailable";
|
|
208
|
-
const usage: SubagentUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
209
|
-
const details: SubagentDetails = {
|
|
210
|
-
agent: definition.name,
|
|
211
|
-
status: "running",
|
|
212
|
-
phase: "startup",
|
|
213
|
-
task,
|
|
214
|
-
model: modelName,
|
|
215
|
-
thinkingLevel,
|
|
216
|
-
toolCalls: 0,
|
|
217
|
-
actions: [],
|
|
218
|
-
omittedActions: 0,
|
|
219
|
-
omittedErrors: 0,
|
|
220
|
-
usage,
|
|
221
|
-
durationMs: 0,
|
|
222
|
-
};
|
|
223
218
|
let session: AgentSession | undefined;
|
|
224
|
-
let unsubscribe: (() => void) | undefined;
|
|
225
|
-
let updateChain = Promise.resolve();
|
|
226
|
-
let lastTextUpdate = 0;
|
|
227
|
-
const publish = (force = false) => {
|
|
228
|
-
const now = Date.now();
|
|
229
|
-
if (!force && now - lastTextUpdate < 100) return;
|
|
230
|
-
lastTextUpdate = now;
|
|
231
|
-
details.durationMs = now - started;
|
|
232
|
-
const snapshot = structuredClone(details);
|
|
233
|
-
snapshot.actions = snapshot.actions.slice(-5);
|
|
234
|
-
updateChain = updateChain.then(() => onUpdate?.(snapshot)).catch(() => undefined);
|
|
235
|
-
};
|
|
236
219
|
try {
|
|
237
220
|
if (!model) throw new Error(`Agent ${definition.name} startup failed: parent has no model`);
|
|
238
221
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
@@ -267,12 +250,78 @@ export async function runSubagent(options: {
|
|
|
267
250
|
`Agent ${definition.name} startup failed: unavailable tools: ${missing.join(", ") || "active tool mismatch"}`,
|
|
268
251
|
);
|
|
269
252
|
}
|
|
270
|
-
|
|
253
|
+
return {
|
|
254
|
+
id,
|
|
255
|
+
definition,
|
|
256
|
+
session,
|
|
257
|
+
cwd: ctx.cwd,
|
|
258
|
+
model: modelName,
|
|
259
|
+
thinkingLevel,
|
|
260
|
+
initialTask,
|
|
261
|
+
turns: 0,
|
|
262
|
+
turnGate: new FifoGate(1),
|
|
263
|
+
pendingTurns: 0,
|
|
264
|
+
lastUsedAt: Date.now(),
|
|
265
|
+
};
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (session?.isStreaming) await session.abort().catch(() => undefined);
|
|
268
|
+
session?.dispose();
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function disposeSubagentThread(thread: SubagentThread): Promise<void> {
|
|
274
|
+
if (thread.session.isStreaming) await thread.session.abort().catch(() => undefined);
|
|
275
|
+
thread.session.dispose();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function runSubagentTurn(options: {
|
|
279
|
+
thread: SubagentThread;
|
|
280
|
+
task: string;
|
|
281
|
+
initial: boolean;
|
|
282
|
+
signal: AbortSignal;
|
|
283
|
+
onUpdate?: (details: SubagentDetails) => void | Promise<void>;
|
|
284
|
+
}): Promise<{ content: string; details: SubagentDetails }> {
|
|
285
|
+
const { thread, task, initial, signal, onUpdate } = options;
|
|
286
|
+
const { definition, session } = thread;
|
|
287
|
+
const started = Date.now();
|
|
288
|
+
const usage: SubagentUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
289
|
+
const details: SubagentDetails = {
|
|
290
|
+
agent: definition.name,
|
|
291
|
+
threadId: thread.id,
|
|
292
|
+
status: "running",
|
|
293
|
+
phase: "run",
|
|
294
|
+
task,
|
|
295
|
+
model: thread.model,
|
|
296
|
+
thinkingLevel: thread.thinkingLevel,
|
|
297
|
+
toolCalls: 0,
|
|
298
|
+
actions: [],
|
|
299
|
+
omittedActions: 0,
|
|
300
|
+
omittedErrors: 0,
|
|
301
|
+
usage,
|
|
302
|
+
durationMs: 0,
|
|
303
|
+
};
|
|
304
|
+
let unsubscribe: (() => void) | undefined;
|
|
305
|
+
let updateChain = Promise.resolve();
|
|
306
|
+
let lastTextUpdate = 0;
|
|
307
|
+
const turnMessages: AssistantMessage[] = [];
|
|
308
|
+
const publish = (force = false) => {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
if (!force && now - lastTextUpdate < 100) return;
|
|
311
|
+
lastTextUpdate = now;
|
|
312
|
+
details.durationMs = now - started;
|
|
313
|
+
const snapshot = structuredClone(details);
|
|
314
|
+
snapshot.actions = snapshot.actions.slice(-5);
|
|
315
|
+
updateChain = updateChain.then(() => onUpdate?.(snapshot)).catch(() => undefined);
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
271
318
|
const actionById = new Map<string, string>();
|
|
272
319
|
unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
273
320
|
if (event.type === "message_update" && event.message.role === "assistant") {
|
|
274
321
|
details.response = capped(textOf(event.message), PREVIEW_LIMIT);
|
|
275
322
|
publish();
|
|
323
|
+
} else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
324
|
+
turnMessages.push(event.message);
|
|
276
325
|
} else if (event.type === "tool_execution_start") {
|
|
277
326
|
details.toolCalls += 1;
|
|
278
327
|
const summary = `${event.toolName} ${capped(event.args)}`.trim();
|
|
@@ -302,14 +351,15 @@ export async function runSubagent(options: {
|
|
|
302
351
|
signal.addEventListener("abort", abort, { once: true });
|
|
303
352
|
try {
|
|
304
353
|
await session.prompt(
|
|
305
|
-
|
|
354
|
+
initial
|
|
355
|
+
? `You are an isolated delegated child agent. Stay within the delegated task and return only the requested result.\n\n## Agent instructions\n${definition.prompt}\n\n## Delegated task\n${task}`
|
|
356
|
+
: `Continue the existing delegated work using the context already in this thread. Return only the requested result.\n\n## Parent follow-up\n${task}`,
|
|
306
357
|
{ expandPromptTemplates: false },
|
|
307
358
|
);
|
|
308
359
|
} finally {
|
|
309
360
|
signal.removeEventListener("abort", abort);
|
|
310
361
|
}
|
|
311
|
-
const
|
|
312
|
-
for (const message of messages) {
|
|
362
|
+
for (const message of turnMessages) {
|
|
313
363
|
usage.input += message.usage.input;
|
|
314
364
|
usage.output += message.usage.output;
|
|
315
365
|
usage.cacheRead += message.usage.cacheRead;
|
|
@@ -317,7 +367,7 @@ export async function runSubagent(options: {
|
|
|
317
367
|
usage.cost += message.usage.cost.total;
|
|
318
368
|
usage.turns += 1;
|
|
319
369
|
}
|
|
320
|
-
const terminal =
|
|
370
|
+
const terminal = turnMessages.at(-1);
|
|
321
371
|
if (!terminal) throw new Error(`Agent ${definition.name} run failed: no terminal assistant response`);
|
|
322
372
|
const response = textOf(terminal);
|
|
323
373
|
if (terminal.stopReason === "aborted") {
|
|
@@ -374,7 +424,8 @@ export async function runSubagent(options: {
|
|
|
374
424
|
return { content: details.error, details };
|
|
375
425
|
} finally {
|
|
376
426
|
unsubscribe?.();
|
|
377
|
-
|
|
378
|
-
|
|
427
|
+
thread.turns += 1;
|
|
428
|
+
thread.lastUsedAt = Date.now();
|
|
429
|
+
if (session.isStreaming) await session.abort().catch(() => undefined);
|
|
379
430
|
}
|
|
380
431
|
}
|
|
@@ -8,7 +8,7 @@ Gives the agent macOS window discovery, screenshots, and app activation tools fo
|
|
|
8
8
|
|
|
9
9
|
## attention
|
|
10
10
|
|
|
11
|
-
Shows attention state when Tau needs the user to look at the chat.
|
|
11
|
+
Shows attention state when Tau needs the user to look at the chat, finishes compacting a session, or summarizes an abandoned branch.
|
|
12
12
|
|
|
13
13
|
## auto-name
|
|
14
14
|
|
|
@@ -60,7 +60,7 @@ Replaces separate edit/write operations with one multi-file `patch` tool. It can
|
|
|
60
60
|
|
|
61
61
|
## publish
|
|
62
62
|
|
|
63
|
-
Adds `/publish` to create a tagged release, trigger trusted npm publishing in GitHub Actions, and show its status. It recommends a semantic version bump from commits since the prior release tag, but you must confirm the release type and publish action.
|
|
63
|
+
Adds `/publish` to create a tagged release, trigger trusted npm publishing in GitHub Actions, and show its status. It recommends a semantic version bump from commits since the prior release tag, but you must confirm the release type and publish action. If publishing fails, the agent investigates with read-only diagnostics and recommends a solution without applying it or retrying the release.
|
|
64
64
|
|
|
65
65
|
## qna
|
|
66
66
|
|
|
@@ -92,7 +92,7 @@ Adds `Alt+S` to stash the current prompt draft and `/pop` to browse stashed draf
|
|
|
92
92
|
|
|
93
93
|
## subagent
|
|
94
94
|
|
|
95
|
-
Gives Tau a subagent delegation tool for isolated, focused work. You can also create your own subagents in the supported subagent directories. Ask Tau how to do it and have it consult the extension’s own documentation; the built-in
|
|
95
|
+
Gives Tau a subagent delegation tool for isolated, focused work. Tau can continue a retained child thread when follow-up work depends on its prior reads and reasoning. You can also create your own subagents in the supported subagent directories. Ask Tau how to do it and have it consult the extension’s own documentation; the built-in agents show the pattern. Each subagent can register its own model and the tools it is allowed to use.
|
|
96
96
|
|
|
97
97
|
## tau-help
|
|
98
98
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shanepadgett/tau-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Tau is a custom agentic harness built with pi extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@shanepadgett/tau-tui": "0.
|
|
31
|
+
"@shanepadgett/tau-tui": "0.13.0",
|
|
32
32
|
"@toon-format/toon": "2.3.0",
|
|
33
33
|
"smol-toml": "1.7.0"
|
|
34
34
|
},
|