coze_lab 0.1.47 → 0.1.49

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.47",
3
+ "version": "0.1.49",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -1472,7 +1472,12 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
1472
1472
  if token:
1473
1473
  os.environ["COZELOOP_API_TOKEN"] = token
1474
1474
  workspace_id = os.environ.get("COZELOOP_WORKSPACE_ID", "") or _DEFAULT_WORKSPACE_ID
1475
- client_kwargs = {"ultra_large_report": True, "upload_timeout": 120}
1475
+ upload_events: List[str] = []
1476
+ client_kwargs = {
1477
+ "ultra_large_report": True,
1478
+ "upload_timeout": 120,
1479
+ "trace_finish_event_processor": _make_finish_event_processor(upload_events),
1480
+ }
1476
1481
  if workspace_id:
1477
1482
  client_kwargs["workspace_id"] = workspace_id
1478
1483
  if token:
@@ -1539,6 +1544,9 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
1539
1544
  # 实测:root finish 后,子 span 仍可用其 header 挂到同一 trace_id 下,不影响后续增量。
1540
1545
  root_span.finish()
1541
1546
  client.flush()
1547
+ if upload_events:
1548
+ debug_log(f"[rt] upload failed after root flush: {upload_events[-1][:500]}")
1549
+ return None
1542
1550
  debug_log(f"[rt] root created+finished trace_id={getattr(root_ctx,'trace_id','?')}")
1543
1551
 
1544
1552
  # ---- 发新完成的 step ----
@@ -1622,6 +1630,9 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
1622
1630
  sent += 1
1623
1631
  # 每个 step 立即 flush —— 这是“结束即可见”的关键。
1624
1632
  client.flush()
1633
+ if upload_events:
1634
+ debug_log(f"[rt] upload failed after step flush: {upload_events[-1][:500]}")
1635
+ return None
1625
1636
 
1626
1637
  new_last = last_global + sent
1627
1638
  state["rt_last_global_step"] = new_last
@@ -1650,8 +1661,15 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
1650
1661
  fin.set_output(last_output)
1651
1662
  fin.finish()
1652
1663
  client.flush()
1664
+ if upload_events:
1665
+ debug_log(f"[rt] upload failed after final flush: {upload_events[-1][:500]}")
1666
+ return None
1653
1667
  debug_log(f"[rt] finalized, total sent steps={new_last}")
1654
1668
 
1669
+ if upload_events:
1670
+ debug_log(f"[rt] upload failed, state not advanced. Last failure: {upload_events[-1][:500]}")
1671
+ return None
1672
+
1655
1673
  debug_log(f"[rt] sent {sent} new step(s), last_global={new_last}/{total_steps}, terminal={is_terminal}")
1656
1674
  return True
1657
1675
  except Exception as e:
@@ -131,6 +131,127 @@ else:
131
131
 
132
132
  # --- Configuration ---
133
133
  DEBUG = os.environ.get("CC_COZELOOP_DEBUG", "").lower() == "true"
134
+ _DEFAULT_WORKSPACE_ID = "7649231955045072915" # hardcoded spaceID fallback
135
+
136
+ # 实时 tool span 上报的批量间隔(秒):距上次上报不足此值则本次不发,把已结束的 tool span
137
+ # 攒到下次一起发(最多每 5s 一批)。终态(Stop/SubagentStop)不受限,立即收尾。
138
+ REALTIME_BATCH_INTERVAL = float(os.environ.get("COZELOOP_REALTIME_BATCH_INTERVAL", "5"))
139
+
140
+
141
+ # --- coze-context parsing -------------------------------------------------
142
+ # User messages may embed a block like:
143
+ # <coze-context>
144
+ # account_id: 0
145
+ # agent_id: 7644920552473395499
146
+ # session_id: 7644919579054997796
147
+ # message_id: 04dd5246-...
148
+ # </coze-context>
149
+ # We parse its key:value pairs and inject them into the trace.
150
+
151
+ _COZE_CTX_OPEN = "<coze-context>"
152
+ _COZE_CTX_CLOSE = "</coze-context>"
153
+
154
+
155
+ def parse_coze_context(text: str) -> Dict[str, str]:
156
+ """Extract the LAST <coze-context> block's key:value pairs from text.
157
+
158
+ Returns {} if no block is present. Tag keys are prefixed with
159
+ 'coze_' by the caller; here we return raw keys as written.
160
+ """
161
+ if not text or _COZE_CTX_OPEN not in text:
162
+ return {}
163
+ # Take the last occurrence (latest context wins).
164
+ open_idx = text.rfind(_COZE_CTX_OPEN)
165
+ close_idx = text.find(_COZE_CTX_CLOSE, open_idx)
166
+ if close_idx == -1:
167
+ return {}
168
+ body = text[open_idx + len(_COZE_CTX_OPEN):close_idx]
169
+ # The block may arrive with real newlines, OR with literal backslash-n
170
+ # (e.g. when the whole message is an embedded JSON string that was never
171
+ # un-escaped). Normalize both forms before splitting into lines.
172
+ body = body.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\\r", "\n")
173
+ result: Dict[str, str] = {}
174
+ for line in body.splitlines():
175
+ line = line.strip()
176
+ if not line or ":" not in line:
177
+ continue
178
+ key, _, value = line.partition(":")
179
+ key = key.strip()
180
+ value = value.strip()
181
+ if key:
182
+ result[key] = value
183
+ return result
184
+
185
+
186
+ def turn_coze_context(turn: Dict[str, Any]) -> Dict[str, str]:
187
+ """Extract coze-context from a grouped turn with fallbacks for Codex rollout shapes."""
188
+ texts = [turn.get("user_message_text", "")]
189
+ for msg in turn.get("input_messages", []):
190
+ if isinstance(msg, dict):
191
+ content = msg.get("content", "")
192
+ if isinstance(content, str):
193
+ texts.append(content)
194
+ elif isinstance(content, list):
195
+ texts.append(extract_message_content_text({"content": content}))
196
+ elif isinstance(content, dict):
197
+ texts.append(extract_message_content_text({"content": [content]}))
198
+ user_payload = turn.get("user_message")
199
+ if isinstance(user_payload, dict):
200
+ texts.append(extract_message_content_text(user_payload))
201
+ for text in texts:
202
+ ctx = parse_coze_context(text)
203
+ if ctx:
204
+ return ctx
205
+ return {}
206
+
207
+
208
+ # --- trace upload failure / logid capture ---------------------------------
209
+ def _extract_logid(msg: str) -> str:
210
+ """Pull the server logid out of an SDK error message, if present.
211
+
212
+ SDK failure messages embed it as 'logid=XXXX' (sometimes within brackets).
213
+ """
214
+ if not msg:
215
+ return ""
216
+ marker = "logid="
217
+ idx = msg.find(marker)
218
+ if idx == -1:
219
+ return ""
220
+ rest = msg[idx + len(marker):]
221
+ logid = []
222
+ for ch in rest:
223
+ if ch.isalnum():
224
+ logid.append(ch)
225
+ else:
226
+ break
227
+ return "".join(logid)
228
+
229
+
230
+ def _make_finish_event_processor(upload_events: Optional[List[str]] = None):
231
+ """Return a trace_finish_event_processor that surfaces failures + logid.
232
+
233
+ The CozeLoop SDK calls this for each flush event; on failure we print the
234
+ server logid to stderr so it can be handed to platform support for tracing
235
+ the root cause (e.g. via `bytedcli log get-logid-log <logid>`).
236
+ """
237
+ def _processor(info):
238
+ try:
239
+ if not getattr(info, "is_event_fail", False):
240
+ hook_log("upload success")
241
+ return
242
+ detail = getattr(info, "detail_msg", "") or ""
243
+ if upload_events is not None:
244
+ upload_events.append(detail or "trace export failed")
245
+ logid = _extract_logid(detail)
246
+ if logid:
247
+ hook_log(f"upload failed logid={logid} detail={detail[:500]}")
248
+ print(f"[CozeLoop] 上报失败 logid={logid} (可用 bytedcli log get-logid-log {logid} 排查)", file=sys.stderr)
249
+ else:
250
+ hook_log(f"upload failed detail={detail[:500]}")
251
+ print(f"[CozeLoop] 上报失败: {detail[:300]}", file=sys.stderr)
252
+ except Exception:
253
+ pass
254
+ return _processor
134
255
 
135
256
 
136
257
  def _normalize_api_base_url(raw: str) -> str:
@@ -1251,7 +1372,12 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
1251
1372
  if token:
1252
1373
  os.environ["COZELOOP_API_TOKEN"] = token
1253
1374
  workspace_id = os.environ.get("COZELOOP_WORKSPACE_ID", "") or _DEFAULT_WORKSPACE_ID
1254
- client_kwargs = {"ultra_large_report": True, "upload_timeout": 120}
1375
+ upload_events: List[str] = []
1376
+ client_kwargs = {
1377
+ "ultra_large_report": True,
1378
+ "upload_timeout": 120,
1379
+ "trace_finish_event_processor": _make_finish_event_processor(upload_events),
1380
+ }
1255
1381
  if workspace_id:
1256
1382
  client_kwargs["workspace_id"] = workspace_id
1257
1383
  if token:
@@ -1307,6 +1433,9 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
1307
1433
  root_ctx = client.get_span_from_header(state["rt_root_header"])
1308
1434
  root_span.finish() # 立即 finish 落库(后端按 root_span 查),子 span 仍可挂同 trace
1309
1435
  client.flush()
1436
+ if upload_events:
1437
+ hook_log(f"[rt] upload failed after root flush detail={upload_events[-1][:500]}")
1438
+ return None
1310
1439
  hook_log(f"[rt] root created trace_id={getattr(root_ctx,'trace_id','?')}")
1311
1440
 
1312
1441
  # 线性展开所有 tool_call(带 result 的才算完成),按全局序增量发
@@ -1361,6 +1490,9 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
1361
1490
  tspan.finish()
1362
1491
  sent += 1
1363
1492
  client.flush() # 每个 tool_call 立即 flush → 结束即可见
1493
+ if upload_events:
1494
+ hook_log(f"[rt] upload failed after tool flush detail={upload_events[-1][:500]}")
1495
+ return None
1364
1496
 
1365
1497
  state["rt_last_tool"] = last_tool + sent
1366
1498
  if sent > 0:
@@ -1382,8 +1514,15 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
1382
1514
  fin.set_output(truncate_text(last_out))
1383
1515
  fin.finish()
1384
1516
  client.flush()
1517
+ if upload_events:
1518
+ hook_log(f"[rt] upload failed after final flush detail={upload_events[-1][:500]}")
1519
+ return None
1385
1520
  hook_log(f"[rt] finalized total_tools={state['rt_last_tool']}")
1386
1521
 
1522
+ if upload_events:
1523
+ hook_log(f"[rt] upload failed state not advanced detail={upload_events[-1][:500]}")
1524
+ return None
1525
+
1387
1526
  hook_log(f"[rt] sent {sent} tool span(s), last_tool={state['rt_last_tool']}, terminal={is_terminal}")
1388
1527
  return True
1389
1528
  except Exception as e: