pi-subagents-j0k3r 1.0.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/.releaserc.json +14 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/index.ts +531 -0
- package/package.json +73 -0
- package/scripts/verify-package-files.mjs +41 -0
- package/skills/subagents-configuration/SKILL.md +182 -0
- package/src/config.ts +262 -0
- package/src/debug.ts +38 -0
- package/src/history.ts +254 -0
- package/src/interaction-channel.ts +197 -0
- package/src/manager.ts +533 -0
- package/src/model-profiles-ui.ts +609 -0
- package/src/profile-resolver.ts +60 -0
- package/src/runner.ts +688 -0
- package/src/thread-view.ts +477 -0
- package/src/tools.ts +492 -0
- package/src/types.ts +234 -0
- package/src/ui.ts +399 -0
package/src/runner.ts
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { writeSubagentsDebugLog } from './debug.js';
|
|
2
|
+
import { resolveEffectiveSubagentProfile } from './profile-resolver.js';
|
|
3
|
+
import { consumeLatestInteractionRequest, interactionRequestFromCandidate, sanitizeInteractionTransportText } from './interaction-channel.js';
|
|
4
|
+
import { boundThreadSnapshot, registerSubagentRuntimeToolDefinition } from './thread-view.js';
|
|
5
|
+
import type { SubagentInteractionRequest } from './interaction-channel.js';
|
|
6
|
+
import type { EffectiveSubagentProfile, ModelRef, SubagentDefinition, SubagentRunner, SubagentsConfig, UsageStats, ThinkingEffort, SubagentThreadItem, SubagentThreadSnapshot, SubagentToolItem, SubagentToolResultPayload } from './types.js';
|
|
7
|
+
|
|
8
|
+
function modelLabel(model: any): string | undefined {
|
|
9
|
+
if (!model) return undefined;
|
|
10
|
+
return `${model.provider ?? 'unknown'}/${model.id ?? model.name ?? 'unknown'}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function modelRefLabel(ref: ModelRef | undefined): string | undefined {
|
|
14
|
+
return ref ? `${ref.provider}/${ref.id}` : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveModel(ctx: any, ref?: ModelRef): any | undefined {
|
|
18
|
+
if (!ref) return undefined;
|
|
19
|
+
return ctx?.modelRegistry?.find?.(ref.provider, ref.id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildPrompt(_definition: SubagentDefinition, task: string, context?: string, _tools: string[] = _definition.tools): string {
|
|
23
|
+
return [
|
|
24
|
+
context ? `## orchestrator context\n${context}` : '',
|
|
25
|
+
`## delegated task\n${task}`,
|
|
26
|
+
].filter(Boolean).join('\n\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SUBAGENT_ALLOWED_EXTENSION_EVENTS = new Set(['tool_call', 'tool_result', 'user_bash']);
|
|
30
|
+
|
|
31
|
+
class NonRetryableSubagentError extends Error {
|
|
32
|
+
readonly nonRetryable = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNonRetryableSubagentError(error: unknown): boolean {
|
|
36
|
+
return Boolean(error && typeof error === 'object' && (error as { nonRetryable?: unknown }).nonRetryable);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isolateSubagentExtensions(base: any): any {
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
extensions: (base?.extensions ?? []).map((extension: any) => ({
|
|
43
|
+
...extension,
|
|
44
|
+
handlers: new Map([...((extension.handlers as Map<string, unknown[]>) ?? new Map())]
|
|
45
|
+
.filter(([event]) => SUBAGENT_ALLOWED_EXTENSION_EVENTS.has(event))),
|
|
46
|
+
commands: new Map(),
|
|
47
|
+
flags: new Map(),
|
|
48
|
+
shortcuts: new Map(),
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function emptyUsage(): UsageStats {
|
|
54
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function addUsage(total: UsageStats, usage: any): UsageStats {
|
|
58
|
+
return {
|
|
59
|
+
input: total.input + (usage?.input ?? 0),
|
|
60
|
+
output: total.output + (usage?.output ?? 0),
|
|
61
|
+
cacheRead: total.cacheRead + (usage?.cacheRead ?? 0),
|
|
62
|
+
cacheWrite: total.cacheWrite + (usage?.cacheWrite ?? 0),
|
|
63
|
+
cost: total.cost + (usage?.cost?.total ?? usage?.cost ?? 0),
|
|
64
|
+
contextTokens: usage?.totalTokens ?? total.contextTokens,
|
|
65
|
+
turns: total.turns + 1,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function summarizeInteractionCarrier(value: unknown): unknown {
|
|
70
|
+
if (!value || typeof value !== 'object') return { type: typeof value };
|
|
71
|
+
const record = value as Record<string, unknown>;
|
|
72
|
+
return {
|
|
73
|
+
keys: Object.keys(record),
|
|
74
|
+
hasInteractionRequest: Object.hasOwn(record, 'interactionRequest'),
|
|
75
|
+
hasInteractionRequestSnake: Object.hasOwn(record, 'interaction_request'),
|
|
76
|
+
details: record.details && typeof record.details === 'object' ? summarizeInteractionCarrier(record.details) : undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shortJson(value: unknown, limit = 900): string {
|
|
81
|
+
try {
|
|
82
|
+
const text = JSON.stringify(value, (_key, val) => typeof val === 'string' && val.length > 300 ? `${val.slice(0, 300)}…` : val);
|
|
83
|
+
return text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
84
|
+
} catch {
|
|
85
|
+
return '[unserializable]';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const SNAPSHOT_TEXT_LIMIT = 4000;
|
|
90
|
+
|
|
91
|
+
function debugLog(cwd: string | undefined, scope: string, data: unknown): void {
|
|
92
|
+
writeSubagentsDebugLog(cwd, scope, data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function truncateSnapshotText(text: string | undefined, limit = SNAPSHOT_TEXT_LIMIT): string | undefined {
|
|
96
|
+
if (text === undefined) return undefined;
|
|
97
|
+
const sanitized = sanitizeInteractionTransportText(text);
|
|
98
|
+
return sanitized.length > limit ? `${sanitized.slice(0, limit - 1)}…` : sanitized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function eventToolCallId(event: any): string | undefined {
|
|
102
|
+
return event?.toolCallId ?? event?.tool_call_id ?? event?.toolUseId ?? event?.id;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resultTextFromContent(content: unknown): string | undefined {
|
|
106
|
+
if (!Array.isArray(content)) return undefined;
|
|
107
|
+
const text = content
|
|
108
|
+
.map((part) => {
|
|
109
|
+
if (!part || typeof part !== 'object') return '';
|
|
110
|
+
const record = part as Record<string, unknown>;
|
|
111
|
+
return typeof record.text === 'string' ? record.text : typeof record.data === 'string' ? record.data : '';
|
|
112
|
+
})
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join('\n');
|
|
115
|
+
return text || undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resultPayload(result: unknown, isError = false): SubagentToolResultPayload {
|
|
119
|
+
let text = '';
|
|
120
|
+
let details: unknown;
|
|
121
|
+
if (typeof result === 'string') text = result;
|
|
122
|
+
else if (result && typeof result === 'object') {
|
|
123
|
+
const record = result as Record<string, unknown>;
|
|
124
|
+
details = record.details;
|
|
125
|
+
const candidate = resultTextFromContent(record.content) ?? record.output ?? record.text ?? record.error ?? record.stderr ?? record.stdout;
|
|
126
|
+
text = typeof candidate === 'string' ? candidate : shortJson(result, SNAPSHOT_TEXT_LIMIT);
|
|
127
|
+
} else if (result !== undefined) text = String(result);
|
|
128
|
+
const bounded = truncateSnapshotText(text, SNAPSHOT_TEXT_LIMIT) ?? '';
|
|
129
|
+
return { content: bounded ? [{ type: 'text', text: bounded }] : [], details, isError, preview: bounded };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isBashTool(name: string): boolean {
|
|
133
|
+
return name === 'bash' || name === 'shell' || name === 'command' || name === 'exec';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function extractStructuredInteractionRequest(value: unknown): SubagentInteractionRequest | undefined {
|
|
137
|
+
return interactionRequestFromCandidate(value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseRawToolJson(text: string): { keys: string[]; kind: string } | undefined {
|
|
141
|
+
const trimmed = text.trim();
|
|
142
|
+
if (!trimmed || !((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')))) return undefined;
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(trimmed);
|
|
145
|
+
if (!parsed || typeof parsed !== 'object') return undefined;
|
|
146
|
+
return { kind: Array.isArray(parsed) ? 'array' : 'object', keys: Array.isArray(parsed) ? [] : Object.keys(parsed).slice(0, 20) };
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class ThreadSnapshotBuilder {
|
|
153
|
+
private cwd?: string;
|
|
154
|
+
private readonly createdAt = new Date().toISOString();
|
|
155
|
+
private readonly items: SubagentThreadItem[] = [];
|
|
156
|
+
private readonly toolIndex = new Map<string, number>();
|
|
157
|
+
private streamingAssistantSequence = 0;
|
|
158
|
+
|
|
159
|
+
constructor(prompt?: string, context?: string, cwd?: string) {
|
|
160
|
+
this.cwd = cwd;
|
|
161
|
+
if (prompt?.trim()) this.items.push({ type: 'user', id: 'delegated-prompt', label: 'delegated_task', text: truncateSnapshotText(prompt) ?? '' });
|
|
162
|
+
if (context?.trim()) this.items.push({ type: 'user', id: 'delegated-context', label: 'context', text: truncateSnapshotText(context) ?? '' });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
update(event: any): void {
|
|
166
|
+
const now = new Date().toISOString();
|
|
167
|
+
const messageEvent = event?.assistantMessageEvent;
|
|
168
|
+
const textDelta = event?.type === 'message_update' && messageEvent?.type !== 'thinking_delta'
|
|
169
|
+
? typeof messageEvent?.delta === 'string'
|
|
170
|
+
? messageEvent.delta
|
|
171
|
+
: messageEvent?.type === 'text_delta' && typeof messageEvent.delta === 'string'
|
|
172
|
+
? messageEvent.delta
|
|
173
|
+
: undefined
|
|
174
|
+
: undefined;
|
|
175
|
+
const thinkingDelta = event?.type === 'message_update' && messageEvent?.type === 'thinking_delta' && typeof messageEvent.delta === 'string'
|
|
176
|
+
? messageEvent.delta
|
|
177
|
+
: undefined;
|
|
178
|
+
if (textDelta !== undefined || thinkingDelta !== undefined) {
|
|
179
|
+
this.appendAssistantDelta(textDelta, thinkingDelta);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (event?.type === 'tool_execution_start') {
|
|
183
|
+
const name = event.toolName ?? event.name ?? 'tool';
|
|
184
|
+
const tool_call_id = eventToolCallId(event);
|
|
185
|
+
this.dropTrailingRawToolJson(name, tool_call_id);
|
|
186
|
+
if (isBashTool(name)) {
|
|
187
|
+
const command = String((event.args ?? event.input ?? {}).command ?? formatToolCall(name, event.args ?? event.input ?? {}));
|
|
188
|
+
const item: any = { type: 'bash', id: tool_call_id, tool_call_id, command: truncateSnapshotText(command) ?? '', status: 'running' };
|
|
189
|
+
this.toolIndex.set(tool_call_id ?? `item-${this.items.length}`, this.items.length);
|
|
190
|
+
this.items.push(item);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const item: SubagentToolItem = { type: 'tool', id: tool_call_id, tool_call_id, name, arguments: event.args ?? event.input ?? {}, status: 'running', started_at: now };
|
|
194
|
+
this.toolIndex.set(tool_call_id ?? `item-${this.items.length}`, this.items.length);
|
|
195
|
+
this.items.push(item);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (event?.type === 'tool_execution_update') {
|
|
199
|
+
const id = eventToolCallId(event);
|
|
200
|
+
const index = id ? this.toolIndex.get(id) : undefined;
|
|
201
|
+
if (index === undefined) return;
|
|
202
|
+
const item: any = this.items[index];
|
|
203
|
+
const payload = resultPayload(event.partialResult, false);
|
|
204
|
+
if (item.type === 'bash') item.output = truncateSnapshotText([item.output, payload.preview].filter(Boolean).join('\n'));
|
|
205
|
+
else if (item.type === 'tool') item.result = payload;
|
|
206
|
+
if (item.type === 'tool') item.status = 'partial';
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (event?.type === 'tool_execution_end') {
|
|
210
|
+
const id = eventToolCallId(event);
|
|
211
|
+
const name = event.toolName ?? event.name ?? 'tool';
|
|
212
|
+
const index = id ? this.toolIndex.get(id) : undefined;
|
|
213
|
+
const payload = resultPayload(event.result ?? event.output ?? event.error, Boolean(event.isError));
|
|
214
|
+
if (index === undefined) {
|
|
215
|
+
this.items.push({ type: 'tool_result', id, tool_call_id: id, name, result: payload });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const item: any = this.items[index];
|
|
219
|
+
if (item.type === 'bash') {
|
|
220
|
+
const result = event.result && typeof event.result === 'object' ? event.result as Record<string, unknown> : {};
|
|
221
|
+
const output = payload.preview ?? '';
|
|
222
|
+
item.output = truncateSnapshotText(output);
|
|
223
|
+
item.truncated = typeof output === 'string' && output.endsWith('…');
|
|
224
|
+
item.exitCode = typeof result.exitCode === 'number' ? result.exitCode : undefined;
|
|
225
|
+
item.status = event.isError ? 'failed' : 'completed';
|
|
226
|
+
} else if (item.type === 'tool') {
|
|
227
|
+
item.status = event.isError ? 'failed' : 'completed';
|
|
228
|
+
item.result = payload;
|
|
229
|
+
item.ended_at = now;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
snapshot(source: SubagentThreadSnapshot['source'] = 'events'): SubagentThreadSnapshot | undefined {
|
|
235
|
+
return boundThreadSnapshot({ version: 1, created_at: this.createdAt, updated_at: new Date().toISOString(), source, items: this.items }, { textLimit: SNAPSHOT_TEXT_LIMIT });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
finalize(messages: any[]): SubagentThreadSnapshot | undefined {
|
|
239
|
+
const messageItems = assistantItemsFromMessages(messages);
|
|
240
|
+
const initialItems: SubagentThreadItem[] = this.items.filter((item) => item.type === 'user');
|
|
241
|
+
const finalMessagesAlreadyHaveThinking = messageItems.some((item) => item.type === 'assistant' && item.message.content.some((part) => part.type === 'thinking'));
|
|
242
|
+
const eventItems = this.items
|
|
243
|
+
.filter((item) => item.type !== 'user')
|
|
244
|
+
.map((item) => this.finalizeEventItem(item, messageItems.length > 0, finalMessagesAlreadyHaveThinking))
|
|
245
|
+
.filter((item): item is SubagentThreadItem => Boolean(item));
|
|
246
|
+
const items: SubagentThreadItem[] = [...initialItems, ...(messageItems.length ? interleaveMessagesWithToolRows(messageItems, eventItems) : eventItems)];
|
|
247
|
+
const source = messageItems.length && eventItems.length ? 'mixed' : messageItems.length ? 'session_messages' : 'events';
|
|
248
|
+
return boundThreadSnapshot({ version: 1, created_at: this.createdAt, updated_at: new Date().toISOString(), source, items }, { textLimit: SNAPSHOT_TEXT_LIMIT });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private dropTrailingRawToolJson(toolName?: string, toolCallId?: string): void {
|
|
252
|
+
const item = this.items.at(-1) as SubagentThreadItem | undefined;
|
|
253
|
+
if (item?.type !== 'assistant' || !item.id?.startsWith('streaming-assistant-')) return;
|
|
254
|
+
const textParts = item.message.content.filter((part): part is { type: 'text'; text: string } => part.type === 'text');
|
|
255
|
+
const hasNonText = item.message.content.some((part) => part.type !== 'text');
|
|
256
|
+
if (hasNonText || !textParts.length) return;
|
|
257
|
+
const text = textParts.map((part) => part.text).join('');
|
|
258
|
+
const parsed = parseRawToolJson(text);
|
|
259
|
+
if (!parsed) return;
|
|
260
|
+
this.items.pop();
|
|
261
|
+
debugLog(this.cwd, 'live_raw_tool_json_dropped', { toolName, toolCallId, jsonKind: parsed.kind, keys: parsed.keys, textLength: text.length });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private appendAssistantDelta(textDelta?: string, thinkingDelta?: string): void {
|
|
265
|
+
let item = this.items.at(-1) as SubagentThreadItem | undefined;
|
|
266
|
+
if (item?.type !== 'assistant' || !item.id?.startsWith('streaming-assistant-')) {
|
|
267
|
+
item = { type: 'assistant', id: `streaming-assistant-${++this.streamingAssistantSequence}`, message: { role: 'assistant', content: [] } };
|
|
268
|
+
this.items.push(item);
|
|
269
|
+
}
|
|
270
|
+
const content = item.message.content as any[];
|
|
271
|
+
if (thinkingDelta) {
|
|
272
|
+
let thinkingPart = content.find((part) => part.type === 'thinking');
|
|
273
|
+
if (!thinkingPart) {
|
|
274
|
+
thinkingPart = { type: 'thinking', text: '', thinking: '' };
|
|
275
|
+
const firstText = content.findIndex((part) => part.type === 'text');
|
|
276
|
+
if (firstText >= 0) content.splice(firstText, 0, thinkingPart);
|
|
277
|
+
else content.push(thinkingPart);
|
|
278
|
+
}
|
|
279
|
+
const thinking = truncateSnapshotText(`${thinkingPart.thinking ?? thinkingPart.text ?? ''}${thinkingDelta}`) ?? '';
|
|
280
|
+
thinkingPart.text = thinking;
|
|
281
|
+
thinkingPart.thinking = thinking;
|
|
282
|
+
}
|
|
283
|
+
if (textDelta) {
|
|
284
|
+
let textPart = content.find((part) => part.type === 'text');
|
|
285
|
+
if (!textPart) {
|
|
286
|
+
textPart = { type: 'text', text: '' };
|
|
287
|
+
content.push(textPart);
|
|
288
|
+
}
|
|
289
|
+
textPart.text = truncateSnapshotText(`${textPart.text ?? ''}${textDelta}`) ?? '';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private finalizeEventItem(item: SubagentThreadItem, hasFinalMessages: boolean, finalMessagesAlreadyHaveThinking: boolean): SubagentThreadItem | undefined {
|
|
294
|
+
if (item.type !== 'assistant' || !item.id?.startsWith('streaming-assistant-')) return item;
|
|
295
|
+
if (!hasFinalMessages) return item;
|
|
296
|
+
if (finalMessagesAlreadyHaveThinking) return undefined;
|
|
297
|
+
const thinkingContent = item.message.content
|
|
298
|
+
.filter((part): part is { type: 'thinking'; text?: string; thinking?: string } => part.type === 'thinking' && Boolean((part.thinking ?? part.text)?.trim()))
|
|
299
|
+
.map((part) => ({ type: 'thinking' as const, text: truncateSnapshotText(part.text), thinking: truncateSnapshotText(part.thinking ?? part.text) }));
|
|
300
|
+
return thinkingContent.length ? { ...item, message: { ...item.message, content: thinkingContent } } : undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function assistantToolCallIds(item: SubagentThreadItem): Set<string> {
|
|
305
|
+
const ids = new Set<string>();
|
|
306
|
+
if (item.type !== 'assistant') return ids;
|
|
307
|
+
for (const part of item.message.content) if (part.type === 'toolCall') ids.add(part.id);
|
|
308
|
+
return ids;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function toolRowId(item: SubagentThreadItem): string | undefined {
|
|
312
|
+
if (item.type === 'tool' || item.type === 'tool_result') return item.tool_call_id ?? item.id;
|
|
313
|
+
if (item.type === 'bash') return item.tool_call_id ?? item.id;
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function interleaveMessagesWithToolRows(messageItems: SubagentThreadItem[], eventItems: SubagentThreadItem[]): SubagentThreadItem[] {
|
|
318
|
+
const used = new Set<number>();
|
|
319
|
+
const ordered: SubagentThreadItem[] = [];
|
|
320
|
+
const deferredMessages: SubagentThreadItem[] = [];
|
|
321
|
+
for (const messageItem of messageItems) {
|
|
322
|
+
const ids = assistantToolCallIds(messageItem);
|
|
323
|
+
if (!ids.size) {
|
|
324
|
+
deferredMessages.push(messageItem);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
for (let index = 0; index < eventItems.length; index++) {
|
|
328
|
+
if (used.has(index)) continue;
|
|
329
|
+
const id = toolRowId(eventItems[index]!);
|
|
330
|
+
if (id && ids.has(id)) break;
|
|
331
|
+
ordered.push(eventItems[index]!);
|
|
332
|
+
used.add(index);
|
|
333
|
+
}
|
|
334
|
+
ordered.push(messageItem);
|
|
335
|
+
for (let index = 0; index < eventItems.length; index++) {
|
|
336
|
+
if (used.has(index)) continue;
|
|
337
|
+
const id = toolRowId(eventItems[index]!);
|
|
338
|
+
if (id && ids.has(id)) {
|
|
339
|
+
ordered.push(eventItems[index]!);
|
|
340
|
+
used.add(index);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (let index = 0; index < eventItems.length; index++) if (!used.has(index)) ordered.push(eventItems[index]!);
|
|
345
|
+
ordered.push(...deferredMessages);
|
|
346
|
+
return ordered;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function assistantItemsFromMessages(messages: any[]): SubagentThreadItem[] {
|
|
350
|
+
const items: SubagentThreadItem[] = [];
|
|
351
|
+
for (const msg of messages) {
|
|
352
|
+
if (msg?.role !== 'assistant') continue;
|
|
353
|
+
if (typeof msg.content === 'string') {
|
|
354
|
+
if (msg.content.trim()) items.push({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: truncateSnapshotText(msg.content) ?? '' }], usage: msg.usage } });
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (!Array.isArray(msg.content)) continue;
|
|
358
|
+
const content = msg.content.flatMap((part: any) => {
|
|
359
|
+
if (part?.type === 'text' && typeof part.text === 'string') return [{ type: 'text' as const, text: truncateSnapshotText(part.text) ?? '' }];
|
|
360
|
+
if (part?.type === 'thinking') {
|
|
361
|
+
const thinking = typeof part.thinking === 'string' ? part.thinking : typeof part.text === 'string' ? part.text : '';
|
|
362
|
+
return thinking ? [{ type: 'thinking' as const, text: truncateSnapshotText(thinking), thinking: truncateSnapshotText(thinking) }] : [];
|
|
363
|
+
}
|
|
364
|
+
if ((part?.type === 'toolCall' || part?.type === 'tool_call') && typeof part.name === 'string') return [{ type: 'toolCall' as const, id: String(part.id ?? part.toolCallId ?? part.tool_call_id ?? part.name), name: part.name, arguments: part.arguments ?? part.args ?? part.input ?? {} }];
|
|
365
|
+
return [];
|
|
366
|
+
});
|
|
367
|
+
if (content.length) items.push({ type: 'assistant', id: msg.id, message: { role: 'assistant', content, stopReason: msg.stopReason, errorMessage: msg.errorMessage, usage: msg.usage } });
|
|
368
|
+
}
|
|
369
|
+
return items;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const SUBAGENT_INTERACTION_SESSION_REGISTRY_KEY = Symbol.for('pi.subagents.interactionSessions');
|
|
373
|
+
|
|
374
|
+
type SubagentInteractionSessionMetadata = {
|
|
375
|
+
origin: 'subagent';
|
|
376
|
+
requester: { subagentName: string; description?: string; taskId?: string };
|
|
377
|
+
parent?: { piSessionId?: string };
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
function subagentInteractionRegistry(): Map<string, SubagentInteractionSessionMetadata> {
|
|
381
|
+
const holder = globalThis as Record<symbol, unknown>;
|
|
382
|
+
const existing = holder[SUBAGENT_INTERACTION_SESSION_REGISTRY_KEY];
|
|
383
|
+
if (existing instanceof Map) return existing as Map<string, SubagentInteractionSessionMetadata>;
|
|
384
|
+
const registry = new Map<string, SubagentInteractionSessionMetadata>();
|
|
385
|
+
holder[SUBAGENT_INTERACTION_SESSION_REGISTRY_KEY] = registry;
|
|
386
|
+
return registry;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function registerInteractionSubagentSession(session: any, definition: SubagentDefinition, taskId?: string, parentPiSessionId?: string): () => void {
|
|
390
|
+
const sessionId = session?.sessionManager?.getSessionId?.() ?? session?.sessionId;
|
|
391
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) return () => undefined;
|
|
392
|
+
const registry = subagentInteractionRegistry();
|
|
393
|
+
const previous = registry.get(sessionId);
|
|
394
|
+
registry.set(sessionId, {
|
|
395
|
+
origin: 'subagent',
|
|
396
|
+
requester: { subagentName: definition.name, description: definition.description, taskId },
|
|
397
|
+
parent: parentPiSessionId ? { piSessionId: parentPiSessionId } : undefined,
|
|
398
|
+
});
|
|
399
|
+
return () => {
|
|
400
|
+
if (previous) registry.set(sessionId, previous);
|
|
401
|
+
else registry.delete(sessionId);
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function formatToolCall(name: string, args: any): string {
|
|
406
|
+
const input = args ?? {};
|
|
407
|
+
if (name === 'read') {
|
|
408
|
+
const file = input.path ?? input.file_path ?? input.file ?? '';
|
|
409
|
+
const offset = input.offset ?? 1;
|
|
410
|
+
const limit = input.limit;
|
|
411
|
+
const range = limit ? `:${offset}-${offset + limit - 1}` : offset && offset !== 1 ? `:${offset}` : '';
|
|
412
|
+
return `read ${file}${range}`.trim();
|
|
413
|
+
}
|
|
414
|
+
if (name === 'bash') return `bash ${String(input.command ?? '').split('\n')[0] ?? ''}`.trim();
|
|
415
|
+
if (name === 'edit') return `edit ${input.path ?? input.file_path ?? ''}`.trim();
|
|
416
|
+
if (name === 'write') return `write ${input.path ?? input.file_path ?? ''}`.trim();
|
|
417
|
+
if (name.startsWith('memory_')) return name;
|
|
418
|
+
return `${name} ${shortJson(input)}`.trim();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function eventTranscript(event: any): string {
|
|
422
|
+
const messageEvent = event?.assistantMessageEvent;
|
|
423
|
+
const delta = messageEvent?.type !== 'thinking_delta' && typeof messageEvent?.delta === 'string'
|
|
424
|
+
? messageEvent.delta
|
|
425
|
+
: messageEvent?.type === 'text_delta' && typeof messageEvent.delta === 'string'
|
|
426
|
+
? messageEvent.delta
|
|
427
|
+
: undefined;
|
|
428
|
+
if (event?.type === 'message_update' && typeof delta === 'string') return sanitizeInteractionTransportText(delta);
|
|
429
|
+
|
|
430
|
+
if (event?.type === 'tool_execution_start') {
|
|
431
|
+
const name = event.toolName ?? 'tool';
|
|
432
|
+
return `\n\n${formatToolCall(name, event.args ?? event.input ?? {})}\n`;
|
|
433
|
+
}
|
|
434
|
+
if (event?.type === 'tool_execution_update') return event.partialResult ? `\n${sanitizeInteractionTransportText(shortJson(event.partialResult, 500))}\n` : '';
|
|
435
|
+
if (event?.type === 'tool_execution_end') return `\n${event.isError ? 'failed' : 'done'}\n`;
|
|
436
|
+
if (event?.type === 'message_start' && event.message?.role === 'assistant') return '\n\nPreparing for response\n\n';
|
|
437
|
+
return '';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function activityMessage(event: any, transcriptChunk: string): string | undefined {
|
|
441
|
+
if (!transcriptChunk.trim()) return undefined;
|
|
442
|
+
if (event?.type === 'message_start' && event.message?.role === 'assistant') return 'preparing response';
|
|
443
|
+
if (event?.type === 'tool_execution_start') return formatToolCall(event.toolName ?? 'tool', event.args ?? event.input ?? {});
|
|
444
|
+
if (event?.type === 'tool_execution_end') return event.isError ? 'tool failed' : 'tool completed';
|
|
445
|
+
if (event?.type === 'tool_execution_update') return 'tool update';
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function promptWithInactivity(
|
|
450
|
+
session: any,
|
|
451
|
+
prompt: string,
|
|
452
|
+
stallTimeoutMs: number,
|
|
453
|
+
signal: AbortSignal,
|
|
454
|
+
onActivity?: (activity: { message: string; output?: string; prompt?: string; system_prompt?: string; transcript?: string; usage?: UsageStats; effort?: ThinkingEffort; thread_snapshot?: SubagentThreadSnapshot; interaction_request?: SubagentInteractionRequest }) => void,
|
|
455
|
+
delegatedContext?: string,
|
|
456
|
+
cwd?: string,
|
|
457
|
+
systemPrompt?: string,
|
|
458
|
+
taskId?: string,
|
|
459
|
+
): Promise<{ result: string; usage: UsageStats; thread_snapshot?: SubagentThreadSnapshot; interaction_request?: SubagentInteractionRequest }> {
|
|
460
|
+
let output = '';
|
|
461
|
+
const snapshotBuilder = new ThreadSnapshotBuilder(prompt, delegatedContext, cwd);
|
|
462
|
+
let latestInteractionRequest: SubagentInteractionRequest | undefined;
|
|
463
|
+
let usage = emptyUsage();
|
|
464
|
+
let transcript = `${systemPrompt ? `# system prompt\n\n${systemPrompt}\n\n` : ''}# delegated prompt\n\n${prompt}\n\n# subagent execution\n`;
|
|
465
|
+
let lastActivity = Date.now();
|
|
466
|
+
let stalled = false;
|
|
467
|
+
let sawToolActivity = false;
|
|
468
|
+
onActivity?.({ message: 'session started', prompt, system_prompt: systemPrompt, transcript, usage, thread_snapshot: snapshotBuilder.snapshot() });
|
|
469
|
+
const unsubscribe = session.subscribe?.((event: any) => {
|
|
470
|
+
lastActivity = Date.now();
|
|
471
|
+
debugLog(cwd, 'runner_event', {
|
|
472
|
+
type: event?.type,
|
|
473
|
+
messageRole: event?.message?.role,
|
|
474
|
+
assistantEventType: event?.assistantMessageEvent?.type,
|
|
475
|
+
hasDelta: typeof event?.assistantMessageEvent?.delta === 'string',
|
|
476
|
+
toolName: event?.toolName,
|
|
477
|
+
toolCallId: event?.toolCallId,
|
|
478
|
+
isError: event?.isError,
|
|
479
|
+
resultKeys: event?.result && typeof event.result === 'object' ? Object.keys(event.result) : undefined,
|
|
480
|
+
interactionCarrier: event?.type === 'tool_execution_end' ? summarizeInteractionCarrier(event?.result) : undefined,
|
|
481
|
+
});
|
|
482
|
+
if (typeof event?.type === 'string' && event.type.startsWith('tool_execution_')) {
|
|
483
|
+
sawToolActivity = true;
|
|
484
|
+
registerSubagentRuntimeToolDefinition(taskId, event?.toolName, session.getToolDefinition?.(event?.toolName));
|
|
485
|
+
}
|
|
486
|
+
snapshotBuilder.update(event);
|
|
487
|
+
const thread_snapshot = snapshotBuilder.snapshot();
|
|
488
|
+
const transcriptChunk = eventTranscript(event);
|
|
489
|
+
transcript += transcriptChunk;
|
|
490
|
+
const interactionRequest = extractStructuredInteractionRequest(event?.result ?? event?.partialResult ?? event);
|
|
491
|
+
if (interactionRequest) {
|
|
492
|
+
latestInteractionRequest = interactionRequest;
|
|
493
|
+
debugLog(cwd, 'interaction_bridge_payload_detected', {
|
|
494
|
+
requestId: interactionRequest.requestId,
|
|
495
|
+
kind: interactionRequest.kind,
|
|
496
|
+
origin: interactionRequest.origin,
|
|
497
|
+
requester: interactionRequest.requester,
|
|
498
|
+
hasPrompt: Boolean(interactionRequest.prompt),
|
|
499
|
+
hasPayload: interactionRequest.payload !== undefined,
|
|
500
|
+
});
|
|
501
|
+
onActivity?.({ message: 'interaction required', output, transcript, usage, thread_snapshot, interaction_request: latestInteractionRequest });
|
|
502
|
+
} else if (event?.type === 'tool_execution_end' && event?.isError) {
|
|
503
|
+
const latest = consumeLatestInteractionRequest({ origin: 'subagent' });
|
|
504
|
+
if (latest) {
|
|
505
|
+
latestInteractionRequest = latest;
|
|
506
|
+
debugLog(cwd, 'interaction_bridge_payload_recovered_from_channel', {
|
|
507
|
+
requestId: latest.requestId,
|
|
508
|
+
kind: latest.kind,
|
|
509
|
+
origin: latest.origin,
|
|
510
|
+
requester: latest.requester,
|
|
511
|
+
hasPrompt: Boolean(latest.prompt),
|
|
512
|
+
hasPayload: latest.payload !== undefined,
|
|
513
|
+
carrier: summarizeInteractionCarrier(event.result),
|
|
514
|
+
});
|
|
515
|
+
onActivity?.({ message: 'interaction required', output, transcript, usage, thread_snapshot, interaction_request: latestInteractionRequest });
|
|
516
|
+
} else {
|
|
517
|
+
debugLog(cwd, 'interaction_bridge_payload_missing', { toolName: event.toolName, carrier: summarizeInteractionCarrier(event.result) });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const messageEvent = event?.assistantMessageEvent;
|
|
521
|
+
const delta = messageEvent?.type !== 'thinking_delta' && typeof messageEvent?.delta === 'string'
|
|
522
|
+
? messageEvent.delta
|
|
523
|
+
: messageEvent?.type === 'text_delta' && typeof messageEvent.delta === 'string'
|
|
524
|
+
? messageEvent.delta
|
|
525
|
+
: undefined;
|
|
526
|
+
if (event?.type === 'message_end' && event.message?.role === 'assistant') usage = addUsage(usage, event.message.usage);
|
|
527
|
+
if (event?.type === 'message_update' && typeof delta === 'string') {
|
|
528
|
+
output += sanitizeInteractionTransportText(delta);
|
|
529
|
+
onActivity?.({ message: 'streaming response', output, transcript, usage, thread_snapshot, interaction_request: latestInteractionRequest });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (event?.type === 'message_update' && messageEvent?.type === 'thinking_delta') {
|
|
533
|
+
onActivity?.({ message: 'streaming thinking', output, transcript, usage, thread_snapshot, interaction_request: latestInteractionRequest });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const message = activityMessage(event, transcriptChunk);
|
|
537
|
+
if (message) onActivity?.({ message, transcript, usage, thread_snapshot, interaction_request: latestInteractionRequest });
|
|
538
|
+
}) ?? (() => {});
|
|
539
|
+
const interval = setInterval(() => {
|
|
540
|
+
if (!stalled && Date.now() - lastActivity > stallTimeoutMs) {
|
|
541
|
+
stalled = true;
|
|
542
|
+
transcript += `\n\n--- stall ---\nstalled for ${stallTimeoutMs}ms; aborting session\n`;
|
|
543
|
+
onActivity?.({ message: `stalled for ${stallTimeoutMs}ms; aborting session`, output, transcript, usage, thread_snapshot: snapshotBuilder.snapshot(), interaction_request: latestInteractionRequest });
|
|
544
|
+
session.abort?.().catch?.(() => {});
|
|
545
|
+
}
|
|
546
|
+
}, Math.min(5000, Math.max(500, stallTimeoutMs / 4)));
|
|
547
|
+
try {
|
|
548
|
+
let promptError: unknown;
|
|
549
|
+
try {
|
|
550
|
+
await session.prompt(prompt, { signal });
|
|
551
|
+
} catch (error) {
|
|
552
|
+
promptError = error;
|
|
553
|
+
}
|
|
554
|
+
const thread_snapshot = snapshotBuilder.finalize(session.messages ?? []);
|
|
555
|
+
debugLog(cwd, 'runner_final_snapshot', { source: thread_snapshot?.source, items: thread_snapshot?.items.map((item) => ({ type: item.type, label: (item as any).label, name: (item as any).name, status: (item as any).status, assistantContent: item.type === 'assistant' ? item.message.content.map((part: any) => part.type) : undefined })) });
|
|
556
|
+
if (stalled) {
|
|
557
|
+
const message = `Subagent stalled for ${stallTimeoutMs}ms without final response.`;
|
|
558
|
+
transcript += `\n\n# subagent failure\n\n${message}`;
|
|
559
|
+
onActivity?.({ message: `failed: ${message}`, output: '', transcript, usage, thread_snapshot, interaction_request: latestInteractionRequest });
|
|
560
|
+
throw new NonRetryableSubagentError(message);
|
|
561
|
+
}
|
|
562
|
+
if (promptError) throw promptError;
|
|
563
|
+
const messageText = collectAssistantText(session.messages ?? []);
|
|
564
|
+
const streamedFallback = sawToolActivity ? '' : output.trim();
|
|
565
|
+
const collected = sanitizeInteractionTransportText(messageText || streamedFallback);
|
|
566
|
+
if (!collected.trim()) {
|
|
567
|
+
const message = sawToolActivity
|
|
568
|
+
? 'Subagent completed tool execution but did not produce a final response.'
|
|
569
|
+
: 'Subagent finished without a final response.';
|
|
570
|
+
transcript += `\n\n# subagent failure\n\n${message}`;
|
|
571
|
+
onActivity?.({ message: `failed: ${message}`, output: '', transcript, usage, thread_snapshot, interaction_request: latestInteractionRequest });
|
|
572
|
+
const error = sawToolActivity ? new NonRetryableSubagentError(message) : new Error(message);
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
transcript += `\n\n# final assistant text\n\n${collected}`;
|
|
576
|
+
onActivity?.({ message: 'collected final response', output: collected, transcript, usage, thread_snapshot, interaction_request: latestInteractionRequest });
|
|
577
|
+
return { result: collected, usage, thread_snapshot, interaction_request: latestInteractionRequest };
|
|
578
|
+
} finally {
|
|
579
|
+
clearInterval(interval);
|
|
580
|
+
unsubscribe();
|
|
581
|
+
await session.dispose?.();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function collectAssistantText(messages: any[]): string {
|
|
586
|
+
const parts: string[] = [];
|
|
587
|
+
for (const msg of messages) {
|
|
588
|
+
if (msg.role !== 'assistant') continue;
|
|
589
|
+
const content = msg.content;
|
|
590
|
+
if (typeof content === 'string') parts.push(content);
|
|
591
|
+
if (Array.isArray(content)) {
|
|
592
|
+
for (const part of content) if (part?.type === 'text' && typeof part.text === 'string') parts.push(part.text);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return parts.join('\n').trim();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let piSdkModulePromise: Promise<any> | undefined;
|
|
599
|
+
|
|
600
|
+
async function loadPiSdkModule(): Promise<any> {
|
|
601
|
+
const moduleName = '@earendil-works/pi-coding-agent';
|
|
602
|
+
piSdkModulePromise ??= import(moduleName) as Promise<any>;
|
|
603
|
+
return piSdkModulePromise;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function createSession(model: any, cwd: string, tools: string[], effort: ThinkingEffort | undefined, config: SubagentsConfig, ctx: any, systemPrompt: string) {
|
|
607
|
+
const piSdk = await loadPiSdkModule();
|
|
608
|
+
const { createAgentSession, SessionManager } = piSdk;
|
|
609
|
+
const options: Record<string, unknown> = {
|
|
610
|
+
cwd,
|
|
611
|
+
model,
|
|
612
|
+
thinkingLevel: effort,
|
|
613
|
+
tools,
|
|
614
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
615
|
+
};
|
|
616
|
+
if (ctx?.authStorage) options.authStorage = ctx.authStorage;
|
|
617
|
+
if (ctx?.modelRegistry) options.modelRegistry = ctx.modelRegistry;
|
|
618
|
+
if (ctx?.settingsManager) options.settingsManager = ctx.settingsManager;
|
|
619
|
+
if (config.session_resources === 'lean') {
|
|
620
|
+
const DefaultResourceLoader = piSdk.DefaultResourceLoader;
|
|
621
|
+
const agentDir = typeof piSdk.getAgentDir === 'function' ? piSdk.getAgentDir() : undefined;
|
|
622
|
+
if (typeof DefaultResourceLoader !== 'function') throw new Error('Subagent lean session resources require DefaultResourceLoader from Pi SDK.');
|
|
623
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
624
|
+
cwd,
|
|
625
|
+
agentDir,
|
|
626
|
+
settingsManager: ctx?.settingsManager,
|
|
627
|
+
noSkills: true,
|
|
628
|
+
noPromptTemplates: true,
|
|
629
|
+
noThemes: true,
|
|
630
|
+
noContextFiles: true,
|
|
631
|
+
systemPrompt,
|
|
632
|
+
extensionsOverride: isolateSubagentExtensions,
|
|
633
|
+
});
|
|
634
|
+
await resourceLoader.reload();
|
|
635
|
+
options.agentDir = agentDir;
|
|
636
|
+
options.resourceLoader = resourceLoader;
|
|
637
|
+
}
|
|
638
|
+
return createAgentSession(options);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function selectedModel(input: { ctx: any; definition: SubagentDefinition; profile: EffectiveSubagentProfile }): any | undefined {
|
|
642
|
+
const ref = input.profile.model.value;
|
|
643
|
+
if (!ref) return input.ctx?.model;
|
|
644
|
+
if (input.profile.model.source === 'orchestrator') return input.ctx?.model ?? resolveModel(input.ctx, ref);
|
|
645
|
+
const resolved = resolveModel(input.ctx, ref);
|
|
646
|
+
if (!resolved) throw new Error(`Subagent ${input.definition.name} could not resolve selected model ${modelRefLabel(ref)} (${input.profile.model.source}).`);
|
|
647
|
+
return resolved;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export const sdkSubagentRunner: SubagentRunner = async ({ definition, task, taskId, parentPiSessionId, context, cwd, ctx, config, signal, effectiveProfile, onActivity }) => {
|
|
651
|
+
const profile = effectiveProfile ?? resolveEffectiveSubagentProfile({ agentName: definition.name, definition, config, ctx });
|
|
652
|
+
const preferred = selectedModel({ ctx, definition, profile });
|
|
653
|
+
const current = ctx?.model;
|
|
654
|
+
const effort = profile.effort.value;
|
|
655
|
+
const tools = definition.tools?.length ? definition.tools : config.default_tools;
|
|
656
|
+
const systemPrompt = definition.instructions;
|
|
657
|
+
const prompt = buildPrompt(definition, task, context, tools);
|
|
658
|
+
onActivity?.({ message: 'orchestrator prompt prepared', prompt, system_prompt: systemPrompt, transcript: `# system prompt\n\n${systemPrompt}\n\n# delegated prompt\n\n${prompt}\n`, effort });
|
|
659
|
+
|
|
660
|
+
async function attempt(model: any) {
|
|
661
|
+
onActivity?.({ message: `starting ${definition.name} with model ${modelLabel(model) ?? 'unknown'}${effort ? ` effort ${effort}` : ''}`, prompt, system_prompt: systemPrompt, effort });
|
|
662
|
+
const { session } = await createSession(model, cwd, tools, effort, config, ctx, systemPrompt);
|
|
663
|
+
const unregisterInteractionSession = registerInteractionSubagentSession(session, definition, taskId, parentPiSessionId ?? ctx?.sessionManager?.getSessionId?.());
|
|
664
|
+
try {
|
|
665
|
+
const effectiveSystemPrompt = typeof session.systemPrompt === 'string' ? session.systemPrompt : systemPrompt;
|
|
666
|
+
const { result, usage, thread_snapshot, interaction_request } = await promptWithInactivity(session, prompt, config.stall_timeout_ms, signal, onActivity, context, cwd, effectiveSystemPrompt, taskId);
|
|
667
|
+
return { result, usage, thread_snapshot, interaction_request, system_prompt: effectiveSystemPrompt };
|
|
668
|
+
} finally {
|
|
669
|
+
unregisterInteractionSession();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
const { result, usage, thread_snapshot, interaction_request, system_prompt } = await attempt(preferred);
|
|
675
|
+
return { result, usage, thread_snapshot, interaction_request, system_prompt, model: modelLabel(preferred) ?? modelRefLabel(profile.model.value), effort, fallback_used: false };
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (signal.aborted) throw new Error('Subagent was aborted');
|
|
678
|
+
if (isNonRetryableSubagentError(error)) throw error;
|
|
679
|
+
const preferredLabel = modelLabel(preferred) ?? modelRefLabel(profile.model.value) ?? 'unknown';
|
|
680
|
+
const currentLabel = modelLabel(current) ?? 'unknown';
|
|
681
|
+
onActivity?.({ message: `failed/stalled on ${preferredLabel}; falling back to ${currentLabel}`, effort });
|
|
682
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
683
|
+
ctx?.ui?.notify?.(`Subagent ${definition.name} failed/stalled on selected model ${preferredLabel}: ${message}. Falling back to current model ${currentLabel}.`, 'warning');
|
|
684
|
+
if (!current || current === preferred) throw new Error(`Subagent ${definition.name} failed on selected model ${preferredLabel}: ${message}`);
|
|
685
|
+
const { result, usage, thread_snapshot, interaction_request, system_prompt } = await attempt(current);
|
|
686
|
+
return { result, usage, thread_snapshot, interaction_request, system_prompt, model: currentLabel, effort, fallback_used: true };
|
|
687
|
+
}
|
|
688
|
+
};
|