coze_lab 0.1.9 → 0.1.11

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/index.js CHANGED
@@ -4294,12 +4294,18 @@ const AGENT_CONFIG = {
4294
4294
  },
4295
4295
  };
4296
4296
 
4297
- function detectAgent(agent) {
4297
+ function detectAgent(agent, cloud) {
4298
4298
  const cfg = AGENT_CONFIG[agent];
4299
4299
  // Check binary exists
4300
4300
  try {
4301
4301
  execSync(`which ${cfg.binary}`, { stdio: 'pipe' });
4302
4302
  } catch {
4303
+ // 云端 sandbox 里 agent 由 coze-bridge 托管运行,binary 不一定在 PATH。
4304
+ // 配置 hook 不依赖 binary 可执行,所以云端只 warn 不退出。
4305
+ if (cloud) {
4306
+ warn(`${agent} binary 不在 PATH(云端托管,跳过检测)`);
4307
+ return 'cloud';
4308
+ }
4303
4309
  errorBox([
4304
4310
  `ERROR: ${agent} is not installed on this machine`,
4305
4311
  '',
@@ -5197,7 +5203,7 @@ async function main() {
5197
5203
 
5198
5204
  // Step 2: Detect agent binary
5199
5205
  info('Step 2/5: Detecting agent...');
5200
- const version = detectAgent(agent);
5206
+ const version = detectAgent(agent, args.cloud);
5201
5207
  console.log('');
5202
5208
 
5203
5209
  // Step 3: Environment checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -331,10 +331,21 @@ def is_tool_result_message(msg: Dict[str, Any]) -> bool:
331
331
  )
332
332
 
333
333
  def extract_tool_result_from_message(msg: Dict[str, Any]) -> List[Dict[str, Any]]:
334
- """Extract tool_result items from a user message."""
334
+ """Extract tool_result items from a user message.
335
+
336
+ 把外层 user 消息的 timestamp 附到每个 tool_result item 上(_result_ts),
337
+ 用作 tool span 的真实完成时间——否则同一 step 内所有 tool 时间会挤成一样。
338
+ """
335
339
  content = msg.get("message", {}).get("content", [])
340
+ result_ts = msg.get("timestamp")
336
341
  if isinstance(content, list):
337
- return [item for item in content if isinstance(item, dict) and item.get("type") == "tool_result"]
342
+ out = []
343
+ for item in content:
344
+ if isinstance(item, dict) and item.get("type") == "tool_result":
345
+ if result_ts and "_result_ts" not in item:
346
+ item["_result_ts"] = result_ts
347
+ out.append(item)
348
+ return out
338
349
  return []
339
350
 
340
351
 
@@ -450,6 +461,16 @@ def _parse_ts(msg):
450
461
  return None
451
462
 
452
463
 
464
+ def _parse_ts_str(ts):
465
+ """ISO8601 字符串 → datetime(带时区)。失败返回 None。"""
466
+ if not ts or not isinstance(ts, str):
467
+ return None
468
+ try:
469
+ return datetime.fromisoformat(ts.replace("Z", "+00:00"))
470
+ except Exception:
471
+ return None
472
+
473
+
453
474
  def _ts_ms(dt):
454
475
  """datetime → 毫秒时间戳(int);None → None。"""
455
476
  return int(dt.timestamp() * 1000) if dt is not None else None
@@ -839,6 +860,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
839
860
  }
840
861
  # 真实耗时写 tag(SDK finish 不支持显式结束时间,duration 用 tag 兜底)
841
862
  if root_start_dt is not None and root_end_dt is not None:
863
+ root_span.set_finish_time(root_end_dt)
842
864
  root_tags["real_start_ms"] = _ts_ms(root_start_dt)
843
865
  root_tags["real_end_ms"] = _ts_ms(root_end_dt)
844
866
  root_tags["latency_ms"] = _ts_ms(root_end_dt) - _ts_ms(root_start_dt)
@@ -897,6 +919,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
897
919
  "source": "claude_code",
898
920
  }
899
921
  if turn_start_dt is not None and turn_end_dt is not None:
922
+ turn_span.set_finish_time(turn_end_dt)
900
923
  _turn_tags["real_start_ms"] = _ts_ms(turn_start_dt)
901
924
  _turn_tags["real_end_ms"] = _ts_ms(turn_end_dt)
902
925
  _turn_tags["latency_ms"] = _ts_ms(turn_end_dt) - _ts_ms(turn_start_dt)
@@ -930,6 +953,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
930
953
  model_span.set_runtime(Runtime(library="claude-code"))
931
954
  model_span.set_model_name(model_name)
932
955
  if step_start_dt is not None and step_end_dt is not None:
956
+ model_span.set_finish_time(step_end_dt)
933
957
  model_span.set_tags({
934
958
  "real_start_ms": _ts_ms(step_start_dt),
935
959
  "real_end_ms": _ts_ms(step_end_dt),
@@ -1028,13 +1052,30 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
1028
1052
  span_type = "agent" if is_agent else "tool"
1029
1053
  span_name = f"agent_{tool_name}" if is_agent else f"tool_{tool_name}"
1030
1054
 
1055
+ # tool 真实时间:start=模型发起调用(step_start_dt),
1056
+ # end=该 tool 对应 tool_result 的 timestamp(工具完成时刻)。
1057
+ # 不同 tool 的 result 时间不同,避免所有 tool span 时间挤成一样。
1058
+ tool_id = tool_call.get("id")
1059
+ tool_end_dt = None
1060
+ for result in step.get("tool_results", []):
1061
+ if result.get("tool_use_id") == tool_id:
1062
+ tool_end_dt = _parse_ts_str(result.get("_result_ts"))
1063
+ break
1064
+
1031
1065
  with client.start_span(name=span_name, span_type=span_type, start_time=step_start_dt) as tool_span:
1032
1066
  tool_span.set_runtime(Runtime(library="claude-code"))
1067
+ # 设真实完成时间,让每个 tool 的 duration 反映实际耗时。
1068
+ if tool_end_dt is not None:
1069
+ tool_span.set_finish_time(tool_end_dt)
1033
1070
  tags = {
1034
1071
  "tool_name": tool_name,
1035
1072
  "tool_call_id": tool_call.get("id"),
1036
1073
  "step_index": j,
1037
1074
  }
1075
+ if step_start_dt is not None and tool_end_dt is not None:
1076
+ tags["real_start_ms"] = _ts_ms(step_start_dt)
1077
+ tags["real_end_ms"] = _ts_ms(tool_end_dt)
1078
+ tags["latency_ms"] = _ts_ms(tool_end_dt) - _ts_ms(step_start_dt)
1038
1079
  if is_agent:
1039
1080
  tags["agent_name"] = agent_id
1040
1081
  tool_span.set_tags(tags)
@@ -1043,7 +1084,6 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
1043
1084
  )
1044
1085
 
1045
1086
  # Find matching tool result
1046
- tool_id = tool_call.get("id")
1047
1087
  for result in step.get("tool_results", []):
1048
1088
  if result.get("tool_use_id") == tool_id:
1049
1089
  result_content = result.get("content", "")