fix(coding-agent): add settled agent lifecycle event

closes #6363
This commit is contained in:
Mario Zechner
2026-07-09 12:40:52 +02:00
parent 050b8176bf
commit e9fa5a68a1
14 changed files with 272 additions and 32 deletions
@@ -1624,7 +1624,7 @@ export class InteractiveMode {
this.restoreQueuedMessagesToEditor({ abort: true });
},
commandContextActions: {
waitForIdle: () => this.session.agent.waitForIdle(),
waitForIdle: () => this.session.waitForIdle(),
newSession: async (options) => {
this.clearStatusIndicator();
try {
@@ -1674,7 +1674,7 @@ export class InteractiveMode {
},
shutdownHandler: () => {
this.shutdownRequested = true;
if (!this.session.isStreaming) {
if (this.session.isIdle) {
void this.shutdown();
}
},
@@ -1774,7 +1774,7 @@ export class InteractiveMode {
sessionManager: this.sessionManager,
modelRegistry: this.session.modelRegistry,
model: this.session.model,
isIdle: () => !this.session.isStreaming,
isIdle: () => this.session.isIdle,
isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
signal: this.session.agent.signal,
abort: () => {
@@ -3024,11 +3024,13 @@ export class InteractiveMode {
}
this.pendingTools.clear();
await this.checkShutdownRequested();
this.ui.requestRender();
break;
case "agent_settled":
await this.checkShutdownRequested();
break;
case "compaction_start": {
if (this.settingsManager.getShowTerminalProgress()) {
this.ui.terminal.setProgress(true);
@@ -73,7 +73,7 @@ export async function runPrintMode(runtimeHost: AgentSessionRuntime, options: Pr
await session.bindExtensions({
mode: mode === "json" ? "json" : "print",
commandContextActions: {
waitForIdle: () => session.agent.waitForIdle(),
waitForIdle: () => session.waitForIdle(),
newSession: async (newSessionOptions) => runtimeHost.newSession(newSessionOptions),
fork: async (entryId, forkOptions) => {
const result = await runtimeHost.fork(entryId, forkOptions);
@@ -5,9 +5,9 @@
*/
import { type ChildProcess, spawn } from "node:child_process";
import type { AgentEvent, AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
import type { ImageContent } from "@earendil-works/pi-ai";
import type { SessionStats } from "../../core/agent-session.ts";
import type { AgentSessionEvent, SessionStats } from "../../core/agent-session.ts";
import type { BashResult } from "../../core/bash-executor.ts";
import type { CompactionResult } from "../../core/compaction/index.ts";
import type { SessionEntry, SessionTreeNode } from "../../core/session-manager.ts";
@@ -46,7 +46,7 @@ export interface ModelInfo {
reasoning: boolean;
}
export type RpcEventListener = (event: AgentEvent) => void;
export type RpcEventListener = (event: AgentSessionEvent) => void;
// ============================================================================
// RPC Client
@@ -442,7 +442,7 @@ export class RpcClient {
/**
* Wait for agent to become idle (no streaming).
* Resolves when agent_end event is received.
* Resolves when agent_settled event is received.
*/
waitForIdle(timeout = 60000): Promise<void> {
return new Promise((resolve, reject) => {
@@ -452,7 +452,7 @@ export class RpcClient {
}, timeout);
const unsubscribe = this.onEvent((event) => {
if (event.type === "agent_end") {
if (event.type === "agent_settled") {
clearTimeout(timer);
unsubscribe();
resolve();
@@ -464,9 +464,9 @@ export class RpcClient {
/**
* Collect events until agent becomes idle.
*/
collectEvents(timeout = 60000): Promise<AgentEvent[]> {
collectEvents(timeout = 60000): Promise<AgentSessionEvent[]> {
return new Promise((resolve, reject) => {
const events: AgentEvent[] = [];
const events: AgentSessionEvent[] = [];
const timer = setTimeout(() => {
unsubscribe();
reject(new Error(`Timeout collecting events. Stderr: ${this.stderr}`));
@@ -474,7 +474,7 @@ export class RpcClient {
const unsubscribe = this.onEvent((event) => {
events.push(event);
if (event.type === "agent_end") {
if (event.type === "agent_settled") {
clearTimeout(timer);
unsubscribe();
resolve(events);
@@ -486,7 +486,7 @@ export class RpcClient {
/**
* Send prompt and wait for completion, returning all events.
*/
async promptAndWait(message: string, images?: ImageContent[], timeout = 60000): Promise<AgentEvent[]> {
async promptAndWait(message: string, images?: ImageContent[], timeout = 60000): Promise<AgentSessionEvent[]> {
const eventsPromise = this.collectEvents(timeout);
await this.prompt(message, images);
return eventsPromise;
@@ -510,7 +510,7 @@ export class RpcClient {
// Otherwise it's an event
for (const listener of this.eventListeners) {
listener(data as AgentEvent);
listener(data as AgentSessionEvent);
}
} catch {
// Ignore non-JSON lines
@@ -319,7 +319,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
uiContext: createExtensionUIContext(),
mode: "rpc",
commandContextActions: {
waitForIdle: () => session.agent.waitForIdle(),
waitForIdle: () => session.waitForIdle(),
newSession: async (options) => runtimeHost.newSession(options),
fork: async (entryId, forkOptions) => {
const result = await runtimeHost.fork(entryId, forkOptions);
@@ -353,6 +353,9 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
unsubscribeBackpressure?.();
unsubscribe = session.subscribe((event) => {
output(event);
if (event.type === "agent_settled") {
void checkShutdownRequested();
}
});
unsubscribeBackpressure = session.agent.subscribe(async () => {
await waitForRawStdoutBackpressure();