coze_lab 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -511,6 +511,31 @@ const cozeloopTracePlugin = {
511
511
  enabledHooks: pluginConfig.enabledHooks,
512
512
  };
513
513
  const exporter = new CozeloopExporter(api, config);
514
+ // per-agent trace 放行:traceAgentIds 为 onboard 写入的 allowlist(小写归一)。
515
+ // 非空时,仅 allowlist 内的 agentId 上报 trace;为空 / 未配置则全部放行(本地全局模式)。
516
+ const traceAgentIds = Array.isArray(pluginConfig.traceAgentIds)
517
+ ? pluginConfig.traceAgentIds.map((s) => String(s).trim().toLowerCase()).filter(Boolean)
518
+ : [];
519
+ const isAgentAllowed = (hookCtx) => {
520
+ if (traceAgentIds.length === 0)
521
+ return true; // 空 allowlist = 全部放行
522
+ const agentId = resolveAgentIdFromHookCtx(hookCtx || {});
523
+ const allowed = traceAgentIds.includes(agentId);
524
+ if (!allowed && config.debug) {
525
+ api.logger.info(`[CozeloopTrace] agent "${agentId}" not in traceAgentIds, skip trace`);
526
+ }
527
+ return allowed;
528
+ };
529
+ // 包裹 api.on:带 hookCtx 的 hook 在 agentId 不在 allowlist 时直接跳过,
530
+ // 不进入任何上报逻辑。gateway_start/stop 等无 hookCtx 的生命周期 hook 不拦截。
531
+ const rawOn = api.on.bind(api);
532
+ const on = (name, handler) => {
533
+ rawOn(name, async (event, hookCtx) => {
534
+ if (hookCtx !== undefined && !isAgentAllowed(hookCtx))
535
+ return;
536
+ return handler(event, hookCtx);
537
+ });
538
+ };
514
539
  const contextByChannelId = new Map();
515
540
  const contextByRunId = new Map();
516
541
  const shouldHookEnabled = (hookName) => {
@@ -787,6 +812,14 @@ const cozeloopTracePlugin = {
787
812
  });
788
813
  if (shouldHookEnabled("gateway_start")) {
789
814
  api.on("gateway_start", async (event) => {
815
+ // gateway_start 是进程级事件、无 agent 归属。云端 per-agent 模式
816
+ // (traceAgentIds 非空)下不上报这条无归属 span,避免越过 allowlist。
817
+ if (traceAgentIds.length > 0) {
818
+ if (config.debug) {
819
+ api.logger.info("[CozeloopTrace] traceAgentIds set, skip gateway_start span");
820
+ }
821
+ return;
822
+ }
790
823
  const now = Date.now();
791
824
  const { ctx, channelId } = getOrCreateContext("system/gateway", undefined, "gateway_start");
792
825
  const span = createSpan(ctx, channelId, "gateway_start", "gateway", now, now, {
@@ -800,7 +833,7 @@ const cozeloopTracePlugin = {
800
833
  });
801
834
  }
802
835
  if (shouldHookEnabled("session_start")) {
803
- api.on("session_start", async (event, hookCtx) => {
836
+ on("session_start", async (event, hookCtx) => {
804
837
  // Refresh token if expiring soon (< 10 min)
805
838
  await exporter.refreshAuthIfNeeded();
806
839
  const rawChannelId = resolveChannelId(hookCtx, event.sessionId);
@@ -817,7 +850,7 @@ const cozeloopTracePlugin = {
817
850
  });
818
851
  }
819
852
  if (shouldHookEnabled("message_received")) {
820
- api.on("message_received", async (event, hookCtx) => {
853
+ on("message_received", async (event, hookCtx) => {
821
854
  const rawChannelId = resolveChannelId(hookCtx, event.from || event.metadata?.senderId);
822
855
  if (config.debug) {
823
856
  api.logger.info(`[CozeloopTrace] message_received hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, event.from=${event.from}`);
@@ -858,7 +891,7 @@ const cozeloopTracePlugin = {
858
891
  });
859
892
  }
860
893
  if (shouldHookEnabled("message_sending")) {
861
- api.on("message_sending", async (event, hookCtx) => {
894
+ on("message_sending", async (event, hookCtx) => {
862
895
  if (lastUserTraceContext) {
863
896
  lastUserTraceContext.lastOutput = event.content;
864
897
  if (config.debug) {
@@ -876,7 +909,7 @@ const cozeloopTracePlugin = {
876
909
  });
877
910
  }
878
911
  if (shouldHookEnabled("message_sent")) {
879
- api.on("message_sent", async (event, hookCtx) => {
912
+ on("message_sent", async (event, hookCtx) => {
880
913
  if (event.content && event.success) {
881
914
  if (lastUserTraceContext) {
882
915
  lastUserTraceContext.lastOutput = event.content;
@@ -899,7 +932,7 @@ const cozeloopTracePlugin = {
899
932
  let lastLlmStartTime = undefined;
900
933
  let lastLlmSpanId = undefined;
901
934
  if (shouldHookEnabled("llm_input")) {
902
- api.on("llm_input", async (event, hookCtx) => {
935
+ on("llm_input", async (event, hookCtx) => {
903
936
  const rawChannelId = resolveChannelId(hookCtx);
904
937
  if (config.debug) {
905
938
  api.logger.info(`[CozeloopTrace] llm_input hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, event.runId=${event.runId}`);
@@ -990,7 +1023,7 @@ const cozeloopTracePlugin = {
990
1023
  });
991
1024
  }
992
1025
  if (shouldHookEnabled("llm_output")) {
993
- api.on("llm_output", async (event, hookCtx) => {
1026
+ on("llm_output", async (event, hookCtx) => {
994
1027
  const rawChannelId = resolveChannelId(hookCtx);
995
1028
  if (config.debug) {
996
1029
  api.logger.info(`[CozeloopTrace][DEBUG] llm_output event.usage=${JSON.stringify(event.usage)}`);
@@ -1086,7 +1119,7 @@ const cozeloopTracePlugin = {
1086
1119
  });
1087
1120
  }
1088
1121
  if (shouldHookEnabled("before_tool_call")) {
1089
- api.on("before_tool_call", async (event, hookCtx) => {
1122
+ on("before_tool_call", async (event, hookCtx) => {
1090
1123
  const rawChannelId = resolveChannelId(hookCtx);
1091
1124
  if (config.debug) {
1092
1125
  api.logger.info(`[CozeloopTrace] before_tool_call hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, toolName=${event.toolName}`);
@@ -1107,7 +1140,7 @@ const cozeloopTracePlugin = {
1107
1140
  });
1108
1141
  }
1109
1142
  if (shouldHookEnabled("after_tool_call")) {
1110
- api.on("after_tool_call", async (event, hookCtx) => {
1143
+ on("after_tool_call", async (event, hookCtx) => {
1111
1144
  if (config.debug) {
1112
1145
  api.logger.info(`[CozeloopTrace] after_tool_call hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, toolName=${event.toolName}`);
1113
1146
  }
@@ -1194,10 +1227,32 @@ const cozeloopTracePlugin = {
1194
1227
  traceFinalized = false;
1195
1228
  }, 200);
1196
1229
  };
1230
+ // OpenClaw runtime 周期性发送的心跳轮询消息,不是真实对话,整条 trace 丢弃。
1231
+ const HEARTBEAT_INPUT = "[OpenClaw heartbeat poll]";
1232
+ const isHeartbeatInput = (input) => {
1233
+ if (typeof input === "string") {
1234
+ return input.trim() === HEARTBEAT_INPUT;
1235
+ }
1236
+ // content 可能是 [{type:'text', text:'...'}] 形式
1237
+ if (Array.isArray(input)) {
1238
+ return input.some((p) => p && typeof p === "object"
1239
+ && typeof p.text === "string" && p.text.trim() === HEARTBEAT_INPUT);
1240
+ }
1241
+ return false;
1242
+ };
1197
1243
  // Helper: ensure root openclaw_request span is started for a given context.
1198
1244
  // Must be called before creating the agent span so that the exporter's
1199
1245
  // currentRootContext is set and the agent span becomes a proper child.
1200
1246
  const ensureRootSpan = async (ctx, channelId) => {
1247
+ // 心跳轮询消息:不是真实对话,整条 trace 不上报(沿用下方“无 coze-context 即 return”的同款范式)。
1248
+ const heartbeatInput = ctx.userInput
1249
+ || lastUserTraceContext?.userInput || lastUserInput;
1250
+ if (isHeartbeatInput(heartbeatInput)) {
1251
+ if (config.debug) {
1252
+ api.logger.info(`[CozeloopTrace] skip heartbeat poll trace, traceId=${ctx.traceId}`);
1253
+ }
1254
+ return;
1255
+ }
1201
1256
  // Resolve coze-context with SESSION-LEVEL inheritance: only the first
1202
1257
  // user message of a Coze session carries the <coze-context> block, so
1203
1258
  // /loop follow-ups and later turns have none. Fall back to the per-
@@ -1295,7 +1350,7 @@ const cozeloopTracePlugin = {
1295
1350
  }
1296
1351
  };
1297
1352
  if (shouldHookEnabled("before_agent_start")) {
1298
- api.on("before_agent_start", async (event, hookCtx) => {
1353
+ on("before_agent_start", async (event, hookCtx) => {
1299
1354
  const rawChannelId = resolveChannelId(hookCtx);
1300
1355
  const agentId = hookCtx.agentId || event.agentId || "main";
1301
1356
  if (config.debug) {
@@ -1313,7 +1368,7 @@ const cozeloopTracePlugin = {
1313
1368
  });
1314
1369
  }
1315
1370
  if (shouldHookEnabled("agent_end")) {
1316
- api.on("agent_end", async (event, hookCtx) => {
1371
+ on("agent_end", async (event, hookCtx) => {
1317
1372
  const rawChannelId = resolveChannelId(hookCtx);
1318
1373
  if (config.debug) {
1319
1374
  api.logger.info(`[CozeloopTrace] agent_end hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}`);
@@ -1333,7 +1388,7 @@ const cozeloopTracePlugin = {
1333
1388
  // versions), finalize the trace here so that agent + root spans get ended
1334
1389
  // and exported.
1335
1390
  if (shouldHookEnabled("session_end")) {
1336
- api.on("session_end", async (event, hookCtx) => {
1391
+ on("session_end", async (event, hookCtx) => {
1337
1392
  const rawChannelId = resolveChannelId(hookCtx, event.sessionId);
1338
1393
  if (config.debug) {
1339
1394
  api.logger.info(`[CozeloopTrace] session_end: ${rawChannelId}`);