@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.
@@ -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 rawFinalOutput = getLatestResult(result.details)?.finalOutput;
239
- if (rawFinalOutput?.trim())
240
- return summarizeFeedbackUiFinalOutput(rawFinalOutput);
241
- return getSubagentText(result).trim() || "(no output)";
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
+ }
@@ -52,6 +52,7 @@ export interface SingleResult {
52
52
  messages?: Message[] | undefined;
53
53
  termination?: TerminationMetadata | undefined;
54
54
  thinkingWarning?: string | undefined;
55
+ outcome?: string | undefined;
55
56
  }
56
57
 
57
58
  export interface SubagentDetails {