takomi 2.1.2 → 2.1.4
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/.pi/README.md +124 -124
- package/.pi/agents/architect.md +15 -15
- package/.pi/agents/coder.md +14 -14
- package/.pi/agents/designer.md +17 -17
- package/.pi/agents/orchestrator.md +22 -22
- package/.pi/agents/reviewer.md +16 -16
- package/.pi/extensions/oauth-router/README.md +125 -125
- package/.pi/extensions/oauth-router/commands.ts +380 -380
- package/.pi/extensions/oauth-router/config.ts +200 -200
- package/.pi/extensions/oauth-router/index.ts +41 -41
- package/.pi/extensions/oauth-router/oauth-flow.ts +154 -154
- package/.pi/extensions/oauth-router/oauth-store.ts +121 -121
- package/.pi/extensions/oauth-router/package.json +14 -14
- package/.pi/extensions/oauth-router/policies.ts +27 -27
- package/.pi/extensions/oauth-router/provider.ts +492 -492
- package/.pi/extensions/oauth-router/scripts/vibe-verify.py +98 -98
- package/.pi/extensions/oauth-router/state.ts +174 -174
- package/.pi/extensions/oauth-router/types.ts +153 -153
- package/.pi/extensions/takomi-runtime/command-text.ts +130 -130
- package/.pi/extensions/takomi-runtime/commands.ts +179 -179
- package/.pi/extensions/takomi-runtime/context-panel.ts +282 -282
- package/.pi/extensions/takomi-runtime/index.ts +1288 -1288
- package/.pi/extensions/takomi-runtime/profile.ts +114 -114
- package/.pi/extensions/takomi-runtime/routing-policy.ts +105 -105
- package/.pi/extensions/takomi-runtime/shared.ts +511 -492
- package/.pi/extensions/takomi-runtime/subagent-controller.ts +364 -364
- package/.pi/extensions/takomi-runtime/subagent-render.ts +501 -501
- package/.pi/extensions/takomi-runtime/subagent-types.ts +90 -83
- package/.pi/extensions/takomi-runtime/ui.ts +133 -133
- package/.pi/extensions/takomi-subagents/agent-aliases.ts +18 -18
- package/.pi/extensions/takomi-subagents/agents.ts +113 -113
- package/.pi/extensions/takomi-subagents/delegation-plan.ts +95 -95
- package/.pi/extensions/takomi-subagents/dispatch-helpers.ts +26 -26
- package/.pi/extensions/takomi-subagents/dispatch.ts +306 -215
- package/.pi/extensions/takomi-subagents/index.ts +76 -75
- package/.pi/extensions/takomi-subagents/live-updates.ts +136 -83
- package/.pi/extensions/takomi-subagents/native-render.ts +5 -142
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +228 -0
- package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -209
- package/.pi/themes/takomi-noir.json +81 -81
- package/package.json +59 -59
- package/src/cli.js +14 -0
- package/src/doctor.js +87 -84
- package/src/pi-harness.js +355 -351
- package/src/pi-installer.js +193 -171
- package/src/pi-takomi-core/index.ts +4 -4
- package/src/pi-takomi-core/orchestration.ts +402 -402
- package/src/pi-takomi-core/routing.ts +93 -93
- package/src/pi-takomi-core/types.ts +173 -173
- package/src/pi-takomi-core/workflows.ts +299 -299
- package/src/skills-installer.js +101 -101
- package/src/update-check.js +140 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { createSubagentExecutor, type SubagentParamsLike } from "pi-subagents/src/runs/foreground/subagent-executor";
|
|
7
|
+
import { discoverAgents as discoverPiAgents, type AgentConfig, type AgentScope } from "pi-subagents/src/agents/agents";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_ARTIFACT_CONFIG,
|
|
10
|
+
TEMP_ARTIFACTS_DIR,
|
|
11
|
+
type Details,
|
|
12
|
+
type ExtensionConfig,
|
|
13
|
+
type SubagentState,
|
|
14
|
+
} from "pi-subagents/src/shared/types";
|
|
15
|
+
import { resolveAgentName } from "./agent-aliases";
|
|
16
|
+
import type { TakomiSubagentToolParams, TakomiSubagentToolTask } from "./tool-runner";
|
|
17
|
+
|
|
18
|
+
type ToolUpdate = (partial: AgentToolResult<Details>) => void;
|
|
19
|
+
|
|
20
|
+
function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
21
|
+
if (parentSessionFile) {
|
|
22
|
+
const baseName = path.basename(parentSessionFile, ".jsonl");
|
|
23
|
+
return path.join(path.dirname(parentSessionFile), baseName);
|
|
24
|
+
}
|
|
25
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "takomi-subagent-session-"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function expandTilde(value: string): string {
|
|
29
|
+
return value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createState(): SubagentState {
|
|
33
|
+
return {
|
|
34
|
+
baseCwd: process.cwd(),
|
|
35
|
+
currentSessionId: null,
|
|
36
|
+
asyncJobs: new Map(),
|
|
37
|
+
foregroundRuns: new Map(),
|
|
38
|
+
foregroundControls: new Map(),
|
|
39
|
+
lastForegroundControlId: null,
|
|
40
|
+
pendingForegroundControlNotices: new Map(),
|
|
41
|
+
cleanupTimers: new Map(),
|
|
42
|
+
lastUiContext: null,
|
|
43
|
+
poller: null,
|
|
44
|
+
completionSeen: new Map(),
|
|
45
|
+
watcher: null,
|
|
46
|
+
watcherRestartTimer: null,
|
|
47
|
+
resultFileCoalescer: {
|
|
48
|
+
schedule: () => false,
|
|
49
|
+
clear: () => {},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveMode(params: TakomiSubagentToolParams): "single" | "parallel" | "chain" | undefined {
|
|
55
|
+
const hasChain = Boolean(params.chain?.length);
|
|
56
|
+
const hasParallel = Boolean(params.tasks?.length);
|
|
57
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
58
|
+
if (Number(hasChain) + Number(hasParallel) + Number(hasSingle) !== 1) return undefined;
|
|
59
|
+
return hasChain ? "chain" : hasParallel ? "parallel" : "single";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveTasks(params: TakomiSubagentToolParams): TakomiSubagentToolTask[] {
|
|
63
|
+
if (params.chain?.length) return params.chain;
|
|
64
|
+
if (params.tasks?.length) return params.tasks;
|
|
65
|
+
if (params.agent && params.task) {
|
|
66
|
+
return [{
|
|
67
|
+
agent: params.agent,
|
|
68
|
+
task: params.task,
|
|
69
|
+
workflow: params.workflow,
|
|
70
|
+
skills: params.skills,
|
|
71
|
+
model: params.model,
|
|
72
|
+
fallbackModels: params.fallbackModels,
|
|
73
|
+
thinking: params.thinking,
|
|
74
|
+
conversationId: params.conversationId,
|
|
75
|
+
cwd: params.cwd,
|
|
76
|
+
checklist: params.checklist,
|
|
77
|
+
}];
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeThinking(value: unknown): string | undefined {
|
|
83
|
+
return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh"].includes(value) ? value : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildTakomiTaskPrompt(task: TakomiSubagentToolTask): string {
|
|
87
|
+
const checklist = task.checklist?.length
|
|
88
|
+
? [
|
|
89
|
+
"Checklist:",
|
|
90
|
+
...task.checklist.map((item) => typeof item === "string" ? `- [ ] ${item}` : `- [${item.done ? "x" : " "}] ${item.text}`),
|
|
91
|
+
].join("\n")
|
|
92
|
+
: "";
|
|
93
|
+
const takomiContext = [
|
|
94
|
+
task.workflow ? `Takomi workflow: ${task.workflow}` : "",
|
|
95
|
+
task.skills?.length ? `Takomi skills/context overlays: ${task.skills.join(", ")}` : "",
|
|
96
|
+
checklist,
|
|
97
|
+
].filter(Boolean).join("\n\n");
|
|
98
|
+
|
|
99
|
+
return takomiContext ? `${takomiContext}\n\n${task.task}` : task.task;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function modelWithThinking(model: string | undefined, thinking: string | undefined): string | undefined {
|
|
103
|
+
const level = normalizeThinking(thinking);
|
|
104
|
+
if (!model || !level || level === "off") return model;
|
|
105
|
+
if (/:(off|minimal|low|medium|high|xhigh)$/i.test(model)) return model;
|
|
106
|
+
return `${model}:${level}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function defaultChildExtensions(): string[] {
|
|
110
|
+
// Child runs must not auto-load every user/project extension because this repo
|
|
111
|
+
// currently has both global and project Takomi extensions, which causes tool
|
|
112
|
+
// name conflicts in children. But model providers such as oauth-router are
|
|
113
|
+
// extensions too, so we explicitly allow the provider extension through.
|
|
114
|
+
const candidates = [
|
|
115
|
+
path.join(os.homedir(), ".pi", "agent", "extensions", "oauth-router", "index.ts"),
|
|
116
|
+
];
|
|
117
|
+
return candidates.filter((candidate) => fs.existsSync(candidate));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function withTakomiAgentDefaults(agent: AgentConfig): AgentConfig {
|
|
121
|
+
return {
|
|
122
|
+
...agent,
|
|
123
|
+
systemPromptMode: agent.systemPromptMode ?? "replace",
|
|
124
|
+
inheritProjectContext: agent.inheritProjectContext ?? true,
|
|
125
|
+
inheritSkills: agent.inheritSkills ?? false,
|
|
126
|
+
defaultContext: agent.defaultContext ?? "fresh",
|
|
127
|
+
extensions: [...new Set([...(agent.extensions ?? []), ...defaultChildExtensions()])],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function discoverUnifiedAgents(cwd: string, scope: AgentScope): { agents: AgentConfig[] } {
|
|
132
|
+
return { agents: discoverPiAgents(cwd, scope).agents.map(withTakomiAgentDefaults) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function agentNameSet(cwd: string): Set<string> {
|
|
136
|
+
return new Set(discoverUnifiedAgents(cwd, "both").agents.map((agent) => agent.name));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function mapSingleTask(task: TakomiSubagentToolTask, names: Set<string>) {
|
|
140
|
+
const resolvedAgent = resolveAgentName(task.agent, new Map([...names].map((name) => [name, { name } as any])));
|
|
141
|
+
return {
|
|
142
|
+
agent: resolvedAgent,
|
|
143
|
+
task: buildTakomiTaskPrompt({ ...task, agent: resolvedAgent }),
|
|
144
|
+
cwd: task.cwd,
|
|
145
|
+
model: modelWithThinking(task.model, task.thinking),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toSubagentParams(params: TakomiSubagentToolParams, rootCwd: string): SubagentParamsLike {
|
|
150
|
+
const mode = resolveMode(params);
|
|
151
|
+
const tasks = resolveTasks(params);
|
|
152
|
+
const names = agentNameSet(rootCwd);
|
|
153
|
+
if (!mode) throw new Error("Provide exactly one mode: agent/task, tasks, or chain.");
|
|
154
|
+
|
|
155
|
+
const base = {
|
|
156
|
+
agentScope: params.agentScope ?? "both",
|
|
157
|
+
cwd: rootCwd,
|
|
158
|
+
context: "fresh" as const,
|
|
159
|
+
async: false,
|
|
160
|
+
clarify: false,
|
|
161
|
+
includeProgress: true,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (mode === "single") {
|
|
165
|
+
const task = tasks[0]!;
|
|
166
|
+
const mapped = mapSingleTask(task, names);
|
|
167
|
+
return {
|
|
168
|
+
...base,
|
|
169
|
+
agent: mapped.agent,
|
|
170
|
+
task: mapped.task,
|
|
171
|
+
cwd: task.cwd ? path.resolve(rootCwd, task.cwd) : rootCwd,
|
|
172
|
+
model: mapped.model,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (mode === "parallel") {
|
|
177
|
+
return {
|
|
178
|
+
...base,
|
|
179
|
+
tasks: tasks.map((task) => mapSingleTask(task, names)),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...base,
|
|
185
|
+
chain: tasks.map((task) => {
|
|
186
|
+
const mapped = mapSingleTask(task, names);
|
|
187
|
+
return {
|
|
188
|
+
agent: mapped.agent,
|
|
189
|
+
task: mapped.task,
|
|
190
|
+
cwd: task.cwd,
|
|
191
|
+
model: mapped.model,
|
|
192
|
+
};
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function createTakomiPiSubagentsEngine(pi: ExtensionAPI) {
|
|
198
|
+
const state = createState();
|
|
199
|
+
const config: ExtensionConfig = {
|
|
200
|
+
maxSubagentDepth: 2,
|
|
201
|
+
asyncByDefault: false,
|
|
202
|
+
forceTopLevelAsync: false,
|
|
203
|
+
};
|
|
204
|
+
const executor = createSubagentExecutor({
|
|
205
|
+
pi,
|
|
206
|
+
state,
|
|
207
|
+
config,
|
|
208
|
+
asyncByDefault: false,
|
|
209
|
+
tempArtifactsDir: TEMP_ARTIFACTS_DIR,
|
|
210
|
+
getSubagentSessionRoot,
|
|
211
|
+
expandTilde,
|
|
212
|
+
discoverAgents: discoverUnifiedAgents,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
async execute(
|
|
217
|
+
id: string,
|
|
218
|
+
params: TakomiSubagentToolParams,
|
|
219
|
+
signal: AbortSignal | undefined,
|
|
220
|
+
onUpdate: ToolUpdate | undefined,
|
|
221
|
+
ctx: ExtensionContext,
|
|
222
|
+
): Promise<AgentToolResult<Details>> {
|
|
223
|
+
const rootCwd = params.cwd ? path.resolve(ctx.cwd, params.cwd) : ctx.cwd;
|
|
224
|
+
const subagentParams = toSubagentParams(params, rootCwd);
|
|
225
|
+
return executor.execute(id, subagentParams, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -1,209 +1,209 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import type { TakomiThinkingLevel } from "../../../src/pi-takomi-core";
|
|
4
|
-
import { loadTakomiProfile } from "../takomi-runtime/profile";
|
|
5
|
-
import {
|
|
6
|
-
TAKOMI_SUBAGENT_EVENT_CHANNEL,
|
|
7
|
-
type TakomiSubagentRuntimeEvent,
|
|
8
|
-
} from "../takomi-runtime/subagent-types";
|
|
9
|
-
import { resolveAgentName } from "./agent-aliases";
|
|
10
|
-
import { discoverTakomiAgents, type TakomiAgentConfig, type TakomiAgentScope } from "./agents";
|
|
11
|
-
import { createTakomiDelegationPlan, renderTakomiDelegationPlan } from "./delegation-plan";
|
|
12
|
-
import { dispatchTakomiSubagent, type TakomiDispatchResult } from "./dispatch";
|
|
13
|
-
import { createTakomiLiveUpdateBridge } from "./live-updates";
|
|
14
|
-
|
|
15
|
-
type ChecklistItem = string | { text: string; done?: boolean };
|
|
16
|
-
|
|
17
|
-
export type TakomiSubagentToolTask = {
|
|
18
|
-
agent: string;
|
|
19
|
-
task: string;
|
|
20
|
-
workflow?: string;
|
|
21
|
-
skills?: string[];
|
|
22
|
-
model?: string;
|
|
23
|
-
fallbackModels?: string[];
|
|
24
|
-
thinking?: TakomiThinkingLevel;
|
|
25
|
-
conversationId?: string;
|
|
26
|
-
cwd?: string;
|
|
27
|
-
checklist?: ChecklistItem[];
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type TakomiSubagentToolParams = Partial<TakomiSubagentToolTask> & {
|
|
31
|
-
tasks?: TakomiSubagentToolTask[];
|
|
32
|
-
chain?: TakomiSubagentToolTask[];
|
|
33
|
-
confirmLaunch?: boolean;
|
|
34
|
-
previewOnly?: boolean;
|
|
35
|
-
agentScope?: TakomiAgentScope;
|
|
36
|
-
confirmProjectAgents?: boolean;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
type ToolUpdate = (partial: {
|
|
40
|
-
content: Array<{ type: "text"; text: string }>;
|
|
41
|
-
details: Record<string, unknown>;
|
|
42
|
-
}) => void;
|
|
43
|
-
const MAX_PARALLEL_TASKS = 8, MAX_CONCURRENCY = 4;
|
|
44
|
-
|
|
45
|
-
function emitRuntimeSubagentEvent(pi: ExtensionAPI, event: TakomiSubagentRuntimeEvent): void {
|
|
46
|
-
pi.events.emit(TAKOMI_SUBAGENT_EVENT_CHANNEL, event);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function resultText(result: TakomiDispatchResult): string {
|
|
50
|
-
return [
|
|
51
|
-
result.preflight,
|
|
52
|
-
result.output || result.stderr || `Subagent ${result.agent} finished without output.`,
|
|
53
|
-
].filter(Boolean).join("\n\n");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function textResult<TDetails extends Record<string, unknown>>(text: string, details: TDetails, isError?: boolean) {
|
|
57
|
-
return { content: [{ type: "text" as const, text }], details, isError };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
61
|
-
items: TIn[],
|
|
62
|
-
concurrency: number,
|
|
63
|
-
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
64
|
-
): Promise<TOut[]> {
|
|
65
|
-
const results = new Array<TOut>(items.length);
|
|
66
|
-
let nextIndex = 0;
|
|
67
|
-
const workers = new Array(Math.min(Math.max(concurrency, 1), items.length)).fill(undefined).map(async () => {
|
|
68
|
-
while (nextIndex < items.length) {
|
|
69
|
-
const current = nextIndex++;
|
|
70
|
-
results[current] = await fn(items[current], current);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
await Promise.all(workers);
|
|
74
|
-
return results;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function hasProjectAgents(tasks: Array<{ agent: string }>, agents: Map<string, TakomiAgentConfig>): boolean {
|
|
78
|
-
return tasks.some((task) => agents.get(task.agent)?.source === "project");
|
|
79
|
-
}
|
|
80
|
-
function resolveMode(params: TakomiSubagentToolParams): "single" | "parallel" | "chain" | undefined {
|
|
81
|
-
const hasChain = Boolean(params.chain?.length);
|
|
82
|
-
const hasParallel = Boolean(params.tasks?.length);
|
|
83
|
-
const hasSingle = Boolean(params.agent && params.task);
|
|
84
|
-
if (Number(hasChain) + Number(hasParallel) + Number(hasSingle) !== 1) return undefined;
|
|
85
|
-
return hasChain ? "chain" : hasParallel ? "parallel" : "single";
|
|
86
|
-
}
|
|
87
|
-
function resolveTasks(params: TakomiSubagentToolParams): TakomiSubagentToolTask[] {
|
|
88
|
-
if (params.chain?.length) return params.chain;
|
|
89
|
-
if (params.tasks?.length) return params.tasks;
|
|
90
|
-
if (params.agent && params.task) {
|
|
91
|
-
return [{
|
|
92
|
-
agent: params.agent,
|
|
93
|
-
task: params.task,
|
|
94
|
-
workflow: params.workflow,
|
|
95
|
-
skills: params.skills,
|
|
96
|
-
model: params.model,
|
|
97
|
-
fallbackModels: params.fallbackModels,
|
|
98
|
-
thinking: params.thinking,
|
|
99
|
-
conversationId: params.conversationId,
|
|
100
|
-
cwd: params.cwd,
|
|
101
|
-
checklist: params.checklist,
|
|
102
|
-
}];
|
|
103
|
-
}
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
export async function executeTakomiSubagentTool(
|
|
107
|
-
pi: ExtensionAPI,
|
|
108
|
-
params: TakomiSubagentToolParams,
|
|
109
|
-
signal: AbortSignal | undefined,
|
|
110
|
-
onUpdate: ToolUpdate | undefined,
|
|
111
|
-
ctx: ExtensionContext,
|
|
112
|
-
) {
|
|
113
|
-
const rootCwd = params.cwd ? path.resolve(ctx.cwd, params.cwd) : ctx.cwd;
|
|
114
|
-
const profile = await loadTakomiProfile(rootCwd);
|
|
115
|
-
const agentScope = params.agentScope ?? "both";
|
|
116
|
-
const agents = discoverTakomiAgents(rootCwd, agentScope);
|
|
117
|
-
const byName = new Map<string, TakomiAgentConfig>(agents.map((agent) => [agent.name, agent]));
|
|
118
|
-
const mode = resolveMode(params);
|
|
119
|
-
const tasks = resolveTasks(params).map((task) => ({
|
|
120
|
-
...task,
|
|
121
|
-
agent: resolveAgentName(task.agent, byName),
|
|
122
|
-
}));
|
|
123
|
-
|
|
124
|
-
if (!mode) {
|
|
125
|
-
return textResult(
|
|
126
|
-
`Provide exactly one mode: agent/task, tasks, or chain.\nAvailable agents: ${agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none"}`,
|
|
127
|
-
{ results: [], availableAgents: agents.map((agent) => agent.name), agentScope },
|
|
128
|
-
true,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
if (mode === "parallel" && tasks.length > MAX_PARALLEL_TASKS) {
|
|
132
|
-
return textResult(`Too many parallel tasks (${tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`, { results: [], agentScope }, true);
|
|
133
|
-
}
|
|
134
|
-
if (params.confirmProjectAgents !== false && ctx.hasUI && hasProjectAgents(tasks, byName)) {
|
|
135
|
-
const names = tasks.map((task) => byName.get(task.agent)).filter((agent): agent is TakomiAgentConfig => agent?.source === "project").map((agent) => agent.name);
|
|
136
|
-
const ok = await ctx.ui.confirm("Run project-local Takomi agents?", `Agents: ${[...new Set(names)].join(", ")}\n\nProject agents are repo-controlled. Continue only for trusted repositories.`);
|
|
137
|
-
if (!ok) return textResult("Canceled: project-local agents not approved.", { results: [], agentScope, mode });
|
|
138
|
-
}
|
|
139
|
-
const plan = createTakomiDelegationPlan({
|
|
140
|
-
source: "takomi-tool",
|
|
141
|
-
launchMode: profile.launchMode ?? "auto",
|
|
142
|
-
profile,
|
|
143
|
-
tasks: tasks.map((task, index) => ({
|
|
144
|
-
id: task.conversationId ?? `direct-${index + 1}`,
|
|
145
|
-
title: task.task,
|
|
146
|
-
agent: task.agent,
|
|
147
|
-
task: task.task,
|
|
148
|
-
workflow: task.workflow,
|
|
149
|
-
model: task.model,
|
|
150
|
-
fallbackModels: task.fallbackModels,
|
|
151
|
-
thinking: task.thinking,
|
|
152
|
-
conversationId: task.conversationId,
|
|
153
|
-
checklist: task.checklist,
|
|
154
|
-
dispatchPolicy: "subagent",
|
|
155
|
-
})),
|
|
156
|
-
});
|
|
157
|
-
if (params.previewOnly || (plan.launchMode === "manual" && !params.confirmLaunch)) {
|
|
158
|
-
return textResult(renderTakomiDelegationPlan(plan), { plan, availableAgents: agents.map((agent) => agent.name), agentScope, mode });
|
|
159
|
-
}
|
|
160
|
-
const live = createTakomiLiveUpdateBridge(tasks, mode, agentScope, onUpdate);
|
|
161
|
-
const runOne = async (item: TakomiSubagentToolTask, index: number, previousOutput = "") => {
|
|
162
|
-
const config = byName.get(item.agent);
|
|
163
|
-
if (!config) throw new Error(`Unknown subagent '${item.agent}'. Available: ${agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none"}`);
|
|
164
|
-
const result = await dispatchTakomiSubagent(ctx, {
|
|
165
|
-
agent: config,
|
|
166
|
-
task: item.task.replaceAll("{previous}", previousOutput),
|
|
167
|
-
rootCwd,
|
|
168
|
-
cwd: item.cwd,
|
|
169
|
-
workflow: item.workflow,
|
|
170
|
-
skills: item.skills,
|
|
171
|
-
model: item.model,
|
|
172
|
-
fallbackModels: item.fallbackModels,
|
|
173
|
-
thinking: item.thinking,
|
|
174
|
-
conversationId: item.conversationId,
|
|
175
|
-
checklist: item.checklist,
|
|
176
|
-
source: "takomi-tool",
|
|
177
|
-
}, signal, {
|
|
178
|
-
emit: (event) => {
|
|
179
|
-
emitRuntimeSubagentEvent(pi, event);
|
|
180
|
-
live.event(index, event);
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
live.finish(index, result);
|
|
184
|
-
return result;
|
|
185
|
-
};
|
|
186
|
-
const results: TakomiDispatchResult[] = [];
|
|
187
|
-
try {
|
|
188
|
-
if (mode === "chain") {
|
|
189
|
-
let previousOutput = "";
|
|
190
|
-
for (const [index, item] of tasks.entries()) {
|
|
191
|
-
const result = await runOne(item, index, previousOutput);
|
|
192
|
-
previousOutput = result.output;
|
|
193
|
-
results.push(result);
|
|
194
|
-
if (result.code !== 0) return textResult(resultText(result), { results, mode, agentScope }, true);
|
|
195
|
-
}
|
|
196
|
-
} else if (mode === "parallel") {
|
|
197
|
-
results.push(...await mapWithConcurrencyLimit(tasks, MAX_CONCURRENCY, async (item, index) => {
|
|
198
|
-
return runOne(item, index);
|
|
199
|
-
}));
|
|
200
|
-
} else {
|
|
201
|
-
results.push(await runOne(tasks[0], 0));
|
|
202
|
-
}
|
|
203
|
-
} catch (error) {
|
|
204
|
-
return textResult(error instanceof Error ? error.message : String(error), { results, mode, agentScope }, true);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const failed = results.find((result) => result.code !== 0);
|
|
208
|
-
return textResult(results.map(resultText).join("\n\n---\n\n"), { results, mode, agentScope }, Boolean(failed) || undefined);
|
|
209
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { TakomiThinkingLevel } from "../../../src/pi-takomi-core";
|
|
4
|
+
import { loadTakomiProfile } from "../takomi-runtime/profile";
|
|
5
|
+
import {
|
|
6
|
+
TAKOMI_SUBAGENT_EVENT_CHANNEL,
|
|
7
|
+
type TakomiSubagentRuntimeEvent,
|
|
8
|
+
} from "../takomi-runtime/subagent-types";
|
|
9
|
+
import { resolveAgentName } from "./agent-aliases";
|
|
10
|
+
import { discoverTakomiAgents, type TakomiAgentConfig, type TakomiAgentScope } from "./agents";
|
|
11
|
+
import { createTakomiDelegationPlan, renderTakomiDelegationPlan } from "./delegation-plan";
|
|
12
|
+
import { dispatchTakomiSubagent, type TakomiDispatchResult } from "./dispatch";
|
|
13
|
+
import { createTakomiLiveUpdateBridge } from "./live-updates";
|
|
14
|
+
|
|
15
|
+
type ChecklistItem = string | { text: string; done?: boolean };
|
|
16
|
+
|
|
17
|
+
export type TakomiSubagentToolTask = {
|
|
18
|
+
agent: string;
|
|
19
|
+
task: string;
|
|
20
|
+
workflow?: string;
|
|
21
|
+
skills?: string[];
|
|
22
|
+
model?: string;
|
|
23
|
+
fallbackModels?: string[];
|
|
24
|
+
thinking?: TakomiThinkingLevel;
|
|
25
|
+
conversationId?: string;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
checklist?: ChecklistItem[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TakomiSubagentToolParams = Partial<TakomiSubagentToolTask> & {
|
|
31
|
+
tasks?: TakomiSubagentToolTask[];
|
|
32
|
+
chain?: TakomiSubagentToolTask[];
|
|
33
|
+
confirmLaunch?: boolean;
|
|
34
|
+
previewOnly?: boolean;
|
|
35
|
+
agentScope?: TakomiAgentScope;
|
|
36
|
+
confirmProjectAgents?: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ToolUpdate = (partial: {
|
|
40
|
+
content: Array<{ type: "text"; text: string }>;
|
|
41
|
+
details: Record<string, unknown>;
|
|
42
|
+
}) => void;
|
|
43
|
+
const MAX_PARALLEL_TASKS = 8, MAX_CONCURRENCY = 4;
|
|
44
|
+
|
|
45
|
+
function emitRuntimeSubagentEvent(pi: ExtensionAPI, event: TakomiSubagentRuntimeEvent): void {
|
|
46
|
+
pi.events.emit(TAKOMI_SUBAGENT_EVENT_CHANNEL, event);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resultText(result: TakomiDispatchResult): string {
|
|
50
|
+
return [
|
|
51
|
+
result.preflight,
|
|
52
|
+
result.output || result.stderr || `Subagent ${result.agent} finished without output.`,
|
|
53
|
+
].filter(Boolean).join("\n\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function textResult<TDetails extends Record<string, unknown>>(text: string, details: TDetails, isError?: boolean) {
|
|
57
|
+
return { content: [{ type: "text" as const, text }], details, isError };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
61
|
+
items: TIn[],
|
|
62
|
+
concurrency: number,
|
|
63
|
+
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
64
|
+
): Promise<TOut[]> {
|
|
65
|
+
const results = new Array<TOut>(items.length);
|
|
66
|
+
let nextIndex = 0;
|
|
67
|
+
const workers = new Array(Math.min(Math.max(concurrency, 1), items.length)).fill(undefined).map(async () => {
|
|
68
|
+
while (nextIndex < items.length) {
|
|
69
|
+
const current = nextIndex++;
|
|
70
|
+
results[current] = await fn(items[current], current);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
await Promise.all(workers);
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasProjectAgents(tasks: Array<{ agent: string }>, agents: Map<string, TakomiAgentConfig>): boolean {
|
|
78
|
+
return tasks.some((task) => agents.get(task.agent)?.source === "project");
|
|
79
|
+
}
|
|
80
|
+
function resolveMode(params: TakomiSubagentToolParams): "single" | "parallel" | "chain" | undefined {
|
|
81
|
+
const hasChain = Boolean(params.chain?.length);
|
|
82
|
+
const hasParallel = Boolean(params.tasks?.length);
|
|
83
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
84
|
+
if (Number(hasChain) + Number(hasParallel) + Number(hasSingle) !== 1) return undefined;
|
|
85
|
+
return hasChain ? "chain" : hasParallel ? "parallel" : "single";
|
|
86
|
+
}
|
|
87
|
+
function resolveTasks(params: TakomiSubagentToolParams): TakomiSubagentToolTask[] {
|
|
88
|
+
if (params.chain?.length) return params.chain;
|
|
89
|
+
if (params.tasks?.length) return params.tasks;
|
|
90
|
+
if (params.agent && params.task) {
|
|
91
|
+
return [{
|
|
92
|
+
agent: params.agent,
|
|
93
|
+
task: params.task,
|
|
94
|
+
workflow: params.workflow,
|
|
95
|
+
skills: params.skills,
|
|
96
|
+
model: params.model,
|
|
97
|
+
fallbackModels: params.fallbackModels,
|
|
98
|
+
thinking: params.thinking,
|
|
99
|
+
conversationId: params.conversationId,
|
|
100
|
+
cwd: params.cwd,
|
|
101
|
+
checklist: params.checklist,
|
|
102
|
+
}];
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
export async function executeTakomiSubagentTool(
|
|
107
|
+
pi: ExtensionAPI,
|
|
108
|
+
params: TakomiSubagentToolParams,
|
|
109
|
+
signal: AbortSignal | undefined,
|
|
110
|
+
onUpdate: ToolUpdate | undefined,
|
|
111
|
+
ctx: ExtensionContext,
|
|
112
|
+
) {
|
|
113
|
+
const rootCwd = params.cwd ? path.resolve(ctx.cwd, params.cwd) : ctx.cwd;
|
|
114
|
+
const profile = await loadTakomiProfile(rootCwd);
|
|
115
|
+
const agentScope = params.agentScope ?? "both";
|
|
116
|
+
const agents = discoverTakomiAgents(rootCwd, agentScope);
|
|
117
|
+
const byName = new Map<string, TakomiAgentConfig>(agents.map((agent) => [agent.name, agent]));
|
|
118
|
+
const mode = resolveMode(params);
|
|
119
|
+
const tasks = resolveTasks(params).map((task) => ({
|
|
120
|
+
...task,
|
|
121
|
+
agent: resolveAgentName(task.agent, byName),
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
if (!mode) {
|
|
125
|
+
return textResult(
|
|
126
|
+
`Provide exactly one mode: agent/task, tasks, or chain.\nAvailable agents: ${agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none"}`,
|
|
127
|
+
{ results: [], availableAgents: agents.map((agent) => agent.name), agentScope },
|
|
128
|
+
true,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (mode === "parallel" && tasks.length > MAX_PARALLEL_TASKS) {
|
|
132
|
+
return textResult(`Too many parallel tasks (${tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`, { results: [], agentScope }, true);
|
|
133
|
+
}
|
|
134
|
+
if (params.confirmProjectAgents !== false && ctx.hasUI && hasProjectAgents(tasks, byName)) {
|
|
135
|
+
const names = tasks.map((task) => byName.get(task.agent)).filter((agent): agent is TakomiAgentConfig => agent?.source === "project").map((agent) => agent.name);
|
|
136
|
+
const ok = await ctx.ui.confirm("Run project-local Takomi agents?", `Agents: ${[...new Set(names)].join(", ")}\n\nProject agents are repo-controlled. Continue only for trusted repositories.`);
|
|
137
|
+
if (!ok) return textResult("Canceled: project-local agents not approved.", { results: [], agentScope, mode });
|
|
138
|
+
}
|
|
139
|
+
const plan = createTakomiDelegationPlan({
|
|
140
|
+
source: "takomi-tool",
|
|
141
|
+
launchMode: profile.launchMode ?? "auto",
|
|
142
|
+
profile,
|
|
143
|
+
tasks: tasks.map((task, index) => ({
|
|
144
|
+
id: task.conversationId ?? `direct-${index + 1}`,
|
|
145
|
+
title: task.task,
|
|
146
|
+
agent: task.agent,
|
|
147
|
+
task: task.task,
|
|
148
|
+
workflow: task.workflow,
|
|
149
|
+
model: task.model,
|
|
150
|
+
fallbackModels: task.fallbackModels,
|
|
151
|
+
thinking: task.thinking,
|
|
152
|
+
conversationId: task.conversationId,
|
|
153
|
+
checklist: task.checklist,
|
|
154
|
+
dispatchPolicy: "subagent",
|
|
155
|
+
})),
|
|
156
|
+
});
|
|
157
|
+
if (params.previewOnly || (plan.launchMode === "manual" && !params.confirmLaunch)) {
|
|
158
|
+
return textResult(renderTakomiDelegationPlan(plan), { plan, availableAgents: agents.map((agent) => agent.name), agentScope, mode });
|
|
159
|
+
}
|
|
160
|
+
const live = createTakomiLiveUpdateBridge(tasks, mode, agentScope, onUpdate);
|
|
161
|
+
const runOne = async (item: TakomiSubagentToolTask, index: number, previousOutput = "") => {
|
|
162
|
+
const config = byName.get(item.agent);
|
|
163
|
+
if (!config) throw new Error(`Unknown subagent '${item.agent}'. Available: ${agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none"}`);
|
|
164
|
+
const result = await dispatchTakomiSubagent(ctx, {
|
|
165
|
+
agent: config,
|
|
166
|
+
task: item.task.replaceAll("{previous}", previousOutput),
|
|
167
|
+
rootCwd,
|
|
168
|
+
cwd: item.cwd,
|
|
169
|
+
workflow: item.workflow,
|
|
170
|
+
skills: item.skills,
|
|
171
|
+
model: item.model,
|
|
172
|
+
fallbackModels: item.fallbackModels,
|
|
173
|
+
thinking: item.thinking,
|
|
174
|
+
conversationId: item.conversationId,
|
|
175
|
+
checklist: item.checklist,
|
|
176
|
+
source: "takomi-tool",
|
|
177
|
+
}, signal, {
|
|
178
|
+
emit: (event) => {
|
|
179
|
+
emitRuntimeSubagentEvent(pi, event);
|
|
180
|
+
live.event(index, event);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
live.finish(index, result);
|
|
184
|
+
return result;
|
|
185
|
+
};
|
|
186
|
+
const results: TakomiDispatchResult[] = [];
|
|
187
|
+
try {
|
|
188
|
+
if (mode === "chain") {
|
|
189
|
+
let previousOutput = "";
|
|
190
|
+
for (const [index, item] of tasks.entries()) {
|
|
191
|
+
const result = await runOne(item, index, previousOutput);
|
|
192
|
+
previousOutput = result.output;
|
|
193
|
+
results.push(result);
|
|
194
|
+
if (result.code !== 0) return textResult(resultText(result), { results, mode, agentScope }, true);
|
|
195
|
+
}
|
|
196
|
+
} else if (mode === "parallel") {
|
|
197
|
+
results.push(...await mapWithConcurrencyLimit(tasks, MAX_CONCURRENCY, async (item, index) => {
|
|
198
|
+
return runOne(item, index);
|
|
199
|
+
}));
|
|
200
|
+
} else {
|
|
201
|
+
results.push(await runOne(tasks[0], 0));
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return textResult(error instanceof Error ? error.message : String(error), { results, mode, agentScope }, true);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const failed = results.find((result) => result.code !== 0);
|
|
208
|
+
return textResult(results.map(resultText).join("\n\n---\n\n"), { results, mode, agentScope }, Boolean(failed) || undefined);
|
|
209
|
+
}
|