@nklisch/pi-enhanced 0.2.7 → 0.3.1
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 +16 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.linux-x64-gnu.node +0 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-plugins/README.md +4 -2
- package/node_modules/@nklisch/pi-plugins/dist/hooks.js +81 -8
- package/node_modules/@nklisch/pi-plugins/dist/hooks.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/mcp.js +32 -1
- package/node_modules/@nklisch/pi-plugins/dist/mcp.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.d.ts +10 -4
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js +144 -10
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js +40 -14
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/CHANGELOG.md +27 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +76 -47
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/public.d.ts +171 -140
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/settings.d.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/FORK-MAINTENANCE.md +8 -7
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/VISION.md +12 -10
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +118 -104
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/comparison-with-upstream.md +3 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0004-reconsider-ui-direction.md +5 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/custom-agents.ts +31 -4
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/default-agents.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/invocation-config.ts +7 -4
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/index.ts +18 -9
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/layered-settings.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/child-lifecycle.ts +10 -63
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/concurrency-limiter.ts +122 -76
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/create-subagent-session.ts +3 -5
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/lifecycle-interceptor.ts +4 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/run-listeners.ts +2 -23
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-manager.ts +177 -252
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-session.ts +90 -280
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-state.ts +153 -239
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent.ts +569 -700
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/workspace.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/notification.ts +78 -122
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/renderer.ts +15 -42
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/subagent-events-observer.ts +44 -95
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service-adapter.ts +157 -90
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service.ts +60 -68
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/conversation.ts +3 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query-source.ts +28 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query.ts +488 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/settings.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/agent-tool.ts +171 -249
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-tool.ts +51 -94
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/helpers.ts +9 -6
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/list-tool.ts +46 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/parent-tool-registry.ts +17 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/query-session-tool.ts +412 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/result-renderer.ts +27 -111
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/resume-tool.ts +105 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/spawn-config.ts +26 -8
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/steer-tool.ts +27 -111
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/stop-tool.ts +53 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/types.ts +11 -5
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/agent-widget.ts +33 -27
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/display.ts +6 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigation.ts +121 -10
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigator.ts +430 -112
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/subagents-settings.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/widget-renderer.ts +8 -9
- package/node_modules/@nklisch/pi-plugins/package.json +2 -2
- package/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/background-spawner.ts +0 -81
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/foreground-runner.ts +0 -149
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-report.ts +0 -77
|
@@ -5,6 +5,11 @@ date: 2026-06-18
|
|
|
5
5
|
|
|
6
6
|
# 0004 — Reconsider the UI direction from first principles
|
|
7
7
|
|
|
8
|
+
> Historical vocabulary note: this decision predates the joined/detached clean
|
|
9
|
+
> break. In current code, its "background-only widget" means detached-only;
|
|
10
|
+
> joined runs use their inline result surface. Historical delivery labels below
|
|
11
|
+
> are retained as decision history, not as current API contracts.
|
|
12
|
+
|
|
8
13
|
## Status
|
|
9
14
|
|
|
10
15
|
Accepted.
|
|
@@ -7,7 +7,7 @@ import { basename, join } from "node:path";
|
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { BUILTIN_TOOL_NAMES } from "#src/config/agent-types";
|
|
9
9
|
import { debugLog } from "#src/debug";
|
|
10
|
-
import type { AgentConfig, ThinkingLevel } from "#src/types";
|
|
10
|
+
import type { AgentConfig, ThinkingLevel, SubagentMode } from "#src/types";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Scan for custom agent .md files from multiple locations.
|
|
@@ -60,6 +60,27 @@ function loadFromDir(
|
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
62
|
const { frontmatter: fm, body } = parseFrontmatter(content);
|
|
63
|
+
if (fm.run_in_background !== undefined || fm.foreground !== undefined) {
|
|
64
|
+
const path = join(dir, file);
|
|
65
|
+
const removedFields = [
|
|
66
|
+
fm.run_in_background !== undefined ? "run_in_background" : undefined,
|
|
67
|
+
fm.foreground !== undefined ? "foreground" : undefined,
|
|
68
|
+
].filter((field): field is string => field !== undefined).join(", ");
|
|
69
|
+
// Reserve the name as disabled so a removed field cannot silently fall
|
|
70
|
+
// through to a lower-priority file or an embedded default with a
|
|
71
|
+
// different delivery mode. The warning gives the owner an actionable
|
|
72
|
+
// migration path instead of making the refusal look like a missing file.
|
|
73
|
+
console.warn(`[pi-subagents] Ignoring ${path}: removed delivery field(s) ${removedFields}; use mode: joined|detached.`);
|
|
74
|
+
agents.set(name, {
|
|
75
|
+
name,
|
|
76
|
+
description: `Invalid agent definition: ${removedFields}`,
|
|
77
|
+
systemPrompt: "",
|
|
78
|
+
promptMode: "append",
|
|
79
|
+
enabled: false,
|
|
80
|
+
source,
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
63
84
|
agents.set(name, {
|
|
64
85
|
name,
|
|
65
86
|
displayName: str(fm.display_name),
|
|
@@ -68,10 +89,11 @@ function loadFromDir(
|
|
|
68
89
|
model: str(fm.model),
|
|
69
90
|
thinking: str(fm.thinking) as ThinkingLevel | undefined,
|
|
70
91
|
maxTurns: nonNegativeInt(fm.max_turns),
|
|
92
|
+
timeoutSeconds: positiveInt(fm.timeout_seconds),
|
|
71
93
|
systemPrompt: body.trim(),
|
|
72
94
|
promptMode: fm.prompt_mode === "replace" ? "replace" : "append",
|
|
73
95
|
inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
|
|
74
|
-
|
|
96
|
+
mode: fm.mode === "joined" || fm.mode === "detached" ? fm.mode as SubagentMode : undefined,
|
|
75
97
|
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
76
98
|
source,
|
|
77
99
|
});
|
|
@@ -91,9 +113,14 @@ function str(val: unknown): string | undefined {
|
|
|
91
113
|
return typeof val === "string" ? val : undefined;
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
/** Extract a non-negative integer
|
|
116
|
+
/** Extract a non-negative integer; 0 means unlimited for max_turns. */
|
|
95
117
|
function nonNegativeInt(val: unknown): number | undefined {
|
|
96
|
-
return typeof val === "number" && val >= 0 ? val : undefined;
|
|
118
|
+
return typeof val === "number" && Number.isInteger(val) && val >= 0 ? val : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Extract a positive integer for an active runtime deadline. */
|
|
122
|
+
function positiveInt(val: unknown): number | undefined {
|
|
123
|
+
return typeof val === "number" && Number.isInteger(val) && val > 0 ? val : undefined;
|
|
97
124
|
}
|
|
98
125
|
|
|
99
126
|
/**
|
|
@@ -17,7 +17,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
17
17
|
description: "General-purpose agent for complex, multi-step tasks",
|
|
18
18
|
toolGuideline: "- Use general-purpose for complex tasks that need file editing.",
|
|
19
19
|
// builtinToolNames omitted — means "all available tools" (resolved at lookup time)
|
|
20
|
-
// inheritContext /
|
|
20
|
+
// inheritContext / mode omitted — delivery strategy is selected by the caller.
|
|
21
21
|
// Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
|
|
22
22
|
systemPrompt: "",
|
|
23
23
|
promptMode: "append",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { AgentConfig, ThinkingLevel } from "#src/types";
|
|
1
|
+
import type { AgentConfig, SubagentMode, ThinkingLevel } from "#src/types";
|
|
2
2
|
|
|
3
3
|
interface AgentInvocationParams {
|
|
4
4
|
model?: string;
|
|
5
5
|
thinking?: string;
|
|
6
6
|
max_turns?: number;
|
|
7
|
-
|
|
7
|
+
mode?: SubagentMode;
|
|
8
|
+
timeout_seconds?: number;
|
|
8
9
|
inherit_context?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -16,15 +17,17 @@ export function resolveAgentInvocationConfig(
|
|
|
16
17
|
modelFromParams: boolean;
|
|
17
18
|
thinking?: ThinkingLevel;
|
|
18
19
|
maxTurns?: number;
|
|
20
|
+
mode: SubagentMode;
|
|
21
|
+
timeoutSeconds?: number;
|
|
19
22
|
inheritContext: boolean;
|
|
20
|
-
runInBackground: boolean;
|
|
21
23
|
} {
|
|
22
24
|
return {
|
|
23
25
|
modelInput: agentConfig?.model ?? params.model,
|
|
24
26
|
modelFromParams: agentConfig?.model == null && params.model != null,
|
|
25
27
|
thinking: (agentConfig?.thinking ?? params.thinking) as ThinkingLevel | undefined,
|
|
26
28
|
maxTurns: agentConfig?.maxTurns ?? params.max_turns,
|
|
29
|
+
mode: agentConfig?.mode ?? params.mode ?? "detached",
|
|
30
|
+
timeoutSeconds: agentConfig?.timeoutSeconds ?? params.timeout_seconds,
|
|
27
31
|
inheritContext: agentConfig?.inheritContext ?? params.inherit_context ?? false,
|
|
28
|
-
runInBackground: agentConfig?.runInBackground ?? params.run_in_background ?? false,
|
|
29
32
|
};
|
|
30
33
|
}
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Tools:
|
|
6
6
|
* Agent — LLM-callable: spawn a sub-agent
|
|
7
|
-
* get_subagent_result — LLM-callable:
|
|
8
|
-
*
|
|
7
|
+
* get_subagent_result — LLM-callable: inspect bounded subagent status/result
|
|
8
|
+
* resume_subagent — LLM-callable: resume a retained settled session
|
|
9
|
+
* stop_subagent — LLM-callable: request cooperative cancellation
|
|
10
|
+
* list_subagents — LLM-callable: inspect fleet state
|
|
11
|
+
* steer_subagent — LLM-callable: send a steering message to a running agent
|
|
12
|
+
* query_subagent_session — LLM-callable: search a bounded child transcript
|
|
9
13
|
*
|
|
10
14
|
* Commands:
|
|
11
15
|
*/
|
|
@@ -41,7 +45,11 @@ import { deriveSubagentSessionDir } from "#src/session/session-dir";
|
|
|
41
45
|
import { SettingsManager } from "#src/settings";
|
|
42
46
|
import { AgentTool } from "#src/tools/agent-tool";
|
|
43
47
|
import { GetResultTool } from "#src/tools/get-result-tool";
|
|
48
|
+
import { ListTool } from "#src/tools/list-tool";
|
|
49
|
+
import { QuerySessionTool } from "#src/tools/query-session-tool";
|
|
50
|
+
import { ResumeTool } from "#src/tools/resume-tool";
|
|
44
51
|
import { SteerTool } from "#src/tools/steer-tool";
|
|
52
|
+
import { StopTool } from "#src/tools/stop-tool";
|
|
45
53
|
import { AgentWidget } from "#src/ui/agent-widget";
|
|
46
54
|
import { SessionNavigatorHandler } from "#src/ui/session-navigator";
|
|
47
55
|
import { SubagentsSettingsHandler } from "#src/ui/subagents-settings";
|
|
@@ -105,7 +113,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
105
113
|
lifecycle: createChildLifecyclePublisher((channel, data) => pi.events.emit(channel, data)),
|
|
106
114
|
};
|
|
107
115
|
|
|
108
|
-
// ConcurrencyLimiter: schedules
|
|
116
|
+
// ConcurrencyLimiter: schedules run thunks FIFO against the limit.
|
|
109
117
|
// It knows nothing about agents or the manager — dependency direction is strictly manager → limiter.
|
|
110
118
|
const limiter = new ConcurrencyLimiter(() => settings.maxConcurrent);
|
|
111
119
|
|
|
@@ -118,7 +126,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
118
126
|
});
|
|
119
127
|
|
|
120
128
|
// Typed service published via Symbol.for() for cross-extension access.
|
|
121
|
-
// Consumers: const { getSubagentsService } = await import("@
|
|
129
|
+
// Consumers: const { getSubagentsService } = await import("@nklisch/pi-subagents");
|
|
122
130
|
const service = new SubagentsServiceAdapter(manager, resolveModel, runtime, registry, settings);
|
|
123
131
|
publishSubagentsService(service);
|
|
124
132
|
|
|
@@ -151,13 +159,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
151
159
|
|
|
152
160
|
pi.registerTool(new AgentTool(manager, runtime, settings, registry, getAgentDir()).toToolDefinition());
|
|
153
161
|
|
|
154
|
-
// ----
|
|
162
|
+
// ---- parent-only lifecycle control tools ----
|
|
155
163
|
|
|
164
|
+
pi.registerTool(new ResumeTool(manager).toToolDefinition());
|
|
165
|
+
pi.registerTool(new StopTool(manager).toToolDefinition());
|
|
166
|
+
pi.registerTool(new ListTool(manager).toToolDefinition());
|
|
156
167
|
pi.registerTool(new GetResultTool(manager, registry).toToolDefinition());
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
pi.registerTool(new SteerTool(manager, pi.events).toToolDefinition());
|
|
168
|
+
pi.registerTool(new SteerTool({ steer: (id, message) => manager.steer(id, message) }, pi.events).toToolDefinition());
|
|
169
|
+
pi.registerTool(new QuerySessionTool(manager, (path) => readFileSync(path, "utf8")).toToolDefinition());
|
|
161
170
|
|
|
162
171
|
// ---- /subagents:settings command ----
|
|
163
172
|
|
package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/layered-settings.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generic layered settings loader for `@
|
|
2
|
+
* Generic layered settings loader for `@nklisch/pi-*` extensions.
|
|
3
3
|
*
|
|
4
4
|
* Extensions that store configuration in JSON files under a global agent
|
|
5
5
|
* directory and a per-project `.pi/` folder share the same three-step idiom:
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* ## Usage
|
|
16
16
|
*
|
|
17
17
|
* ```typescript
|
|
18
|
-
* import { loadLayeredSettings, type LayeredSettingsSource } from "@
|
|
18
|
+
* import { loadLayeredSettings, type LayeredSettingsSource } from "@nklisch/pi-subagents/settings";
|
|
19
19
|
*
|
|
20
20
|
* interface MyConfig { enabled?: boolean; limit?: number }
|
|
21
21
|
*
|
|
@@ -1,69 +1,35 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* child-lifecycle.ts — Child-execution lifecycle event contract and publisher.
|
|
3
|
-
*
|
|
4
|
-
* The core publishes its child-execution lifecycle as ordered events on the Pi
|
|
5
|
-
* event bus; reactive consumers (permissions, telemetry, UI) subscribe rather
|
|
6
|
-
* than the core reaching out to them (ADR 0002). This module owns the channel
|
|
7
|
-
* names, payload shapes, and the publisher that emits them.
|
|
8
|
-
*
|
|
9
|
-
* The publisher takes an injected `emit` callback so this module stays free of
|
|
10
|
-
* Pi SDK imports — `index.ts` wires it to `pi.events.emit`.
|
|
11
|
-
*/
|
|
1
|
+
/** Ordered child-session lifecycle hooks used by cooperating extensions. */
|
|
12
2
|
|
|
13
3
|
import { debugLog } from "#src/debug";
|
|
4
|
+
import type { SubagentLifecycleOutcome } from "#src/lifecycle/lifecycle-interceptor";
|
|
14
5
|
|
|
15
|
-
/** Emitted at the start of a child run, before the session is created. */
|
|
16
6
|
export const SUBAGENT_CHILD_SPAWNING = "subagents:child:spawning";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Emitted after the child session is created, immediately before
|
|
20
|
-
* `bindExtensions()`. Carries the child session id consumers need to register
|
|
21
|
-
* the session in `SubagentSessionRegistry`. Subscribers must register
|
|
22
|
-
* synchronously so the entry lands before binding proceeds (see ADR 0002 /
|
|
23
|
-
* the event-bus synchronous-dispatch guarantee).
|
|
24
|
-
*/
|
|
25
7
|
export const SUBAGENT_CHILD_SESSION_CREATED = "subagents:child:session-created";
|
|
26
|
-
|
|
27
|
-
/** Emitted after the child's prompt resolves (normal, steered, or aborted). */
|
|
28
8
|
export const SUBAGENT_CHILD_COMPLETED = "subagents:child:completed";
|
|
29
|
-
|
|
30
|
-
/** Emitted in the run's `finally` — always fires, on success and error. */
|
|
31
9
|
export const SUBAGENT_CHILD_DISPOSED = "subagents:child:disposed";
|
|
32
10
|
|
|
33
|
-
/** Payload for `subagents:child:spawning`. */
|
|
34
11
|
export interface ChildSpawningEvent {
|
|
35
12
|
agentName: string;
|
|
36
13
|
parentSessionId?: string;
|
|
37
14
|
}
|
|
38
15
|
|
|
39
|
-
/** Payload for `subagents:child:session-created`. */
|
|
40
16
|
export interface ChildSessionCreatedEvent {
|
|
41
|
-
/** Child session id — the registry key. Unique per child; concurrent
|
|
42
|
-
* siblings of the same parent occupy distinct keys. */
|
|
43
17
|
sessionId: string;
|
|
44
18
|
parentSessionId?: string;
|
|
45
19
|
}
|
|
46
20
|
|
|
47
|
-
/** Payload for `subagents:child:completed`. */
|
|
48
21
|
export interface ChildCompletedEvent {
|
|
49
22
|
sessionDir: string;
|
|
50
23
|
agentName: string;
|
|
51
|
-
|
|
52
|
-
aborted: boolean;
|
|
53
|
-
/** True if the run was steered to wrap up (soft turn limit) but finished. */
|
|
54
|
-
steered: boolean;
|
|
24
|
+
terminalReason: SubagentLifecycleOutcome | "lifecycle_abort";
|
|
55
25
|
}
|
|
56
26
|
|
|
57
|
-
/** Payload for `subagents:child:disposed`. */
|
|
58
27
|
export interface ChildDisposedEvent {
|
|
59
|
-
/** Child session id — the registry key. Must match `session-created`. */
|
|
60
28
|
sessionId: string;
|
|
61
29
|
}
|
|
62
30
|
|
|
63
|
-
/** Narrow emit seam — injected, never imports the Pi SDK. */
|
|
64
31
|
export type LifecycleEmit = (channel: string, data: unknown) => void;
|
|
65
32
|
|
|
66
|
-
/** Publishes the child-execution lifecycle on the event bus. */
|
|
67
33
|
export interface ChildLifecyclePublisher {
|
|
68
34
|
spawning(event: ChildSpawningEvent): void;
|
|
69
35
|
sessionCreated(event: ChildSessionCreatedEvent): void;
|
|
@@ -71,38 +37,19 @@ export interface ChildLifecyclePublisher {
|
|
|
71
37
|
disposed(event: ChildDisposedEvent): void;
|
|
72
38
|
}
|
|
73
39
|
|
|
74
|
-
|
|
75
|
-
export function createChildLifecyclePublisher(
|
|
76
|
-
emit: LifecycleEmit,
|
|
77
|
-
): ChildLifecyclePublisher {
|
|
40
|
+
export function createChildLifecyclePublisher(emit: LifecycleEmit): ChildLifecyclePublisher {
|
|
78
41
|
return {
|
|
79
|
-
spawning(event)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
publish(emit, "child lifecycle session-created", SUBAGENT_CHILD_SESSION_CREATED, event);
|
|
84
|
-
},
|
|
85
|
-
completed(event) {
|
|
86
|
-
publish(emit, "child lifecycle completed", SUBAGENT_CHILD_COMPLETED, event);
|
|
87
|
-
},
|
|
88
|
-
disposed(event) {
|
|
89
|
-
publish(emit, "child lifecycle disposed", SUBAGENT_CHILD_DISPOSED, event);
|
|
90
|
-
},
|
|
42
|
+
spawning: (event) => publish(emit, "child lifecycle spawning", SUBAGENT_CHILD_SPAWNING, event),
|
|
43
|
+
sessionCreated: (event) => publish(emit, "child lifecycle session-created", SUBAGENT_CHILD_SESSION_CREATED, event),
|
|
44
|
+
completed: (event) => publish(emit, "child lifecycle completed", SUBAGENT_CHILD_COMPLETED, event),
|
|
45
|
+
disposed: (event) => publish(emit, "child lifecycle disposed", SUBAGENT_CHILD_DISPOSED, event),
|
|
91
46
|
};
|
|
92
47
|
}
|
|
93
48
|
|
|
94
|
-
|
|
95
|
-
function publish(
|
|
96
|
-
emit: LifecycleEmit,
|
|
97
|
-
channelContext: string,
|
|
98
|
-
channel: string,
|
|
99
|
-
data: unknown,
|
|
100
|
-
): void {
|
|
49
|
+
function publish(emit: LifecycleEmit, context: string, channel: string, data: unknown): void {
|
|
101
50
|
try {
|
|
102
|
-
// Pi's event bus is synchronous for child lifecycle hooks by design.
|
|
103
|
-
// Keep that ordering while preventing a subscriber from escaping into Pi.
|
|
104
51
|
emit(channel, data);
|
|
105
52
|
} catch (error) {
|
|
106
|
-
debugLog(
|
|
53
|
+
debugLog(context, error);
|
|
107
54
|
}
|
|
108
55
|
}
|
|
@@ -1,84 +1,130 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* FIFO admission for every child run.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Every scheduled promise settles: it follows the task's settlement when the
|
|
9
|
-
* task runs, or resolves early if clear() drops it before it starts.
|
|
4
|
+
* The limiter deliberately knows nothing about subagents. A queued handle can
|
|
5
|
+
* be cancelled by its owner, which removes that exact entry rather than
|
|
6
|
+
* leaving a tombstone for a later capacity change.
|
|
10
7
|
*/
|
|
11
8
|
|
|
12
9
|
import { debugLog } from "#src/debug";
|
|
13
10
|
|
|
11
|
+
export interface AdmissionHandle {
|
|
12
|
+
readonly promise: Promise<void>;
|
|
13
|
+
readonly admitted: boolean;
|
|
14
|
+
cancel(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PendingEntry {
|
|
18
|
+
readonly task: () => Promise<void>;
|
|
19
|
+
readonly resolve: () => void;
|
|
20
|
+
readonly reject: (error: unknown) => void;
|
|
21
|
+
handle: AdmissionHandleImpl;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class AdmissionHandleImpl implements AdmissionHandle {
|
|
25
|
+
admitted = false;
|
|
26
|
+
settled = false;
|
|
27
|
+
private cancelled = false;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
readonly promise: Promise<void>,
|
|
31
|
+
private readonly cancelPending: () => void,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
cancel(): void {
|
|
35
|
+
if (this.cancelled || this.admitted || this.settled) return;
|
|
36
|
+
this.cancelled = true;
|
|
37
|
+
this.cancelPending();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
markAdmitted(): void {
|
|
41
|
+
this.admitted = true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
markSettled(): void {
|
|
45
|
+
this.settled = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get isCancelled(): boolean {
|
|
49
|
+
return this.cancelled;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
14
53
|
export class ConcurrencyLimiter {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
54
|
+
private active = 0;
|
|
55
|
+
private readonly pending: PendingEntry[] = [];
|
|
56
|
+
|
|
57
|
+
constructor(private readonly getLimit: () => number) {}
|
|
58
|
+
|
|
59
|
+
isSaturated(): boolean {
|
|
60
|
+
try {
|
|
61
|
+
return this.active >= this.getLimit();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
debugLog("concurrency limiter limit", error);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get activeCount(): number {
|
|
69
|
+
return this.active;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get queuedCount(): number {
|
|
73
|
+
return this.pending.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
schedule(task: () => Promise<void>): AdmissionHandle {
|
|
77
|
+
let entry!: PendingEntry;
|
|
78
|
+
let resolve!: () => void;
|
|
79
|
+
let reject!: (error: unknown) => void;
|
|
80
|
+
const promise = new Promise<void>((res, rej) => {
|
|
81
|
+
resolve = res;
|
|
82
|
+
reject = rej;
|
|
83
|
+
});
|
|
84
|
+
const handle = new AdmissionHandleImpl(promise, () => {
|
|
85
|
+
const index = this.pending.indexOf(entry);
|
|
86
|
+
if (index === -1) return;
|
|
87
|
+
this.pending.splice(index, 1);
|
|
88
|
+
handle.markSettled();
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
entry = { task, resolve, reject, handle };
|
|
92
|
+
this.pending.push(entry);
|
|
93
|
+
this.recheck();
|
|
94
|
+
return handle;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
recheck(): void {
|
|
98
|
+
try {
|
|
99
|
+
while (this.active < this.getLimit()) {
|
|
100
|
+
const next = this.pending.shift();
|
|
101
|
+
if (!next) return;
|
|
102
|
+
if (next.handle.isCancelled) continue;
|
|
103
|
+
next.handle.markAdmitted();
|
|
104
|
+
this.active++;
|
|
105
|
+
let taskPromise: Promise<void>;
|
|
106
|
+
try {
|
|
107
|
+
taskPromise = Promise.resolve(next.task());
|
|
108
|
+
} catch (error) {
|
|
109
|
+
taskPromise = Promise.reject(error);
|
|
110
|
+
}
|
|
111
|
+
taskPromise.then(next.resolve, next.reject).finally(() => {
|
|
112
|
+
next.handle.markSettled();
|
|
113
|
+
this.active--;
|
|
114
|
+
try {
|
|
115
|
+
this.recheck();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
debugLog("concurrency limiter recheck", error);
|
|
118
|
+
}
|
|
119
|
+
}).catch((error: unknown) => {
|
|
120
|
+
// The task promise was already forwarded to the caller. This catch
|
|
121
|
+
// only contains a rejected `finally` continuation.
|
|
122
|
+
debugLog("concurrency limiter cleanup", error);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
debugLog("concurrency limiter recheck", error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
84
130
|
}
|
|
@@ -24,9 +24,7 @@ import { SubagentSession } from "#src/lifecycle/subagent-session";
|
|
|
24
24
|
import type { EnvInfo } from "#src/session/env";
|
|
25
25
|
import { type AssemblerIO, assembleSessionConfig } from "#src/session/session-config";
|
|
26
26
|
import type { ParentSessionInfo, ShellExec, SubagentType, ThinkingLevel } from "#src/types";
|
|
27
|
-
|
|
28
|
-
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
29
|
-
const EXCLUDED_TOOL_NAMES = ["subagent", "get_subagent_result", "steer_subagent"];
|
|
27
|
+
import { PARENT_ONLY_TOOL_NAMES, PARENT_ONLY_TOOL_SET } from "#src/tools/parent-tool-registry";
|
|
30
28
|
|
|
31
29
|
/**
|
|
32
30
|
* Apply the recursion guard: remove this extension's dispatch tools from the
|
|
@@ -36,7 +34,7 @@ const EXCLUDED_TOOL_NAMES = ["subagent", "get_subagent_result", "steer_subagent"
|
|
|
36
34
|
function applyRecursionGuard(session: AgentSession): void {
|
|
37
35
|
const filtered = session
|
|
38
36
|
.getActiveToolNames()
|
|
39
|
-
.filter((t) => !
|
|
37
|
+
.filter((t) => !PARENT_ONLY_TOOL_SET.has(t));
|
|
40
38
|
session.setActiveToolsByName(filtered);
|
|
41
39
|
}
|
|
42
40
|
|
|
@@ -212,7 +210,7 @@ export async function createSubagentSession(
|
|
|
212
210
|
modelRegistry: snapshot.modelRegistry,
|
|
213
211
|
modelRuntime: snapshot.modelRuntime,
|
|
214
212
|
model: cfg.model,
|
|
215
|
-
excludeTools: [...new Set([...cfg.excludedBuiltinToolNames, ...
|
|
213
|
+
excludeTools: [...new Set([...cfg.excludedBuiltinToolNames, ...PARENT_ONLY_TOOL_NAMES])],
|
|
216
214
|
resourceLoader: loader,
|
|
217
215
|
thinkingLevel: cfg.thinkingLevel,
|
|
218
216
|
});
|
|
@@ -8,21 +8,22 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { debugLog } from "#src/debug";
|
|
11
|
+
import type { SubagentMode } from "#src/types";
|
|
11
12
|
|
|
12
13
|
/** The fixed bound prevents a provider from turning completion into an unbounded loop. */
|
|
13
14
|
export const MAX_LIFECYCLE_CONTINUATION_ROUNDS = 3;
|
|
14
15
|
|
|
15
16
|
export type SubagentExecutionPhase = "initial" | "resume";
|
|
16
17
|
export type SubagentExecutionOrigin = "tool" | "service";
|
|
17
|
-
export type SubagentExecutionMode =
|
|
18
|
+
export type SubagentExecutionMode = SubagentMode;
|
|
18
19
|
export type SubagentExecutionAdmission = "immediate" | "queued";
|
|
19
|
-
export type SubagentLifecycleOutcome = "completed" | "
|
|
20
|
+
export type SubagentLifecycleOutcome = "completed" | "turn_limit_graceful" | "turn_limit_hard";
|
|
20
21
|
|
|
21
22
|
/** Immutable identifiers for one initial or resumed execution attempt. */
|
|
22
23
|
export interface SubagentLifecycleIdentity {
|
|
23
24
|
readonly agentId: string;
|
|
24
25
|
readonly sessionId: string;
|
|
25
|
-
readonly runId:
|
|
26
|
+
readonly runId: number;
|
|
26
27
|
readonly agentType: string;
|
|
27
28
|
readonly parentSessionId?: string;
|
|
28
29
|
}
|
|
@@ -1,28 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* run-listeners.ts — Per-run observer-unsubscribe and signal-detach handles.
|
|
3
|
-
*
|
|
4
|
-
* Owns the two teardown handles that a Subagent wires at run start (signal
|
|
5
|
-
* listener) and after session creation (record-observer unsub), releasing
|
|
6
|
-
* both atomically when the run ends or the agent is resumed.
|
|
7
|
-
*/
|
|
1
|
+
/** Per-run observer-unsubscribe handle. */
|
|
8
2
|
|
|
9
3
|
import { runSafely } from "#src/debug";
|
|
10
4
|
|
|
11
|
-
/** Owns the per-run observer-unsubscribe
|
|
5
|
+
/** Owns the per-run observer-unsubscribe handle. */
|
|
12
6
|
export class RunListeners {
|
|
13
7
|
private unsub?: () => void;
|
|
14
|
-
private detach?: () => void;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Wire a parent AbortSignal so it triggers onAbort when fired.
|
|
18
|
-
* No-op when signal is undefined.
|
|
19
|
-
*/
|
|
20
|
-
wireSignal(signal: AbortSignal | undefined, onAbort: () => void): void {
|
|
21
|
-
if (!signal) return;
|
|
22
|
-
const listener = () => onAbort();
|
|
23
|
-
signal.addEventListener("abort", listener, { once: true });
|
|
24
|
-
this.detach = () => signal.removeEventListener("abort", listener);
|
|
25
|
-
}
|
|
26
8
|
|
|
27
9
|
/** Store the record-observer unsubscribe handle. */
|
|
28
10
|
attachObserver(unsub: () => void): void {
|
|
@@ -33,9 +15,6 @@ export class RunListeners {
|
|
|
33
15
|
release(): void {
|
|
34
16
|
const unsub = this.unsub;
|
|
35
17
|
this.unsub = undefined;
|
|
36
|
-
const detach = this.detach;
|
|
37
|
-
this.detach = undefined;
|
|
38
18
|
runSafely("subagent observer unsubscribe", () => unsub?.());
|
|
39
|
-
runSafely("subagent abort listener detach", () => detach?.());
|
|
40
19
|
}
|
|
41
20
|
}
|