@gotgenes/pi-subagents 5.1.0 → 5.3.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 +37 -0
- package/README.md +176 -133
- package/docs/architecture/architecture.md +148 -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 +22 -52
- package/docs/plans/0069-create-subagent-runtime.md +345 -0
- package/docs/plans/0071-extract-session-config-assembler.md +362 -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/docs/retro/0069-create-subagent-runtime.md +43 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +7 -0
- package/src/agent-runner.ts +51 -189
- package/src/debug.ts +4 -2
- package/src/index.ts +37 -28
- package/src/runtime.ts +62 -0
- package/src/session-config.ts +263 -0
- package/src/tools/agent-tool.ts +4 -2
- package/src/ui/agent-menu.ts +16 -13
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-config.ts — Pure configuration assembler for agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* `assembleSessionConfig()` is the pure core extracted from `runAgent()`.
|
|
5
|
+
* It accepts resolved inputs (agent type, narrow context, run options, env info)
|
|
6
|
+
* and returns everything `runAgent()` needs to create the SDK session — without
|
|
7
|
+
* importing or constructing any Pi SDK types.
|
|
8
|
+
*
|
|
9
|
+
* The only async IO in the assembly phase (`detectEnv`) is handled by the caller
|
|
10
|
+
* before invoking this function, keeping the assembler synchronous.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getAgentConfig,
|
|
15
|
+
getConfig,
|
|
16
|
+
getMemoryToolNames,
|
|
17
|
+
getReadOnlyMemoryToolNames,
|
|
18
|
+
getToolNamesForType,
|
|
19
|
+
} from "./agent-types.js";
|
|
20
|
+
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
21
|
+
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
22
|
+
import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
|
|
23
|
+
import { preloadSkills } from "./skill-loader.js";
|
|
24
|
+
import type { EnvInfo, SubagentType, ThinkingLevel } from "./types.js";
|
|
25
|
+
|
|
26
|
+
// ── Public interfaces ────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Narrow context the assembler reads from the parent session.
|
|
30
|
+
* Tests construct plain objects satisfying this interface — no SDK mocking needed.
|
|
31
|
+
*
|
|
32
|
+
* Models are treated as opaque handles: the assembler never inspects their
|
|
33
|
+
* internals, only passes them through. `getAvailable` returns just enough
|
|
34
|
+
* structural information ({ provider, id }) for the availability check in
|
|
35
|
+
* `resolveDefaultModel`.
|
|
36
|
+
*/
|
|
37
|
+
export interface AssemblerContext {
|
|
38
|
+
/** Parent working directory (overridable via options.cwd). */
|
|
39
|
+
cwd: string;
|
|
40
|
+
/** Parent's effective system prompt (for append-mode agents). */
|
|
41
|
+
parentSystemPrompt: string;
|
|
42
|
+
/** Parent's current model instance (fallback when agent config has no model). */
|
|
43
|
+
parentModel?: unknown;
|
|
44
|
+
/** Model registry for resolving config.model strings. */
|
|
45
|
+
modelRegistry: {
|
|
46
|
+
find(provider: string, modelId: string): unknown;
|
|
47
|
+
getAvailable?(): Array<{ provider: string; id: string }>;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Narrow slice of RunOptions consumed by the assembler.
|
|
53
|
+
* All fields are optional — callers pass only what they have.
|
|
54
|
+
*/
|
|
55
|
+
export interface AssemblerOptions {
|
|
56
|
+
/** Override working directory (e.g. for worktree isolation). */
|
|
57
|
+
cwd?: string;
|
|
58
|
+
/** When true, forces extensions and skills to false. */
|
|
59
|
+
isolated?: boolean;
|
|
60
|
+
/** Explicit model override — wins over agentConfig.model and parent model. */
|
|
61
|
+
model?: unknown;
|
|
62
|
+
/** Explicit thinking level — wins over agentConfig.thinking. */
|
|
63
|
+
thinkingLevel?: ThinkingLevel;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Assembled configuration returned to `runAgent()`.
|
|
68
|
+
* Contains everything needed to create the SDK session and filter tools —
|
|
69
|
+
* with no SDK object references.
|
|
70
|
+
*/
|
|
71
|
+
export interface SessionConfig {
|
|
72
|
+
/** Resolved working directory (`options.cwd ?? ctx.cwd`). */
|
|
73
|
+
effectiveCwd: string;
|
|
74
|
+
/** Fully-assembled system prompt string (ready for `systemPromptOverride`). */
|
|
75
|
+
systemPrompt: string;
|
|
76
|
+
/** Built-in tool names for session creation, filtering, and memory augmentation. */
|
|
77
|
+
toolNames: string[];
|
|
78
|
+
/** Disallowed tool set from agentConfig (for `filterActiveTools`). undefined when empty. */
|
|
79
|
+
disallowedSet: Set<string> | undefined;
|
|
80
|
+
/** Resolved extensions setting for resource loader and tool filtering. */
|
|
81
|
+
extensions: boolean | string[];
|
|
82
|
+
/**
|
|
83
|
+
* Resolved model instance (undefined → use parent model as passed to SDK).
|
|
84
|
+
* Opaque handle — the assembler passes it through without inspection.
|
|
85
|
+
* Caller casts to the SDK’s Model<any> at the session-creation boundary.
|
|
86
|
+
*/
|
|
87
|
+
model: unknown;
|
|
88
|
+
/** Resolved thinking level (undefined → inherit from session). */
|
|
89
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
90
|
+
/** Whether to skip skill loading in the resource loader (`noSkills` flag). */
|
|
91
|
+
noSkills: boolean;
|
|
92
|
+
/** Prompt extras (memory block, preloaded skill blocks) — for transparency. */
|
|
93
|
+
extras: PromptExtras;
|
|
94
|
+
/** Per-agent configured max turns (from agentConfig.maxTurns). */
|
|
95
|
+
agentMaxTurns: number | undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the default model from the agent config's model string.
|
|
102
|
+
*
|
|
103
|
+
* Priority: parentModel is the fallback; if `configModel` is a "provider/modelId"
|
|
104
|
+
* string that resolves against the registry AND is in the available set, return
|
|
105
|
+
* that model instead.
|
|
106
|
+
*/
|
|
107
|
+
function resolveDefaultModel(
|
|
108
|
+
parentModel: unknown,
|
|
109
|
+
registry: AssemblerContext["modelRegistry"],
|
|
110
|
+
configModel?: string,
|
|
111
|
+
): unknown {
|
|
112
|
+
if (configModel) {
|
|
113
|
+
const slashIdx = configModel.indexOf("/");
|
|
114
|
+
if (slashIdx !== -1) {
|
|
115
|
+
const provider = configModel.slice(0, slashIdx);
|
|
116
|
+
const modelId = configModel.slice(slashIdx + 1);
|
|
117
|
+
|
|
118
|
+
const available = registry.getAvailable?.();
|
|
119
|
+
const availableKeys = available
|
|
120
|
+
? new Set(available.map((m) => `${m.provider}/${m.id}`))
|
|
121
|
+
: undefined;
|
|
122
|
+
const isAvailable = (p: string, id: string) =>
|
|
123
|
+
!availableKeys || availableKeys.has(`${p}/${id}`);
|
|
124
|
+
|
|
125
|
+
const found = registry.find(provider, modelId);
|
|
126
|
+
if (found && isAvailable(provider, modelId)) return found;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return parentModel;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Public function ──────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Assemble all configuration needed to create an agent session.
|
|
136
|
+
*
|
|
137
|
+
* Synchronous and side-effect-free (beyond calling `preloadSkills` which reads
|
|
138
|
+
* the filesystem). The caller is responsible for resolving `EnvInfo` beforehand
|
|
139
|
+
* via `detectEnv()`.
|
|
140
|
+
*
|
|
141
|
+
* @param type The subagent type name (case-insensitive registry lookup).
|
|
142
|
+
* @param ctx Narrow context from the parent session.
|
|
143
|
+
* @param options Per-call overrides (cwd, isolated, model, thinkingLevel).
|
|
144
|
+
* @param env Pre-resolved environment info from `detectEnv()`.
|
|
145
|
+
*/
|
|
146
|
+
export function assembleSessionConfig(
|
|
147
|
+
type: SubagentType,
|
|
148
|
+
ctx: AssemblerContext,
|
|
149
|
+
options: AssemblerOptions,
|
|
150
|
+
env: EnvInfo,
|
|
151
|
+
): SessionConfig {
|
|
152
|
+
const config = getConfig(type);
|
|
153
|
+
const agentConfig = getAgentConfig(type);
|
|
154
|
+
|
|
155
|
+
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
156
|
+
|
|
157
|
+
// Resolve extensions/skills: isolated overrides to false
|
|
158
|
+
const extensions = options.isolated ? false : config.extensions;
|
|
159
|
+
const skills = options.isolated ? false : config.skills;
|
|
160
|
+
|
|
161
|
+
// Build prompt extras (memory, preloaded skills)
|
|
162
|
+
const extras: PromptExtras = {};
|
|
163
|
+
|
|
164
|
+
// Skill preloading: when skills is string[], preload their content into the prompt
|
|
165
|
+
if (Array.isArray(skills)) {
|
|
166
|
+
const loaded = preloadSkills(skills, effectiveCwd);
|
|
167
|
+
if (loaded.length > 0) {
|
|
168
|
+
extras.skillBlocks = loaded;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let toolNames = getToolNamesForType(type);
|
|
173
|
+
|
|
174
|
+
// Persistent memory: detect write capability and branch accordingly.
|
|
175
|
+
// Account for disallowedTools — a tool in the base set but on the denylist
|
|
176
|
+
// is not truly available.
|
|
177
|
+
if (agentConfig?.memory) {
|
|
178
|
+
const existingNames = new Set(toolNames);
|
|
179
|
+
const denied = agentConfig.disallowedTools
|
|
180
|
+
? new Set(agentConfig.disallowedTools)
|
|
181
|
+
: undefined;
|
|
182
|
+
const effectivelyHas = (name: string) =>
|
|
183
|
+
existingNames.has(name) && !denied?.has(name);
|
|
184
|
+
const hasWriteTools = effectivelyHas("write") || effectivelyHas("edit");
|
|
185
|
+
|
|
186
|
+
if (hasWriteTools) {
|
|
187
|
+
const extraNames = getMemoryToolNames(existingNames);
|
|
188
|
+
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
189
|
+
extras.memoryBlock = buildMemoryBlock(
|
|
190
|
+
agentConfig.name,
|
|
191
|
+
agentConfig.memory,
|
|
192
|
+
effectiveCwd,
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
const extraNames = getReadOnlyMemoryToolNames(existingNames);
|
|
196
|
+
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
197
|
+
extras.memoryBlock = buildReadOnlyMemoryBlock(
|
|
198
|
+
agentConfig.name,
|
|
199
|
+
agentConfig.memory,
|
|
200
|
+
effectiveCwd,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Build system prompt from agent config (or general-purpose fallback for unknown types)
|
|
206
|
+
let systemPrompt: string;
|
|
207
|
+
if (agentConfig) {
|
|
208
|
+
systemPrompt = buildAgentPrompt(
|
|
209
|
+
agentConfig,
|
|
210
|
+
effectiveCwd,
|
|
211
|
+
env,
|
|
212
|
+
ctx.parentSystemPrompt,
|
|
213
|
+
extras,
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
// Unknown type fallback: spread the canonical general-purpose config (defensive —
|
|
217
|
+
// unreachable in practice since index.ts resolves unknown types before calling runAgent).
|
|
218
|
+
const fallback = DEFAULT_AGENTS.get("general-purpose");
|
|
219
|
+
if (!fallback) {
|
|
220
|
+
throw new Error(`No fallback config available for unknown type "${type}"`);
|
|
221
|
+
}
|
|
222
|
+
systemPrompt = buildAgentPrompt(
|
|
223
|
+
{ ...fallback, name: type },
|
|
224
|
+
effectiveCwd,
|
|
225
|
+
env,
|
|
226
|
+
ctx.parentSystemPrompt,
|
|
227
|
+
extras,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// noSkills: when we've already preloaded skills into the prompt, or skills = false,
|
|
232
|
+
// tell the resource loader not to load them again.
|
|
233
|
+
const noSkills = skills === false || Array.isArray(skills);
|
|
234
|
+
|
|
235
|
+
// Disallowed tools set (for filterActiveTools in runAgent)
|
|
236
|
+
const disallowedSet = agentConfig?.disallowedTools
|
|
237
|
+
? new Set(agentConfig.disallowedTools)
|
|
238
|
+
: undefined;
|
|
239
|
+
|
|
240
|
+
// Model resolution: explicit option > config model string > parent model
|
|
241
|
+
const model =
|
|
242
|
+
options.model ??
|
|
243
|
+
resolveDefaultModel(ctx.parentModel, ctx.modelRegistry, agentConfig?.model);
|
|
244
|
+
|
|
245
|
+
// Thinking level: explicit option > agent config > undefined (inherit)
|
|
246
|
+
const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
|
|
247
|
+
|
|
248
|
+
// Per-agent max turns (combined with options.maxTurns and defaultMaxTurns by runAgent)
|
|
249
|
+
const agentMaxTurns = agentConfig?.maxTurns;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
effectiveCwd,
|
|
253
|
+
systemPrompt,
|
|
254
|
+
toolNames,
|
|
255
|
+
disallowedSet,
|
|
256
|
+
extensions,
|
|
257
|
+
model,
|
|
258
|
+
thinkingLevel,
|
|
259
|
+
noSkills,
|
|
260
|
+
extras,
|
|
261
|
+
agentMaxTurns,
|
|
262
|
+
};
|
|
263
|
+
}
|
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");
|