takomi 2.1.3 → 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.
@@ -194,6 +194,13 @@ export type RunHooks = {
194
194
  export type JsonRunHooks = {
195
195
  onAssistantText?: (text: string) => void;
196
196
  onEventText?: (line: string) => void;
197
+ onToolEvent?: (event: {
198
+ type: "start" | "update" | "end";
199
+ toolName: string;
200
+ args?: string;
201
+ isError?: boolean;
202
+ summary?: string;
203
+ }) => void;
197
204
  onStderr?: (chunk: string) => void;
198
205
  };
199
206
 
@@ -346,6 +353,18 @@ export async function runPiAgentJson(cwd: string, args: string[], signal?: Abort
346
353
  const messageText = summarizeJsonEvent(event);
347
354
  if (messageText) hooks?.onEventText?.(messageText);
348
355
 
356
+ if (event.type === "tool_execution_start") {
357
+ const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
358
+ const args = typeof event.args === "string" ? event.args : event.args ? JSON.stringify(event.args) : undefined;
359
+ hooks?.onToolEvent?.({ type: "start", toolName, args, summary: messageText });
360
+ } else if (event.type === "tool_execution_update") {
361
+ const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
362
+ hooks?.onToolEvent?.({ type: "update", toolName, summary: messageText });
363
+ } else if (event.type === "tool_execution_end") {
364
+ const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
365
+ hooks?.onToolEvent?.({ type: "end", toolName, isError: event.isError === true, summary: messageText });
366
+ }
367
+
349
368
  if (event.type === "message_end") {
350
369
  const message = event.message;
351
370
  if (message && typeof message === "object" && (message as { role?: string }).role === "assistant") {
@@ -25,6 +25,13 @@ export type TakomiSubagentRun = {
25
25
  boardTaskStatus?: TakomiBoardTaskStatus;
26
26
  summary?: string;
27
27
  outputText?: string;
28
+ currentTool?: string;
29
+ currentToolArgs?: string;
30
+ currentToolStartedAt?: number;
31
+ recentTools?: Array<{ tool: string; args: string; endMs: number }>;
32
+ recentOutput?: string[];
33
+ toolCount?: number;
34
+ sessionFile?: string;
28
35
  logs: string[];
29
36
  startedAt: number;
30
37
  updatedAt: number;
@@ -53,6 +53,16 @@ export type TakomiDispatchResult = {
53
53
  output: string;
54
54
  stderr: string;
55
55
  preflight: string;
56
+ startedAt?: number;
57
+ endedAt?: number;
58
+ lastActivityAt?: number;
59
+ currentTool?: string;
60
+ currentToolArgs?: string;
61
+ currentToolStartedAt?: number;
62
+ recentTools?: Array<{ tool: string; args: string; endMs: number }>;
63
+ recentOutput?: string[];
64
+ toolCount?: number;
65
+ sessionFile?: string;
56
66
  };
57
67
 
58
68
  export type TakomiDispatchHooks = {
@@ -72,6 +82,21 @@ export async function dispatchTakomiSubagent(
72
82
  const sessionDir = path.join(input.rootCwd, ".pi", "takomi", "subagents");
73
83
  const sessionPath = path.join(sessionDir, `${conversationId}.jsonl`);
74
84
  await mkdir(sessionDir, { recursive: true });
85
+ const startedAt = Date.now();
86
+ let lastActivityAt = startedAt;
87
+ let currentTool: string | undefined;
88
+ let currentToolArgs: string | undefined;
89
+ let currentToolStartedAt: number | undefined;
90
+ let toolCount = 0;
91
+ let recentTools: Array<{ tool: string; args: string; endMs: number }> = [];
92
+ let recentOutput: string[] = [];
93
+
94
+ const appendRecentOutput = (text: string) => {
95
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
96
+ if (!lines.length) return;
97
+ recentOutput = [...recentOutput, ...lines].slice(-8);
98
+ lastActivityAt = Date.now();
99
+ };
75
100
 
76
101
  hooks?.emit?.({
77
102
  type: "start",
@@ -123,6 +148,13 @@ export async function dispatchTakomiSubagent(
123
148
  output: "",
124
149
  stderr: preflight.report,
125
150
  preflight: preflight.report,
151
+ startedAt,
152
+ endedAt: Date.now(),
153
+ lastActivityAt,
154
+ recentTools,
155
+ recentOutput,
156
+ toolCount,
157
+ sessionFile: sessionPath,
126
158
  };
127
159
  hooks?.emit?.({
128
160
  type: "block",
@@ -154,13 +186,62 @@ export async function dispatchTakomiSubagent(
154
186
 
155
187
  const result = await runPiAgentJson(subagentCwd, args, signal, {
156
188
  onAssistantText: (text) => {
157
- hooks?.emit?.({ type: "update", runKey, patch: { outputText: text, boardTaskStatus: input.boardTaskStatus, checklist: input.checklist } });
189
+ appendRecentOutput(text);
190
+ hooks?.emit?.({
191
+ type: "update",
192
+ runKey,
193
+ patch: {
194
+ outputText: text,
195
+ recentOutput,
196
+ currentTool,
197
+ currentToolArgs,
198
+ currentToolStartedAt,
199
+ recentTools,
200
+ toolCount,
201
+ boardTaskStatus: input.boardTaskStatus,
202
+ checklist: input.checklist,
203
+ },
204
+ });
158
205
  },
159
206
  onEventText: (line) => {
207
+ appendRecentOutput(line);
160
208
  hooks?.emit?.({ type: "appendLog", runKey, chunk: line });
209
+ hooks?.emit?.({ type: "update", runKey, patch: { recentOutput, boardTaskStatus: input.boardTaskStatus, checklist: input.checklist } });
210
+ },
211
+ onToolEvent: (event) => {
212
+ lastActivityAt = Date.now();
213
+ if (event.type === "start") {
214
+ currentTool = event.toolName;
215
+ currentToolArgs = event.args;
216
+ currentToolStartedAt = Date.now();
217
+ toolCount += 1;
218
+ } else if (event.type === "end") {
219
+ recentTools = [...recentTools, { tool: event.toolName, args: currentTool === event.toolName ? currentToolArgs ?? "" : "", endMs: Date.now() }].slice(-8);
220
+ if (currentTool === event.toolName) {
221
+ currentTool = undefined;
222
+ currentToolArgs = undefined;
223
+ currentToolStartedAt = undefined;
224
+ }
225
+ }
226
+ hooks?.emit?.({
227
+ type: "update",
228
+ runKey,
229
+ patch: {
230
+ currentTool,
231
+ currentToolArgs,
232
+ currentToolStartedAt,
233
+ recentTools,
234
+ recentOutput,
235
+ toolCount,
236
+ boardTaskStatus: input.boardTaskStatus,
237
+ checklist: input.checklist,
238
+ },
239
+ });
161
240
  },
162
241
  onStderr: (chunk) => {
242
+ appendRecentOutput(chunk);
163
243
  hooks?.emit?.({ type: "appendLog", runKey, chunk });
244
+ hooks?.emit?.({ type: "update", runKey, patch: { recentOutput, boardTaskStatus: input.boardTaskStatus, checklist: input.checklist } });
164
245
  },
165
246
  });
166
247
 
@@ -177,6 +258,16 @@ export async function dispatchTakomiSubagent(
177
258
  output,
178
259
  stderr: result.stderr.trim(),
179
260
  preflight: preflight.report,
261
+ startedAt,
262
+ endedAt: Date.now(),
263
+ lastActivityAt,
264
+ currentTool,
265
+ currentToolArgs,
266
+ currentToolStartedAt,
267
+ recentTools,
268
+ recentOutput,
269
+ toolCount,
270
+ sessionFile: sessionPath,
180
271
  };
181
272
 
182
273
  if (result.code !== 0) {
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import { renderTakomiSubagentCall, renderTakomiSubagentResult } from "./native-render";
4
- import { executeTakomiSubagentTool } from "./tool-runner";
4
+ import { createTakomiPiSubagentsEngine } from "./pi-subagents-engine";
5
5
 
6
6
  const ChecklistItemSchema = Type.Object({
7
7
  text: Type.String(),
@@ -50,6 +50,7 @@ const SubagentParameters = Type.Object({
50
50
  });
51
51
 
52
52
  function registerSubagentTool(pi: ExtensionAPI): void {
53
+ const engine = createTakomiPiSubagentsEngine(pi);
53
54
  pi.registerTool({
54
55
  name: "takomi_subagent",
55
56
  label: "Takomi",
@@ -62,8 +63,8 @@ function registerSubagentTool(pi: ExtensionAPI): void {
62
63
  "If review sends work back to the same agent, reuse the same conversationId for continuity.",
63
64
  ],
64
65
  parameters: SubagentParameters,
65
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
66
- return executeTakomiSubagentTool(pi, params, signal, onUpdate, ctx);
66
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
67
+ return engine.execute(toolCallId, params, signal, onUpdate as any, ctx);
67
68
  },
68
69
  renderCall: renderTakomiSubagentCall,
69
70
  renderResult: renderTakomiSubagentResult,
@@ -16,11 +16,30 @@ type LiveTask = {
16
16
  conversationId?: string;
17
17
  };
18
18
 
19
+ function mergeRecentTools(
20
+ existing: Array<{ tool: string; args: string; endMs: number }> | undefined,
21
+ incoming: Array<{ tool: string; args: string; endMs: number }> | undefined,
22
+ ): Array<{ tool: string; args: string; endMs: number }> | undefined {
23
+ if (!incoming?.length) return existing;
24
+ return incoming.slice(-8);
25
+ }
26
+
19
27
  function appendLine(value: string, line: string): string {
20
28
  const next = [value, line.trim()].filter(Boolean).join("\n");
21
29
  return next.split(/\r?\n/).slice(-12).join("\n");
22
30
  }
23
31
 
32
+ function compactOutputPreview(text: string): string[] {
33
+ const lines = text
34
+ .split(/\r?\n/)
35
+ .map((line) => line.replace(/\s+/g, " ").trim())
36
+ .filter(Boolean)
37
+ .filter((line) => !/^\*\*[^*]+\*\*\s+I\s/i.test(line))
38
+ .filter((line) => !/\bI need to make sure\b/i.test(line))
39
+ .filter((line) => !/\bI want the user to\b/i.test(line));
40
+ return lines.slice(-3);
41
+ }
42
+
24
43
  export function createTakomiLiveUpdateBridge(
25
44
  tasks: LiveTask[],
26
45
  mode: "single" | "parallel" | "chain",
@@ -38,6 +57,11 @@ export function createTakomiLiveUpdateBridge(
38
57
  output: "Queued.",
39
58
  stderr: "",
40
59
  preflight: "",
60
+ startedAt: Date.now(),
61
+ lastActivityAt: Date.now(),
62
+ recentTools: [],
63
+ recentOutput: ["Queued."],
64
+ toolCount: 0,
41
65
  }));
42
66
 
43
67
  const emit = () => {
@@ -54,23 +78,52 @@ export function createTakomiLiveUpdateBridge(
54
78
  event(index: number, event: TakomiSubagentRuntimeEvent): void {
55
79
  const current = results[index];
56
80
  if (!current) return;
81
+ current.lastActivityAt = Date.now();
57
82
  if (event.type === "start") {
83
+ current.startedAt = current.startedAt ?? Date.now();
58
84
  current.conversationId = event.state.conversationId ?? current.conversationId;
59
85
  current.thinking = event.state.thinking ?? current.thinking;
86
+ current.sessionFile = event.state.sessionFile ?? current.sessionFile;
60
87
  current.output = event.state.summary ?? "Starting.";
88
+ current.recentOutput = [current.output].filter(Boolean).slice(-8);
61
89
  } else if (event.type === "update") {
62
90
  current.model = event.patch.model ?? current.model;
63
91
  current.thinking = event.patch.thinking ?? current.thinking;
92
+ current.sessionFile = event.patch.sessionFile ?? current.sessionFile;
93
+ current.currentTool = event.patch.currentTool;
94
+ current.currentToolArgs = event.patch.currentToolArgs;
95
+ current.currentToolStartedAt = event.patch.currentToolStartedAt;
96
+ current.recentTools = mergeRecentTools(current.recentTools, event.patch.recentTools);
97
+ current.toolCount = event.patch.toolCount ?? current.toolCount;
64
98
  current.output = event.patch.outputText ?? event.patch.summary ?? current.output;
99
+ current.recentOutput = event.patch.recentOutput
100
+ ?? (event.patch.outputText ? compactOutputPreview(event.patch.outputText) : current.recentOutput);
65
101
  if (event.patch.logs?.length) current.output = appendLine(current.output, event.patch.logs.join("\n"));
66
102
  } else if (event.type === "appendLog") {
67
103
  current.output = appendLine(current.output, event.chunk);
104
+ current.recentOutput = [...(current.recentOutput ?? []), event.chunk.trim()].filter(Boolean).slice(-8);
68
105
  } else if (event.type === "complete") {
69
106
  current.code = 0;
107
+ current.endedAt = Date.now();
108
+ current.currentTool = undefined;
109
+ current.currentToolArgs = undefined;
110
+ current.currentToolStartedAt = undefined;
70
111
  current.output = event.patch?.outputText ?? event.patch?.summary ?? current.output;
112
+ current.recentOutput = event.patch?.recentOutput ?? current.recentOutput;
113
+ current.recentTools = mergeRecentTools(current.recentTools, event.patch?.recentTools);
114
+ current.toolCount = event.patch?.toolCount ?? current.toolCount;
115
+ current.sessionFile = event.patch?.sessionFile ?? current.sessionFile;
71
116
  } else if (event.type === "block") {
72
117
  current.code = 1;
118
+ current.endedAt = Date.now();
119
+ current.currentTool = undefined;
120
+ current.currentToolArgs = undefined;
121
+ current.currentToolStartedAt = undefined;
73
122
  current.output = event.patch?.outputText ?? event.patch?.summary ?? current.output;
123
+ current.recentOutput = event.patch?.recentOutput ?? current.recentOutput;
124
+ current.recentTools = mergeRecentTools(current.recentTools, event.patch?.recentTools);
125
+ current.toolCount = event.patch?.toolCount ?? current.toolCount;
126
+ current.sessionFile = event.patch?.sessionFile ?? current.sessionFile;
74
127
  if (event.patch?.logs?.length) current.stderr = event.patch.logs.join("\n");
75
128
  }
76
129
  emit();
@@ -1,174 +1,37 @@
1
- import { renderSubagentResult, syncResultAnimation } from "pi-subagents/src/tui/render";
2
- import type { AgentToolResult } from "@mariozechner/pi-agent-core";
3
- import type { Details, SingleResult, AgentProgress } from "pi-subagents/src/shared/types";
4
- import type { Theme } from "@mariozechner/pi-coding-agent";
5
- import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
6
- import type { TakomiDispatchResult } from "./dispatch";
7
- import type { TakomiSubagentToolParams } from "./tool-runner";
8
-
9
- type ToolResult = {
10
- content?: Array<{ type: string; text?: string }>;
11
- details?: {
12
- results?: TakomiDispatchResult[];
13
- mode?: "single" | "parallel" | "chain";
14
- agentScope?: string;
15
- plan?: unknown;
16
- };
17
- isError?: boolean;
18
- };
19
-
20
- function taskList(params: TakomiSubagentToolParams): Array<{ agent: string; task: string }> {
21
- if (params.chain?.length) return params.chain;
22
- if (params.tasks?.length) return params.tasks;
23
- if (params.agent || params.task) return [{ agent: params.agent ?? "...", task: params.task ?? "..." }];
24
- return [];
25
- }
26
-
27
- export function renderTakomiSubagentCall(params: TakomiSubagentToolParams, theme: Theme) {
28
- const tasks = taskList(params);
29
- const mode = params.chain?.length ? "chain" : params.tasks?.length ? "parallel" : "single";
30
- if (tasks.length === 1) {
31
- return new Text(
32
- `${theme.fg("toolTitle", theme.bold("takomi_subagent "))}${theme.fg("accent", tasks[0]?.agent || "?")}`,
33
- 0,
34
- 0,
35
- );
36
- }
37
- return new Text(
38
- `${theme.fg("toolTitle", theme.bold("takomi_subagent "))}${mode} (${tasks.length})`,
39
- 0,
40
- 0,
41
- );
42
- }
43
-
44
- function parseTakomiOutput(outputText: string) {
45
- const rawLines = outputText.split(/\r?\n/);
46
- const textLines: string[] = [];
47
- const recentTools: Array<{ tool: string; args: string; endMs: number }> = [];
48
- let currentTool: string | undefined;
49
- let currentToolArgs: string | undefined;
50
-
51
- for (const line of rawLines) {
52
- if (!line.trim()) continue;
53
-
54
- // 1. Check for tool lifecycle markers
55
- if (line.startsWith("Tool start: ")) {
56
- currentTool = line.replace("Tool start: ", "").trim();
57
- continue;
58
- }
59
- if (line.startsWith("Tool complete: ") || line.startsWith("Tool failed: ")) {
60
- const toolName = line.replace(/Tool (complete|failed): /, "").trim();
61
- recentTools.push({ tool: toolName, args: "", endMs: Date.now() });
62
- if (currentTool === toolName) {
63
- currentTool = undefined;
64
- currentToolArgs = undefined;
65
- }
66
- continue;
67
- }
68
-
69
- // 2. Handle JSON blobs (tool calls)
70
- const jsonStartIdx = line.indexOf('{"');
71
- if (jsonStartIdx !== -1) {
72
- const beforeJson = line.substring(0, jsonStartIdx).trim();
73
- if (beforeJson) textLines.push(beforeJson);
74
-
75
- const jsonPart = line.substring(jsonStartIdx);
76
- try {
77
- const parsed = JSON.parse(jsonPart);
78
- const toolName = parsed.tool || (parsed.command ? parsed.command.split(" ")[0] : undefined);
79
- if (toolName) {
80
- const args = parsed.args ? JSON.stringify(parsed.args) : (parsed.command || "");
81
- currentTool = toolName;
82
- currentToolArgs = args;
83
- }
84
- } catch (e) {
85
- if (jsonPart.trim().length > 1) {
86
- textLines.push(jsonPart);
87
- }
88
- }
89
- continue;
90
- }
91
-
92
- // 3. Skip lone brackets
93
- const trimmed = line.trim();
94
- if (trimmed === "{" || trimmed === "}") continue;
95
-
96
- // 4. Everything else is text (thoughts)
97
- textLines.push(line);
98
- }
99
-
100
- return { textLines, recentTools, currentTool, currentToolArgs };
101
- }
102
-
103
- export function renderTakomiSubagentResult(result: ToolResult, options: { expanded?: boolean; isPartial?: boolean }, theme: Theme, context: any) {
104
- const details = result.details;
105
- const results = details?.results ?? [];
106
-
107
- if (results.length === 0) {
108
- const text = result.content?.find((item) => item.type === "text")?.text ?? "(no output)";
109
- return new Text(text, 0, 0);
110
- }
111
-
112
- const mappedResults: SingleResult[] = results.map((r, i) => {
113
- const isRunning = r.code === -1;
114
- const outputText = r.output || r.stderr || "";
115
- const { textLines, recentTools, currentTool, currentToolArgs } = parseTakomiOutput(outputText);
116
-
117
- const recentOutput = textLines.slice(-5);
118
-
119
- const progress: AgentProgress | undefined = isRunning ? {
120
- index: i,
121
- agent: r.agent,
122
- status: "running",
123
- task: r.task || "",
124
- lastActivityAt: Date.now(),
125
- currentTool,
126
- currentToolArgs,
127
- currentToolStartedAt: currentTool ? Date.now() : undefined,
128
- recentTools,
129
- recentOutput,
130
- toolCount: recentTools.length + (currentTool ? 1 : 0),
131
- tokens: 0,
132
- durationMs: 0,
133
- } : {
134
- index: i,
135
- agent: r.agent,
136
- status: "completed", // Takomi results in mappedResults are usually final if code !== -1
137
- task: r.task || "",
138
- lastActivityAt: Date.now(),
139
- recentTools,
140
- recentOutput: [],
141
- toolCount: recentTools.length,
142
- tokens: 0,
143
- durationMs: 0,
144
- };
145
-
146
- return {
147
- agent: r.agent,
148
- task: r.task || "",
149
- exitCode: isRunning ? 0 : r.code,
150
- model: r.model,
151
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
152
- progress,
153
- truncation: isRunning ? undefined : {
154
- text: textLines.join("\n").replace(/\n/g, " \n"),
155
- truncated: false
156
- }
157
- };
158
- });
159
-
160
- const mappedDetails: Details = {
161
- mode: details?.mode ?? "single",
162
- results: mappedResults,
163
- chainAgents: details?.mode === "chain" ? results.map(r => r.agent) : undefined,
164
- };
165
-
166
- const agentToolResult: AgentToolResult<Details> = {
167
- content: result.content as any || [],
168
- details: mappedDetails,
169
- };
170
-
171
- syncResultAnimation(agentToolResult, context);
172
- return renderSubagentResult(agentToolResult, { expanded: options.expanded ?? false }, theme);
173
- }
174
-
1
+ import { renderSubagentResult, syncResultAnimation } from "pi-subagents/src/tui/render";
2
+ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
3
+ import type { Details } from "pi-subagents/src/shared/types";
4
+ import type { Theme } from "@mariozechner/pi-coding-agent";
5
+ import { Text } from "@mariozechner/pi-tui";
6
+ import type { TakomiSubagentToolParams } from "./tool-runner";
7
+
8
+ type ToolResult = AgentToolResult<Details>;
9
+
10
+ function taskList(params: TakomiSubagentToolParams): Array<{ agent: string; task: string }> {
11
+ if (params.chain?.length) return params.chain;
12
+ if (params.tasks?.length) return params.tasks;
13
+ if (params.agent || params.task) return [{ agent: params.agent ?? "...", task: params.task ?? "..." }];
14
+ return [];
15
+ }
16
+
17
+ export function renderTakomiSubagentCall(params: TakomiSubagentToolParams, theme: Theme) {
18
+ const tasks = taskList(params);
19
+ const mode = params.chain?.length ? "chain" : params.tasks?.length ? "parallel" : "single";
20
+ if (tasks.length === 1) {
21
+ return new Text(
22
+ `${theme.fg("toolTitle", theme.bold("takomi_subagent "))}${theme.fg("accent", tasks[0]?.agent || "?")}`,
23
+ 0,
24
+ 0,
25
+ );
26
+ }
27
+ return new Text(
28
+ `${theme.fg("toolTitle", theme.bold("takomi_subagent "))}${mode} (${tasks.length})`,
29
+ 0,
30
+ 0,
31
+ );
32
+ }
33
+
34
+ export function renderTakomiSubagentResult(result: ToolResult, options: { expanded?: boolean; isPartial?: boolean }, theme: Theme, context: any) {
35
+ syncResultAnimation(result, context);
36
+ return renderSubagentResult(result, { expanded: options.expanded ?? false }, theme);
37
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takomi",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -44,6 +44,7 @@ import { runDoctor } from './doctor.js';
44
44
  import { ensurePiInstalled, ensurePiSubagentsInstalled, launchTakomiHarness, printPiInstallResult, printPiSubagentsInstallResult } from './pi-harness.js';
45
45
  import { installPiHarnessAssets, printPiInstallSummary, syncPiHarnessAssets, validatePiHarnessInstall } from './pi-installer.js';
46
46
  import { installBundledSkills, printSkillsInstallSummary, validateSkillsInstall } from './skills-installer.js';
47
+ import { notifyIfTakomiUpdateAvailable, printTakomiUpdateStatus, upgradeTakomiPackage } from './update-check.js';
47
48
 
48
49
  const packageJson = await fs.readJson(PATHS.packageJson);
49
50
  const program = new Command();
@@ -713,6 +714,18 @@ program
713
714
  .description('Run Pi/Takomi installation diagnostics')
714
715
  .action(() => runDoctor({ version: program.version() }));
715
716
 
717
+ program
718
+ .command('check-update')
719
+ .description('Check whether a newer Takomi package is available')
720
+ .action(() => printTakomiUpdateStatus(program.version()));
721
+
722
+ program
723
+ .command('upgrade')
724
+ .description('Manually update the global Takomi CLI package from npm')
725
+ .action(() => {
726
+ process.exitCode = upgradeTakomiPackage();
727
+ });
728
+
716
729
  // Update from GitHub (EXISTING — enhanced)
717
730
  program
718
731
  .command('update')
@@ -783,6 +796,7 @@ program
783
796
  });
784
797
 
785
798
  if (process.argv.length <= 2) {
799
+ notifyIfTakomiUpdateAvailable(program.version());
786
800
  const exitCode = await launchTakomiHarness(process.cwd());
787
801
  process.exitCode = exitCode;
788
802
  } else {
@@ -0,0 +1,140 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import https from 'https';
5
+ import { spawnSync } from 'child_process';
6
+ import pc from 'picocolors';
7
+
8
+ const CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
9
+ const CACHE_PATH = path.join(os.homedir(), '.takomi', 'update-check.json');
10
+ const REGISTRY_URL = 'https://registry.npmjs.org/takomi/latest';
11
+
12
+ function parseVersion(version = '') {
13
+ const [core] = String(version).replace(/^v/, '').split('-');
14
+ return core.split('.').map((part) => Number.parseInt(part, 10) || 0);
15
+ }
16
+
17
+ export function isNewerVersion(latest, current) {
18
+ const a = parseVersion(latest);
19
+ const b = parseVersion(current);
20
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
21
+ const left = a[i] || 0;
22
+ const right = b[i] || 0;
23
+ if (left > right) return true;
24
+ if (left < right) return false;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ function fetchLatestPackageInfo(timeoutMs = 2500) {
30
+ return new Promise((resolve) => {
31
+ const req = https.get(REGISTRY_URL, {
32
+ headers: {
33
+ 'accept': 'application/json',
34
+ 'user-agent': 'takomi-update-check',
35
+ },
36
+ timeout: timeoutMs,
37
+ }, (res) => {
38
+ let body = '';
39
+ res.setEncoding('utf8');
40
+ res.on('data', (chunk) => { body += chunk; });
41
+ res.on('end', () => {
42
+ if (res.statusCode && res.statusCode >= 400) return resolve(null);
43
+ try {
44
+ resolve(JSON.parse(body));
45
+ } catch {
46
+ resolve(null);
47
+ }
48
+ });
49
+ });
50
+ req.on('timeout', () => {
51
+ req.destroy();
52
+ resolve(null);
53
+ });
54
+ req.on('error', () => resolve(null));
55
+ });
56
+ }
57
+
58
+ async function readCache() {
59
+ try {
60
+ return await fs.readJson(CACHE_PATH);
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ async function writeCache(cache) {
67
+ try {
68
+ await fs.ensureDir(path.dirname(CACHE_PATH));
69
+ await fs.writeJson(CACHE_PATH, cache, { spaces: 2 });
70
+ } catch {
71
+ // Update checks must never block normal Takomi startup.
72
+ }
73
+ }
74
+
75
+ export async function getLatestTakomiVersion({ currentVersion, force = false } = {}) {
76
+ const now = Date.now();
77
+ const cache = await readCache();
78
+ if (!force && cache?.checkedAt && now - cache.checkedAt < CHECK_INTERVAL_MS) {
79
+ return cache;
80
+ }
81
+
82
+ const info = await fetchLatestPackageInfo();
83
+ const latestVersion = typeof info?.version === 'string' ? info.version : null;
84
+ const next = {
85
+ checkedAt: now,
86
+ currentVersion,
87
+ latestVersion,
88
+ updateAvailable: Boolean(latestVersion && currentVersion && isNewerVersion(latestVersion, currentVersion)),
89
+ };
90
+ await writeCache(next);
91
+ return next;
92
+ }
93
+
94
+ export function notifyIfTakomiUpdateAvailable(currentVersion) {
95
+ if (process.env.TAKOMI_NO_UPDATE_CHECK === '1') return;
96
+
97
+ // Fire-and-forget by design: launching the Takomi harness must never wait on
98
+ // network, DNS, npm registry latency, or cache file IO.
99
+ setTimeout(() => {
100
+ getLatestTakomiVersion({ currentVersion })
101
+ .then((result) => {
102
+ if (!result?.updateAvailable) return;
103
+ console.log(pc.yellow(`\n⬆ Takomi ${result.latestVersion} is available (installed: ${currentVersion}).`));
104
+ console.log(pc.dim(' Run: takomi upgrade'));
105
+ console.log(pc.dim(' Disable this check with TAKOMI_NO_UPDATE_CHECK=1.\n'));
106
+ })
107
+ .catch(() => {
108
+ // Silent: update checks must never affect harness startup or usage.
109
+ });
110
+ }, 0).unref?.();
111
+ }
112
+
113
+ export async function printTakomiUpdateStatus(currentVersion) {
114
+ const result = await getLatestTakomiVersion({ currentVersion, force: true });
115
+ if (!result?.latestVersion) {
116
+ console.log(pc.yellow('Could not check the npm registry for Takomi updates.'));
117
+ return;
118
+ }
119
+ if (result.updateAvailable) {
120
+ console.log(pc.yellow(`Takomi ${result.latestVersion} is available (installed: ${currentVersion}).`));
121
+ console.log(pc.dim('Run: takomi upgrade'));
122
+ return;
123
+ }
124
+ console.log(pc.green(`Takomi is up to date (${currentVersion}).`));
125
+ }
126
+
127
+ export function upgradeTakomiPackage() {
128
+ console.log(pc.cyan('Updating Takomi from npm...\n'));
129
+ const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
130
+ const result = spawnSync(command, ['install', '-g', 'takomi@latest'], {
131
+ stdio: 'inherit',
132
+ shell: process.platform === 'win32',
133
+ });
134
+ if (result.status === 0) {
135
+ console.log(pc.green('\nTakomi updated. Run `takomi --version` to confirm.'));
136
+ return 0;
137
+ }
138
+ console.log(pc.red('\nTakomi update failed. Try manually: npm install -g takomi@latest'));
139
+ return result.status || 1;
140
+ }