coze_lab 0.1.55 → 0.1.56

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -1465,6 +1465,33 @@ def _flatten_completed_steps(turns: List[Dict[str, Any]], is_terminal: bool = Fa
1465
1465
  return completed, len(flat)
1466
1466
 
1467
1467
 
1468
+ def _reconstructed_model_bounds(item: Dict[str, Any]):
1469
+ """Return request-ready -> assistant-emitted bounds for one Claude step."""
1470
+ turn = item.get("turn") or {}
1471
+ step = item.get("step") or {}
1472
+ step_index = item.get("step_index", 0)
1473
+ assistant_dt = _parse_ts(step.get("assistant_message"))
1474
+ start_dt = None
1475
+ if step_index > 0:
1476
+ previous_steps = turn.get("steps", [])
1477
+ if step_index <= len(previous_steps) - 1:
1478
+ previous = previous_steps[step_index - 1]
1479
+ result_times = [
1480
+ _parse_ts_str(result.get("_result_ts"))
1481
+ for result in previous.get("tool_results", [])
1482
+ ]
1483
+ result_times = [value for value in result_times if value is not None]
1484
+ if result_times:
1485
+ start_dt = max(result_times)
1486
+ else:
1487
+ start_dt = _parse_ts(previous.get("assistant_message"))
1488
+ if start_dt is None:
1489
+ start_dt = _parse_ts(turn.get("user_message"))
1490
+ if start_dt is None:
1491
+ start_dt = assistant_dt
1492
+ return start_dt, assistant_dt or start_dt
1493
+
1494
+
1468
1495
  def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_override=None, is_terminal=False):
1469
1496
  """实时上报:把新完成的 step 作为 span 挂到稳定 root,结束即 flush。
1470
1497
 
@@ -1569,17 +1596,14 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
1569
1596
  amo = assistant_msg.get("message", {})
1570
1597
  model_name = amo.get("model", "claude-code")
1571
1598
  gidx = last_global + sent # 全局 step 序号,用于命名
1572
- step_start_dt = _parse_ts(assistant_msg)
1573
- step_end_dt = None
1574
- for r in step.get("tool_results", []):
1575
- te = _parse_ts_str(r.get("_result_ts"))
1576
- if te:
1577
- step_end_dt = te
1599
+ step_start_dt, step_end_dt = _reconstructed_model_bounds(item)
1600
+ assistant_dt = _parse_ts(assistant_msg) or step_end_dt
1578
1601
 
1579
1602
  # model_span 挂 root
1580
1603
  mspan = client.start_span(name=f"model_call_{gidx}", span_type="model",
1581
1604
  start_time=step_start_dt, child_of=root_ctx)
1582
1605
  mspan.set_runtime(Runtime(library="claude-code"))
1606
+ mspan.set_tags({"timing_source": "reconstructed"})
1583
1607
  try:
1584
1608
  mspan.set_model_name(model_name)
1585
1609
  except Exception:
@@ -1635,7 +1659,7 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
1635
1659
  tool_end_dt = _parse_ts_str(r.get("_result_ts"))
1636
1660
  break
1637
1661
  tspan = client.start_span(name=f"tool_{tname}", span_type="tool",
1638
- start_time=step_start_dt, child_of=mspan_ctx)
1662
+ start_time=assistant_dt, child_of=mspan_ctx)
1639
1663
  tspan.set_runtime(Runtime(library="claude-code"))
1640
1664
  if tool_end_dt is not None:
1641
1665
  _set_finish_time_safe(tspan, tool_end_dt)
@@ -822,6 +822,12 @@ def extract_model_invocations(turns: List[Dict[str, Any]],
822
822
  }
823
823
  seen = {"content": set(), "reasoning": set()}
824
824
 
825
+ def request_timestamp(turn: Optional[Dict[str, Any]]) -> Optional[str]:
826
+ if not turn:
827
+ return None
828
+ user_message = turn.get("user_message") or {}
829
+ return user_message.get("_ts") or turn.get("start_timestamp")
830
+
825
831
  def append_text(category: str, value: Any):
826
832
  if not isinstance(value, str):
827
833
  return
@@ -842,7 +848,11 @@ def extract_model_invocations(turns: List[Dict[str, Any]],
842
848
  if not has_payload:
843
849
  return
844
850
  current["line"] = boundary_line
845
- current["end_timestamp"] = boundary_ts or current["end_timestamp"]
851
+ if current["end_timestamp"] is None:
852
+ current["end_timestamp"] = boundary_ts
853
+ current["timing_source"] = "reconstructed_usage_fallback"
854
+ else:
855
+ current["timing_source"] = "reconstructed"
846
856
  current["usage"] = usage or {}
847
857
  current["total_usage"] = total_usage or {}
848
858
  current["content"] = "\n".join(current.pop("content_parts"))
@@ -866,7 +876,8 @@ def extract_model_invocations(turns: List[Dict[str, Any]],
866
876
  "protected": {"source": "", "count": 0, "bytes": 0},
867
877
  "tool_calls": [],
868
878
  "turn": None,
869
- "start_timestamp": None,
879
+ "start_timestamp": event.get("timestamp"),
880
+ "start_turn": event.get("turn"),
870
881
  "end_timestamp": None,
871
882
  "line": -1,
872
883
  "usage": {},
@@ -876,7 +887,14 @@ def extract_model_invocations(turns: List[Dict[str, Any]],
876
887
  continue
877
888
 
878
889
  current["turn"] = current["turn"] or event.get("turn")
879
- current["start_timestamp"] = current["start_timestamp"] or event.get("timestamp")
890
+ if current.get("start_turn") is not None and current.get("start_turn") is not event.get("turn"):
891
+ current["start_timestamp"] = None
892
+ current["start_turn"] = event.get("turn")
893
+ current["start_timestamp"] = (
894
+ current["start_timestamp"]
895
+ or request_timestamp(event.get("turn"))
896
+ or event.get("timestamp")
897
+ )
880
898
  current["end_timestamp"] = event.get("timestamp") or current["end_timestamp"]
881
899
  current["line"] = max(current["line"], event.get("line", -1))
882
900
  if kind == "record":
@@ -944,6 +962,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
944
962
  "subagent_calls": [],
945
963
  "token_usage": {},
946
964
  "start_line": entry.get("_line_number", 0),
965
+ "start_timestamp": payload.get("_ts"),
947
966
  }
948
967
  pending_calls = {}
949
968
  elif msg_type == "task_complete":
@@ -1793,6 +1812,7 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
1793
1812
  tags = {
1794
1813
  "invocation_boundary_line": line,
1795
1814
  "token_source": "codex_token_count" if usage else "unavailable",
1815
+ "timing_source": invocation.get("timing_source", "reconstructed"),
1796
1816
  "cached_input_tokens": _usage_int(usage, "cached_input_tokens"),
1797
1817
  "reasoning_output_tokens": _usage_int(usage, "reasoning_output_tokens"),
1798
1818
  "total_tokens": _usage_int(usage, "total_tokens"),
@@ -870,6 +870,34 @@ const cozeloopTracePlugin = {
870
870
  parentSpanId: parentSpanId || ctx.rootSpanId,
871
871
  };
872
872
  };
873
+ const nativeTimingAttributes = (timing, fallbackSource = "reconstructed_session") => {
874
+ if (!timing)
875
+ return { timing_source: fallbackSource };
876
+ const attributes = {
877
+ timing_source: "native_model_call",
878
+ model_call_id: timing.callId,
879
+ model_call_outcome: timing.outcome,
880
+ };
881
+ if (timing.timeToFirstByteMs !== undefined)
882
+ attributes.time_to_first_byte_ms = timing.timeToFirstByteMs;
883
+ if (timing.requestPayloadBytes !== undefined)
884
+ attributes.request_payload_bytes = timing.requestPayloadBytes;
885
+ if (timing.responseStreamBytes !== undefined)
886
+ attributes.response_stream_bytes = timing.responseStreamBytes;
887
+ if (timing.errorCategory)
888
+ attributes.error_category = timing.errorCategory;
889
+ if (timing.failureKind)
890
+ attributes.failure_kind = timing.failureKind;
891
+ if (timing.outcome === "error")
892
+ attributes.error = true;
893
+ return attributes;
894
+ };
895
+ const consumeNativeModelTiming = (ctx) => {
896
+ const queue = ctx.completedNativeModelCalls;
897
+ if (!queue || queue.length === 0)
898
+ return undefined;
899
+ return queue.shift();
900
+ };
873
901
  const buildReactSpans = async (ctx, channelId, entries, initialInput, agentStartTime, userWrittenAt, skipCount, sessionUserContent) => {
874
902
  const entriesToSkip = skipCount || 0;
875
903
  const reactMessages = [];
@@ -947,8 +975,9 @@ const cozeloopTracePlugin = {
947
975
  modelSpanCount++;
948
976
  const provider = entry.provider || ctx.lastModelProvider || "unknown";
949
977
  const model = entry.model || ctx.lastModelId || "unknown";
950
- const spanStartTime = prevWrittenAt;
951
- const spanEndTime = entryWrittenAt;
978
+ const nativeTiming = consumeNativeModelTiming(ctx);
979
+ const spanStartTime = nativeTiming?.startTime ?? prevWrittenAt;
980
+ const spanEndTime = nativeTiming?.endTime ?? entryWrittenAt;
952
981
  const tokenUsage = readTokenUsage(entry.usage);
953
982
  const modelSpan = createSpan(ctx, channelId, `${provider}/${model}`, "model", spanStartTime, spanEndTime, {
954
983
  "gen_ai.provider.name": provider,
@@ -959,6 +988,7 @@ const cozeloopTracePlugin = {
959
988
  "input_tokens": tokenUsage.input,
960
989
  "output_tokens": tokenUsage.output,
961
990
  "react_round": reactRound,
991
+ ...nativeTimingAttributes(nativeTiming),
962
992
  }, { messages: reactMessages.map((msg) => safeClone(msg)) }, formatAssistantOutput(entry.content, entry.stopReason));
963
993
  await exporter.export(modelSpan);
964
994
  }
@@ -1161,9 +1191,47 @@ const cozeloopTracePlugin = {
1161
1191
  }
1162
1192
  });
1163
1193
  }
1164
- let lastLlmInput = undefined;
1165
- let lastLlmStartTime = undefined;
1166
- let lastLlmSpanId = undefined;
1194
+ if (shouldHookEnabled("model_call_started")) {
1195
+ on("model_call_started", async (event, hookCtx) => {
1196
+ const rawChannelId = resolveChannelId(hookCtx);
1197
+ const { ctx } = resolveActiveContext(rawChannelId, event.runId, "model_call_started", hookCtx);
1198
+ if (!ctx.nativeModelCallStarts)
1199
+ ctx.nativeModelCallStarts = new Map();
1200
+ ctx.nativeModelCallStarts.set(event.callId, {
1201
+ callId: event.callId,
1202
+ runId: event.runId,
1203
+ provider: event.provider,
1204
+ model: event.model,
1205
+ startTime: Date.now(),
1206
+ });
1207
+ });
1208
+ }
1209
+ if (shouldHookEnabled("model_call_ended")) {
1210
+ on("model_call_ended", async (event, hookCtx) => {
1211
+ const rawChannelId = resolveChannelId(hookCtx);
1212
+ const { ctx } = resolveActiveContext(rawChannelId, event.runId, "model_call_ended", hookCtx);
1213
+ const start = ctx.nativeModelCallStarts?.get(event.callId);
1214
+ if (!start)
1215
+ return;
1216
+ ctx.nativeModelCallStarts.delete(event.callId);
1217
+ const durationMs = Math.max(0, Number(event.durationMs) || 0);
1218
+ const endTime = Date.now();
1219
+ if (!ctx.completedNativeModelCalls)
1220
+ ctx.completedNativeModelCalls = [];
1221
+ ctx.completedNativeModelCalls.push({
1222
+ ...start,
1223
+ startTime: endTime - durationMs,
1224
+ endTime,
1225
+ durationMs,
1226
+ outcome: event.outcome,
1227
+ errorCategory: event.errorCategory,
1228
+ failureKind: event.failureKind,
1229
+ requestPayloadBytes: event.requestPayloadBytes,
1230
+ responseStreamBytes: event.responseStreamBytes,
1231
+ timeToFirstByteMs: event.timeToFirstByteMs,
1232
+ });
1233
+ });
1234
+ }
1167
1235
  if (shouldHookEnabled("llm_input")) {
1168
1236
  on("llm_input", async (event, hookCtx) => {
1169
1237
  const rawChannelId = resolveChannelId(hookCtx);
@@ -1248,9 +1316,6 @@ const cozeloopTracePlugin = {
1248
1316
  ctx.llmInput = {
1249
1317
  "messages": messages,
1250
1318
  };
1251
- lastLlmInput = ctx.llmInput;
1252
- lastLlmStartTime = ctx.llmStartTime;
1253
- lastLlmSpanId = ctx.llmSpanId;
1254
1319
  if (config.debug) {
1255
1320
  api.logger.info(`[CozeloopTrace] LLM input started: ${event.provider}/${event.model}, runId=${event.runId}, traceId=${ctx.traceId}`);
1256
1321
  }
@@ -1267,7 +1332,7 @@ const cozeloopTracePlugin = {
1267
1332
  }
1268
1333
  const { ctx, channelId } = resolveActiveContext(rawChannelId, event.runId, "llm_output", hookCtx);
1269
1334
  const now = Date.now();
1270
- const startTime = ctx.llmStartTime || lastLlmStartTime || now;
1335
+ const startTime = ctx.llmStartTime || now;
1271
1336
  if (event.assistantTexts && event.assistantTexts.length > 0) {
1272
1337
  const outputText = event.assistantTexts.join("\n");
1273
1338
  ctx.lastOutput = outputText;
@@ -1279,8 +1344,8 @@ const cozeloopTracePlugin = {
1279
1344
  api.logger.info(`[CozeloopTrace] Captured output from llm_output (will use last): traceId=${ctx.traceId}, length=${outputText.length}`);
1280
1345
  }
1281
1346
  }
1282
- const llmInput = ctx.llmInput || lastLlmInput;
1283
- const llmSpanId = ctx.llmSpanId || lastLlmSpanId;
1347
+ const llmInput = ctx.llmInput;
1348
+ const llmSpanId = ctx.llmSpanId;
1284
1349
  if (config.debug) {
1285
1350
  api.logger.info(`[CozeloopTrace] llm_output ctx: traceId=${ctx.traceId}, rootSpanId=${ctx.rootSpanId}, llmSpanId=${llmSpanId || "none"}, hasInput=${!!llmInput}`);
1286
1351
  }
@@ -1289,7 +1354,7 @@ const cozeloopTracePlugin = {
1289
1354
  const { entries, userWrittenAt, userContent } = readCurrentTurnReactSequence(hookCtx);
1290
1355
  const hasAssistantEntry = entries.some((e) => e.type === "assistant");
1291
1356
  if (entries.length > 0 && hasAssistantEntry) {
1292
- const agentStart = ctx.agentStartTime || ctx.llmStartTime || lastLlmStartTime || now;
1357
+ const agentStart = ctx.agentStartTime || ctx.llmStartTime || now;
1293
1358
  const skipCount = ctx.sessionBasedExportedCount || 0;
1294
1359
  const modelCount = await buildReactSpans(ctx, channelId, entries, llmInput, agentStart, userWrittenAt, skipCount, userContent);
1295
1360
  if (modelCount > 0) {
@@ -1332,7 +1397,9 @@ const cozeloopTracePlugin = {
1332
1397
  "output_tokens": tokenUsage.output,
1333
1398
  };
1334
1399
  const finalOutput = formatAssistantOutput(event.assistantTexts?.map((t) => ({ type: "text", text: t })) ?? [], "stop");
1335
- const span = createSpan(ctx, channelId, `${event.provider}/${event.model}`, "model", startTime, now, spanAttributes, llmInput, finalOutput);
1400
+ const nativeTiming = consumeNativeModelTiming(ctx);
1401
+ Object.assign(spanAttributes, nativeTimingAttributes(nativeTiming, "llm_hooks_fallback"));
1402
+ const span = createSpan(ctx, channelId, `${event.provider}/${event.model}`, "model", nativeTiming?.startTime ?? startTime, nativeTiming?.endTime ?? now, spanAttributes, llmInput, finalOutput);
1336
1403
  if (llmSpanId) {
1337
1404
  span.spanId = llmSpanId;
1338
1405
  }
@@ -1350,9 +1417,6 @@ const cozeloopTracePlugin = {
1350
1417
  ctx.reactCount = 0;
1351
1418
  ctx.pendingToolSpans = undefined;
1352
1419
  ctx.sessionBasedSpansCreated = undefined;
1353
- lastLlmInput = undefined;
1354
- lastLlmStartTime = undefined;
1355
- lastLlmSpanId = undefined;
1356
1420
  });
1357
1421
  }
1358
1422
  if (shouldHookEnabled("before_tool_call")) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-cozeloop-trace",
3
3
  "name": "OpenClaw CozeLoop Trace",
4
- "version": "0.1.21",
4
+ "version": "0.1.22",
5
5
  "description": "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
6
6
  "type": "plugin",
7
7
  "entry": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cozeloop/openclaw-cozeloop-trace",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "OpenClaw Plugin for reporting traces to CozeLoop via OpenTelemetry",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",