pythinker-code 2.2.1__py3-none-any.whl → 2.3.0__py3-none-any.whl
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.
- pythinker_code/CHANGELOG.md +13 -0
- pythinker_code/acp/host.py +3 -0
- pythinker_code/acp/session.py +7 -1
- pythinker_code/auth/oauth.py +18 -0
- pythinker_code/auth/openai.py +12 -0
- pythinker_code/auth/platforms.py +9 -0
- pythinker_code/hooks/engine.py +13 -1
- pythinker_code/hooks/runner.py +3 -0
- pythinker_code/soul/btw.py +6 -0
- pythinker_code/soul/pythinkersoul.py +34 -4
- pythinker_code/soul/toolset.py +42 -13
- pythinker_code/subagents/runner.py +7 -1
- pythinker_code/telemetry/config.py +29 -0
- pythinker_code/telemetry/errors.py +107 -0
- pythinker_code/telemetry/otel.py +24 -2
- pythinker_code/tools/agent/__init__.py +3 -0
- pythinker_code/tools/ask_user/__init__.py +4 -1
- pythinker_code/tools/file/glob.py +3 -0
- pythinker_code/tools/file/grep_local.py +3 -0
- pythinker_code/tools/file/read.py +3 -0
- pythinker_code/tools/file/read_media.py +3 -0
- pythinker_code/tools/file/replace.py +3 -0
- pythinker_code/tools/file/write.py +3 -0
- pythinker_code/tools/plan/__init__.py +4 -1
- pythinker_code/tools/plan/enter.py +4 -1
- pythinker_code/tools/shell/__init__.py +6 -0
- pythinker_code/ui/shell/slash.py +121 -0
- pythinker_code/web/static/assets/{_baseUniq-D7qiASUy.js → _baseUniq-DYwtr3m4.js} +1 -1
- pythinker_code/web/static/assets/{arc-G4WaEU6H.js → arc-CNhBgyVb.js} +1 -1
- pythinker_code/web/static/assets/{architectureDiagram-VXUJARFQ-CcYeuWPi.js → architectureDiagram-VXUJARFQ-DpvaxB3Y.js} +1 -1
- pythinker_code/web/static/assets/{blockDiagram-VD42YOAC-DWIqpSyD.js → blockDiagram-VD42YOAC-IlYHIkrW.js} +1 -1
- pythinker_code/web/static/assets/{c4Diagram-YG6GDRKO-BLk0cKfQ.js → c4Diagram-YG6GDRKO-D_jGrUIu.js} +1 -1
- pythinker_code/web/static/assets/channel-BPOuE91b.js +1 -0
- pythinker_code/web/static/assets/{chunk-4BX2VUAB-ChVR7Ju_.js → chunk-4BX2VUAB-uYRqFG6q.js} +1 -1
- pythinker_code/web/static/assets/{chunk-55IACEB6-DEiz531X.js → chunk-55IACEB6-5K_8Tvtf.js} +1 -1
- pythinker_code/web/static/assets/{chunk-B4BG7PRW-QjrqjAAP.js → chunk-B4BG7PRW-BAp2tokd.js} +1 -1
- pythinker_code/web/static/assets/{chunk-DI55MBZ5-Bw4GqGyI.js → chunk-DI55MBZ5-C3ICALbg.js} +1 -1
- pythinker_code/web/static/assets/{chunk-FMBD7UC4-bEv-7epi.js → chunk-FMBD7UC4-B3ntDoat.js} +1 -1
- pythinker_code/web/static/assets/{chunk-QN33PNHL-DOmXNpah.js → chunk-QN33PNHL-Dy8y3fp6.js} +1 -1
- pythinker_code/web/static/assets/{chunk-QZHKN3VN-D-6zUU9h.js → chunk-QZHKN3VN-BXmiK1aE.js} +1 -1
- pythinker_code/web/static/assets/{chunk-TZMSLE5B-CfuYCubo.js → chunk-TZMSLE5B-BbI6RHhP.js} +1 -1
- pythinker_code/web/static/assets/classDiagram-2ON5EDUG-C1S9FRV4.js +1 -0
- pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-C1S9FRV4.js +1 -0
- pythinker_code/web/static/assets/clone-D2vuslet.js +1 -0
- pythinker_code/web/static/assets/{code-block-IT6T5CEO-DsAmzUoy.js → code-block-IT6T5CEO-C09u1ZPS.js} +1 -1
- pythinker_code/web/static/assets/{cose-bilkent-S5V4N54A-aT1O0X9J.js → cose-bilkent-S5V4N54A-OfdgQa9b.js} +1 -1
- pythinker_code/web/static/assets/{cytoscape.esm-B4IyJ4dz.js → cytoscape.esm-BHPoE92Y.js} +1 -1
- pythinker_code/web/static/assets/{dagre-6UL2VRFP-DaTedwqX.js → dagre-6UL2VRFP-Dqsjg8sJ.js} +1 -1
- pythinker_code/web/static/assets/{diagram-PSM6KHXK-BykkJf3F.js → diagram-PSM6KHXK-DxkId0Z8.js} +1 -1
- pythinker_code/web/static/assets/{diagram-QEK2KX5R-Dui_gc9d.js → diagram-QEK2KX5R-CkPNihvj.js} +1 -1
- pythinker_code/web/static/assets/{diagram-S2PKOQOG-B7DmELTs.js → diagram-S2PKOQOG-C_N5Jjql.js} +1 -1
- pythinker_code/web/static/assets/{erDiagram-Q2GNP2WA-C_CHmY_j.js → erDiagram-Q2GNP2WA-C8_5yrCr.js} +1 -1
- pythinker_code/web/static/assets/{flowDiagram-NV44I4VS-BLZZYAxJ.js → flowDiagram-NV44I4VS-BfV7xDb8.js} +1 -1
- pythinker_code/web/static/assets/{ganttDiagram-JELNMOA3-Cb4lBZT4.js → ganttDiagram-JELNMOA3-Cld5kwhV.js} +1 -1
- pythinker_code/web/static/assets/{gitGraphDiagram-NY62KEGX-BrRnu-oS.js → gitGraphDiagram-NY62KEGX-F3FwjYQD.js} +1 -1
- pythinker_code/web/static/assets/{graph-Cdru59AN.js → graph-BWlEpfBO.js} +1 -1
- pythinker_code/web/static/assets/{index-Ix9ej5i-.js → index-BqPJMGF-.js} +2 -2
- pythinker_code/web/static/assets/{index-CHZz2B-g.js → index-DYUDz2ym.js} +1 -1
- pythinker_code/web/static/assets/{index-B9rDlDTV.js → index-DpudRZuI.js} +1 -1
- pythinker_code/web/static/assets/{infoDiagram-WHAUD3N6-BWbx2G71.js → infoDiagram-WHAUD3N6-BJOGXqn7.js} +1 -1
- pythinker_code/web/static/assets/{journeyDiagram-XKPGCS4Q-e5r2mFfX.js → journeyDiagram-XKPGCS4Q-BZdlH-JG.js} +1 -1
- pythinker_code/web/static/assets/{kanban-definition-3W4ZIXB7-Cw-5DMZz.js → kanban-definition-3W4ZIXB7-CmgmSsYi.js} +1 -1
- pythinker_code/web/static/assets/{layout-Bz5NLD6Y.js → layout-CWaYhVVo.js} +1 -1
- pythinker_code/web/static/assets/{linear-C6xei9WU.js → linear-Bw6Dncma.js} +1 -1
- pythinker_code/web/static/assets/{mermaid-VLURNSYL-B2s4EhdL.js → mermaid-VLURNSYL-CzjjwzDB.js} +7 -7
- pythinker_code/web/static/assets/{mermaid.core-DE-3B90d.js → mermaid.core-Bb0_1h52.js} +5 -5
- pythinker_code/web/static/assets/{min-DIq8zNFr.js → min-Df20Er5m.js} +1 -1
- pythinker_code/web/static/assets/{mindmap-definition-VGOIOE7T-CRmn6NBA.js → mindmap-definition-VGOIOE7T-CAe0siLd.js} +1 -1
- pythinker_code/web/static/assets/{pieDiagram-ADFJNKIX-xaDAWjUB.js → pieDiagram-ADFJNKIX-CLMBAwjU.js} +1 -1
- pythinker_code/web/static/assets/{quadrantDiagram-AYHSOK5B-DL9984Cg.js → quadrantDiagram-AYHSOK5B-B9vjzD3o.js} +1 -1
- pythinker_code/web/static/assets/{requirementDiagram-UZGBJVZJ-DMnNecbQ.js → requirementDiagram-UZGBJVZJ-Bbjo8TGX.js} +1 -1
- pythinker_code/web/static/assets/{sankeyDiagram-TZEHDZUN-BhFUMQqC.js → sankeyDiagram-TZEHDZUN-xnxkDnDQ.js} +1 -1
- pythinker_code/web/static/assets/{sequenceDiagram-WL72ISMW-Da3-18pZ.js → sequenceDiagram-WL72ISMW-qkafBa71.js} +1 -1
- pythinker_code/web/static/assets/{stateDiagram-FKZM4ZOC-D-vEOsdI.js → stateDiagram-FKZM4ZOC-BzTcRJpG.js} +1 -1
- pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-TyAV0qk_.js +1 -0
- pythinker_code/web/static/assets/{timeline-definition-IT6M3QCI-jjwwP1hV.js → timeline-definition-IT6M3QCI-0ls9u7xd.js} +1 -1
- pythinker_code/web/static/assets/{treemap-KMMF4GRG-CsfY6M0V.js → treemap-KMMF4GRG-C1ChVaOv.js} +1 -1
- pythinker_code/web/static/assets/{xychartDiagram-PRI3JC2R-Dk2xKhgL.js → xychartDiagram-PRI3JC2R-Ba9eacUU.js} +1 -1
- pythinker_code/web/static/index.html +1 -1
- {pythinker_code-2.2.1.dist-info → pythinker_code-2.3.0.dist-info}/METADATA +18 -4
- {pythinker_code-2.2.1.dist-info → pythinker_code-2.3.0.dist-info}/RECORD +85 -84
- pythinker_code/web/static/assets/channel-N3T2k8B2.js +0 -1
- pythinker_code/web/static/assets/classDiagram-2ON5EDUG-mMiORCnw.js +0 -1
- pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-mMiORCnw.js +0 -1
- pythinker_code/web/static/assets/clone-hquVYw4G.js +0 -1
- pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-CsiNChEg.js +0 -1
- {pythinker_code-2.2.1.dist-info → pythinker_code-2.3.0.dist-info}/WHEEL +0 -0
- {pythinker_code-2.2.1.dist-info → pythinker_code-2.3.0.dist-info}/entry_points.txt +0 -0
- {pythinker_code-2.2.1.dist-info → pythinker_code-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {pythinker_code-2.2.1.dist-info → pythinker_code-2.3.0.dist-info}/licenses/NOTICE +0 -0
pythinker_code/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 2.3.0 (2026-05-09)
|
|
6
|
+
|
|
7
|
+
Telemetry & observability audit.
|
|
8
|
+
|
|
9
|
+
- New `pythinker_code/telemetry/errors.py` helper `report_handled_error(exc, *, site, tool=None, **attrs)` forwards caught-and-rendered exceptions to both Sentry/Bugsink and the OTel `error` event stream. Both forwarding paths are `contextlib.suppress`-wrapped so monitoring can never break the host program.
|
|
10
|
+
- 38 silent-catch sites instrumented across `tools/`, `auth/`, `soul/`, `acp/`, `hooks/`, `subagents/`. Tool failures, OAuth errors, MCP server hiccups, hook callback failures, and subagent crashes now reach Bugsink and SigNoz.
|
|
11
|
+
- `pythinker_code/telemetry/otel.py`: TracerProvider now uses `ParentBased(TraceIdRatioBased(rate))` driven by `PYTHINKER_OTEL_TRACE_SAMPLE_RATE` (default 1.0).
|
|
12
|
+
- New `pythinker.mcp.call` span around every MCPTool RPC.
|
|
13
|
+
- New `/report-error` slash command (aliases `/report`, `/report-error`).
|
|
14
|
+
- `docs/en/reference/telemetry.md` documents the full telemetry contract.
|
|
15
|
+
- `chore(test)`: updated google-genai snapshot for pydantic 2.13.4 + google-genai 2.0.0.
|
|
16
|
+
|
|
17
|
+
|
|
5
18
|
## 2.2.1 (2026-05-09)
|
|
6
19
|
|
|
7
20
|
CI hardening: macOS binary build is now optional-codesign.
|
pythinker_code/acp/host.py
CHANGED
|
@@ -148,6 +148,9 @@ class ACPProcess:
|
|
|
148
148
|
)
|
|
149
149
|
self._feed_output(final_output)
|
|
150
150
|
except Exception as exc:
|
|
151
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
152
|
+
|
|
153
|
+
report_handled_error(exc, site="acp.host.terminal")
|
|
151
154
|
error_note = f"[acp terminal error] {exc}\n"
|
|
152
155
|
self._stdout.feed_data(error_note.encode("utf-8", "replace"))
|
|
153
156
|
if exit_code is None:
|
pythinker_code/acp/session.py
CHANGED
|
@@ -233,6 +233,9 @@ class ACPSession:
|
|
|
233
233
|
logger.info("Prompt cancelled by user")
|
|
234
234
|
return acp.PromptResponse(stop_reason="cancelled")
|
|
235
235
|
except Exception as e:
|
|
236
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
237
|
+
|
|
238
|
+
report_handled_error(e, site="acp.session.prompt")
|
|
236
239
|
logger.exception("Unexpected error during prompt:")
|
|
237
240
|
raise acp.RequestError.internal_error({"error": str(e)}) from e
|
|
238
241
|
finally:
|
|
@@ -461,7 +464,10 @@ class ACPSession:
|
|
|
461
464
|
# cancelled
|
|
462
465
|
logger.debug("Permission request cancelled for: {action}", action=request.action)
|
|
463
466
|
request.resolve("reject")
|
|
464
|
-
except Exception:
|
|
467
|
+
except Exception as exc:
|
|
468
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
469
|
+
|
|
470
|
+
report_handled_error(exc, site="acp.session.approval")
|
|
465
471
|
logger.exception("Error handling approval request:")
|
|
466
472
|
# On error, reject the request
|
|
467
473
|
request.resolve("reject")
|
pythinker_code/auth/oauth.py
CHANGED
|
@@ -358,6 +358,9 @@ def _load_from_keyring(key: str) -> OAuthToken | None:
|
|
|
358
358
|
try:
|
|
359
359
|
raw = keyring.get_password(KEYRING_SERVICE, key)
|
|
360
360
|
except Exception as exc:
|
|
361
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
362
|
+
|
|
363
|
+
report_handled_error(exc, site="auth.keyring.read")
|
|
361
364
|
logger.warning("Failed to read token from keyring: {error}", error=exc)
|
|
362
365
|
return None
|
|
363
366
|
if not raw:
|
|
@@ -632,6 +635,9 @@ async def login_pythinker_code(
|
|
|
632
635
|
try:
|
|
633
636
|
auth = await request_device_authorization()
|
|
634
637
|
except Exception as exc:
|
|
638
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
639
|
+
|
|
640
|
+
report_handled_error(exc, site="auth.oauth.device_authorize")
|
|
635
641
|
yield OAuthEvent("error", f"Login failed: {exc}")
|
|
636
642
|
return
|
|
637
643
|
|
|
@@ -680,6 +686,9 @@ async def login_pythinker_code(
|
|
|
680
686
|
yield OAuthEvent("info", "Device code expired, restarting login...")
|
|
681
687
|
continue
|
|
682
688
|
except Exception as exc:
|
|
689
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
690
|
+
|
|
691
|
+
report_handled_error(exc, site="auth.oauth.device_poll")
|
|
683
692
|
yield OAuthEvent("error", f"Login failed: {exc}")
|
|
684
693
|
return
|
|
685
694
|
break
|
|
@@ -692,6 +701,9 @@ async def login_pythinker_code(
|
|
|
692
701
|
try:
|
|
693
702
|
models = await list_models(platform, token.access_token)
|
|
694
703
|
except Exception as exc:
|
|
704
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
705
|
+
|
|
706
|
+
report_handled_error(exc, site="auth.models.fetch")
|
|
695
707
|
logger.error("Failed to get models: {error}", error=exc)
|
|
696
708
|
yield OAuthEvent("error", f"Failed to get models: {exc}")
|
|
697
709
|
return
|
|
@@ -947,6 +959,9 @@ class OAuthManager:
|
|
|
947
959
|
try:
|
|
948
960
|
await self.ensure_fresh(runtime, force=force)
|
|
949
961
|
except Exception as exc:
|
|
962
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
963
|
+
|
|
964
|
+
report_handled_error(exc, site="auth.oauth.refresh.background")
|
|
950
965
|
logger.warning(
|
|
951
966
|
"Failed to refresh OAuth token in background: {error}",
|
|
952
967
|
error=exc,
|
|
@@ -1071,6 +1086,9 @@ class OAuthManager:
|
|
|
1071
1086
|
except Exception as exc:
|
|
1072
1087
|
if force:
|
|
1073
1088
|
raise
|
|
1089
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
1090
|
+
|
|
1091
|
+
report_handled_error(exc, site="auth.oauth.refresh")
|
|
1074
1092
|
logger.warning("Failed to refresh OAuth token: {error}", error=exc)
|
|
1075
1093
|
from pythinker_code.telemetry import track
|
|
1076
1094
|
|
pythinker_code/auth/openai.py
CHANGED
|
@@ -612,6 +612,9 @@ async def _finish_chatgpt_login(
|
|
|
612
612
|
models = await _discover_chatgpt_models(token.access_token)
|
|
613
613
|
selected_model, thinking = _select_default_openai_model(models)
|
|
614
614
|
except Exception as exc:
|
|
615
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
616
|
+
|
|
617
|
+
report_handled_error(exc, site="auth.openai.discover_chatgpt_models")
|
|
615
618
|
yield OAuthEvent("error", f"Failed to discover OpenAI ChatGPT models: {exc}")
|
|
616
619
|
return
|
|
617
620
|
|
|
@@ -649,6 +652,9 @@ async def login_openai_browser(
|
|
|
649
652
|
code, verifier, redirect_uri = await _wait_for_browser_code(open_browser=open_browser)
|
|
650
653
|
token_payload = await _exchange_code_for_tokens(code, verifier, redirect_uri)
|
|
651
654
|
except Exception as exc:
|
|
655
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
656
|
+
|
|
657
|
+
report_handled_error(exc, site="auth.openai.browser_login")
|
|
652
658
|
yield OAuthEvent("error", f"OpenAI browser login failed: {exc}")
|
|
653
659
|
return
|
|
654
660
|
|
|
@@ -667,6 +673,9 @@ async def login_openai_headless(config: Config) -> AsyncIterator[OAuthEvent]:
|
|
|
667
673
|
try:
|
|
668
674
|
device_code = await _request_device_code()
|
|
669
675
|
except Exception as exc:
|
|
676
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
677
|
+
|
|
678
|
+
report_handled_error(exc, site="auth.openai.device_start")
|
|
670
679
|
yield OAuthEvent("error", f"Failed to start OpenAI device login: {exc}")
|
|
671
680
|
return
|
|
672
681
|
|
|
@@ -688,6 +697,9 @@ async def login_openai_headless(config: Config) -> AsyncIterator[OAuthEvent]:
|
|
|
688
697
|
OPENAI_DEVICE_REDIRECT_URI,
|
|
689
698
|
)
|
|
690
699
|
except Exception as exc:
|
|
700
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
701
|
+
|
|
702
|
+
report_handled_error(exc, site="auth.openai.device_poll")
|
|
691
703
|
yield OAuthEvent("error", f"OpenAI device login failed: {exc}")
|
|
692
704
|
return
|
|
693
705
|
|
pythinker_code/auth/platforms.py
CHANGED
|
@@ -248,6 +248,9 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
248
248
|
try:
|
|
249
249
|
await oauth_manager.ensure_fresh()
|
|
250
250
|
except Exception as exc:
|
|
251
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
252
|
+
|
|
253
|
+
report_handled_error(exc, site="auth.platforms.refresh.pre_sync")
|
|
251
254
|
logger.warning(
|
|
252
255
|
"Failed to refresh OAuth token before model sync for {platform}: {error}",
|
|
253
256
|
platform=platform_id,
|
|
@@ -281,6 +284,9 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
281
284
|
try:
|
|
282
285
|
await oauth_manager.ensure_fresh(force=True)
|
|
283
286
|
except Exception as exc2:
|
|
287
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
288
|
+
|
|
289
|
+
report_handled_error(exc2, site="auth.platforms.refresh.after_401")
|
|
284
290
|
refresh_exc = exc2
|
|
285
291
|
logger.warning(
|
|
286
292
|
"Failed to refresh OAuth token after 401 for {platform}: {error}",
|
|
@@ -323,6 +329,9 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
323
329
|
changed = True
|
|
324
330
|
continue
|
|
325
331
|
except Exception as exc:
|
|
332
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
333
|
+
|
|
334
|
+
report_handled_error(exc, site="auth.platforms.sync")
|
|
326
335
|
fallback_models = _fallback_or_log(platform_id=platform_id, error=exc)
|
|
327
336
|
if fallback_models is None:
|
|
328
337
|
continue
|
pythinker_code/hooks/engine.py
CHANGED
|
@@ -237,7 +237,10 @@ class HookEngine:
|
|
|
237
237
|
results = await self._execute_hooks(
|
|
238
238
|
event, matcher_value, server_matched, wire_matched, input_data
|
|
239
239
|
)
|
|
240
|
-
except Exception:
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
242
|
+
|
|
243
|
+
report_handled_error(exc, site="hooks.engine.run")
|
|
241
244
|
logger.warning("Hook engine error for {}, failing open", event)
|
|
242
245
|
return []
|
|
243
246
|
|
|
@@ -278,6 +281,9 @@ class HookEngine:
|
|
|
278
281
|
try:
|
|
279
282
|
self._on_triggered(event, matcher_value, total)
|
|
280
283
|
except Exception as e:
|
|
284
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
285
|
+
|
|
286
|
+
report_handled_error(e, site="hooks.engine.triggered_cb")
|
|
281
287
|
logger.warning(
|
|
282
288
|
"HookTriggered callback failed for {event}: {error}, continuing",
|
|
283
289
|
event=event,
|
|
@@ -328,6 +334,9 @@ class HookEngine:
|
|
|
328
334
|
try:
|
|
329
335
|
self._on_resolved(event, matcher_value, action, reason, duration_ms)
|
|
330
336
|
except Exception as e:
|
|
337
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
338
|
+
|
|
339
|
+
report_handled_error(e, site="hooks.engine.resolved_cb")
|
|
331
340
|
logger.warning(
|
|
332
341
|
"HookResolved callback failed for {event}: {error}, continuing",
|
|
333
342
|
event=event,
|
|
@@ -366,6 +375,9 @@ class HookEngine:
|
|
|
366
375
|
logger.warning("Wire hook timed out: {} {}", event, target)
|
|
367
376
|
return HookResult(action="allow", timed_out=True)
|
|
368
377
|
except Exception as e:
|
|
378
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
379
|
+
|
|
380
|
+
report_handled_error(e, site="hooks.engine.wire")
|
|
369
381
|
hook_task.cancel()
|
|
370
382
|
logger.warning("Wire hook failed: {} {}: {}", event, target, e)
|
|
371
383
|
return HookResult(action="allow")
|
pythinker_code/hooks/runner.py
CHANGED
|
@@ -51,6 +51,9 @@ async def run_hook(
|
|
|
51
51
|
await proc.wait()
|
|
52
52
|
raise
|
|
53
53
|
except Exception as e:
|
|
54
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
55
|
+
|
|
56
|
+
report_handled_error(e, site="hooks.runner")
|
|
54
57
|
logger.warning("Hook failed: {}: {}", command, e)
|
|
55
58
|
return HookResult(action="allow", stderr=str(e))
|
|
56
59
|
|
pythinker_code/soul/btw.py
CHANGED
|
@@ -176,6 +176,9 @@ async def execute_side_question(
|
|
|
176
176
|
|
|
177
177
|
return None, "No response received."
|
|
178
178
|
except Exception as e:
|
|
179
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
180
|
+
|
|
181
|
+
report_handled_error(e, site="soul.btw.execute")
|
|
179
182
|
logger.warning("Side question failed: {error}", error=e)
|
|
180
183
|
return None, str(e)
|
|
181
184
|
|
|
@@ -210,5 +213,8 @@ async def run_side_question(soul: PythinkerSoul, question: str) -> None:
|
|
|
210
213
|
else:
|
|
211
214
|
wire_send(BtwEnd(id=btw_id, error=error or "No response received."))
|
|
212
215
|
except Exception as e:
|
|
216
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
217
|
+
|
|
218
|
+
report_handled_error(e, site="soul.btw.run_wire")
|
|
213
219
|
logger.warning("Side question failed: {error}", error=e)
|
|
214
220
|
wire_send(BtwEnd(id=btw_id, error=str(e)))
|
|
@@ -286,7 +286,14 @@ class PythinkerSoul:
|
|
|
286
286
|
try:
|
|
287
287
|
result = await provider.get_injections(self._context.history, self)
|
|
288
288
|
injections.extend(result)
|
|
289
|
-
except Exception:
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
291
|
+
|
|
292
|
+
report_handled_error(
|
|
293
|
+
exc,
|
|
294
|
+
site="soul.injection.get",
|
|
295
|
+
provider=type(provider).__name__,
|
|
296
|
+
)
|
|
290
297
|
logger.warning(
|
|
291
298
|
"injection provider %s failed",
|
|
292
299
|
type(provider).__name__,
|
|
@@ -304,7 +311,14 @@ class PythinkerSoul:
|
|
|
304
311
|
for provider in self._injection_providers:
|
|
305
312
|
try:
|
|
306
313
|
await provider.on_context_compacted()
|
|
307
|
-
except Exception:
|
|
314
|
+
except Exception as exc:
|
|
315
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
316
|
+
|
|
317
|
+
report_handled_error(
|
|
318
|
+
exc,
|
|
319
|
+
site="soul.injection.on_context_compacted",
|
|
320
|
+
provider=type(provider).__name__,
|
|
321
|
+
)
|
|
308
322
|
logger.warning(
|
|
309
323
|
"injection provider %s on_context_compacted failed",
|
|
310
324
|
type(provider).__name__,
|
|
@@ -316,7 +330,14 @@ class PythinkerSoul:
|
|
|
316
330
|
for provider in self._injection_providers:
|
|
317
331
|
try:
|
|
318
332
|
await provider.on_auto_changed(enabled)
|
|
319
|
-
except Exception:
|
|
333
|
+
except Exception as exc:
|
|
334
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
335
|
+
|
|
336
|
+
report_handled_error(
|
|
337
|
+
exc,
|
|
338
|
+
site="soul.injection.on_auto_changed",
|
|
339
|
+
provider=type(provider).__name__,
|
|
340
|
+
)
|
|
320
341
|
logger.warning(
|
|
321
342
|
"injection provider %s on_auto_changed failed",
|
|
322
343
|
type(provider).__name__,
|
|
@@ -937,6 +958,9 @@ class PythinkerSoul:
|
|
|
937
958
|
try:
|
|
938
959
|
await self.compact_context()
|
|
939
960
|
except Exception as compact_err:
|
|
961
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
962
|
+
|
|
963
|
+
report_handled_error(compact_err, site="soul.context.compact")
|
|
940
964
|
logger.error(
|
|
941
965
|
"Context compaction failed at step {step_no}: {error_type}: {error}",
|
|
942
966
|
step_no=step_no,
|
|
@@ -952,6 +976,9 @@ class PythinkerSoul:
|
|
|
952
976
|
except BackToTheFuture as e:
|
|
953
977
|
back_to_the_future = e
|
|
954
978
|
except Exception as e:
|
|
979
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
980
|
+
|
|
981
|
+
report_handled_error(e, site="soul.step.error")
|
|
955
982
|
# any other exception should interrupt the step
|
|
956
983
|
req_id = getattr(e, "request_id", None)
|
|
957
984
|
logger.error(
|
|
@@ -1453,7 +1480,10 @@ class PythinkerSoul:
|
|
|
1453
1480
|
raise
|
|
1454
1481
|
try:
|
|
1455
1482
|
recovered = chat_provider.on_retryable_error(error)
|
|
1456
|
-
except Exception:
|
|
1483
|
+
except Exception as recover_exc:
|
|
1484
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
1485
|
+
|
|
1486
|
+
report_handled_error(recover_exc, site="soul.chat.recover")
|
|
1457
1487
|
logger.exception(
|
|
1458
1488
|
"Failed to recover chat provider during {name} after {error_type}.",
|
|
1459
1489
|
name=name,
|
pythinker_code/soul/toolset.py
CHANGED
|
@@ -319,6 +319,9 @@ class PythinkerToolset:
|
|
|
319
319
|
parameters=parameters,
|
|
320
320
|
)
|
|
321
321
|
except Exception as e:
|
|
322
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
323
|
+
|
|
324
|
+
report_handled_error(e, site="soul.toolset.register_external")
|
|
322
325
|
return False, str(e)
|
|
323
326
|
self.add(tool)
|
|
324
327
|
return True, None
|
|
@@ -490,6 +493,9 @@ class PythinkerToolset:
|
|
|
490
493
|
logger.info("Connected MCP server: {server_name}", server_name=server_name)
|
|
491
494
|
return server_name, None
|
|
492
495
|
except Exception as e:
|
|
496
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
497
|
+
|
|
498
|
+
report_handled_error(e, site="soul.toolset.mcp.connect")
|
|
493
499
|
logger.error(
|
|
494
500
|
"Failed to connect MCP server: {server_name}, error: {error}",
|
|
495
501
|
server_name=server_name,
|
|
@@ -608,6 +614,7 @@ class MCPTool[T: ClientTransport](CallableTool):
|
|
|
608
614
|
**kwargs,
|
|
609
615
|
)
|
|
610
616
|
self._mcp_tool = mcp_tool
|
|
617
|
+
self._mcp_server_name = server_name
|
|
611
618
|
self._client = client
|
|
612
619
|
self._runtime = runtime
|
|
613
620
|
self._timeout = timedelta(milliseconds=runtime.config.mcp.client.tool_call_timeout_ms)
|
|
@@ -619,25 +626,43 @@ class MCPTool[T: ClientTransport](CallableTool):
|
|
|
619
626
|
if not result:
|
|
620
627
|
return result.rejection_error()
|
|
621
628
|
|
|
629
|
+
from pythinker_code.telemetry import otel as _otel
|
|
630
|
+
|
|
631
|
+
# `start_span` returns a sync context manager (the OTel SDK uses
|
|
632
|
+
# `_AgnosticContextManager`, which intentionally has no __aenter__).
|
|
633
|
+
# Keep it as a sync `with` and use `async with` only on the fastmcp
|
|
634
|
+
# client.
|
|
622
635
|
try:
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
636
|
+
with _otel.start_span(
|
|
637
|
+
"pythinker.mcp.call",
|
|
638
|
+
{
|
|
639
|
+
"mcp.server": self._mcp_server_name,
|
|
640
|
+
"mcp.tool": self._mcp_tool.name,
|
|
641
|
+
"mcp.timeout_ms": int(self._timeout.total_seconds() * 1000),
|
|
642
|
+
},
|
|
643
|
+
) as span:
|
|
644
|
+
async with self._client as client:
|
|
645
|
+
result = await client.call_tool(
|
|
646
|
+
self._mcp_tool.name,
|
|
647
|
+
kwargs,
|
|
648
|
+
timeout=self._timeout,
|
|
649
|
+
raise_on_error=False,
|
|
635
650
|
)
|
|
636
|
-
|
|
651
|
+
span.set_attribute("mcp.is_error", bool(result.is_error))
|
|
652
|
+
if result.is_error:
|
|
653
|
+
logger.warning(
|
|
654
|
+
"MCP tool returned error: {tool_name}: {content}",
|
|
655
|
+
tool_name=self._mcp_tool.name,
|
|
656
|
+
content=[str(p) for p in result.content][:3],
|
|
657
|
+
)
|
|
658
|
+
return convert_mcp_tool_result(result)
|
|
637
659
|
except Exception as e:
|
|
660
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
661
|
+
|
|
638
662
|
# fastmcp raises `RuntimeError` on timeout and we cannot tell it from other errors
|
|
639
663
|
exc_msg = str(e).lower()
|
|
640
664
|
if "timeout" in exc_msg or "timed out" in exc_msg:
|
|
665
|
+
report_handled_error(e, site="soul.toolset.mcp.call.timeout", tool="MCP")
|
|
641
666
|
logger.warning(
|
|
642
667
|
"MCP tool call timed out: {tool_name}: {error}",
|
|
643
668
|
tool_name=self._mcp_tool.name,
|
|
@@ -650,6 +675,7 @@ class MCPTool[T: ClientTransport](CallableTool):
|
|
|
650
675
|
),
|
|
651
676
|
brief="Timeout",
|
|
652
677
|
)
|
|
678
|
+
report_handled_error(e, site="soul.toolset.mcp.call", tool="MCP")
|
|
653
679
|
logger.error(
|
|
654
680
|
"MCP tool call failed: {tool_name}: {error}",
|
|
655
681
|
tool_name=self._mcp_tool.name,
|
|
@@ -693,6 +719,9 @@ class WireExternalTool(CallableTool):
|
|
|
693
719
|
except asyncio.CancelledError:
|
|
694
720
|
raise
|
|
695
721
|
except Exception as e:
|
|
722
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
723
|
+
|
|
724
|
+
report_handled_error(e, site="soul.toolset.external_tool", tool="External")
|
|
696
725
|
logger.exception("External tool call failed: {tool_name}:", tool_name=self.name)
|
|
697
726
|
return ToolError(
|
|
698
727
|
message=f"External tool call failed: {e}",
|
|
@@ -124,6 +124,9 @@ async def run_soul_checked(
|
|
|
124
124
|
brief="LLM provider error",
|
|
125
125
|
)
|
|
126
126
|
except Exception as exc:
|
|
127
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
128
|
+
|
|
129
|
+
report_handled_error(exc, site="subagents.run.soul")
|
|
127
130
|
logger.exception("Subagent soul run failed when {phase}", phase=phase)
|
|
128
131
|
return SoulRunFailure(
|
|
129
132
|
message=f"Unexpected error when {phase}: {exc}",
|
|
@@ -316,7 +319,10 @@ class ForegroundSubagentRunner:
|
|
|
316
319
|
self._store.update_instance(agent_id, status="killed")
|
|
317
320
|
output_writer.stage("cancelled")
|
|
318
321
|
raise RunCancelled("Subagent run was cancelled.") from exc
|
|
319
|
-
except Exception:
|
|
322
|
+
except Exception as exc:
|
|
323
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
324
|
+
|
|
325
|
+
report_handled_error(exc, site="subagents.run.background")
|
|
320
326
|
self._store.update_instance(agent_id, status="failed")
|
|
321
327
|
output_writer.stage("failed_exception")
|
|
322
328
|
raise
|
|
@@ -52,3 +52,32 @@ def is_disabled() -> bool:
|
|
|
52
52
|
Sentry and OTel emission for the process."""
|
|
53
53
|
raw = os.environ.get("PYTHINKER_DISABLE_TELEMETRY", "").strip().lower()
|
|
54
54
|
return raw in {"1", "true", "yes", "on"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Sampling
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
DEFAULT_OTEL_TRACE_SAMPLE_RATE = 1.0
|
|
62
|
+
"""Default fraction of root-trace spans to record. 1.0 = always-on; 0.0 = none."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def otel_trace_sample_rate() -> float:
|
|
66
|
+
"""Resolve the OTel trace sampling rate.
|
|
67
|
+
|
|
68
|
+
Honors ``PYTHINKER_OTEL_TRACE_SAMPLE_RATE``. Clamped to ``[0.0, 1.0]``.
|
|
69
|
+
Malformed input falls back to the default rather than disabling tracing
|
|
70
|
+
or raising — telemetry config must never break the host program.
|
|
71
|
+
"""
|
|
72
|
+
raw = os.environ.get("PYTHINKER_OTEL_TRACE_SAMPLE_RATE", "").strip()
|
|
73
|
+
if not raw:
|
|
74
|
+
return DEFAULT_OTEL_TRACE_SAMPLE_RATE
|
|
75
|
+
try:
|
|
76
|
+
rate = float(raw)
|
|
77
|
+
except ValueError:
|
|
78
|
+
return DEFAULT_OTEL_TRACE_SAMPLE_RATE
|
|
79
|
+
if rate < 0.0:
|
|
80
|
+
return 0.0
|
|
81
|
+
if rate > 1.0:
|
|
82
|
+
return 1.0
|
|
83
|
+
return rate
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Helper for reporting handled exceptions to Sentry/Bugsink + OTel.
|
|
2
|
+
|
|
3
|
+
Use this at any except-block site that intentionally turns an exception into
|
|
4
|
+
a graceful user-facing error result (a ``ToolError``, a TUI message, a logged
|
|
5
|
+
warning). The site keeps its existing failure-rendering behaviour; the helper
|
|
6
|
+
additionally informs the monitoring stack so dashboards can see the failure
|
|
7
|
+
rate, class, and bucket.
|
|
8
|
+
|
|
9
|
+
Privacy posture
|
|
10
|
+
---------------
|
|
11
|
+
Only pass primitive enum-like attributes through ``**attrs`` (tool name,
|
|
12
|
+
class names, mode flags). The OTel ``error`` event is forwarded verbatim, so
|
|
13
|
+
**never** pass user input, file paths, or code snippets there. Sentry is fine
|
|
14
|
+
with full exception data — its ``before_send`` hook already scrubs paths and
|
|
15
|
+
strips PII before transmission.
|
|
16
|
+
|
|
17
|
+
Both forwarding paths are wrapped in :func:`contextlib.suppress` because
|
|
18
|
+
telemetry must never break the host program.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import contextlib
|
|
24
|
+
import time
|
|
25
|
+
from collections import deque
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from pythinker_code.telemetry import sentry as _sentry
|
|
30
|
+
from pythinker_code.telemetry import track
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Process-local ring buffer of recent errors
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Keeps just enough metadata to populate the ``/report-error`` slash command
|
|
36
|
+
# without retaining the full exception object (which can hold large frames
|
|
37
|
+
# and references). Class name + a short, redacted message is what shows in
|
|
38
|
+
# the buffer; the *full* scrubbed stack is already in Sentry/Bugsink.
|
|
39
|
+
|
|
40
|
+
_RECENT_BUFFER_SIZE = 10
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class RecentError:
|
|
45
|
+
timestamp: float
|
|
46
|
+
site: str
|
|
47
|
+
exc_class: str
|
|
48
|
+
message: str # truncated to 200 chars
|
|
49
|
+
tool: str | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_recent: deque[RecentError] = deque(maxlen=_RECENT_BUFFER_SIZE)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def recent_errors() -> list[RecentError]:
|
|
56
|
+
"""Snapshot of the most-recent reported errors (oldest first)."""
|
|
57
|
+
return list(_recent)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def clear_recent_errors() -> None:
|
|
61
|
+
"""Drop the recent-errors buffer. Used by tests and by ``/report-error``
|
|
62
|
+
after a successful submission."""
|
|
63
|
+
_recent.clear()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def report_handled_error(
|
|
67
|
+
exc: BaseException,
|
|
68
|
+
*,
|
|
69
|
+
site: str,
|
|
70
|
+
tool: str | None = None,
|
|
71
|
+
**attrs: bool | int | float | str | None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Forward a caught-and-rendered exception to Sentry + the OTel error stream.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
exc: The exception that was caught at the call site.
|
|
77
|
+
site: Stable identifier for the catch site, e.g. ``"tool.read"`` or
|
|
78
|
+
``"auth.oauth.refresh"``. Used to bucket failures in dashboards.
|
|
79
|
+
Must be a stable enum-like string, not a free-form message.
|
|
80
|
+
tool: Optional tool name when the site is a tool implementation
|
|
81
|
+
(e.g. ``"ReadFile"``). Forwarded as a separate property so SigNoz
|
|
82
|
+
queries can group by tool without parsing ``site``.
|
|
83
|
+
**attrs: Additional primitive enum-like attributes. Booleans, numbers
|
|
84
|
+
and short strings only. Values must not contain user input,
|
|
85
|
+
absolute paths, or code snippets.
|
|
86
|
+
"""
|
|
87
|
+
properties: dict[str, Any] = {
|
|
88
|
+
"site": site,
|
|
89
|
+
"exc_class": type(exc).__name__,
|
|
90
|
+
}
|
|
91
|
+
if tool is not None:
|
|
92
|
+
properties["tool"] = tool
|
|
93
|
+
properties.update(attrs)
|
|
94
|
+
with contextlib.suppress(Exception):
|
|
95
|
+
track("error", **properties)
|
|
96
|
+
with contextlib.suppress(Exception):
|
|
97
|
+
_sentry.capture_exception(exc)
|
|
98
|
+
with contextlib.suppress(Exception):
|
|
99
|
+
_recent.append(
|
|
100
|
+
RecentError(
|
|
101
|
+
timestamp=time.time(),
|
|
102
|
+
site=site,
|
|
103
|
+
exc_class=type(exc).__name__,
|
|
104
|
+
message=str(exc)[:200],
|
|
105
|
+
tool=tool,
|
|
106
|
+
)
|
|
107
|
+
)
|
pythinker_code/telemetry/otel.py
CHANGED
|
@@ -36,9 +36,21 @@ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
|
36
36
|
from opentelemetry.sdk.resources import Resource
|
|
37
37
|
from opentelemetry.sdk.trace import TracerProvider
|
|
38
38
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
39
|
+
from opentelemetry.sdk.trace.sampling import (
|
|
40
|
+
ALWAYS_OFF,
|
|
41
|
+
ALWAYS_ON,
|
|
42
|
+
ParentBased,
|
|
43
|
+
Sampler,
|
|
44
|
+
TraceIdRatioBased,
|
|
45
|
+
)
|
|
39
46
|
from opentelemetry.trace import Tracer
|
|
40
47
|
|
|
41
|
-
from pythinker_code.telemetry.config import
|
|
48
|
+
from pythinker_code.telemetry.config import (
|
|
49
|
+
is_disabled,
|
|
50
|
+
otel_endpoint,
|
|
51
|
+
otel_ingest_token,
|
|
52
|
+
otel_trace_sample_rate,
|
|
53
|
+
)
|
|
42
54
|
|
|
43
55
|
_TRACER_NAME = "pythinker-code"
|
|
44
56
|
_initialized: bool = False
|
|
@@ -97,7 +109,17 @@ def init(
|
|
|
97
109
|
headers=headers,
|
|
98
110
|
timeout=10,
|
|
99
111
|
)
|
|
100
|
-
|
|
112
|
+
rate = otel_trace_sample_rate()
|
|
113
|
+
sampler: Sampler
|
|
114
|
+
if rate >= 1.0:
|
|
115
|
+
sampler = ALWAYS_ON
|
|
116
|
+
elif rate <= 0.0:
|
|
117
|
+
sampler = ALWAYS_OFF
|
|
118
|
+
else:
|
|
119
|
+
# ParentBased honors a parent's sampling decision (e.g. an upstream
|
|
120
|
+
# ACP/wire request). For root spans the ratio sampler decides.
|
|
121
|
+
sampler = ParentBased(root=TraceIdRatioBased(rate))
|
|
122
|
+
tracer_provider = TracerProvider(resource=resource, sampler=sampler)
|
|
101
123
|
tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))
|
|
102
124
|
trace.set_tracer_provider(tracer_provider)
|
|
103
125
|
_tracer = tracer_provider.get_tracer(_TRACER_NAME, version)
|
|
@@ -157,6 +157,9 @@ class AgentTool(CallableTool2[Params]):
|
|
|
157
157
|
logger.exception("Foreground agent run failed")
|
|
158
158
|
return ToolError(message=f"Failed to run agent: {exc}", brief="Agent failed")
|
|
159
159
|
except Exception as exc:
|
|
160
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
161
|
+
|
|
162
|
+
report_handled_error(exc, site="tool.agent.foreground", tool="Agent")
|
|
160
163
|
logger.exception("Foreground agent run failed")
|
|
161
164
|
return ToolError(message=f"Failed to run agent: {exc}", brief="Agent failed")
|
|
162
165
|
|
|
@@ -135,7 +135,10 @@ class AskUserQuestion(CallableTool2[Params]):
|
|
|
135
135
|
),
|
|
136
136
|
brief="Client unsupported",
|
|
137
137
|
)
|
|
138
|
-
except Exception:
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
from pythinker_code.telemetry.errors import report_handled_error
|
|
140
|
+
|
|
141
|
+
report_handled_error(exc, site="tool.ask_user", tool="AskUser")
|
|
139
142
|
logger.exception("Failed to get user response for question %s", request.id)
|
|
140
143
|
return ToolError(
|
|
141
144
|
message="Failed to get user response.",
|