@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,88 @@
|
|
|
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
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Emitted at the start of a child run, before the session is created. */
|
|
14
|
+
export const SUBAGENT_CHILD_SPAWNING = "subagents:child:spawning";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Emitted after the child session is created, immediately before
|
|
18
|
+
* `bindExtensions()`. Carries the child session id consumers need to register
|
|
19
|
+
* the session in `SubagentSessionRegistry`. Subscribers must register
|
|
20
|
+
* synchronously so the entry lands before binding proceeds (see ADR 0002 /
|
|
21
|
+
* the event-bus synchronous-dispatch guarantee).
|
|
22
|
+
*/
|
|
23
|
+
export const SUBAGENT_CHILD_SESSION_CREATED = "subagents:child:session-created";
|
|
24
|
+
|
|
25
|
+
/** Emitted after the child's prompt resolves (normal, steered, or aborted). */
|
|
26
|
+
export const SUBAGENT_CHILD_COMPLETED = "subagents:child:completed";
|
|
27
|
+
|
|
28
|
+
/** Emitted in the run's `finally` — always fires, on success and error. */
|
|
29
|
+
export const SUBAGENT_CHILD_DISPOSED = "subagents:child:disposed";
|
|
30
|
+
|
|
31
|
+
/** Payload for `subagents:child:spawning`. */
|
|
32
|
+
export interface ChildSpawningEvent {
|
|
33
|
+
agentName: string;
|
|
34
|
+
parentSessionId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Payload for `subagents:child:session-created`. */
|
|
38
|
+
export interface ChildSessionCreatedEvent {
|
|
39
|
+
/** Child session id — the registry key. Unique per child; concurrent
|
|
40
|
+
* siblings of the same parent occupy distinct keys. */
|
|
41
|
+
sessionId: string;
|
|
42
|
+
parentSessionId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Payload for `subagents:child:completed`. */
|
|
46
|
+
export interface ChildCompletedEvent {
|
|
47
|
+
sessionDir: string;
|
|
48
|
+
agentName: string;
|
|
49
|
+
/** True if the run was hard-aborted (max turns + grace exceeded). */
|
|
50
|
+
aborted: boolean;
|
|
51
|
+
/** True if the run was steered to wrap up (soft turn limit) but finished. */
|
|
52
|
+
steered: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Payload for `subagents:child:disposed`. */
|
|
56
|
+
export interface ChildDisposedEvent {
|
|
57
|
+
/** Child session id — the registry key. Must match `session-created`. */
|
|
58
|
+
sessionId: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Narrow emit seam — injected, never imports the Pi SDK. */
|
|
62
|
+
export type LifecycleEmit = (channel: string, data: unknown) => void;
|
|
63
|
+
|
|
64
|
+
/** Publishes the child-execution lifecycle on the event bus. */
|
|
65
|
+
export interface ChildLifecyclePublisher {
|
|
66
|
+
spawning(event: ChildSpawningEvent): void;
|
|
67
|
+
sessionCreated(event: ChildSessionCreatedEvent): void;
|
|
68
|
+
completed(event: ChildCompletedEvent): void;
|
|
69
|
+
disposed(event: ChildDisposedEvent): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Build a publisher backed by an injected `emit` callback. */
|
|
73
|
+
export function createChildLifecyclePublisher(emit: LifecycleEmit): ChildLifecyclePublisher {
|
|
74
|
+
return {
|
|
75
|
+
spawning(event) {
|
|
76
|
+
emit(SUBAGENT_CHILD_SPAWNING, event);
|
|
77
|
+
},
|
|
78
|
+
sessionCreated(event) {
|
|
79
|
+
emit(SUBAGENT_CHILD_SESSION_CREATED, event);
|
|
80
|
+
},
|
|
81
|
+
completed(event) {
|
|
82
|
+
emit(SUBAGENT_CHILD_COMPLETED, event);
|
|
83
|
+
},
|
|
84
|
+
disposed(event) {
|
|
85
|
+
emit(SUBAGENT_CHILD_DISPOSED, event);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* concurrency-limiter.ts — FIFO admission gate for background work.
|
|
3
|
+
*
|
|
4
|
+
* Schedules run closures (thunks) against a dynamic limit, running them in
|
|
5
|
+
* scheduling order as slots free. The limiter knows nothing about agents, IDs,
|
|
6
|
+
* or the manager — it owns only the active count and the pending queue.
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class ConcurrencyLimiter {
|
|
13
|
+
private active = 0;
|
|
14
|
+
private readonly pending: Array<{ start: () => void; settle: () => void }> = [];
|
|
15
|
+
|
|
16
|
+
constructor(private readonly getLimit: () => number) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Schedule a task to run FIFO once a slot is free.
|
|
20
|
+
* Returns a promise that settles with the task, or resolves early if the
|
|
21
|
+
* task is dropped by clear() before it starts.
|
|
22
|
+
*/
|
|
23
|
+
schedule(task: () => Promise<void>): Promise<void> {
|
|
24
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>(); // eslint-disable-line @typescript-eslint/no-invalid-void-type -- Promise.withResolvers<void> is valid; rule does not allow void in generic fn call type args
|
|
25
|
+
this.pending.push({
|
|
26
|
+
start: () => {
|
|
27
|
+
this.active++;
|
|
28
|
+
task()
|
|
29
|
+
.then(resolve, reject)
|
|
30
|
+
.finally(() => {
|
|
31
|
+
this.active--;
|
|
32
|
+
this.recheck();
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
settle: resolve,
|
|
36
|
+
});
|
|
37
|
+
this.recheck();
|
|
38
|
+
return promise;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Start pending tasks until the limit is reached. Call when the limit may have grown. */
|
|
42
|
+
recheck(): void {
|
|
43
|
+
while (this.active < this.getLimit()) {
|
|
44
|
+
const next = this.pending.shift();
|
|
45
|
+
if (!next) break;
|
|
46
|
+
next.start();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Drop all pending tasks, resolving their promises without running them. */
|
|
51
|
+
clear(): void {
|
|
52
|
+
const dropped = this.pending.splice(0);
|
|
53
|
+
for (const task of dropped) task.settle();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create-subagent-session.ts — Assembly factory for born-complete child sessions (issue #265).
|
|
3
|
+
*
|
|
4
|
+
* `createSubagentSession()` does the assembly portion that the old runner's
|
|
5
|
+
* `runAgent()` did up front: detect the environment, assemble the session config,
|
|
6
|
+
* create the SDK session, publish `spawning`/`session-created`, bind extensions,
|
|
7
|
+
* and apply the recursion guard. It returns a fully usable `SubagentSession` —
|
|
8
|
+
* `Subagent` then only coordinates (turn loop, steer, dispose).
|
|
9
|
+
*
|
|
10
|
+
* The factory takes a resolved `cwd` value, never the WorkspaceProvider: `cwd`
|
|
11
|
+
* is a value the factory consumes directly (detectEnv, assembleSessionConfig,
|
|
12
|
+
* createSession), so threading the provider through here would be a relay smell.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
16
|
+
import type { AgentSession, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import type { AgentConfigLookup } from "../config/agent-types";
|
|
18
|
+
import type { ChildLifecyclePublisher } from "../lifecycle/child-lifecycle";
|
|
19
|
+
import type { ParentSnapshot } from "../lifecycle/parent-snapshot";
|
|
20
|
+
import { SubagentSession } from "../lifecycle/subagent-session";
|
|
21
|
+
import type { EnvInfo } from "../session/env";
|
|
22
|
+
import { type AssemblerIO, assembleSessionConfig } from "../session/session-config";
|
|
23
|
+
import type { ParentSessionInfo, ShellExec, SubagentType, ThinkingLevel } from "../types";
|
|
24
|
+
|
|
25
|
+
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
26
|
+
const EXCLUDED_TOOL_NAMES = ["subagent", "get_subagent_result", "steer_subagent"];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Apply the recursion guard: remove this extension's dispatch tools from the
|
|
30
|
+
* child's active set. Runs after `bindExtensions` so extension-registered tools
|
|
31
|
+
* are also covered. Unconditional: children always load the parent's extensions.
|
|
32
|
+
*/
|
|
33
|
+
function applyRecursionGuard(session: AgentSession): void {
|
|
34
|
+
const filtered = session.getActiveToolNames().filter((t) => !EXCLUDED_TOOL_NAMES.includes(t));
|
|
35
|
+
session.setActiveToolsByName(filtered);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── IO boundary ───────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/** Minimal resource-loader contract used by the factory. */
|
|
41
|
+
export interface ResourceLoaderLike {
|
|
42
|
+
reload(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Minimal session-manager contract used by the factory. */
|
|
46
|
+
export interface SessionManagerLike {
|
|
47
|
+
newSession(opts: { parentSession?: string }): void;
|
|
48
|
+
getSessionFile(): string | undefined;
|
|
49
|
+
getSessionId(): string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Options passed to EnvironmentIO/SessionFactoryIO methods. */
|
|
53
|
+
export interface ResourceLoaderOptions {
|
|
54
|
+
cwd: string;
|
|
55
|
+
agentDir: string;
|
|
56
|
+
noPromptTemplates?: boolean;
|
|
57
|
+
noThemes?: boolean;
|
|
58
|
+
noContextFiles?: boolean;
|
|
59
|
+
systemPromptOverride?: () => string;
|
|
60
|
+
/** Override the append system prompt. Receives the current base value; return the replacement. */
|
|
61
|
+
appendSystemPromptOverride?: (base: string[]) => string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Options passed to SessionFactoryIO.createSession. */
|
|
65
|
+
export interface CreateSessionOptions {
|
|
66
|
+
cwd: string;
|
|
67
|
+
agentDir: string;
|
|
68
|
+
sessionManager: SessionManagerLike;
|
|
69
|
+
settingsManager: SettingsManager;
|
|
70
|
+
modelRegistry: unknown;
|
|
71
|
+
model?: unknown;
|
|
72
|
+
tools: string[];
|
|
73
|
+
resourceLoader: ResourceLoaderLike;
|
|
74
|
+
thinkingLevel?: ThinkingLevel;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Environment discovery - detect runtime context and resolve directories.
|
|
79
|
+
*
|
|
80
|
+
* Decouples the factory from direct process/SDK reads so each can be stubbed
|
|
81
|
+
* independently in tests.
|
|
82
|
+
*/
|
|
83
|
+
export interface EnvironmentIO {
|
|
84
|
+
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
85
|
+
getAgentDir: () => string;
|
|
86
|
+
deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Session factory - create SDK objects for a child agent session.
|
|
91
|
+
*
|
|
92
|
+
* Decouples the factory from direct Pi SDK imports and sibling-module IO,
|
|
93
|
+
* making it testable via plain stub objects without vi.mock().
|
|
94
|
+
*/
|
|
95
|
+
export interface SessionFactoryIO {
|
|
96
|
+
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
|
|
97
|
+
createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
|
|
98
|
+
createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
|
|
99
|
+
createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
|
|
100
|
+
assemblerIO: AssemblerIO;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* IO boundary injected into createSubagentSession().
|
|
105
|
+
*
|
|
106
|
+
* Intersection of EnvironmentIO and SessionFactoryIO — callers satisfy both
|
|
107
|
+
* sub-interfaces via TypeScript's structural typing.
|
|
108
|
+
*/
|
|
109
|
+
export type SubagentSessionIO = EnvironmentIO & SessionFactoryIO;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Dependencies injected at construction time — the IO boundary plus the two
|
|
113
|
+
* static domain deps (exec, registry) every creation needs.
|
|
114
|
+
*/
|
|
115
|
+
export interface SubagentSessionDeps {
|
|
116
|
+
io: SubagentSessionIO;
|
|
117
|
+
exec: ShellExec;
|
|
118
|
+
registry: AgentConfigLookup;
|
|
119
|
+
/** Publishes the child-execution lifecycle so consumers can observe it. */
|
|
120
|
+
lifecycle: ChildLifecyclePublisher;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Per-spawn parameters — the fields that vary per child session. */
|
|
124
|
+
export interface CreateSubagentSessionParams {
|
|
125
|
+
snapshot: ParentSnapshot;
|
|
126
|
+
type: SubagentType;
|
|
127
|
+
/** Resolved workspace cwd; undefined → parent cwd. */
|
|
128
|
+
cwd?: string;
|
|
129
|
+
/** Parent session identity (file path + session ID). */
|
|
130
|
+
parentSession?: ParentSessionInfo;
|
|
131
|
+
model?: Model<any>;
|
|
132
|
+
thinkingLevel?: ThinkingLevel;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build a born-complete SubagentSession: assemble config, create the SDK
|
|
137
|
+
* session, publish lifecycle events, bind extensions, apply the recursion guard.
|
|
138
|
+
*/
|
|
139
|
+
export async function createSubagentSession(
|
|
140
|
+
params: CreateSubagentSessionParams,
|
|
141
|
+
deps: SubagentSessionDeps,
|
|
142
|
+
): Promise<SubagentSession> {
|
|
143
|
+
const { snapshot, type } = params;
|
|
144
|
+
const parentSessionId = params.parentSession?.parentSessionId;
|
|
145
|
+
deps.lifecycle.spawning({ agentName: type, parentSessionId });
|
|
146
|
+
|
|
147
|
+
// Resolve working directory upfront - needed for detectEnv before assembly.
|
|
148
|
+
const effectiveCwd = params.cwd ?? snapshot.cwd;
|
|
149
|
+
const env = await deps.io.detectEnv(deps.exec, effectiveCwd);
|
|
150
|
+
|
|
151
|
+
// Assemble session configuration (synchronous, no SDK objects).
|
|
152
|
+
const cfg = assembleSessionConfig(
|
|
153
|
+
type,
|
|
154
|
+
{
|
|
155
|
+
cwd: snapshot.cwd,
|
|
156
|
+
parentSystemPrompt: snapshot.systemPrompt,
|
|
157
|
+
parentModel: snapshot.model,
|
|
158
|
+
modelRegistry: snapshot.modelRegistry,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
cwd: params.cwd,
|
|
162
|
+
model: params.model,
|
|
163
|
+
thinkingLevel: params.thinkingLevel,
|
|
164
|
+
},
|
|
165
|
+
env,
|
|
166
|
+
deps.registry,
|
|
167
|
+
deps.io.assemblerIO,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const agentDir = deps.io.getAgentDir();
|
|
171
|
+
|
|
172
|
+
// Children always load the parent's extensions and skills.
|
|
173
|
+
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md - upstream's
|
|
174
|
+
// buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
|
|
175
|
+
// would defeat prompt_mode: replace. Parent context, if wanted, reaches the
|
|
176
|
+
// subagent via prompt_mode: append (parentSystemPrompt is embedded in
|
|
177
|
+
// systemPromptOverride) or inherit_context (conversation).
|
|
178
|
+
const loader = deps.io.createResourceLoader({
|
|
179
|
+
cwd: cfg.effectiveCwd,
|
|
180
|
+
agentDir,
|
|
181
|
+
noPromptTemplates: true,
|
|
182
|
+
noThemes: true,
|
|
183
|
+
noContextFiles: true,
|
|
184
|
+
systemPromptOverride: () => cfg.systemPrompt,
|
|
185
|
+
appendSystemPromptOverride: () => [],
|
|
186
|
+
});
|
|
187
|
+
await loader.reload();
|
|
188
|
+
|
|
189
|
+
// Create a persisted SessionManager so transcripts are written in Pi's
|
|
190
|
+
// official JSONL format. Falls back to a temp directory when the parent
|
|
191
|
+
// session is not persisted (e.g. headless/API mode).
|
|
192
|
+
const sessionDir = deps.io.deriveSessionDir(params.parentSession?.parentSessionFile, cfg.effectiveCwd);
|
|
193
|
+
const sessionManager = deps.io.createSessionManager(cfg.effectiveCwd, sessionDir);
|
|
194
|
+
sessionManager.newSession({ parentSession: params.parentSession?.parentSessionId });
|
|
195
|
+
const sessionId = sessionManager.getSessionId();
|
|
196
|
+
|
|
197
|
+
const { session } = await deps.io.createSession({
|
|
198
|
+
cwd: cfg.effectiveCwd,
|
|
199
|
+
agentDir,
|
|
200
|
+
sessionManager,
|
|
201
|
+
settingsManager: deps.io.createSettingsManager(cfg.effectiveCwd, agentDir),
|
|
202
|
+
modelRegistry: snapshot.modelRegistry,
|
|
203
|
+
model: cfg.model,
|
|
204
|
+
tools: cfg.toolNames,
|
|
205
|
+
resourceLoader: loader,
|
|
206
|
+
thinkingLevel: cfg.thinkingLevel,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const subagentSession = new SubagentSession(session, {
|
|
210
|
+
outputFile: sessionManager.getSessionFile(),
|
|
211
|
+
sessionId,
|
|
212
|
+
sessionDir,
|
|
213
|
+
agentName: type,
|
|
214
|
+
agentMaxTurns: cfg.agentMaxTurns,
|
|
215
|
+
parentContext: snapshot.parentContext,
|
|
216
|
+
lifecycle: deps.lifecycle,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Publish session-created before bindExtensions() so observers (e.g. the
|
|
220
|
+
// permission system) can register the child synchronously and have their
|
|
221
|
+
// entry in place for the first permission check during child extension
|
|
222
|
+
// initialization. The event bus dispatches synchronously, so a synchronous
|
|
223
|
+
// subscriber completes before this returns.
|
|
224
|
+
deps.lifecycle.sessionCreated({ sessionId, parentSessionId });
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Bind extensions so that session_start fires and extensions can initialize.
|
|
228
|
+
await session.bindExtensions({});
|
|
229
|
+
// Apply recursion guard after bindExtensions so extension-registered tools
|
|
230
|
+
// are included in the post-bind active set.
|
|
231
|
+
applyRecursionGuard(session);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
// Binding failed after session-created — dispose (emit disposed +
|
|
234
|
+
// session.dispose()) before rethrowing so registration is never leaked.
|
|
235
|
+
subagentSession.dispose();
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return subagentSession;
|
|
240
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parent-snapshot.ts — Capture parent session state as a plain data snapshot.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { buildParentContext } from "../session/context";
|
|
6
|
+
import type { SessionContext } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plain data snapshot of the parent session state captured at spawn time.
|
|
10
|
+
* Replaces live `ExtensionContext` references so queued agents don't read stale state.
|
|
11
|
+
*/
|
|
12
|
+
export interface ParentSnapshot {
|
|
13
|
+
/** Parent working directory. */
|
|
14
|
+
cwd: string;
|
|
15
|
+
/** Parent's effective system prompt (for append-mode agents). */
|
|
16
|
+
systemPrompt: string;
|
|
17
|
+
/** Parent's current model instance (fallback when agent config has no model). */
|
|
18
|
+
model: unknown;
|
|
19
|
+
/** Model registry for resolving config.model strings and creating sessions. */
|
|
20
|
+
modelRegistry: {
|
|
21
|
+
find(provider: string, modelId: string): unknown;
|
|
22
|
+
getAvailable?(): Array<{ provider: string; id: string }>;
|
|
23
|
+
};
|
|
24
|
+
/** Pre-built parent conversation text (when inheritContext was requested). */
|
|
25
|
+
parentContext?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build an immutable snapshot of the parent session state.
|
|
30
|
+
*
|
|
31
|
+
* Called once at spawn time so queued agents capture state as it existed
|
|
32
|
+
* when the user requested the agent, not when a queue slot opens.
|
|
33
|
+
*/
|
|
34
|
+
export function buildParentSnapshot(ctx: SessionContext, inheritContext?: boolean): ParentSnapshot {
|
|
35
|
+
const parentContext = inheritContext ? buildParentContext(ctx) : undefined;
|
|
36
|
+
return {
|
|
37
|
+
cwd: ctx.cwd,
|
|
38
|
+
systemPrompt: ctx.getSystemPrompt(),
|
|
39
|
+
model: ctx.model,
|
|
40
|
+
|
|
41
|
+
modelRegistry: ctx.modelRegistry!,
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: converts empty string to undefined as well as null/undefined
|
|
43
|
+
parentContext: parentContext || undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Owns the per-run observer-unsubscribe and signal-detach handles. */
|
|
10
|
+
export class RunListeners {
|
|
11
|
+
private unsub?: () => void;
|
|
12
|
+
private detach?: () => void;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Wire a parent AbortSignal so it triggers onAbort when fired.
|
|
16
|
+
* No-op when signal is undefined.
|
|
17
|
+
*/
|
|
18
|
+
wireSignal(signal: AbortSignal | undefined, onAbort: () => void): void {
|
|
19
|
+
if (!signal) return;
|
|
20
|
+
const listener = () => onAbort();
|
|
21
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
22
|
+
this.detach = () => signal.removeEventListener("abort", listener);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Store the record-observer unsubscribe handle. */
|
|
26
|
+
attachObserver(unsub: () => void): void {
|
|
27
|
+
this.unsub = unsub;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Release the observer + signal handles. Idempotent. */
|
|
31
|
+
release(): void {
|
|
32
|
+
this.unsub?.();
|
|
33
|
+
this.unsub = undefined;
|
|
34
|
+
this.detach?.();
|
|
35
|
+
this.detach = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|