@mystilleef/pi-subagent 0.10.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -10
- package/package.json +8 -8
- package/src/agent/agents.ts +75 -9
- package/src/child/complete-extension.ts +36 -0
- package/src/child/complete-outcome.ts +35 -0
- package/src/child/model-resolution.ts +81 -0
- package/src/child/process-utils.ts +114 -0
- package/src/child/process.ts +225 -471
- package/src/child/prompt-contract.ts +22 -10
- package/src/child/prompt-setup.ts +36 -0
- package/src/child/result-builder.ts +142 -0
- package/src/child/sampling-extension.ts +49 -0
- package/src/child/streaming-progress.ts +138 -0
- package/src/orchestration/subagent-orchestrator.ts +20 -16
- package/src/output/normalize.ts +1 -1
- package/src/output/summary.ts +22 -6
- package/src/output/ui.ts +19 -21
- package/src/progress/progress-state.ts +10 -12
- package/src/progress/result-details.ts +54 -45
- package/src/shared/limits.ts +81 -0
- package/src/shared/message-utils.ts +56 -0
- package/src/shared/resource-resolution.ts +289 -0
- package/src/shared/sampling.ts +56 -0
- package/src/shared/types.ts +1 -0
- package/src/shared/utils.ts +31 -204
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
export const SUBAGENT_RESULT_CONTRACT = `
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Don't present outcome line in the main agent's response.
|
|
8
|
-
`;
|
|
2
|
+
## Subagent Result Contract
|
|
3
|
+
|
|
4
|
+
**MANDATORY**: Emit the task result upon completion.
|
|
5
|
+
|
|
6
|
+
### Directives
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
- Write the complete task result as assistant text — mandatory, non-empty, and trimmed,
|
|
9
|
+
before calling \`complete\`.
|
|
10
|
+
- Call \`complete\` as the final action after the result text; \`outcome\` must be a short,
|
|
11
|
+
one-sentence progress summary only, not the full result.
|
|
12
|
+
- This contract applies after any amount of tool use, reading, editing, or multi-step work.
|
|
13
|
+
|
|
14
|
+
### Constraints
|
|
15
|
+
|
|
16
|
+
- **NEVER** omit the result text response.
|
|
17
|
+
- **NEVER** emit whitespace-only, empty-string, or blank result text before \`complete\`.
|
|
18
|
+
- **NEVER** wrap the entire result in a code block or code fence.
|
|
19
|
+
- **NEVER** end the assistant response with text alone after tool calls; a terminal
|
|
20
|
+
\`complete\` call is required.
|
|
21
|
+
- **NEVER** write assistant text after \`complete\`.
|
|
22
|
+
- Call \`complete\` exactly once; multiple \`complete\` calls are not allowed.
|
|
23
|
+
- \`outcome\` must be concise and contain only a progress summary.
|
|
24
|
+
`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { AgentConfig } from "../agent/agents.js";
|
|
3
|
+
import { writePromptToTempFile } from "../shared/utils.js";
|
|
4
|
+
|
|
5
|
+
export type TempPrompt = { dir: string; filePath: string };
|
|
6
|
+
|
|
7
|
+
export type PromptSetupResult =
|
|
8
|
+
| { tmpPrompt: TempPrompt | null }
|
|
9
|
+
| { error: unknown };
|
|
10
|
+
|
|
11
|
+
export async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
|
|
12
|
+
try {
|
|
13
|
+
await fs.promises.unlink(tmpPrompt.filePath);
|
|
14
|
+
await fs.promises.rmdir(tmpPrompt.dir);
|
|
15
|
+
} catch {
|
|
16
|
+
/* temp file cleanup failures are non-fatal; OS will clean up eventually */
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function beginPromptSetup(
|
|
21
|
+
agent: AgentConfig,
|
|
22
|
+
): Promise<PromptSetupResult> {
|
|
23
|
+
if (!agent.systemPrompt.trim()) return Promise.resolve({ tmpPrompt: null });
|
|
24
|
+
return writePromptToTempFile(agent.name, agent.systemPrompt).then(
|
|
25
|
+
(tmpPrompt) => ({ tmpPrompt }),
|
|
26
|
+
(error: unknown) => ({ error }),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function cleanupPromptSetupResult(
|
|
31
|
+
setup: PromptSetupResult,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
if ("tmpPrompt" in setup && setup.tmpPrompt) {
|
|
34
|
+
await cleanupTempPrompt(setup.tmpPrompt);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AgentConfig } from "../agent/agents.js";
|
|
3
|
+
import {
|
|
4
|
+
type SingleResult,
|
|
5
|
+
TOOL_RESULT_FAILED_MESSAGE,
|
|
6
|
+
} from "../shared/types.js";
|
|
7
|
+
import {
|
|
8
|
+
extractFinalOutputFromMessages,
|
|
9
|
+
truncateOutput,
|
|
10
|
+
} from "../shared/utils.js";
|
|
11
|
+
import { resolveContextWindowTokens } from "./process-utils.js";
|
|
12
|
+
|
|
13
|
+
export type RuntimeResult = SingleResult & { messages: Message[] };
|
|
14
|
+
|
|
15
|
+
const EMPTY_USAGE = {
|
|
16
|
+
input: 0,
|
|
17
|
+
output: 0,
|
|
18
|
+
cacheRead: 0,
|
|
19
|
+
cacheWrite: 0,
|
|
20
|
+
cost: 0,
|
|
21
|
+
contextTokens: 0,
|
|
22
|
+
turns: 0,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function initRuntimeResult(
|
|
26
|
+
agentName: string,
|
|
27
|
+
source: "user" | "project" | "unknown",
|
|
28
|
+
task: string,
|
|
29
|
+
modelDisplay: string | undefined,
|
|
30
|
+
): RuntimeResult {
|
|
31
|
+
return {
|
|
32
|
+
agent: agentName,
|
|
33
|
+
agentSource: source,
|
|
34
|
+
task,
|
|
35
|
+
exitCode: 0,
|
|
36
|
+
finalOutput: "",
|
|
37
|
+
messages: [],
|
|
38
|
+
stderr: "",
|
|
39
|
+
usage: { ...EMPTY_USAGE },
|
|
40
|
+
model: modelDisplay,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function accumulateUsage(result: RuntimeResult, msg: Message): void {
|
|
45
|
+
if (msg.role !== "assistant") return;
|
|
46
|
+
result.usage.turns++;
|
|
47
|
+
const { usage } = msg;
|
|
48
|
+
if (!usage) return;
|
|
49
|
+
result.usage.input += usage.input || 0;
|
|
50
|
+
result.usage.output += usage.output || 0;
|
|
51
|
+
result.usage.cacheRead += usage.cacheRead || 0;
|
|
52
|
+
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
53
|
+
result.usage.cost += usage.cost?.total || 0;
|
|
54
|
+
result.usage.contextTokens = usage.totalTokens || 0;
|
|
55
|
+
const ctxWindowTokens = resolveContextWindowTokens(msg);
|
|
56
|
+
if (ctxWindowTokens !== undefined)
|
|
57
|
+
result.usage.contextWindowTokens = ctxWindowTokens;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function addMessageToResult(result: RuntimeResult, msg: Message): void {
|
|
61
|
+
result.messages.push(msg);
|
|
62
|
+
result.finalOutput = truncateOutput(
|
|
63
|
+
extractFinalOutputFromMessages(result.messages),
|
|
64
|
+
);
|
|
65
|
+
if (msg.role === "toolResult" && msg.isError) {
|
|
66
|
+
result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
|
|
67
|
+
} else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
|
|
68
|
+
delete result.errorMessage;
|
|
69
|
+
}
|
|
70
|
+
if (msg.role === "assistant") {
|
|
71
|
+
accumulateUsage(result, msg);
|
|
72
|
+
if (!result.model && msg.model) result.model = msg.model;
|
|
73
|
+
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
74
|
+
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function rebuildResultFromMessages(
|
|
79
|
+
result: RuntimeResult,
|
|
80
|
+
messages: Message[],
|
|
81
|
+
): void {
|
|
82
|
+
const { model } = result;
|
|
83
|
+
result.messages = [];
|
|
84
|
+
result.finalOutput = "";
|
|
85
|
+
result.usage = { ...EMPTY_USAGE };
|
|
86
|
+
result.model = model;
|
|
87
|
+
delete result.errorMessage;
|
|
88
|
+
delete result.stopReason;
|
|
89
|
+
for (const msg of messages) {
|
|
90
|
+
addMessageToResult(result, msg);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createErrorResult(
|
|
95
|
+
agent: string,
|
|
96
|
+
source: "user" | "project" | "unknown",
|
|
97
|
+
task: string,
|
|
98
|
+
error: string,
|
|
99
|
+
model?: string,
|
|
100
|
+
): SingleResult {
|
|
101
|
+
return {
|
|
102
|
+
agent,
|
|
103
|
+
agentSource: source,
|
|
104
|
+
task,
|
|
105
|
+
exitCode: 1,
|
|
106
|
+
finalOutput: "",
|
|
107
|
+
stderr: error,
|
|
108
|
+
usage: { ...EMPTY_USAGE },
|
|
109
|
+
model,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function errorForUnknownAgent(
|
|
114
|
+
agentName: string,
|
|
115
|
+
agents: AgentConfig[],
|
|
116
|
+
task: string,
|
|
117
|
+
): SingleResult {
|
|
118
|
+
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
119
|
+
return createErrorResult(
|
|
120
|
+
agentName,
|
|
121
|
+
"unknown",
|
|
122
|
+
task,
|
|
123
|
+
`Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function errorForDepthLimit(
|
|
128
|
+
agentName: string,
|
|
129
|
+
source: "user" | "project" | "unknown",
|
|
130
|
+
task: string,
|
|
131
|
+
depth: number,
|
|
132
|
+
maxDepth: number,
|
|
133
|
+
model?: string,
|
|
134
|
+
): SingleResult {
|
|
135
|
+
return createErrorResult(
|
|
136
|
+
agentName,
|
|
137
|
+
source,
|
|
138
|
+
task,
|
|
139
|
+
`Subagent nesting limit reached (depth ${depth}/${maxDepth}).`,
|
|
140
|
+
model,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
parseSamplingParams,
|
|
4
|
+
type SamplingParams,
|
|
5
|
+
} from "../shared/sampling.js";
|
|
6
|
+
|
|
7
|
+
function applySamplingParams(
|
|
8
|
+
target: Record<string, unknown>,
|
|
9
|
+
params: SamplingParams,
|
|
10
|
+
topPKey: "topP" | "top_p",
|
|
11
|
+
): Record<string, unknown> {
|
|
12
|
+
const result: Record<string, unknown> = { ...target };
|
|
13
|
+
if (params.temperature !== undefined)
|
|
14
|
+
result["temperature"] = params.temperature;
|
|
15
|
+
if (params.topP !== undefined) result[topPKey] = params.topP;
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function patchPayload(
|
|
20
|
+
payload: Record<string, unknown>,
|
|
21
|
+
params: SamplingParams,
|
|
22
|
+
): Record<string, unknown> {
|
|
23
|
+
if ("generationConfig" in payload) {
|
|
24
|
+
const origConfig =
|
|
25
|
+
payload["generationConfig"] &&
|
|
26
|
+
typeof payload["generationConfig"] === "object"
|
|
27
|
+
? (payload["generationConfig"] as Record<string, unknown>)
|
|
28
|
+
: {};
|
|
29
|
+
return {
|
|
30
|
+
...payload,
|
|
31
|
+
generationConfig: applySamplingParams(origConfig, params, "topP"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return applySamplingParams(payload, params, "top_p");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function (pi: ExtensionAPI) {
|
|
38
|
+
const params = parseSamplingParams(process.env["PI_SAMPLING_PARAMS"]);
|
|
39
|
+
if (!params) return;
|
|
40
|
+
pi.on("before_provider_request", (event) => {
|
|
41
|
+
if (
|
|
42
|
+
!event.payload ||
|
|
43
|
+
typeof event.payload !== "object" ||
|
|
44
|
+
Array.isArray(event.payload)
|
|
45
|
+
)
|
|
46
|
+
return;
|
|
47
|
+
return patchPayload(event.payload as Record<string, unknown>, params);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import { makeToolPreview, renderToolActivity } from "../progress/progress.js";
|
|
3
|
+
import { SENSITIVE_PATTERN } from "../progress/progress-format.js";
|
|
4
|
+
import { isToolCallPart } from "../progress/progress-state.js";
|
|
5
|
+
import type {
|
|
6
|
+
OnUpdateCallback,
|
|
7
|
+
StreamingProgress,
|
|
8
|
+
SubagentDetails,
|
|
9
|
+
ToolActivity,
|
|
10
|
+
} from "../shared/types.js";
|
|
11
|
+
import { findLastAssistantTextMessage } from "../shared/utils.js";
|
|
12
|
+
import type { RuntimeResult } from "./result-builder.js";
|
|
13
|
+
|
|
14
|
+
function findRecentMessagesAnchor(messages: Message[]): number {
|
|
15
|
+
return findLastAssistantTextMessage(messages);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sanitizeProgressPreview(preview: string, toolName: string): string {
|
|
19
|
+
return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
23
|
+
const toolCalls: { id: string; preview: string }[] = [];
|
|
24
|
+
let lastToolPreview: string | undefined;
|
|
25
|
+
let activeToolActivity: ToolActivity | undefined;
|
|
26
|
+
for (const msg of messages) {
|
|
27
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
28
|
+
for (const part of msg.content) {
|
|
29
|
+
if (!isToolCallPart(part)) continue;
|
|
30
|
+
const preview = sanitizeProgressPreview(
|
|
31
|
+
makeToolPreview(part.name, part.arguments),
|
|
32
|
+
part.name,
|
|
33
|
+
);
|
|
34
|
+
toolCalls.push({ id: part.id, preview });
|
|
35
|
+
lastToolPreview = preview;
|
|
36
|
+
activeToolActivity = { toolName: part.name, inputSummary: preview };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const activityText = renderToolActivity(activeToolActivity);
|
|
40
|
+
return {
|
|
41
|
+
activeToolActivity,
|
|
42
|
+
activityText,
|
|
43
|
+
toolCalls,
|
|
44
|
+
lastToolPreview,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Merge incoming tool activity with existing progress activity.
|
|
50
|
+
* When tool names match, prefer richer inputSummary from incoming.
|
|
51
|
+
* Otherwise, replace entirely with incoming activity.
|
|
52
|
+
*/
|
|
53
|
+
function mergeToolActivity(
|
|
54
|
+
existing: ToolActivity | undefined,
|
|
55
|
+
incoming: ToolActivity,
|
|
56
|
+
): ToolActivity {
|
|
57
|
+
if (existing && existing.toolName === incoming.toolName) {
|
|
58
|
+
const incomingSummary = incoming.inputSummary;
|
|
59
|
+
const preferIncoming =
|
|
60
|
+
incomingSummary && incomingSummary !== incoming.toolName;
|
|
61
|
+
return {
|
|
62
|
+
...existing,
|
|
63
|
+
inputSummary: preferIncoming ? incomingSummary : existing.inputSummary,
|
|
64
|
+
instanceName: incoming.instanceName ?? existing.instanceName,
|
|
65
|
+
child: incoming.child ?? existing.child,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return incoming;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Apply tool activity and result completion updates to progress state.
|
|
73
|
+
* Handles merging of child events with parent activity tree.
|
|
74
|
+
*/
|
|
75
|
+
function applyActivityUpdates(
|
|
76
|
+
progress: StreamingProgress,
|
|
77
|
+
options: { toolActivity?: ToolActivity; toolResultCompleted?: boolean },
|
|
78
|
+
previousActivity?: ToolActivity,
|
|
79
|
+
): void {
|
|
80
|
+
if (options.toolResultCompleted && previousActivity) {
|
|
81
|
+
progress.activeToolActivity = previousActivity;
|
|
82
|
+
const renderedText = renderToolActivity(previousActivity);
|
|
83
|
+
if (renderedText !== undefined) progress.activityText = renderedText;
|
|
84
|
+
else delete progress.activityText;
|
|
85
|
+
}
|
|
86
|
+
if (options.toolActivity) {
|
|
87
|
+
progress.activeToolActivity = mergeToolActivity(
|
|
88
|
+
progress.activeToolActivity,
|
|
89
|
+
options.toolActivity,
|
|
90
|
+
);
|
|
91
|
+
const renderedActivity = renderToolActivity(progress.activeToolActivity);
|
|
92
|
+
if (renderedActivity !== undefined)
|
|
93
|
+
progress.activityText = renderedActivity;
|
|
94
|
+
else delete progress.activityText;
|
|
95
|
+
}
|
|
96
|
+
if (options.toolResultCompleted) {
|
|
97
|
+
progress.toolResultCompleted = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type EmitUpdateFn = (options?: {
|
|
102
|
+
toolActivity?: ToolActivity;
|
|
103
|
+
toolResultCompleted?: boolean;
|
|
104
|
+
}) => void;
|
|
105
|
+
|
|
106
|
+
export function makeEmitUpdate(
|
|
107
|
+
result: RuntimeResult,
|
|
108
|
+
onUpdate: OnUpdateCallback | undefined,
|
|
109
|
+
makeDetails: (
|
|
110
|
+
results: RuntimeResult[],
|
|
111
|
+
options?: { includeMessages?: boolean; recentMessages?: Message[] },
|
|
112
|
+
) => SubagentDetails,
|
|
113
|
+
): EmitUpdateFn {
|
|
114
|
+
return (options) => {
|
|
115
|
+
const msgs = result.messages;
|
|
116
|
+
const anchorIdx = findRecentMessagesAnchor(msgs);
|
|
117
|
+
const recentMessages =
|
|
118
|
+
anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
|
|
119
|
+
const progress = deriveStreamingProgress(msgs);
|
|
120
|
+
if (options) {
|
|
121
|
+
applyActivityUpdates(
|
|
122
|
+
progress,
|
|
123
|
+
options,
|
|
124
|
+
result.progress?.activeToolActivity,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
result.progress = progress;
|
|
128
|
+
onUpdate?.({
|
|
129
|
+
content: [
|
|
130
|
+
{ type: "text", text: progress.activityText ?? "(running...)" },
|
|
131
|
+
],
|
|
132
|
+
details: makeDetails([result], {
|
|
133
|
+
includeMessages: true,
|
|
134
|
+
recentMessages,
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -11,14 +11,17 @@ import type {
|
|
|
11
11
|
AgentScope,
|
|
12
12
|
ThinkingLevel,
|
|
13
13
|
} from "../agent/agents.js";
|
|
14
|
-
import { runSingleAgent
|
|
14
|
+
import { runSingleAgent } from "../child/process.js";
|
|
15
15
|
import { deliverNotification } from "../notification/delivery.js";
|
|
16
16
|
import {
|
|
17
17
|
buildNotificationRequest,
|
|
18
18
|
isDesktopNotificationsEnabled,
|
|
19
19
|
isPerJobNotificationEnabled,
|
|
20
20
|
} from "../notification/desktop-notification.js";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
formatSubagentFailureForParent,
|
|
23
|
+
formatSubagentResultForParent,
|
|
24
|
+
} from "../output/summary.js";
|
|
22
25
|
import {
|
|
23
26
|
cancelProgressState,
|
|
24
27
|
createProgressState,
|
|
@@ -31,7 +34,6 @@ import {
|
|
|
31
34
|
type DetailsOptions,
|
|
32
35
|
getFeedbackSummaryText,
|
|
33
36
|
getLatestResult,
|
|
34
|
-
getResultDisplayText,
|
|
35
37
|
patchProgressFromDetails,
|
|
36
38
|
sanitizeDetailsForDisplay,
|
|
37
39
|
sanitizeResultDetails,
|
|
@@ -198,9 +200,7 @@ function finishLifecycleFailure(
|
|
|
198
200
|
failProgressState(lc.requestId, errorMessage);
|
|
199
201
|
lc.ctx.ui?.notify(errorMessage, "error");
|
|
200
202
|
const latestResult = getLatestResult(details);
|
|
201
|
-
const content = latestResult
|
|
202
|
-
? formatSubagentResultForParent(latestResult) || "(failed)"
|
|
203
|
-
: errorMessage;
|
|
203
|
+
const content = formatSubagentFailureForParent(errorMessage, latestResult);
|
|
204
204
|
sendSubagentResultMessage(lc.pi, content, displayDetails);
|
|
205
205
|
return createCompletedToolResult(content, displayDetails);
|
|
206
206
|
}
|
|
@@ -220,12 +220,12 @@ function finishLifecycleResult(
|
|
|
220
220
|
const displayDetails = sanitizeDetailsForDisplay(details, lc.debug);
|
|
221
221
|
const content = formatSubagentResultForParent(result) || "(no output)";
|
|
222
222
|
const toolResult = createCompletedToolResult(content, displayDetails);
|
|
223
|
-
finalizeProgressState(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
displayDetails,
|
|
223
|
+
finalizeProgressState(
|
|
224
|
+
lc.requestId,
|
|
225
|
+
getFeedbackSummaryText(toolResult),
|
|
226
|
+
result.outcome,
|
|
228
227
|
);
|
|
228
|
+
sendSubagentResultMessage(lc.pi, content, displayDetails);
|
|
229
229
|
return toolResult;
|
|
230
230
|
}
|
|
231
231
|
|
|
@@ -277,7 +277,7 @@ async function runSubagentLifecycle(
|
|
|
277
277
|
};
|
|
278
278
|
const timerTick = setInterval(requestProgressRender, 500);
|
|
279
279
|
try {
|
|
280
|
-
const
|
|
280
|
+
const outcome = await runSingleAgent(
|
|
281
281
|
lc.ctx.cwd,
|
|
282
282
|
lc.agents,
|
|
283
283
|
lc.agentName,
|
|
@@ -289,14 +289,18 @@ async function runSubagentLifecycle(
|
|
|
289
289
|
lc.parentThinking,
|
|
290
290
|
lc.debug,
|
|
291
291
|
);
|
|
292
|
-
|
|
292
|
+
if (outcome.kind === "aborted") {
|
|
293
|
+
const details = lc.makeDetails([outcome.result]);
|
|
294
|
+
const cancelReason =
|
|
295
|
+
outcome.result.termination?.cancelReason ?? "Aborted";
|
|
296
|
+
return finishLifecycleFailure(lc, cancelReason, details);
|
|
297
|
+
}
|
|
298
|
+
return finishLifecycleResult(lc, outcome.result);
|
|
293
299
|
} catch (error) {
|
|
294
|
-
const abortResult =
|
|
295
|
-
error instanceof SubagentAbortError ? error.result : undefined;
|
|
296
300
|
return finishLifecycleFailure(
|
|
297
301
|
lc,
|
|
298
302
|
error instanceof Error ? error.message : String(error),
|
|
299
|
-
|
|
303
|
+
lc.makeDetails([]),
|
|
300
304
|
);
|
|
301
305
|
} finally {
|
|
302
306
|
clearInterval(timerTick);
|
package/src/output/normalize.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function normalizeTerminalSentence(
|
|
|
58
58
|
"",
|
|
59
59
|
);
|
|
60
60
|
const withoutLabel = withoutStatusPrefix.replace(
|
|
61
|
-
/^\s*(?:status|summary|result|output|message|error|check|
|
|
61
|
+
/^\s*(?:status|summary|result|output|message|error|check|project summary):\s+/i,
|
|
62
62
|
"",
|
|
63
63
|
);
|
|
64
64
|
const normalizedOnce = normalizeSummaryValue(withoutLabel);
|
package/src/output/summary.ts
CHANGED
|
@@ -17,7 +17,7 @@ const FEEDBACK_UI_GENERIC_CANDIDATES = new Set([
|
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
19
|
const FEEDBACK_UI_LABEL_PATTERN =
|
|
20
|
-
/^\s*(
|
|
20
|
+
/^\s*(project summary|result|summary|status|output|message|error|check):\s*/i;
|
|
21
21
|
|
|
22
22
|
export function formatSubagentResultForParent(result: SingleResult): string {
|
|
23
23
|
return result.thinkingWarning
|
|
@@ -25,15 +25,31 @@ export function formatSubagentResultForParent(result: SingleResult): string {
|
|
|
25
25
|
: result.finalOutput;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function
|
|
28
|
+
export function formatSubagentFailureForParent(
|
|
29
|
+
errorMessage: string,
|
|
30
|
+
result?: SingleResult,
|
|
31
|
+
): string {
|
|
32
|
+
if (!result) return errorMessage;
|
|
33
|
+
const formatted = formatSubagentResultForParent(result);
|
|
34
|
+
if (!formatted.trim()) return `(failed) ${errorMessage}`;
|
|
35
|
+
return `(failed) ${errorMessage}\n\n${formatted}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function summarizeFeedbackUiFinalOutput(
|
|
39
|
+
finalOutput: string,
|
|
40
|
+
outcome?: string,
|
|
41
|
+
): string {
|
|
42
|
+
if (outcome?.trim()) {
|
|
43
|
+
return normalizeTerminalSentence(
|
|
44
|
+
outcome,
|
|
45
|
+
FEEDBACK_UI_SUMMARY_MAX_CHARS,
|
|
46
|
+
).toLowerCase();
|
|
47
|
+
}
|
|
29
48
|
const candidates = finalOutput
|
|
30
49
|
.split(/\r?\n|(?<=[.!?])\s+/)
|
|
31
50
|
.map((candidate) => normalizeFeedbackUiSummaryCandidate(candidate))
|
|
32
51
|
.filter(({ text }) => hasSummaryValue(text));
|
|
33
|
-
const selected =
|
|
34
|
-
candidates.find(({ label }) => label === "outcome") ??
|
|
35
|
-
candidates.find(({ label }) => label) ??
|
|
36
|
-
candidates[0];
|
|
52
|
+
const selected = candidates.find(({ label }) => label) ?? candidates[0];
|
|
37
53
|
return (selected?.text ?? "completed task").toLowerCase();
|
|
38
54
|
}
|
|
39
55
|
|
package/src/output/ui.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Message } from "@earendil-works/pi-ai";
|
|
2
1
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import {
|
|
4
3
|
Box,
|
|
@@ -130,19 +129,7 @@ export function formatToolCall(
|
|
|
130
129
|
return themeFg("accent", toolName) + themeFg("dim", ` ${target}`);
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
export
|
|
134
|
-
const lastAsst = messages.findLast((m) => m.role === "assistant");
|
|
135
|
-
const lastText = lastAsst?.content.findLast((p) => p.type === "text");
|
|
136
|
-
return lastText?.type === "text" ? lastText.text : "";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Removes the "Outcome:" line from subagent output to avoid redundancy in result cards.
|
|
141
|
-
*/
|
|
142
|
-
function stripOutcomeLineForResultUi(output: string): string {
|
|
143
|
-
const stripped = output.replace(/^\s*Outcome:[^\r\n]*(?:\r?\n|$)/gim, "");
|
|
144
|
-
return stripped.trim() ? stripped : output;
|
|
145
|
-
}
|
|
132
|
+
export { extractFinalOutputFromMessages as getFinalOutput } from "../shared/utils.js";
|
|
146
133
|
|
|
147
134
|
function makeMarkdownTheme(theme: SubagentTheme): MarkdownTheme {
|
|
148
135
|
const fg = (c: ThemeColor) => (text: string) => theme.fg(c, text);
|
|
@@ -191,7 +178,7 @@ export function renderSubagentToolResult(
|
|
|
191
178
|
return renderSubagentResult(result, theme, display);
|
|
192
179
|
}
|
|
193
180
|
|
|
194
|
-
// Invariants: Red background = failure, green = success.
|
|
181
|
+
// Invariants: Red background = failure, green = success. Shows usage stats + duration in footer.
|
|
195
182
|
export function renderSubagentResult(
|
|
196
183
|
result: { content: { type: string; text?: string }[]; details?: unknown },
|
|
197
184
|
theme: SubagentTheme,
|
|
@@ -215,7 +202,7 @@ export function renderSubagentResult(
|
|
|
215
202
|
: failed
|
|
216
203
|
? "error"
|
|
217
204
|
: "success";
|
|
218
|
-
const finalOutput = r.finalOutput ??
|
|
205
|
+
const finalOutput = r.finalOutput ?? "";
|
|
219
206
|
const title = formatSubagentTitle(r.agent, r.instanceName, theme);
|
|
220
207
|
let effectiveBody = bodyOverride ?? finalOutput;
|
|
221
208
|
if (display?.isPartial && !finalOutput?.trim() && !bodyOverride) {
|
|
@@ -225,14 +212,17 @@ export function renderSubagentResult(
|
|
|
225
212
|
r.progress?.lastToolPreview ||
|
|
226
213
|
"(running...)";
|
|
227
214
|
}
|
|
228
|
-
const bodyText =
|
|
215
|
+
const bodyText = effectiveBody.trim();
|
|
229
216
|
const toolCount = r.progress?.toolCalls?.length ?? 0;
|
|
230
|
-
const toolLabel = `${toolCount} ${toolCount === 1 ? "tool" : "tools"}`;
|
|
231
217
|
const ctxPercent = formatContextPercent({
|
|
232
218
|
contextTokens: r.usage.contextTokens,
|
|
233
219
|
contextWindowTokens: r.usage.contextWindowTokens,
|
|
234
220
|
});
|
|
235
|
-
const metadata =
|
|
221
|
+
const metadata = formatProgressMetadata(
|
|
222
|
+
toolCount,
|
|
223
|
+
ctxPercent,
|
|
224
|
+
formatElapsed(r.durationMs ?? 0),
|
|
225
|
+
);
|
|
236
226
|
const usageStr = formatResultFooter(r.usage, r.model);
|
|
237
227
|
return renderStatusCard(
|
|
238
228
|
{
|
|
@@ -310,6 +300,15 @@ function selectRunsBoardBody(state: SubagentProgressState): string {
|
|
|
310
300
|
);
|
|
311
301
|
}
|
|
312
302
|
|
|
303
|
+
function formatProgressMetadata(
|
|
304
|
+
toolCount: number,
|
|
305
|
+
ctxPercent: string,
|
|
306
|
+
elapsed: string,
|
|
307
|
+
): string {
|
|
308
|
+
const toolLabel = toolCount === 1 ? "tool" : "tools";
|
|
309
|
+
return `${toolCount} ${toolLabel} · ${ctxPercent} ctx · ${elapsed}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
313
312
|
function renderJobCard(
|
|
314
313
|
state: SubagentProgressState,
|
|
315
314
|
theme: SubagentTheme,
|
|
@@ -319,8 +318,7 @@ function renderJobCard(
|
|
|
319
318
|
state.durationMs ?? Date.now() - state.startTime,
|
|
320
319
|
);
|
|
321
320
|
const ctxPercent = formatContextPercent(state);
|
|
322
|
-
const
|
|
323
|
-
const metadata = `${state.toolCount} ${toolLabel} · ${ctxPercent} ctx · ${elapsed}`;
|
|
321
|
+
const metadata = formatProgressMetadata(state.toolCount, ctxPercent, elapsed);
|
|
324
322
|
const bodyText = selectRunsBoardBody(state);
|
|
325
323
|
const preview =
|
|
326
324
|
bodyText.length > BODY_PREVIEW_MAX
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
normalizeSummaryValue,
|
|
8
8
|
normalizeTerminalSentence,
|
|
9
9
|
} from "../output/normalize.js";
|
|
10
|
+
import { summarizeFeedbackUiFinalOutput } from "../output/summary.js";
|
|
10
11
|
import type {
|
|
11
12
|
SingleResult,
|
|
12
13
|
SubagentDetails,
|
|
@@ -133,10 +134,11 @@ function storeTerminalProgressState(
|
|
|
133
134
|
export function finalizeProgressState(
|
|
134
135
|
requestId: string,
|
|
135
136
|
finalOutput: string,
|
|
137
|
+
outcome?: string,
|
|
136
138
|
): void {
|
|
137
139
|
storeTerminalProgressState(requestId, {
|
|
138
140
|
status: "success",
|
|
139
|
-
finalOutput: makeProgressFinalOutput(finalOutput),
|
|
141
|
+
finalOutput: makeProgressFinalOutput(finalOutput, outcome),
|
|
140
142
|
});
|
|
141
143
|
}
|
|
142
144
|
|
|
@@ -167,17 +169,13 @@ export function makeTaskPreview(task: string): string {
|
|
|
167
169
|
return flat || "(agent default)";
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
function makeProgressFinalOutput(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const normalized = normalizeTerminalSentence(selected);
|
|
178
|
-
if (!normalized) return "";
|
|
179
|
-
if (isStatusOnlySuccess(normalized)) return "completed task";
|
|
180
|
-
return normalized;
|
|
172
|
+
function makeProgressFinalOutput(
|
|
173
|
+
finalOutput: string,
|
|
174
|
+
outcome?: string,
|
|
175
|
+
): string {
|
|
176
|
+
const summary = summarizeFeedbackUiFinalOutput(finalOutput, outcome);
|
|
177
|
+
if (isStatusOnlySuccess(summary)) return "completed task";
|
|
178
|
+
return summary;
|
|
181
179
|
}
|
|
182
180
|
|
|
183
181
|
function deriveFailureTerminalSentence(errorText: string): string {
|