Files
pikasTech-HWLAB/internal/cloud/kafka-event-bridge.ts
T

1277 lines
64 KiB
TypeScript

// SPEC: PJ2026-0104010803 Workbench事件流可见性 draft-2026-07-09-p0-kafka-authority.
// Responsibility: subscribe AgentRun Kafka events, project them into HWLAB Kafka events, and provide bounded Kafka stream queries for diagnostics.
import { createHash, randomUUID } from "node:crypto";
import { Kafka, logLevel } from "kafkajs";
import { emitCodeAgentOtelSpan } from "./otel-trace.ts";
import { buildWorkbenchProjectionEventFacts } from "./workbench-projection-writer.ts";
import {
WORKBENCH_REALTIME_CAPABILITY_ENVS,
workbenchRealtimeCapabilities
} from "./workbench-realtime-capabilities.ts";
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
const DEFAULT_AGENTRUN_EVENT_TOPIC = "agentrun.event.v1";
const DEFAULT_HWLAB_EVENT_TOPIC = "hwlab.event.v1";
const DEFAULT_STDIO_TOPIC = "codex-stdio.raw.v1";
export const DEFAULT_AGENTRUN_DEBUG_EVENT_TOPIC = "agentrun.event.debug.v1";
export const DEFAULT_HWLAB_DEBUG_EVENT_TOPIC = "hwlab.event.debug.v1";
export const AGENTRUN_STDIO_RECONSTRUCTION_SCHEMA = "agentrun.event.reconstruction.v1";
export const AGENTRUN_STDIO_RECONSTRUCTION_KIND = "stdio-derived partial reconstruction";
const DEFAULT_CLIENT_ID = "hwlab-v03-cloud-api";
const DEFAULT_QUERY_TIMEOUT_MS = 5000;
const DEFAULT_QUERY_LIMIT = 50;
const LIVE_KAFKA_COMMAND_OUTPUT_OTEL_SAMPLE_MODULUS = 32;
const REQUIRED_KAFKA_ENV = Object.freeze([
"HWLAB_KAFKA_BOOTSTRAP_SERVERS",
"HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC",
"HWLAB_KAFKA_EVENT_TOPIC",
"HWLAB_KAFKA_CLIENT_ID"
]);
export function kafkaEventBridgeConfig(env = process.env) {
const featureEnvPresent = Object.values(WORKBENCH_REALTIME_CAPABILITY_ENVS).some((name) => stringValue(env[name]));
const legacyKafkaEnabled = truthy(env.HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED ?? env.HWLAB_KAFKA_AGENTRUN_CONSUME_ENABLED ?? env.HWLAB_KAFKA_ENABLED);
if (!featureEnvPresent && !legacyKafkaEnabled) return null;
const capabilities = workbenchRealtimeCapabilities(env);
const kafkaEnabled = capabilities.directPublish || capabilities.liveKafkaSse || capabilities.transactionalProjector || capabilities.projectionOutboxRelay;
const transactionalEnabled = capabilities.transactionalProjector || capabilities.projectionOutboxRelay || capabilities.projectionRealtime;
if (!kafkaEnabled && !transactionalEnabled) return null;
const required = [...REQUIRED_KAFKA_ENV];
if (!kafkaEnabled) required.length = 0;
if (capabilities.directPublish) required.push("HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID");
if (capabilities.liveKafkaSse) required.push("HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID");
if (capabilities.transactionalProjector) required.push("HWLAB_KAFKA_PROJECTOR_GROUP_ID", "HWLAB_KAFKA_PROJECTOR_HEARTBEAT_INTERVAL_MS");
if (capabilities.projectionOutboxRelay) required.push(
"HWLAB_KAFKA_OUTBOX_RELAY_INTERVAL_MS",
"HWLAB_KAFKA_OUTBOX_RELAY_BATCH_SIZE",
"HWLAB_KAFKA_OUTBOX_RELAY_LEASE_MS",
"HWLAB_KAFKA_OUTBOX_RELAY_SEND_TIMEOUT_MS",
"HWLAB_KAFKA_OUTBOX_RELAY_RETRY_BACKOFF_MS"
);
const missing = required.filter((name) => !stringValue(env[name]));
if (missing.length > 0) {
const error = new Error(`HWLAB Kafka capability configuration is incomplete: ${missing.join(", ")}`);
error.code = "hwlab_kafka_realtime_config_incomplete";
error.missing = missing;
throw error;
}
const brokers = csv(env.HWLAB_KAFKA_BOOTSTRAP_SERVERS);
const agentRunTopic = stringValue(env.HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC);
const hwlabTopic = stringValue(env.HWLAB_KAFKA_EVENT_TOPIC);
const clientId = stringValue(env.HWLAB_KAFKA_CLIENT_ID);
const relay = capabilities.projectionOutboxRelay ? {
intervalMs: strictPositiveInteger(env.HWLAB_KAFKA_OUTBOX_RELAY_INTERVAL_MS, "HWLAB_KAFKA_OUTBOX_RELAY_INTERVAL_MS"),
batchSize: strictPositiveInteger(env.HWLAB_KAFKA_OUTBOX_RELAY_BATCH_SIZE, "HWLAB_KAFKA_OUTBOX_RELAY_BATCH_SIZE"),
leaseMs: strictPositiveInteger(env.HWLAB_KAFKA_OUTBOX_RELAY_LEASE_MS, "HWLAB_KAFKA_OUTBOX_RELAY_LEASE_MS"),
sendTimeoutMs: strictPositiveInteger(env.HWLAB_KAFKA_OUTBOX_RELAY_SEND_TIMEOUT_MS, "HWLAB_KAFKA_OUTBOX_RELAY_SEND_TIMEOUT_MS"),
retryBackoffMs: strictPositiveInteger(env.HWLAB_KAFKA_OUTBOX_RELAY_RETRY_BACKOFF_MS, "HWLAB_KAFKA_OUTBOX_RELAY_RETRY_BACKOFF_MS")
} : null;
if (relay && relay.sendTimeoutMs + 5000 >= relay.leaseMs) {
const error = new Error("HWLAB Kafka relay lease must exceed producer send timeout by at least 5000ms.");
error.code = "hwlab_kafka_relay_lease_invalid";
throw error;
}
const enabledConsumerGroups = [
capabilities.directPublish ? stringValue(env.HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID) : null,
capabilities.transactionalProjector ? stringValue(env.HWLAB_KAFKA_PROJECTOR_GROUP_ID) : null,
capabilities.liveKafkaSse ? stringValue(env.HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID) : null
].filter(Boolean);
if (new Set(enabledConsumerGroups).size !== enabledConsumerGroups.length) {
const error = new Error("HWLAB Kafka direct publisher, projector, and live SSE consumer groups must be distinct when enabled together.");
error.code = "hwlab_kafka_consumer_groups_not_distinct";
error.groupIds = enabledConsumerGroups;
throw error;
}
return {
capabilities,
brokers,
agentRunTopic,
hwlabTopic,
clientId,
directPublishGroupId: stringValue(env.HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID),
projectorGroupId: stringValue(env.HWLAB_KAFKA_PROJECTOR_GROUP_ID),
hwlabEventGroupId: stringValue(env.HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID),
projectorHeartbeatIntervalMs: capabilities.transactionalProjector ? strictPositiveInteger(env.HWLAB_KAFKA_PROJECTOR_HEARTBEAT_INTERVAL_MS, "HWLAB_KAFKA_PROJECTOR_HEARTBEAT_INTERVAL_MS") : null,
relay
};
}
export function startHwlabKafkaEventBridge({ env = process.env, logger = console, kafkaFactory = defaultKafkaFactory, runtimeStore = null, now = () => new Date().toISOString(), otelSpanEmitter = emitCodeAgentOtelSpan } = {}) {
const config = kafkaEventBridgeConfig(env);
if (!config) return { started: false, reason: "disabled_or_unconfigured", stop() {}, subscribeProjectionCommits() { return () => {}; }, valuesPrinted: false };
const components = [];
if (config.capabilities.directPublish || config.capabilities.liveKafkaSse) {
components.push(startLiveHwlabKafkaEventBridge({ config, env, logger, kafkaFactory, otelSpanEmitter }));
}
if (config.capabilities.transactionalProjector || config.capabilities.projectionOutboxRelay || config.capabilities.projectionRealtime) {
components.push(startTransactionalHwlabKafkaEventBridge({ config, logger, kafkaFactory, runtimeStore, now }));
}
return components.length === 1 ? components[0] : combineKafkaEventBridgeComponents(config, components);
}
function startTransactionalHwlabKafkaEventBridge({ config, logger = console, kafkaFactory = defaultKafkaFactory, runtimeStore = null, now = () => new Date().toISOString() } = {}) {
requireKafkaProjectorStore(runtimeStore, config.capabilities);
let stopped = false;
let consumer = null;
let producer = null;
let relayTimer = null;
let relayRunning = false;
let stopProjectionNotifications = null;
const projectionCommitListeners = new Set();
const publishProjectionCommit = (signal = {}) => {
for (const listener of [...projectionCommitListeners]) {
try { listener({ sessionId: signal.sessionId ?? null, traceId: signal.traceId ?? null, outboxSeq: signal.outboxSeq ?? null, recovery: signal.recovery === true, valuesRedacted: true }); } catch {}
}
};
const relayOwner = `${config.clientId}:outbox-relay:${randomUUID()}`;
let startupError = null;
let startupReady = false;
const ready = (async () => {
await runtimeStore.assertWorkbenchTransactionalRealtimeReady();
const kafka = config.capabilities.transactionalProjector || config.capabilities.projectionOutboxRelay ? kafkaFactory(config) : null;
if (config.capabilities.transactionalProjector) consumer = kafka.consumer({ groupId: config.projectorGroupId, allowAutoTopicCreation: false });
if (config.capabilities.projectionOutboxRelay) producer = kafka.producer({ allowAutoTopicCreation: false });
if (producer) await producer.connect();
if (consumer) await consumer.connect();
if (config.capabilities.projectionRealtime && typeof runtimeStore.subscribeWorkbenchProjectionCommits === "function") {
stopProjectionNotifications = await runtimeStore.subscribeWorkbenchProjectionCommits(publishProjectionCommit);
}
if (consumer) await consumer.subscribe({ topic: config.agentRunTopic, fromBeginning: false });
if (consumer) await consumer.run({
autoCommit: false,
eachBatchAutoResolve: false,
eachBatch: async ({ batch, resolveOffset, heartbeat, isRunning, isStale }) => {
for (const message of batch.messages) {
if (stopped || !isRunning() || isStale()) break;
await heartbeat();
const heartbeatLoop = startKafkaHeartbeatLoop(heartbeat, config.projectorHeartbeatIntervalMs);
let projection;
try {
projection = await projectAgentRunKafkaMessage({ topic: batch.topic, partition: batch.partition, message }, { runtimeStore, config, logger });
} finally {
await heartbeatLoop.stop();
}
if (projection?.projected) publishProjectionCommit(projection);
if (heartbeatLoop.error) throw heartbeatLoop.error;
if (stopped || !isRunning() || isStale()) {
logWarn(logger, "hwlab-kafka-offset-commit-skipped", { topic: batch.topic, partition: batch.partition, offset: message.offset, reason: isStale() ? "stale_generation" : "consumer_stopped", valuesPrinted: false });
break;
}
resolveOffset(message.offset);
await consumer.commitOffsets([{ topic: batch.topic, partition: batch.partition, offset: nextKafkaOffset(message.offset) }]);
await heartbeat();
}
}
});
const runRelay = async () => {
if (!config.capabilities.projectionOutboxRelay) return;
if (stopped || relayRunning) return;
relayRunning = true;
try {
await relayHwlabKafkaOutboxOnce({ runtimeStore, producer, config, owner: relayOwner, now, logger });
} finally {
relayRunning = false;
}
};
if (config.capabilities.projectionOutboxRelay) {
relayTimer = setInterval(() => { void runRelay().catch((error) => logWarn(logger, "hwlab-kafka-outbox-relay-failed", { message: errorMessage(error), valuesPrinted: false })); }, config.relay.intervalMs);
relayTimer.unref?.();
await runRelay();
}
startupReady = true;
logInfo(logger, "hwlab-kafka-event-bridge-started", {
agentRunTopic: config.agentRunTopic,
hwlabTopic: config.hwlabTopic,
clientId: config.clientId,
projectorGroupId: config.projectorGroupId,
capabilities: config.capabilities,
valuesPrinted: false
});
})();
void ready.catch((error) => {
startupError = error;
logWarn(logger, "hwlab-kafka-event-bridge-start-failed", {
message: errorMessage(error),
agentRunTopic: config.agentRunTopic,
hwlabTopic: config.hwlabTopic,
valuesPrinted: false
});
});
return {
started: true,
...config,
ready,
async stop() {
stopped = true;
if (relayTimer) clearInterval(relayTimer);
if (typeof stopProjectionNotifications === "function") {
try { await stopProjectionNotifications(); } catch {}
}
if (consumer?.stop) await consumer.stop().catch(() => undefined);
await Promise.allSettled([consumer?.disconnect?.(), producer?.disconnect?.()]);
},
async status() {
if (startupError) return { status: "blocked", errorCode: startupError.code ?? "hwlab_kafka_projector_start_failed", message: errorMessage(startupError), valuesRedacted: true };
if (!startupReady) return { status: "initializing", valuesRedacted: true };
const transactionalReadiness = typeof runtimeStore.workbenchTransactionalRealtimeReadiness === "function"
? await runtimeStore.workbenchTransactionalRealtimeReadiness()
: { ready: true, valuesRedacted: true };
const projectorStatus = config.capabilities.transactionalProjector || config.capabilities.projectionOutboxRelay
? await runtimeStore.hwlabKafkaProjectorStatus()
: {};
return { status: transactionalReadiness.ready === false ? "blocked" : "ready", capabilities: config.capabilities, transactionalReadiness, ...projectorStatus, valuesRedacted: true };
},
startupStatus() {
if (startupError) return { status: "blocked", errorCode: startupError.code ?? "hwlab_kafka_projector_start_failed", message: errorMessage(startupError), valuesRedacted: true };
return { status: startupReady ? "ready" : "initializing", valuesRedacted: true };
},
subscribeProjectionCommits(listener) {
if (typeof listener !== "function") return () => {};
projectionCommitListeners.add(listener);
return () => projectionCommitListeners.delete(listener);
},
valuesPrinted: false
};
}
function startLiveHwlabKafkaEventBridge({ config, env = process.env, logger = console, kafkaFactory = defaultKafkaFactory, otelSpanEmitter = emitCodeAgentOtelSpan } = {}) {
let stopped = false;
let bridgeConsumer = null;
let fanoutConsumer = null;
let producer = null;
let startupError = null;
let startupReady = false;
const subscribers = new Set();
const lagByPartition = new Map();
const publishLiveEnvelope = (envelope, transport) => {
for (const listener of [...subscribers]) {
try {
listener(envelope, transport);
} catch (error) {
logWarn(logger, "hwlab-live-kafka-subscriber-failed", { message: errorMessage(error), valuesPrinted: false });
}
}
};
const ready = (async () => {
const kafka = kafkaFactory(config);
if (config.capabilities.directPublish) {
bridgeConsumer = kafka.consumer({ groupId: config.directPublishGroupId, allowAutoTopicCreation: false });
producer = kafka.producer({ allowAutoTopicCreation: false });
}
if (config.capabilities.liveKafkaSse) fanoutConsumer = kafka.consumer({ groupId: config.hwlabEventGroupId, allowAutoTopicCreation: false });
if (producer) await producer.connect();
if (bridgeConsumer) await bridgeConsumer.connect();
if (fanoutConsumer) await fanoutConsumer.connect();
if (bridgeConsumer) await bridgeConsumer.subscribe({ topic: config.agentRunTopic, fromBeginning: false });
if (fanoutConsumer) await fanoutConsumer.subscribe({ topic: config.hwlabTopic, fromBeginning: false });
if (fanoutConsumer) await fanoutConsumer.run({
eachBatchAutoResolve: false,
eachBatch: async ({ batch, resolveOffset, heartbeat, isRunning, isStale }) => {
let lastOffset = null;
for (const message of batch.messages) {
if (stopped || !isRunning() || isStale()) break;
const envelope = parseJson(message?.value ? Buffer.from(message.value).toString("utf8") : "");
if (!envelope || envelope.schema !== "hwlab.event.v1") {
logWarn(logger, "hwlab-live-kafka-envelope-ignored", { reason: "schema-invalid", valuesPrinted: false });
} else {
const transport = { topic: batch.topic, partition: batch.partition, offset: message.offset };
emitLiveKafkaOtelSpan("hwlab.kafka.live.fanout_receive", envelope, transport, { env, otelSpanEmitter });
publishLiveEnvelope(envelope, transport);
}
resolveOffset(message.offset);
lastOffset = message.offset;
await heartbeat();
}
if (lastOffset !== null) lagByPartition.set(batch.partition, kafkaPartitionLag(batch.highWatermark, lastOffset));
}
});
if (bridgeConsumer) await bridgeConsumer.run({
eachMessage: async ({ topic, partition, message }) => {
if (stopped) return;
await publishAgentRunKafkaMessageLive({ topic, partition, message }, { producer, config, env, logger, otelSpanEmitter });
}
});
startupReady = true;
logInfo(logger, "hwlab-live-kafka-event-bridge-started", {
capabilities: config.capabilities,
agentRunTopic: config.agentRunTopic,
hwlabTopic: config.hwlabTopic,
clientId: config.clientId,
directPublishGroupId: config.directPublishGroupId,
hwlabEventGroupId: config.hwlabEventGroupId,
valuesPrinted: false
});
})();
void ready.catch((error) => {
startupError = error;
logWarn(logger, "hwlab-live-kafka-event-bridge-start-failed", {
message: errorMessage(error),
agentRunTopic: config.agentRunTopic,
hwlabTopic: config.hwlabTopic,
valuesPrinted: false
});
});
return {
started: true,
...config,
ready,
async stop() {
stopped = true;
subscribers.clear();
await Promise.allSettled([bridgeConsumer?.stop?.(), fanoutConsumer?.stop?.()]);
await Promise.allSettled([bridgeConsumer?.disconnect?.(), fanoutConsumer?.disconnect?.(), producer?.disconnect?.()]);
},
async status() {
if (startupError) return { status: "blocked", capabilities: config.capabilities, errorCode: startupError.code ?? "hwlab_live_kafka_start_failed", message: errorMessage(startupError), valuesRedacted: true };
return { status: startupReady ? "ready" : "initializing", capabilities: config.capabilities, subscriberCount: subscribers.size, consumerLag: liveKafkaLagSummary(lagByPartition), lossPossible: true, valuesRedacted: true };
},
startupStatus() {
if (startupError) return { status: "blocked", capabilities: config.capabilities, errorCode: startupError.code ?? "hwlab_live_kafka_start_failed", message: errorMessage(startupError), valuesRedacted: true };
return { status: startupReady ? "ready" : "initializing", capabilities: config.capabilities, valuesRedacted: true };
},
subscribeProjectionCommits() { return () => {}; },
subscribeLiveHwlabEvents(listener) {
if (typeof listener !== "function") return () => {};
subscribers.add(listener);
return () => subscribers.delete(listener);
},
liveSubscriberCount() { return subscribers.size; },
valuesPrinted: false
};
}
function kafkaPartitionLag(highWatermark, lastOffset) {
try {
const lag = BigInt(String(highWatermark ?? "0")) - (BigInt(String(lastOffset ?? "0")) + 1n);
return Number(lag > 0n ? lag : 0n);
} catch {
return null;
}
}
function liveKafkaLagSummary(lagByPartition) {
const partitions = [...lagByPartition.entries()].map(([partition, lag]) => ({ partition, lag })).sort((left, right) => left.partition - right.partition);
const known = partitions.map((item) => item.lag).filter((lag) => Number.isFinite(lag));
return {
known: known.length > 0,
total: known.length > 0 ? known.reduce((sum, lag) => sum + lag, 0) : null,
partitions,
valuesRedacted: true
};
}
function combineKafkaEventBridgeComponents(config, components) {
const ready = Promise.all(components.map((component) => component.ready));
const componentStartup = () => components.map((component) => component.startupStatus?.() ?? { status: "initializing" });
return {
started: true,
...config,
ready,
async stop() { await Promise.allSettled(components.map((component) => component.stop())); },
async status() {
const statuses = await Promise.all(components.map((component) => component.status()));
const blocked = statuses.find((status) => status.status === "blocked");
const initializing = statuses.some((status) => status.status !== "ready");
return {
status: blocked ? "blocked" : initializing ? "initializing" : "ready",
capabilities: config.capabilities,
components: statuses,
errorCode: blocked?.errorCode ?? null,
message: blocked?.message ?? null,
valuesRedacted: true
};
},
startupStatus() {
const statuses = componentStartup();
const blocked = statuses.find((status) => status.status === "blocked");
return {
status: blocked ? "blocked" : statuses.every((status) => status.status === "ready") ? "ready" : "initializing",
capabilities: config.capabilities,
components: statuses,
errorCode: blocked?.errorCode ?? null,
message: blocked?.message ?? null,
valuesRedacted: true
};
},
subscribeProjectionCommits(listener) {
const stops = components.map((component) => component.subscribeProjectionCommits?.(listener)).filter((stop) => typeof stop === "function");
return () => stops.forEach((stop) => stop());
},
subscribeLiveHwlabEvents(listener) {
const owner = components.find((component) => typeof component.subscribeLiveHwlabEvents === "function");
return owner?.subscribeLiveHwlabEvents(listener) ?? (() => {});
},
liveSubscriberCount() {
const owner = components.find((component) => typeof component.liveSubscriberCount === "function");
return owner?.liveSubscriberCount() ?? 0;
},
valuesPrinted: false
};
}
export async function publishAgentRunKafkaMessageLive(kafkaMessage = {}, { producer, config, env = process.env, logger = console, otelSpanEmitter = emitCodeAgentOtelSpan } = {}) {
const transport = kafkaTransport(kafkaMessage);
const decoded = decodeCanonicalAgentRunKafkaMessage(kafkaMessage);
if (!decoded.ok) {
logWarn(logger, "hwlab-live-kafka-message-invalid", { topic: transport.sourceTopic, partition: transport.sourcePartition, offset: transport.sourceOffset, errorCode: decoded.error.code, valuesPrinted: false });
return { handled: true, invalid: true, published: false, valuesPrinted: false };
}
if (!stringValue(decoded.event.traceId) || !stringValue(decoded.event.hwlabSessionId)) {
return { handled: true, ignored: true, published: false, valuesPrinted: false };
}
const projected = projectAgentRunKafkaEventToHwlabEvent(decoded.event, {
source: config.clientId,
sourceTopic: transport.sourceTopic,
sourcePartition: transport.sourcePartition,
sourceOffset: transport.sourceOffset,
sourceKey: transport.sourceKey,
inputSha256: transport.inputSha256
});
const publishMetadata = await producer.send({ topic: config.hwlabTopic, messages: [buildHwlabKafkaProducerMessage(projected)] });
const publishedRecord = Array.isArray(publishMetadata) ? publishMetadata[0] : null;
emitLiveKafkaOtelSpan("hwlab.kafka.live.direct_publish", projected, {
topic: config.hwlabTopic,
partition: publishedRecord?.partition,
offset: publishedRecord?.baseOffset ?? publishedRecord?.offset,
sourceTopic: transport.sourceTopic,
sourcePartition: transport.sourcePartition,
sourceOffset: transport.sourceOffset
}, { env, otelSpanEmitter });
return { handled: true, published: true, envelope: projected, sessionId: projected.sessionId, traceId: projected.traceId, valuesPrinted: false };
}
export function shouldEmitLiveKafkaOtelSpan(envelope = {}) {
const event = objectValue(envelope.event);
const context = objectValue(envelope.context);
const eventType = firstText(context.agentRunEventType, event.eventType, event.type, envelope.eventType)?.toLowerCase();
if (event.terminal === true || eventType === "terminal" || eventType === "terminal_status") return true;
if (["assistant", "assistant_message", "tool", "tool_call"].includes(eventType)) return true;
if (eventType !== "command_output" && event.type !== "output") return true;
const sourceSeq = integerValue(context.sourceSeq ?? event.sourceSeq);
if (Number.isInteger(sourceSeq) && sourceSeq > 0) return sourceSeq % LIVE_KAFKA_COMMAND_OUTPUT_OTEL_SAMPLE_MODULUS === 0;
const identity = firstText(envelope.sourceEventId, envelope.eventId, envelope.traceId, envelope.hwlabSessionId, envelope.sessionId);
if (!identity) return false;
return createHash("sha256").update(identity).digest().readUInt32BE(0) % LIVE_KAFKA_COMMAND_OUTPUT_OTEL_SAMPLE_MODULUS === 0;
}
export function liveKafkaOtelSpanAttributes(envelope = {}, transport = {}) {
const event = objectValue(envelope.event);
const context = objectValue(envelope.context);
const sourceEvent = objectValue(envelope.sourceEvent);
return {
businessTraceId: firstText(envelope.traceId, event.traceId),
hwlabSessionId: firstText(envelope.hwlabSessionId, envelope.sessionId, event.sessionId),
runId: firstText(envelope.runId, context.runId, event.runId),
commandId: firstText(envelope.commandId, context.commandId, event.commandId),
topic: firstText(transport.topic, sourceEvent.topic),
partition: integerValue(transport.partition ?? sourceEvent.partition),
offset: firstText(transport.offset, sourceEvent.offset),
sourceTopic: firstText(transport.sourceTopic),
sourcePartition: integerValue(transport.sourcePartition),
sourceOffset: firstText(transport.sourceOffset),
eventType: firstText(context.agentRunEventType, event.eventType, event.type, envelope.eventType),
terminal: event.terminal === true,
valuesRedacted: true
};
}
export function emitLiveKafkaOtelSpan(name, envelope = {}, transport = {}, { env = process.env, otelSpanEmitter = emitCodeAgentOtelSpan } = {}) {
if (typeof otelSpanEmitter !== "function" || !shouldEmitLiveKafkaOtelSpan(envelope)) return { emitted: false, valuesRedacted: true };
const attributes = liveKafkaOtelSpanAttributes(envelope, transport);
if (!attributes.businessTraceId) return { emitted: false, valuesRedacted: true };
try {
const pending = otelSpanEmitter(name, attributes.businessTraceId, env, { attributes });
void Promise.resolve(pending).catch(() => undefined);
} catch {
// OTel is best effort and must never change the live Kafka delivery path.
}
return { emitted: true, valuesRedacted: true };
}
export async function projectAgentRunKafkaMessage(kafkaMessage = {}, { runtimeStore, config, logger = console } = {}) {
const transport = kafkaTransport(kafkaMessage);
const decoded = decodeCanonicalAgentRunKafkaMessage(kafkaMessage);
if (!decoded.ok) {
await runtimeStore.recordFailedAgentRunKafkaMessage({ transport, sourceEventId: decoded.sourceEventId, envelope: decoded.envelope, error: decoded.error });
logWarn(logger, "hwlab-kafka-message-dlq", { topic: transport.sourceTopic, partition: transport.sourcePartition, offset: transport.sourceOffset, errorCode: decoded.error.code, valuesPrinted: false });
return { handled: true, invalid: true, valuesPrinted: false };
}
if (!stringValue(decoded.event.traceId) || !stringValue(decoded.event.hwlabSessionId)) {
const ignored = await runtimeStore.recordIgnoredAgentRunKafkaMessage({ transport, envelope: decoded.event, reason: "hwlab-correlation-absent" });
return { handled: true, ignored: true, duplicate: ignored?.duplicate === true, valuesPrinted: false };
}
const projected = projectAgentRunKafkaEventToHwlabEvent(decoded.event, {
source: config.clientId,
sourceTopic: transport.sourceTopic,
sourcePartition: transport.sourcePartition,
sourceOffset: transport.sourceOffset,
sourceKey: transport.sourceKey,
inputSha256: transport.inputSha256
});
const result = await runtimeStore.commitAgentRunKafkaProjection({
transport,
canonicalEvent: decoded.event,
projectedEvent: projected.event,
requestMeta: { traceId: projected.traceId, sessionId: projected.sessionId, agentSessionId: projected.sessionId, valuesPrinted: false },
factsFactory: ({ projectedSeq, previousCheckpoint, projectedAt }) => buildWorkbenchProjectionEventFacts({ event: projected.event, requestMeta: { traceId: projected.traceId, sessionId: projected.sessionId }, previousCheckpoint, projectedSeq, projectedAt }),
hwlabEvent: projected,
hwlabTopic: config.hwlabTopic,
partitionKey: kafkaMessageKey(projected),
headers: kafkaHeaders(projected)
});
if (result.conflict) logWarn(logger, "hwlab-kafka-message-conflict", { topic: transport.sourceTopic, partition: transport.sourcePartition, offset: transport.sourceOffset, errorCode: result.diagnostic?.code, valuesPrinted: false });
return {
handled: true,
projected: !result.duplicate && !result.conflict && result.projectionWritten !== false && result.suppressedAfterSeal !== true,
duplicate: result.duplicate === true,
conflict: result.conflict === true,
suppressedAfterSeal: result.suppressedAfterSeal === true,
projectedSeq: result.projectedSeq,
sessionId: projected.sessionId,
traceId: projected.traceId,
valuesPrinted: false
};
}
export async function relayHwlabKafkaOutboxOnce({ runtimeStore, producer, config, owner, now = () => new Date().toISOString(), logger = console } = {}) {
let claimedCount = 0;
let deliveredCount = 0;
let retryCount = 0;
for (let index = 0; index < config.relay.batchSize; index += 1) {
const item = (await runtimeStore.claimHwlabKafkaOutbox({ owner, leaseMs: config.relay.leaseMs, limit: 1 }))[0];
if (!item) break;
claimedCount += 1;
try {
await producer.send({ topic: item.topic, timeout: config.relay.sendTimeoutMs, messages: [{ key: item.partitionKey, value: JSON.stringify(item.payload), headers: item.headers }] });
await runtimeStore.completeHwlabKafkaOutbox(item);
deliveredCount += 1;
} catch (error) {
const retryAt = new Date(Date.parse(now()) + config.relay.retryBackoffMs).toISOString();
await runtimeStore.retryHwlabKafkaOutbox(item, error, retryAt);
retryCount += 1;
logWarn(logger, "hwlab-kafka-outbox-publish-retry", { outboxSeq: item.outboxSeq, eventId: item.eventId, retryAt, message: errorMessage(error), valuesPrinted: false });
break;
}
}
return { claimedCount, deliveredCount, retryCount, valuesPrinted: false };
}
export function startKafkaHeartbeatLoop(heartbeat, intervalMs) {
let stopped = false;
let running = null;
let heartbeatError = null;
const pulse = () => {
if (stopped || running || typeof heartbeat !== "function") return;
running = Promise.resolve()
.then(() => heartbeat())
.catch((error) => { heartbeatError ??= error; })
.finally(() => { running = null; });
};
pulse();
const timer = setInterval(pulse, intervalMs);
timer.unref?.();
return {
get error() { return heartbeatError; },
async stop() {
stopped = true;
clearInterval(timer);
if (running) await running;
}
};
}
export function projectAgentRunKafkaMessageToHwlabEvent(kafkaMessage = {}, options = {}) {
const valueText = kafkaMessage?.message?.value ? Buffer.from(kafkaMessage.message.value).toString("utf8") : "";
const input = parseJson(valueText);
if (!input) return null;
return projectAgentRunKafkaEventToHwlabEvent(input, {
...options,
sourceTopic: kafkaMessage.topic,
sourcePartition: kafkaMessage.partition,
sourceOffset: kafkaMessage.message?.offset,
sourceKey: kafkaMessage.message?.key ? Buffer.from(kafkaMessage.message.key).toString("utf8") : null,
inputSha256: sha256(valueText)
});
}
export function projectAgentRunKafkaEventToHwlabEvent(input = {}, options = {}) {
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
const run = objectValue(input.run ?? input.agentRun?.run);
const command = objectValue(input.command ?? input.agentRun?.command);
const sourceEvent = objectValue(input.event ?? input.payload?.event ?? input.agentRunEvent);
const payload = objectValue(sourceEvent.payload ?? input.payload);
const hwlab = objectValue(input.hwlab ?? payload.hwlab);
const context = objectValue(input.context ?? payload.context);
const sourceSeq = integerValue(sourceEvent.seq ?? input.seq ?? input.sourceSeq);
const traceId = firstText(input.traceId, hwlab.traceId, context.traceId, sourceEvent.traceId, payload.traceId, payload.hwlabTraceId, payload.businessTraceId, payload.metadata?.traceId, command.traceId, run.traceId);
const sourceEventId = firstText(input.eventId, sourceEvent.id);
const sessionId = firstText(input.hwlabSessionId, payload.hwlabSessionId, hwlab.sessionId, context.hwlabSessionId, input.sessionId, context.sessionId, run.hwlabSessionId, run.sessionId, payload.sessionId);
const runId = firstText(run.runId, sourceEvent.runId, payload.runId, input.runId);
const commandId = firstText(command.commandId, sourceEvent.commandId, payload.commandId, input.commandId);
const event = mapAgentRunSourceEventToHwlabEvent({ input, sourceEvent, payload, run, command, traceId, sessionId, sourceSeq, runId, commandId });
const producedAt = timestampValue(options.producedAt) || new Date().toISOString();
return {
schema: "hwlab.event.v1",
eventType: "hwlab.trace.event.projected",
eventId: sourceEventId ? `hwlab:${sourceEventId}` : null,
sourceEventId,
source: stringValue(options.source) || DEFAULT_CLIENT_ID,
producedAt,
traceId,
hwlabSessionId: sessionId,
sessionId,
runId,
commandId,
context: {
conversationId: firstText(input.conversationId, hwlab.conversationId, context.conversationId, run.conversationId, payload.conversationId),
threadId: firstText(input.threadId, hwlab.threadId, context.threadId, run.threadId, payload.threadId),
projectId: firstText(input.projectId, hwlab.projectId, context.projectId, run.projectId, payload.projectId),
runId,
commandId,
sourceSeq,
sourceEventType: firstText(input.eventType, sourceEvent.eventType),
agentRunEventType: firstText(sourceEvent.type, payload.type),
valuesRedacted: true
},
event: { ...event, sourceEventId },
sourceEvent: {
schema: firstText(input.schema),
topic: stringValue(options.sourceTopic),
partition: integerValue(options.sourcePartition),
offset: stringValue(options.sourceOffset),
key: stringValue(options.sourceKey),
sha256: stringValue(options.inputSha256) || sha256(JSON.stringify(input)),
valuesRedacted: true
},
diagnostics: {
projectedBy: "hwlab-kafka-event-bridge",
valuesPrinted: false
},
valuesPrinted: false
};
}
export function decodeCanonicalAgentRunKafkaMessage(kafkaMessage = {}) {
const valueText = kafkaMessage?.message?.value ? Buffer.from(kafkaMessage.message.value).toString("utf8") : "";
const envelope = parseJson(valueText);
const sourceEventId = firstText(envelope?.eventId, envelope?.event?.id);
try {
assertCanonicalAgentRunEvent(envelope);
return { ok: true, event: envelope, sourceEventId, inputSha256: sha256(valueText), valuesPrinted: false };
} catch (error) {
return { ok: false, envelope, sourceEventId, inputSha256: sha256(valueText), error: { code: error?.code ?? "workbench_kafka_message_invalid", message: errorMessage(error), valuesRedacted: true }, valuesPrinted: false };
}
}
export function decodeAgentRunDebugProjectionMessage(kafkaMessage = {}) {
const valueText = kafkaMessage?.message?.value ? Buffer.from(kafkaMessage.message.value).toString("utf8") : "";
const envelope = parseJson(valueText);
if (envelope?.schema === "agentrun.event.v1") {
const decoded = decodeCanonicalAgentRunKafkaMessage(kafkaMessage);
return decoded.ok
? { ...decoded, inputKind: "canonical-durable", reconstruction: null }
: { ...decoded, inputKind: "canonical-invalid", reconstruction: null };
}
try {
assertAgentRunStdioReconstruction(envelope);
return {
ok: true,
event: envelope,
sourceEventId: null,
inputSha256: sha256(valueText),
inputKind: AGENTRUN_STDIO_RECONSTRUCTION_KIND,
reconstruction: envelope.reconstruction,
valuesPrinted: false
};
} catch (error) {
return {
ok: false,
envelope,
sourceEventId: null,
inputSha256: sha256(valueText),
inputKind: "reconstruction-invalid",
reconstruction: envelope?.reconstruction ?? null,
error: {
code: error?.code ?? "workbench_kafka_debug_message_invalid",
message: errorMessage(error),
valuesRedacted: true
},
valuesPrinted: false
};
}
}
export function projectAgentRunKafkaMessageToHwlabDebugEvent(kafkaMessage = {}, options = {}) {
const decoded = decodeAgentRunDebugProjectionMessage(kafkaMessage);
if (!decoded.ok) return decoded;
const transport = kafkaTransport(kafkaMessage);
const projected = projectAgentRunKafkaEventToHwlabEvent(decoded.event, {
source: stringValue(options.source) || "hwlab-cli-kafka-debug",
producedAt: options.producedAt,
sourceTopic: transport.sourceTopic,
sourcePartition: transport.sourcePartition,
sourceOffset: transport.sourceOffset,
sourceKey: transport.sourceKey,
inputSha256: transport.inputSha256
});
if (!stringValue(projected?.traceId) || (!decoded.reconstruction && !stringValue(projected?.sessionId))) {
return {
ok: false,
inputKind: decoded.inputKind,
error: {
code: "hwlab_kafka_debug_correlation_missing",
message: decoded.reconstruction
? "AgentRun reconstruction debug projection requires traceId lineage."
: "Canonical AgentRun debug projection requires traceId and sessionId lineage.",
valuesRedacted: true
},
valuesPrinted: false
};
}
const debugSessionId = decoded.reconstruction ? stringValue(decoded.event.hwlabSessionId) : stringValue(projected.sessionId);
const debugEnvelope = {
...projected,
schema: DEFAULT_HWLAB_DEBUG_EVENT_TOPIC,
eventType: "hwlab.trace.event.debug-projected",
sessionId: debugSessionId,
hwlabSessionId: debugSessionId,
event: decoded.reconstruction ? { ...projected.event, sessionId: debugSessionId } : projected.event,
outboxSeq: null,
...(decoded.reconstruction ? { eventId: null, sourceEventId: null } : {}),
diagnostics: {
projectedBy: "hwlab-kafka-event-bridge",
projectionMode: "source-direct-debug",
inputKind: decoded.inputKind,
valuesPrinted: false
},
debugLineage: {
inputSchema: firstText(decoded.event.schema),
inputEventType: firstText(decoded.event.eventType),
inputEventId: firstText(decoded.event.eventId),
inputOutboxSeq: integerValue(decoded.event.outboxSeq),
inputTraceId: firstText(decoded.event.traceId, decoded.event.event?.payload?.traceId),
inputSessionId: firstText(decoded.event.sessionId, decoded.event.run?.sessionId),
inputHwlabSessionId: firstText(decoded.event.hwlabSessionId, decoded.event.run?.hwlabSessionId),
inputRunId: firstText(decoded.event.runId, decoded.event.run?.runId),
inputCommandId: firstText(decoded.event.commandId, decoded.event.command?.commandId),
sourceSeq: integerValue(decoded.event.sourceSeq ?? decoded.event.event?.seq),
valuesPrinted: false
},
...(decoded.reconstruction ? {
reconstruction: {
...decoded.reconstruction,
observedAt: timestampValue(decoded.event.observedAt),
inputSchema: decoded.event.schema,
inputEventType: decoded.event.eventType,
valuesPrinted: false
}
} : {}),
valuesPrinted: false
};
return {
ok: true,
inputKind: decoded.inputKind,
envelope: debugEnvelope,
sourceSeq: integerValue(decoded.event.sourceSeq ?? decoded.event.event?.seq),
traceId: debugEnvelope.traceId,
sessionId: debugEnvelope.sessionId,
valuesPrinted: false
};
}
function assertAgentRunStdioReconstruction(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw contractError("hwlab_kafka_debug_json_invalid", "AgentRun reconstruction value must be a JSON object.");
if (value.schema !== AGENTRUN_STDIO_RECONSTRUCTION_SCHEMA || value.eventType !== "agentrun.event.reconstructed") {
throw contractError("hwlab_kafka_debug_schema_invalid", "AgentRun debug input must be a canonical committed event or a stdio reconstruction envelope.");
}
if (value.eventId !== null || value.outboxSeq !== null) {
throw contractError("hwlab_kafka_debug_identity_invalid", "Stdio reconstruction must keep eventId and outboxSeq null.");
}
for (const [name, field] of [["traceId", value.traceId], ["sessionId", value.sessionId], ["runId", value.runId]]) {
if (!stringValue(field)) throw contractError("hwlab_kafka_debug_lineage_invalid", `Stdio reconstruction ${name} is required.`);
}
if (!Number.isInteger(Number(value.sourceSeq)) || Number(value.sourceSeq) <= 0) {
throw contractError("hwlab_kafka_debug_sequence_invalid", "Stdio reconstruction sourceSeq must be a positive integer.");
}
if (!value.event || typeof value.event !== "object" || Array.isArray(value.event) || value.event.id !== null || Number(value.event.seq) !== Number(value.sourceSeq)) {
throw contractError("hwlab_kafka_debug_identity_invalid", "Stdio reconstruction event identity must remain null and its seq must match sourceSeq.");
}
if (!value.event.payload || typeof value.event.payload !== "object" || Array.isArray(value.event.payload)) {
throw contractError("hwlab_kafka_debug_schema_invalid", "Stdio reconstruction event payload must be an object.");
}
if (value.event.payload.reconstructed !== true || value.event.payload.reconstructionKind !== AGENTRUN_STDIO_RECONSTRUCTION_KIND) {
throw contractError("hwlab_kafka_debug_lineage_invalid", "Stdio reconstruction payload must disclose its partial reconstruction kind.");
}
const reconstruction = value.reconstruction;
if (!reconstruction || typeof reconstruction !== "object" || Array.isArray(reconstruction)
|| reconstruction.kind !== AGENTRUN_STDIO_RECONSTRUCTION_KIND
|| reconstruction.completeLifecycle !== false
|| reconstruction.originalEventId !== null
|| reconstruction.originalOutboxSeq !== null) {
throw contractError("hwlab_kafka_debug_lineage_invalid", "Stdio reconstruction lineage is incomplete or claims durable identity.");
}
assertReconstructionIdentityDerivation(value, reconstruction.identityDerivation);
if (value.valuesPrinted !== false || containsSecretLikeKey(value)) {
throw contractError("hwlab_kafka_debug_redaction_invalid", "Stdio reconstruction must be redacted and declare valuesPrinted=false.");
}
return value;
}
function assertReconstructionIdentityDerivation(value, derivation) {
if (!derivation || typeof derivation !== "object" || Array.isArray(derivation) || derivation.valuesPrinted !== false
|| derivation.sourceField !== "sessionId" || derivation.targetField !== "hwlabSessionId") {
throw contractError("hwlab_kafka_debug_identity_derivation_invalid", "Stdio reconstruction must disclose the session identity derivation contract.");
}
const rawSessionId = stringValue(value.sessionId);
const match = rawSessionId?.match(/^ses_agentrun_([0-9a-fA-F]{8})_([0-9a-fA-F]{4})_([0-9a-fA-F]{4})_([0-9a-fA-F]{4})_([0-9a-fA-F]{12})$/u);
if (match) {
const expectedHwlabSessionId = `ses_${match.slice(1).join("-").toLowerCase()}`;
if (value.hwlabSessionId !== expectedHwlabSessionId || value.run?.hwlabSessionId !== expectedHwlabSessionId
|| derivation.kind !== "agentrun-session-uuid-to-hwlab-session" || derivation.reversible !== true
|| derivation.sourcePrefix !== "ses_agentrun_" || derivation.targetPrefix !== "ses_"
|| derivation.separatorTransform !== "underscore-to-hyphen") {
throw contractError("hwlab_kafka_debug_identity_derivation_invalid", "Derived hwlabSessionId does not match the disclosed reversible AgentRun session identity transform.");
}
return;
}
if (value.hwlabSessionId !== null || value.run?.hwlabSessionId !== null
|| derivation.kind !== "none" || derivation.reversible !== false
|| derivation.reason !== "source sessionId is not ses_agentrun_<uuid-with-underscores>") {
throw contractError("hwlab_kafka_debug_identity_derivation_invalid", "Non-reversible AgentRun session identity must keep hwlabSessionId null and disclose why.");
}
}
function assertCanonicalAgentRunEvent(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw contractError("workbench_kafka_json_invalid", "Kafka value must be a JSON object.");
assertClosedObject(value, ["schema", "eventType", "source", "eventId", "outboxSeq", "sourceSeq", "committedAt", "traceId", "sessionId", "hwlabSessionId", "runId", "commandId", "run", "command", "event", "valuesPrinted"], "AgentRun envelope");
if (value.schema !== "agentrun.event.v1" || value.eventType !== "agentrun.event.committed") throw contractError("workbench_kafka_schema_invalid", "Kafka value is not a canonical AgentRun committed event.");
for (const [name, field] of [["eventId", value.eventId], ["runId", value.runId]]) {
if (!stringValue(field)) throw contractError("workbench_kafka_schema_invalid", `Canonical AgentRun ${name} is required.`);
}
for (const [name, field] of [["traceId", value.traceId], ["hwlabSessionId", value.hwlabSessionId]]) {
if (field !== null && field !== undefined && !stringValue(field)) throw contractError("workbench_kafka_schema_invalid", `Canonical AgentRun ${name} must be a string or null.`);
}
if (!Number.isInteger(Number(value.sourceSeq)) || Number(value.sourceSeq) <= 0) throw contractError("workbench_kafka_schema_invalid", "Canonical AgentRun sourceSeq must be a positive integer.");
if (value.valuesPrinted !== false) throw contractError("workbench_kafka_redaction_invalid", "Canonical AgentRun event must declare valuesPrinted=false.");
assertClosedObject(value.run, ["runId", "status", "terminalStatus", "failureKind", "tenantId", "projectId", "providerId", "backendProfile", "sessionId", "hwlabSessionId", "conversationId", "threadId", "valuesPrinted"], "AgentRun run");
if (value.command !== null && value.command !== undefined) assertClosedObject(value.command, ["commandId", "runId", "seq", "type", "state", "payloadHash", "valuesPrinted"], "AgentRun command");
assertClosedObject(value.event, ["id", "runId", "seq", "type", "payload", "createdAt"], "AgentRun event");
if (value.event.id !== value.eventId || Number(value.event.seq) !== Number(value.sourceSeq)) throw contractError("workbench_kafka_identity_invalid", "Canonical AgentRun event identity does not match the envelope.");
if (!value.event.payload || typeof value.event.payload !== "object" || Array.isArray(value.event.payload)) throw contractError("workbench_kafka_schema_invalid", "Canonical AgentRun event payload must be an object.");
if (containsSecretLikeKey(value)) throw contractError("workbench_kafka_redaction_invalid", "Canonical AgentRun event contains a secret-like field.");
return value;
}
function assertClosedObject(value, allowed, label) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw contractError("workbench_kafka_schema_invalid", `${label} must be an object.`);
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
if (unknown.length > 0) throw contractError("workbench_kafka_schema_invalid", `${label} contains unsupported fields: ${unknown.join(", ")}`);
}
function containsSecretLikeKey(value, depth = 0) {
if (depth > 12 || value === null || value === undefined) return false;
if (Array.isArray(value)) return value.some((item) => containsSecretLikeKey(item, depth + 1));
if (typeof value !== "object") return false;
return Object.entries(value).some(([key, entry]) => {
const normalized = key.replace(/[^a-z0-9]/giu, "").toLowerCase();
if (/^(apikey|accesstoken|refreshtoken|password|authorization|credentialvalue|secretvalue|privatekey|clientsecret|bearertoken)$/u.test(normalized)) return !redactedSecretMarker(entry);
if (typeof entry === "string" && /^(?:https?|postgres(?:ql)?):\/\/[^/@\s]+:[^/@\s]+@/iu.test(entry)) return true;
return containsSecretLikeKey(entry, depth + 1);
});
}
function redactedSecretMarker(value) {
if (value === null || value === undefined) return true;
const normalized = stringValue(value)?.toLowerCase();
return normalized === "redacted" || normalized === "[redacted]" || normalized === "present" || normalized === "missing";
}
function kafkaTransport(kafkaMessage = {}) {
const valueText = kafkaMessage?.message?.value ? Buffer.from(kafkaMessage.message.value).toString("utf8") : "";
return {
sourceTopic: stringValue(kafkaMessage.topic),
sourcePartition: integerValue(kafkaMessage.partition),
sourceOffset: stringValue(kafkaMessage.message?.offset),
sourceKey: kafkaMessage.message?.key ? Buffer.from(kafkaMessage.message.key).toString("utf8") : null,
inputSha256: stringValue(kafkaMessage.inputSha256) || sha256(valueText),
valuesPrinted: false
};
}
function requireKafkaProjectorStore(runtimeStore, capabilities = {}) {
const required = [];
if (capabilities.transactionalProjector) required.push("commitAgentRunKafkaProjection", "recordFailedAgentRunKafkaMessage", "recordIgnoredAgentRunKafkaMessage");
if (capabilities.projectionOutboxRelay) required.push("claimHwlabKafkaOutbox", "completeHwlabKafkaOutbox", "retryHwlabKafkaOutbox");
if (capabilities.projectionRealtime) required.push("subscribeWorkbenchProjectionCommits");
if (capabilities.transactionalProjector || capabilities.projectionOutboxRelay) required.push("hwlabKafkaProjectorStatus");
if (capabilities.transactionalProjector || capabilities.projectionOutboxRelay || capabilities.projectionRealtime) required.push("assertWorkbenchTransactionalRealtimeReady");
const missing = required.filter((name) => typeof runtimeStore?.[name] !== "function");
if (missing.length > 0) throw contractError("hwlab_kafka_projector_store_invalid", `Kafka durable capabilities require a runtime store: ${missing.join(", ")}`);
}
export async function queryKafkaEventStream({ env = process.env, stream = "hwlab", topic = null, traceId = null, sessionId = null, runId = null, commandId = null, limit = DEFAULT_QUERY_LIMIT, timeoutMs = DEFAULT_QUERY_TIMEOUT_MS, fromBeginning = true, groupIdPrefix = null, kafkaFactory = defaultKafkaFactory } = {}) {
const brokers = csv(env.HWLAB_KAFKA_BOOTSTRAP_SERVERS);
if (brokers.length === 0) throw new Error("HWLAB_KAFKA_BOOTSTRAP_SERVERS is required for Kafka event queries.");
const clientId = stringValue(env.HWLAB_KAFKA_CLIENT_ID) || DEFAULT_CLIENT_ID;
const resolvedTopic = stringValue(topic) || kafkaTopicForStream(stream, env);
const maxEvents = Math.max(1, integerValue(limit) || DEFAULT_QUERY_LIMIT);
const budgetMs = Math.max(250, integerValue(timeoutMs) || DEFAULT_QUERY_TIMEOUT_MS);
const kafka = kafkaFactory({ brokers, clientId: `${clientId}-query` });
const resolvedGroupIdPrefix = stringValue(groupIdPrefix) || `${clientId}-query`;
const groupId = `${resolvedGroupIdPrefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
const consumer = kafka.consumer({ groupId, allowAutoTopicCreation: false });
const events = [];
let timer = null;
let running = false;
await consumer.connect();
await consumer.subscribe({ topic: resolvedTopic, fromBeginning: fromBeginning !== false });
try {
await new Promise((resolve, reject) => {
let finished = false;
const finish = (value = null) => {
if (finished) return;
finished = true;
resolve(value);
};
timer = setTimeout(finish, budgetMs);
timer.unref?.();
running = true;
consumer.run({
eachMessage: async ({ topic: messageTopic, partition, message }) => {
const valueText = message.value ? Buffer.from(message.value).toString("utf8") : "";
const value = parseJson(valueText);
if (!value || !eventMatchesFilters(value, { traceId, sessionId, runId, commandId })) return;
events.push({
topic: messageTopic,
partition,
offset: message.offset,
key: message.key ? Buffer.from(message.key).toString("utf8") : null,
timestamp: message.timestamp ?? null,
valueSha256: sha256(valueText),
value
});
if (events.length >= maxEvents) finish();
}
}).catch(reject);
});
} finally {
if (timer) clearTimeout(timer);
if (running) await consumer.stop().catch(() => undefined);
await boundedDisconnect(consumer);
}
return {
ok: true,
stream,
topic: resolvedTopic,
groupId,
count: events.length,
limit: maxEvents,
timeoutMs: budgetMs,
filters: compactObject({ traceId, sessionId, runId, commandId }),
events,
valuesPrinted: false
};
}
export async function openKafkaEventStream({ env = process.env, stream = "hwlab", topic = null, traceId = null, sessionId = null, runId = null, commandId = null, fromBeginning = false, onEvent, onError = null, kafkaFactory = defaultKafkaFactory } = {}) {
if (typeof onEvent !== "function") throw new Error("onEvent callback is required for Kafka event streaming.");
const brokers = csv(env.HWLAB_KAFKA_BOOTSTRAP_SERVERS);
if (brokers.length === 0) throw new Error("HWLAB_KAFKA_BOOTSTRAP_SERVERS is required for Kafka event streams.");
const clientId = stringValue(env.HWLAB_KAFKA_CLIENT_ID) || DEFAULT_CLIENT_ID;
const resolvedTopic = stringValue(topic) || kafkaTopicForStream(stream, env);
const filters = compactObject({ traceId, sessionId, runId, commandId });
const kafka = kafkaFactory({ brokers, clientId: `${clientId}-debug-sse` });
const groupId = `${clientId}-debug-sse-${Date.now()}-${randomUUID().slice(0, 8)}`;
const consumer = kafka.consumer({ groupId, allowAutoTopicCreation: false });
let running = false;
let stopped = false;
await consumer.connect();
await consumer.subscribe({ topic: resolvedTopic, fromBeginning: fromBeginning === true });
running = true;
try {
await consumer.run({
eachMessage: async ({ topic: messageTopic, partition, message }) => {
if (stopped) return;
const valueText = message.value ? Buffer.from(message.value).toString("utf8") : "";
const value = parseJson(valueText);
if (!value || !eventMatchesFilters(value, filters)) return;
await onEvent({
topic: messageTopic,
partition,
offset: message.offset,
key: message.key ? Buffer.from(message.key).toString("utf8") : null,
timestamp: message.timestamp ?? null,
value
});
}
});
} catch (error) {
if (typeof onError === "function") onError(error);
stopped = true;
if (running) await consumer.stop().catch(() => undefined);
await boundedDisconnect(consumer);
throw error;
}
return {
ok: true,
stream,
topic: resolvedTopic,
groupId,
filters,
async stop() {
stopped = true;
if (running) await consumer.stop().catch(() => undefined);
await boundedDisconnect(consumer);
},
valuesPrinted: false
};
}
function mapAgentRunSourceEventToHwlabEvent({ input, sourceEvent, payload, run, command, traceId, sessionId, sourceSeq, runId, commandId }) {
const base = {
traceId,
sessionId,
source: "agentrun.kafka",
sourceSeq,
runId,
commandId,
attemptId: firstText(command.attemptId, sourceEvent.attemptId, payload.attemptId),
runnerId: firstText(command.runnerId, sourceEvent.runnerId, payload.runnerId),
backend: firstText(run.backendProfile, payload.backendProfile, payload.providerProfile),
createdAt: timestampValue(sourceEvent.createdAt ?? input.producedAt),
valuesPrinted: false
};
const type = firstText(sourceEvent.type, payload.type, input.eventType) || "event";
if (type === "assistant_message") {
const authoritative = payload.replyAuthority === true || payload.final === true;
const assistantText = textPayload(payload, "assistant message");
return {
...base,
type: "assistant",
eventType: "assistant",
status: "running",
label: "agentrun:assistant:message",
message: assistantText,
text: assistantText,
assistantText,
finalResponse: authoritative ? { text: assistantText, status: "completed", traceId, source: "agentrun.kafka", valuesPrinted: false } : null,
itemId: firstText(payload.itemId),
replyAuthority: payload.replyAuthority === true,
final: payload.final === true,
terminal: false
};
}
if (type === "backend_status") {
const phase = firstText(payload.phase) || "status";
return { ...base, type: "backend", eventType: "backend", status: "running", label: `agentrun:backend:${phase}`, message: textPayload(payload, phase) };
}
if (type === "terminal_status") {
const terminalStatus = firstText(payload.terminalStatus, sourceEvent.terminalStatus, payload.status, sourceEvent.status) || "failed";
const normalized = terminalStatus === "cancelled" ? "canceled" : terminalStatus;
return { ...base, type: "result", eventType: "terminal", status: normalized, label: `agentrun:terminal:${terminalStatus}`, errorCode: firstText(payload.failureKind, payload.errorCode, sourceEvent.failureKind, sourceEvent.errorCode), failureKind: firstText(payload.failureKind, sourceEvent.failureKind), message: textPayload({ ...sourceEvent, ...payload }, `AgentRun terminal status ${terminalStatus}`), terminal: true };
}
if (type === "tool_call") {
const toolName = firstText(payload.toolName, payload.name, payload.method) || "tool";
return {
...base,
type: "tool",
eventType: "tool",
status: firstText(payload.status) || "running",
label: `agentrun:tool:${toolName}`,
message: textPayload(payload, `AgentRun tool ${toolName}`),
toolName,
method: firstText(payload.method),
itemId: firstText(payload.itemId),
exitCode: integerValue(payload.exitCode),
durationMs: integerValue(payload.durationMs),
outputSummary: firstText(payload.outputSummary, payload.summary?.text),
terminal: false
};
}
if (type === "command_output") {
const stream = payload.stream === "stderr" ? "stderr" : "stdout";
return { ...base, type: "output", eventType: "status", status: "running", label: `agentrun:output:${stream}`, stream, message: textPayload(payload, ""), outputTruncated: payload.outputTruncated === true };
}
if (type === "error") {
const failureKind = firstText(payload.failureKind, payload.errorCode, sourceEvent.failureKind, sourceEvent.errorCode) || "backend";
return { ...base, type: "error", eventType: "error", status: "failed", label: `agentrun:error:${failureKind}`, errorCode: failureKind, failureKind, message: textPayload(payload, "AgentRun error"), terminal: false };
}
return { ...base, type: "backend", eventType: "backend", status: "running", label: `agentrun:event:${type}`, message: textPayload(payload, firstText(input.eventType, type) || "AgentRun event") };
}
function eventMatchesFilters(value, filters) {
for (const [name, expected] of Object.entries(filters)) {
const needle = stringValue(expected);
if (!needle) continue;
if (!eventFieldCandidates(value, name).includes(needle)) return false;
}
return true;
}
function eventFieldCandidates(value, name) {
const event = objectValue(value.event);
const context = objectValue(value.context);
const run = objectValue(value.run);
const command = objectValue(value.command);
const payload = objectValue(value.payload);
const metadata = objectValue(value.metadata ?? value.meta);
const trace = objectValue(value.trace);
const ids = objectValue(value.ids);
const stdio = objectValue(value.stdio ?? value.frame ?? value.codexStdio);
const sourceEvent = objectValue(value.sourceEvent);
const nestedEvent = objectValue(value.event?.sourceEvent ?? value.agentRunEvent ?? payload.event);
const nestedPayload = objectValue(nestedEvent.payload);
if (name === "traceId") return textCandidates(value.traceId, value.trace_id, event.traceId, event.trace_id, context.traceId, context.trace_id, sourceEvent.traceId, sourceEvent.trace_id, nestedEvent.traceId, nestedEvent.trace_id, nestedPayload.traceId, nestedPayload.trace_id, payload.traceId, payload.trace_id, metadata.traceId, metadata.trace_id, trace.traceId, trace.trace_id, ids.traceId, ids.trace_id, stdio.traceId, stdio.trace_id);
if (name === "sessionId") return textCandidates(value.sessionId, value.session_id, event.sessionId, event.session_id, context.sessionId, context.session_id, run.sessionId, run.session_id, nestedPayload.sessionId, nestedPayload.session_id, payload.sessionId, payload.session_id, metadata.sessionId, metadata.session_id, ids.sessionId, ids.session_id, stdio.sessionId, stdio.session_id);
if (name === "runId") return textCandidates(value.runId, value.run_id, event.runId, event.run_id, context.runId, context.run_id, run.runId, run.run_id, nestedEvent.runId, nestedEvent.run_id, nestedPayload.runId, nestedPayload.run_id, payload.runId, payload.run_id, metadata.runId, metadata.run_id, ids.runId, ids.run_id, stdio.runId, stdio.run_id);
if (name === "commandId") return textCandidates(value.commandId, value.command_id, event.commandId, event.command_id, context.commandId, context.command_id, command.commandId, command.command_id, nestedEvent.commandId, nestedEvent.command_id, nestedPayload.commandId, nestedPayload.command_id, payload.commandId, payload.command_id, metadata.commandId, metadata.command_id, ids.commandId, ids.command_id, stdio.commandId, stdio.command_id);
return [];
}
function kafkaTopicForStream(stream, env) {
const normalized = stringValue(stream) || "hwlab";
if (normalized === "stdio" || normalized === "codex-stdio") return stringValue(env.HWLAB_KAFKA_STDIO_TOPIC ?? env.AGENTRUN_KAFKA_STDIO_TOPIC) || DEFAULT_STDIO_TOPIC;
if (normalized === "agentrun") return stringValue(env.HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC) || DEFAULT_AGENTRUN_EVENT_TOPIC;
if (normalized === "hwlab") return stringValue(env.HWLAB_KAFKA_EVENT_TOPIC) || DEFAULT_HWLAB_EVENT_TOPIC;
return normalized;
}
function defaultKafkaFactory(config) {
return new Kafka({ clientId: config.clientId, brokers: config.brokers, logLevel: logLevel.NOTHING });
}
async function boundedDisconnect(consumer, timeoutMs = 2000) {
let timer = null;
try {
await Promise.race([
consumer.disconnect(),
new Promise((resolve) => {
timer = setTimeout(resolve, timeoutMs);
timer.unref?.();
})
]);
} finally {
if (timer) clearTimeout(timer);
}
}
export function buildHwlabKafkaProducerMessage(projected) {
return {
key: kafkaMessageKey(projected),
value: JSON.stringify(projected),
headers: kafkaHeaders(projected)
};
}
function kafkaMessageKey(projected) {
return firstText(projected.sessionId, projected.traceId, projected.context?.commandId, projected.context?.runId) || "hwlab-event";
}
function kafkaHeaders(projected) {
return {
"x-trace-id": projected.traceId ?? "",
"x-session-id": projected.sessionId ?? "",
"x-run-id": projected.context?.runId ?? "",
"x-command-id": projected.context?.commandId ?? "",
"x-values-printed": "false"
};
}
function textPayload(payload, fallback) {
return firstText(payload.text, payload.message, payload.summary, payload.output, payload.errorMessage, payload.reason) || fallback;
}
function truthy(value) {
return TRUE_VALUES.has(String(value ?? "").trim().toLowerCase());
}
function csv(value) {
return String(value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
}
function parseJson(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}
function objectValue(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function firstText(...values) {
for (const value of values) {
const text = stringValue(value);
if (text) return text;
}
return null;
}
function textCandidates(...values) {
return values.map(stringValue).filter(Boolean);
}
function stringValue(value) {
const text = typeof value === "string" ? value.trim() : value === null || value === undefined ? "" : String(value).trim();
return text.length > 0 ? text : null;
}
function integerValue(value) {
if (value === null || value === undefined || (typeof value === "string" && value.trim() === "")) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.floor(parsed) : null;
}
function nextKafkaOffset(value) {
try {
return (BigInt(String(value)) + 1n).toString();
} catch {
throw contractError("workbench_kafka_offset_invalid", "Kafka source offset must be an integer string.");
}
}
function strictPositiveInteger(value, name) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) throw contractError("hwlab_kafka_projector_config_invalid", `${name} must be a positive integer.`);
return parsed;
}
function contractError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
function timestampValue(value) {
const text = stringValue(value);
if (!text) return null;
const ms = Date.parse(text);
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
}
function sha256(text) {
return createHash("sha256").update(String(text ?? "")).digest("hex");
}
function compactObject(value) {
return Object.fromEntries(Object.entries(value).filter(([, entry]) => stringValue(entry)));
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function logInfo(logger, code, details) {
logger?.log?.(JSON.stringify({ code, component: "hwlab-kafka-event-bridge", ...details, valuesPrinted: false }));
}
function logWarn(logger, code, details) {
logger?.warn?.(JSON.stringify({ code, component: "hwlab-kafka-event-bridge", ...details, valuesPrinted: false }));
}