@shanepadgett/tau-agent 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/context.md +14 -9
- package/docs/subagents.md +7 -5
- package/extensions/cache-diagnostics/index.ts +3 -4
- package/extensions/context/README.md +8 -5
- package/extensions/context/definitions.ts +28 -17
- package/extensions/context/evidence.ts +4 -3
- package/extensions/context/index.ts +57 -35
- package/extensions/context/panel.ts +10 -9
- package/extensions/context/projection.ts +141 -0
- package/extensions/context/state.ts +30 -0
- package/extensions/explore/ast/read/hook.ts +3 -1
- package/extensions/ideas/browser.ts +27 -16
- package/extensions/review/README.md +11 -0
- package/extensions/review/index.ts +135 -0
- package/extensions/review/model.ts +144 -0
- package/extensions/review/panel.ts +128 -0
- package/extensions/review/session.ts +106 -0
- package/extensions/script-runner/README.md +7 -0
- package/extensions/script-runner/index.ts +275 -0
- package/extensions/stash/browser.ts +30 -18
- package/extensions/subagent/README.md +17 -4
- package/extensions/subagent/agents/context-sync.md +2 -2
- package/extensions/subagent/agents/scout.md +48 -61
- package/extensions/subagent/agents.ts +0 -1
- package/extensions/subagent/cmux-dashboard.ts +7 -3
- package/extensions/subagent/index.ts +105 -4
- package/extensions/subagent/panel.ts +124 -0
- package/extensions/subagent/render.ts +2 -1
- package/extensions/subagent/run.ts +9 -9
- package/extensions/subagent/runtime.ts +5 -5
- package/extensions/subagent/session-resource.ts +19 -127
- package/extensions/subagent/settings.ts +18 -0
- package/extensions/tau-help/help.md +11 -7
- package/extensions/working-memory/README.md +1 -1
- package/extensions/working-memory/checkpoint.ts +3 -4
- package/extensions/working-memory/index.ts +25 -7
- package/extensions/working-memory/memory.ts +53 -82
- package/package.json +2 -2
- package/schemas/tau.schema.json +9 -23
- package/shared/context-messages.ts +19 -0
- package/shared/injected-context.ts +2 -2
- package/shared/isolated-session.ts +151 -0
- package/extensions/subagent/agents/review.md +0 -75
- package/extensions/turn-budget/README.md +0 -14
- package/extensions/turn-budget/index.ts +0 -116
- package/extensions/turn-budget/settings.ts +0 -35
|
@@ -6,9 +6,17 @@ import { createToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
|
6
6
|
import contextSettings from "../context/settings.ts";
|
|
7
7
|
import { discoverAgents, type AgentDiscovery } from "./agents.ts";
|
|
8
8
|
import { createCmuxDashboard, type CmuxDashboard, type DashboardOrphan } from "./cmux-dashboard.ts";
|
|
9
|
+
import { createAgentsPanel } from "./panel.ts";
|
|
9
10
|
import { renderSubagentCall, renderSubagentResult } from "./render.ts";
|
|
10
11
|
import type { SubagentDetails } from "./run.ts";
|
|
11
12
|
import { failedToolResult, SubagentRuntime } from "./runtime.ts";
|
|
13
|
+
import subagentSettings from "./settings.ts";
|
|
14
|
+
|
|
15
|
+
interface SubagentSessionState {
|
|
16
|
+
disabled: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SUBAGENT_SESSION_STATE_TYPE = "tau.subagent.disabled";
|
|
12
20
|
|
|
13
21
|
const params = Type.Union([
|
|
14
22
|
Type.Object(
|
|
@@ -39,6 +47,7 @@ const params = Type.Union([
|
|
|
39
47
|
|
|
40
48
|
export default function subagentExtension(pi: ExtensionAPI): void {
|
|
41
49
|
const runtime = new SubagentRuntime(pi);
|
|
50
|
+
let sessionDisabled = new Set<string>();
|
|
42
51
|
let failureNotify: ((message: string) => void) | undefined;
|
|
43
52
|
const orphans: DashboardOrphan[] = [];
|
|
44
53
|
const retryOrphans = async () => {
|
|
@@ -122,13 +131,74 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
122
131
|
}
|
|
123
132
|
for (const path of fingerprints.keys()) if (!current.has(path)) fingerprints.delete(path);
|
|
124
133
|
};
|
|
134
|
+
const disabledAgentNames = async (ctx: ExtensionContext) => {
|
|
135
|
+
const configured = new Set((await loadTauExtensionSettings(ctx, subagentSettings)).disabled);
|
|
136
|
+
return { configured, effective: new Set([...configured, ...sessionDisabled]) };
|
|
137
|
+
};
|
|
125
138
|
const parentVisibleAgents = async (ctx: ExtensionContext, discovery: AgentDiscovery) => {
|
|
126
|
-
const sync =
|
|
139
|
+
const [sync, disabled] = await Promise.all([
|
|
140
|
+
loadTauExtensionSettings(ctx, contextSettings).then((settings) => settings.sync),
|
|
141
|
+
disabledAgentNames(ctx),
|
|
142
|
+
]);
|
|
127
143
|
const parentVisible = sync.enabled && sync.automation;
|
|
128
144
|
return [...discovery.agents.values()]
|
|
129
|
-
.filter((agent) => agent.name !== "context-sync" || parentVisible)
|
|
145
|
+
.filter((agent) => !disabled.effective.has(agent.name) && (agent.name !== "context-sync" || parentVisible))
|
|
130
146
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
131
147
|
};
|
|
148
|
+
const restoreSessionState = (ctx: ExtensionContext) => {
|
|
149
|
+
let saved: string[] | undefined;
|
|
150
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
151
|
+
if (entry.type !== "custom" || entry.customType !== SUBAGENT_SESSION_STATE_TYPE) continue;
|
|
152
|
+
const data = entry.data as { disabled?: unknown } | undefined;
|
|
153
|
+
if (Array.isArray(data?.disabled)) {
|
|
154
|
+
saved = data.disabled.filter((name): name is string => typeof name === "string" && name.length > 0);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
sessionDisabled = new Set(saved ?? []);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
pi.registerCommand("agents", {
|
|
161
|
+
description: "Enable/disable subagents for this session",
|
|
162
|
+
handler: async (_args, ctx) => {
|
|
163
|
+
if (ctx.mode !== "tui") {
|
|
164
|
+
ctx.ui.notify("/agents requires TUI mode", "error");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const [discovery, disabled] = await Promise.all([
|
|
169
|
+
discoverAgents(ctx.cwd, ctx.isProjectTrusted()),
|
|
170
|
+
disabledAgentNames(ctx),
|
|
171
|
+
]);
|
|
172
|
+
warn(discovery, ctx);
|
|
173
|
+
const agents = [...discovery.agents.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
174
|
+
if (agents.length === 0) {
|
|
175
|
+
ctx.ui.notify("No valid subagents found", "warning");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await ctx.ui.custom((tui, theme, _keybindings, done) =>
|
|
180
|
+
createAgentsPanel(
|
|
181
|
+
tui,
|
|
182
|
+
theme,
|
|
183
|
+
agents.map((agent) => ({
|
|
184
|
+
id: agent.name,
|
|
185
|
+
disabled: disabled.effective.has(agent.name),
|
|
186
|
+
configured: disabled.configured.has(agent.name),
|
|
187
|
+
})),
|
|
188
|
+
(disabledNames) => {
|
|
189
|
+
for (const agent of agents) {
|
|
190
|
+
if (!disabled.configured.has(agent.name)) sessionDisabled.delete(agent.name);
|
|
191
|
+
}
|
|
192
|
+
for (const name of disabledNames) sessionDisabled.add(name);
|
|
193
|
+
pi.appendEntry<SubagentSessionState>(SUBAGENT_SESSION_STATE_TYPE, {
|
|
194
|
+
disabled: [...sessionDisabled].sort(),
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
() => done(undefined),
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
132
202
|
|
|
133
203
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
134
204
|
if (!pi.getActiveTools().includes("subagent")) return undefined;
|
|
@@ -147,7 +217,7 @@ Pass \`files\` when exact relevant files are already known. Tau autoreads curren
|
|
|
147
217
|
|
|
148
218
|
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.
|
|
149
219
|
|
|
150
|
-
|
|
220
|
+
Scout only for substantial multi-hop lookup that would flood parent context. Skip few known-path reads, single declaration lookups, or small digs with most evidence already in hand; use tools directly. When uncertain, dig yourself.`;
|
|
151
221
|
return { systemPrompt: `${event.systemPrompt}\n\n${prompt}` };
|
|
152
222
|
});
|
|
153
223
|
pi.registerTool(
|
|
@@ -180,6 +250,27 @@ Review limit: for one user task or coherent implementation batch, call the \`rev
|
|
|
180
250
|
return failedToolResult(agent, task, "queue", parentModel, parentThinking, error, threadKey);
|
|
181
251
|
}
|
|
182
252
|
|
|
253
|
+
if (continuing) {
|
|
254
|
+
const thread = runtime.listThreads(ctx.cwd).find((item) => item.id === threadKey);
|
|
255
|
+
if (thread) {
|
|
256
|
+
const disabled = await disabledAgentNames(ctx);
|
|
257
|
+
if (disabled.effective.has(thread.definition.name)) {
|
|
258
|
+
const source = disabled.configured.has(thread.definition.name)
|
|
259
|
+
? "in Tau settings"
|
|
260
|
+
: "for this session";
|
|
261
|
+
return failedToolResult(
|
|
262
|
+
thread.definition.name,
|
|
263
|
+
task,
|
|
264
|
+
"discovery",
|
|
265
|
+
parentModel,
|
|
266
|
+
parentThinking,
|
|
267
|
+
`Agent ${thread.definition.name} is disabled ${source}.`,
|
|
268
|
+
threadKey,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
183
274
|
return runtime.execute({
|
|
184
275
|
agent,
|
|
185
276
|
task,
|
|
@@ -215,6 +306,11 @@ Review limit: for one user task or coherent implementation batch, call the \`rev
|
|
|
215
306
|
error: `Agent ${agent} discovery failed: ${reason}. Runnable agents: ${names}`,
|
|
216
307
|
};
|
|
217
308
|
}
|
|
309
|
+
const disabled = await disabledAgentNames(ctx);
|
|
310
|
+
if (disabled.effective.has(definition.name)) {
|
|
311
|
+
const source = disabled.configured.has(definition.name) ? "in Tau settings" : "for this session";
|
|
312
|
+
return { ok: false, phase: "discovery", error: `Agent ${definition.name} is disabled ${source}.` };
|
|
313
|
+
}
|
|
218
314
|
if (definition.name === "context-sync") {
|
|
219
315
|
const sync = (await loadTauExtensionSettings(ctx, contextSettings)).sync;
|
|
220
316
|
if (!sync.enabled) {
|
|
@@ -257,13 +353,17 @@ Review limit: for one user task or coherent implementation batch, call the \`rev
|
|
|
257
353
|
const details = event.details as SubagentDetails | undefined;
|
|
258
354
|
if (details?.status === "failed" || details?.status === "aborted") return { isError: true };
|
|
259
355
|
});
|
|
260
|
-
pi.on("session_start", async () => {
|
|
356
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
357
|
+
restoreSessionState(ctx);
|
|
261
358
|
await runtime.reset();
|
|
262
359
|
await replaceDashboard();
|
|
263
360
|
rowState.clear();
|
|
264
361
|
fingerprints.clear();
|
|
265
362
|
failureNotify = undefined;
|
|
266
363
|
});
|
|
364
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
365
|
+
restoreSessionState(ctx);
|
|
366
|
+
});
|
|
267
367
|
pi.on("session_shutdown", async () => {
|
|
268
368
|
unsubscribeDashboard();
|
|
269
369
|
await runtime.shutdown();
|
|
@@ -273,5 +373,6 @@ Review limit: for one user task or coherent implementation batch, call the \`rev
|
|
|
273
373
|
rowState.clear();
|
|
274
374
|
fingerprints.clear();
|
|
275
375
|
failureNotify = undefined;
|
|
376
|
+
sessionDisabled.clear();
|
|
276
377
|
});
|
|
277
378
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, getKeybindings, Key, matchesKey, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
rawHint,
|
|
5
|
+
SelectableList,
|
|
6
|
+
type SelectableListResult,
|
|
7
|
+
ToolPanel,
|
|
8
|
+
type ToolPanelConfig,
|
|
9
|
+
} from "@shanepadgett/tau-tui";
|
|
10
|
+
|
|
11
|
+
export interface AgentPanelItem {
|
|
12
|
+
id: string;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
configured: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createAgentsPanel(
|
|
18
|
+
tui: TUI,
|
|
19
|
+
theme: Theme,
|
|
20
|
+
initial: readonly AgentPanelItem[],
|
|
21
|
+
onApply: (disabled: readonly string[]) => void,
|
|
22
|
+
done: () => void,
|
|
23
|
+
): Component {
|
|
24
|
+
return new AgentsPanel(tui, theme, initial, onApply, done);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class AgentsPanel implements Component {
|
|
28
|
+
private readonly tui: TUI;
|
|
29
|
+
private readonly onApply: (disabled: readonly string[]) => void;
|
|
30
|
+
private readonly done: () => void;
|
|
31
|
+
private readonly list: SelectableList<AgentPanelItem>;
|
|
32
|
+
private readonly panelConfig: ToolPanelConfig;
|
|
33
|
+
private readonly panel: ToolPanel;
|
|
34
|
+
private items: readonly AgentPanelItem[];
|
|
35
|
+
private closed = false;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
tui: TUI,
|
|
39
|
+
theme: Theme,
|
|
40
|
+
initial: readonly AgentPanelItem[],
|
|
41
|
+
onApply: (disabled: readonly string[]) => void,
|
|
42
|
+
done: () => void,
|
|
43
|
+
) {
|
|
44
|
+
this.tui = tui;
|
|
45
|
+
this.items = initial;
|
|
46
|
+
this.onApply = onApply;
|
|
47
|
+
this.done = done;
|
|
48
|
+
this.list = new SelectableList(theme, {
|
|
49
|
+
items: initial,
|
|
50
|
+
emptyMessage: "No valid subagents found.",
|
|
51
|
+
selection: { kind: "single", primaryLabel: "apply" },
|
|
52
|
+
actions: [],
|
|
53
|
+
cancelLabel: "close",
|
|
54
|
+
maxVisible: 10,
|
|
55
|
+
renderItem: (item, state, width) => {
|
|
56
|
+
const label = state.active ? theme.bold(item.id) : item.id;
|
|
57
|
+
const status = item.configured
|
|
58
|
+
? theme.fg("muted", "disabled by Tau settings")
|
|
59
|
+
: item.disabled
|
|
60
|
+
? theme.fg("warning", "disabled")
|
|
61
|
+
: theme.fg("success", "enabled");
|
|
62
|
+
return [truncateToWidth(`${label} ${status}`, width, "…")];
|
|
63
|
+
},
|
|
64
|
+
onResult: (result) => this.handleResult(result),
|
|
65
|
+
});
|
|
66
|
+
this.panelConfig = {
|
|
67
|
+
title: "Subagents",
|
|
68
|
+
secondary: "Changes apply to this session.",
|
|
69
|
+
body: this.list,
|
|
70
|
+
footer: { kind: "hints", hints: this.hints() },
|
|
71
|
+
border: "box",
|
|
72
|
+
};
|
|
73
|
+
this.panel = new ToolPanel(theme, this.panelConfig);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
render(width: number): string[] {
|
|
77
|
+
return this.panel.render(width);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
invalidate(): void {
|
|
81
|
+
this.panel.invalidate();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
handleInput(data: string): void {
|
|
85
|
+
if (matchesKey(data, Key.space) || data === " ") {
|
|
86
|
+
const current = this.list.getCurrentItem();
|
|
87
|
+
if (!current || current.configured) return;
|
|
88
|
+
this.items = this.items.map((item) => (item.id === current.id ? { ...item, disabled: !item.disabled } : item));
|
|
89
|
+
this.list.setItems(this.items, current.id);
|
|
90
|
+
this.syncPanel();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (getKeybindings().matches(data, "tui.select.confirm")) {
|
|
94
|
+
this.onApply(this.items.filter((item) => item.disabled && !item.configured).map((item) => item.id));
|
|
95
|
+
this.closed = true;
|
|
96
|
+
this.done();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.list.handleInput(data);
|
|
100
|
+
if (!this.closed) this.syncPanel();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private handleResult(result: SelectableListResult<AgentPanelItem>): void {
|
|
104
|
+
if (result.kind === "cancel") {
|
|
105
|
+
this.closed = true;
|
|
106
|
+
this.done();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private syncPanel(): void {
|
|
112
|
+
this.panelConfig.footer = { kind: "hints", hints: this.hints() };
|
|
113
|
+
this.tui.requestRender();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private hints() {
|
|
117
|
+
const hints = this.list.getKeyHints();
|
|
118
|
+
const current = this.list.getCurrentItem();
|
|
119
|
+
if (current && !current.configured) {
|
|
120
|
+
hints.splice(1, 0, rawHint("Space", "toggle"));
|
|
121
|
+
}
|
|
122
|
+
return hints;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -48,7 +48,8 @@ export function renderSubagentResult(
|
|
|
48
48
|
return text;
|
|
49
49
|
}
|
|
50
50
|
const identity = `${details.displayName} (${details.agent})`;
|
|
51
|
-
const
|
|
51
|
+
const ctx = typeof details.contextPercent === "number" ? ` · ${details.contextPercent.toFixed(1)}% ctx` : "";
|
|
52
|
+
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${ctx}`)}`;
|
|
52
53
|
if (!expanded) {
|
|
53
54
|
text.setText(`${header} ${theme.fg("muted", details.task.replace(/\s+/g, " ").trim())}`);
|
|
54
55
|
return text;
|
|
@@ -13,14 +13,10 @@ import {
|
|
|
13
13
|
type ExtensionContext,
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { createCompleteFileMeta } from "../../shared/full-file-knowledge.ts";
|
|
16
|
+
import { createIsolatedSessionResource, type IsolatedSessionResource } from "../../shared/isolated-session.ts";
|
|
16
17
|
import type { AgentDefinition } from "./agents.ts";
|
|
17
18
|
import { emptySubagentResumeState, type RetainedTurnOutcome, type SubagentResumeState } from "./resume.ts";
|
|
18
|
-
import {
|
|
19
|
-
createSubagentSessionResource,
|
|
20
|
-
resolveSubagentSessionInputs,
|
|
21
|
-
type SubagentSessionInputs,
|
|
22
|
-
type SubagentSessionResource,
|
|
23
|
-
} from "./session-resource.ts";
|
|
19
|
+
import { resolveSubagentSessionInputs, type SubagentSessionInputs } from "./session-resource.ts";
|
|
24
20
|
|
|
25
21
|
const PREVIEW_LIMIT = 600;
|
|
26
22
|
const VALUE_LIMIT = 180;
|
|
@@ -62,6 +58,7 @@ export interface SubagentDetails {
|
|
|
62
58
|
omittedActions: number;
|
|
63
59
|
omittedErrors: number;
|
|
64
60
|
usage: SubagentUsage;
|
|
61
|
+
contextPercent?: number;
|
|
65
62
|
durationMs: number;
|
|
66
63
|
truncation?: {
|
|
67
64
|
truncated: boolean;
|
|
@@ -85,7 +82,7 @@ export interface SubagentThread {
|
|
|
85
82
|
displayName: string;
|
|
86
83
|
definition: AgentDefinition;
|
|
87
84
|
sessionInputs: SubagentSessionInputs;
|
|
88
|
-
resource:
|
|
85
|
+
resource: IsolatedSessionResource;
|
|
89
86
|
cwd: string;
|
|
90
87
|
model: string;
|
|
91
88
|
thinkingLevel: string;
|
|
@@ -199,7 +196,7 @@ export async function createSubagentThread(options: {
|
|
|
199
196
|
extensionPaths: readonly string[];
|
|
200
197
|
initialTask: string;
|
|
201
198
|
ctx: ExtensionContext;
|
|
202
|
-
thinkingLevel:
|
|
199
|
+
thinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
|
|
203
200
|
signal: AbortSignal;
|
|
204
201
|
onWarning?: (warning: string) => void;
|
|
205
202
|
}): Promise<SubagentThread> {
|
|
@@ -222,7 +219,7 @@ export async function createSubagentThread(options: {
|
|
|
222
219
|
signal,
|
|
223
220
|
...(onWarning === undefined ? {} : { onWarning }),
|
|
224
221
|
});
|
|
225
|
-
const resource = await
|
|
222
|
+
const resource = await createIsolatedSessionResource(sessionInputs, signal);
|
|
226
223
|
try {
|
|
227
224
|
return {
|
|
228
225
|
id,
|
|
@@ -307,6 +304,9 @@ export async function runSubagentTurn(options: {
|
|
|
307
304
|
if (!force && now - lastTextUpdate < 100) return;
|
|
308
305
|
lastTextUpdate = now;
|
|
309
306
|
details.durationMs = now - started;
|
|
307
|
+
const context = session.getContextUsage();
|
|
308
|
+
if (typeof context?.percent === "number") details.contextPercent = context.percent;
|
|
309
|
+
else delete details.contextPercent;
|
|
310
310
|
const snapshot = cloneSnapshot(details);
|
|
311
311
|
snapshot.actions = snapshot.actions.slice(-5);
|
|
312
312
|
// Detached observer chain — must not delay prompt completion.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { createIsolatedSessionResource, type IsolatedSessionResource } from "../../shared/isolated-session.ts";
|
|
3
4
|
import type { AgentDefinition } from "./agents.ts";
|
|
4
5
|
import { buildColdResumePrompt, retainSubagentTurn } from "./resume.ts";
|
|
5
6
|
import {
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
type SubagentPhase,
|
|
16
17
|
type SubagentThread,
|
|
17
18
|
} from "./run.ts";
|
|
18
|
-
import { createSubagentSessionResource, type SubagentSessionResource } from "./session-resource.ts";
|
|
19
19
|
|
|
20
20
|
const MAX_RETAINED_THREADS = 16;
|
|
21
21
|
const GLOBAL_CONCURRENCY = 4;
|
|
@@ -216,7 +216,7 @@ export class SubagentRuntime {
|
|
|
216
216
|
definition?: AgentDefinition;
|
|
217
217
|
ctx: ExtensionContext;
|
|
218
218
|
parentModel: string;
|
|
219
|
-
parentThinking:
|
|
219
|
+
parentThinking: NonNullable<ExtensionContext["thinkingLevel"]>;
|
|
220
220
|
signal?: AbortSignal;
|
|
221
221
|
onUpdate?: (details: SubagentDetails) => void | Promise<void>;
|
|
222
222
|
resolveFreshDefinition: () => Promise<
|
|
@@ -286,7 +286,7 @@ export class SubagentRuntime {
|
|
|
286
286
|
threadKey?: string;
|
|
287
287
|
ctx: ExtensionContext;
|
|
288
288
|
parentModel: string;
|
|
289
|
-
parentThinking:
|
|
289
|
+
parentThinking: NonNullable<ExtensionContext["thinkingLevel"]>;
|
|
290
290
|
definition?: AgentDefinition;
|
|
291
291
|
signal?: AbortSignal;
|
|
292
292
|
onUpdate?: (details: SubagentDetails) => void | Promise<void>;
|
|
@@ -316,7 +316,7 @@ export class SubagentRuntime {
|
|
|
316
316
|
let releaseGlobal: (() => void) | undefined;
|
|
317
317
|
let reservedThread: TrackedThread | undefined;
|
|
318
318
|
let provisionalThread: TrackedThread | undefined;
|
|
319
|
-
let provisionalResource:
|
|
319
|
+
let provisionalResource: IsolatedSessionResource | undefined;
|
|
320
320
|
let reservationToken: symbol | undefined;
|
|
321
321
|
let admitAdvanced = false;
|
|
322
322
|
let phase: SubagentPhase = "queue";
|
|
@@ -513,7 +513,7 @@ export class SubagentRuntime {
|
|
|
513
513
|
phase = "startup";
|
|
514
514
|
fanOut({ ...active.snapshot, status: "starting", phase: "startup", agent, threadId: thread.id }, true);
|
|
515
515
|
const oldResource = thread.resource;
|
|
516
|
-
provisionalResource = await
|
|
516
|
+
provisionalResource = await createIsolatedSessionResource(thread.sessionInputs, combined);
|
|
517
517
|
if (!this.isLive(generation, combined) || thread.disposed || this.threads.get(thread.id) !== thread) {
|
|
518
518
|
await provisionalResource.dispose();
|
|
519
519
|
provisionalResource = undefined;
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
DefaultResourceLoader,
|
|
4
|
-
getAgentDir,
|
|
5
|
-
ModelRuntime,
|
|
6
|
-
SessionManager,
|
|
7
|
-
type AgentSession,
|
|
8
|
-
type ExtensionContext,
|
|
9
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { resolveIsolatedSessionModel, type IsolatedSessionInputs } from "../../shared/isolated-session.ts";
|
|
10
3
|
import type { AgentDefinition, ThinkingLevel } from "./agents.ts";
|
|
11
4
|
|
|
12
5
|
const CHILD_UI_BLOCKED_METHODS = new Set([
|
|
@@ -18,25 +11,10 @@ const CHILD_UI_BLOCKED_METHODS = new Set([
|
|
|
18
11
|
"setWorkingIndicator",
|
|
19
12
|
]);
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
type SelectedProvider = NonNullable<ReturnType<ExtensionContext["modelRegistry"]["getProvider"]>>;
|
|
23
|
-
|
|
24
|
-
export interface SubagentSessionInputs {
|
|
14
|
+
export interface SubagentSessionInputs extends IsolatedSessionInputs {
|
|
25
15
|
definition: AgentDefinition;
|
|
26
|
-
extensionPaths: readonly string[];
|
|
27
|
-
cwd: string;
|
|
28
|
-
model: SelectedModel;
|
|
29
16
|
modelName: string;
|
|
30
|
-
provider: SelectedProvider;
|
|
31
|
-
runtimeApiKey: string | undefined;
|
|
32
17
|
thinkingLevel: ThinkingLevel;
|
|
33
|
-
bindTarget: { mode: "print" } | { mode: "tui"; uiContext: ExtensionContext["ui"] };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SubagentSessionResource {
|
|
37
|
-
readonly inputs: SubagentSessionInputs;
|
|
38
|
-
readonly session: AgentSession;
|
|
39
|
-
dispose(): Promise<void>;
|
|
40
18
|
}
|
|
41
19
|
|
|
42
20
|
function childUiContext(ui: ExtensionContext["ui"]): ExtensionContext["ui"] {
|
|
@@ -53,117 +31,31 @@ export async function resolveSubagentSessionInputs(options: {
|
|
|
53
31
|
definition: AgentDefinition;
|
|
54
32
|
extensionPaths: readonly string[];
|
|
55
33
|
ctx: ExtensionContext;
|
|
56
|
-
parentThinkingLevel:
|
|
34
|
+
parentThinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
|
|
57
35
|
signal: AbortSignal;
|
|
58
36
|
onWarning?: (warning: string) => void;
|
|
59
37
|
}): Promise<SubagentSessionInputs> {
|
|
60
38
|
const { definition, extensionPaths, ctx, signal, onWarning } = options;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
else {
|
|
72
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(configured);
|
|
73
|
-
if (!auth.ok) onWarning?.(`model ${definition.model} is unavailable: ${auth.error}; using parent model`);
|
|
74
|
-
else {
|
|
75
|
-
model = configured;
|
|
76
|
-
selectedAuth = auth;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (definition.thinking) {
|
|
81
|
-
const mapped = model?.thinkingLevelMap?.[definition.thinking];
|
|
82
|
-
const unsupported =
|
|
83
|
-
!model?.reasoning ||
|
|
84
|
-
mapped === null ||
|
|
85
|
-
((definition.thinking === "xhigh" || definition.thinking === "max") && mapped === undefined);
|
|
86
|
-
if (unsupported)
|
|
87
|
-
onWarning?.(`thinking ${definition.thinking} is unavailable for the selected model; using parent thinking`);
|
|
88
|
-
else thinkingLevel = definition.thinking;
|
|
89
|
-
}
|
|
90
|
-
if (!model) throw new Error(`Agent ${definition.name} startup failed: parent has no model`);
|
|
91
|
-
const auth = selectedAuth ?? (await ctx.modelRegistry.getApiKeyAndHeaders(model));
|
|
92
|
-
if (!auth.ok) throw new Error(`Agent ${definition.name} startup failed: ${auth.error}`);
|
|
93
|
-
const provider = ctx.modelRegistry.getProvider(model.provider);
|
|
94
|
-
if (!provider) throw new Error(`Agent ${definition.name} startup failed: provider ${model.provider} is unavailable`);
|
|
95
|
-
if (signal.aborted) throw new Error(`Agent ${definition.name} startup aborted`);
|
|
39
|
+
const selected = await resolveIsolatedSessionModel({
|
|
40
|
+
label: `Agent ${definition.name}`,
|
|
41
|
+
preferredModel: definition.model,
|
|
42
|
+
preferredThinkingLevel: definition.thinking,
|
|
43
|
+
usePreferredThinkingAfterModelFallback: true,
|
|
44
|
+
ctx,
|
|
45
|
+
parentThinkingLevel: options.parentThinkingLevel,
|
|
46
|
+
signal,
|
|
47
|
+
...(onWarning === undefined ? {} : { onWarning }),
|
|
48
|
+
});
|
|
96
49
|
return {
|
|
50
|
+
...selected,
|
|
51
|
+
label: `Agent ${definition.name}`,
|
|
97
52
|
definition,
|
|
98
53
|
extensionPaths: [...extensionPaths],
|
|
99
54
|
cwd: ctx.cwd,
|
|
100
|
-
model
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
runtimeApiKey:
|
|
104
|
-
auth.apiKey && provider.auth.apiKey && !ctx.modelRegistry.isUsingOAuth(model) ? auth.apiKey : undefined,
|
|
105
|
-
thinkingLevel,
|
|
55
|
+
modelName: `${selected.model.provider}/${selected.model.id}`,
|
|
56
|
+
tools: definition.tools,
|
|
57
|
+
customTools: [],
|
|
106
58
|
bindTarget:
|
|
107
59
|
ctx.mode === "tui" && ctx.hasUI ? { mode: "tui", uiContext: childUiContext(ctx.ui) } : { mode: "print" },
|
|
108
60
|
};
|
|
109
61
|
}
|
|
110
|
-
|
|
111
|
-
export async function createSubagentSessionResource(
|
|
112
|
-
inputs: SubagentSessionInputs,
|
|
113
|
-
signal: AbortSignal,
|
|
114
|
-
): Promise<SubagentSessionResource> {
|
|
115
|
-
let session: AgentSession | undefined;
|
|
116
|
-
try {
|
|
117
|
-
if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
|
|
118
|
-
const modelRuntime = await ModelRuntime.create();
|
|
119
|
-
modelRuntime.registerNativeProvider(inputs.provider);
|
|
120
|
-
if (inputs.runtimeApiKey !== undefined)
|
|
121
|
-
await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey, { allowNetwork: false });
|
|
122
|
-
if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
|
|
123
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
124
|
-
cwd: inputs.cwd,
|
|
125
|
-
agentDir: getAgentDir(),
|
|
126
|
-
noExtensions: true,
|
|
127
|
-
additionalExtensionPaths: [...inputs.extensionPaths],
|
|
128
|
-
});
|
|
129
|
-
await resourceLoader.reload();
|
|
130
|
-
if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
|
|
131
|
-
const created = await createAgentSession({
|
|
132
|
-
cwd: inputs.cwd,
|
|
133
|
-
model: inputs.model,
|
|
134
|
-
modelRuntime,
|
|
135
|
-
thinkingLevel: inputs.thinkingLevel,
|
|
136
|
-
tools: inputs.definition.tools,
|
|
137
|
-
excludeTools: ["subagent"],
|
|
138
|
-
resourceLoader,
|
|
139
|
-
sessionManager: SessionManager.inMemory(inputs.cwd),
|
|
140
|
-
});
|
|
141
|
-
session = created.session;
|
|
142
|
-
if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
|
|
143
|
-
await session.bindExtensions(inputs.bindTarget);
|
|
144
|
-
if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
|
|
145
|
-
const active = session.getActiveToolNames().sort();
|
|
146
|
-
const expected = [...inputs.definition.tools].sort();
|
|
147
|
-
if (active.join("\0") !== expected.join("\0") || active.includes("subagent")) {
|
|
148
|
-
const missing = expected.filter((tool) => !active.includes(tool));
|
|
149
|
-
throw new Error(
|
|
150
|
-
`Agent ${inputs.definition.name} startup failed: unavailable tools: ${missing.join(", ") || "active tool mismatch"}`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
let disposed = false;
|
|
154
|
-
return {
|
|
155
|
-
inputs,
|
|
156
|
-
session,
|
|
157
|
-
async dispose() {
|
|
158
|
-
if (disposed) return;
|
|
159
|
-
disposed = true;
|
|
160
|
-
if (session?.isStreaming) await session.abort().catch(() => undefined);
|
|
161
|
-
session?.dispose();
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
} catch (error) {
|
|
165
|
-
if (session?.isStreaming) await session.abort().catch(() => undefined);
|
|
166
|
-
session?.dispose();
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
|
|
3
|
+
|
|
4
|
+
export default defineTauExtensionSettings({
|
|
5
|
+
key: "subagent",
|
|
6
|
+
defaults: { disabled: [] as string[] },
|
|
7
|
+
schema: Type.Object(
|
|
8
|
+
{
|
|
9
|
+
disabled: Type.Optional(
|
|
10
|
+
Type.Array(Type.String({ minLength: 1 }), {
|
|
11
|
+
default: [],
|
|
12
|
+
description: "Agent names unavailable for delegation.",
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
},
|
|
16
|
+
{ additionalProperties: false },
|
|
17
|
+
),
|
|
18
|
+
});
|