@nklisch/pi-enhanced 0.2.7 → 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 +10 -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 +19 -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 +111 -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 +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 -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 +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
package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/helpers.ts
CHANGED
|
@@ -5,12 +5,8 @@ import { type AgentDetails, formatTokens } from "#src/ui/display";
|
|
|
5
5
|
/** Parenthetical status note for completed agent result text. */
|
|
6
6
|
export function getStatusNote(status: string): string {
|
|
7
7
|
switch (status) {
|
|
8
|
-
case "aborted":
|
|
9
|
-
return " (aborted \u2014 max turns exceeded, output may be incomplete)";
|
|
10
|
-
case "steered":
|
|
11
|
-
return " (wrapped up \u2014 reached turn limit)";
|
|
12
8
|
case "stopped":
|
|
13
|
-
return " (stopped
|
|
9
|
+
return " (stopped)";
|
|
14
10
|
default:
|
|
15
11
|
return "";
|
|
16
12
|
}
|
|
@@ -23,7 +19,10 @@ export function buildDetails(
|
|
|
23
19
|
toolUses: number;
|
|
24
20
|
startedAt: number;
|
|
25
21
|
completedAt?: number;
|
|
22
|
+
/** Active wall-clock duration for the current run lease. */
|
|
23
|
+
activeRuntimeMs?: number;
|
|
26
24
|
status: string;
|
|
25
|
+
terminalReason?: import("#src/lifecycle/subagent-state").SubagentTerminalReason;
|
|
27
26
|
error?: string;
|
|
28
27
|
id?: string;
|
|
29
28
|
lifetimeUsage: LifetimeUsage;
|
|
@@ -39,8 +38,12 @@ export function buildDetails(
|
|
|
39
38
|
tokens: formatLifetimeTokens(record),
|
|
40
39
|
turnCount: record.turnCount,
|
|
41
40
|
maxTurns: record.maxTurns,
|
|
42
|
-
|
|
41
|
+
// Do not derive runtime from record timestamps: startedAt may represent a
|
|
42
|
+
// prior lease and completedAt includes queue/resume waiting time. The
|
|
43
|
+
// lifecycle record owns the active-runtime measurement.
|
|
44
|
+
durationMs: record.activeRuntimeMs ?? 0,
|
|
43
45
|
status: record.status as AgentDetails["status"],
|
|
46
|
+
terminalReason: record.terminalReason,
|
|
44
47
|
agentId: record.id,
|
|
45
48
|
error: record.error,
|
|
46
49
|
...overrides,
|
package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/list-tool.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { toSubagentRecord } from "#src/service/service-adapter";
|
|
4
|
+
import { textResult } from "#src/tools/helpers";
|
|
5
|
+
|
|
6
|
+
export interface ListToolManager {
|
|
7
|
+
listAgents(): import("#src/lifecycle/subagent").Subagent[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ListTool {
|
|
11
|
+
constructor(private readonly manager: ListToolManager) {}
|
|
12
|
+
|
|
13
|
+
async execute(_toolCallId: string, params: { state?: "active" | "terminal" | "all"; limit?: number }, _signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: unknown) {
|
|
14
|
+
const state = params.state ?? "all";
|
|
15
|
+
const limit = params.limit ?? 20;
|
|
16
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) return textResult("limit must be an integer from 1 to 100");
|
|
17
|
+
const records = this.manager.listAgents()
|
|
18
|
+
.filter((record) => state === "all" || (state === "active" ? record.isActive() : !record.isActive()))
|
|
19
|
+
.slice(0, limit)
|
|
20
|
+
.map(toSubagentRecord);
|
|
21
|
+
const lines = records.length === 0
|
|
22
|
+
? ["No subagents match the requested state."]
|
|
23
|
+
: records.map((record) => [
|
|
24
|
+
`${record.id} run=${record.runId} ${record.mode} ${record.status}${record.stopRequested ? " stop_requested" : ""}`,
|
|
25
|
+
` ${record.modelLabel} · thinking: ${record.thinkingLevel} · ${record.activeRuntimeMs}ms${record.status === "queued" || record.status === "running" ? ` · ${record.currentActivity.slice(0, 160)}` : ""}`,
|
|
26
|
+
record.terminalReason ? ` reason=${record.terminalReason}` : undefined,
|
|
27
|
+
record.activeTools.length > 0 ? ` tools=${record.activeTools.join(",")}` : undefined,
|
|
28
|
+
` ${record.description}`,
|
|
29
|
+
].filter((line): line is string => line !== undefined).join("\n"));
|
|
30
|
+
return textResult(lines.join("\n"), records);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
toToolDefinition() {
|
|
34
|
+
return defineTool({
|
|
35
|
+
name: "list_subagents" as const,
|
|
36
|
+
label: "List Subagents",
|
|
37
|
+
promptSnippet: "list_subagents: Inspect bounded subagent fleet state.",
|
|
38
|
+
description: "List a bounded newest-first summary of active, terminal, or all subagents, including run identity, delivery mode, exact model/thinking, active runtime, live activity, tools, and terminal reason.",
|
|
39
|
+
parameters: Type.Object({
|
|
40
|
+
state: Type.Optional(Type.Union([Type.Literal("active"), Type.Literal("terminal"), Type.Literal("all")], { description: "Which records to include. Defaults to all." })),
|
|
41
|
+
limit: Type.Optional(Type.Integer({ description: "Maximum records, 1-100, default 20.", minimum: 1, maximum: 100 })),
|
|
42
|
+
}),
|
|
43
|
+
execute: (toolCallId: string, params: { state?: "active" | "terminal" | "all"; limit?: number }, signal: AbortSignal | undefined, onUpdate: unknown, ctx: unknown) => this.execute(toolCallId, params, signal, onUpdate, ctx),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single source of truth for tools that belong to the parent orchestrator.
|
|
3
|
+
* Children inherit ordinary extension registrations, but these controls are
|
|
4
|
+
* removed to prevent recursive orchestration.
|
|
5
|
+
*/
|
|
6
|
+
export const PARENT_ONLY_TOOL_NAMES = [
|
|
7
|
+
"subagent",
|
|
8
|
+
"resume_subagent",
|
|
9
|
+
"stop_subagent",
|
|
10
|
+
"steer_subagent",
|
|
11
|
+
"list_subagents",
|
|
12
|
+
"get_subagent_result",
|
|
13
|
+
"query_subagent_session",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type ParentOnlyToolName = typeof PARENT_ONLY_TOOL_NAMES[number];
|
|
17
|
+
export const PARENT_ONLY_TOOL_SET: ReadonlySet<string> = new Set(PARENT_ONLY_TOOL_NAMES);
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { Subagent } from "#src/lifecycle/subagent";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_QUERY_LIMIT,
|
|
6
|
+
MAX_QUERY_LIMIT,
|
|
7
|
+
MIN_QUERY_LIMIT,
|
|
8
|
+
querySession,
|
|
9
|
+
type SessionQueryEntry,
|
|
10
|
+
type SessionQueryKind,
|
|
11
|
+
type SessionQueryOrder,
|
|
12
|
+
} from "#src/session/query";
|
|
13
|
+
import { readSessionFileMessages } from "#src/session/query-source";
|
|
14
|
+
import { textResult } from "#src/tools/helpers";
|
|
15
|
+
import { describeActivity, formatMs, formatModelThinking } from "#src/ui/display";
|
|
16
|
+
|
|
17
|
+
const MAX_QUERY_OUTPUT_BYTES = 50 * 1024;
|
|
18
|
+
const MAX_QUERY_OUTPUT_LINES = 2_000;
|
|
19
|
+
// Leave room for the summary, source path, omission tail, and a final line/
|
|
20
|
+
// byte boundary. The entry budget is intentionally smaller than Pi's limits;
|
|
21
|
+
// entries are additionally checked against the actual fixed text below.
|
|
22
|
+
const MAX_QUERY_ENTRY_BYTES = 40 * 1024;
|
|
23
|
+
const MAX_QUERY_ENTRY_LINES = 1_800;
|
|
24
|
+
const MAX_QUERY_METADATA_LINE_BYTES = 2_048;
|
|
25
|
+
const MAX_QUERY_TRANSCRIPT_PATH_BYTES = 1_024;
|
|
26
|
+
const utf8Encoder = new TextEncoder();
|
|
27
|
+
const VALID_KINDS = new Set<SessionQueryKind>(["all", "messages", "tool_calls", "tool_results"]);
|
|
28
|
+
const VALID_ORDERS = new Set<SessionQueryOrder>(["newest", "oldest"]);
|
|
29
|
+
|
|
30
|
+
export type QuerySessionOutcome = "matches" | "no_matches" | "transcript_unavailable" | "not_found" | "read_error";
|
|
31
|
+
|
|
32
|
+
export interface QuerySessionDetails {
|
|
33
|
+
readonly outcome: QuerySessionOutcome;
|
|
34
|
+
readonly agentId: string;
|
|
35
|
+
readonly runId?: number;
|
|
36
|
+
readonly mode?: string;
|
|
37
|
+
readonly status?: string;
|
|
38
|
+
readonly terminalReason?: string;
|
|
39
|
+
readonly model?: string;
|
|
40
|
+
readonly thinkingLevel?: Subagent["effectiveThinkingLevel"];
|
|
41
|
+
readonly activeRuntimeMs?: number;
|
|
42
|
+
readonly activity?: string;
|
|
43
|
+
readonly source?: "live" | "file";
|
|
44
|
+
readonly transcriptPath?: string;
|
|
45
|
+
readonly query?: string;
|
|
46
|
+
readonly kind?: SessionQueryKind;
|
|
47
|
+
readonly order?: SessionQueryOrder;
|
|
48
|
+
readonly limit?: number;
|
|
49
|
+
readonly entries?: readonly SessionQueryEntry[];
|
|
50
|
+
readonly totalMatches?: number;
|
|
51
|
+
readonly hasMore?: boolean;
|
|
52
|
+
readonly omittedCount?: number;
|
|
53
|
+
readonly truncation?: {
|
|
54
|
+
readonly output: boolean;
|
|
55
|
+
readonly omittedMatches: number;
|
|
56
|
+
readonly transcriptPath?: string;
|
|
57
|
+
};
|
|
58
|
+
readonly error?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface QuerySessionToolManager {
|
|
62
|
+
getRecord(id: string): Subagent | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type QuerySessionFileReader = (path: string) => string;
|
|
66
|
+
|
|
67
|
+
/** Parent-only bounded transcript drill-down. */
|
|
68
|
+
export class QuerySessionTool {
|
|
69
|
+
constructor(
|
|
70
|
+
private readonly manager: QuerySessionToolManager,
|
|
71
|
+
private readonly readFile: QuerySessionFileReader,
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
async execute(
|
|
75
|
+
_toolCallId: string,
|
|
76
|
+
params: {
|
|
77
|
+
agent_id?: string;
|
|
78
|
+
query?: string;
|
|
79
|
+
kind?: SessionQueryKind;
|
|
80
|
+
order?: SessionQueryOrder;
|
|
81
|
+
limit?: number;
|
|
82
|
+
},
|
|
83
|
+
_signal: AbortSignal | undefined,
|
|
84
|
+
_onUpdate: unknown,
|
|
85
|
+
_ctx: unknown,
|
|
86
|
+
) {
|
|
87
|
+
const validation = validateParams(params);
|
|
88
|
+
if (validation) return textResult(validation);
|
|
89
|
+
|
|
90
|
+
const agentId = params.agent_id!;
|
|
91
|
+
const record = this.manager.getRecord(agentId);
|
|
92
|
+
if (!record) {
|
|
93
|
+
return textResult(`Agent not found: "${boundedOutputLine(agentId, MAX_QUERY_METADATA_LINE_BYTES)}". It may have been cleaned up.`, {
|
|
94
|
+
outcome: "not_found",
|
|
95
|
+
agentId,
|
|
96
|
+
} satisfies QuerySessionDetails);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const base = recordDetails(record, agentId);
|
|
100
|
+
let source: "live" | "file";
|
|
101
|
+
let messages;
|
|
102
|
+
try {
|
|
103
|
+
if (record.isSessionReady()) {
|
|
104
|
+
source = "live";
|
|
105
|
+
messages = record.agentMessages;
|
|
106
|
+
} else if (record.outputFile) {
|
|
107
|
+
source = "file";
|
|
108
|
+
messages = readSessionFileMessages(record.outputFile, this.readFile);
|
|
109
|
+
} else {
|
|
110
|
+
return textResult(
|
|
111
|
+
`${boundedOutputLine(summaryLine(base), MAX_QUERY_METADATA_LINE_BYTES)}\nTranscript unavailable: no retained live session or persisted transcript is available.`,
|
|
112
|
+
{ ...base, outcome: "transcript_unavailable" } satisfies QuerySessionDetails,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (sourceIsMissingFile(record, error)) {
|
|
117
|
+
return textResult(
|
|
118
|
+
`${boundedOutputLine(summaryLine(base), MAX_QUERY_METADATA_LINE_BYTES)}\nTranscript unavailable: ${boundedOutputLine(record.outputFile ?? "", MAX_QUERY_TRANSCRIPT_PATH_BYTES)}.`,
|
|
119
|
+
{ ...base, outcome: "transcript_unavailable", source: "file", transcriptPath: record.outputFile } satisfies QuerySessionDetails,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
return textResult(
|
|
124
|
+
`${boundedOutputLine(summaryLine(base), MAX_QUERY_METADATA_LINE_BYTES)}\nCould not read the subagent transcript: ${boundedOutputLine(message, MAX_QUERY_METADATA_LINE_BYTES)}`,
|
|
125
|
+
{ ...base, outcome: "read_error", source: record.isSessionReady() ? "live" : "file", transcriptPath: record.outputFile, error: message } satisfies QuerySessionDetails,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const result = querySession(messages, {
|
|
130
|
+
query: params.query,
|
|
131
|
+
kind: params.kind,
|
|
132
|
+
order: params.order,
|
|
133
|
+
limit: params.limit,
|
|
134
|
+
});
|
|
135
|
+
const summary = boundedOutputLine(summaryLine(base), MAX_QUERY_METADATA_LINE_BYTES);
|
|
136
|
+
const sourceDescription = boundedOutputLine(
|
|
137
|
+
`${source}${record.outputFile ? ` (${record.outputFile})` : ""}`,
|
|
138
|
+
MAX_QUERY_METADATA_LINE_BYTES,
|
|
139
|
+
);
|
|
140
|
+
const prefix = `${summary}\nSource: ${sourceDescription}\n`;
|
|
141
|
+
// Reserve the largest possible omission tail before selecting entries. This
|
|
142
|
+
// prevents a later bounds notice from pushing an otherwise-fitting body
|
|
143
|
+
// over Pi's byte or line ceiling.
|
|
144
|
+
const maximumOmittedCount = result.omittedCount + result.entries.length;
|
|
145
|
+
const reservedTail = omissionTail(maximumOmittedCount, record.outputFile);
|
|
146
|
+
const output = formatEntries(
|
|
147
|
+
result.entries,
|
|
148
|
+
Math.min(
|
|
149
|
+
MAX_QUERY_ENTRY_BYTES,
|
|
150
|
+
Math.max(0, MAX_QUERY_OUTPUT_BYTES - utf8ByteLength(prefix) - utf8ByteLength(reservedTail) - 1),
|
|
151
|
+
),
|
|
152
|
+
Math.min(
|
|
153
|
+
MAX_QUERY_ENTRY_LINES,
|
|
154
|
+
Math.max(0, MAX_QUERY_OUTPUT_LINES - lineCount(prefix) - lineCount(reservedTail) - 1),
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
const omittedCount = result.omittedCount + output.omittedCount;
|
|
158
|
+
const outcome: QuerySessionOutcome = result.entries.length > 0 ? "matches" : "no_matches";
|
|
159
|
+
const details: QuerySessionDetails = {
|
|
160
|
+
...base,
|
|
161
|
+
outcome,
|
|
162
|
+
source,
|
|
163
|
+
transcriptPath: record.outputFile,
|
|
164
|
+
query: params.query ?? "",
|
|
165
|
+
kind: params.kind ?? "all",
|
|
166
|
+
order: params.order ?? "newest",
|
|
167
|
+
limit: params.limit ?? DEFAULT_QUERY_LIMIT,
|
|
168
|
+
entries: output.entries,
|
|
169
|
+
totalMatches: result.totalMatches,
|
|
170
|
+
hasMore: omittedCount > 0,
|
|
171
|
+
omittedCount,
|
|
172
|
+
...(omittedCount > 0 ? {
|
|
173
|
+
truncation: {
|
|
174
|
+
output: output.omittedCount > 0,
|
|
175
|
+
omittedMatches: omittedCount,
|
|
176
|
+
...(record.outputFile ? { transcriptPath: record.outputFile } : {}),
|
|
177
|
+
},
|
|
178
|
+
} : {}),
|
|
179
|
+
};
|
|
180
|
+
const body = result.entries.length === 0
|
|
181
|
+
? "No transcript entries match the requested query."
|
|
182
|
+
: output.lines.join("\n");
|
|
183
|
+
const tail = omissionTail(omittedCount, record.outputFile);
|
|
184
|
+
return textResult(`${prefix}${body}${tail}`, details);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
toToolDefinition() {
|
|
188
|
+
return defineTool({
|
|
189
|
+
name: "query_subagent_session" as const,
|
|
190
|
+
label: "Query Subagent Session",
|
|
191
|
+
promptSnippet: "query_subagent_session: Search a bounded child transcript without steering it.",
|
|
192
|
+
description: "Query a subagent's recent or matching messages and tool calls. Search is case-insensitive literal text, bounded, and read-only; use get_subagent_result for the final result and list_subagents for fleet triage.",
|
|
193
|
+
parameters: Type.Object({
|
|
194
|
+
agent_id: Type.String({ description: "The subagent ID to inspect." }),
|
|
195
|
+
query: Type.Optional(Type.String({ description: "Case-insensitive literal text. Omit or use an empty string for recent entries." })),
|
|
196
|
+
kind: Type.Optional(Type.Union([
|
|
197
|
+
Type.Literal("all"),
|
|
198
|
+
Type.Literal("messages"),
|
|
199
|
+
Type.Literal("tool_calls"),
|
|
200
|
+
Type.Literal("tool_results"),
|
|
201
|
+
], { description: "Search scope. Defaults to all." })),
|
|
202
|
+
order: Type.Optional(Type.Union([
|
|
203
|
+
Type.Literal("newest"),
|
|
204
|
+
Type.Literal("oldest"),
|
|
205
|
+
], { description: "Result order. Defaults to newest." })),
|
|
206
|
+
limit: Type.Optional(Type.Integer({ minimum: MIN_QUERY_LIMIT, maximum: MAX_QUERY_LIMIT, description: "Maximum entries, 1-50; defaults to 20." })),
|
|
207
|
+
}),
|
|
208
|
+
execute: (toolCallId: string, params: {
|
|
209
|
+
agent_id?: string;
|
|
210
|
+
query?: string;
|
|
211
|
+
kind?: SessionQueryKind;
|
|
212
|
+
order?: SessionQueryOrder;
|
|
213
|
+
limit?: number;
|
|
214
|
+
}, signal: AbortSignal | undefined, onUpdate: unknown, ctx: unknown) => this.execute(toolCallId, params, signal, onUpdate, ctx),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function validateParams(params: {
|
|
220
|
+
agent_id?: string;
|
|
221
|
+
query?: string;
|
|
222
|
+
kind?: SessionQueryKind;
|
|
223
|
+
order?: SessionQueryOrder;
|
|
224
|
+
limit?: number;
|
|
225
|
+
}): string | undefined {
|
|
226
|
+
if (typeof params.agent_id !== "string" || params.agent_id.length === 0) return "agent_id must be a non-empty string";
|
|
227
|
+
if (params.query !== undefined && typeof params.query !== "string") return "query must be a string";
|
|
228
|
+
if (params.kind !== undefined && !VALID_KINDS.has(params.kind)) return "kind must be all, messages, tool_calls, or tool_results";
|
|
229
|
+
if (params.order !== undefined && !VALID_ORDERS.has(params.order)) return "order must be newest or oldest";
|
|
230
|
+
if (params.limit !== undefined && (!Number.isInteger(params.limit) || params.limit < MIN_QUERY_LIMIT || params.limit > MAX_QUERY_LIMIT)) {
|
|
231
|
+
return `limit must be an integer from ${MIN_QUERY_LIMIT} to ${MAX_QUERY_LIMIT}`;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function recordDetails(record: Subagent, agentId: string): QuerySessionDetails {
|
|
237
|
+
return {
|
|
238
|
+
outcome: "read_error",
|
|
239
|
+
agentId,
|
|
240
|
+
runId: record.runId,
|
|
241
|
+
mode: record.mode,
|
|
242
|
+
status: record.status,
|
|
243
|
+
terminalReason: record.stateTerminalReason,
|
|
244
|
+
model: record.modelLabel,
|
|
245
|
+
thinkingLevel: record.effectiveThinkingLevel,
|
|
246
|
+
activeRuntimeMs: record.activeRuntimeMs,
|
|
247
|
+
activity: describeActivity(record.activeTools, record.responseText),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function summaryLine(details: QuerySessionDetails): string {
|
|
252
|
+
const model = details.model && details.thinkingLevel ? ` · ${formatModelThinking(details.model, details.thinkingLevel)}` : "";
|
|
253
|
+
const status = details.status ? ` · ${details.status}` : "";
|
|
254
|
+
const activity = details.activity ? ` · ${details.activity}` : "";
|
|
255
|
+
return `Agent ${details.agentId} run=${details.runId ?? "?"}${status}${model} · ${formatMs(details.activeRuntimeMs ?? 0)}${activity}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function sourceIsMissingFile(record: Subagent, error: unknown): boolean {
|
|
259
|
+
return !record.isSessionReady() && (error as { code?: unknown } | undefined)?.code === "ENOENT";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatEntries(
|
|
263
|
+
entries: readonly SessionQueryEntry[],
|
|
264
|
+
maxBytes = MAX_QUERY_ENTRY_BYTES,
|
|
265
|
+
maxLines = MAX_QUERY_ENTRY_LINES,
|
|
266
|
+
): {
|
|
267
|
+
entries: SessionQueryEntry[];
|
|
268
|
+
lines: string[];
|
|
269
|
+
omittedCount: number;
|
|
270
|
+
} {
|
|
271
|
+
const selected: SessionQueryEntry[] = [];
|
|
272
|
+
const lines: string[] = [];
|
|
273
|
+
let bytes = 0;
|
|
274
|
+
let lineTotal = 0;
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
const line = formatEntry(entry);
|
|
277
|
+
const addition = selected.length === 0 ? line : `\n${line}`;
|
|
278
|
+
const additionBytes = utf8ByteLength(addition);
|
|
279
|
+
const additionLines = lineCount(addition);
|
|
280
|
+
if (bytes + additionBytes > maxBytes || lineTotal + additionLines > maxLines) break;
|
|
281
|
+
selected.push(entry);
|
|
282
|
+
lines.push(line);
|
|
283
|
+
bytes += additionBytes;
|
|
284
|
+
lineTotal += additionLines;
|
|
285
|
+
}
|
|
286
|
+
return { entries: selected, lines, omittedCount: entries.length - selected.length };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function formatEntry(entry: SessionQueryEntry): string {
|
|
290
|
+
if (entry.kind === "message") {
|
|
291
|
+
return `[${entry.role}] ${entry.id}\n${entry.text}`;
|
|
292
|
+
}
|
|
293
|
+
const callId = entry.toolCallId ? ` id=${entry.toolCallId}` : "";
|
|
294
|
+
const result = entry.result === undefined ? "" : `\nresult: ${entry.result}`;
|
|
295
|
+
return `[tool ${entry.toolName}${callId} · ${entry.state}] ${entry.id}\narguments: ${entry.arguments}${result}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function utf8ByteLength(value: string): number {
|
|
299
|
+
return utf8Encoder.encode(value).byteLength;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Count lines conservatively, including the empty line after a trailing newline. */
|
|
303
|
+
function lineCount(value: string): number {
|
|
304
|
+
return value.length === 0 ? 0 : value.split(/\r\n|\r|\n/).length;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Keep metadata single-line and bounded; full paths remain available in details. */
|
|
308
|
+
function boundedOutputLine(value: string, maxBytes: number): string {
|
|
309
|
+
const singleLine = value.replace(/\r\n|\r|\n/g, " ");
|
|
310
|
+
if (utf8ByteLength(singleLine) <= maxBytes) return singleLine;
|
|
311
|
+
const ellipsis = "…";
|
|
312
|
+
const budget = Math.max(0, maxBytes - utf8ByteLength(ellipsis));
|
|
313
|
+
let output = "";
|
|
314
|
+
let bytes = 0;
|
|
315
|
+
for (const character of singleLine) {
|
|
316
|
+
const characterBytes = utf8ByteLength(character);
|
|
317
|
+
if (bytes + characterBytes > budget) break;
|
|
318
|
+
output += character;
|
|
319
|
+
bytes += characterBytes;
|
|
320
|
+
}
|
|
321
|
+
return `${output}${ellipsis}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function omissionTail(omittedCount: number, transcriptPath: string | undefined): string {
|
|
325
|
+
if (omittedCount <= 0) return "";
|
|
326
|
+
const path = transcriptPath
|
|
327
|
+
? ` Full transcript: ${boundedOutputLine(transcriptPath, MAX_QUERY_TRANSCRIPT_PATH_BYTES)}`
|
|
328
|
+
: "";
|
|
329
|
+
return `\n\n${omittedCount} matching entr${omittedCount === 1 ? "y was" : "ies were"} omitted by the bounds.${path}`;
|
|
330
|
+
}
|
|
@@ -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
|
}
|