1589: first draft #2

This commit is contained in:
USO Status 2026-01-25 18:33:03 +01:00
parent b5b8e1ae08
commit c253b76c3a
3 changed files with 126 additions and 10 deletions

View File

@ -37,6 +37,13 @@ import type { FollowupRun } from "./queue.js";
import { parseReplyDirectives } from "./reply-directives.js";
import { applyReplyTagsToPayload, isRenderablePayload } from "./reply-payloads.js";
import type { TypingSignaler } from "./typing-mode.js";
import {
handleAgentEventForStatus,
type StatusUpdateRunContext,
} from "./status-updates-integration.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
const log = createSubsystemLogger("agent-runner-execution");
export type AgentRunLoopResult =
| {
@ -77,6 +84,7 @@ export async function runAgentTurnWithFallback(params: {
activeSessionStore?: Record<string, SessionEntry>;
storePath?: string;
resolvedVerboseLevel: VerboseLevel;
statusUpdateContext?: StatusUpdateRunContext;
}): Promise<AgentRunLoopResult> {
let didLogHeartbeatStrip = false;
let autoCompactionCompleted = false;
@ -283,6 +291,13 @@ export async function runAgentTurnWithFallback(params: {
}
: undefined,
onAgentEvent: async (evt) => {
// ===== STATUS UPDATES: Handle agent events =====
if (params.statusUpdateContext) {
log.debug(`onAgentEvent: forwarding to status updates, stream=${evt.stream}`);
await handleAgentEventForStatus(params.statusUpdateContext, evt);
}
// ===== END STATUS UPDATES =====
// Trigger typing when tools start executing.
// Must await to ensure typing indicator starts before tool summaries are emitted.
if (evt.stream === "tool") {

View File

@ -41,6 +41,17 @@ import { incrementCompactionCount } from "./session-updates.js";
import type { TypingController } from "./typing.js";
import { createTypingSignaler } from "./typing-mode.js";
import { emitDiagnosticEvent, isDiagnosticsEnabled } from "../../infra/diagnostic-events.js";
import {
createAgentStatusController,
createStatusUpdateRunContext,
completeStatusUpdate,
cleanupStatusUpdate,
noopStatusCallbacks,
type StatusUpdateRunContext,
} from "./status-updates-integration.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
const log = createSubsystemLogger("agent-runner");
const BLOCK_REPLY_SEND_TIMEOUT_MS = 15_000;
@ -298,6 +309,29 @@ export async function runReplyAgent(params: {
`Role ordering conflict (${reason}). Restarting session ${sessionKey} -> ${nextSessionId}.`,
cleanupTranscripts: true,
});
// ===== STATUS UPDATES INTEGRATION =====
// Create status update controller for this agent run
const agentId = resolveAgentIdFromSessionKey(sessionKey);
log.info(`Setting up status updates for agentId=${agentId}, channel=${replyToChannel}`);
// TODO: Create proper callbacks that integrate with the channel's send/edit methods
// For now, using noop callbacks to trace the integration path
const statusCallbacks = noopStatusCallbacks;
const statusController = createAgentStatusController({
cfg,
agentId,
callbacks: statusCallbacks,
});
const statusUpdateContext = createStatusUpdateRunContext(statusController);
log.info(
`Status update context created, controller=${statusController ? "present" : "undefined"}`,
);
// ===== END STATUS UPDATES INTEGRATION =====
try {
const runStartedAt = Date.now();
const runOutcome = await runAgentTurnWithFallback({
@ -322,6 +356,7 @@ export async function runReplyAgent(params: {
activeSessionStore,
storePath,
resolvedVerboseLevel,
statusUpdateContext,
});
if (runOutcome.kind === "final") {
@ -502,6 +537,19 @@ export async function runReplyAgent(params: {
finalPayloads = appendUsageLine(finalPayloads, responseUsageLine);
}
// ===== STATUS UPDATES: Complete and mark final response =====
if (statusUpdateContext && finalPayloads.length > 0) {
const lastPayload = finalPayloads[finalPayloads.length - 1];
if (lastPayload?.text) {
log.info(`Completing status update with final text`);
const markedText = await completeStatusUpdate(statusUpdateContext, lastPayload.text);
if (markedText && markedText !== lastPayload.text) {
finalPayloads[finalPayloads.length - 1] = { ...lastPayload, text: markedText };
}
}
}
// ===== END STATUS UPDATES =====
return finalizeWithFollowup(
finalPayloads.length === 1 ? finalPayloads[0] : finalPayloads,
queueKey,
@ -510,5 +558,12 @@ export async function runReplyAgent(params: {
} finally {
blockReplyPipeline?.stop();
typing.markRunComplete();
// ===== STATUS UPDATES: Cleanup =====
if (statusUpdateContext) {
log.info(`Cleaning up status update context`);
await cleanupStatusUpdate(statusUpdateContext);
}
// ===== END STATUS UPDATES =====
}
}

View File

@ -14,6 +14,9 @@ import {
type StatusUpdateCallbacks,
type StatusUpdateController,
} from "./status-updates.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
const log = createSubsystemLogger("status-updates-integration");
/**
* Resolve status update configuration from the Clawdbot config.
@ -22,16 +25,21 @@ export function resolveStatusUpdateConfigFromConfig(
cfg: ClawdbotConfig,
agentId?: string,
): StatusUpdateConfig {
log.debug(`Resolving status update config for agentId=${agentId}`);
// Check agent-specific config first
if (agentId && cfg.agents?.list) {
const agentCfg = cfg.agents.list.find((a) => a.id === agentId);
if (agentCfg?.statusUpdates) {
log.debug(`Found agent-specific config: ${JSON.stringify(agentCfg.statusUpdates)}`);
return agentCfg.statusUpdates;
}
}
// Fall back to agent defaults
return cfg.agents?.defaults?.statusUpdates ?? {};
const defaultConfig = cfg.agents?.defaults?.statusUpdates ?? {};
log.debug(`Using default config: ${JSON.stringify(defaultConfig)}`);
return defaultConfig;
}
/**
@ -51,21 +59,30 @@ export function createAgentStatusController(params: {
callbacks: StatusUpdateCallbacks;
}): StatusUpdateController | undefined {
const { cfg, agentId, callbacks } = params;
log.info(`createAgentStatusController called for agentId=${agentId}`);
const config = resolveStatusUpdateConfigFromConfig(cfg, agentId);
if (!config.enabled) {
log.info(`Status updates disabled (enabled=${config.enabled})`);
return undefined;
}
return createStatusUpdateController(config, callbacks);
log.info(
`Creating status update controller with mode=${config.mode}, supportsEdit=${callbacks.supportsEdit?.()}`,
);
const controller = createStatusUpdateController(config, callbacks);
log.info(`Status update controller created successfully`);
return controller;
}
/**
* Map agent lifecycle events to status phases.
*/
export function mapAgentEventToPhase(
event: { stream: string; data: Record<string, unknown> },
): StatusPhase | undefined {
export function mapAgentEventToPhase(event: {
stream: string;
data: Record<string, unknown>;
}): StatusPhase | undefined {
const { stream, data } = event;
const phase = typeof data.phase === "string" ? data.phase : "";
@ -104,11 +121,25 @@ export type StatusUpdateRunContext = {
export function createStatusUpdateRunContext(
controller?: StatusUpdateController,
): StatusUpdateRunContext {
return {
log.info(`createStatusUpdateRunContext: controller=${controller ? "present" : "undefined"}`);
const ctx = {
controller,
startedAt: Date.now(),
currentPhase: "sending_query",
currentPhase: "sending_query" as StatusPhase,
};
if (controller) {
log.info(`Status update context created with controller, starting...`);
// Start the controller asynchronously
controller.start().catch((err) => {
log.debug(`Failed to start status controller: ${String(err)}`);
});
} else {
log.info(`Status update context created WITHOUT controller (status updates disabled)`);
}
return ctx;
}
/**
@ -118,10 +149,18 @@ export async function handleAgentEventForStatus(
ctx: StatusUpdateRunContext,
event: { stream: string; data: Record<string, unknown> },
): Promise<void> {
if (!ctx.controller) return;
if (!ctx.controller) {
log.debug(`handleAgentEventForStatus: no controller, skipping event stream=${event.stream}`);
return;
}
const phase = mapAgentEventToPhase(event);
log.debug(
`handleAgentEventForStatus: stream=${event.stream}, phase=${event.data.phase}, mappedPhase=${phase}, currentPhase=${ctx.currentPhase}`,
);
if (phase && phase !== ctx.currentPhase) {
log.info(`Status phase change: ${ctx.currentPhase} -> ${phase}`);
ctx.currentPhase = phase;
await ctx.controller.setPhase(phase);
}
@ -134,8 +173,15 @@ export async function completeStatusUpdate(
ctx: StatusUpdateRunContext,
finalText?: string,
): Promise<string | undefined> {
if (!ctx.controller) return finalText;
return ctx.controller.complete(finalText);
if (!ctx.controller) {
log.debug(`completeStatusUpdate: no controller, returning original text`);
return finalText;
}
log.info(`completeStatusUpdate: finalizing status update`);
const result = await ctx.controller.complete(finalText);
log.debug(`completeStatusUpdate: result=${result?.substring(0, 50)}...`);
return result;
}
/**