mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-05-01 06:36:23 +08:00
feat(diagnostics): carry trace context through hooks
Pass immutable diagnostic trace contexts through agent and tool hook surfaces, emit model usage with the run trace, and parent OTEL spans/logs from validated trace context without retained global state.\n\nThanks @vincentkoc.
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Diagnostics/OTEL: add a lightweight diagnostic trace-context carrier for future span correlation without adding OTEL SDK state to core. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: attach diagnostic trace context to exported OTEL logs so log records can correlate with future spans without adding retained process state. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: pass immutable per-run diagnostic trace context through agent and tool hook contexts, and parent exported diagnostic spans from validated context without retaining global trace state. Thanks @vincentkoc.
|
||||
- Control UI/chat: add a Steer action on queued messages so a browser follow-up can be injected into the active run without retyping it.
|
||||
- Control UI/Talk: add browser WebRTC realtime voice sessions backed by OpenAI Realtime, with Gateway-minted ephemeral client secrets and `openclaw_agent_consult` handoff to the full OpenClaw agent.
|
||||
- Agents/tools: add optional per-call `timeoutMs` support for image, video, music, and TTS generation tools so agents can extend provider request timeouts only when a specific generation needs it.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ad7ec565b1702a76a87b1a08904445c9838e10d4d41fb1c58909af886b702d80 plugin-sdk-api-baseline.json
|
||||
907a07c206dd52ebd910793fab7bca8640c37cf82ff7e7cca88ab1b12b4fbdfe plugin-sdk-api-baseline.jsonl
|
||||
c0f788d1895ced2ffdad9f82e6afc592171e6651c61c0fc5083f0040437cda6d plugin-sdk-api-baseline.json
|
||||
70e320157331080b98f9c2acae58e89ad1dc70b48adad265225a7eb76b6ac29f plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -106,7 +106,7 @@ const handler = async (event) => {
|
||||
export default handler;
|
||||
```
|
||||
|
||||
Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (push to send to user), and `context` (event-specific data).
|
||||
Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (push to send to user), and `context` (event-specific data). Agent and tool plugin hook contexts can also include `trace`, a read-only W3C-compatible diagnostic trace context that plugins may pass into structured logs for OTEL correlation.
|
||||
|
||||
### Event context highlights
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const telemetryState = vi.hoisted(() => {
|
||||
const counters = new Map<string, { add: ReturnType<typeof vi.fn> }>();
|
||||
const histograms = new Map<string, { record: ReturnType<typeof vi.fn> }>();
|
||||
const tracer = {
|
||||
startSpan: vi.fn((_name: string, _opts?: unknown) => ({
|
||||
startSpan: vi.fn((_name: string, _opts?: unknown, _ctx?: unknown) => ({
|
||||
end: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
})),
|
||||
@@ -384,6 +384,64 @@ describe("diagnostics-otel service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("parents diagnostic event spans from trace context", async () => {
|
||||
const service = createDiagnosticsOtelService();
|
||||
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true });
|
||||
await service.start(ctx);
|
||||
|
||||
emitDiagnosticEvent({
|
||||
type: "model.usage",
|
||||
trace: {
|
||||
traceId: TRACE_ID,
|
||||
spanId: SPAN_ID,
|
||||
traceFlags: "01",
|
||||
},
|
||||
provider: "openai",
|
||||
model: "gpt-5.4",
|
||||
usage: { total: 4 },
|
||||
durationMs: 12,
|
||||
});
|
||||
|
||||
const modelUsageCall = telemetryState.tracer.startSpan.mock.calls.find(
|
||||
(call) => call[0] === "openclaw.model.usage",
|
||||
);
|
||||
expect(modelUsageCall?.[2]).toEqual({
|
||||
spanContext: expect.objectContaining({
|
||||
traceId: TRACE_ID,
|
||||
spanId: SPAN_ID,
|
||||
traceFlags: 1,
|
||||
isRemote: true,
|
||||
}),
|
||||
});
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
test("ignores invalid diagnostic event trace parents", async () => {
|
||||
const service = createDiagnosticsOtelService();
|
||||
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true });
|
||||
await service.start(ctx);
|
||||
|
||||
emitDiagnosticEvent({
|
||||
type: "model.usage",
|
||||
trace: {
|
||||
traceId: "0".repeat(32),
|
||||
spanId: "not-a-span",
|
||||
traceFlags: "zz",
|
||||
},
|
||||
provider: "openai",
|
||||
model: "gpt-5.4",
|
||||
usage: { total: 4 },
|
||||
durationMs: 12,
|
||||
});
|
||||
|
||||
const modelUsageCall = telemetryState.tracer.startSpan.mock.calls.find(
|
||||
(call) => call[0] === "openclaw.model.usage",
|
||||
);
|
||||
expect(telemetryState.tracer.setSpanContext).not.toHaveBeenCalled();
|
||||
expect(modelUsageCall?.[2]).toBeUndefined();
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
test("redacts sensitive reason in session.state metric attributes", async () => {
|
||||
const service = createDiagnosticsOtelService();
|
||||
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { metrics: true });
|
||||
|
||||
@@ -137,22 +137,36 @@ function traceFlagsToOtel(traceFlags: string | undefined): TraceFlags {
|
||||
return (parsed & TraceFlags.SAMPLED) !== 0 ? TraceFlags.SAMPLED : TraceFlags.NONE;
|
||||
}
|
||||
|
||||
function contextForTraceContext(traceContext: DiagnosticTraceContext | undefined) {
|
||||
const normalized = normalizeTraceContext(traceContext);
|
||||
if (!normalized?.spanId) {
|
||||
return undefined;
|
||||
}
|
||||
return trace.setSpanContext(otelContextApi.active(), {
|
||||
traceId: normalized.traceId,
|
||||
spanId: normalized.spanId,
|
||||
traceFlags: traceFlagsToOtel(normalized.traceFlags),
|
||||
isRemote: true,
|
||||
});
|
||||
}
|
||||
|
||||
function addTraceAttributes(
|
||||
attributes: Record<string, string | number | boolean>,
|
||||
traceContext: DiagnosticTraceContext | undefined,
|
||||
): void {
|
||||
if (!traceContext) {
|
||||
const normalized = normalizeTraceContext(traceContext);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
attributes["openclaw.traceId"] = traceContext.traceId;
|
||||
if (traceContext.spanId) {
|
||||
attributes["openclaw.spanId"] = traceContext.spanId;
|
||||
attributes["openclaw.traceId"] = normalized.traceId;
|
||||
if (normalized.spanId) {
|
||||
attributes["openclaw.spanId"] = normalized.spanId;
|
||||
}
|
||||
if (traceContext.parentSpanId) {
|
||||
attributes["openclaw.parentSpanId"] = traceContext.parentSpanId;
|
||||
if (normalized.parentSpanId) {
|
||||
attributes["openclaw.parentSpanId"] = normalized.parentSpanId;
|
||||
}
|
||||
if (traceContext.traceFlags) {
|
||||
attributes["openclaw.traceFlags"] = traceContext.traceFlags;
|
||||
if (normalized.traceFlags) {
|
||||
attributes["openclaw.traceFlags"] = normalized.traceFlags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,13 +462,9 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
attributes: redactOtelAttributes(attributes),
|
||||
timestamp: meta?.date ?? new Date(),
|
||||
};
|
||||
if (traceContext?.spanId) {
|
||||
logRecord.context = trace.setSpanContext(otelContextApi.active(), {
|
||||
traceId: traceContext.traceId,
|
||||
spanId: traceContext.spanId,
|
||||
traceFlags: traceFlagsToOtel(traceContext.traceFlags),
|
||||
isRemote: true,
|
||||
});
|
||||
const logContext = contextForTraceContext(traceContext);
|
||||
if (logContext) {
|
||||
logRecord.context = logContext;
|
||||
}
|
||||
otelLogger.emit(logRecord);
|
||||
} catch (err) {
|
||||
@@ -467,13 +477,19 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
name: string,
|
||||
attributes: Record<string, string | number>,
|
||||
durationMs?: number,
|
||||
traceContext?: DiagnosticTraceContext,
|
||||
) => {
|
||||
const startTime =
|
||||
typeof durationMs === "number" ? Date.now() - Math.max(0, durationMs) : undefined;
|
||||
const span = tracer.startSpan(name, {
|
||||
attributes,
|
||||
...(startTime ? { startTime } : {}),
|
||||
});
|
||||
const parentContext = contextForTraceContext(traceContext);
|
||||
const span = tracer.startSpan(
|
||||
name,
|
||||
{
|
||||
attributes,
|
||||
...(startTime ? { startTime } : {}),
|
||||
},
|
||||
parentContext,
|
||||
);
|
||||
return span;
|
||||
};
|
||||
|
||||
@@ -537,7 +553,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
"openclaw.tokens.total": usage.total ?? 0,
|
||||
};
|
||||
|
||||
const span = spanWithDuration("openclaw.model.usage", spanAttrs, evt.durationMs);
|
||||
const span = spanWithDuration("openclaw.model.usage", spanAttrs, evt.durationMs, evt.trace);
|
||||
span.end();
|
||||
};
|
||||
|
||||
@@ -568,7 +584,12 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
if (evt.chatId !== undefined) {
|
||||
spanAttrs["openclaw.chatId"] = String(evt.chatId);
|
||||
}
|
||||
const span = spanWithDuration("openclaw.webhook.processed", spanAttrs, evt.durationMs);
|
||||
const span = spanWithDuration(
|
||||
"openclaw.webhook.processed",
|
||||
spanAttrs,
|
||||
evt.durationMs,
|
||||
evt.trace,
|
||||
);
|
||||
span.end();
|
||||
};
|
||||
|
||||
@@ -591,9 +612,13 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
if (evt.chatId !== undefined) {
|
||||
spanAttrs["openclaw.chatId"] = String(evt.chatId);
|
||||
}
|
||||
const span = tracer.startSpan("openclaw.webhook.error", {
|
||||
attributes: spanAttrs,
|
||||
});
|
||||
const span = tracer.startSpan(
|
||||
"openclaw.webhook.error",
|
||||
{
|
||||
attributes: spanAttrs,
|
||||
},
|
||||
contextForTraceContext(evt.trace),
|
||||
);
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: redactedError });
|
||||
span.end();
|
||||
};
|
||||
@@ -648,7 +673,12 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
if (evt.reason) {
|
||||
spanAttrs["openclaw.reason"] = redactSensitiveText(evt.reason);
|
||||
}
|
||||
const span = spanWithDuration("openclaw.message.processed", spanAttrs, evt.durationMs);
|
||||
const span = spanWithDuration(
|
||||
"openclaw.message.processed",
|
||||
spanAttrs,
|
||||
evt.durationMs,
|
||||
evt.trace,
|
||||
);
|
||||
if (evt.outcome === "error" && evt.error) {
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: redactSensitiveText(evt.error) });
|
||||
}
|
||||
@@ -699,7 +729,11 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
addSessionIdentityAttrs(spanAttrs, evt);
|
||||
spanAttrs["openclaw.queueDepth"] = evt.queueDepth ?? 0;
|
||||
spanAttrs["openclaw.ageMs"] = evt.ageMs;
|
||||
const span = tracer.startSpan("openclaw.session.stuck", { attributes: spanAttrs });
|
||||
const span = tracer.startSpan(
|
||||
"openclaw.session.stuck",
|
||||
{ attributes: spanAttrs },
|
||||
contextForTraceContext(evt.trace),
|
||||
);
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: "session stuck" });
|
||||
span.end();
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ensureContextEnginesInitialized } from "../../context-engine/init.js";
|
||||
import { resolveContextEngine } from "../../context-engine/registry.js";
|
||||
import { emitAgentPlanEvent } from "../../infra/agent-events.js";
|
||||
import { sleepWithAbort } from "../../infra/backoff.js";
|
||||
import { freezeDiagnosticTraceContext } from "../../infra/diagnostic-trace-context.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { enqueueCommandInLane } from "../../process/command-queue.js";
|
||||
@@ -2134,6 +2135,9 @@ export async function runEmbeddedPiAgent(
|
||||
});
|
||||
return {
|
||||
payloads: payloadsWithToolMedia?.length ? payloadsWithToolMedia : undefined,
|
||||
...(attempt.diagnosticTrace
|
||||
? { diagnosticTrace: freezeDiagnosticTraceContext(attempt.diagnosticTrace) }
|
||||
: {}),
|
||||
meta: {
|
||||
durationMs: Date.now() - started,
|
||||
agentMeta,
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import {
|
||||
freezeDiagnosticTraceContext,
|
||||
type DiagnosticTraceContext,
|
||||
} from "../../../infra/diagnostic-trace-context.js";
|
||||
import type { EmbeddedRunTrigger } from "./params.js";
|
||||
|
||||
export function buildEmbeddedAttemptToolRunContext(params: {
|
||||
trigger?: EmbeddedRunTrigger;
|
||||
memoryFlushWritePath?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
}): {
|
||||
trigger?: EmbeddedRunTrigger;
|
||||
memoryFlushWritePath?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
} {
|
||||
return {
|
||||
trigger: params.trigger,
|
||||
memoryFlushWritePath: params.memoryFlushWritePath,
|
||||
...(params.trace ? { trace: freezeDiagnosticTraceContext(params.trace) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { filterHeartbeatPairs } from "../../../auto-reply/heartbeat-filter.js";
|
||||
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
|
||||
import {
|
||||
createDiagnosticTraceContext,
|
||||
freezeDiagnosticTraceContext,
|
||||
} from "../../../infra/diagnostic-trace-context.js";
|
||||
import { isEmbeddedMode } from "../../../infra/embedded-mode.js";
|
||||
import { formatErrorMessage } from "../../../infra/errors.js";
|
||||
import { resolveHeartbeatSummaryForAgent } from "../../../infra/heartbeat-summary.js";
|
||||
@@ -504,12 +508,13 @@ export async function runEmbeddedAttempt(
|
||||
const sessionLabel = params.sessionKey ?? params.sessionId;
|
||||
const contextInjectionMode = resolveContextInjectionMode(params.config);
|
||||
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
|
||||
const diagnosticTrace = freezeDiagnosticTraceContext(createDiagnosticTraceContext());
|
||||
const toolsRaw = params.disableTools
|
||||
? []
|
||||
: (() => {
|
||||
const allTools = createOpenClawCodingTools({
|
||||
agentId: sessionAgentId,
|
||||
...buildEmbeddedAttemptToolRunContext(params),
|
||||
...buildEmbeddedAttemptToolRunContext({ ...params, trace: diagnosticTrace }),
|
||||
exec: {
|
||||
...params.execOverrides,
|
||||
elevated: params.bashElevated,
|
||||
@@ -1942,6 +1947,7 @@ export async function runEmbeddedAttempt(
|
||||
}
|
||||
const hookCtx = {
|
||||
runId: params.runId,
|
||||
trace: freezeDiagnosticTraceContext(diagnosticTrace),
|
||||
agentId: hookAgentId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionId,
|
||||
@@ -2173,6 +2179,7 @@ export async function runEmbeddedAttempt(
|
||||
},
|
||||
{
|
||||
runId: params.runId,
|
||||
trace: freezeDiagnosticTraceContext(diagnosticTrace),
|
||||
agentId: hookAgentId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionId,
|
||||
@@ -2580,6 +2587,7 @@ export async function runEmbeddedAttempt(
|
||||
},
|
||||
{
|
||||
runId: params.runId,
|
||||
trace: freezeDiagnosticTraceContext(diagnosticTrace),
|
||||
agentId: hookAgentId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionId,
|
||||
@@ -2681,6 +2689,7 @@ export async function runEmbeddedAttempt(
|
||||
},
|
||||
{
|
||||
runId: params.runId,
|
||||
trace: freezeDiagnosticTraceContext(diagnosticTrace),
|
||||
agentId: hookAgentId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionId,
|
||||
@@ -2768,6 +2777,7 @@ export async function runEmbeddedAttempt(
|
||||
promptErrorSource,
|
||||
preflightRecovery,
|
||||
sessionIdUsed,
|
||||
diagnosticTrace,
|
||||
bootstrapPromptWarningSignaturesSeen: bootstrapPromptWarning.warningSignaturesSeen,
|
||||
bootstrapPromptWarningSignature: bootstrapPromptWarning.signature,
|
||||
systemPromptReport,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
import type { ThinkLevel } from "../../../auto-reply/thinking.js";
|
||||
import type { SessionSystemPromptReport } from "../../../config/sessions/types.js";
|
||||
import type { ContextEngine, ContextEnginePromptCacheInfo } from "../../../context-engine/types.js";
|
||||
import type { DiagnosticTraceContext } from "../../../infra/diagnostic-trace-context.js";
|
||||
import type { PluginHookBeforeAgentStartResult } from "../../../plugins/hook-before-agent-start.types.js";
|
||||
import type { MessagingToolSend } from "../../pi-embedded-messaging.types.js";
|
||||
import type { ToolErrorSummary } from "../../tool-error-summary.js";
|
||||
@@ -72,6 +73,7 @@ export type EmbeddedRunAttemptResult = {
|
||||
handled?: false;
|
||||
};
|
||||
sessionIdUsed: string;
|
||||
diagnosticTrace?: DiagnosticTraceContext;
|
||||
agentHarnessId?: string;
|
||||
bootstrapPromptWarningSignaturesSeen?: string[];
|
||||
bootstrapPromptWarningSignature?: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CliSessionBinding, SessionSystemPromptReport } from "../../config/sessions/types.js";
|
||||
import type { DiagnosticTraceContext } from "../../infra/diagnostic-trace-context.js";
|
||||
import type { MessagingToolSend } from "../pi-embedded-messaging.types.js";
|
||||
|
||||
export type EmbeddedPiAgentMeta = {
|
||||
@@ -141,6 +142,7 @@ export type EmbeddedPiRunResult = {
|
||||
audioAsVoice?: boolean;
|
||||
}>;
|
||||
meta: EmbeddedPiRunMeta;
|
||||
diagnosticTrace?: DiagnosticTraceContext;
|
||||
// True if a messaging tool successfully sent a message.
|
||||
// Used to suppress agent's confirmation text.
|
||||
didSendViaMessagingTool?: boolean;
|
||||
|
||||
@@ -425,6 +425,39 @@ describe("before_tool_call requireApproval handling", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes diagnostic trace context to before_tool_call hooks", async () => {
|
||||
const trace = {
|
||||
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
|
||||
spanId: "00f067aa0ba902b7",
|
||||
traceFlags: "01",
|
||||
};
|
||||
hookRunner.runBeforeToolCall.mockResolvedValue(undefined);
|
||||
|
||||
const result = await runBeforeToolCallHook({
|
||||
toolName: "bash",
|
||||
params: { command: "pwd" },
|
||||
toolCallId: "tool-1",
|
||||
ctx: { agentId: "main", sessionKey: "main", runId: "run-1", trace },
|
||||
});
|
||||
|
||||
expect(result.blocked).toBe(false);
|
||||
const call = hookRunner.runBeforeToolCall.mock.calls[0];
|
||||
expect(call?.[0]).toMatchObject({
|
||||
toolName: "exec",
|
||||
runId: "run-1",
|
||||
toolCallId: "tool-1",
|
||||
});
|
||||
const toolContext = call?.[1] as { trace?: typeof trace } | undefined;
|
||||
expect(toolContext).toMatchObject({
|
||||
toolName: "exec",
|
||||
runId: "run-1",
|
||||
toolCallId: "tool-1",
|
||||
trace,
|
||||
});
|
||||
expect(toolContext?.trace).not.toBe(trace);
|
||||
expect(Object.isFrozen(toolContext?.trace)).toBe(true);
|
||||
});
|
||||
|
||||
it("calls gateway RPC and unblocks on allow-once", async () => {
|
||||
hookRunner.runBeforeToolCall.mockResolvedValue({
|
||||
requireApproval: {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { ToolLoopDetectionConfig } from "../config/types.tools.js";
|
||||
import {
|
||||
freezeDiagnosticTraceContext,
|
||||
type DiagnosticTraceContext,
|
||||
} from "../infra/diagnostic-trace-context.js";
|
||||
import type { SessionState } from "../logging/diagnostic-session-state.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
||||
@@ -17,6 +21,7 @@ export type HookContext = {
|
||||
/** Ephemeral session UUID — regenerated on /new and /reset. */
|
||||
sessionId?: string;
|
||||
runId?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
loopDetection?: ToolLoopDetectionConfig;
|
||||
};
|
||||
|
||||
@@ -197,6 +202,7 @@ export async function runBeforeToolCallHook(args: {
|
||||
...(args.ctx?.sessionKey && { sessionKey: args.ctx.sessionKey }),
|
||||
...(args.ctx?.sessionId && { sessionId: args.ctx.sessionId }),
|
||||
...(args.ctx?.runId && { runId: args.ctx.runId }),
|
||||
...(args.ctx?.trace && { trace: freezeDiagnosticTraceContext(args.ctx.trace) }),
|
||||
...(args.toolCallId && { toolCallId: args.toolCallId }),
|
||||
};
|
||||
const hookResult = await hookRunner.runBeforeToolCall(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createCodingTools, createReadTool } from "@mariozechner/pi-coding-agent
|
||||
import type { ModelCompatConfig } from "../config/types.models.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { ToolLoopDetectionConfig } from "../config/types.tools.js";
|
||||
import type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js";
|
||||
import { resolveMergedSafeBinProfileFixtures } from "../infra/exec-safe-bin-runtime-policy.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
import { getPluginToolMeta } from "../plugins/tools.js";
|
||||
@@ -268,6 +269,8 @@ export function createOpenClawCodingTools(options?: {
|
||||
sessionId?: string;
|
||||
/** Stable run identifier for this agent invocation. */
|
||||
runId?: string;
|
||||
/** Diagnostic trace context for hook/log correlation during this run. */
|
||||
trace?: DiagnosticTraceContext;
|
||||
/** What initiated this run (for trigger-specific tool restrictions). */
|
||||
trigger?: string;
|
||||
/** Relative workspace path that memory-triggered writes may append to. */
|
||||
@@ -707,6 +710,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
sessionKey: options?.sessionKey,
|
||||
sessionId: options?.sessionId,
|
||||
runId: options?.runId,
|
||||
...(options?.trace ? { trace: options.trace } : {}),
|
||||
loopDetection: resolveToolLoopDetectionConfig({ cfg: options?.config, agentId }),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { TypingMode } from "../../config/types.js";
|
||||
import { resolveSessionTranscriptCandidates } from "../../gateway/session-utils.fs.js";
|
||||
import { emitAgentEvent } from "../../infra/agent-events.js";
|
||||
import { emitDiagnosticEvent, isDiagnosticsEnabled } from "../../infra/diagnostic-events.js";
|
||||
import { freezeDiagnosticTraceContext } from "../../infra/diagnostic-trace-context.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { CommandLaneClearedError, GatewayDrainingError } from "../../process/command-queue.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
@@ -1423,6 +1424,9 @@ export async function runReplyAgent(params: {
|
||||
const costUsd = estimateUsageCost({ usage, cost: costConfig });
|
||||
emitDiagnosticEvent({
|
||||
type: "model.usage",
|
||||
...(runResult.diagnosticTrace
|
||||
? { trace: freezeDiagnosticTraceContext(runResult.diagnosticTrace) }
|
||||
: {}),
|
||||
sessionKey,
|
||||
sessionId: followupRun.run.sessionId,
|
||||
channel: replyToChannel,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createChildDiagnosticTraceContext,
|
||||
createDiagnosticTraceContext,
|
||||
freezeDiagnosticTraceContext,
|
||||
formatDiagnosticTraceparent,
|
||||
isValidDiagnosticSpanId,
|
||||
isValidDiagnosticTraceFlags,
|
||||
@@ -113,4 +114,17 @@ describe("diagnostic-trace-context", () => {
|
||||
createChildDiagnosticTraceContext(parent, { spanId: SPAN_ID }).parentSpanId,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("freezes a defensive trace context copy", () => {
|
||||
const context = createDiagnosticTraceContext({
|
||||
traceId: TRACE_ID,
|
||||
spanId: SPAN_ID,
|
||||
traceFlags: "01",
|
||||
});
|
||||
const frozen = freezeDiagnosticTraceContext(context);
|
||||
|
||||
expect(frozen).toEqual(context);
|
||||
expect(frozen).not.toBe(context);
|
||||
expect(Object.isFrozen(frozen)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,13 +10,13 @@ const TRACEPARENT_VERSION_RE = /^[0-9a-f]{2}$/;
|
||||
|
||||
export type DiagnosticTraceContext = {
|
||||
/** W3C trace id, 32 lowercase hex chars. */
|
||||
traceId: string;
|
||||
readonly traceId: string;
|
||||
/** Current span id, 16 lowercase hex chars. */
|
||||
spanId?: string;
|
||||
readonly spanId?: string;
|
||||
/** Parent span id, 16 lowercase hex chars. */
|
||||
parentSpanId?: string;
|
||||
readonly parentSpanId?: string;
|
||||
/** W3C trace flags, 2 lowercase hex chars. Defaults to sampled. */
|
||||
traceFlags?: string;
|
||||
readonly traceFlags?: string;
|
||||
};
|
||||
|
||||
export type DiagnosticTraceContextInput = Partial<DiagnosticTraceContext> & {
|
||||
@@ -156,3 +156,14 @@ export function createChildDiagnosticTraceContext(
|
||||
traceFlags: input.traceFlags ?? parent.traceFlags,
|
||||
});
|
||||
}
|
||||
|
||||
export function freezeDiagnosticTraceContext(
|
||||
context: DiagnosticTraceContext,
|
||||
): DiagnosticTraceContext {
|
||||
return Object.freeze({
|
||||
traceId: context.traceId,
|
||||
...(context.spanId ? { spanId: context.spanId } : {}),
|
||||
...(context.parentSpanId ? { parentSpanId: context.parentSpanId } : {}),
|
||||
...(context.traceFlags ? { traceFlags: context.traceFlags } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ export type { ReplyPayload } from "./reply-payload.js";
|
||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export type { ContextEngineFactory } from "../context-engine/registry.js";
|
||||
export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js";
|
||||
export type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js";
|
||||
export type {
|
||||
AssembleResult,
|
||||
BootstrapResult,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
import type { FinalizedMsgContext } from "../auto-reply/templating.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { TtsAutoMode } from "../config/types.tts.js";
|
||||
import type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js";
|
||||
import {
|
||||
PLUGIN_PROMPT_MUTATION_RESULT_FIELDS,
|
||||
stripPromptMutationFieldsFromLegacyHookResult,
|
||||
@@ -153,6 +154,7 @@ export const isConversationHookName = (hookName: PluginHookName): boolean =>
|
||||
|
||||
export type PluginHookAgentContext = {
|
||||
runId?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
@@ -300,6 +302,7 @@ export type PluginHookToolContext = {
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
runId?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
toolName: string;
|
||||
toolCallId?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user