@nklisch/pi-enhanced 0.2.6 → 0.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 +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 +25 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +75 -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 +112 -105
- 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 -223
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent.ts +569 -683
- 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 -119
- 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 +327 -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 -92
- 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 +330 -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
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* subagent-session.ts — The born-complete child-session value object (issue #265).
|
|
3
|
-
*
|
|
4
|
-
* A SubagentSession wraps one SDK AgentSession plus its turn-driving and teardown.
|
|
5
|
-
* It is born complete: `createSubagentSession()` returns a fully usable instance
|
|
6
|
-
* (session created, extensions bound, recursion guard applied), so the only thing
|
|
7
|
-
* left for `Subagent` to do is coordinate — drive the turn loop, steer, dispose.
|
|
8
|
-
*
|
|
9
|
-
* Turn driving lives here, on the object that owns the AgentSession, rather than
|
|
10
|
-
* reaching through `subagentSession.session` from `Subagent` (Law of Demeter).
|
|
11
|
-
*/
|
|
1
|
+
/** Born-complete child session: turn driving, steering, and teardown. */
|
|
12
2
|
|
|
13
3
|
import {
|
|
14
4
|
type AgentSession,
|
|
@@ -18,9 +8,9 @@ import {
|
|
|
18
8
|
import type { Model } from "@earendil-works/pi-ai";
|
|
19
9
|
import { debugLog, runDetached, runSafely } from "#src/debug";
|
|
20
10
|
import type { ChildLifecyclePublisher } from "#src/lifecycle/child-lifecycle";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
import type {
|
|
12
|
+
SubagentLifecycleOutcome,
|
|
13
|
+
SubagentTurnLifecycle,
|
|
24
14
|
} from "#src/lifecycle/lifecycle-interceptor";
|
|
25
15
|
import { normalizeMaxTurns } from "#src/lifecycle/turn-limits";
|
|
26
16
|
import { getSessionContextPercent, type SessionStatsLike } from "#src/lifecycle/usage";
|
|
@@ -28,51 +18,34 @@ import { extractText } from "#src/session/context";
|
|
|
28
18
|
import { getAgentConversation } from "#src/session/conversation";
|
|
29
19
|
import type { SessionMessage, ThinkingLevel } from "#src/types";
|
|
30
20
|
|
|
31
|
-
|
|
21
|
+
export type TurnLimitReason = "turn_limit_graceful" | "turn_limit_hard";
|
|
22
|
+
|
|
32
23
|
export interface TurnLoopResult {
|
|
33
24
|
responseText: string;
|
|
34
|
-
/**
|
|
25
|
+
/** Provider failure reported by a normally-resolved assistant turn. */
|
|
35
26
|
failure?: string;
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
/** True if the agent was steered to wrap up (soft turn limit) but finished in time. */
|
|
39
|
-
steered: boolean;
|
|
40
|
-
/** A lifecycle provider denied finalization; no child completion is emitted. */
|
|
41
|
-
lifecycleAborted?: boolean;
|
|
27
|
+
/** Turn-limit or lifecycle result owned by the session loop. */
|
|
28
|
+
terminalReason?: TurnLimitReason | "lifecycle_abort";
|
|
42
29
|
}
|
|
43
30
|
|
|
44
|
-
/** Per-call options for the initial run's turn loop. */
|
|
45
31
|
export interface TurnLoopOptions {
|
|
46
|
-
/** Per-call max-turns override — highest precedence. */
|
|
47
32
|
maxTurns?: number;
|
|
48
|
-
/** Runtime-config fallback when neither per-call nor per-agent limit is set. */
|
|
49
33
|
defaultMaxTurns?: number;
|
|
50
|
-
/** Grace turns after the soft-limit steer message before a hard abort. */
|
|
51
34
|
graceTurns?: number;
|
|
52
35
|
signal?: AbortSignal;
|
|
53
|
-
/** Present only for an execution that captured active lifecycle providers. */
|
|
54
36
|
lifecycle?: SubagentTurnLifecycle;
|
|
55
37
|
}
|
|
56
38
|
|
|
57
|
-
/** Session-level facts known at creation, supplied by the factory. */
|
|
58
39
|
export interface SubagentSessionMeta {
|
|
59
|
-
/** Path to the persisted session JSONL file, if the session was persisted. */
|
|
60
40
|
outputFile: string | undefined;
|
|
61
|
-
/** Child session id — the registry key carried on session-created/disposed events. */
|
|
62
41
|
sessionId: string;
|
|
63
|
-
/** Child session directory — carried on the completed event as transcript location. */
|
|
64
42
|
sessionDir: string;
|
|
65
43
|
agentName: string;
|
|
66
|
-
/** Per-agent max-turns from the resolved agent config — middle precedence. */
|
|
67
44
|
agentMaxTurns: number | undefined;
|
|
68
|
-
/** Parent context prepended to the run prompt, captured at spawn time. */
|
|
69
45
|
parentContext: string | undefined;
|
|
70
46
|
lifecycle: ChildLifecyclePublisher;
|
|
71
47
|
}
|
|
72
48
|
|
|
73
|
-
/**
|
|
74
|
-
* One child AgentSession plus its turn-driving and teardown — born complete.
|
|
75
|
-
*/
|
|
76
49
|
export class SubagentSession {
|
|
77
50
|
private disposalPromise?: Promise<void>;
|
|
78
51
|
|
|
@@ -81,62 +54,32 @@ export class SubagentSession {
|
|
|
81
54
|
private readonly meta: SubagentSessionMeta,
|
|
82
55
|
) {}
|
|
83
56
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
get
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
get outputFile(): string | undefined {
|
|
93
|
-
return this.meta.outputFile;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Stable child session identity for lifecycle provider metadata. */
|
|
97
|
-
get sessionId(): string {
|
|
98
|
-
return this.meta.sessionId;
|
|
99
|
-
}
|
|
57
|
+
/** @internal lifecycle-only access to SDK teardown hooks. */
|
|
58
|
+
get session(): AgentSession { return this._session; }
|
|
59
|
+
get outputFile(): string | undefined { return this.meta.outputFile; }
|
|
60
|
+
get sessionId(): string { return this.meta.sessionId; }
|
|
61
|
+
get model(): Model<any> | undefined { return this._session.model; }
|
|
62
|
+
get thinkingLevel(): ThinkingLevel { return this._session.thinkingLevel; }
|
|
100
63
|
|
|
101
|
-
/** Current model selected by the child session. */
|
|
102
|
-
get model(): Model<any> | undefined {
|
|
103
|
-
return this._session.model;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Effective level after the child session applied defaults and clamping. */
|
|
107
|
-
get thinkingLevel(): ThinkingLevel {
|
|
108
|
-
return this._session.thinkingLevel;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Drive the initial run's turn loop; emits `completed` on accepted success. */
|
|
112
64
|
async runTurnLoop(prompt: string, opts: TurnLoopOptions): Promise<TurnLoopResult> {
|
|
113
65
|
const session = this._session;
|
|
114
|
-
|
|
115
|
-
// Track turns for graceful max_turns enforcement.
|
|
116
66
|
let turnCount = 0;
|
|
117
|
-
const maxTurns = normalizeMaxTurns(
|
|
118
|
-
opts.maxTurns ?? this.meta.agentMaxTurns ?? opts.defaultMaxTurns,
|
|
119
|
-
);
|
|
67
|
+
const maxTurns = normalizeMaxTurns(opts.maxTurns ?? this.meta.agentMaxTurns ?? opts.defaultMaxTurns);
|
|
120
68
|
let softLimitReached = false;
|
|
121
|
-
let
|
|
69
|
+
let turnLimitReason: TurnLimitReason | undefined;
|
|
122
70
|
|
|
123
71
|
const unsubTurns = session.subscribe((event: AgentSessionEvent) => {
|
|
124
72
|
runSafely("subagent turn-limit observer", () => {
|
|
125
|
-
if (event.type
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
} else if (softLimitReached && turnCount >= maxTurns + (opts.graceTurns ?? 5)) {
|
|
136
|
-
aborted = true;
|
|
137
|
-
runDetached("turn-limit abort", () => session.abort());
|
|
138
|
-
}
|
|
139
|
-
}
|
|
73
|
+
if (event.type !== "turn_end" || maxTurns == null) return;
|
|
74
|
+
turnCount++;
|
|
75
|
+
if (!softLimitReached && turnCount >= maxTurns) {
|
|
76
|
+
softLimitReached = true;
|
|
77
|
+
runDetached("turn-limit steer", () => session.steer(
|
|
78
|
+
"You have reached your turn limit. Wrap up immediately - provide your final answer now.",
|
|
79
|
+
));
|
|
80
|
+
} else if (softLimitReached && turnCount >= maxTurns + (opts.graceTurns ?? 5)) {
|
|
81
|
+
turnLimitReason = "turn_limit_hard";
|
|
82
|
+
runDetached("turn-limit abort", () => session.abort());
|
|
140
83
|
}
|
|
141
84
|
});
|
|
142
85
|
});
|
|
@@ -144,33 +87,26 @@ export class SubagentSession {
|
|
|
144
87
|
const startIndex = session.messages.length;
|
|
145
88
|
const collector = collectResponseText(session);
|
|
146
89
|
const cleanupAbort = forwardAbortSignal(session, opts.signal);
|
|
147
|
-
|
|
148
|
-
// Prepend parent context before lifecycle providers see the exact prompt.
|
|
149
|
-
const effectivePrompt = this.meta.parentContext
|
|
150
|
-
? this.meta.parentContext + prompt
|
|
151
|
-
: prompt;
|
|
90
|
+
const effectivePrompt = this.meta.parentContext ? this.meta.parentContext + prompt : prompt;
|
|
152
91
|
|
|
153
92
|
try {
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
aborted,
|
|
164
|
-
steered: softLimitReached,
|
|
165
|
-
};
|
|
93
|
+
if (opts.lifecycle) {
|
|
94
|
+
return await this.driveLifecycleTurns(
|
|
95
|
+
effectivePrompt,
|
|
96
|
+
opts.lifecycle,
|
|
97
|
+
() => collector.getText().trim() || getLastAssistantText(session, startIndex),
|
|
98
|
+
() => finalTurnError(session, startIndex),
|
|
99
|
+
() => turnLimitReason,
|
|
100
|
+
() => softLimitReached,
|
|
101
|
+
);
|
|
166
102
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
);
|
|
103
|
+
|
|
104
|
+
await session.prompt(effectivePrompt);
|
|
105
|
+
const responseText = collector.getText().trim() || getLastAssistantText(session, startIndex);
|
|
106
|
+
const failure = finalTurnError(session, startIndex);
|
|
107
|
+
const terminalReason = failure ? undefined : turnLimitReason ?? (softLimitReached ? "turn_limit_graceful" : undefined);
|
|
108
|
+
if (!failure) this.publishCompleted(terminalReason ?? "completed");
|
|
109
|
+
return { responseText, failure, ...(terminalReason ? { terminalReason } : {}) };
|
|
174
110
|
} finally {
|
|
175
111
|
releaseTurnLoopHandles([
|
|
176
112
|
["subagent turn-limit unsubscribe", unsubTurns],
|
|
@@ -180,22 +116,16 @@ export class SubagentSession {
|
|
|
180
116
|
}
|
|
181
117
|
}
|
|
182
118
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
): Promise<{ text: string; failure?: string }> {
|
|
188
|
-
const session = this._session;
|
|
189
|
-
const startIndex = session.messages.length;
|
|
190
|
-
const collector = collectResponseText(session);
|
|
191
|
-
const cleanupAbort = forwardAbortSignal(session, signal);
|
|
192
|
-
|
|
119
|
+
async resumeTurnLoop(prompt: string, signal?: AbortSignal): Promise<{ text: string; failure?: string; terminalReason?: TurnLimitReason }> {
|
|
120
|
+
const startIndex = this._session.messages.length;
|
|
121
|
+
const collector = collectResponseText(this._session);
|
|
122
|
+
const cleanupAbort = forwardAbortSignal(this._session, signal);
|
|
193
123
|
try {
|
|
194
|
-
await
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
};
|
|
124
|
+
await this._session.prompt(prompt);
|
|
125
|
+
const text = collector.getText().trim() || getLastAssistantText(this._session, startIndex);
|
|
126
|
+
const failure = finalTurnError(this._session, startIndex);
|
|
127
|
+
if (!failure) this.publishCompleted("completed");
|
|
128
|
+
return { text, failure };
|
|
199
129
|
} finally {
|
|
200
130
|
releaseTurnLoopHandles([
|
|
201
131
|
["subagent response collector unsubscribe", collector.unsubscribe],
|
|
@@ -204,10 +134,6 @@ export class SubagentSession {
|
|
|
204
134
|
}
|
|
205
135
|
}
|
|
206
136
|
|
|
207
|
-
/**
|
|
208
|
-
* Resume with active providers. This is separate from resumeTurnLoop so the
|
|
209
|
-
* historical no-provider method signature and event behavior remain intact.
|
|
210
|
-
*/
|
|
211
137
|
async resumeLifecycleTurnLoop(
|
|
212
138
|
prompt: string,
|
|
213
139
|
signal: AbortSignal | undefined,
|
|
@@ -222,7 +148,8 @@ export class SubagentSession {
|
|
|
222
148
|
lifecycle,
|
|
223
149
|
() => collector.getText().trim() || getLastAssistantText(this._session, startIndex),
|
|
224
150
|
() => finalTurnError(this._session, startIndex),
|
|
225
|
-
() =>
|
|
151
|
+
() => undefined,
|
|
152
|
+
() => false,
|
|
226
153
|
);
|
|
227
154
|
} finally {
|
|
228
155
|
releaseTurnLoopHandles([
|
|
@@ -237,38 +164,35 @@ export class SubagentSession {
|
|
|
237
164
|
lifecycle: SubagentTurnLifecycle,
|
|
238
165
|
responseText: () => string,
|
|
239
166
|
failure: () => string | undefined,
|
|
240
|
-
|
|
167
|
+
turnLimitReason: () => TurnLimitReason | undefined,
|
|
168
|
+
softLimitReached: () => boolean,
|
|
241
169
|
): Promise<TurnLoopResult> {
|
|
242
170
|
const start = await lifecycle.beforeStart(initialPrompt);
|
|
243
|
-
if (start?.action === "abort")
|
|
244
|
-
return this.lifecycleAbort(start.reason, outcome());
|
|
245
|
-
}
|
|
171
|
+
if (start?.action === "abort") return this.lifecycleAbort(start.reason);
|
|
246
172
|
|
|
247
173
|
let prompt = start?.prompt ?? initialPrompt;
|
|
248
174
|
let continuationRound = 0;
|
|
249
175
|
for (;;) {
|
|
250
176
|
lifecycle.signal.throwIfAborted();
|
|
251
177
|
await this._session.prompt(prompt);
|
|
252
|
-
const turnOutcome = outcome();
|
|
253
178
|
const proposedResult = responseText();
|
|
254
179
|
const turnFailure = failure();
|
|
255
180
|
if (turnFailure) {
|
|
256
|
-
|
|
181
|
+
// Provider failure is a record terminal reason, not a lifecycle outcome;
|
|
182
|
+
// do not publish a misleading child-completed event.
|
|
183
|
+
return { responseText: proposedResult, failure: turnFailure };
|
|
257
184
|
}
|
|
185
|
+
|
|
186
|
+
const turnReason = turnLimitReason() ?? (softLimitReached() ? "turn_limit_graceful" : undefined);
|
|
258
187
|
const completion = await lifecycle.beforeComplete(
|
|
259
188
|
proposedResult,
|
|
260
|
-
|
|
189
|
+
turnReason === "turn_limit_hard" ? "turn_limit_hard" : turnReason === "turn_limit_graceful" ? "turn_limit_graceful" : "completed",
|
|
261
190
|
continuationRound,
|
|
262
191
|
);
|
|
263
|
-
if (completion?.action === "abort")
|
|
264
|
-
return this.lifecycleAbort(completion.reason, turnOutcome);
|
|
265
|
-
}
|
|
192
|
+
if (completion?.action === "abort") return this.lifecycleAbort(completion.reason);
|
|
266
193
|
if (completion?.action === "continue") {
|
|
267
194
|
if (continuationRound >= lifecycle.maxContinuationRounds) {
|
|
268
|
-
return this.lifecycleAbort(
|
|
269
|
-
`Lifecycle continuation limit of ${lifecycle.maxContinuationRounds} reached`,
|
|
270
|
-
turnOutcome,
|
|
271
|
-
);
|
|
195
|
+
return this.lifecycleAbort(`Lifecycle continuation limit of ${lifecycle.maxContinuationRounds} reached`);
|
|
272
196
|
}
|
|
273
197
|
continuationRound++;
|
|
274
198
|
prompt = completion.prompt;
|
|
@@ -278,51 +202,28 @@ export class SubagentSession {
|
|
|
278
202
|
const finalResult = completion?.action === "complete"
|
|
279
203
|
? completion.result ?? proposedResult
|
|
280
204
|
: proposedResult;
|
|
281
|
-
this.publishCompleted(
|
|
282
|
-
return {
|
|
283
|
-
responseText: finalResult,
|
|
284
|
-
aborted: turnOutcome.aborted,
|
|
285
|
-
steered: turnOutcome.steered,
|
|
286
|
-
};
|
|
205
|
+
this.publishCompleted(turnReason ?? "completed");
|
|
206
|
+
return { responseText: finalResult, ...(turnReason ? { terminalReason: turnReason } : {}) };
|
|
287
207
|
}
|
|
288
208
|
}
|
|
289
209
|
|
|
290
|
-
private lifecycleAbort(
|
|
291
|
-
reason:
|
|
292
|
-
outcome: Readonly<{ aborted: boolean; steered: boolean }>,
|
|
293
|
-
): TurnLoopResult {
|
|
294
|
-
// No child completion is emitted: the provider declined to accept a turn.
|
|
295
|
-
return {
|
|
296
|
-
responseText: reason,
|
|
297
|
-
aborted: true,
|
|
298
|
-
steered: outcome.steered,
|
|
299
|
-
lifecycleAborted: true,
|
|
300
|
-
};
|
|
210
|
+
private lifecycleAbort(reason: string): TurnLoopResult {
|
|
211
|
+
return { responseText: reason, terminalReason: "lifecycle_abort" };
|
|
301
212
|
}
|
|
302
213
|
|
|
303
|
-
private publishCompleted(
|
|
214
|
+
private publishCompleted(terminalReason: "completed" | TurnLimitReason): void {
|
|
304
215
|
this.meta.lifecycle.completed({
|
|
305
216
|
sessionDir: this.meta.sessionDir,
|
|
306
217
|
agentName: this.meta.agentName,
|
|
307
|
-
|
|
308
|
-
steered,
|
|
218
|
+
terminalReason,
|
|
309
219
|
});
|
|
310
220
|
}
|
|
311
221
|
|
|
312
|
-
|
|
313
|
-
get isIdle(): boolean {
|
|
314
|
-
return this._session.isIdle;
|
|
315
|
-
}
|
|
222
|
+
get isIdle(): boolean { return this._session.isIdle; }
|
|
316
223
|
|
|
317
|
-
/**
|
|
318
|
-
* Wait for Pi's authoritative idle boundary before starting another prompt.
|
|
319
|
-
* A record can already look terminal while AgentSession is still finishing an
|
|
320
|
-
* extension-driven continuation, so domain status alone is insufficient.
|
|
321
|
-
*/
|
|
322
224
|
async waitUntilIdle(signal?: AbortSignal): Promise<void> {
|
|
323
225
|
if (this._session.isIdle) return;
|
|
324
226
|
signal?.throwIfAborted();
|
|
325
|
-
|
|
326
227
|
await new Promise<void>((resolve, reject) => {
|
|
327
228
|
let settled = false;
|
|
328
229
|
let unsubscribe: () => void = () => {};
|
|
@@ -342,67 +243,24 @@ export class SubagentSession {
|
|
|
342
243
|
cleanup();
|
|
343
244
|
reject(signal?.reason ?? new Error("Resume wait aborted"));
|
|
344
245
|
};
|
|
345
|
-
|
|
346
246
|
unsubscribe = this._session.subscribe((event: AgentSessionEvent) => {
|
|
347
247
|
if (event.type === "agent_settled" || this._session.isIdle) finish();
|
|
348
248
|
});
|
|
349
249
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
350
|
-
|
|
351
|
-
// Close the check/subscribe race: Pi may have settled between the first
|
|
352
|
-
// isIdle read and listener registration.
|
|
353
250
|
if (this._session.isIdle) finish();
|
|
354
251
|
else if (signal?.aborted) onAbort();
|
|
355
252
|
});
|
|
356
253
|
}
|
|
357
254
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Return the session context window utilization (0-100), or null when unavailable. */
|
|
369
|
-
getContextPercent(): number | null {
|
|
370
|
-
return getSessionContextPercent(this._session);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/** Subscribe to session events. Satisfies `SubscribableSession`. */
|
|
374
|
-
subscribe(fn: (event: AgentSessionEvent) => void): () => void {
|
|
375
|
-
return this._session.subscribe(fn);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/** Return session token statistics. Satisfies `SessionLike`. */
|
|
379
|
-
getSessionStats(): SessionStatsLike {
|
|
380
|
-
return this._session.getSessionStats();
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/** The session's message history. */
|
|
384
|
-
get messages(): readonly unknown[] {
|
|
385
|
-
return this._session.messages as readonly unknown[];
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/** The session's message history, typed for Pi's session-rendering machinery. */
|
|
389
|
-
get agentMessages(): readonly SessionMessage[] {
|
|
390
|
-
return this._session.messages;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/** Resolve a registered tool definition by name, for Pi's tool-execution components. */
|
|
394
|
-
getToolDefinition(name: string): ToolDefinition | undefined {
|
|
395
|
-
return this._session.getToolDefinition(name);
|
|
396
|
-
}
|
|
255
|
+
async steer(message: string): Promise<void> { await this._session.steer(message); }
|
|
256
|
+
getConversation(): string { return getAgentConversation(this._session); }
|
|
257
|
+
getContextPercent(): number | null { return getSessionContextPercent(this._session); }
|
|
258
|
+
subscribe(fn: (event: AgentSessionEvent) => void): () => void { return this._session.subscribe(fn); }
|
|
259
|
+
getSessionStats(): SessionStatsLike { return this._session.getSessionStats(); }
|
|
260
|
+
get messages(): readonly unknown[] { return this._session.messages as readonly unknown[]; }
|
|
261
|
+
get agentMessages(): readonly SessionMessage[] { return this._session.messages; }
|
|
262
|
+
getToolDefinition(name: string): ToolDefinition | undefined { return this._session.getToolDefinition(name); }
|
|
397
263
|
|
|
398
|
-
/**
|
|
399
|
-
* Tear down the child session exactly once.
|
|
400
|
-
*
|
|
401
|
-
* AgentSession.dispose() revokes extension contexts immediately but does not
|
|
402
|
-
* emit the extension lifecycle event. Child-owned extensions need the same
|
|
403
|
-
* awaited shutdown boundary as root-session replacement so they can cancel
|
|
404
|
-
* detached work before their Pi API is invalidated.
|
|
405
|
-
*/
|
|
406
264
|
dispose(): Promise<void> {
|
|
407
265
|
this.disposalPromise ??= this.disposeOnce();
|
|
408
266
|
return this.disposalPromise;
|
|
@@ -410,21 +268,15 @@ export class SubagentSession {
|
|
|
410
268
|
|
|
411
269
|
private async disposeOnce(): Promise<void> {
|
|
412
270
|
try {
|
|
413
|
-
await this._session.extensionRunner.emit({
|
|
414
|
-
type: "session_shutdown",
|
|
415
|
-
reason: "quit",
|
|
416
|
-
});
|
|
271
|
+
await this._session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
|
|
417
272
|
} catch (error) {
|
|
418
|
-
// A faulty extension must not prevent the SDK session from being revoked.
|
|
419
273
|
debugLog("child extension session_shutdown", error);
|
|
420
274
|
}
|
|
421
|
-
|
|
422
275
|
try {
|
|
423
276
|
this._session.dispose();
|
|
424
277
|
} catch (error) {
|
|
425
278
|
debugLog("child session dispose", error);
|
|
426
279
|
}
|
|
427
|
-
|
|
428
280
|
try {
|
|
429
281
|
this.meta.lifecycle.disposed({ sessionId: this.meta.sessionId });
|
|
430
282
|
} catch (error) {
|
|
@@ -433,39 +285,17 @@ export class SubagentSession {
|
|
|
433
285
|
}
|
|
434
286
|
}
|
|
435
287
|
|
|
436
|
-
// ── Private turn-loop helpers ───────────────────────────────────────────────────
|
|
437
|
-
|
|
438
|
-
function toLifecycleOutcome(
|
|
439
|
-
outcome: Readonly<{ aborted: boolean; steered: boolean }>,
|
|
440
|
-
): SubagentLifecycleOutcome {
|
|
441
|
-
if (outcome.aborted) return "aborted";
|
|
442
|
-
if (outcome.steered) return "steered";
|
|
443
|
-
return "completed";
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Subscribe to a session and collect the last assistant message text.
|
|
448
|
-
* Returns an object with a `getText()` getter and an `unsubscribe` function.
|
|
449
|
-
*/
|
|
450
288
|
function collectResponseText(session: AgentSession) {
|
|
451
289
|
let text = "";
|
|
452
290
|
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
453
291
|
runSafely("subagent response observer", () => {
|
|
454
|
-
if (event.type === "message_start" && event.message?.role === "assistant")
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
if (
|
|
458
|
-
event.type === "message_update" &&
|
|
459
|
-
event.assistantMessageEvent.type === "text_delta"
|
|
460
|
-
) {
|
|
461
|
-
text += event.assistantMessageEvent.delta;
|
|
462
|
-
}
|
|
292
|
+
if (event.type === "message_start" && event.message?.role === "assistant") text = "";
|
|
293
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") text += event.assistantMessageEvent.delta;
|
|
463
294
|
});
|
|
464
295
|
});
|
|
465
296
|
return { getText: () => text, unsubscribe };
|
|
466
297
|
}
|
|
467
298
|
|
|
468
|
-
/** Get the last assistant text from the completed session history. */
|
|
469
299
|
function getLastAssistantText(session: AgentSession, startIndex = 0): string {
|
|
470
300
|
for (let i = session.messages.length - 1; i >= startIndex; i--) {
|
|
471
301
|
const msg = session.messages[i];
|
|
@@ -476,45 +306,25 @@ function getLastAssistantText(session: AgentSession, startIndex = 0): string {
|
|
|
476
306
|
return "";
|
|
477
307
|
}
|
|
478
308
|
|
|
479
|
-
/** Classify a normally-resolved failure from this invocation's final assistant turn. */
|
|
480
309
|
function finalTurnError(session: AgentSession, startIndex = 0): string | undefined {
|
|
481
310
|
for (let i = session.messages.length - 1; i >= startIndex; i--) {
|
|
482
311
|
const msg = session.messages[i];
|
|
483
312
|
if (msg.role !== "assistant") continue;
|
|
484
|
-
if (msg.stopReason === "error")
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
if (msg.stopReason === "length" && !extractText(msg.content).trim()) {
|
|
488
|
-
return "run hit the output token limit before producing any text";
|
|
489
|
-
}
|
|
313
|
+
if (msg.stopReason === "error") return msg.errorMessage?.trim() || "provider error with no output";
|
|
314
|
+
if (msg.stopReason === "length" && !extractText(msg.content).trim()) return "run hit the output token limit before producing any text";
|
|
490
315
|
return undefined;
|
|
491
316
|
}
|
|
492
317
|
return undefined;
|
|
493
318
|
}
|
|
494
319
|
|
|
495
|
-
/**
|
|
496
|
-
* Release the turn loop's SDK-owned cleanup handles one by one. Each handle is
|
|
497
|
-
* code outside this extension, so a throwing unsubscribe must neither skip the
|
|
498
|
-
* remaining handles nor mask the primary turn-loop outcome settling above.
|
|
499
|
-
*/
|
|
500
320
|
function releaseTurnLoopHandles(handles: ReadonlyArray<readonly [string, () => void]>): void {
|
|
501
|
-
for (const [context, release] of handles)
|
|
502
|
-
runSafely(context, release);
|
|
503
|
-
}
|
|
321
|
+
for (const [context, release] of handles) runSafely(context, release);
|
|
504
322
|
}
|
|
505
323
|
|
|
506
|
-
|
|
507
|
-
* Wire an AbortSignal to abort a session.
|
|
508
|
-
* Returns a cleanup function to remove the listener.
|
|
509
|
-
*/
|
|
510
|
-
function forwardAbortSignal(
|
|
511
|
-
session: AgentSession,
|
|
512
|
-
signal?: AbortSignal,
|
|
513
|
-
): () => void {
|
|
324
|
+
function forwardAbortSignal(session: AgentSession, signal?: AbortSignal): () => void {
|
|
514
325
|
if (!signal) return () => {};
|
|
515
|
-
const onAbort = (): void =>
|
|
516
|
-
runDetached("parent-signal abort", () => session.abort());
|
|
517
|
-
};
|
|
326
|
+
const onAbort = (): void => runDetached("parent-signal abort", () => session.abort());
|
|
518
327
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
328
|
+
if (signal.aborted) onAbort();
|
|
519
329
|
return () => signal.removeEventListener("abort", onAbort);
|
|
520
330
|
}
|