@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
|
@@ -79,6 +79,47 @@ function redactSensitiveDebugMessages(messages: unknown): unknown {
|
|
|
79
79
|
return messages.map(redactSensitiveDebugValue);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function sanitizeProgressObject(
|
|
83
|
+
progress: SingleResult["progress"],
|
|
84
|
+
_includeDebugMessages: boolean,
|
|
85
|
+
): StreamingProgress | undefined {
|
|
86
|
+
if (!progress) return undefined;
|
|
87
|
+
const {
|
|
88
|
+
activityText,
|
|
89
|
+
activeToolActivity,
|
|
90
|
+
lastToolPreview,
|
|
91
|
+
toolResultCompleted,
|
|
92
|
+
...progBase
|
|
93
|
+
} = progress;
|
|
94
|
+
return {
|
|
95
|
+
toolCalls: progBase.toolCalls.map((tc) => ({
|
|
96
|
+
id: tc.id,
|
|
97
|
+
preview: tc.preview,
|
|
98
|
+
})),
|
|
99
|
+
...(activityText !== undefined && { activityText }),
|
|
100
|
+
...(activeToolActivity !== undefined && { activeToolActivity }),
|
|
101
|
+
...(lastToolPreview !== undefined && { lastToolPreview }),
|
|
102
|
+
...(toolResultCompleted !== undefined && { toolResultCompleted }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sanitizeTerminationObject(
|
|
107
|
+
termination: TerminationMetadata | undefined,
|
|
108
|
+
includeMessages: boolean,
|
|
109
|
+
includeDebugMessages: boolean,
|
|
110
|
+
): TerminationMetadata | undefined {
|
|
111
|
+
if (!includeMessages || !includeDebugMessages || !termination)
|
|
112
|
+
return undefined;
|
|
113
|
+
const { cancelReason, terminationSignal, fallbackCause, ...termBase } =
|
|
114
|
+
termination;
|
|
115
|
+
return {
|
|
116
|
+
...termBase,
|
|
117
|
+
...(cancelReason !== undefined && { cancelReason }),
|
|
118
|
+
...(terminationSignal !== undefined && { terminationSignal }),
|
|
119
|
+
...(fallbackCause !== undefined && { fallbackCause }),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
82
123
|
export function sanitizeResultDetails(
|
|
83
124
|
result: SingleResult,
|
|
84
125
|
includeDebugMessages: boolean,
|
|
@@ -88,37 +129,6 @@ export function sanitizeResultDetails(
|
|
|
88
129
|
includeDebugMessages && (options?.includeMessages ?? true);
|
|
89
130
|
const { messages, termination, progress, stderr, usage, ...core } = result;
|
|
90
131
|
const { contextWindowTokens, ...usageBase } = usage;
|
|
91
|
-
let progressValue: StreamingProgress | undefined;
|
|
92
|
-
if (progress) {
|
|
93
|
-
const {
|
|
94
|
-
activityText,
|
|
95
|
-
activeToolActivity,
|
|
96
|
-
lastToolPreview,
|
|
97
|
-
toolResultCompleted,
|
|
98
|
-
...progBase
|
|
99
|
-
} = progress;
|
|
100
|
-
progressValue = {
|
|
101
|
-
toolCalls: progBase.toolCalls.map((tc) => ({
|
|
102
|
-
id: tc.id,
|
|
103
|
-
preview: tc.preview,
|
|
104
|
-
})),
|
|
105
|
-
...(activityText !== undefined && { activityText }),
|
|
106
|
-
...(activeToolActivity !== undefined && { activeToolActivity }),
|
|
107
|
-
...(lastToolPreview !== undefined && { lastToolPreview }),
|
|
108
|
-
...(toolResultCompleted !== undefined && { toolResultCompleted }),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
let terminationValue: TerminationMetadata | undefined;
|
|
112
|
-
if (includeMessages && includeDebugMessages && termination) {
|
|
113
|
-
const { cancelReason, terminationSignal, fallbackCause, ...termBase } =
|
|
114
|
-
termination;
|
|
115
|
-
terminationValue = {
|
|
116
|
-
...termBase,
|
|
117
|
-
...(cancelReason !== undefined && { cancelReason }),
|
|
118
|
-
...(terminationSignal !== undefined && { terminationSignal }),
|
|
119
|
-
...(fallbackCause !== undefined && { fallbackCause }),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
132
|
const sanitized: SingleResult = {
|
|
123
133
|
...core,
|
|
124
134
|
stderr: includeDebugMessages ? stderr : "",
|
|
@@ -127,6 +137,7 @@ export function sanitizeResultDetails(
|
|
|
127
137
|
...(contextWindowTokens !== undefined && { contextWindowTokens }),
|
|
128
138
|
},
|
|
129
139
|
};
|
|
140
|
+
const progressValue = sanitizeProgressObject(progress, includeDebugMessages);
|
|
130
141
|
if (progressValue !== undefined) sanitized.progress = progressValue;
|
|
131
142
|
if (includeMessages) {
|
|
132
143
|
sanitized.messages = options?.recentMessages
|
|
@@ -135,6 +146,11 @@ export function sanitizeResultDetails(
|
|
|
135
146
|
? [...messages]
|
|
136
147
|
: undefined;
|
|
137
148
|
}
|
|
149
|
+
const terminationValue = sanitizeTerminationObject(
|
|
150
|
+
termination,
|
|
151
|
+
includeMessages,
|
|
152
|
+
includeDebugMessages,
|
|
153
|
+
);
|
|
138
154
|
if (terminationValue !== undefined) sanitized.termination = terminationValue;
|
|
139
155
|
return sanitized;
|
|
140
156
|
}
|
|
@@ -224,19 +240,12 @@ export function patchProgressFromDetails(
|
|
|
224
240
|
patchProgressState(requestId, patch);
|
|
225
241
|
}
|
|
226
242
|
|
|
227
|
-
function getSubagentText(result: SubagentToolResult): string {
|
|
228
|
-
return (result.content[0] as { text?: string })?.text ?? "";
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export function getResultDisplayText(result: SubagentToolResult): string {
|
|
232
|
-
return (
|
|
233
|
-
getLatestResult(result.details)?.finalOutput ?? getSubagentText(result)
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
243
|
export function getFeedbackSummaryText(result: SubagentToolResult): string {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
const latestResult = getLatestResult(result.details);
|
|
245
|
+
const rawFinalOutput = latestResult?.finalOutput ?? "";
|
|
246
|
+
const outcome = latestResult?.outcome;
|
|
247
|
+
if (!outcome?.trim() && !rawFinalOutput.trim()) {
|
|
248
|
+
return "(no output)";
|
|
249
|
+
}
|
|
250
|
+
return summarizeFeedbackUiFinalOutput(rawFinalOutput, outcome);
|
|
242
251
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output and runtime limit configuration for subagent processes.
|
|
3
|
+
* Handles byte/line caps for child output and runtime safety thresholds.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_MAX_OUTPUT_BYTES = 50_000;
|
|
7
|
+
export const DEFAULT_MAX_OUTPUT_LINES = 500;
|
|
8
|
+
export const DEFAULT_AGENT_END_GRACE_MS = 250;
|
|
9
|
+
export const DEFAULT_MAX_STDERR_BYTES = 10_000;
|
|
10
|
+
export const DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
11
|
+
const MAX_SUBAGENT_DEPTH_CEILING = 10;
|
|
12
|
+
|
|
13
|
+
export interface SubagentOutputLimits {
|
|
14
|
+
maxBytes: number;
|
|
15
|
+
maxLines: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SubagentRuntimeLimits {
|
|
19
|
+
agentEndGraceMs: number;
|
|
20
|
+
maxStderrBytes: number;
|
|
21
|
+
maxDepth: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type EnvLimitConfig = Partial<Record<string, string | number | undefined>>;
|
|
25
|
+
|
|
26
|
+
function parsePositiveInteger(
|
|
27
|
+
value: string | number | undefined,
|
|
28
|
+
): number | undefined {
|
|
29
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
30
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1)
|
|
31
|
+
return undefined;
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getSubagentOutputLimits(
|
|
36
|
+
config: EnvLimitConfig = process.env,
|
|
37
|
+
): SubagentOutputLimits {
|
|
38
|
+
return {
|
|
39
|
+
maxBytes:
|
|
40
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_BYTES"]) ??
|
|
41
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
42
|
+
maxLines:
|
|
43
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_LINES"]) ??
|
|
44
|
+
DEFAULT_MAX_OUTPUT_LINES,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getSubagentRuntimeLimits(
|
|
49
|
+
config: EnvLimitConfig = process.env,
|
|
50
|
+
): SubagentRuntimeLimits {
|
|
51
|
+
const maxDepth =
|
|
52
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_DEPTH"]) ??
|
|
53
|
+
DEFAULT_MAX_SUBAGENT_DEPTH;
|
|
54
|
+
return {
|
|
55
|
+
agentEndGraceMs:
|
|
56
|
+
parsePositiveInteger(config["PI_SUBAGENT_AGENT_END_GRACE_MS"]) ??
|
|
57
|
+
DEFAULT_AGENT_END_GRACE_MS,
|
|
58
|
+
maxStderrBytes:
|
|
59
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_STDERR_BYTES"]) ??
|
|
60
|
+
DEFAULT_MAX_STDERR_BYTES,
|
|
61
|
+
maxDepth: Math.min(maxDepth, MAX_SUBAGENT_DEPTH_CEILING),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function truncateOutput(
|
|
66
|
+
text: string,
|
|
67
|
+
limits: SubagentOutputLimits = getSubagentOutputLimits(),
|
|
68
|
+
): string {
|
|
69
|
+
const lines = text.split("\n");
|
|
70
|
+
const maxBytes = Math.max(1, Math.floor(limits.maxBytes));
|
|
71
|
+
const maxLines = Math.max(1, Math.floor(limits.maxLines));
|
|
72
|
+
if (lines.length <= maxLines && Buffer.byteLength(text, "utf-8") <= maxBytes)
|
|
73
|
+
return text;
|
|
74
|
+
let result = lines.slice(0, maxLines).join("\n");
|
|
75
|
+
if (Buffer.byteLength(result, "utf-8") > maxBytes) {
|
|
76
|
+
const buf = Buffer.from(result).subarray(0, maxBytes);
|
|
77
|
+
result = buf.toString("utf-8").replace(/\uFFFD$/, "");
|
|
78
|
+
}
|
|
79
|
+
const kept = result.split("\n").length;
|
|
80
|
+
return `[TRUNCATED: first ${kept} of ${lines.length} lines]\n${result}`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message processing utilities for subagent results.
|
|
3
|
+
* Handles assistant message extraction, error detection, and failure判定.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
7
|
+
import type { SingleResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export function findLastAssistantTextMessage(messages: Message[]): number {
|
|
10
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11
|
+
const msg = messages[i];
|
|
12
|
+
if (
|
|
13
|
+
msg?.role === "assistant" &&
|
|
14
|
+
Array.isArray(msg.content) &&
|
|
15
|
+
msg.content.some(
|
|
16
|
+
(c) =>
|
|
17
|
+
c.type === "text" &&
|
|
18
|
+
typeof c.text === "string" &&
|
|
19
|
+
c.text.trim().length > 0,
|
|
20
|
+
)
|
|
21
|
+
) {
|
|
22
|
+
return i;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return -1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function extractFinalOutputFromMessages(messages: Message[]): string {
|
|
29
|
+
const lastAsstIdx = findLastAssistantTextMessage(messages);
|
|
30
|
+
if (lastAsstIdx < 0) return "";
|
|
31
|
+
const content = messages[lastAsstIdx]?.content;
|
|
32
|
+
if (!Array.isArray(content)) return "";
|
|
33
|
+
const lastText = content.findLast((p) => p.type === "text");
|
|
34
|
+
return lastText?.type === "text" ? (lastText.text ?? "") : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function detectMessageError(messages: Message[]): boolean {
|
|
38
|
+
const lastAssistantIdx = findLastAssistantTextMessage(messages);
|
|
39
|
+
const from = lastAssistantIdx >= 0 ? lastAssistantIdx + 1 : 0;
|
|
40
|
+
for (let i = messages.length - 1; i >= from; i--) {
|
|
41
|
+
const msg = messages[i];
|
|
42
|
+
if (msg?.role === "toolResult" && msg.isError) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hasSubagentFailed(result: SingleResult): boolean {
|
|
48
|
+
if (result.outcome?.trim()) return false;
|
|
49
|
+
return (
|
|
50
|
+
result.exitCode !== 0 ||
|
|
51
|
+
result.stopReason === "error" ||
|
|
52
|
+
result.stopReason === "aborted" ||
|
|
53
|
+
Boolean(result.errorMessage?.trim()) ||
|
|
54
|
+
detectMessageError(result.messages ?? [])
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic resource resolution for skills and extensions.
|
|
3
|
+
* Handles caching, discovery via DefaultResourceLoader, and name-to-path mapping.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
DefaultResourceLoader,
|
|
10
|
+
getAgentDir,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
const RESOURCE_DISCOVERY_CACHE_TTL_MS = 300_000;
|
|
14
|
+
|
|
15
|
+
export const EXTENSION_DISCOVERY_CACHE_TTL_MS = RESOURCE_DISCOVERY_CACHE_TTL_MS;
|
|
16
|
+
|
|
17
|
+
type ResourceCacheEntry<T> = {
|
|
18
|
+
data: T;
|
|
19
|
+
ts: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
class ResourceCache<T> {
|
|
23
|
+
private store = new Map<string, ResourceCacheEntry<T>>();
|
|
24
|
+
|
|
25
|
+
get(key: string): T | undefined {
|
|
26
|
+
const entry = this.store.get(key);
|
|
27
|
+
if (entry && Date.now() - entry.ts <= RESOURCE_DISCOVERY_CACHE_TTL_MS) {
|
|
28
|
+
return entry.data;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
set(key: string, data: T): void {
|
|
34
|
+
this.store.set(key, { data, ts: Date.now() });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
clear(): void {
|
|
38
|
+
this.store.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const skillArgsCache = new ResourceCache<Map<string, string>>();
|
|
43
|
+
const extensionPathsCache = new ResourceCache<Map<string, string>>();
|
|
44
|
+
|
|
45
|
+
export function resetResolvedAgentSkillArgsCache(): void {
|
|
46
|
+
skillArgsCache.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resetResolvedAgentExtensionPathsCache(): void {
|
|
50
|
+
extensionPathsCache.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function canonicalPath(filePath: string): Promise<string> {
|
|
54
|
+
try {
|
|
55
|
+
return await fs.promises.realpath(filePath);
|
|
56
|
+
} catch {
|
|
57
|
+
/* symlinks or missing paths fall back to absolute path resolution */
|
|
58
|
+
return path.resolve(filePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function buildResourceCacheKey(
|
|
63
|
+
cwd: string,
|
|
64
|
+
agentDir: string,
|
|
65
|
+
names: string[],
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
const sortedNames = [...names].sort();
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
cwd: await canonicalPath(cwd),
|
|
70
|
+
agentDir: await canonicalPath(agentDir),
|
|
71
|
+
names: sortedNames,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type ResourceResolverResult<T> = { data: T } | { error: string };
|
|
76
|
+
|
|
77
|
+
interface ResourceResolverConfig<TResult> {
|
|
78
|
+
cache: ResourceCache<Map<string, string>>;
|
|
79
|
+
loaderOptions: Record<string, boolean>;
|
|
80
|
+
getResources: (loader: DefaultResourceLoader) => {
|
|
81
|
+
items: unknown[];
|
|
82
|
+
errors: Array<{ path: string; error: string }>;
|
|
83
|
+
};
|
|
84
|
+
buildNameToResource: (items: unknown[]) => Map<string, string>;
|
|
85
|
+
buildResult: (
|
|
86
|
+
requested: string[],
|
|
87
|
+
nameToResource: Map<string, string>,
|
|
88
|
+
) => TResult;
|
|
89
|
+
resourceType: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function resolveResources<TResult>(
|
|
93
|
+
cwd: string,
|
|
94
|
+
names: string[],
|
|
95
|
+
config: ResourceResolverConfig<TResult>,
|
|
96
|
+
): Promise<ResourceResolverResult<TResult>> {
|
|
97
|
+
const requested = Array.from(new Set(names));
|
|
98
|
+
if (requested.length === 0) {
|
|
99
|
+
return { data: config.buildResult(requested, new Map()) };
|
|
100
|
+
}
|
|
101
|
+
const agentDir = getAgentDir();
|
|
102
|
+
const cacheKey = await buildResourceCacheKey(cwd, agentDir, requested);
|
|
103
|
+
const cached = config.cache.get(cacheKey);
|
|
104
|
+
if (cached) {
|
|
105
|
+
return { data: config.buildResult(requested, cached) };
|
|
106
|
+
}
|
|
107
|
+
const loader = new DefaultResourceLoader({
|
|
108
|
+
cwd,
|
|
109
|
+
agentDir,
|
|
110
|
+
...config.loaderOptions,
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
await loader.reload();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
error: `Failed to discover ${config.resourceType}s: ${error instanceof Error ? error.message : String(error)}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const { items, errors } = config.getResources(loader);
|
|
120
|
+
if (errors.length > 0) {
|
|
121
|
+
const details = errors.map((e) => ` ${e.path}: ${e.error}`).join("\n");
|
|
122
|
+
return {
|
|
123
|
+
error: `Failed to discover ${config.resourceType}s:\n${details}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const nameToResource = config.buildNameToResource(items);
|
|
127
|
+
const missing = requested.filter((name) => !nameToResource.has(name));
|
|
128
|
+
if (missing.length > 0) {
|
|
129
|
+
const available =
|
|
130
|
+
Array.from(nameToResource.keys()).sort().join(", ") || "none";
|
|
131
|
+
return {
|
|
132
|
+
error: `Unknown ${config.resourceType}${missing.length === 1 ? "" : "s"}: ${missing
|
|
133
|
+
.map((name) => `"${name}"`)
|
|
134
|
+
.join(", ")}. Available ${config.resourceType}s: ${available}.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const result = config.buildResult(requested, nameToResource);
|
|
138
|
+
config.cache.set(cacheKey, nameToResource);
|
|
139
|
+
return { data: result };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildSkillArgs(
|
|
143
|
+
requested: string[],
|
|
144
|
+
skillPaths: Map<string, string>,
|
|
145
|
+
): string[] {
|
|
146
|
+
return requested.flatMap((name) => ["--skill", skillPaths.get(name) ?? name]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function resolveAgentSkillArgs(
|
|
150
|
+
cwd: string,
|
|
151
|
+
skillNames: string[],
|
|
152
|
+
): Promise<{ args: string[] } | { error: string }> {
|
|
153
|
+
const result = await resolveResources(cwd, skillNames, {
|
|
154
|
+
cache: skillArgsCache,
|
|
155
|
+
loaderOptions: {
|
|
156
|
+
noContextFiles: true,
|
|
157
|
+
noPromptTemplates: true,
|
|
158
|
+
noThemes: true,
|
|
159
|
+
},
|
|
160
|
+
getResources: (loader) => {
|
|
161
|
+
const { skills } = loader.getSkills();
|
|
162
|
+
return {
|
|
163
|
+
items: skills,
|
|
164
|
+
errors: [],
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
buildNameToResource: (items) => {
|
|
168
|
+
const skillMap = new Map<string, string>();
|
|
169
|
+
for (const item of items) {
|
|
170
|
+
const skill = item as { name: string; filePath: string };
|
|
171
|
+
skillMap.set(skill.name, skill.filePath ?? skill.name);
|
|
172
|
+
}
|
|
173
|
+
return skillMap;
|
|
174
|
+
},
|
|
175
|
+
buildResult: (requested, nameToResource) => {
|
|
176
|
+
return buildSkillArgs(requested, nameToResource);
|
|
177
|
+
},
|
|
178
|
+
resourceType: "skill",
|
|
179
|
+
});
|
|
180
|
+
return "error" in result ? result : { args: result.data };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getTerminalPackageName(source: string): string {
|
|
184
|
+
const spec = source.startsWith("npm:") ? source.slice(4) : source;
|
|
185
|
+
let name: string;
|
|
186
|
+
if (spec.startsWith("@")) {
|
|
187
|
+
const slashIdx = spec.indexOf("/");
|
|
188
|
+
if (slashIdx >= 0) {
|
|
189
|
+
name = spec.slice(slashIdx + 1);
|
|
190
|
+
} else {
|
|
191
|
+
name = spec;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
name = spec;
|
|
195
|
+
}
|
|
196
|
+
const versionIdx = name.indexOf("@");
|
|
197
|
+
if (versionIdx >= 0) {
|
|
198
|
+
return name.slice(0, versionIdx);
|
|
199
|
+
}
|
|
200
|
+
return name;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isLocalPathSpec(source: string): boolean {
|
|
204
|
+
return (
|
|
205
|
+
source.startsWith(".") ||
|
|
206
|
+
source.startsWith("/") ||
|
|
207
|
+
source.startsWith("~") ||
|
|
208
|
+
source.startsWith("file:")
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildExtensionShortName(ext: {
|
|
213
|
+
resolvedPath: string;
|
|
214
|
+
sourceInfo: { source: string; origin: string };
|
|
215
|
+
}): string {
|
|
216
|
+
if (
|
|
217
|
+
ext.sourceInfo.origin === "package" &&
|
|
218
|
+
!isLocalPathSpec(ext.sourceInfo.source)
|
|
219
|
+
) {
|
|
220
|
+
return getTerminalPackageName(ext.sourceInfo.source);
|
|
221
|
+
}
|
|
222
|
+
const fileName = path.basename(ext.resolvedPath);
|
|
223
|
+
if (/^index\.(?:ts|js)$/.test(fileName)) {
|
|
224
|
+
const dirName = path.dirname(ext.resolvedPath);
|
|
225
|
+
const base = path.basename(dirName);
|
|
226
|
+
if (base === "src" || base === "dist") {
|
|
227
|
+
return path.basename(path.dirname(dirName));
|
|
228
|
+
}
|
|
229
|
+
return base;
|
|
230
|
+
}
|
|
231
|
+
const dotIdx = fileName.lastIndexOf(".");
|
|
232
|
+
if (dotIdx > 0) {
|
|
233
|
+
return fileName.slice(0, dotIdx);
|
|
234
|
+
}
|
|
235
|
+
return fileName;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function resolveAgentExtensionPaths(
|
|
239
|
+
cwd: string,
|
|
240
|
+
extensionNames: string[],
|
|
241
|
+
): Promise<{ resolvedPaths: string[] } | { error: string }> {
|
|
242
|
+
const result = await resolveResources(cwd, extensionNames, {
|
|
243
|
+
cache: extensionPathsCache,
|
|
244
|
+
loaderOptions: {
|
|
245
|
+
noContextFiles: true,
|
|
246
|
+
noPromptTemplates: true,
|
|
247
|
+
noThemes: true,
|
|
248
|
+
noSkills: true,
|
|
249
|
+
},
|
|
250
|
+
getResources: (loader) => {
|
|
251
|
+
const { extensions, errors } = loader.getExtensions();
|
|
252
|
+
const shortNameToExtension = new Map<
|
|
253
|
+
string,
|
|
254
|
+
(typeof extensions)[number]
|
|
255
|
+
>();
|
|
256
|
+
for (const ext of extensions) {
|
|
257
|
+
const shortName = buildExtensionShortName(ext);
|
|
258
|
+
if (!shortNameToExtension.has(shortName)) {
|
|
259
|
+
shortNameToExtension.set(shortName, ext);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
items: Array.from(shortNameToExtension.values()),
|
|
264
|
+
errors,
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
buildNameToResource: (items) => {
|
|
268
|
+
const shortNameToPath = new Map<string, string>();
|
|
269
|
+
for (const item of items) {
|
|
270
|
+
const ext = item as {
|
|
271
|
+
resolvedPath: string;
|
|
272
|
+
sourceInfo: { source: string; origin: string };
|
|
273
|
+
};
|
|
274
|
+
const shortName = buildExtensionShortName(ext);
|
|
275
|
+
if (!shortNameToPath.has(shortName)) {
|
|
276
|
+
shortNameToPath.set(shortName, ext.resolvedPath ?? shortName);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return shortNameToPath;
|
|
280
|
+
},
|
|
281
|
+
buildResult: (requested, nameToResource) => {
|
|
282
|
+
return requested
|
|
283
|
+
.map((name) => nameToResource.get(name))
|
|
284
|
+
.filter((p): p is string => typeof p === "string");
|
|
285
|
+
},
|
|
286
|
+
resourceType: "extension",
|
|
287
|
+
});
|
|
288
|
+
return "error" in result ? result : { resolvedPaths: result.data };
|
|
289
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface SamplingParams {
|
|
2
|
+
temperature?: number | undefined;
|
|
3
|
+
topP?: number | undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function isValidSamplingValue(value: unknown): value is number {
|
|
7
|
+
return (
|
|
8
|
+
typeof value === "number" &&
|
|
9
|
+
Number.isFinite(value) &&
|
|
10
|
+
value >= 0.0 &&
|
|
11
|
+
value <= 1.0
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses a JSON-encoded sampling parameters string.
|
|
17
|
+
* Returns undefined for missing, malformed, or wholly invalid values.
|
|
18
|
+
*/
|
|
19
|
+
export function parseSamplingParams(
|
|
20
|
+
envVal?: string,
|
|
21
|
+
): SamplingParams | undefined {
|
|
22
|
+
if (!envVal) return undefined;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(envVal);
|
|
25
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
26
|
+
return undefined;
|
|
27
|
+
const { temperature: rawTemperature, topP: rawTopP } = parsed as Record<
|
|
28
|
+
string,
|
|
29
|
+
unknown
|
|
30
|
+
>;
|
|
31
|
+
const result: SamplingParams = {};
|
|
32
|
+
if (isValidSamplingValue(rawTemperature))
|
|
33
|
+
result.temperature = rawTemperature;
|
|
34
|
+
if (isValidSamplingValue(rawTopP)) result.topP = rawTopP;
|
|
35
|
+
if (result.temperature !== undefined || result.topP !== undefined)
|
|
36
|
+
return result;
|
|
37
|
+
} catch {
|
|
38
|
+
/* malformed env value: run without sampling overrides */
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Serializes sampling parameters into a JSON string for environment passing.
|
|
45
|
+
* Returns undefined when no valid parameters are present.
|
|
46
|
+
*/
|
|
47
|
+
export function serializeSamplingParams(
|
|
48
|
+
params: SamplingParams,
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (params.temperature === undefined && params.topP === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const config: SamplingParams = {};
|
|
53
|
+
if (params.temperature !== undefined) config.temperature = params.temperature;
|
|
54
|
+
if (params.topP !== undefined) config.topP = params.topP;
|
|
55
|
+
return JSON.stringify(config);
|
|
56
|
+
}
|
package/src/shared/types.ts
CHANGED