@gotgenes/pi-subagents 5.0.0 → 5.2.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/CHANGELOG.md +41 -0
- package/README.md +176 -133
- package/docs/architecture/architecture.md +141 -92
- package/docs/decisions/0001-deferred-patches.md +11 -5
- package/docs/plans/0048-implement-subagents-api.md +2 -1
- package/docs/plans/0049-remove-group-join-output-file-rpc.md +22 -5
- package/docs/plans/0051-update-adr-0001-hard-fork.md +2 -1
- package/docs/plans/0052-remove-scheduled-subagents.md +4 -2
- package/docs/plans/0057-structured-debug-logging.md +154 -0
- package/docs/plans/0069-create-subagent-runtime.md +345 -0
- package/docs/retro/0049-remove-group-join-output-file-rpc.md +15 -4
- package/docs/retro/0051-update-adr-0001-hard-fork.md +7 -3
- package/docs/retro/0053-extract-model-resolution-from-execute.md +14 -4
- package/docs/retro/0054-decompose-index-into-modules.md +20 -5
- package/docs/retro/0057-structured-debug-logging.md +77 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +13 -5
- package/src/agent-runner.ts +13 -26
- package/src/custom-agents.ts +5 -2
- package/src/debug.ts +14 -0
- package/src/env.ts +5 -3
- package/src/index.ts +37 -28
- package/src/memory.ts +5 -2
- package/src/notification.ts +3 -2
- package/src/output-file.ts +4 -1
- package/src/runtime.ts +62 -0
- package/src/skill-loader.ts +3 -1
- package/src/tools/agent-tool.ts +4 -2
- package/src/ui/agent-menu.ts +16 -13
- package/src/worktree.ts +14 -12
package/src/agent-runner.ts
CHANGED
|
@@ -68,36 +68,12 @@ function filterActiveTools(
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/** Default max turns. undefined = unlimited (no turn limit). */
|
|
72
|
-
let defaultMaxTurns: number | undefined;
|
|
73
|
-
|
|
74
71
|
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
75
72
|
export function normalizeMaxTurns(n: number | undefined): number | undefined {
|
|
76
73
|
if (n == null || n === 0) return undefined;
|
|
77
74
|
return Math.max(1, n);
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
/** Get the default max turns value. undefined = unlimited. */
|
|
81
|
-
export function getDefaultMaxTurns(): number | undefined {
|
|
82
|
-
return defaultMaxTurns;
|
|
83
|
-
}
|
|
84
|
-
/** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
85
|
-
export function setDefaultMaxTurns(n: number | undefined): void {
|
|
86
|
-
defaultMaxTurns = normalizeMaxTurns(n);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Additional turns allowed after the soft limit steer message. */
|
|
90
|
-
let graceTurns = 5;
|
|
91
|
-
|
|
92
|
-
/** Get the grace turns value. */
|
|
93
|
-
export function getGraceTurns(): number {
|
|
94
|
-
return graceTurns;
|
|
95
|
-
}
|
|
96
|
-
/** Set the grace turns value (minimum 1). */
|
|
97
|
-
export function setGraceTurns(n: number): void {
|
|
98
|
-
graceTurns = Math.max(1, n);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
77
|
/**
|
|
102
78
|
* Try to find the right model for an agent type.
|
|
103
79
|
* Priority: explicit option > config.model > parent model.
|
|
@@ -174,6 +150,17 @@ export interface RunOptions {
|
|
|
174
150
|
reason: "manual" | "threshold" | "overflow";
|
|
175
151
|
tokensBefore: number;
|
|
176
152
|
}) => void;
|
|
153
|
+
/**
|
|
154
|
+
* Default max turns from runtime config. Falls back to the module-scope
|
|
155
|
+
* `defaultMaxTurns` during the lift-and-shift migration; superseded by
|
|
156
|
+
* per-call `maxTurns` and per-agent `agentConfig.maxTurns`.
|
|
157
|
+
*/
|
|
158
|
+
defaultMaxTurns?: number;
|
|
159
|
+
/**
|
|
160
|
+
* Grace turns after the soft-limit steer message. Falls back to the
|
|
161
|
+
* module-scope `graceTurns` during migration.
|
|
162
|
+
*/
|
|
163
|
+
graceTurns?: number;
|
|
177
164
|
}
|
|
178
165
|
|
|
179
166
|
export interface RunResult {
|
|
@@ -424,7 +411,7 @@ export async function runAgent(
|
|
|
424
411
|
// Track turns for graceful max_turns enforcement
|
|
425
412
|
let turnCount = 0;
|
|
426
413
|
const maxTurns = normalizeMaxTurns(
|
|
427
|
-
options.maxTurns ?? agentConfig?.maxTurns ?? defaultMaxTurns,
|
|
414
|
+
options.maxTurns ?? agentConfig?.maxTurns ?? options.defaultMaxTurns,
|
|
428
415
|
);
|
|
429
416
|
let softLimitReached = false;
|
|
430
417
|
let aborted = false;
|
|
@@ -440,7 +427,7 @@ export async function runAgent(
|
|
|
440
427
|
session.steer(
|
|
441
428
|
"You have reached your turn limit. Wrap up immediately — provide your final answer now.",
|
|
442
429
|
);
|
|
443
|
-
} else if (softLimitReached && turnCount >= maxTurns + graceTurns) {
|
|
430
|
+
} else if (softLimitReached && turnCount >= maxTurns + (options.graceTurns ?? 5)) {
|
|
444
431
|
aborted = true;
|
|
445
432
|
session.abort();
|
|
446
433
|
}
|
package/src/custom-agents.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
9
|
+
import { debugLog } from "./debug.js";
|
|
9
10
|
import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -34,7 +35,8 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
34
35
|
let files: string[];
|
|
35
36
|
try {
|
|
36
37
|
files = readdirSync(dir).filter(f => f.endsWith(".md"));
|
|
37
|
-
} catch {
|
|
38
|
+
} catch (err) {
|
|
39
|
+
debugLog("readdirSync agents dir", err);
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -44,7 +46,8 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
44
46
|
let content: string;
|
|
45
47
|
try {
|
|
46
48
|
content = readFileSync(join(dir, file), "utf-8");
|
|
47
|
-
} catch {
|
|
49
|
+
} catch (err) {
|
|
50
|
+
debugLog("readFileSync agent file", err);
|
|
48
51
|
continue;
|
|
49
52
|
}
|
|
50
53
|
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* debug.ts — Debug logging utility for silenced catch blocks.
|
|
3
|
+
*
|
|
4
|
+
* Set PI_SUBAGENTS_DEBUG=1 to reveal silent failures in catch blocks
|
|
5
|
+
* throughout the package. Production behavior is unchanged when unset.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function isDebug(): boolean {
|
|
9
|
+
return process.env.PI_SUBAGENTS_DEBUG === "1";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function debugLog(context: string, err: unknown): void {
|
|
13
|
+
if (isDebug()) console.warn(`[pi-subagents:debug] ${context}:`, err);
|
|
14
|
+
}
|
package/src/env.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { debugLog } from "./debug.js";
|
|
6
7
|
import type { EnvInfo } from "./types.js";
|
|
7
8
|
|
|
8
9
|
export async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo> {
|
|
@@ -12,15 +13,16 @@ export async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo>
|
|
|
12
13
|
try {
|
|
13
14
|
const result = await pi.exec("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 5000 });
|
|
14
15
|
isGitRepo = result.code === 0 && result.stdout.trim() === "true";
|
|
15
|
-
} catch {
|
|
16
|
-
|
|
16
|
+
} catch (err) {
|
|
17
|
+
debugLog("git rev-parse", err);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
if (isGitRepo) {
|
|
20
21
|
try {
|
|
21
22
|
const result = await pi.exec("git", ["branch", "--show-current"], { cwd, timeout: 5000 });
|
|
22
23
|
branch = result.code === 0 ? result.stdout.trim() : "unknown";
|
|
23
|
-
} catch {
|
|
24
|
+
} catch (err) {
|
|
25
|
+
debugLog("git branch", err);
|
|
24
26
|
branch = "unknown";
|
|
25
27
|
}
|
|
26
28
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { defineTool, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { AgentManager } from "./agent-manager.js";
|
|
16
|
-
import { getAgentConversation,
|
|
16
|
+
import { getAgentConversation, normalizeMaxTurns, steerAgent } from "./agent-runner.js";
|
|
17
17
|
import { getAgentConfig, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, } from "./agent-types.js";
|
|
18
18
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
19
19
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
20
20
|
import { buildEventData, createNotificationSystem } from "./notification.js";
|
|
21
21
|
import { createNotificationRenderer } from "./renderer.js";
|
|
22
|
+
import { createSubagentRuntime } from "./runtime.js";
|
|
22
23
|
import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
|
|
23
24
|
import { createSubagentsService } from "./service-adapter.js";
|
|
24
25
|
import { applyAndEmitLoaded, saveAndEmitChanged } from "./settings.js";
|
|
@@ -29,7 +30,6 @@ import { createSteerTool } from "./tools/steer-tool.js";
|
|
|
29
30
|
import { type NotificationDetails } from "./types.js";
|
|
30
31
|
import { createAgentsMenuHandler } from "./ui/agent-menu.js";
|
|
31
32
|
import {
|
|
32
|
-
type AgentActivity,
|
|
33
33
|
AgentWidget,
|
|
34
34
|
type UICtx,
|
|
35
35
|
} from "./ui/agent-widget.js";
|
|
@@ -47,17 +47,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
47
47
|
// Initial load
|
|
48
48
|
reloadCustomAgents();
|
|
49
49
|
|
|
50
|
-
// ----
|
|
51
|
-
const
|
|
50
|
+
// ---- Runtime: all mutable extension state in one place ----
|
|
51
|
+
const runtime = createSubagentRuntime();
|
|
52
52
|
|
|
53
53
|
// ---- Notification system ----
|
|
54
|
-
//
|
|
55
|
-
|
|
54
|
+
// runtime.widget is assigned after AgentManager construction; arrow closures
|
|
55
|
+
// capture `runtime` by reference so they always read the current value.
|
|
56
56
|
const notifications = createNotificationSystem({
|
|
57
57
|
sendMessage: (msg, opts) => pi.sendMessage(msg as any, opts as any),
|
|
58
|
-
agentActivity,
|
|
59
|
-
markFinished: (id) => widget
|
|
60
|
-
updateWidget: () => widget
|
|
58
|
+
agentActivity: runtime.agentActivity,
|
|
59
|
+
markFinished: (id) => runtime.widget!.markFinished(id),
|
|
60
|
+
updateWidget: () => runtime.widget!.update(),
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
// Background completion: emit lifecycle event and delegate to notification system
|
|
@@ -102,21 +102,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
102
102
|
tokensBefore: info.tokensBefore,
|
|
103
103
|
compactionCount: record.compactionCount,
|
|
104
104
|
});
|
|
105
|
-
}
|
|
105
|
+
},
|
|
106
|
+
() => ({ defaultMaxTurns: runtime.defaultMaxTurns, graceTurns: runtime.graceTurns }));
|
|
106
107
|
|
|
107
108
|
// Typed service published via Symbol.for() for cross-extension access.
|
|
108
109
|
// Consumers: const { getSubagentsService } = await import("@gotgenes/pi-subagents");
|
|
109
|
-
let currentCtx: { pi: unknown; ctx: unknown } | undefined;
|
|
110
110
|
const service = createSubagentsService({
|
|
111
111
|
manager,
|
|
112
112
|
resolveModel,
|
|
113
|
-
getCtx: () => currentCtx,
|
|
114
|
-
getModelRegistry: () => (currentCtx?.ctx as { modelRegistry?: ModelRegistry } | undefined)?.modelRegistry,
|
|
113
|
+
getCtx: () => runtime.currentCtx,
|
|
114
|
+
getModelRegistry: () => (runtime.currentCtx?.ctx as { modelRegistry?: ModelRegistry } | undefined)?.modelRegistry,
|
|
115
115
|
});
|
|
116
116
|
publishSubagentsService(service);
|
|
117
117
|
|
|
118
118
|
pi.on("session_start", async (_event, ctx) => {
|
|
119
|
-
currentCtx = { pi, ctx };
|
|
119
|
+
runtime.currentCtx = { pi, ctx };
|
|
120
120
|
manager.clearCompleted();
|
|
121
121
|
});
|
|
122
122
|
|
|
@@ -128,19 +128,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
128
128
|
// If the session is going down, there's nothing left to consume agent results.
|
|
129
129
|
pi.on("session_shutdown", async () => {
|
|
130
130
|
unpublishSubagentsService();
|
|
131
|
-
currentCtx = undefined;
|
|
131
|
+
runtime.currentCtx = undefined;
|
|
132
132
|
manager.abortAll();
|
|
133
133
|
notifications.dispose();
|
|
134
134
|
manager.dispose();
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
// Live widget: show running agents above editor
|
|
138
|
-
widget = new AgentWidget(manager, agentActivity);
|
|
138
|
+
runtime.widget = new AgentWidget(manager, runtime.agentActivity);
|
|
139
139
|
|
|
140
140
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
141
141
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
142
|
-
widget
|
|
143
|
-
widget
|
|
142
|
+
runtime.widget!.setUICtx(ctx.ui as UICtx);
|
|
143
|
+
runtime.widget!.onTurnStart();
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
/** Build the full type list text dynamically from the unified registry. */
|
|
@@ -176,8 +176,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
176
176
|
applyAndEmitLoaded(
|
|
177
177
|
{
|
|
178
178
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
179
|
-
setDefaultMaxTurns,
|
|
180
|
-
setGraceTurns,
|
|
179
|
+
setDefaultMaxTurns: (n) => { runtime.defaultMaxTurns = normalizeMaxTurns(n); },
|
|
180
|
+
setGraceTurns: (n) => { runtime.graceTurns = Math.max(1, n); },
|
|
181
181
|
},
|
|
182
182
|
(event, payload) => pi.events.emit(event, payload),
|
|
183
183
|
);
|
|
@@ -194,17 +194,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
194
194
|
listAgents: () => manager.listAgents(),
|
|
195
195
|
},
|
|
196
196
|
widget: {
|
|
197
|
-
setUICtx: (ctx) => widget
|
|
198
|
-
ensureTimer: () => widget
|
|
199
|
-
update: () => widget
|
|
200
|
-
markFinished: (id) => widget
|
|
197
|
+
setUICtx: (ctx) => runtime.widget!.setUICtx(ctx as UICtx),
|
|
198
|
+
ensureTimer: () => runtime.widget!.ensureTimer(),
|
|
199
|
+
update: () => runtime.widget!.update(),
|
|
200
|
+
markFinished: (id) => runtime.widget!.markFinished(id),
|
|
201
201
|
},
|
|
202
|
-
agentActivity,
|
|
202
|
+
agentActivity: runtime.agentActivity,
|
|
203
203
|
emitEvent: (name, data) => pi.events.emit(name, data),
|
|
204
204
|
reloadCustomAgents,
|
|
205
205
|
typeListText,
|
|
206
206
|
availableTypesText: getAvailableTypes().join(", "),
|
|
207
207
|
agentDir: getAgentDir(),
|
|
208
|
+
getDefaultMaxTurns: () => runtime.defaultMaxTurns,
|
|
208
209
|
}) as any));
|
|
209
210
|
|
|
210
211
|
// ---- get_subagent_result tool ----
|
|
@@ -234,7 +235,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
234
235
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
235
236
|
},
|
|
236
237
|
reloadCustomAgents,
|
|
237
|
-
agentActivity,
|
|
238
|
+
agentActivity: runtime.agentActivity,
|
|
238
239
|
getModelLabel: (type, registry) => {
|
|
239
240
|
const cfg = getAgentConfig(type);
|
|
240
241
|
if (!cfg?.model) return 'inherit';
|
|
@@ -246,9 +247,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
246
247
|
},
|
|
247
248
|
snapshotSettings: () => ({
|
|
248
249
|
maxConcurrent: manager.getMaxConcurrent(),
|
|
249
|
-
defaultMaxTurns:
|
|
250
|
-
graceTurns:
|
|
250
|
+
defaultMaxTurns: runtime.defaultMaxTurns ?? 0,
|
|
251
|
+
graceTurns: runtime.graceTurns,
|
|
251
252
|
}),
|
|
253
|
+
getDefaultMaxTurns: () => runtime.defaultMaxTurns,
|
|
254
|
+
getGraceTurns: () => runtime.graceTurns,
|
|
255
|
+
setDefaultMaxTurns: (n) => {
|
|
256
|
+
runtime.defaultMaxTurns = normalizeMaxTurns(n);
|
|
257
|
+
},
|
|
258
|
+
setGraceTurns: (n) => {
|
|
259
|
+
runtime.graceTurns = Math.max(1, n);
|
|
260
|
+
},
|
|
252
261
|
saveSettings: (settings, successMsg) => saveAndEmitChanged(
|
|
253
262
|
settings,
|
|
254
263
|
successMsg,
|
package/src/memory.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join, } from "node:path";
|
|
13
|
+
import { debugLog } from "./debug.js";
|
|
13
14
|
import type { MemoryScope } from "./types.js";
|
|
14
15
|
|
|
15
16
|
/** Maximum lines to read from MEMORY.md */
|
|
@@ -30,7 +31,8 @@ export function isUnsafeName(name: string): boolean {
|
|
|
30
31
|
export function isSymlink(filePath: string): boolean {
|
|
31
32
|
try {
|
|
32
33
|
return lstatSync(filePath).isSymbolicLink();
|
|
33
|
-
} catch {
|
|
34
|
+
} catch (err) {
|
|
35
|
+
debugLog("lstatSync", err);
|
|
34
36
|
return false;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -44,7 +46,8 @@ export function safeReadFile(filePath: string): string | undefined {
|
|
|
44
46
|
if (isSymlink(filePath)) return undefined;
|
|
45
47
|
try {
|
|
46
48
|
return readFileSync(filePath, "utf-8");
|
|
47
|
-
} catch {
|
|
49
|
+
} catch (err) {
|
|
50
|
+
debugLog("readFileSync", err);
|
|
48
51
|
return undefined;
|
|
49
52
|
}
|
|
50
53
|
}
|
package/src/notification.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { debugLog } from "./debug.js";
|
|
1
2
|
import type { AgentRecord, NotificationDetails } from "./types.js";
|
|
2
3
|
import type { AgentActivity } from "./ui/agent-widget.js";
|
|
3
4
|
import { getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
@@ -142,8 +143,8 @@ export function createNotificationSystem(deps: NotificationDeps): NotificationSy
|
|
|
142
143
|
pendingNudges.delete(key);
|
|
143
144
|
try {
|
|
144
145
|
send();
|
|
145
|
-
} catch {
|
|
146
|
-
|
|
146
|
+
} catch (err) {
|
|
147
|
+
debugLog("notification render", err);
|
|
147
148
|
}
|
|
148
149
|
}, delay),
|
|
149
150
|
);
|
package/src/output-file.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { appendFileSync, chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { debugLog } from "./debug.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Encode a cwd path as a filesystem-safe directory name. Handles:
|
|
@@ -80,7 +81,9 @@ export function streamToOutputFile(
|
|
|
80
81
|
};
|
|
81
82
|
try {
|
|
82
83
|
appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
|
|
83
|
-
} catch {
|
|
84
|
+
} catch (err) {
|
|
85
|
+
debugLog("write JSONL chunk", err);
|
|
86
|
+
}
|
|
84
87
|
writtenCount++;
|
|
85
88
|
}
|
|
86
89
|
};
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime.ts — SubagentRuntime: composition root for all mutable extension state.
|
|
3
|
+
*
|
|
4
|
+
* Eliminates module-scope state in agent-runner.ts and closure-scoped state
|
|
5
|
+
* in index.ts by consolidating them into a single, testable object.
|
|
6
|
+
* Follows the same pattern as pi-permission-system's ExtensionRuntime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentActivity, AgentWidget } from "./ui/agent-widget.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Narrow config subset read by AgentManager when constructing RunOptions.
|
|
13
|
+
* Kept separate so callers can satisfy it without depending on the full runtime.
|
|
14
|
+
*/
|
|
15
|
+
export interface RunConfig {
|
|
16
|
+
readonly defaultMaxTurns: number | undefined;
|
|
17
|
+
readonly graceTurns: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* All mutable state owned by the pi-subagents extension.
|
|
22
|
+
*
|
|
23
|
+
* Created once inside `piSubagentsExtension()` via `createSubagentRuntime()`.
|
|
24
|
+
* Tests construct a fresh runtime per test for full isolation.
|
|
25
|
+
*/
|
|
26
|
+
export interface SubagentRuntime {
|
|
27
|
+
// ── Execution config (was module-scope in agent-runner.ts) ──────────────
|
|
28
|
+
/** Default max turns for all agents. undefined = unlimited. */
|
|
29
|
+
defaultMaxTurns: number | undefined;
|
|
30
|
+
/** Additional turns allowed after the soft-limit steer message. */
|
|
31
|
+
graceTurns: number;
|
|
32
|
+
|
|
33
|
+
// ── Session state (was closure-scoped in index.ts) ───────────────────────
|
|
34
|
+
/** Active Pi session context — set on session_start, cleared on session_shutdown. */
|
|
35
|
+
currentCtx: { pi: unknown; ctx: unknown } | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Per-agent live activity state shared across the notification system,
|
|
38
|
+
* widget, and tool handlers. The Map itself is never replaced.
|
|
39
|
+
*/
|
|
40
|
+
readonly agentActivity: Map<string, AgentActivity>;
|
|
41
|
+
/**
|
|
42
|
+
* Persistent widget reference. Null until constructed after AgentManager.
|
|
43
|
+
* Notification closures use `runtime.widget!` — safe because agents always
|
|
44
|
+
* complete after widget construction.
|
|
45
|
+
*/
|
|
46
|
+
widget: AgentWidget | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a fully-initialized SubagentRuntime with default values.
|
|
51
|
+
*
|
|
52
|
+
* Call once at extension startup; pass the result to factories and handlers.
|
|
53
|
+
*/
|
|
54
|
+
export function createSubagentRuntime(): SubagentRuntime {
|
|
55
|
+
return {
|
|
56
|
+
defaultMaxTurns: undefined,
|
|
57
|
+
graceTurns: 5,
|
|
58
|
+
currentCtx: undefined,
|
|
59
|
+
agentActivity: new Map(),
|
|
60
|
+
widget: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
package/src/skill-loader.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
23
23
|
import { homedir } from "node:os";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { debugLog } from "./debug.js";
|
|
26
27
|
import { isSymlink, isUnsafeName, safeReadFile } from "./memory.js";
|
|
27
28
|
|
|
28
29
|
export interface PreloadedSkill {
|
|
@@ -71,7 +72,8 @@ function findSkillDirectory(root: string, name: string): string | undefined {
|
|
|
71
72
|
let entries: Dirent<string>[];
|
|
72
73
|
try {
|
|
73
74
|
entries = readdirSync(current, { withFileTypes: true });
|
|
74
|
-
} catch {
|
|
75
|
+
} catch (err) {
|
|
76
|
+
debugLog("readdirSync skill root", err);
|
|
75
77
|
continue;
|
|
76
78
|
}
|
|
77
79
|
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeMaxTurns } from "../agent-runner.js";
|
|
4
4
|
import { getAgentConfig, resolveType } from "../agent-types.js";
|
|
5
5
|
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
6
6
|
import { resolveInvocationModel } from "../model-resolver.js";
|
|
@@ -146,6 +146,8 @@ export interface AgentToolDeps {
|
|
|
146
146
|
typeListText: string;
|
|
147
147
|
availableTypesText: string;
|
|
148
148
|
agentDir: string;
|
|
149
|
+
/** Returns the runtime default max turns (undefined = unlimited). */
|
|
150
|
+
getDefaultMaxTurns: () => number | undefined;
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
// ---- Factory ----
|
|
@@ -396,7 +398,7 @@ Guidelines:
|
|
|
396
398
|
? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
|
|
397
399
|
: undefined;
|
|
398
400
|
const effectiveMaxTurns = normalizeMaxTurns(
|
|
399
|
-
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
401
|
+
resolvedConfig.maxTurns ?? deps.getDefaultMaxTurns(),
|
|
400
402
|
);
|
|
401
403
|
const agentInvocation: AgentInvocation = {
|
|
402
404
|
modelName,
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
getDefaultMaxTurns,
|
|
5
|
-
getGraceTurns,
|
|
6
|
-
setDefaultMaxTurns,
|
|
7
|
-
setGraceTurns,
|
|
8
|
-
} from "../agent-runner.js";
|
|
3
|
+
|
|
9
4
|
import {
|
|
10
5
|
BUILTIN_TOOL_NAMES,
|
|
11
6
|
getAgentConfig,
|
|
@@ -42,6 +37,14 @@ export interface AgentMenuDeps {
|
|
|
42
37
|
) => { message: string; level: string };
|
|
43
38
|
emitEvent: (name: string, data: unknown) => void;
|
|
44
39
|
personalAgentsDir: string;
|
|
40
|
+
/** Returns the runtime default max turns (undefined = unlimited). */
|
|
41
|
+
getDefaultMaxTurns: () => number | undefined;
|
|
42
|
+
/** Returns the runtime grace turns value. */
|
|
43
|
+
getGraceTurns: () => number;
|
|
44
|
+
/** Updates the runtime default max turns (undefined = unlimited). */
|
|
45
|
+
setDefaultMaxTurns: (n: number | undefined) => void;
|
|
46
|
+
/** Updates the runtime grace turns value (minimum 1). */
|
|
47
|
+
setGraceTurns: (n: number) => void;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
// ---- Narrow UI context types ----
|
|
@@ -620,8 +623,8 @@ ${systemPrompt}
|
|
|
620
623
|
async function showSettings(ctx: MenuContext) {
|
|
621
624
|
const choice = await ctx.ui.select("Settings", [
|
|
622
625
|
`Max concurrency (current: ${deps.manager.getMaxConcurrent()})`,
|
|
623
|
-
`Default max turns (current: ${getDefaultMaxTurns() ?? "unlimited"})`,
|
|
624
|
-
`Grace turns (current: ${getGraceTurns()})`,
|
|
626
|
+
`Default max turns (current: ${deps.getDefaultMaxTurns() ?? "unlimited"})`,
|
|
627
|
+
`Grace turns (current: ${deps.getGraceTurns()})`,
|
|
625
628
|
]);
|
|
626
629
|
if (!choice) return;
|
|
627
630
|
|
|
@@ -642,15 +645,15 @@ ${systemPrompt}
|
|
|
642
645
|
} else if (choice.startsWith("Default max turns")) {
|
|
643
646
|
const val = await ctx.ui.input(
|
|
644
647
|
"Default max turns before wrap-up (0 = unlimited)",
|
|
645
|
-
String(getDefaultMaxTurns() ?? 0),
|
|
648
|
+
String(deps.getDefaultMaxTurns() ?? 0),
|
|
646
649
|
);
|
|
647
650
|
if (val) {
|
|
648
651
|
const n = parseInt(val, 10);
|
|
649
652
|
if (n === 0) {
|
|
650
|
-
setDefaultMaxTurns(undefined);
|
|
653
|
+
deps.setDefaultMaxTurns(undefined);
|
|
651
654
|
notifyApplied(ctx, "Default max turns set to unlimited");
|
|
652
655
|
} else if (n >= 1) {
|
|
653
|
-
setDefaultMaxTurns(n);
|
|
656
|
+
deps.setDefaultMaxTurns(n);
|
|
654
657
|
notifyApplied(ctx, `Default max turns set to ${n}`);
|
|
655
658
|
} else {
|
|
656
659
|
ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
@@ -659,12 +662,12 @@ ${systemPrompt}
|
|
|
659
662
|
} else if (choice.startsWith("Grace turns")) {
|
|
660
663
|
const val = await ctx.ui.input(
|
|
661
664
|
"Grace turns after wrap-up steer",
|
|
662
|
-
String(getGraceTurns()),
|
|
665
|
+
String(deps.getGraceTurns()),
|
|
663
666
|
);
|
|
664
667
|
if (val) {
|
|
665
668
|
const n = parseInt(val, 10);
|
|
666
669
|
if (n >= 1) {
|
|
667
|
-
setGraceTurns(n);
|
|
670
|
+
deps.setGraceTurns(n);
|
|
668
671
|
notifyApplied(ctx, `Grace turns set to ${n}`);
|
|
669
672
|
} else {
|
|
670
673
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
package/src/worktree.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { randomUUID } from "node:crypto";
|
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
+
import { debugLog } from "./debug.js";
|
|
14
15
|
|
|
15
16
|
export interface WorktreeInfo {
|
|
16
17
|
/** Absolute path to the worktree directory. */
|
|
@@ -37,7 +38,8 @@ export function createWorktree(cwd: string, agentId: string): WorktreeInfo | und
|
|
|
37
38
|
try {
|
|
38
39
|
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
39
40
|
execFileSync("git", ["rev-parse", "HEAD"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
40
|
-
} catch {
|
|
41
|
+
} catch (err) {
|
|
42
|
+
debugLog("createWorktree git rev-parse", err);
|
|
41
43
|
return undefined;
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -53,8 +55,8 @@ export function createWorktree(cwd: string, agentId: string): WorktreeInfo | und
|
|
|
53
55
|
timeout: 30000,
|
|
54
56
|
});
|
|
55
57
|
return { path: worktreePath, branch };
|
|
56
|
-
} catch {
|
|
57
|
-
|
|
58
|
+
} catch (err) {
|
|
59
|
+
debugLog("git worktree add", err);
|
|
58
60
|
return undefined;
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -107,8 +109,8 @@ export function cleanupWorktree(
|
|
|
107
109
|
stdio: "pipe",
|
|
108
110
|
timeout: 5000,
|
|
109
111
|
});
|
|
110
|
-
} catch {
|
|
111
|
-
|
|
112
|
+
} catch (err) {
|
|
113
|
+
debugLog("git branch", err);
|
|
112
114
|
branchName = `${worktree.branch}-${Date.now()}`;
|
|
113
115
|
execFileSync("git", ["branch", branchName], {
|
|
114
116
|
cwd: worktree.path,
|
|
@@ -127,9 +129,9 @@ export function cleanupWorktree(
|
|
|
127
129
|
branch: worktree.branch,
|
|
128
130
|
path: worktree.path,
|
|
129
131
|
};
|
|
130
|
-
} catch {
|
|
131
|
-
|
|
132
|
-
try { removeWorktree(cwd, worktree.path); } catch {
|
|
132
|
+
} catch (err) {
|
|
133
|
+
debugLog("cleanupWorktree", err);
|
|
134
|
+
try { removeWorktree(cwd, worktree.path); } catch (removeErr) { debugLog("removeWorktree on cleanup error", removeErr); }
|
|
133
135
|
return { hasChanges: false };
|
|
134
136
|
}
|
|
135
137
|
}
|
|
@@ -144,11 +146,11 @@ function removeWorktree(cwd: string, worktreePath: string): void {
|
|
|
144
146
|
stdio: "pipe",
|
|
145
147
|
timeout: 10000,
|
|
146
148
|
});
|
|
147
|
-
} catch {
|
|
148
|
-
|
|
149
|
+
} catch (err) {
|
|
150
|
+
debugLog("git worktree remove", err);
|
|
149
151
|
try {
|
|
150
152
|
execFileSync("git", ["worktree", "prune"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
151
|
-
} catch {
|
|
153
|
+
} catch (pruneErr) { debugLog("git worktree prune", pruneErr); }
|
|
152
154
|
}
|
|
153
155
|
}
|
|
154
156
|
|
|
@@ -158,5 +160,5 @@ function removeWorktree(cwd: string, worktreePath: string): void {
|
|
|
158
160
|
export function pruneWorktrees(cwd: string): void {
|
|
159
161
|
try {
|
|
160
162
|
execFileSync("git", ["worktree", "prune"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
161
|
-
} catch {
|
|
163
|
+
} catch (err) { debugLog("pruneWorktrees", err); }
|
|
162
164
|
}
|