@mystilleef/pi-subagent 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/LICENSE +21 -0
- package/README.md +45 -0
- package/package.json +58 -0
- package/src/agent-cache.ts +41 -0
- package/src/agents.ts +178 -0
- package/src/cancel-command.ts +49 -0
- package/src/child-events.ts +32 -0
- package/src/index.ts +70 -0
- package/src/normalize.ts +109 -0
- package/src/process.ts +661 -0
- package/src/progress-state.ts +233 -0
- package/src/progress.ts +221 -0
- package/src/prompt-contract.ts +10 -0
- package/src/result-details.ts +97 -0
- package/src/run-command.ts +46 -0
- package/src/run-registry.ts +53 -0
- package/src/run.ts +53 -0
- package/src/subagent-orchestrator.ts +377 -0
- package/src/summary.ts +63 -0
- package/src/termination.ts +209 -0
- package/src/types.ts +59 -0
- package/src/ui.ts +218 -0
- package/src/utils.ts +165 -0
- package/tsconfig.json +30 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AgentScope } from "./agents.js";
|
|
3
|
+
import type { TerminationMetadata } from "./termination.js";
|
|
4
|
+
|
|
5
|
+
export interface UsageStats {
|
|
6
|
+
input: number;
|
|
7
|
+
output: number;
|
|
8
|
+
cacheRead: number;
|
|
9
|
+
cacheWrite: number;
|
|
10
|
+
cost: number;
|
|
11
|
+
contextTokens: number;
|
|
12
|
+
contextWindowTokens?: number;
|
|
13
|
+
turns: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StreamingProgressToolCall {
|
|
17
|
+
id: string;
|
|
18
|
+
preview: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StreamingProgress {
|
|
22
|
+
activityText?: string;
|
|
23
|
+
toolCalls: StreamingProgressToolCall[];
|
|
24
|
+
lastToolPreview?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SingleResult {
|
|
28
|
+
agent: string;
|
|
29
|
+
agentSource: "user" | "project" | "unknown";
|
|
30
|
+
task: string;
|
|
31
|
+
exitCode: number;
|
|
32
|
+
finalOutput: string;
|
|
33
|
+
stderr: string;
|
|
34
|
+
usage: UsageStats;
|
|
35
|
+
model?: string;
|
|
36
|
+
stopReason?: string;
|
|
37
|
+
errorMessage?: string;
|
|
38
|
+
durationMs?: number;
|
|
39
|
+
progress?: StreamingProgress;
|
|
40
|
+
messages?: Message[];
|
|
41
|
+
termination?: TerminationMetadata;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SubagentDetails {
|
|
45
|
+
mode: "single";
|
|
46
|
+
agentScope: AgentScope;
|
|
47
|
+
projectAgentsDir: string | null;
|
|
48
|
+
results: SingleResult[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SubagentToolResult {
|
|
52
|
+
content: { type: "text"; text: string }[];
|
|
53
|
+
details: SubagentDetails;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type OnUpdateCallback = (partial: {
|
|
57
|
+
content: { type: "text"; text: string }[];
|
|
58
|
+
details: SubagentDetails;
|
|
59
|
+
}) => void;
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
type Component,
|
|
6
|
+
Markdown,
|
|
7
|
+
type MarkdownTheme,
|
|
8
|
+
Text,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import type { AgentScope } from "./agents.js";
|
|
11
|
+
import {
|
|
12
|
+
extractSemanticToolTarget,
|
|
13
|
+
normalizeSummaryValue,
|
|
14
|
+
} from "./normalize.js";
|
|
15
|
+
import { hasSubagentFailed } from "./result-details.js";
|
|
16
|
+
import type { SubagentDetails, UsageStats } from "./types.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Background theme keys for subagent tool status.
|
|
20
|
+
*/
|
|
21
|
+
export type ThemeBg = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Abstraction for theme-aware text formatting.
|
|
25
|
+
*/
|
|
26
|
+
export type SubagentTheme = {
|
|
27
|
+
fg: (color: ThemeColor, text: string) => string;
|
|
28
|
+
bg: (color: ThemeBg, text: string) => string;
|
|
29
|
+
bold: (text: string) => string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Formats token counts into human-readable strings (e.g., "1.2k", "1.5M").
|
|
34
|
+
*/
|
|
35
|
+
export function formatTokens(count: number): string {
|
|
36
|
+
if (count < 1000) return count.toString();
|
|
37
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
38
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
39
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Formats cumulative usage statistics for compact UI display.
|
|
44
|
+
*/
|
|
45
|
+
export function formatUsageStats(
|
|
46
|
+
usage: UsageStats,
|
|
47
|
+
model?: string,
|
|
48
|
+
compact?: boolean,
|
|
49
|
+
): string {
|
|
50
|
+
const parts: string[] = [];
|
|
51
|
+
if (usage.turns)
|
|
52
|
+
parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
53
|
+
if (usage.input || usage.output) {
|
|
54
|
+
const tokens: string[] = [];
|
|
55
|
+
if (usage.input) tokens.push(`↑${formatTokens(usage.input)}`);
|
|
56
|
+
if (usage.output) tokens.push(`↓${formatTokens(usage.output)}`);
|
|
57
|
+
parts.push(tokens.join(" "));
|
|
58
|
+
}
|
|
59
|
+
if (!compact && (usage.cacheRead || usage.cacheWrite)) {
|
|
60
|
+
const cache: string[] = [];
|
|
61
|
+
if (usage.cacheRead) cache.push(`R${formatTokens(usage.cacheRead)}`);
|
|
62
|
+
if (usage.cacheWrite) cache.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
63
|
+
parts.push(`cache:${cache.join("/")}`);
|
|
64
|
+
}
|
|
65
|
+
if (!compact && usage.contextTokens && usage.contextTokens > 0)
|
|
66
|
+
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
67
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
68
|
+
if (model) parts.push(model);
|
|
69
|
+
return parts.join(" · ");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Formats millisecond durations into human-readable time strings.
|
|
74
|
+
*/
|
|
75
|
+
export function formatDuration(ms: number): string {
|
|
76
|
+
if (ms < 1000) return `${Math.floor(ms)}ms`;
|
|
77
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
78
|
+
const minutes = Math.floor(ms / 60000);
|
|
79
|
+
const seconds = Math.floor((ms % 60000) / 1000);
|
|
80
|
+
return `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Formats the footer for subagent result cards, including model, context, turns, and cost.
|
|
85
|
+
*/
|
|
86
|
+
export function formatResultFooter(
|
|
87
|
+
usage: UsageStats,
|
|
88
|
+
model?: string,
|
|
89
|
+
durationMs?: number,
|
|
90
|
+
): string {
|
|
91
|
+
const parts: string[] = [];
|
|
92
|
+
if (model) parts.push(model);
|
|
93
|
+
if (usage.contextTokens && usage.contextTokens > 0)
|
|
94
|
+
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
95
|
+
if (usage.turns)
|
|
96
|
+
parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
97
|
+
if (typeof durationMs === "number") parts.push(formatDuration(durationMs));
|
|
98
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
99
|
+
return `\n${parts.join(" · ")}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Formats a tool call for the UI, optionally extracting a semantic target for clarity.
|
|
104
|
+
*/
|
|
105
|
+
export function formatToolCall(
|
|
106
|
+
toolName: string,
|
|
107
|
+
args: Record<string, unknown>,
|
|
108
|
+
themeFg: (color: ThemeColor, text: string) => string,
|
|
109
|
+
forceJson = false,
|
|
110
|
+
): string {
|
|
111
|
+
const target = normalizeSummaryValue(
|
|
112
|
+
extractSemanticToolTarget(toolName, args, forceJson),
|
|
113
|
+
);
|
|
114
|
+
if (!target) return themeFg("accent", toolName);
|
|
115
|
+
return themeFg("accent", toolName) + themeFg("dim", ` ${target}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extracts the final text response from an array of assistant messages.
|
|
120
|
+
*/
|
|
121
|
+
export function getFinalOutput(messages: Message[]): string {
|
|
122
|
+
const lastAsst = messages.findLast((m) => m.role === "assistant");
|
|
123
|
+
const lastText = lastAsst?.content.findLast((p) => p.type === "text");
|
|
124
|
+
return lastText?.type === "text" ? lastText.text : "";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Removes the "Outcome:" line from subagent output to avoid redundancy in result cards.
|
|
129
|
+
*/
|
|
130
|
+
function stripOutcomeLineForResultUi(output: string): string {
|
|
131
|
+
const stripped = output.replace(/^\s*Outcome:[^\r\n]*(?:\r?\n|$)/gim, "");
|
|
132
|
+
return stripped.trim() ? stripped : output;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Maps subagent theme colors to Markdown rendering components.
|
|
137
|
+
*/
|
|
138
|
+
function makeMarkdownTheme(theme: SubagentTheme): MarkdownTheme {
|
|
139
|
+
const fg = (c: ThemeColor) => (text: string) => theme.fg(c, text);
|
|
140
|
+
return {
|
|
141
|
+
heading: fg("mdHeading"),
|
|
142
|
+
link: fg("mdLink"),
|
|
143
|
+
linkUrl: fg("mdLinkUrl"),
|
|
144
|
+
code: fg("mdCode"),
|
|
145
|
+
codeBlock: fg("mdCodeBlock"),
|
|
146
|
+
codeBlockBorder: fg("mdCodeBlockBorder"),
|
|
147
|
+
quote: fg("mdQuote"),
|
|
148
|
+
quoteBorder: fg("mdQuoteBorder"),
|
|
149
|
+
hr: fg("mdHr"),
|
|
150
|
+
listBullet: fg("mdListBullet"),
|
|
151
|
+
bold: (text) => theme.bold(text),
|
|
152
|
+
italic: (text) => `\x1b[3m${text}\x1b[23m`,
|
|
153
|
+
strikethrough: (text) => `\x1b[9m${text}\x1b[29m`,
|
|
154
|
+
underline: (text) => `\x1b[4m${text}\x1b[24m`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Renders the pending subagent call UI component.
|
|
160
|
+
*/
|
|
161
|
+
export function renderSubagentCall(
|
|
162
|
+
args: { agent?: string; task?: string; agentScope?: AgentScope },
|
|
163
|
+
theme: SubagentTheme,
|
|
164
|
+
): Text {
|
|
165
|
+
const scope: AgentScope = args.agentScope ?? "both";
|
|
166
|
+
const agentName = args.agent || "...";
|
|
167
|
+
const target = extractSemanticToolTarget("subagent", args);
|
|
168
|
+
let text =
|
|
169
|
+
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
170
|
+
theme.fg("accent", agentName) +
|
|
171
|
+
theme.fg("muted", ` [${scope}]`);
|
|
172
|
+
text += `\n ${theme.fg("dim", target)}`;
|
|
173
|
+
return new Text(text, 0, 0, (line) => theme.bg("toolPendingBg", line));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Renders the subagent result box.
|
|
178
|
+
*
|
|
179
|
+
* Invariants:
|
|
180
|
+
* - Red background indicates failure (exit code, error reason, or message error).
|
|
181
|
+
* - Green background indicates success.
|
|
182
|
+
* - Trims redundant "Outcome:" lines from the body.
|
|
183
|
+
* - Displays usage stats and duration in the footer.
|
|
184
|
+
*/
|
|
185
|
+
export function renderSubagentResult(
|
|
186
|
+
result: { content: { type: string; text?: string }[]; details?: unknown },
|
|
187
|
+
theme: SubagentTheme,
|
|
188
|
+
_display?: { isPartial?: boolean },
|
|
189
|
+
bodyOverride?: string,
|
|
190
|
+
): Component {
|
|
191
|
+
const details = result.details as SubagentDetails | undefined;
|
|
192
|
+
const r = details?.results?.[0];
|
|
193
|
+
if (!r) {
|
|
194
|
+
const text = result.content[0];
|
|
195
|
+
return new Text(
|
|
196
|
+
text?.type === "text" ? (text.text ?? "(no output)") : "(no output)",
|
|
197
|
+
0,
|
|
198
|
+
0,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const failed = hasSubagentFailed(r);
|
|
202
|
+
const finalOutput = r.finalOutput ?? getFinalOutput(r.messages ?? []);
|
|
203
|
+
const bg = failed ? "toolErrorBg" : "toolSuccessBg";
|
|
204
|
+
const box = new Box(1, 1, (line) => theme.bg(bg, line));
|
|
205
|
+
const bodyText = stripOutcomeLineForResultUi(bodyOverride ?? finalOutput);
|
|
206
|
+
if (bodyText) {
|
|
207
|
+
box.addChild(
|
|
208
|
+
new Markdown(bodyText, 0, 0, makeMarkdownTheme(theme), {
|
|
209
|
+
color: (text) => theme.fg("toolOutput", text),
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
box.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
|
214
|
+
}
|
|
215
|
+
const usageStr = formatResultFooter(r.usage, r.model, r.durationMs);
|
|
216
|
+
if (usageStr) box.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
217
|
+
return box;
|
|
218
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
5
|
+
import {
|
|
6
|
+
DefaultResourceLoader,
|
|
7
|
+
getAgentDir,
|
|
8
|
+
withFileMutationQueue,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_MAX_OUTPUT_BYTES = 50_000;
|
|
12
|
+
export const DEFAULT_MAX_OUTPUT_LINES = 500;
|
|
13
|
+
|
|
14
|
+
export interface SubagentOutputLimits {
|
|
15
|
+
maxBytes: number;
|
|
16
|
+
maxLines: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type OutputLimitConfig = Partial<Record<string, string | number | undefined>>;
|
|
20
|
+
|
|
21
|
+
function parsePositiveInteger(
|
|
22
|
+
value: string | number | undefined,
|
|
23
|
+
): number | undefined {
|
|
24
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
25
|
+
if (!Number.isFinite(parsed) || parsed < 1) return undefined;
|
|
26
|
+
return Math.floor(parsed);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getSubagentOutputLimits(
|
|
30
|
+
config: OutputLimitConfig = process.env,
|
|
31
|
+
): SubagentOutputLimits {
|
|
32
|
+
return {
|
|
33
|
+
maxBytes:
|
|
34
|
+
parsePositiveInteger(config.PI_SUBAGENT_MAX_OUTPUT_BYTES) ??
|
|
35
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
36
|
+
maxLines:
|
|
37
|
+
parsePositiveInteger(config.PI_SUBAGENT_MAX_OUTPUT_LINES) ??
|
|
38
|
+
DEFAULT_MAX_OUTPUT_LINES,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function truncateOutput(
|
|
43
|
+
text: string,
|
|
44
|
+
limits: SubagentOutputLimits = getSubagentOutputLimits(),
|
|
45
|
+
): string {
|
|
46
|
+
const lines = text.split("\n");
|
|
47
|
+
const maxBytes = Math.max(1, Math.floor(limits.maxBytes));
|
|
48
|
+
const maxLines = Math.max(1, Math.floor(limits.maxLines));
|
|
49
|
+
if (lines.length <= maxLines && Buffer.byteLength(text, "utf-8") <= maxBytes)
|
|
50
|
+
return text;
|
|
51
|
+
let result = lines.slice(0, maxLines).join("\n");
|
|
52
|
+
if (Buffer.byteLength(result, "utf-8") > maxBytes) {
|
|
53
|
+
const buf = Buffer.from(result).subarray(0, maxBytes);
|
|
54
|
+
result = buf.toString("utf-8").replace(/\uFFFD$/, "");
|
|
55
|
+
}
|
|
56
|
+
const kept = result.split("\n").length;
|
|
57
|
+
return `[TRUNCATED: first ${kept} of ${lines.length} lines]\n${result}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function writePromptToTempFile(
|
|
61
|
+
agentName: string,
|
|
62
|
+
prompt: string,
|
|
63
|
+
): Promise<{ dir: string; filePath: string }> {
|
|
64
|
+
const tmpDir = await fs.promises.mkdtemp(
|
|
65
|
+
path.join(os.tmpdir(), "pi-subagent-"),
|
|
66
|
+
);
|
|
67
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
68
|
+
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
69
|
+
await withFileMutationQueue(filePath, async () => {
|
|
70
|
+
await fs.promises.writeFile(filePath, prompt, {
|
|
71
|
+
encoding: "utf-8",
|
|
72
|
+
mode: 0o600,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
return { dir: tmpDir, filePath };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getPiInvocation(args: string[]): {
|
|
79
|
+
command: string;
|
|
80
|
+
args: string[];
|
|
81
|
+
} {
|
|
82
|
+
const currentScript = process.argv[1];
|
|
83
|
+
if (currentScript && fs.existsSync(currentScript)) {
|
|
84
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
85
|
+
}
|
|
86
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
87
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
88
|
+
if (!isGenericRuntime) {
|
|
89
|
+
return { command: process.execPath, args };
|
|
90
|
+
}
|
|
91
|
+
return { command: "pi", args };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function resolveAgentSkillArgs(
|
|
95
|
+
cwd: string,
|
|
96
|
+
skillNames: string[],
|
|
97
|
+
): Promise<{ args: string[] } | { error: string }> {
|
|
98
|
+
const requested = Array.from(new Set(skillNames));
|
|
99
|
+
if (requested.length === 0) return { args: [] };
|
|
100
|
+
const loader = new DefaultResourceLoader({
|
|
101
|
+
cwd,
|
|
102
|
+
agentDir: getAgentDir(),
|
|
103
|
+
noContextFiles: true,
|
|
104
|
+
noPromptTemplates: true,
|
|
105
|
+
noThemes: true,
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
await loader.reload();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
error: `Failed to discover skills: ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const { skills } = loader.getSkills();
|
|
115
|
+
const skillMap = new Map(skills.map((skill) => [skill.name, skill]));
|
|
116
|
+
const missing = requested.filter((name) => !skillMap.has(name));
|
|
117
|
+
if (missing.length > 0) {
|
|
118
|
+
const available =
|
|
119
|
+
skills
|
|
120
|
+
.map((skill) => skill.name)
|
|
121
|
+
.sort()
|
|
122
|
+
.join(", ") || "none";
|
|
123
|
+
return {
|
|
124
|
+
error: `Unknown skill${missing.length === 1 ? "" : "s"}: ${missing
|
|
125
|
+
.map((name) => `"${name}"`)
|
|
126
|
+
.join(", ")}. Available skills: ${available}.`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
args: requested.flatMap((name) => [
|
|
131
|
+
"--skill",
|
|
132
|
+
skillMap.get(name)?.filePath ?? name,
|
|
133
|
+
]),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getSubagentDepth(): number {
|
|
138
|
+
const d = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
|
|
139
|
+
if (!Number.isFinite(d) || d < 0) return 0;
|
|
140
|
+
return Math.floor(d);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function subagentDepthEnv(): Record<string, string> {
|
|
144
|
+
return { PI_SUBAGENT_DEPTH: String(getSubagentDepth() + 1) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function detectMessageError(messages: Message[]): boolean {
|
|
148
|
+
let lastAssistantIdx = -1;
|
|
149
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
150
|
+
const msg = messages[i];
|
|
151
|
+
if (
|
|
152
|
+
msg?.role === "assistant" &&
|
|
153
|
+
msg.content.some((c) => c.type === "text" && c.text.trim().length > 0)
|
|
154
|
+
) {
|
|
155
|
+
lastAssistantIdx = i;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const from = lastAssistantIdx >= 0 ? lastAssistantIdx + 1 : 0;
|
|
160
|
+
for (let i = messages.length - 1; i >= from; i--) {
|
|
161
|
+
const msg = messages[i];
|
|
162
|
+
if (msg?.role === "toolResult" && msg.isError) return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"types": ["bun"],
|
|
11
|
+
|
|
12
|
+
// Bundler mode
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
|
|
18
|
+
// Best practices
|
|
19
|
+
"strict": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedIndexedAccess": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
|
|
25
|
+
// Some stricter flags (disabled by default)
|
|
26
|
+
"noUnusedLocals": false,
|
|
27
|
+
"noUnusedParameters": false,
|
|
28
|
+
"noPropertyAccessFromIndexSignature": false
|
|
29
|
+
}
|
|
30
|
+
}
|