@giladbarnea/pi-user-agents 0.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/INTERNALS.md +65 -0
- package/PARSER_SPEC.md +264 -0
- package/README.md +127 -0
- package/agent-viewer.ts +653 -0
- package/autocomplete.ts +576 -0
- package/command-line.ts +746 -0
- package/context-meter.ts +41 -0
- package/editor-coloring.ts +160 -0
- package/index.ts +74 -0
- package/package.json +44 -0
- package/runner.ts +591 -0
- package/shared.ts +214 -0
- package/tool-render/FORK.md +108 -0
- package/tool-render/ansi.ts +77 -0
- package/tool-render/constants.ts +48 -0
- package/tool-render/diff.ts +957 -0
- package/tool-render/index.ts +137 -0
- package/tool-render/text.ts +212 -0
- package/tool-render/theme.ts +71 -0
- package/tool-render/tools.ts +502 -0
- package/transcript.ts +303 -0
- package/widget.ts +523 -0
package/runner.ts
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type AgentSessionEvent,
|
|
5
|
+
type AgentSessionServices,
|
|
6
|
+
type Args,
|
|
7
|
+
buildSessionContext,
|
|
8
|
+
createAgentSessionFromServices,
|
|
9
|
+
createAgentSessionServices,
|
|
10
|
+
getAgentDir,
|
|
11
|
+
type ModelRuntime,
|
|
12
|
+
parseArgs,
|
|
13
|
+
resolveCliModel,
|
|
14
|
+
SessionManager,
|
|
15
|
+
SettingsManager,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { type AgentValueValidator, parseAgentCommand } from "./command-line.js";
|
|
18
|
+
import type {
|
|
19
|
+
AgentCommandName,
|
|
20
|
+
AgentMessage,
|
|
21
|
+
AgentResultMessage,
|
|
22
|
+
AgentSession,
|
|
23
|
+
ExtensionAPI,
|
|
24
|
+
ExtensionCommandContext,
|
|
25
|
+
Model,
|
|
26
|
+
ParsedAgentCommand,
|
|
27
|
+
RunningAgent,
|
|
28
|
+
ThinkingLevel,
|
|
29
|
+
} from "./shared.js";
|
|
30
|
+
import { errorMessage, formatModel, formatModelLabel, logSteering } from "./shared.js";
|
|
31
|
+
import {
|
|
32
|
+
buildAgentResultMessage,
|
|
33
|
+
formatStartNotification,
|
|
34
|
+
reportCommandError,
|
|
35
|
+
} from "./transcript.js";
|
|
36
|
+
import type { UserAgentWidget } from "./widget.js";
|
|
37
|
+
|
|
38
|
+
export { parseAgentCommand };
|
|
39
|
+
|
|
40
|
+
export async function handleAgentCommand(
|
|
41
|
+
pi: ExtensionAPI,
|
|
42
|
+
runningAgents: Set<RunningAgent>,
|
|
43
|
+
widget: UserAgentWidget,
|
|
44
|
+
isShuttingDown: () => boolean,
|
|
45
|
+
nextAgentNumber: () => number,
|
|
46
|
+
command: AgentCommandName,
|
|
47
|
+
args: string,
|
|
48
|
+
invocation: string,
|
|
49
|
+
ctx: ExtensionCommandContext,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
await startUserAgent(
|
|
53
|
+
pi,
|
|
54
|
+
runningAgents,
|
|
55
|
+
widget,
|
|
56
|
+
isShuttingDown,
|
|
57
|
+
nextAgentNumber,
|
|
58
|
+
command,
|
|
59
|
+
args,
|
|
60
|
+
invocation,
|
|
61
|
+
ctx,
|
|
62
|
+
);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
reportCommandError(pi, command, args, ctx, errorMessage(error));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function startUserAgent(
|
|
69
|
+
pi: ExtensionAPI,
|
|
70
|
+
runningAgents: Set<RunningAgent>,
|
|
71
|
+
widget: UserAgentWidget,
|
|
72
|
+
isShuttingDown: () => boolean,
|
|
73
|
+
nextAgentNumber: () => number,
|
|
74
|
+
command: AgentCommandName,
|
|
75
|
+
args: string,
|
|
76
|
+
invocation: string,
|
|
77
|
+
ctx: ExtensionCommandContext,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const parsed = parseAgentCommand(args, command);
|
|
80
|
+
const parsedForwardedArgs = parseForwardedArgs(parsed.forwardedArgs);
|
|
81
|
+
const agentDir = getAgentDir();
|
|
82
|
+
const settingsManager = SettingsManager.create(ctx.cwd, agentDir, {
|
|
83
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
84
|
+
});
|
|
85
|
+
const services = await createAgentSessionServices({
|
|
86
|
+
cwd: ctx.cwd,
|
|
87
|
+
agentDir,
|
|
88
|
+
settingsManager,
|
|
89
|
+
modelRuntime: (ctx.modelRegistry as unknown as { runtime: ModelRuntime }).runtime,
|
|
90
|
+
resourceLoaderOptions: buildChildResourceLoaderOptions(parsedForwardedArgs, ctx.cwd),
|
|
91
|
+
});
|
|
92
|
+
const forwarded = resolveForwardedOptions(parsedForwardedArgs, services.modelRuntime);
|
|
93
|
+
const selectedModel = forwarded.model ?? ctx.model;
|
|
94
|
+
if (!selectedModel) throw new Error("No current model is selected; pass -m MODELNAME");
|
|
95
|
+
const model =
|
|
96
|
+
services.modelRuntime.getModel(selectedModel.provider, selectedModel.id) ?? selectedModel;
|
|
97
|
+
const thinkingLevel = forwarded.thinkingLevel ?? pi.getThinkingLevel();
|
|
98
|
+
const inheritedMessages = parsed.isolate ? [] : buildInheritedMessages(ctx);
|
|
99
|
+
const runningAgent = createRunningAgent(nextAgentNumber(), command, model, parsed, invocation);
|
|
100
|
+
|
|
101
|
+
runningAgents.add(runningAgent);
|
|
102
|
+
logSteering(runningAgent.id, "agent-created", { command, taskLength: parsed.task.length });
|
|
103
|
+
if (ctx.hasUI) {
|
|
104
|
+
widget.setUI(ctx.ui);
|
|
105
|
+
for (const warning of parsed.warnings) ctx.ui.notify(warning, "warning");
|
|
106
|
+
ctx.ui.notify(formatStartNotification(runningAgent), "info");
|
|
107
|
+
widget.ensureTimer();
|
|
108
|
+
widget.update();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
runningAgent.finished = runAgentLifecycle(
|
|
112
|
+
pi,
|
|
113
|
+
isShuttingDown,
|
|
114
|
+
parsed.task,
|
|
115
|
+
model,
|
|
116
|
+
thinkingLevel,
|
|
117
|
+
forwarded,
|
|
118
|
+
inheritedMessages,
|
|
119
|
+
services,
|
|
120
|
+
runningAgent,
|
|
121
|
+
widget,
|
|
122
|
+
).finally(() => {
|
|
123
|
+
logSteering(runningAgent.id, "agent-disposed", { status: runningAgent.status });
|
|
124
|
+
runningAgent.session?.dispose();
|
|
125
|
+
runningAgents.delete(runningAgent);
|
|
126
|
+
widget.update();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createRunningAgent(
|
|
131
|
+
sequenceNumber: number,
|
|
132
|
+
command: AgentCommandName,
|
|
133
|
+
model: Model,
|
|
134
|
+
parsed: ParsedAgentCommand,
|
|
135
|
+
invocation: string,
|
|
136
|
+
): RunningAgent {
|
|
137
|
+
return {
|
|
138
|
+
id: `user-${sequenceNumber.toString(36)}`,
|
|
139
|
+
command,
|
|
140
|
+
inheritedContext: !parsed.isolate,
|
|
141
|
+
model: formatModel(model),
|
|
142
|
+
modelLabel: formatModelLabel(model),
|
|
143
|
+
task: parsed.task,
|
|
144
|
+
invocation,
|
|
145
|
+
notifyMainAgent: parsed.context,
|
|
146
|
+
mainContextState: parsed.context ? "will-join" : "separate",
|
|
147
|
+
status: "starting",
|
|
148
|
+
startedAt: Date.now(),
|
|
149
|
+
turnStartedAt: Date.now(),
|
|
150
|
+
activeTools: new Map(),
|
|
151
|
+
toolUses: 0,
|
|
152
|
+
turnCount: 0,
|
|
153
|
+
responseText: "",
|
|
154
|
+
conversationMessages: [],
|
|
155
|
+
finished: Promise.resolve(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function postUserAgentResult(
|
|
160
|
+
pi: ExtensionAPI,
|
|
161
|
+
isShuttingDown: () => boolean,
|
|
162
|
+
notifyMainAgent: boolean,
|
|
163
|
+
message: AgentResultMessage,
|
|
164
|
+
): boolean {
|
|
165
|
+
if (isShuttingDown()) return false;
|
|
166
|
+
if (notifyMainAgent) {
|
|
167
|
+
// Steers into a streaming turn, or triggers an immediate response when idle.
|
|
168
|
+
pi.sendMessage(message, { triggerTurn: true });
|
|
169
|
+
} else {
|
|
170
|
+
// TUI-only session entry: renders in the transcript but never enters the main agent's context.
|
|
171
|
+
pi.appendEntry(message.customType, { content: message.content, details: message.details });
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Session inputs derived from forwarded pi CLI options, ready to hand to the child session. */
|
|
177
|
+
type ChildResourceLoaderOptions = {
|
|
178
|
+
systemPrompt?: string;
|
|
179
|
+
appendSystemPrompt?: string[];
|
|
180
|
+
additionalExtensionPaths?: string[];
|
|
181
|
+
additionalSkillPaths?: string[];
|
|
182
|
+
additionalPromptTemplatePaths?: string[];
|
|
183
|
+
noExtensions?: boolean;
|
|
184
|
+
noSkills?: boolean;
|
|
185
|
+
noPromptTemplates?: boolean;
|
|
186
|
+
noThemes?: boolean;
|
|
187
|
+
noContextFiles?: boolean;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
type ForwardedOptions = {
|
|
191
|
+
model?: Model;
|
|
192
|
+
thinkingLevel?: ThinkingLevel;
|
|
193
|
+
tools?: string[];
|
|
194
|
+
excludeTools?: string[];
|
|
195
|
+
noTools?: "all" | "builtin";
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** Parse forwarded pi CLI tokens once before model and session resolution. */
|
|
199
|
+
export function parseForwardedArgs(forwardedArgs: string[]): Args {
|
|
200
|
+
const parsed = parseArgs(forwardedArgs);
|
|
201
|
+
const errors = parsed.diagnostics.filter((diagnostic) => diagnostic.type === "error");
|
|
202
|
+
if (errors.length > 0) throw new Error(errors.map((diagnostic) => diagnostic.message).join("; "));
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build editor value validation from the same Pi parser, model resolver, and live catalogs
|
|
208
|
+
* used at submission and session creation.
|
|
209
|
+
*/
|
|
210
|
+
export function createAgentValueValidator(
|
|
211
|
+
modelRuntime: ModelRuntime,
|
|
212
|
+
getToolNames: () => Iterable<string>,
|
|
213
|
+
): AgentValueValidator {
|
|
214
|
+
return (option, value, scan) => {
|
|
215
|
+
if (option.completionDomain === "thinking") {
|
|
216
|
+
return !parseArgs(["--thinking", value]).diagnostics.some(
|
|
217
|
+
(diagnostic) => diagnostic.type === "warning",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (option.completionDomain === "provider") {
|
|
221
|
+
return modelRuntime
|
|
222
|
+
.getModels()
|
|
223
|
+
.some((model) => model.provider.toLowerCase() === value.toLowerCase());
|
|
224
|
+
}
|
|
225
|
+
if (option.completionDomain === "tool") {
|
|
226
|
+
const parsed = parseArgs([option.names[0]!, value]);
|
|
227
|
+
const requestedNames = option.semanticId === "tools" ? parsed.tools : parsed.excludeTools;
|
|
228
|
+
const knownNames = new Set(getToolNames());
|
|
229
|
+
return (requestedNames ?? []).every((name) => knownNames.has(name));
|
|
230
|
+
}
|
|
231
|
+
if (option.completionDomain !== "model") return true;
|
|
232
|
+
const parsed = parseArgs(scan.forwardedArgs);
|
|
233
|
+
try {
|
|
234
|
+
resolveForwardedOptions({ ...parsed, model: value }, modelRuntime);
|
|
235
|
+
return true;
|
|
236
|
+
} catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Resolve forwarded runtime options against the child session's model runtime. */
|
|
243
|
+
export function resolveForwardedOptions(
|
|
244
|
+
parsed: Args,
|
|
245
|
+
modelRuntime: ModelRuntime,
|
|
246
|
+
): ForwardedOptions {
|
|
247
|
+
const forwarded: ForwardedOptions = {};
|
|
248
|
+
if (parsed.model) {
|
|
249
|
+
const nameMatch = modelRuntime
|
|
250
|
+
.getModels()
|
|
251
|
+
.find((m) => m.name?.toLowerCase() === parsed.model!.toLowerCase());
|
|
252
|
+
if (nameMatch) {
|
|
253
|
+
forwarded.model = nameMatch;
|
|
254
|
+
} else {
|
|
255
|
+
const resolved = resolveCliModel({
|
|
256
|
+
cliProvider: parsed.provider,
|
|
257
|
+
cliModel: parsed.model,
|
|
258
|
+
cliThinking: parsed.thinking,
|
|
259
|
+
modelRuntime,
|
|
260
|
+
});
|
|
261
|
+
if (resolved.error) throw new Error(resolved.error);
|
|
262
|
+
forwarded.model = resolved.model;
|
|
263
|
+
if (!parsed.thinking && resolved.thinkingLevel)
|
|
264
|
+
forwarded.thinkingLevel = resolved.thinkingLevel;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (parsed.thinking) forwarded.thinkingLevel = parsed.thinking;
|
|
268
|
+
|
|
269
|
+
if (parsed.noTools) forwarded.noTools = "all";
|
|
270
|
+
else if (parsed.noBuiltinTools) forwarded.noTools = "builtin";
|
|
271
|
+
if (parsed.tools) forwarded.tools = parsed.tools;
|
|
272
|
+
if (parsed.excludeTools) forwarded.excludeTools = parsed.excludeTools;
|
|
273
|
+
return forwarded;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function buildChildResourceLoaderOptions(
|
|
277
|
+
parsed: Args,
|
|
278
|
+
cwd: string,
|
|
279
|
+
): ChildResourceLoaderOptions | undefined {
|
|
280
|
+
const options: ChildResourceLoaderOptions = {};
|
|
281
|
+
if (parsed.systemPrompt !== undefined) options.systemPrompt = parsed.systemPrompt;
|
|
282
|
+
if (parsed.appendSystemPrompt) options.appendSystemPrompt = parsed.appendSystemPrompt;
|
|
283
|
+
if (parsed.extensions)
|
|
284
|
+
options.additionalExtensionPaths = resolveResourcePaths(parsed.extensions, cwd);
|
|
285
|
+
if (parsed.skills) options.additionalSkillPaths = resolveResourcePaths(parsed.skills, cwd);
|
|
286
|
+
if (parsed.promptTemplates)
|
|
287
|
+
options.additionalPromptTemplatePaths = resolveResourcePaths(parsed.promptTemplates, cwd);
|
|
288
|
+
if (parsed.noExtensions) options.noExtensions = true;
|
|
289
|
+
if (parsed.noSkills) options.noSkills = true;
|
|
290
|
+
if (parsed.noPromptTemplates) options.noPromptTemplates = true;
|
|
291
|
+
if (parsed.noThemes) options.noThemes = true;
|
|
292
|
+
if (parsed.noContextFiles) options.noContextFiles = true;
|
|
293
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Resolve resource paths to absolute against the session cwd, matching how the CLI hands them to the loader. */
|
|
297
|
+
function resolveResourcePaths(values: string[], cwd: string): string[] {
|
|
298
|
+
return values.map((value) => {
|
|
299
|
+
if (value === "~" || value.startsWith("~/")) return path.join(os.homedir(), value.slice(1));
|
|
300
|
+
return path.isAbsolute(value) ? value : path.resolve(cwd, value);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function buildInheritedMessages(ctx: ExtensionCommandContext): AgentMessage[] {
|
|
305
|
+
return structuredClone(buildSessionContext(ctx.sessionManager.getBranch()).messages);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function runAgentLifecycle(
|
|
309
|
+
pi: ExtensionAPI,
|
|
310
|
+
isShuttingDown: () => boolean,
|
|
311
|
+
task: string,
|
|
312
|
+
model: Model,
|
|
313
|
+
thinkingLevel: ThinkingLevel,
|
|
314
|
+
forwarded: ForwardedOptions,
|
|
315
|
+
inheritedMessages: AgentMessage[],
|
|
316
|
+
services: AgentSessionServices,
|
|
317
|
+
runningAgent: RunningAgent,
|
|
318
|
+
widget: UserAgentWidget,
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
let session: AgentSession | undefined;
|
|
321
|
+
let unsubscribe: (() => void) | undefined;
|
|
322
|
+
try {
|
|
323
|
+
session = await createChildSession(
|
|
324
|
+
model,
|
|
325
|
+
thinkingLevel,
|
|
326
|
+
forwarded,
|
|
327
|
+
inheritedMessages,
|
|
328
|
+
services,
|
|
329
|
+
runningAgent,
|
|
330
|
+
widget,
|
|
331
|
+
);
|
|
332
|
+
unsubscribe = subscribeToChildSession(session, runningAgent, widget);
|
|
333
|
+
await runChildTurns(pi, isShuttingDown, task, session, runningAgent, widget);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (runningAgent.aborted) {
|
|
336
|
+
logSteering(runningAgent.id, "agent-aborted");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const message = errorMessage(error);
|
|
340
|
+
logSteering(runningAgent.id, "agent-error", { error: message });
|
|
341
|
+
runningAgent.status = "error";
|
|
342
|
+
runningAgent.completedAt = Date.now();
|
|
343
|
+
runningAgent.error = message;
|
|
344
|
+
if (runningAgent.notifyMainAgent && isShuttingDown()) {
|
|
345
|
+
runningAgent.mainContextState = "separate";
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const resultMessage = buildAgentResultMessage(
|
|
349
|
+
runningAgent,
|
|
350
|
+
{ ok: false, error: message },
|
|
351
|
+
{ display: runningAgent.notifyMainAgent },
|
|
352
|
+
);
|
|
353
|
+
widget.addCompleted(runningAgent, resultMessage, { joinable: !runningAgent.notifyMainAgent });
|
|
354
|
+
if (postUserAgentResult(pi, isShuttingDown, runningAgent.notifyMainAgent, resultMessage))
|
|
355
|
+
runningAgent.status = "posted";
|
|
356
|
+
} finally {
|
|
357
|
+
unsubscribe?.();
|
|
358
|
+
if (session) {
|
|
359
|
+
logSteering(runningAgent.id, "session-shutdown", { streaming: session.isStreaming });
|
|
360
|
+
await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Run turns on an initialized child session until it posts, retires, or shuts down. */
|
|
366
|
+
export async function runChildTurns(
|
|
367
|
+
pi: ExtensionAPI,
|
|
368
|
+
isShuttingDown: () => boolean,
|
|
369
|
+
task: string,
|
|
370
|
+
session: AgentSession,
|
|
371
|
+
runningAgent: RunningAgent,
|
|
372
|
+
widget: UserAgentWidget,
|
|
373
|
+
): Promise<void> {
|
|
374
|
+
let instruction = `You are running in an ephemeral, forked background process now, concurrently with the main session. ${task}`;
|
|
375
|
+
while (true) {
|
|
376
|
+
const turnMessageStart = session.agent.state.messages.length;
|
|
377
|
+
if (!runningAgent.interruptRequested) {
|
|
378
|
+
logSteering(runningAgent.id, "turn-prompt-started", {
|
|
379
|
+
streaming: session.isStreaming,
|
|
380
|
+
instructionLength: instruction.length,
|
|
381
|
+
});
|
|
382
|
+
await session.prompt(instruction);
|
|
383
|
+
}
|
|
384
|
+
if (runningAgent.interruptRequested) {
|
|
385
|
+
const response = interruptedTurnResponse(session, turnMessageStart);
|
|
386
|
+
runningAgent.interruptRequested = false;
|
|
387
|
+
runningAgent.mainContextState = "separate";
|
|
388
|
+
runningAgent.status = "done-waiting-to-post";
|
|
389
|
+
runningAgent.completedAt = Date.now();
|
|
390
|
+
runningAgent.responseText = response;
|
|
391
|
+
const resultMessage = buildAgentResultMessage(
|
|
392
|
+
runningAgent,
|
|
393
|
+
{ ok: true, response },
|
|
394
|
+
{ display: false },
|
|
395
|
+
);
|
|
396
|
+
postUserAgentResult(pi, isShuttingDown, false, resultMessage);
|
|
397
|
+
runningAgent.pendingJoinMessage = runningAgent.notifyMainAgent
|
|
398
|
+
? undefined
|
|
399
|
+
: resultMessage;
|
|
400
|
+
if (isShuttingDown()) return;
|
|
401
|
+
runningAgent.status = "idle";
|
|
402
|
+
logSteering(runningAgent.id, "turn-interrupted", { turnCount: runningAgent.turnCount });
|
|
403
|
+
widget.update();
|
|
404
|
+
const next = await waitForInstruction(runningAgent, widget);
|
|
405
|
+
if (next === undefined) return;
|
|
406
|
+
instruction = next;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const response = getFinalAssistantText(session);
|
|
411
|
+
logSteering(runningAgent.id, "turn-prompt-resolved", {
|
|
412
|
+
responseLength: response.length,
|
|
413
|
+
messageCount: session.agent.state.messages.length,
|
|
414
|
+
});
|
|
415
|
+
runningAgent.status = "done-waiting-to-post";
|
|
416
|
+
runningAgent.completedAt = Date.now();
|
|
417
|
+
runningAgent.responseText = response;
|
|
418
|
+
const resultMessage = buildAgentResultMessage(
|
|
419
|
+
runningAgent,
|
|
420
|
+
{ ok: true, response },
|
|
421
|
+
{ display: runningAgent.notifyMainAgent },
|
|
422
|
+
);
|
|
423
|
+
if (runningAgent.notifyMainAgent) {
|
|
424
|
+
if (isShuttingDown()) {
|
|
425
|
+
runningAgent.mainContextState = "separate";
|
|
426
|
+
resultMessage.details.mainContextState = "separate";
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
widget.addCompleted(runningAgent, resultMessage, { joinable: false });
|
|
430
|
+
const posted = postUserAgentResult(pi, isShuttingDown, true, resultMessage);
|
|
431
|
+
runningAgent.status = posted ? "posted" : runningAgent.status;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
postUserAgentResult(pi, isShuttingDown, false, resultMessage);
|
|
435
|
+
runningAgent.pendingJoinMessage = resultMessage;
|
|
436
|
+
if (isShuttingDown()) return;
|
|
437
|
+
runningAgent.status = "idle";
|
|
438
|
+
logSteering(runningAgent.id, "agent-idle", { turnCount: runningAgent.turnCount });
|
|
439
|
+
widget.update();
|
|
440
|
+
const next = await waitForInstruction(runningAgent, widget);
|
|
441
|
+
if (next === undefined) return;
|
|
442
|
+
instruction = next;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function interruptedTurnResponse(session: AgentSession, turnMessageStart: number): string {
|
|
447
|
+
const assistantMessages = session.agent.state.messages
|
|
448
|
+
.slice(turnMessageStart)
|
|
449
|
+
.filter((message) => message.role === "assistant");
|
|
450
|
+
const lastAssistantMessage = assistantMessages.at(-1);
|
|
451
|
+
const partialResponse = lastAssistantMessage ? assistantText(lastAssistantMessage).trim() : "";
|
|
452
|
+
if (!partialResponse) return "Interrupted by user.";
|
|
453
|
+
return `Interrupted by user.\n\n${partialResponse}`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function createChildSession(
|
|
457
|
+
model: Model,
|
|
458
|
+
thinkingLevel: ThinkingLevel,
|
|
459
|
+
forwarded: ForwardedOptions,
|
|
460
|
+
inheritedMessages: AgentMessage[],
|
|
461
|
+
services: AgentSessionServices,
|
|
462
|
+
runningAgent: RunningAgent,
|
|
463
|
+
widget: UserAgentWidget,
|
|
464
|
+
): Promise<AgentSession> {
|
|
465
|
+
const { session } = await createAgentSessionFromServices({
|
|
466
|
+
services,
|
|
467
|
+
sessionManager: SessionManager.inMemory(services.cwd),
|
|
468
|
+
model,
|
|
469
|
+
thinkingLevel,
|
|
470
|
+
tools: forwarded.tools,
|
|
471
|
+
excludeTools: forwarded.excludeTools,
|
|
472
|
+
noTools: forwarded.noTools,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
runningAgent.session = session;
|
|
476
|
+
runningAgent.status = "running";
|
|
477
|
+
logSteering(runningAgent.id, "session-created", { streaming: session.isStreaming });
|
|
478
|
+
if (runningAgent.aborted) void session.abort();
|
|
479
|
+
widget.update();
|
|
480
|
+
|
|
481
|
+
await session.bindExtensions({ mode: "print" });
|
|
482
|
+
logSteering(runningAgent.id, "session-bound", { streaming: session.isStreaming });
|
|
483
|
+
if (inheritedMessages.length > 0) {
|
|
484
|
+
session.agent.state.messages = inheritedMessages;
|
|
485
|
+
logSteering(runningAgent.id, "context-inherited", { messageCount: inheritedMessages.length });
|
|
486
|
+
}
|
|
487
|
+
return session;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Parks an idle agent until the user resumes it with another instruction (resolves with it) or retires it (resolves undefined). */
|
|
491
|
+
export function waitForInstruction(
|
|
492
|
+
agent: RunningAgent,
|
|
493
|
+
widget: { update(): void },
|
|
494
|
+
): Promise<string | undefined> {
|
|
495
|
+
return new Promise((resolve) => {
|
|
496
|
+
const settle = (instruction: string | undefined) => {
|
|
497
|
+
agent.resume = undefined;
|
|
498
|
+
agent.retire = undefined;
|
|
499
|
+
resolve(instruction);
|
|
500
|
+
};
|
|
501
|
+
agent.resume = (instruction) => {
|
|
502
|
+
logSteering(agent.id, "agent-resumed", { instructionLength: instruction.length });
|
|
503
|
+
agent.mainContextState = agent.notifyMainAgent ? "will-join" : "separate";
|
|
504
|
+
agent.status = "running";
|
|
505
|
+
agent.turnStartedAt = Date.now();
|
|
506
|
+
agent.completedAt = undefined;
|
|
507
|
+
settle(instruction);
|
|
508
|
+
widget.update();
|
|
509
|
+
};
|
|
510
|
+
agent.retire = () => {
|
|
511
|
+
logSteering(agent.id, "agent-retired", { status: agent.status });
|
|
512
|
+
settle(undefined);
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function subscribeToChildSession(
|
|
518
|
+
session: AgentSession,
|
|
519
|
+
runningAgent: RunningAgent,
|
|
520
|
+
widget: UserAgentWidget,
|
|
521
|
+
): () => void {
|
|
522
|
+
return session.subscribe((event: AgentSessionEvent) => {
|
|
523
|
+
if (event.type === "agent_start")
|
|
524
|
+
logSteering(runningAgent.id, "child-agent-started", { streaming: session.isStreaming });
|
|
525
|
+
if (event.type === "agent_end")
|
|
526
|
+
logSteering(runningAgent.id, "child-agent-ended", {
|
|
527
|
+
streaming: session.isStreaming,
|
|
528
|
+
messageCount: event.messages.length,
|
|
529
|
+
});
|
|
530
|
+
if (event.type === "agent_settled")
|
|
531
|
+
logSteering(runningAgent.id, "child-agent-settled", { streaming: session.isStreaming });
|
|
532
|
+
if (event.type === "turn_start")
|
|
533
|
+
logSteering(runningAgent.id, "turn-started", { streaming: session.isStreaming });
|
|
534
|
+
if (event.type === "turn_end")
|
|
535
|
+
logSteering(runningAgent.id, "turn-ended", { streaming: session.isStreaming });
|
|
536
|
+
if (event.type === "message_start")
|
|
537
|
+
logSteering(runningAgent.id, "message-started", {
|
|
538
|
+
role: event.message.role,
|
|
539
|
+
streaming: session.isStreaming,
|
|
540
|
+
});
|
|
541
|
+
const finalizedMessageUpdate =
|
|
542
|
+
event.type === "message_update" && event.assistantMessageEvent.type.endsWith("_end");
|
|
543
|
+
if (event.type === "message_end")
|
|
544
|
+
runningAgent.conversationMessages.push(structuredClone(event.message));
|
|
545
|
+
if (finalizedMessageUpdate || event.type === "message_end")
|
|
546
|
+
runningAgent.latestFinalizedMessage = structuredClone(event.message);
|
|
547
|
+
if (
|
|
548
|
+
finalizedMessageUpdate &&
|
|
549
|
+
event.message.role === "assistant" &&
|
|
550
|
+
event.assistantMessageEvent.type === "text_end"
|
|
551
|
+
) {
|
|
552
|
+
runningAgent.responseText = assistantText(event.message);
|
|
553
|
+
}
|
|
554
|
+
if (event.type === "message_end" && event.message.role === "assistant")
|
|
555
|
+
runningAgent.responseText = assistantText(event.message);
|
|
556
|
+
if (event.type === "tool_execution_start") {
|
|
557
|
+
runningAgent.activeTools.set(event.toolCallId, event.toolName);
|
|
558
|
+
}
|
|
559
|
+
if (event.type === "tool_execution_end") {
|
|
560
|
+
runningAgent.activeTools.delete(event.toolCallId);
|
|
561
|
+
runningAgent.toolUses += 1;
|
|
562
|
+
}
|
|
563
|
+
if (event.type === "turn_end") {
|
|
564
|
+
runningAgent.turnCount += 1;
|
|
565
|
+
}
|
|
566
|
+
if (event.type === "message_update" && !finalizedMessageUpdate) return;
|
|
567
|
+
widget.update();
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function assistantText(message: Extract<AgentMessage, { role: "assistant" }>): string {
|
|
572
|
+
return message.content
|
|
573
|
+
.filter((part) => part.type === "text")
|
|
574
|
+
.map((part) => part.text)
|
|
575
|
+
.join("\n");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function getFinalAssistantText(session: AgentSession): string {
|
|
579
|
+
const assistantMessages = session.agent.state.messages.filter(
|
|
580
|
+
(message) => message.role === "assistant",
|
|
581
|
+
);
|
|
582
|
+
const lastMessage = assistantMessages.at(-1);
|
|
583
|
+
if (!lastMessage) throw new Error("User agent finished without an assistant message");
|
|
584
|
+
if (lastMessage.stopReason === "error" || lastMessage.stopReason === "aborted") {
|
|
585
|
+
throw new Error(lastMessage.errorMessage ?? `User agent ${lastMessage.stopReason}`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const text = assistantText(lastMessage).trim();
|
|
589
|
+
if (!text) throw new Error("User agent returned no text response");
|
|
590
|
+
return text;
|
|
591
|
+
}
|