Add runtime provenance metadata for alert remediation

This commit is contained in:
yamato 2026-03-21 13:22:39 +09:00
parent 598f1826d8
commit 7136087486
10 changed files with 93 additions and 2 deletions

View File

@ -492,6 +492,7 @@ function runAgentAttempt(params: {
sessionKey: params.sessionKey,
agentId: params.sessionAgentId,
trigger: "user",
runContext: { sessionTarget: "main" },
messageChannel: params.messageChannel,
agentAccountId: params.runContext.accountId,
messageTo: params.opts.replyTo ?? params.opts.to,

View File

@ -2549,6 +2549,13 @@ export async function runEmbeddedAttempt(
sessionKey: sandboxSessionKey,
sessionId: params.sessionId,
agentId: sessionAgentId,
runContext: {
sessionTarget:
params.runContext?.sessionTarget ??
(params.trigger === "cron" ? "isolated" : sandboxSessionKey ? "main" : undefined),
cronJobId: params.runContext?.cronJobId,
maintenanceScope: params.runContext?.maintenanceScope,
},
});
const {

View File

@ -20,6 +20,12 @@ export type ClientToolDefinition = {
};
};
export type RunEmbeddedPiAgentContext = {
sessionTarget?: "main" | "isolated";
cronJobId?: string;
maintenanceScope?: string;
};
export type RunEmbeddedPiAgentParams = {
sessionId: string;
sessionKey?: string;
@ -119,6 +125,7 @@ export type RunEmbeddedPiAgentParams = {
streamParams?: AgentStreamParams;
ownerNumbers?: string[];
enforceFinalTag?: boolean;
runContext?: RunEmbeddedPiAgentContext;
/**
* Allow a single run attempt even when all auth profiles are in cooldown,
* but only for inferred transient cooldowns like `rate_limit` or `overloaded`.

View File

@ -9,13 +9,22 @@ vi.mock("../infra/agent-events.js", () => ({
function createContext(
lastAssistant: unknown,
overrides?: { onAgentEvent?: (event: unknown) => void },
overrides?: {
onAgentEvent?: (event: unknown) => void;
sessionKey?: string;
runContext?: {
sessionTarget?: "main" | "isolated";
cronJobId?: string;
maintenanceScope?: string;
};
},
): EmbeddedPiSubscribeContext {
return {
params: {
runId: "run-1",
config: {},
sessionKey: "agent:main:main",
sessionKey: overrides?.sessionKey ?? "agent:main:main",
runContext: overrides?.runContext,
onAgentEvent: overrides?.onAgentEvent,
},
state: {
@ -60,6 +69,8 @@ describe("handleAgentEnd", () => {
runId: "run-1",
error: "connection refused",
rawErrorPreview: "connection refused",
sessionTarget: "main",
sessionKey: "agent:main:main",
});
expect(onAgentEvent).toHaveBeenCalledWith({
stream: "lifecycle",
@ -157,4 +168,38 @@ describe("handleAgentEnd", () => {
expect(ctx.log.warn).not.toHaveBeenCalled();
expect(ctx.log.debug).toHaveBeenCalledWith("embedded run agent end: runId=run-1 isError=false");
});
it("includes explicit provenance metadata in error logs when provided by the runtime", () => {
const ctx = createContext(
{
role: "assistant",
stopReason: "error",
errorMessage: "provider overloaded",
content: [{ type: "text", text: "" }],
},
{
sessionKey: "agent:main:cron:job-1",
runContext: {
sessionTarget: "isolated",
cronJobId: "job-1",
maintenanceScope: "watchdog",
},
},
);
handleAgentEnd(ctx);
expect(vi.mocked(ctx.log.warn).mock.calls[0]?.[1]).toMatchObject({
sessionTarget: "isolated",
sessionKey: "agent:main:cron:job-1",
cronJobId: "job-1",
maintenanceScope: "watchdog",
runContext: {
sessionTarget: "isolated",
sessionKey: "agent:main:cron:job-1",
cronJobId: "job-1",
maintenanceScope: "watchdog",
},
});
});
});

View File

@ -1,5 +1,6 @@
import { emitAgentEvent } from "../infra/agent-events.js";
import { createInlineCodeState } from "../markdown/code-spans.js";
import { isCronSessionKey } from "../sessions/session-key-utils.js";
import {
buildApiErrorObservationFields,
buildTextObservationFields,
@ -33,6 +34,14 @@ export function handleAgentStart(ctx: EmbeddedPiSubscribeContext) {
export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
const lastAssistant = ctx.state.lastAssistant;
const isError = isAssistantMessage(lastAssistant) && lastAssistant.stopReason === "error";
const runContext = ctx.params.runContext ?? {};
const sessionTarget =
runContext.sessionTarget ??
(isCronSessionKey(ctx.params.sessionKey)
? "isolated"
: ctx.params.sessionKey
? "main"
: undefined);
if (isError && lastAssistant) {
const friendlyError = formatAssistantErrorText(lastAssistant, {
@ -59,6 +68,16 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
failoverReason,
model: lastAssistant.model,
provider: lastAssistant.provider,
sessionTarget,
sessionKey: ctx.params.sessionKey,
cronJobId: runContext.cronJobId,
maintenanceScope: runContext.maintenanceScope,
runContext: {
sessionTarget,
sessionKey: ctx.params.sessionKey,
cronJobId: runContext.cronJobId,
maintenanceScope: runContext.maintenanceScope,
},
...observedError,
consoleMessage: `embedded run agent end: runId=${safeRunId} isError=true model=${safeModel} provider=${safeProvider} error=${safeErrorText}`,
});

View File

@ -36,6 +36,11 @@ export type SubscribeEmbeddedPiSessionParams = {
sessionId?: string;
/** Agent identity for hook context — resolved from session config in attempt.ts. */
agentId?: string;
runContext?: {
sessionTarget?: "main" | "isolated";
cronJobId?: string;
maintenanceScope?: string;
};
};
export type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";

View File

@ -327,6 +327,7 @@ export async function runAgentTurnWithFallback(params: {
...embeddedContext,
allowGatewaySubagentBinding: true,
trigger: params.isHeartbeat ? "heartbeat" : "user",
runContext: { sessionTarget: "main" },
groupId: resolveGroupSessionKey(params.sessionCtx)?.id,
groupChannel:
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(),

View File

@ -496,6 +496,7 @@ export async function runMemoryFlushIfNeeded(params: {
...runBaseParams,
allowGatewaySubagentBinding: true,
trigger: "memory",
runContext: { maintenanceScope: "memory-flush" },
memoryFlushWritePath,
prompt: resolveMemoryFlushPromptForRun({
prompt: memoryFlushSettings.prompt,

View File

@ -179,6 +179,7 @@ export function createFollowupRunner(params: {
sessionKey: queued.run.sessionKey,
agentId: queued.run.agentId,
trigger: "user",
runContext: { sessionTarget: "main" },
messageChannel: queued.originatingChannel ?? undefined,
messageProvider: queued.run.messageProvider,
agentAccountId: queued.run.agentAccountId,

View File

@ -659,6 +659,10 @@ export async function runCronIsolatedAgentTurn(params: {
bootstrapContextMode: agentPayload?.lightContext ? "lightweight" : undefined,
bootstrapContextRunKind: "cron",
runId: cronSession.sessionEntry.sessionId,
runContext: {
sessionTarget: "isolated",
cronJobId: params.job.id,
},
requireExplicitMessageTarget: toolPolicy.requireExplicitMessageTarget,
disableMessageTool: toolPolicy.disableMessageTool,
allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe,