1589: first draft #2
This commit is contained in:
parent
b5b8e1ae08
commit
c253b76c3a
@ -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") {
|
||||
|
||||
@ -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 =====
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user