@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
|
@@ -1,130 +1,46 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* result-renderer.ts — Pure per-status rendering functions for Agent tool results.
|
|
3
|
-
*
|
|
4
|
-
* All functions are stateless: they receive AgentDetails and a Theme, returning
|
|
5
|
-
* formatted strings. No SDK types, no timers, no side effects.
|
|
6
|
-
* Consumed by the renderResult hook in agent-tool.ts.
|
|
7
|
-
*/
|
|
1
|
+
/** Pure presentation for the joined/detached launch result. */
|
|
8
2
|
|
|
9
3
|
import type { AgentDetails, Theme } from "#src/ui/display";
|
|
10
4
|
import { formatModelThinking, formatMs, formatTurns, SPINNER } from "#src/ui/display";
|
|
11
5
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
resultText: string,
|
|
18
|
-
expanded: boolean,
|
|
19
|
-
isPartial: boolean,
|
|
20
|
-
theme: Theme,
|
|
21
|
-
): string {
|
|
22
|
-
if (isPartial || details.status === "running") return renderRunning(details, theme);
|
|
23
|
-
if (details.status === "background") return renderBackground(details, theme);
|
|
24
|
-
if (details.status === "completed" || details.status === "steered")
|
|
25
|
-
return renderCompleted(details, resultText, expanded, theme);
|
|
26
|
-
if (details.status === "stopped") return renderStopped(details, theme);
|
|
27
|
-
return renderFailed(details, theme);
|
|
6
|
+
export function renderAgentResult(details: AgentDetails, resultText: string, expanded: boolean, isPartial: boolean, theme: Theme): string {
|
|
7
|
+
if (isPartial || details.status === "running" || details.status === "queued") return renderRunning(details, theme);
|
|
8
|
+
if (details.status === "completed") return renderCompleted(details, resultText, expanded, theme);
|
|
9
|
+
if (details.status === "stopped") return renderStopped(details, theme);
|
|
10
|
+
return renderFailed(details, theme);
|
|
28
11
|
}
|
|
29
12
|
|
|
30
|
-
// ---- Per-status renderers ----
|
|
31
|
-
|
|
32
|
-
/** Render running/partial status: spinner + stats + activity line. */
|
|
33
13
|
export function renderRunning(details: AgentDetails, theme: Theme): string {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
line += `${s ? " " + theme.fg("dim", "·") + " " : " "}${duration}`;
|
|
39
|
-
line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking\u2026"}`);
|
|
40
|
-
return line;
|
|
14
|
+
const frame = SPINNER[details.spinnerFrame ?? 0];
|
|
15
|
+
const stats = renderStats(details, theme);
|
|
16
|
+
const duration = theme.fg("dim", formatMs(details.durationMs));
|
|
17
|
+
return theme.fg("accent", frame) + (stats ? " " + stats : "") + " " + duration + "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
|
|
41
18
|
}
|
|
42
19
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
20
|
+
export function renderCompleted(details: AgentDetails, resultText: string, expanded: boolean, theme: Theme): string {
|
|
21
|
+
let line = theme.fg("success", "✓") + " " + renderStats(details, theme) + " " + theme.fg("dim", "·") + " " + theme.fg("dim", formatMs(details.durationMs));
|
|
22
|
+
if (expanded) {
|
|
23
|
+
for (const item of resultText.split("\n").slice(0, 50)) line += "\n" + theme.fg("dim", ` ${item}`);
|
|
24
|
+
if (resultText.split("\n").length > 50) line += "\n" + theme.fg("muted", " ... (output truncated; use get_subagent_result for the bounded final output)");
|
|
25
|
+
} else {
|
|
26
|
+
line += "\n" + theme.fg("dim", ` ⎿ ${details.terminalReason === "turn_limit_graceful" ? "Completed (turn limit)" : "Done"}`);
|
|
27
|
+
}
|
|
28
|
+
return line;
|
|
50
29
|
}
|
|
51
30
|
|
|
52
|
-
/** Render completed or steered status with optional expanded result text. */
|
|
53
|
-
export function renderCompleted(
|
|
54
|
-
details: AgentDetails,
|
|
55
|
-
resultText: string,
|
|
56
|
-
expanded: boolean,
|
|
57
|
-
theme: Theme,
|
|
58
|
-
): string {
|
|
59
|
-
const duration = formatMs(details.durationMs);
|
|
60
|
-
const isSteered = details.status === "steered";
|
|
61
|
-
const icon = isSteered ? theme.fg("warning", "\u2713") : theme.fg("success", "\u2713");
|
|
62
|
-
const s = renderStats(details, theme);
|
|
63
|
-
let line = icon + (s ? " " + s : "");
|
|
64
|
-
line += " " + theme.fg("dim", "\u00B7") + " " + theme.fg("dim", duration);
|
|
65
|
-
|
|
66
|
-
if (expanded) {
|
|
67
|
-
if (resultText) {
|
|
68
|
-
const lines = resultText.split("\n").slice(0, 50);
|
|
69
|
-
for (const l of lines) {
|
|
70
|
-
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
71
|
-
}
|
|
72
|
-
if (resultText.split("\n").length > 50) {
|
|
73
|
-
line +=
|
|
74
|
-
"\n" +
|
|
75
|
-
theme.fg(
|
|
76
|
-
"muted",
|
|
77
|
-
" ... (use get_subagent_result with verbose for full output)",
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
const doneText = isSteered ? "Wrapped up (turn limit)" : "Done";
|
|
83
|
-
line += "\n" + theme.fg("dim", ` \u23BF ${doneText}`);
|
|
84
|
-
}
|
|
85
|
-
return line;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Render stopped status: dim stop icon + stats + "Stopped". */
|
|
89
31
|
export function renderStopped(details: AgentDetails, theme: Theme): string {
|
|
90
|
-
|
|
91
|
-
let line = theme.fg("dim", "\u25A0") + (s ? " " + s : "");
|
|
92
|
-
line += ` ${theme.fg("dim", "·")} ${theme.fg("dim", formatMs(details.durationMs))}`;
|
|
93
|
-
line += "\n" + theme.fg("dim", " \u23BF Stopped");
|
|
94
|
-
return line;
|
|
32
|
+
return theme.fg("dim", "■") + " " + renderStats(details, theme) + " " + theme.fg("dim", "·") + " " + theme.fg("dim", formatMs(details.durationMs)) + "\n" + theme.fg("dim", ` ⎿ Stopped${details.terminalReason ? ` (${details.terminalReason.replaceAll("_", " ")})` : ""}`);
|
|
95
33
|
}
|
|
96
34
|
|
|
97
|
-
/** Render error or aborted status: error icon + stats + status message. */
|
|
98
35
|
export function renderFailed(details: AgentDetails, theme: Theme): string {
|
|
99
|
-
|
|
100
|
-
let line = theme.fg("error", "\u2717") + (s ? " " + s : "");
|
|
101
|
-
line += ` ${theme.fg("dim", "·")} ${theme.fg("dim", formatMs(details.durationMs))}`;
|
|
102
|
-
|
|
103
|
-
if (details.status === "error") {
|
|
104
|
-
line += "\n" + theme.fg("error", ` \u23BF Error: ${details.error ?? "unknown"}`);
|
|
105
|
-
} else {
|
|
106
|
-
line += "\n" + theme.fg("warning", " \u23BF Aborted (max turns exceeded)");
|
|
107
|
-
}
|
|
108
|
-
return line;
|
|
36
|
+
return theme.fg("error", "✗") + " " + renderStats(details, theme) + " " + theme.fg("dim", "·") + " " + theme.fg("dim", formatMs(details.durationMs)) + "\n" + theme.fg("error", ` ⎿ Error: ${details.error ?? "unknown"}`);
|
|
109
37
|
}
|
|
110
38
|
|
|
111
|
-
// ---- Shared helper ----
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Build the stats string: "haiku · thinking: high · ⟳5≤30 · 3 tool uses · 33.8k token".
|
|
115
|
-
* Returns an empty string when all fields are absent or zero.
|
|
116
|
-
*/
|
|
117
39
|
export function renderStats(details: AgentDetails, theme: Theme): string {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (details.toolUses > 0)
|
|
125
|
-
parts.push(`${details.toolUses} tool use${details.toolUses === 1 ? "" : "s"}`);
|
|
126
|
-
if (details.tokens) parts.push(details.tokens);
|
|
127
|
-
return parts
|
|
128
|
-
.map((p) => theme.fg("dim", p))
|
|
129
|
-
.join(" " + theme.fg("dim", "\u00B7") + " ");
|
|
40
|
+
const parts: string[] = [formatModelThinking(details.modelName ?? "unknown model", details.thinkingLevel)];
|
|
41
|
+
if (details.tags) parts.push(...details.tags.filter((tag) => !tag.startsWith("thinking: ")));
|
|
42
|
+
if (details.turnCount != null && details.turnCount > 0) parts.push(formatTurns(details.turnCount, details.maxTurns));
|
|
43
|
+
if (details.toolUses > 0) parts.push(`${details.toolUses} tool use${details.toolUses === 1 ? "" : "s"}`);
|
|
44
|
+
if (details.tokens) parts.push(details.tokens);
|
|
45
|
+
return parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
130
46
|
}
|
package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/resume-tool.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { SubagentMode } from "#src/types";
|
|
4
|
+
import type { ResumeOutcome } from "#src/lifecycle/subagent-manager";
|
|
5
|
+
import type { Subagent } from "#src/lifecycle/subagent";
|
|
6
|
+
import { buildDetails, formatLifetimeTokens, textResult } from "#src/tools/helpers";
|
|
7
|
+
import { formatModelThinking, formatMs } from "#src/ui/display";
|
|
8
|
+
|
|
9
|
+
export interface ResumeToolManager {
|
|
10
|
+
resume(id: string, prompt: string, mode: SubagentMode, timeoutSeconds: number | undefined, signal?: AbortSignal, onReserved?: (record: Subagent) => void): Promise<ResumeOutcome>;
|
|
11
|
+
getRecord(id: string): Subagent | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ResumeTool {
|
|
15
|
+
constructor(private readonly manager: ResumeToolManager) {}
|
|
16
|
+
|
|
17
|
+
async execute(_toolCallId: string, params: { agent_id: string; prompt: string; mode?: SubagentMode; timeout_seconds?: number }, signal: AbortSignal | undefined, onUpdate: ((update: unknown) => void) | undefined, _ctx: unknown) {
|
|
18
|
+
if (params.timeout_seconds != null && (!Number.isInteger(params.timeout_seconds) || params.timeout_seconds <= 0)) {
|
|
19
|
+
return textResult("timeout_seconds must be a positive integer");
|
|
20
|
+
}
|
|
21
|
+
const mode = params.mode ?? "detached";
|
|
22
|
+
let spinnerFrame = 0;
|
|
23
|
+
const timer = setInterval(() => {
|
|
24
|
+
if (!onUpdate || mode !== "joined") return;
|
|
25
|
+
const record = this.manager.getRecord(params.agent_id);
|
|
26
|
+
if (!record) return;
|
|
27
|
+
onUpdate({ content: [{ type: "text", text: `${record.toolUses} tool uses...` }], details: buildDetails({
|
|
28
|
+
displayName: record.type, description: record.description, subagentType: record.type,
|
|
29
|
+
modelName: record.modelLabel, thinkingLevel: record.effectiveThinkingLevel, tags: [`mode: ${record.mode}`],
|
|
30
|
+
}, record, {
|
|
31
|
+
status: record.status,
|
|
32
|
+
terminalReason: record.stateTerminalReason,
|
|
33
|
+
durationMs: record.activeRuntimeMs,
|
|
34
|
+
spinnerFrame: spinnerFrame++,
|
|
35
|
+
}) });
|
|
36
|
+
}, 500);
|
|
37
|
+
let outcome: ResumeOutcome;
|
|
38
|
+
try {
|
|
39
|
+
outcome = await this.manager.resume(params.agent_id, params.prompt, mode, params.timeout_seconds, signal, (record) => {
|
|
40
|
+
if (onUpdate && mode === "joined") onUpdate({ content: [{ type: "text", text: "Resuming..." }], details: buildDetails({
|
|
41
|
+
displayName: record.type, description: record.description, subagentType: record.type,
|
|
42
|
+
modelName: record.modelLabel, thinkingLevel: record.effectiveThinkingLevel, tags: [`mode: ${record.mode}`],
|
|
43
|
+
}, record, {
|
|
44
|
+
status: record.status,
|
|
45
|
+
terminalReason: record.stateTerminalReason,
|
|
46
|
+
durationMs: record.activeRuntimeMs,
|
|
47
|
+
}) });
|
|
48
|
+
});
|
|
49
|
+
} finally {
|
|
50
|
+
clearInterval(timer);
|
|
51
|
+
}
|
|
52
|
+
if (outcome.kind === "not_found") return textResult(`Agent not found: "${outcome.agentId}". It may have been cleaned up.`);
|
|
53
|
+
if (outcome.kind === "wrong_state") return textResult(`Agent "${outcome.agentId}" cannot be resumed (status: ${outcome.status}). It must have a retained settled session.`);
|
|
54
|
+
|
|
55
|
+
const record = outcome.kind === "joined" ? outcome.record : this.manager.getRecord(outcome.agentId);
|
|
56
|
+
if (!record) return textResult(`Agent not found after resume: "${params.agent_id}".`);
|
|
57
|
+
if (outcome.kind === "detached") {
|
|
58
|
+
return textResult(`Resume detached for agent ${record.id}.\nRun ID: ${record.runId}\nMode: detached\nCompletion will send one notification.`, buildDetails({
|
|
59
|
+
displayName: record.type,
|
|
60
|
+
description: record.description,
|
|
61
|
+
subagentType: record.type,
|
|
62
|
+
modelName: record.modelLabel,
|
|
63
|
+
thinkingLevel: record.effectiveThinkingLevel,
|
|
64
|
+
tags: [`mode: ${record.mode}`],
|
|
65
|
+
}, record, {
|
|
66
|
+
status: record.status,
|
|
67
|
+
terminalReason: record.stateTerminalReason,
|
|
68
|
+
durationMs: record.activeRuntimeMs,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
record.markConsumed();
|
|
72
|
+
return textResult(`Agent ${record.id} ${record.status}.\nRun ID: ${record.runId}\nModel: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatMs(record.activeRuntimeMs)}\nReason: ${record.stateTerminalReason ?? "unknown"}\n\n${boundResult(record.result, record.outputFile)}`, buildDetails({
|
|
73
|
+
displayName: record.type,
|
|
74
|
+
description: record.description,
|
|
75
|
+
subagentType: record.type,
|
|
76
|
+
modelName: record.modelLabel,
|
|
77
|
+
thinkingLevel: record.effectiveThinkingLevel,
|
|
78
|
+
tags: [`mode: ${record.mode}`],
|
|
79
|
+
}, record, { tokens: formatLifetimeTokens(record), durationMs: record.activeRuntimeMs, terminalReason: record.stateTerminalReason }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
toToolDefinition() {
|
|
83
|
+
return defineTool({
|
|
84
|
+
name: "resume_subagent" as const,
|
|
85
|
+
label: "Resume Subagent",
|
|
86
|
+
promptSnippet: "resume_subagent: Continue a retained subagent session.",
|
|
87
|
+
description: "Continue a retained settled subagent session. Delivery defaults to detached; joined waits for the resumed run to settle.",
|
|
88
|
+
parameters: Type.Object({
|
|
89
|
+
agent_id: Type.String({ description: "The retained subagent ID to resume." }),
|
|
90
|
+
prompt: Type.String({ description: "The next task or instruction." }),
|
|
91
|
+
mode: Type.Optional(Type.Union([Type.Literal("joined"), Type.Literal("detached")], { description: "Result delivery mode. Defaults to detached." })),
|
|
92
|
+
timeout_seconds: Type.Optional(Type.Integer({ description: "Active runtime deadline in seconds; queued time is excluded.", minimum: 1 })),
|
|
93
|
+
}),
|
|
94
|
+
execute: (toolCallId: string, params: { agent_id: string; prompt: string; mode?: SubagentMode; timeout_seconds?: number }, signal: AbortSignal | undefined, onUpdate: unknown, ctx: unknown) => this.execute(toolCallId, params, signal, onUpdate as ((update: unknown) => void) | undefined, ctx),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const MAX_RESULT_OUTPUT = 12_000;
|
|
100
|
+
function boundResult(result: string | undefined, outputFile: string | undefined): string {
|
|
101
|
+
const output = result?.trim() || "No output.";
|
|
102
|
+
return output.length > MAX_RESULT_OUTPUT
|
|
103
|
+
? output.slice(0, MAX_RESULT_OUTPUT) + `\n\nOutput truncated. Full transcript: ${outputFile ?? "unavailable"}`
|
|
104
|
+
: output;
|
|
105
|
+
}
|
|
@@ -15,7 +15,7 @@ import type { ModelRegistry } from "#src/session/model-resolver";
|
|
|
15
15
|
import { formatModelLabel } from "#src/session/model-label";
|
|
16
16
|
import { resolveInvocationModel } from "#src/session/model-resolver";
|
|
17
17
|
import { resolveEffectiveThinkingLevel } from "#src/session/thinking-level";
|
|
18
|
-
import type { AgentInvocation, SubagentType, ThinkingLevel } from "#src/types";
|
|
18
|
+
import type { AgentInvocation, SubagentMode, SubagentType, ThinkingLevel } from "#src/types";
|
|
19
19
|
import {
|
|
20
20
|
type AgentDetails,
|
|
21
21
|
buildInvocationTags,
|
|
@@ -50,7 +50,8 @@ export interface SpawnExecution {
|
|
|
50
50
|
/** Exact level passed to the child session after model-capability clamping. */
|
|
51
51
|
effectiveThinkingLevel: ThinkingLevel;
|
|
52
52
|
inheritContext: boolean;
|
|
53
|
-
|
|
53
|
+
mode: SubagentMode;
|
|
54
|
+
timeoutSeconds?: number;
|
|
54
55
|
agentInvocation: AgentInvocation;
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -88,6 +89,13 @@ export function resolveSpawnConfig(
|
|
|
88
89
|
readonly fallbackSubagent?: string | false;
|
|
89
90
|
},
|
|
90
91
|
): ResolvedSpawnConfig | SpawnConfigError {
|
|
92
|
+
const removedField = ["run_in_background", "foreground", "resume"].find((field) =>
|
|
93
|
+
Object.prototype.hasOwnProperty.call(params, field),
|
|
94
|
+
);
|
|
95
|
+
if (removedField) {
|
|
96
|
+
return { error: `Removed field "${removedField}"; use mode: joined|detached${removedField === "resume" ? " and resume_subagent" : ""}.` };
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
const rawType = params.subagent_type as SubagentType;
|
|
92
100
|
const typeResolution = resolveDispatchAgentType(
|
|
93
101
|
rawType,
|
|
@@ -122,21 +130,30 @@ export function resolveSpawnConfig(
|
|
|
122
130
|
modelInfo.parentThinkingLevel,
|
|
123
131
|
);
|
|
124
132
|
const inheritContext = resolvedConfig.inheritContext;
|
|
125
|
-
const
|
|
133
|
+
const mode = resolvedConfig.mode;
|
|
134
|
+
if (mode !== "joined" && mode !== "detached") {
|
|
135
|
+
return { error: "mode must be joined or detached" };
|
|
136
|
+
}
|
|
137
|
+
const maxTurns = resolvedConfig.maxTurns ?? settings.defaultMaxTurns;
|
|
138
|
+
if (maxTurns != null && (!Number.isInteger(maxTurns) || maxTurns < 0)) {
|
|
139
|
+
return { error: "max_turns must be a non-negative integer" };
|
|
140
|
+
}
|
|
141
|
+
if (resolvedConfig.timeoutSeconds != null && (!Number.isInteger(resolvedConfig.timeoutSeconds) || resolvedConfig.timeoutSeconds <= 0)) {
|
|
142
|
+
return { error: "timeout_seconds must be a positive integer" };
|
|
143
|
+
}
|
|
126
144
|
|
|
127
145
|
// Every status surface shows the exact effective model, including inheritance.
|
|
128
146
|
const modelName = formatModelLabel(model);
|
|
129
147
|
|
|
130
|
-
const effectiveMaxTurns = normalizeMaxTurns(
|
|
131
|
-
resolvedConfig.maxTurns ?? settings.defaultMaxTurns,
|
|
132
|
-
);
|
|
148
|
+
const effectiveMaxTurns = normalizeMaxTurns(maxTurns);
|
|
133
149
|
|
|
134
150
|
const agentInvocation: AgentInvocation = {
|
|
135
151
|
modelName,
|
|
136
152
|
thinking,
|
|
137
153
|
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
138
154
|
inheritContext,
|
|
139
|
-
|
|
155
|
+
mode,
|
|
156
|
+
timeoutSeconds: resolvedConfig.timeoutSeconds,
|
|
140
157
|
};
|
|
141
158
|
|
|
142
159
|
const modeLabel = getPromptModeLabel(subagentType, registry);
|
|
@@ -162,7 +179,8 @@ export function resolveSpawnConfig(
|
|
|
162
179
|
thinking,
|
|
163
180
|
effectiveThinkingLevel,
|
|
164
181
|
inheritContext,
|
|
165
|
-
|
|
182
|
+
mode,
|
|
183
|
+
timeoutSeconds: resolvedConfig.timeoutSeconds,
|
|
166
184
|
agentInvocation,
|
|
167
185
|
},
|
|
168
186
|
presentation: { modelName, agentTags, detailBase },
|
package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/steer-tool.ts
CHANGED
|
@@ -1,117 +1,33 @@
|
|
|
1
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import { formatDuration, formatModelThinking } from "#src/ui/display";
|
|
3
|
+
import type { ManagerSteerOutcome } from "#src/lifecycle/subagent-manager";
|
|
4
|
+
import { textResult } from "#src/tools/helpers";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export interface SteerToolManager {
|
|
10
|
-
getRecord(id: string): Subagent | undefined;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface SteerToolEvents {
|
|
14
|
-
emit(name: string, data: unknown): void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// ---- Class ----
|
|
6
|
+
export interface SteerToolManager { steer(id: string, message: string): Promise<ManagerSteerOutcome>; }
|
|
7
|
+
export interface SteerToolEvents { emit(name: string, data: unknown): void; }
|
|
18
8
|
|
|
19
9
|
export class SteerTool {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return textResult(
|
|
44
|
-
`Failed to steer agent: ${err instanceof Error ? err.message : String(err)}\n`
|
|
45
|
-
+ this.renderRunIdentity(record),
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
switch (outcome.kind) {
|
|
50
|
-
case "rejected":
|
|
51
|
-
return textResult(
|
|
52
|
-
`Agent "${params.agent_id}" is not running (status: ${outcome.status}). Cannot steer a non-running agent.\n`
|
|
53
|
-
+ this.renderRunIdentity(record),
|
|
54
|
-
);
|
|
55
|
-
case "buffered":
|
|
56
|
-
this.events.emit("subagents:steered", { id: record.id, message: params.message });
|
|
57
|
-
return textResult(
|
|
58
|
-
`Steering message queued for agent ${record.id}. It will be delivered once the session initializes.\n`
|
|
59
|
-
+ this.renderRunIdentity(record),
|
|
60
|
-
);
|
|
61
|
-
case "delivered":
|
|
62
|
-
this.events.emit("subagents:steered", { id: record.id, message: params.message });
|
|
63
|
-
return this.renderDelivered(record);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Render the success message with live state for a delivered steer. */
|
|
68
|
-
private renderDelivered(record: Subagent) {
|
|
69
|
-
const tokens = formatLifetimeTokens(record);
|
|
70
|
-
const contextPercent = record.getContextPercent();
|
|
71
|
-
const stateParts: string[] = [formatModelThinking(record.modelLabel, record.effectiveThinkingLevel), formatDuration(record.startedAt, record.completedAt)];
|
|
72
|
-
if (tokens) stateParts.push(tokens);
|
|
73
|
-
stateParts.push(`${record.toolUses} tool ${record.toolUses === 1 ? "use" : "uses"}`);
|
|
74
|
-
if (contextPercent !== null)
|
|
75
|
-
stateParts.push(`context ${Math.round(contextPercent)}% full`);
|
|
76
|
-
if (record.compactionCount)
|
|
77
|
-
stateParts.push(
|
|
78
|
-
`${record.compactionCount} compaction${record.compactionCount === 1 ? "" : "s"}`,
|
|
79
|
-
);
|
|
80
|
-
return textResult(
|
|
81
|
-
`Steering message sent to agent ${record.id}. The agent will process it after its current tool execution.\n` +
|
|
82
|
-
`Current state: ${stateParts.join(" · ")}`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private renderRunIdentity(record: Subagent): string {
|
|
87
|
-
return `Model: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatDuration(record.startedAt, record.completedAt)}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
toToolDefinition() {
|
|
91
|
-
return defineTool({
|
|
92
|
-
name: "steer_subagent" as const,
|
|
93
|
-
label: "Steer Agent",
|
|
94
|
-
promptSnippet:
|
|
95
|
-
"steer_subagent: Send a mid-run message to redirect a running background agent.",
|
|
96
|
-
description:
|
|
97
|
-
"Send a steering message to a running agent. The message will interrupt the agent after its current tool execution " +
|
|
98
|
-
"and be injected into its conversation, allowing you to redirect its work mid-run. Only works on running agents.",
|
|
99
|
-
parameters: Type.Object({
|
|
100
|
-
agent_id: Type.String({
|
|
101
|
-
description: "The agent ID to steer (must be currently running).",
|
|
102
|
-
}),
|
|
103
|
-
message: Type.String({
|
|
104
|
-
description:
|
|
105
|
-
"The steering message to send. This will appear as a user message in the agent's conversation.",
|
|
106
|
-
}),
|
|
107
|
-
}),
|
|
108
|
-
execute: (
|
|
109
|
-
toolCallId: string,
|
|
110
|
-
params: { agent_id: string; message: string },
|
|
111
|
-
signal: AbortSignal,
|
|
112
|
-
onUpdate: unknown,
|
|
113
|
-
ctx: unknown,
|
|
114
|
-
) => this.execute(toolCallId, params, signal, onUpdate, ctx),
|
|
115
|
-
});
|
|
116
|
-
}
|
|
10
|
+
constructor(private readonly manager: SteerToolManager, private readonly events: SteerToolEvents) {}
|
|
11
|
+
|
|
12
|
+
async execute(_toolCallId: string, params: { agent_id: string; message: string }, _signal: AbortSignal, _onUpdate: unknown, _ctx: unknown) {
|
|
13
|
+
const outcome = await this.manager.steer(params.agent_id, params.message);
|
|
14
|
+
if (outcome.kind === "not_found") return textResult(`Agent not found: "${outcome.agentId}". It may have been cleaned up.`, outcome);
|
|
15
|
+
if (outcome.kind === "rejected") return textResult(`Agent "${params.agent_id}" cannot be steered (status: ${outcome.status}).`, { ...outcome, agentId: params.agent_id });
|
|
16
|
+
this.events.emit("subagents:steered", { id: params.agent_id, runId: outcome.runId, outcome: outcome.kind, message: params.message });
|
|
17
|
+
return textResult(`Steering message ${outcome.kind} for agent ${params.agent_id}.\nRun ID: ${outcome.runId}.`, { ...outcome, agentId: params.agent_id });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
toToolDefinition() {
|
|
21
|
+
return defineTool({
|
|
22
|
+
name: "steer_subagent" as const,
|
|
23
|
+
label: "Steer Subagent",
|
|
24
|
+
promptSnippet: "steer_subagent: Send a message to a running subagent.",
|
|
25
|
+
description: "Send a steering message to a running subagent. The structured outcome distinguishes delivered, buffered, rejected, and not-found cases.",
|
|
26
|
+
parameters: Type.Object({
|
|
27
|
+
agent_id: Type.String({ description: "The running subagent ID." }),
|
|
28
|
+
message: Type.String({ description: "The message to add to the subagent's conversation." }),
|
|
29
|
+
}),
|
|
30
|
+
execute: (toolCallId: string, params: { agent_id: string; message: string }, signal: AbortSignal, onUpdate: unknown, ctx: unknown) => this.execute(toolCallId, params, signal, onUpdate, ctx),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
117
33
|
}
|
package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/stop-tool.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { StopOutcome } from "#src/lifecycle/subagent-manager";
|
|
4
|
+
import type { Subagent } from "#src/lifecycle/subagent";
|
|
5
|
+
import { buildDetails, formatLifetimeTokens, textResult } from "#src/tools/helpers";
|
|
6
|
+
import { formatModelThinking, formatMs } from "#src/ui/display";
|
|
7
|
+
|
|
8
|
+
export interface StopToolManager {
|
|
9
|
+
stop(id: string, settlementTimeoutSeconds?: number, waitSignal?: AbortSignal): Promise<StopOutcome>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class StopTool {
|
|
13
|
+
constructor(private readonly manager: StopToolManager) {}
|
|
14
|
+
|
|
15
|
+
async execute(_toolCallId: string, params: { agent_id: string; settlement_timeout_seconds?: number }, signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: unknown) {
|
|
16
|
+
const timeout = params.settlement_timeout_seconds ?? 5;
|
|
17
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 30) return textResult("settlement_timeout_seconds must be an integer from 1 to 30");
|
|
18
|
+
const outcome = await this.manager.stop(params.agent_id, timeout, signal);
|
|
19
|
+
if (outcome.kind === "not_found") return textResult(`Agent not found: "${outcome.agentId}". It may have been cleaned up.`, { kind: outcome.kind, agentId: outcome.agentId });
|
|
20
|
+
if (outcome.kind === "already_terminal") return textResult(`Agent ${outcome.agentId} is already terminal (${outcome.record.status}, ${outcome.record.stateTerminalReason ?? "unknown"}).`, renderDetails(outcome.record));
|
|
21
|
+
if (outcome.kind === "stop_pending") return textResult(`Stop requested for agent ${outcome.agentId}, but its run has not settled within ${timeout}s. It remains active; no hard kill was attempted.\nRun ID: ${outcome.runId}\nReason: ${outcome.reason}`, renderDetails(outcome.record));
|
|
22
|
+
return textResult(`Agent ${outcome.agentId} stopped.\nRun ID: ${outcome.runId}\nReason: ${outcome.reason}\nRuntime: ${formatMs(outcome.record.activeRuntimeMs)}`, renderDetails(outcome.record));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toToolDefinition() {
|
|
26
|
+
return defineTool({
|
|
27
|
+
name: "stop_subagent" as const,
|
|
28
|
+
label: "Stop Subagent",
|
|
29
|
+
promptSnippet: "stop_subagent: Request cooperative cancellation of a subagent.",
|
|
30
|
+
description: "Stop queued work immediately or request cooperative cancellation of a live subagent. The result distinguishes settled, already-terminal, not-found, and stop-pending outcomes; it never hard-kills JavaScript.",
|
|
31
|
+
parameters: Type.Object({
|
|
32
|
+
agent_id: Type.String({ description: "The subagent ID to stop." }),
|
|
33
|
+
settlement_timeout_seconds: Type.Optional(Type.Integer({ description: "How long to wait for cooperative settlement (1-30 seconds, default 5).", minimum: 1, maximum: 30 })),
|
|
34
|
+
}),
|
|
35
|
+
execute: (toolCallId: string, params: { agent_id: string; settlement_timeout_seconds?: number }, signal: AbortSignal | undefined, onUpdate: unknown, ctx: unknown) => this.execute(toolCallId, params, signal, onUpdate, ctx),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderDetails(record: Subagent) {
|
|
41
|
+
return buildDetails({
|
|
42
|
+
displayName: record.type,
|
|
43
|
+
description: record.description,
|
|
44
|
+
subagentType: record.type,
|
|
45
|
+
modelName: record.modelLabel,
|
|
46
|
+
thinkingLevel: record.effectiveThinkingLevel,
|
|
47
|
+
tags: [`mode: ${record.mode}`],
|
|
48
|
+
}, record, {
|
|
49
|
+
terminalReason: record.stateTerminalReason,
|
|
50
|
+
tokens: formatLifetimeTokens(record),
|
|
51
|
+
durationMs: record.activeRuntimeMs,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -33,6 +33,9 @@ export interface SubscribableSession {
|
|
|
33
33
|
/** Agent type: any string name (built-in defaults or user-defined). */
|
|
34
34
|
export type SubagentType = string;
|
|
35
35
|
|
|
36
|
+
/** Delivery mode for one initial or resumed run. */
|
|
37
|
+
export type SubagentMode = "joined" | "detached";
|
|
38
|
+
|
|
36
39
|
/** UI display and agent listing — name, display name, description, prompt mode. */
|
|
37
40
|
export interface AgentIdentity {
|
|
38
41
|
name: string;
|
|
@@ -54,10 +57,12 @@ export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
|
|
|
54
57
|
model?: string;
|
|
55
58
|
thinking?: ThinkingLevel;
|
|
56
59
|
maxTurns?: number;
|
|
57
|
-
/**
|
|
60
|
+
/** Active runtime deadline in seconds for launch/resume. */
|
|
61
|
+
timeoutSeconds?: number;
|
|
62
|
+
/** Default for launch/resume: fork parent conversation. undefined = caller decides. */
|
|
58
63
|
inheritContext?: boolean;
|
|
59
|
-
/** Default
|
|
60
|
-
|
|
64
|
+
/** Default delivery mode for launch/resume. */
|
|
65
|
+
mode?: SubagentMode;
|
|
61
66
|
/** One-line usage guideline for the subagent tool's Guidelines: block. Omitted — no guideline line. */
|
|
62
67
|
toolGuideline?: string;
|
|
63
68
|
/** true = this is an embedded default agent (informational) */
|
|
@@ -74,7 +79,8 @@ export interface AgentInvocation {
|
|
|
74
79
|
thinking?: ThinkingLevel;
|
|
75
80
|
maxTurns?: number;
|
|
76
81
|
inheritContext?: boolean;
|
|
77
|
-
|
|
82
|
+
mode?: SubagentMode;
|
|
83
|
+
timeoutSeconds?: number;
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
/**
|
|
@@ -115,7 +121,7 @@ export interface ParentSessionInfo {
|
|
|
115
121
|
parentSessionFile?: string;
|
|
116
122
|
/** Session ID of the parent agent (stored in the child session's parentSession header). */
|
|
117
123
|
parentSessionId?: string;
|
|
118
|
-
/** Tool call ID for
|
|
124
|
+
/** Tool call ID for detached completion notification wiring. Exposed on the record via Subagent.toolCallId. */
|
|
119
125
|
toolCallId?: string;
|
|
120
126
|
}
|
|
121
127
|
|