@yandy0725/pi-subagents 0.1.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/README.md +155 -0
- package/README.zh.md +155 -0
- package/index.ts +1 -0
- package/package.json +49 -0
- package/src/config/agent-types.ts +127 -0
- package/src/config/custom-agents.ts +109 -0
- package/src/config/default-agents.ts +117 -0
- package/src/config/invocation-config.ts +30 -0
- package/src/debug.ts +14 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/interrupt.ts +49 -0
- package/src/handlers/lifecycle.ts +63 -0
- package/src/handlers/tool-start.ts +32 -0
- package/src/index.ts +186 -0
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/child-lifecycle.ts +88 -0
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/create-subagent-session.ts +240 -0
- package/src/lifecycle/parent-snapshot.ts +45 -0
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/subagent-manager.ts +353 -0
- package/src/lifecycle/subagent-session.ts +232 -0
- package/src/lifecycle/subagent-state.ts +216 -0
- package/src/lifecycle/subagent.ts +498 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/lifecycle/usage.ts +65 -0
- package/src/lifecycle/workspace-bracket.ts +59 -0
- package/src/lifecycle/workspace.ts +47 -0
- package/src/observation/composite-subagent-observer.ts +49 -0
- package/src/observation/notification-state.ts +27 -0
- package/src/observation/notification.ts +186 -0
- package/src/observation/record-observer.ts +75 -0
- package/src/observation/renderer.ts +63 -0
- package/src/observation/subagent-events-observer.ts +94 -0
- package/src/runtime.ts +77 -0
- package/src/service/service-adapter.ts +131 -0
- package/src/service/service.ts +123 -0
- package/src/session/content-items.ts +51 -0
- package/src/session/context.ts +78 -0
- package/src/session/conversation.ts +44 -0
- package/src/session/env.ts +40 -0
- package/src/session/model-resolver.ts +121 -0
- package/src/session/prompts.ts +83 -0
- package/src/session/session-config.ts +172 -0
- package/src/session/session-dir.ts +38 -0
- package/src/settings.ts +227 -0
- package/src/tools/agent-tool.ts +220 -0
- package/src/tools/background-spawner.ts +66 -0
- package/src/tools/foreground-runner.ts +114 -0
- package/src/tools/get-result-tool.ts +120 -0
- package/src/tools/helpers.ts +105 -0
- package/src/tools/result-renderer.ts +109 -0
- package/src/tools/spawn-config.ts +150 -0
- package/src/tools/steer-tool.ts +90 -0
- package/src/types.ts +115 -0
- package/src/ui/agent-widget.ts +311 -0
- package/src/ui/display.ts +174 -0
- package/src/ui/session-navigation.ts +147 -0
- package/src/ui/session-navigator.ts +406 -0
- package/src/ui/subagents-settings.ts +77 -0
- package/src/ui/widget-renderer.ts +296 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* default-agents.ts — Embedded default agent configurations.
|
|
3
|
+
*
|
|
4
|
+
* These are always available but can be overridden by user .md files with the same name.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentConfig } from "../types";
|
|
8
|
+
|
|
9
|
+
const READ_ONLY_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
12
|
+
[
|
|
13
|
+
"general-purpose",
|
|
14
|
+
{
|
|
15
|
+
name: "general-purpose",
|
|
16
|
+
displayName: "Agent",
|
|
17
|
+
description: "General-purpose agent for complex, multi-step tasks",
|
|
18
|
+
// builtinToolNames omitted — means "all available tools" (resolved at lookup time)
|
|
19
|
+
// inheritContext / runInBackground omitted — strategy fields, callers decide per-call.
|
|
20
|
+
// Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
|
|
21
|
+
systemPrompt: "",
|
|
22
|
+
promptMode: "append",
|
|
23
|
+
isDefault: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
"Explore",
|
|
28
|
+
{
|
|
29
|
+
name: "Explore",
|
|
30
|
+
displayName: "Explore",
|
|
31
|
+
description: "Fast codebase exploration agent (read-only)",
|
|
32
|
+
builtinToolNames: READ_ONLY_TOOLS,
|
|
33
|
+
model: "anthropic/claude-haiku-4-5-20251001",
|
|
34
|
+
systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
35
|
+
You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
36
|
+
Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
|
|
37
|
+
|
|
38
|
+
You are STRICTLY PROHIBITED from:
|
|
39
|
+
- Creating new files
|
|
40
|
+
- Modifying existing files
|
|
41
|
+
- Deleting files
|
|
42
|
+
- Moving or copying files
|
|
43
|
+
- Creating temporary files anywhere, including /tmp
|
|
44
|
+
- Using redirect operators (>, >>, |) or heredocs to write to files
|
|
45
|
+
- Running ANY commands that change system state
|
|
46
|
+
|
|
47
|
+
Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find, cat, head, tail.
|
|
48
|
+
|
|
49
|
+
# Tool Usage
|
|
50
|
+
- Use the find tool for file pattern matching (NOT the bash find command)
|
|
51
|
+
- Use the grep tool for content search (NOT bash grep/rg command)
|
|
52
|
+
- Use the read tool for reading files (NOT bash cat/head/tail)
|
|
53
|
+
- Use Bash ONLY for read-only operations
|
|
54
|
+
- Make independent tool calls in parallel for efficiency
|
|
55
|
+
- Adapt search approach based on thoroughness level specified
|
|
56
|
+
|
|
57
|
+
# Output
|
|
58
|
+
- Use absolute file paths in all references
|
|
59
|
+
- Report findings as regular messages
|
|
60
|
+
- Do not use emojis
|
|
61
|
+
- Be thorough and precise`,
|
|
62
|
+
promptMode: "replace",
|
|
63
|
+
isDefault: true,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"Plan",
|
|
68
|
+
{
|
|
69
|
+
name: "Plan",
|
|
70
|
+
displayName: "Plan",
|
|
71
|
+
description: "Software architect for implementation planning (read-only)",
|
|
72
|
+
builtinToolNames: READ_ONLY_TOOLS,
|
|
73
|
+
systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
74
|
+
You are a software architect and planning specialist.
|
|
75
|
+
Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
|
|
76
|
+
You do NOT have access to file editing tools — attempting to edit files will fail.
|
|
77
|
+
|
|
78
|
+
You are STRICTLY PROHIBITED from:
|
|
79
|
+
- Creating new files
|
|
80
|
+
- Modifying existing files
|
|
81
|
+
- Deleting files
|
|
82
|
+
- Moving or copying files
|
|
83
|
+
- Creating temporary files anywhere, including /tmp
|
|
84
|
+
- Using redirect operators (>, >>, |) or heredocs to write to files
|
|
85
|
+
- Running ANY commands that change system state
|
|
86
|
+
|
|
87
|
+
# Planning Process
|
|
88
|
+
1. Understand requirements
|
|
89
|
+
2. Explore thoroughly (read files, find patterns, understand architecture)
|
|
90
|
+
3. Design solution based on your assigned perspective
|
|
91
|
+
4. Detail the plan with step-by-step implementation strategy
|
|
92
|
+
|
|
93
|
+
# Requirements
|
|
94
|
+
- Consider trade-offs and architectural decisions
|
|
95
|
+
- Identify dependencies and sequencing
|
|
96
|
+
- Anticipate potential challenges
|
|
97
|
+
- Follow existing patterns where appropriate
|
|
98
|
+
|
|
99
|
+
# Tool Usage
|
|
100
|
+
- Use the find tool for file pattern matching (NOT the bash find command)
|
|
101
|
+
- Use the grep tool for content search (NOT bash grep/rg command)
|
|
102
|
+
- Use the read tool for reading files (NOT bash cat/head/tail)
|
|
103
|
+
- Use Bash ONLY for read-only operations
|
|
104
|
+
|
|
105
|
+
# Output Format
|
|
106
|
+
- Use absolute file paths
|
|
107
|
+
- Do not use emojis
|
|
108
|
+
- End your response with:
|
|
109
|
+
|
|
110
|
+
### Critical Files for Implementation
|
|
111
|
+
List 3-5 files most critical for implementing this plan:
|
|
112
|
+
- /absolute/path/to/file.ts - [Brief reason]`,
|
|
113
|
+
promptMode: "replace",
|
|
114
|
+
isDefault: true,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
]);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentConfig, ThinkingLevel } from "../types";
|
|
2
|
+
|
|
3
|
+
interface AgentInvocationParams {
|
|
4
|
+
model?: string;
|
|
5
|
+
thinking?: string;
|
|
6
|
+
max_turns?: number;
|
|
7
|
+
run_in_background?: boolean;
|
|
8
|
+
inherit_context?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveAgentInvocationConfig(
|
|
12
|
+
agentConfig: AgentConfig | undefined,
|
|
13
|
+
params: AgentInvocationParams,
|
|
14
|
+
): {
|
|
15
|
+
modelInput?: string;
|
|
16
|
+
modelFromParams: boolean;
|
|
17
|
+
thinking?: ThinkingLevel;
|
|
18
|
+
maxTurns?: number;
|
|
19
|
+
inheritContext: boolean;
|
|
20
|
+
runInBackground: boolean;
|
|
21
|
+
} {
|
|
22
|
+
return {
|
|
23
|
+
modelInput: agentConfig?.model ?? params.model,
|
|
24
|
+
modelFromParams: agentConfig?.model == null && params.model != null,
|
|
25
|
+
thinking: (agentConfig?.thinking ?? params.thinking) as ThinkingLevel | undefined,
|
|
26
|
+
maxTurns: agentConfig?.maxTurns ?? params.max_turns,
|
|
27
|
+
inheritContext: agentConfig?.inheritContext ?? params.inherit_context ?? false,
|
|
28
|
+
runInBackground: agentConfig?.runInBackground ?? params.run_in_background ?? false,
|
|
29
|
+
};
|
|
30
|
+
}
|
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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn_start event handler that aborts subagents on a parent interrupt (ESC).
|
|
3
|
+
*
|
|
4
|
+
* The parent agent loop creates a fresh AbortController per run and only aborts
|
|
5
|
+
* it on an explicit interrupt — never on normal completion. So latching to the
|
|
6
|
+
* current run's signal and aborting on its `abort` event fires exactly on ESC.
|
|
7
|
+
*
|
|
8
|
+
* `turn_start` carries the live per-run `ctx.signal`, so re-latching each turn
|
|
9
|
+
* keeps the handler tracking the current signal across runs and tool-less turns.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Narrow manager interface — only the method the interrupt handler calls. */
|
|
13
|
+
export interface InterruptManager {
|
|
14
|
+
abortAll(): number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Minimal context shape — only the field the handler reads. */
|
|
18
|
+
interface InterruptCtx {
|
|
19
|
+
signal: AbortSignal | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Latches the current parent abort signal and aborts all subagents when it fires.
|
|
24
|
+
*
|
|
25
|
+
* The latch dedups by reference: most turns reuse the same signal (no-op); a new
|
|
26
|
+
* run's signal triggers a detach-and-rewire. The `abort` listener is one-shot.
|
|
27
|
+
*/
|
|
28
|
+
export class InterruptHandler {
|
|
29
|
+
private latched?: AbortSignal;
|
|
30
|
+
private detach?: () => void;
|
|
31
|
+
|
|
32
|
+
constructor(private readonly manager: InterruptManager) {}
|
|
33
|
+
|
|
34
|
+
handleTurnStart(ctx: InterruptCtx): void {
|
|
35
|
+
const signal = ctx.signal;
|
|
36
|
+
if (signal === this.latched) return;
|
|
37
|
+
|
|
38
|
+
this.detach?.();
|
|
39
|
+
this.detach = undefined;
|
|
40
|
+
this.latched = signal;
|
|
41
|
+
if (!signal) return;
|
|
42
|
+
|
|
43
|
+
const onAbort = (): void => {
|
|
44
|
+
this.manager.abortAll();
|
|
45
|
+
};
|
|
46
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
47
|
+
this.detach = () => signal.removeEventListener("abort", onAbort);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { SessionContext } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session lifecycle event handlers: session_start, session_before_switch, session_shutdown.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from index.ts so each handler can be tested in isolation
|
|
7
|
+
* with mocked narrow interfaces.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Narrow manager interface — only the methods lifecycle handlers call. */
|
|
11
|
+
export interface LifecycleManager {
|
|
12
|
+
clearCompleted(): void;
|
|
13
|
+
abortAll(): void;
|
|
14
|
+
dispose(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Narrow runtime interface — only the methods lifecycle handlers call. */
|
|
18
|
+
export interface LifecycleRuntime {
|
|
19
|
+
setSessionContext(ctx: SessionContext): void;
|
|
20
|
+
clearSessionContext(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Handles session lifecycle events.
|
|
25
|
+
*
|
|
26
|
+
* Constructor deps:
|
|
27
|
+
* - `runtime` — owns session context state
|
|
28
|
+
* - `manager` — manages agent lifecycle (clear, abort, dispose)
|
|
29
|
+
* - `disposeNotifications` — tears down the notification system on shutdown
|
|
30
|
+
* - `unpublishService` — unpublishes the SubagentsService symbol on shutdown
|
|
31
|
+
*/
|
|
32
|
+
export class SessionLifecycleHandler {
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly runtime: LifecycleRuntime,
|
|
35
|
+
private readonly manager: LifecycleManager,
|
|
36
|
+
private readonly disposeNotifications: () => void,
|
|
37
|
+
private readonly unpublishService: () => void,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
handleSessionStart(_event: unknown, ctx: unknown): void {
|
|
41
|
+
this.runtime.setSessionContext(ctx as SessionContext);
|
|
42
|
+
this.manager.clearCompleted();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handleSessionBeforeSwitch(): void {
|
|
46
|
+
this.manager.clearCompleted();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Cleanup order matters:
|
|
50
|
+
// 1. Unpublish service — prevent new cross-extension calls
|
|
51
|
+
// 2. Clear session context — no more session state
|
|
52
|
+
// 3. Abort all agents — stop running work
|
|
53
|
+
// 4. Dispose notifications — cancel pending nudges/timers
|
|
54
|
+
// 5. Dispose manager — final cleanup
|
|
55
|
+
handleSessionShutdown(): Promise<void> {
|
|
56
|
+
this.unpublishService();
|
|
57
|
+
this.runtime.clearSessionContext();
|
|
58
|
+
this.manager.abortAll();
|
|
59
|
+
this.disposeNotifications();
|
|
60
|
+
this.manager.dispose();
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool_execution_start event handler.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts so the handler can be tested in isolation
|
|
5
|
+
* with a mocked narrow runtime interface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Narrow widget interface — only the methods the handler calls. */
|
|
9
|
+
export interface ToolStartWidget {
|
|
10
|
+
setUICtx(ctx: unknown): void;
|
|
11
|
+
onTurnStart(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Minimal context shape for tool_execution_start — only the field the handler reads. */
|
|
15
|
+
interface ToolStartCtx {
|
|
16
|
+
ui: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handles tool_execution_start events.
|
|
21
|
+
*
|
|
22
|
+
* Grabs UI context from the first tool execution of each turn
|
|
23
|
+
* and signals the widget to clear lingering state.
|
|
24
|
+
*/
|
|
25
|
+
export class ToolStartHandler {
|
|
26
|
+
constructor(private readonly widget: ToolStartWidget) {}
|
|
27
|
+
|
|
28
|
+
handleToolExecutionStart(_event: unknown, ctx: ToolStartCtx): void {
|
|
29
|
+
this.widget.setUICtx(ctx.ui);
|
|
30
|
+
this.widget.onTurnStart();
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
2
|
+
/**
|
|
3
|
+
* pi-agents — A pi extension providing focused, in-process autonomous sub-agents.
|
|
4
|
+
*
|
|
5
|
+
* Tools:
|
|
6
|
+
* Agent — LLM-callable: spawn a sub-agent
|
|
7
|
+
* get_subagent_result — LLM-callable: check background agent status/result
|
|
8
|
+
* steer_subagent — LLM-callable: send a steering message to a running agent
|
|
9
|
+
*
|
|
10
|
+
* Commands:
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import {
|
|
15
|
+
createAgentSession,
|
|
16
|
+
DefaultResourceLoader,
|
|
17
|
+
type ExtensionAPI,
|
|
18
|
+
getAgentDir,
|
|
19
|
+
SettingsManager as SdkSettingsManager,
|
|
20
|
+
SessionManager,
|
|
21
|
+
} from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { AgentTypeRegistry } from "./config/agent-types";
|
|
23
|
+
import { loadCustomAgents } from "./config/custom-agents";
|
|
24
|
+
import { InterruptHandler, SessionLifecycleHandler, ToolStartHandler } from "./handlers/index";
|
|
25
|
+
import { createChildLifecyclePublisher } from "./lifecycle/child-lifecycle";
|
|
26
|
+
import { ConcurrencyLimiter } from "./lifecycle/concurrency-limiter";
|
|
27
|
+
import { createSubagentSession, type SubagentSessionDeps } from "./lifecycle/create-subagent-session";
|
|
28
|
+
import { SubagentManager } from "./lifecycle/subagent-manager";
|
|
29
|
+
import { CompositeSubagentObserver } from "./observation/composite-subagent-observer";
|
|
30
|
+
import { type NotificationDetails, NotificationManager } from "./observation/notification";
|
|
31
|
+
import { createNotificationRenderer } from "./observation/renderer";
|
|
32
|
+
import { SubagentEventsObserver } from "./observation/subagent-events-observer";
|
|
33
|
+
import { createSubagentRuntime } from "./runtime";
|
|
34
|
+
import { publishSubagentsService, unpublishSubagentsService } from "./service/service";
|
|
35
|
+
import { SubagentsServiceAdapter } from "./service/service-adapter";
|
|
36
|
+
import { detectEnv } from "./session/env";
|
|
37
|
+
|
|
38
|
+
import { resolveModel } from "./session/model-resolver";
|
|
39
|
+
import { buildAgentPrompt } from "./session/prompts";
|
|
40
|
+
import { deriveSubagentSessionDir } from "./session/session-dir";
|
|
41
|
+
import { SettingsManager } from "./settings";
|
|
42
|
+
import { AgentTool } from "./tools/agent-tool";
|
|
43
|
+
import { GetResultTool } from "./tools/get-result-tool";
|
|
44
|
+
import { SteerTool } from "./tools/steer-tool";
|
|
45
|
+
import { AgentWidget } from "./ui/agent-widget";
|
|
46
|
+
import { SessionNavigatorHandler } from "./ui/session-navigator";
|
|
47
|
+
import { SubagentsSettingsHandler } from "./ui/subagents-settings";
|
|
48
|
+
|
|
49
|
+
export default function (pi: ExtensionAPI) {
|
|
50
|
+
// ---- Register custom notification renderer ----
|
|
51
|
+
pi.registerMessageRenderer<NotificationDetails>("subagent-notification", createNotificationRenderer());
|
|
52
|
+
|
|
53
|
+
const registry = new AgentTypeRegistry(() => loadCustomAgents(process.cwd()));
|
|
54
|
+
|
|
55
|
+
// ---- Runtime: all mutable extension state in one place ----
|
|
56
|
+
const runtime = createSubagentRuntime();
|
|
57
|
+
|
|
58
|
+
// ---- Notification system ----
|
|
59
|
+
// Owns completion nudges and live-activity cleanup. The widget detects finished
|
|
60
|
+
// agents itself (AgentWidget.update self-seeds), so NotificationManager has no
|
|
61
|
+
// widget dependency — keeping the construction graph a cycle-free DAG.
|
|
62
|
+
const notifications = new NotificationManager((msg, opts) => pi.sendMessage(msg, opts));
|
|
63
|
+
|
|
64
|
+
// Settings: owns all three in-memory values and handles load/save/emit.
|
|
65
|
+
// onMaxConcurrentChanged is wired to the limiter directly (closure captures by reference).
|
|
66
|
+
const settings = new SettingsManager({
|
|
67
|
+
emit: (event, payload) => pi.events.emit(event, payload),
|
|
68
|
+
cwd: process.cwd(),
|
|
69
|
+
agentDir: getAgentDir(),
|
|
70
|
+
onMaxConcurrentChanged: () => limiter.recheck(),
|
|
71
|
+
});
|
|
72
|
+
settings.load();
|
|
73
|
+
|
|
74
|
+
// Observer: receives agent lifecycle notifications and dispatches events/notifications.
|
|
75
|
+
const eventsObserver = new SubagentEventsObserver({
|
|
76
|
+
emit: (channel, data) => pi.events.emit(channel, data),
|
|
77
|
+
appendEntry: (customType, data) => pi.appendEntry(customType, data),
|
|
78
|
+
notifications,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Fan-out observer: lets the widget subscribe as a second lifecycle consumer
|
|
82
|
+
// while the manager keeps its single-observer contract. The widget is added
|
|
83
|
+
// after construction (it needs the manager); the manager consults the observer
|
|
84
|
+
// only at spawn time, so registering late is safe.
|
|
85
|
+
const observer = new CompositeSubagentObserver([eventsObserver]);
|
|
86
|
+
|
|
87
|
+
const subagentSessionDeps: SubagentSessionDeps = {
|
|
88
|
+
io: {
|
|
89
|
+
detectEnv,
|
|
90
|
+
getAgentDir,
|
|
91
|
+
createResourceLoader: (opts) => new DefaultResourceLoader(opts),
|
|
92
|
+
deriveSessionDir: deriveSubagentSessionDir,
|
|
93
|
+
createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
|
|
94
|
+
createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
|
|
95
|
+
createSession: (opts) => createAgentSession(opts as any),
|
|
96
|
+
assemblerIO: {
|
|
97
|
+
buildAgentPrompt,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
|
|
101
|
+
registry,
|
|
102
|
+
lifecycle: createChildLifecyclePublisher((channel, data) => pi.events.emit(channel, data)),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ConcurrencyLimiter: schedules background run thunks FIFO against the limit.
|
|
106
|
+
// It knows nothing about agents or the manager — dependency direction is strictly manager → limiter.
|
|
107
|
+
const limiter = new ConcurrencyLimiter(() => settings.maxConcurrent);
|
|
108
|
+
|
|
109
|
+
const manager = new SubagentManager({
|
|
110
|
+
createSubagentSession: (params) => createSubagentSession(params, subagentSessionDeps),
|
|
111
|
+
baseCwd: process.cwd(),
|
|
112
|
+
observer,
|
|
113
|
+
limiter,
|
|
114
|
+
getRunConfig: () => settings,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Typed service published via Symbol.for() for cross-extension access.
|
|
118
|
+
// Consumers: const { getSubagentsService } = await import("@yandy0725/pi-subagents");
|
|
119
|
+
const service = new SubagentsServiceAdapter(manager, resolveModel, runtime);
|
|
120
|
+
publishSubagentsService(service);
|
|
121
|
+
|
|
122
|
+
const lifecycle = new SessionLifecycleHandler(
|
|
123
|
+
runtime,
|
|
124
|
+
manager,
|
|
125
|
+
() => notifications.dispose(),
|
|
126
|
+
unpublishSubagentsService,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
pi.on("session_start", (event, ctx) => lifecycle.handleSessionStart(event, ctx));
|
|
130
|
+
pi.on("session_before_switch", () => lifecycle.handleSessionBeforeSwitch());
|
|
131
|
+
pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
|
|
132
|
+
|
|
133
|
+
// Live widget: constructed after the manager (it polls listAgents()) and
|
|
134
|
+
// registered as a lifecycle observer so it self-drives its update timer.
|
|
135
|
+
const widget = new AgentWidget(manager, registry);
|
|
136
|
+
observer.add(widget);
|
|
137
|
+
|
|
138
|
+
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
139
|
+
const toolStart = new ToolStartHandler(widget);
|
|
140
|
+
pi.on("tool_execution_start", (event, ctx) => toolStart.handleToolExecutionStart(event, ctx));
|
|
141
|
+
|
|
142
|
+
// Abort all subagents when the parent agent loop is interrupted (ESC).
|
|
143
|
+
const interrupt = new InterruptHandler(manager);
|
|
144
|
+
pi.on("turn_start", (_event, ctx) => interrupt.handleTurnStart(ctx));
|
|
145
|
+
|
|
146
|
+
// ---- Agent tool ----
|
|
147
|
+
|
|
148
|
+
pi.registerTool(new AgentTool(manager, runtime, settings, registry, getAgentDir()).toToolDefinition());
|
|
149
|
+
|
|
150
|
+
// ---- get_subagent_result tool ----
|
|
151
|
+
|
|
152
|
+
pi.registerTool(new GetResultTool(manager, notifications, registry).toToolDefinition());
|
|
153
|
+
|
|
154
|
+
// ---- steer_subagent tool ----
|
|
155
|
+
|
|
156
|
+
pi.registerTool(new SteerTool(manager, pi.events).toToolDefinition());
|
|
157
|
+
|
|
158
|
+
// ---- /subagents:settings command ----
|
|
159
|
+
|
|
160
|
+
const subagentsSettings = new SubagentsSettingsHandler(settings);
|
|
161
|
+
|
|
162
|
+
pi.registerCommand("subagents:settings", {
|
|
163
|
+
description: "Configure subagent settings (concurrency, turn limits)",
|
|
164
|
+
handler: async (_args, ctx) => {
|
|
165
|
+
await subagentsSettings.handle({ ui: ctx.ui });
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ---- /subagents:sessions command ----
|
|
170
|
+
|
|
171
|
+
const sessionNavigator = new SessionNavigatorHandler();
|
|
172
|
+
|
|
173
|
+
pi.registerCommand("subagents:sessions", {
|
|
174
|
+
description: "View a subagent's session transcript (read-only)",
|
|
175
|
+
handler: async (_args, ctx) => {
|
|
176
|
+
await sessionNavigator.handle({
|
|
177
|
+
ui: ctx.ui,
|
|
178
|
+
agents: manager.listAgents(),
|
|
179
|
+
evicted: manager.listEvicted(),
|
|
180
|
+
registry,
|
|
181
|
+
cwd: ctx.cwd,
|
|
182
|
+
readFile: (path) => readFileSync(path, "utf8"),
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic layered settings loader for `@yandy0725/pi-*` extensions.
|
|
3
|
+
*
|
|
4
|
+
* Extensions that store configuration in JSON files under a global agent
|
|
5
|
+
* directory and a per-project `.pi/` folder share the same three-step idiom:
|
|
6
|
+
*
|
|
7
|
+
* 1. Read the global file (`<agentDir>/<filename>`).
|
|
8
|
+
* 2. Read the project file (`<cwd>/.pi/<filename>`).
|
|
9
|
+
* 3. Merge them — project wins on conflicts — and return the result.
|
|
10
|
+
*
|
|
11
|
+
* Both layers are optional: a missing file is silent (`{}`), and a file that
|
|
12
|
+
* cannot be parsed warns to stderr and is treated as absent so startup
|
|
13
|
+
* proceeds normally.
|
|
14
|
+
*
|
|
15
|
+
* ## Usage
|
|
16
|
+
*
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { loadLayeredSettings, type LayeredSettingsSource } from "@yandy0725/pi-subagents/settings";
|
|
19
|
+
*
|
|
20
|
+
* interface MyConfig { enabled?: boolean; limit?: number }
|
|
21
|
+
*
|
|
22
|
+
* function sanitize(raw: unknown): Partial<MyConfig> {
|
|
23
|
+
* if (!raw || typeof raw !== "object") return {};
|
|
24
|
+
* const r = raw as Record<string, unknown>;
|
|
25
|
+
* const out: Partial<MyConfig> = {};
|
|
26
|
+
* if (typeof r.enabled === "boolean") out.enabled = r.enabled;
|
|
27
|
+
* if (typeof r.limit === "number") out.limit = r.limit;
|
|
28
|
+
* return out;
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* const config = loadLayeredSettings<MyConfig>({
|
|
32
|
+
* agentDir, // e.g. from the Pi runtime env — the agent home directory
|
|
33
|
+
* cwd, // project root — project file is at <cwd>/.pi/<filename>
|
|
34
|
+
* filename: "my-extension.json",
|
|
35
|
+
* sanitize,
|
|
36
|
+
* warnLabel: "my-extension",
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
44
|
+
import { join } from "node:path";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parameters for one layered settings load: describes where the files live,
|
|
48
|
+
* how to validate their contents, and what label to use in warnings.
|
|
49
|
+
*
|
|
50
|
+
* @public
|
|
51
|
+
*/
|
|
52
|
+
export interface LayeredSettingsSource<T> {
|
|
53
|
+
/** Directory holding the global settings file (typically the Pi agent dir). */
|
|
54
|
+
agentDir: string;
|
|
55
|
+
/** Project root; the project file lives at `<cwd>/.pi/<filename>`. */
|
|
56
|
+
cwd: string;
|
|
57
|
+
/** Base filename for both layers, e.g. `"subagents.json"`. */
|
|
58
|
+
filename: string;
|
|
59
|
+
/**
|
|
60
|
+
* Validate and coerce parsed JSON into a partial settings object.
|
|
61
|
+
* Unknown or invalid fields should be silently dropped — return `{}` for
|
|
62
|
+
* unrecognised shapes. Never throw.
|
|
63
|
+
*/
|
|
64
|
+
sanitize: (raw: unknown) => Partial<T>;
|
|
65
|
+
/**
|
|
66
|
+
* Short label used in the malformed-file warning prefix,
|
|
67
|
+
* e.g. `"pi-subagents"` → `"[pi-subagents] Ignoring malformed settings at …"`.
|
|
68
|
+
*/
|
|
69
|
+
warnLabel: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Load merged layered settings: global provides defaults, project overrides.
|
|
74
|
+
*
|
|
75
|
+
* - A missing file is silent — returns `{}` for that layer.
|
|
76
|
+
* - A file that exists but cannot be parsed warns to stderr and returns `{}` for
|
|
77
|
+
* that layer, so startup proceeds normally.
|
|
78
|
+
* - The two layers are merged with a shallow spread; project keys win.
|
|
79
|
+
*
|
|
80
|
+
* Throws nothing. All error conditions produce a warning and fall back to `{}`.
|
|
81
|
+
*
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
export function loadLayeredSettings<T>(source: LayeredSettingsSource<T>): Partial<T> {
|
|
85
|
+
const { agentDir, cwd, filename, sanitize, warnLabel } = source;
|
|
86
|
+
const global = readLayer(join(agentDir, filename), sanitize, warnLabel);
|
|
87
|
+
const project = readLayer(join(cwd, ".pi", filename), sanitize, warnLabel);
|
|
88
|
+
return { ...global, ...project };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Private helpers ──────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read one settings file. Missing → `{}` (silent). Malformed → `{}` + warn.
|
|
95
|
+
*/
|
|
96
|
+
function readLayer<T>(path: string, sanitize: (raw: unknown) => Partial<T>, warnLabel: string): Partial<T> {
|
|
97
|
+
if (!existsSync(path)) return {};
|
|
98
|
+
try {
|
|
99
|
+
return sanitize(JSON.parse(readFileSync(path, "utf-8")));
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
102
|
+
console.warn(`[${warnLabel}] Ignoring malformed settings at ${path}: ${reason}`);
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
}
|