refactor(agents): extract attempt prompt observability

This commit is contained in:
Peter Steinberger
2026-07-14 04:03:12 +01:00
parent 4da0eb19c5
commit 661cf9644d
9 changed files with 505 additions and 201 deletions
@@ -5,7 +5,8 @@ import { resolveBlockMessage } from "../../../plugins/hook-decision-types.js";
import type { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import type { AgentMessage } from "../../runtime/index.js";
import { log } from "../logger.js";
import { cloneHookMessages, flushSessionManagerTranscript } from "./attempt-transcript-helpers.js";
import { cloneHookMessages } from "./attempt-hook-messages.js";
import { flushSessionManagerTranscript } from "./attempt-transcript-helpers.js";
import { sessionMessagesContainIdempotencyKey } from "./pre-persisted-user-turn.js";
import type { EmbeddedRunAttemptParams } from "./types.js";
@@ -0,0 +1,6 @@
import type { AgentMessage } from "../../runtime/index.js";
/** Gives hooks an isolated message snapshot they cannot mutate in-session. */
export function cloneHookMessages(messages: AgentMessage[]): AgentMessage[] {
return messages.map((message) => structuredClone(message));
}
@@ -0,0 +1,208 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AgentMessage } from "../../runtime/index.js";
const hoisted = vi.hoisted(() => ({
buildAgentHookContextChannelFields: vi.fn(() => ({ channel: "discord" })),
buildAgentHookContextIdentityFields: vi.fn(() => ({ senderId: "sender-1" })),
emitTrustedDiagnosticEvent: vi.fn(),
hasHooks: vi.fn(() => true),
log: {
debug: vi.fn(),
info: vi.fn(),
isEnabled: vi.fn(() => false),
warn: vi.fn(),
},
onExecutionPhase: vi.fn(),
recordStage: vi.fn(),
recordTrajectoryEvent: vi.fn(),
runLlmInput: vi.fn(
async (
_event: {
historyMessages?: unknown[];
imagesCount?: number;
prompt?: string;
tools?: unknown[];
},
_context: Record<string, unknown>,
) => undefined,
),
toTrajectoryToolDefinitions: vi.fn((tools: ReadonlyArray<{ name?: string }>) =>
tools.map((tool) => ({ name: tool.name })),
),
}));
vi.mock("../../../infra/diagnostic-events.js", () => ({
emitTrustedDiagnosticEvent: hoisted.emitTrustedDiagnosticEvent,
}));
vi.mock("../../../plugins/hook-agent-context.js", () => ({
buildAgentHookContextChannelFields: hoisted.buildAgentHookContextChannelFields,
buildAgentHookContextIdentityFields: hoisted.buildAgentHookContextIdentityFields,
}));
vi.mock("../../../trajectory/runtime.js", () => ({
toTrajectoryToolDefinitions: hoisted.toTrajectoryToolDefinitions,
}));
vi.mock("../logger.js", () => ({ log: hoisted.log }));
import { observeEmbeddedAttemptPrompt } from "./attempt-prompt-observability.js";
type PromptObservabilityInput = Parameters<typeof observeEmbeddedAttemptPrompt>[0];
function createInput(overrides: Partial<PromptObservabilityInput> = {}): PromptObservabilityInput {
const sessionMessages: AgentMessage[] = [
{
role: "user",
content: [{ type: "text", text: "history" }],
timestamp: 1,
},
];
return {
attempt: {
messageChannel: "discord",
modelId: "model-1",
onExecutionPhase: hoisted.onExecutionPhase,
provider: "provider-1",
runId: "run-1",
senderId: "sender-1",
sessionFile: "/tmp/session.jsonl",
sessionId: "session-1",
sessionKey: "agent:main:discord:channel-1",
trigger: "user",
workspaceDir: "/tmp/workspace",
},
cacheTrace: { recordStage: hoisted.recordStage },
contextTokenBudget: 32_000,
diagnosticTrace: {
traceId: "1".repeat(32),
spanId: "2".repeat(16),
},
effectivePrompt: "effective prompt",
effectiveTools: [{ name: "visible-tool" }],
hookAgentId: "main",
hookMessagesForCurrentPrompt: sessionMessages,
hookRunner: {
hasHooks: hoisted.hasHooks,
runLlmInput: hoisted.runLlmInput,
},
imageCount: 2,
isRawModelRun: false,
llmBoundaryPromptForPrecheck: "[boundary] model prompt",
promptForModel: "model prompt",
promptSubmissionRuntimeOnly: false,
reserveTokens: 4_096,
runTrace: {
traceId: "1".repeat(32),
spanId: "3".repeat(16),
},
sessionMessages,
skipPromptSubmission: false,
streamStrategy: "provider-stream",
systemPromptForHook: "system prompt",
systemPromptText: "system prompt",
toolSearchCompacted: true,
tools: [{ name: "hook-tool" }],
trajectoryRecorder: { recordEvent: hoisted.recordTrajectoryEvent },
transcriptLeafId: "leaf-1",
transport: "sse",
uncompactedEffectiveTools: [{ name: "visible-tool" }, { name: "deferred-tool" }],
...overrides,
} as PromptObservabilityInput;
}
describe("observeEmbeddedAttemptPrompt", () => {
beforeEach(() => {
vi.clearAllMocks();
hoisted.hasHooks.mockReturnValue(true);
hoisted.log.isEnabled.mockReturnValue(false);
});
it("records the assembled prompt boundary and dispatches a cloned llm_input snapshot", () => {
const input = createInput();
expect(observeEmbeddedAttemptPrompt(input)).toEqual({ skipPromptSubmission: false });
expect(hoisted.recordStage).toHaveBeenNthCalledWith(1, "prompt:before", {
prompt: "model prompt",
messages: input.sessionMessages,
});
expect(hoisted.recordStage).toHaveBeenNthCalledWith(2, "prompt:images", {
prompt: "model prompt",
messages: input.sessionMessages,
note: "images: prompt=2",
});
expect(hoisted.recordTrajectoryEvent).toHaveBeenCalledWith(
"context.compiled",
expect.objectContaining({
imagesCount: 2,
providerVisibleTools: [{ name: "visible-tool" }],
tools: [{ name: "visible-tool" }, { name: "deferred-tool" }],
transcriptLeafId: "leaf-1",
}),
);
expect(hoisted.emitTrustedDiagnosticEvent).toHaveBeenCalledWith(
expect.objectContaining({
type: "context.assembled",
contextTokenBudget: 32_000,
historyTextChars: 7,
messageCount: 1,
promptChars: 16,
reserveTokens: 4_096,
}),
);
expect(hoisted.onExecutionPhase).toHaveBeenCalledWith({
phase: "context_assembled",
provider: "provider-1",
model: "model-1",
});
expect(hoisted.runLlmInput).toHaveBeenCalledOnce();
const [event, context] = hoisted.runLlmInput.mock.calls[0] ?? [];
expect(event).toMatchObject({
prompt: "[boundary] model prompt",
imagesCount: 2,
tools: [{ name: "hook-tool" }],
});
expect(event?.historyMessages).toEqual(input.hookMessagesForCurrentPrompt);
expect(event?.historyMessages).not.toBe(input.hookMessagesForCurrentPrompt);
expect(context).toMatchObject({
agentId: "main",
channel: "discord",
senderId: "sender-1",
});
});
it("marks a blank current prompt as skipped while preserving its compiled trace", () => {
const input = createInput({
imageCount: 0,
llmBoundaryPromptForPrecheck: " ",
promptForModel: " ",
});
expect(observeEmbeddedAttemptPrompt(input)).toEqual({ skipPromptSubmission: true });
expect(hoisted.recordTrajectoryEvent).toHaveBeenNthCalledWith(
1,
"context.compiled",
expect.any(Object),
);
expect(hoisted.recordTrajectoryEvent).toHaveBeenNthCalledWith(2, "prompt.skipped", {
reason: "blank_user_prompt",
prompt: " ",
messages: input.sessionMessages,
imagesCount: 0,
});
expect(hoisted.log.warn).toHaveBeenCalledWith(
expect.stringContaining("embedded run prompt skipped: blank user prompt"),
);
expect(hoisted.runLlmInput).not.toHaveBeenCalled();
});
it("keeps an already-blocked prompt out of cache, trajectory, and hook dispatch", () => {
const input = createInput({ skipPromptSubmission: true });
expect(observeEmbeddedAttemptPrompt(input)).toEqual({ skipPromptSubmission: true });
expect(hoisted.recordStage).not.toHaveBeenCalled();
expect(hoisted.recordTrajectoryEvent).not.toHaveBeenCalled();
expect(hoisted.runLlmInput).not.toHaveBeenCalled();
expect(hoisted.emitTrustedDiagnosticEvent).toHaveBeenCalledOnce();
});
});
@@ -0,0 +1,218 @@
/** Records the fully assembled prompt boundary before preflight and submission. */
import { emitTrustedDiagnosticEvent } from "../../../infra/diagnostic-events.js";
import {
createChildDiagnosticTraceContext,
type DiagnosticTraceContext,
freezeDiagnosticTraceContext,
} from "../../../infra/diagnostic-trace-context.js";
import {
buildAgentHookContextChannelFields,
buildAgentHookContextIdentityFields,
} from "../../../plugins/hook-agent-context.js";
import type { PluginHookLlmInputEvent } from "../../../plugins/hook-types.js";
import type { HookRunner } from "../../../plugins/hooks.js";
import {
type createTrajectoryRuntimeRecorder,
toTrajectoryToolDefinitions,
} from "../../../trajectory/runtime.js";
import type { createCacheTrace } from "../../cache-trace.js";
import type { AgentMessage } from "../../runtime/index.js";
import type { AgentSession } from "../../sessions/index.js";
import { log } from "../logger.js";
import { summarizeSessionContext } from "./attempt-context-summary.js";
import { cloneHookMessages } from "./attempt-hook-messages.js";
import { resolvePromptSubmissionSkipReason } from "./attempt-prompt-skip.js";
import type { EmbeddedRunAttemptParams } from "./types.js";
type AttemptPromptObservabilityParams = Pick<
EmbeddedRunAttemptParams,
| "channelContext"
| "chatId"
| "currentChannelId"
| "messageChannel"
| "messageProvider"
| "messageTo"
| "modelId"
| "onExecutionPhase"
| "provider"
| "runId"
| "senderId"
| "sessionFile"
| "sessionId"
| "sessionKey"
| "trigger"
| "workspaceDir"
>;
type CacheTrace = Pick<NonNullable<ReturnType<typeof createCacheTrace>>, "recordStage"> | null;
type PromptHookRunner = Pick<HookRunner, "hasHooks" | "runLlmInput"> | null;
type TrajectoryRecorder = Pick<
NonNullable<ReturnType<typeof createTrajectoryRuntimeRecorder>>,
"recordEvent"
> | null;
type TrajectoryTool = Parameters<typeof toTrajectoryToolDefinitions>[0][number];
export function observeEmbeddedAttemptPrompt(input: {
attempt: AttemptPromptObservabilityParams;
cacheTrace: CacheTrace;
contextTokenBudget: number;
diagnosticTrace: DiagnosticTraceContext;
effectivePrompt: string;
effectiveTools: readonly TrajectoryTool[];
hookAgentId: string;
hookMessagesForCurrentPrompt: AgentMessage[];
hookRunner: PromptHookRunner;
imageCount: number;
isRawModelRun: boolean;
llmBoundaryPromptForPrecheck: string;
promptForModel: string;
promptSubmissionRuntimeOnly?: boolean;
reserveTokens: number;
runTrace: DiagnosticTraceContext;
sessionMessages: AgentMessage[];
skipPromptSubmission: boolean;
streamStrategy: string;
systemPromptForHook: string;
systemPromptText?: string;
toolSearchCompacted: boolean;
tools: PluginHookLlmInputEvent["tools"];
trajectoryRecorder: TrajectoryRecorder;
transcriptLeafId: string | null;
transport: AgentSession["agent"]["transport"];
uncompactedEffectiveTools: readonly TrajectoryTool[];
}): { skipPromptSubmission: boolean } {
const { attempt } = input;
let skipPromptSubmission = input.skipPromptSubmission;
if (!skipPromptSubmission) {
input.cacheTrace?.recordStage("prompt:before", {
prompt: input.promptForModel,
messages: input.sessionMessages,
});
input.cacheTrace?.recordStage("prompt:images", {
prompt: input.promptForModel,
messages: input.sessionMessages,
note: `images: prompt=${input.imageCount}`,
});
const providerVisibleTools = toTrajectoryToolDefinitions(input.effectiveTools);
input.trajectoryRecorder?.recordEvent("context.compiled", {
systemPrompt: input.systemPromptForHook,
prompt: input.promptForModel,
messages: input.sessionMessages,
tools: toTrajectoryToolDefinitions(
input.toolSearchCompacted ? input.uncompactedEffectiveTools : input.effectiveTools,
),
...(input.toolSearchCompacted ? { providerVisibleTools } : {}),
imagesCount: input.imageCount,
streamStrategy: input.streamStrategy,
transport: input.transport,
transcriptLeafId: input.transcriptLeafId,
});
}
const promptSkipReason = skipPromptSubmission
? null
: resolvePromptSubmissionSkipReason({
prompt: input.promptForModel,
messages: input.sessionMessages,
runtimeOnly: input.promptSubmissionRuntimeOnly,
imageCount: input.imageCount,
});
if (promptSkipReason) {
skipPromptSubmission = true;
const skipContext =
`runId=${attempt.runId} sessionId=${attempt.sessionId} trigger=${attempt.trigger} ` +
`provider=${attempt.provider}/${attempt.modelId}`;
if (promptSkipReason === "blank_user_prompt") {
log.warn(`embedded run prompt skipped: blank user prompt ${skipContext}`);
} else {
log.info(`embedded run prompt skipped: empty prompt/history/images ${skipContext}`);
}
input.trajectoryRecorder?.recordEvent("prompt.skipped", {
reason: promptSkipReason,
prompt: input.promptForModel,
messages: input.sessionMessages,
imagesCount: input.imageCount,
});
}
const sessionSummary = summarizeSessionContext(input.sessionMessages);
emitTrustedDiagnosticEvent({
type: "context.assembled",
runId: attempt.runId,
...(attempt.sessionKey && { sessionKey: attempt.sessionKey }),
...(attempt.sessionId && { sessionId: attempt.sessionId }),
provider: attempt.provider,
model: attempt.modelId,
...((attempt.messageChannel ?? attempt.messageProvider)
? { channel: attempt.messageChannel ?? attempt.messageProvider }
: {}),
trigger: attempt.trigger,
messageCount: input.sessionMessages.length,
historyTextChars: sessionSummary.totalTextChars,
historyImageBlocks: sessionSummary.totalImageBlocks,
maxMessageTextChars: sessionSummary.maxMessageTextChars,
systemPromptChars: input.systemPromptText?.length ?? 0,
promptChars: input.effectivePrompt.length,
promptImages: input.imageCount,
contextTokenBudget: input.contextTokenBudget,
reserveTokens: input.reserveTokens,
trace: freezeDiagnosticTraceContext(createChildDiagnosticTraceContext(input.runTrace)),
});
attempt.onExecutionPhase?.({
phase: "context_assembled",
provider: attempt.provider,
model: attempt.modelId,
});
if (log.isEnabled("debug")) {
log.debug(
`[context-diag] pre-prompt: sessionKey=${attempt.sessionKey ?? attempt.sessionId} ` +
`messages=${input.sessionMessages.length} roleCounts=${sessionSummary.roleCounts} ` +
`historyTextChars=${sessionSummary.totalTextChars} ` +
`maxMessageTextChars=${sessionSummary.maxMessageTextChars} ` +
`historyImageBlocks=${sessionSummary.totalImageBlocks} ` +
`systemPromptChars=${input.systemPromptText?.length ?? 0} ` +
`promptChars=${input.effectivePrompt.length} ` +
`promptImages=${input.imageCount} ` +
`provider=${attempt.provider}/${attempt.modelId} sessionFile=${attempt.sessionFile}`,
);
}
if (!skipPromptSubmission && !input.isRawModelRun && input.hookRunner?.hasHooks("llm_input")) {
void input.hookRunner
.runLlmInput(
{
runId: attempt.runId,
sessionId: attempt.sessionId,
provider: attempt.provider,
model: attempt.modelId,
systemPrompt: input.systemPromptForHook,
prompt: input.llmBoundaryPromptForPrecheck,
historyMessages: cloneHookMessages(input.hookMessagesForCurrentPrompt),
imagesCount: input.imageCount,
tools: input.tools,
},
{
runId: attempt.runId,
trace: freezeDiagnosticTraceContext(input.diagnosticTrace),
agentId: input.hookAgentId,
sessionKey: attempt.sessionKey,
sessionId: attempt.sessionId,
workspaceDir: attempt.workspaceDir,
trigger: attempt.trigger,
...buildAgentHookContextChannelFields(attempt),
...buildAgentHookContextIdentityFields({
trigger: attempt.trigger,
senderId: attempt.senderId,
chatId: attempt.chatId,
channelContext: attempt.channelContext,
}),
},
)
.catch((err: unknown) => {
log.warn(`llm_input hook failed: ${String(err)}`);
});
}
return { skipPromptSubmission };
}
@@ -0,0 +1,41 @@
export type PromptSubmissionSkipReason = "blank_user_prompt" | "empty_prompt_history_images";
/** Classifies prompt submissions that have no visible current-turn content. */
export function resolvePromptSubmissionSkipReason(params: {
prompt: string;
messages: readonly unknown[];
imageCount: number;
runtimeOnly?: boolean;
}): PromptSubmissionSkipReason | null {
if (params.prompt.trim().length > 0 || params.imageCount > 0) {
return null;
}
return params.messages.some(hasVisiblePromptHistory)
? "blank_user_prompt"
: "empty_prompt_history_images";
}
function hasVisiblePromptHistory(message: unknown): boolean {
if (!message || typeof message !== "object") {
return false;
}
const record = message as { role?: unknown; content?: unknown };
if (record.role !== "user" && record.role !== "assistant") {
return false;
}
return hasNonEmptyContent(record.content);
}
function hasNonEmptyContent(content: unknown): boolean {
if (typeof content === "string") {
return content.trim().length > 0;
}
if (Array.isArray(content)) {
return content.some(hasNonEmptyContent);
}
if (!content || typeof content !== "object") {
return false;
}
const record = content as { text?: unknown; content?: unknown };
return hasNonEmptyContent(record.text) || hasNonEmptyContent(record.content);
}
@@ -21,10 +21,6 @@ import type { EmbeddedRunAttemptParams } from "./types.js";
type AttemptSessionManager = ReturnType<typeof guardSessionManager>;
export function cloneHookMessages(messages: AgentMessage[]): AgentMessage[] {
return messages.map((message) => structuredClone(message));
}
export function flushSessionManagerTranscript(sessionManager: AttemptSessionManager): void {
(
sessionManager as unknown as {
@@ -36,10 +36,10 @@ vi.mock("../../music-generation-task-status.js", () => musicGenerationTaskStatus
vi.mock("../../video-generation-task-status.js", () => videoGenerationTaskStatusMocks);
vi.mock("../../../plugins/host-hook-state.js", () => hostHookStateMocks);
import { resolvePromptSubmissionSkipReason } from "./attempt-prompt-skip.js";
import {
forgetPromptBuildDrainCacheForRun,
mergeOrphanedTrailingUserPrompt,
resolvePromptSubmissionSkipReason,
resolveAttemptMediaTaskSystemPromptAddition,
resolvePromptBuildHookResult,
shouldInjectHeartbeatPrompt,
@@ -272,52 +272,6 @@ export function shouldWarnOnOrphanedUserRepair(
return trigger === "user" || trigger === "manual";
}
type PromptSubmissionSkipReason = "blank_user_prompt" | "empty_prompt_history_images";
/**
* Distinguishes a truly empty prompt/history from a blank follow-up in a visible
* conversation. This lets callers skip model submission while reporting the
* reason accurately.
*/
export function resolvePromptSubmissionSkipReason(params: {
prompt: string;
messages: readonly unknown[];
imageCount: number;
runtimeOnly?: boolean;
}): PromptSubmissionSkipReason | null {
if (params.prompt.trim().length > 0 || params.imageCount > 0) {
return null;
}
return params.messages.some(hasVisiblePromptHistory)
? "blank_user_prompt"
: "empty_prompt_history_images";
}
function hasVisiblePromptHistory(message: unknown): boolean {
if (!message || typeof message !== "object") {
return false;
}
const record = message as { role?: unknown; content?: unknown };
if (record.role !== "user" && record.role !== "assistant") {
return false;
}
return hasNonEmptyContent(record.content);
}
function hasNonEmptyContent(content: unknown): boolean {
if (typeof content === "string") {
return content.trim().length > 0;
}
if (Array.isArray(content)) {
return content.some(hasNonEmptyContent);
}
if (!content || typeof content !== "object") {
return false;
}
const record = content as { text?: unknown; content?: unknown };
return hasNonEmptyContent(record.text) || hasNonEmptyContent(record.content);
}
const QUEUED_USER_MESSAGE_MARKER =
"[Queued user message from a previous active turn; preserved as context only. " +
"Continue with the active prompt below.]";
+29 -149
View File
@@ -13,18 +13,8 @@ import {
OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST,
} from "../../../context-engine/host-compat.js";
import { resolveContextEngineOwnerPluginId } from "../../../context-engine/registry.js";
import { emitTrustedDiagnosticEvent } from "../../../infra/diagnostic-events.js";
import {
createChildDiagnosticTraceContext,
freezeDiagnosticTraceContext,
} from "../../../infra/diagnostic-trace-context.js";
import { formatErrorMessage } from "../../../infra/errors.js";
import type { AssistantMessage } from "../../../llm/types.js";
import {
buildAgentHookContextChannelFields,
buildAgentHookContextIdentityFields,
} from "../../../plugins/hook-agent-context.js";
import { toTrajectoryToolDefinitions } from "../../../trajectory/runtime.js";
import { createBundleLspToolRuntime } from "../../agent-bundle-lsp-runtime.js";
import { materializeBundleMcpToolsForRun } from "../../agent-bundle-mcp-tools.js";
import { resolveAgentDir, resolveSessionAgentIds } from "../../agent-scope.js";
@@ -61,10 +51,10 @@ import { runEmbeddedAttemptBeforeAgentRun } from "./attempt-before-agent-run.js"
import { prepareEmbeddedAttemptBootstrap } from "./attempt-bootstrap-prepare.js";
import { prepareEmbeddedAttemptBundleTools } from "./attempt-bundle-tools.js";
import { installEmbeddedAttemptContextGuards } from "./attempt-context-guards.js";
import { summarizeSessionContext } from "./attempt-context-summary.js";
import { prepareEmbeddedAttemptHistory } from "./attempt-history-prepare.js";
import { prepareEmbeddedAttemptPromptAssembly } from "./attempt-prompt-assembly.js";
import { prepareEmbeddedAttemptPromptContext } from "./attempt-prompt-context.js";
import { observeEmbeddedAttemptPrompt } from "./attempt-prompt-observability.js";
import {
handleEmbeddedAttemptMidTurnPrecheck,
prepareEmbeddedAttemptPromptPreflight,
@@ -92,11 +82,7 @@ import { prepareEmbeddedAttemptTimeout } from "./attempt-timeout-prepare.js";
import { prepareEmbeddedAttemptToolBase } from "./attempt-tool-base-prepare.js";
import { prepareEmbeddedAttemptToolCatalog } from "./attempt-tool-catalog.js";
import { prepareEmbeddedAttemptTrajectory } from "./attempt-trajectory.js";
import {
cloneHookMessages,
removeTrailingMidTurnPrecheckAssistantError,
} from "./attempt-transcript-helpers.js";
import { resolvePromptSubmissionSkipReason } from "./attempt.prompt-helpers.js";
import { removeTrailingMidTurnPrecheckAssistantError } from "./attempt-transcript-helpers.js";
import { resolveEmbeddedAttemptSessionWriteLockOptions } from "./attempt.run-decisions.js";
import {
acquireEmbeddedAttemptSessionFileOwner,
@@ -996,142 +982,36 @@ export async function runEmbeddedAttempt(
: undefined,
});
if (!skipPromptSubmission) {
cacheTrace?.recordStage("prompt:before", {
prompt: promptForModel,
messages: activeSession.messages,
});
cacheTrace?.recordStage("prompt:images", {
prompt: promptForModel,
messages: activeSession.messages,
note: `images: prompt=${imageResult.images.length}`,
});
const trajectoryProviderVisibleTools = toTrajectoryToolDefinitions(effectiveTools);
trajectoryRecorder?.recordEvent("context.compiled", {
systemPrompt: systemPromptForHook,
prompt: promptForModel,
messages: activeSession.messages,
tools: toTrajectoryToolDefinitions(
toolSearch.compacted ? uncompactedEffectiveTools : effectiveTools,
),
...(toolSearch.compacted
? { providerVisibleTools: trajectoryProviderVisibleTools }
: {}),
imagesCount: imageResult.images.length,
streamStrategy,
transport: effectiveAgentTransport,
transcriptLeafId,
});
}
const promptSkipReason = skipPromptSubmission
? null
: resolvePromptSubmissionSkipReason({
prompt: promptForModel,
messages: activeSession.messages,
runtimeOnly: promptSubmission.runtimeOnly,
imageCount: imageResult.images.length,
});
if (promptSkipReason) {
skipPromptSubmission = true;
const skipContext =
`runId=${params.runId} sessionId=${params.sessionId} trigger=${params.trigger} ` +
`provider=${params.provider}/${params.modelId}`;
if (promptSkipReason === "blank_user_prompt") {
log.warn(`embedded run prompt skipped: blank user prompt ${skipContext}`);
} else {
log.info(`embedded run prompt skipped: empty prompt/history/images ${skipContext}`);
}
trajectoryRecorder?.recordEvent("prompt.skipped", {
reason: promptSkipReason,
prompt: promptForModel,
messages: activeSession.messages,
imagesCount: imageResult.images.length,
});
}
const msgCount = activeSession.messages.length;
const systemLen = systemPromptText?.length ?? 0;
const promptLen = effectivePrompt.length;
const sessionSummary = summarizeSessionContext(activeSession.messages);
const reserveTokens = settingsManager.getCompactionReserveTokens();
emitTrustedDiagnosticEvent({
type: "context.assembled",
runId: params.runId,
...(params.sessionKey && { sessionKey: params.sessionKey }),
...(params.sessionId && { sessionId: params.sessionId }),
provider: params.provider,
model: params.modelId,
...((params.messageChannel ?? params.messageProvider)
? { channel: params.messageChannel ?? params.messageProvider }
: {}),
trigger: params.trigger,
messageCount: msgCount,
historyTextChars: sessionSummary.totalTextChars,
historyImageBlocks: sessionSummary.totalImageBlocks,
maxMessageTextChars: sessionSummary.maxMessageTextChars,
systemPromptChars: systemLen,
promptChars: promptLen,
promptImages: imageResult.images.length,
skipPromptSubmission = observeEmbeddedAttemptPrompt({
attempt: params,
cacheTrace,
contextTokenBudget,
diagnosticTrace,
effectivePrompt,
effectiveTools,
hookAgentId,
hookMessagesForCurrentPrompt,
hookRunner,
imageCount: imageResult.images.length,
isRawModelRun,
llmBoundaryPromptForPrecheck,
promptForModel,
promptSubmissionRuntimeOnly: promptSubmission.runtimeOnly,
reserveTokens,
trace: freezeDiagnosticTraceContext(createChildDiagnosticTraceContext(runTrace)),
});
params.onExecutionPhase?.({
phase: "context_assembled",
provider: params.provider,
model: params.modelId,
});
// Diagnostic: log context sizes before prompt to help debug early overflow errors.
if (log.isEnabled("debug")) {
log.debug(
`[context-diag] pre-prompt: sessionKey=${params.sessionKey ?? params.sessionId} ` +
`messages=${msgCount} roleCounts=${sessionSummary.roleCounts} ` +
`historyTextChars=${sessionSummary.totalTextChars} ` +
`maxMessageTextChars=${sessionSummary.maxMessageTextChars} ` +
`historyImageBlocks=${sessionSummary.totalImageBlocks} ` +
`systemPromptChars=${systemLen} promptChars=${promptLen} ` +
`promptImages=${imageResult.images.length} ` +
`provider=${params.provider}/${params.modelId} sessionFile=${params.sessionFile}`,
);
}
if (!skipPromptSubmission && !isRawModelRun && hookRunner?.hasHooks("llm_input")) {
hookRunner
.runLlmInput(
{
runId: params.runId,
sessionId: params.sessionId,
provider: params.provider,
model: params.modelId,
systemPrompt: systemPromptForHook,
prompt: llmBoundaryPromptForPrecheck,
historyMessages: cloneHookMessages(hookMessagesForCurrentPrompt),
imagesCount: imageResult.images.length,
tools,
},
{
runId: params.runId,
trace: freezeDiagnosticTraceContext(diagnosticTrace),
agentId: hookAgentId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
workspaceDir: params.workspaceDir,
trigger: params.trigger,
...buildAgentHookContextChannelFields(params),
...buildAgentHookContextIdentityFields({
trigger: params.trigger,
senderId: params.senderId,
chatId: params.chatId,
channelContext: params.channelContext,
}),
},
)
.catch((err: unknown) => {
log.warn(`llm_input hook failed: ${String(err)}`);
});
}
runTrace,
sessionMessages: activeSession.messages,
skipPromptSubmission,
streamStrategy,
systemPromptForHook,
systemPromptText,
toolSearchCompacted: toolSearch.compacted,
tools,
trajectoryRecorder,
transcriptLeafId,
transport: effectiveAgentTransport,
uncompactedEffectiveTools,
}).skipPromptSubmission;
const promptPreflight = await prepareEmbeddedAttemptPromptPreflight({
attempt: params,