coding-agent-wrapper 0.1.1__tar.gz → 0.1.4__tar.gz

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.
Files changed (34) hide show
  1. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/PKG-INFO +4 -3
  2. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/README.md +3 -2
  3. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/__init__.py +19 -12
  4. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/agent.py +16 -1
  5. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/providers.py +73 -0
  6. coding_agent_wrapper-0.1.4/caw/logger.py +89 -0
  7. coding_agent_wrapper-0.1.4/caw/pricing.json +32 -0
  8. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/provider.py +5 -0
  9. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/providers/claude_code.py +36 -5
  10. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/providers/codex.py +37 -0
  11. coding_agent_wrapper-0.1.4/caw/providers/opencode.py +715 -0
  12. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/viewer/static/index.html +2 -2
  13. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/pyproject.toml +1 -1
  14. coding_agent_wrapper-0.1.1/caw/pricing.json +0 -15
  15. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/.gitignore +0 -0
  16. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/LICENSE +0 -0
  17. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/README.md +0 -0
  18. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/__init__.py +0 -0
  19. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/cli.py +0 -0
  20. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/collector.py +0 -0
  21. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/manifest.py +0 -0
  22. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/status.py +0 -0
  23. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/cli.py +0 -0
  24. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/display.py +0 -0
  25. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/faststats.py +0 -0
  26. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/mcp.py +0 -0
  27. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/models.py +0 -0
  28. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/pricing.py +0 -0
  29. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/providers/__init__.py +0 -0
  30. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/py.typed +0 -0
  31. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/storage.py +0 -0
  32. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/toolkit.py +0 -0
  33. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/traj_cli.py +0 -0
  34. {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/viewer/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-wrapper
3
- Version: 0.1.1
3
+ Version: 0.1.4
4
4
  Summary: Unified Python library and CLI for orchestrating coding agents (Claude Code, Codex, etc.) with MCP tool servers and credential management.
5
5
  Project-URL: Homepage, https://github.com/zzjas/caw
6
6
  Project-URL: Repository, https://github.com/zzjas/caw
@@ -30,7 +30,7 @@ Description-Content-Type: text/markdown
30
30
 
31
31
  # caw
32
32
 
33
- **Coding Agent Wrapper** — a Python library and CLI for orchestrating coding agents (Claude Code, Codex) with a unified interface, MCP tool servers, and credential management for Docker containers.
33
+ **Coding Agent Wrapper** — a Python library and CLI for orchestrating coding agents (Claude Code, Codex, opencode) with a unified interface, MCP tool servers, and credential management for Docker containers.
34
34
 
35
35
  ## Install
36
36
 
@@ -91,6 +91,7 @@ with agent.start_session() as session:
91
91
  |----------|-----|---------------|
92
92
  | Claude Code | `claude` | `claude_code` |
93
93
  | Codex | `codex` | `codex` |
94
+ | opencode | `opencode` | `opencode` |
94
95
 
95
96
  Set via constructor, environment variable, or at runtime:
96
97
 
@@ -197,7 +198,7 @@ Sessions are persisted to JSONL in `caw_data/` by default.
197
198
 
198
199
  ## CLI: `caw auth` — Credential Management for Docker Containers
199
200
 
200
- Manages coding agent OAuth credentials so they stay in sync between your host and Docker containers. Supports Claude Code and Codex. Host credential files are never modified — they are bind-mounted into the container at run time.
201
+ Manages coding agent OAuth credentials so they stay in sync between your host and Docker containers. Supports Claude Code, Codex, and opencode. Host credential files are never modified — they are bind-mounted into the container at run time.
201
202
 
202
203
  ```bash
203
204
  caw auth setup # snapshot configs, write mount manifest
@@ -1,6 +1,6 @@
1
1
  # caw
2
2
 
3
- **Coding Agent Wrapper** — a Python library and CLI for orchestrating coding agents (Claude Code, Codex) with a unified interface, MCP tool servers, and credential management for Docker containers.
3
+ **Coding Agent Wrapper** — a Python library and CLI for orchestrating coding agents (Claude Code, Codex, opencode) with a unified interface, MCP tool servers, and credential management for Docker containers.
4
4
 
5
5
  ## Install
6
6
 
@@ -61,6 +61,7 @@ with agent.start_session() as session:
61
61
  |----------|-----|---------------|
62
62
  | Claude Code | `claude` | `claude_code` |
63
63
  | Codex | `codex` | `codex` |
64
+ | opencode | `opencode` | `opencode` |
64
65
 
65
66
  Set via constructor, environment variable, or at runtime:
66
67
 
@@ -167,7 +168,7 @@ Sessions are persisted to JSONL in `caw_data/` by default.
167
168
 
168
169
  ## CLI: `caw auth` — Credential Management for Docker Containers
169
170
 
170
- Manages coding agent OAuth credentials so they stay in sync between your host and Docker containers. Supports Claude Code and Codex. Host credential files are never modified — they are bind-mounted into the container at run time.
171
+ Manages coding agent OAuth credentials so they stay in sync between your host and Docker containers. Supports Claude Code, Codex, and opencode. Host credential files are never modified — they are bind-mounted into the container at run time.
171
172
 
172
173
  ```bash
173
174
  caw auth setup # snapshot configs, write mount manifest
@@ -1,11 +1,23 @@
1
1
  """caw - Coding Agent Wrapper."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.4"
4
4
 
5
5
  from caw.agent import Agent, Session, register_provider
6
+ from caw.auth import get_docker_flags as auth_get_docker_flags
7
+ from caw.auth import get_status as auth_get_status
8
+ from caw.auth import setup as auth_setup
6
9
  from caw.display import Display, DisplayMode, get_global_display, set_global_display
7
10
  from caw.faststats import FastStats
8
- from caw.storage import JsonlWriter, SessionStore
11
+ from caw.logger import AgentLogger
12
+ from caw.mcp import (
13
+ MCPServerHandle,
14
+ create_mcp_http_server_bundle,
15
+ create_stateless_tool_server,
16
+ create_subagent_tool_server,
17
+ get_state_from_context,
18
+ mcp_tool,
19
+ register_tool,
20
+ )
9
21
  from caw.models import (
10
22
  AgentSpec,
11
23
  ContentBlock,
@@ -24,30 +36,25 @@ from caw.models import (
24
36
  from caw.provider import Provider, ProviderSession
25
37
  from caw.providers.claude_code import ClaudeCodeProvider
26
38
  from caw.providers.codex import CodexProvider
27
- from caw.mcp import (
28
- MCPServerHandle,
29
- create_mcp_http_server_bundle,
30
- create_stateless_tool_server,
31
- create_subagent_tool_server,
32
- get_state_from_context,
33
- mcp_tool,
34
- register_tool,
35
- )
39
+ from caw.providers.opencode import OpencodeProvider
40
+ from caw.storage import JsonlWriter, SessionStore
36
41
  from caw.toolkit import ToolKit, tool
37
42
  from caw.viewer import ViewerServer, start_viewer_server
38
- from caw.auth import setup as auth_setup, get_status as auth_get_status, get_docker_flags as auth_get_docker_flags
39
43
 
40
44
  # Auto-register built-in providers
41
45
  register_provider("claude_code", ClaudeCodeProvider)
42
46
  register_provider("claude", ClaudeCodeProvider)
43
47
  register_provider("cc", ClaudeCodeProvider)
44
48
  register_provider("codex", CodexProvider)
49
+ register_provider("opencode", OpencodeProvider)
45
50
 
46
51
  __all__ = [
47
52
  "Agent",
53
+ "AgentLogger",
48
54
  "AgentSpec",
49
55
  "ClaudeCodeProvider",
50
56
  "CodexProvider",
57
+ "OpencodeProvider",
51
58
  "JsonlWriter",
52
59
  "ContentBlock",
53
60
  "InteractiveResult",
@@ -16,6 +16,7 @@ from pathlib import Path
16
16
  from typing import Any
17
17
 
18
18
  from caw.display import get_global_display
19
+ from caw.logger import AgentLogger
19
20
  from caw.models import AgentSpec, InteractiveResult, MCPServer, ModelTier, ToolGroup, ToolUse, Trajectory, Turn
20
21
  from caw.provider import Provider, ProviderSession
21
22
  from caw.storage import SessionStore
@@ -103,6 +104,7 @@ class Session:
103
104
  tool_handles: list[Any] | None = None,
104
105
  auto_wait: bool = True,
105
106
  metadata: dict[str, Any] | None = None,
107
+ logger: AgentLogger | None = None,
106
108
  ) -> None:
107
109
  self._session = provider_session
108
110
  self._store = store
@@ -114,6 +116,9 @@ class Session:
114
116
  self._send_lock = threading.Lock()
115
117
  self._async_send_lock: asyncio.Lock | None = None
116
118
  self._traj_path: str | Path | None = None
119
+ self._logger = logger
120
+ if logger is not None:
121
+ self._session.set_logger(logger)
117
122
 
118
123
  async def send_async(self, message: str) -> Turn:
119
124
  """Async version of :meth:`send` — runs in a thread.
@@ -296,12 +301,14 @@ class Agent:
296
301
  stateless_tools: list[Any] | None = None,
297
302
  name: str = "",
298
303
  description: str = "",
304
+ logger: AgentLogger | None = None,
299
305
  **kwargs: Any,
300
306
  ) -> None:
301
307
  self._provider_name = provider
302
308
  self._provider: Provider | None = None
303
309
  self._mcp_servers: list[MCPServer] = []
304
310
  self._subagents: list[AgentSpec] = []
311
+ self._logger = logger
305
312
  self._tool_servers: list[Any] = [] # list[MCPServerHandle], lazy import
306
313
  if tool_servers:
307
314
  for ts in tool_servers:
@@ -504,12 +511,19 @@ class Agent:
504
511
  traj_path:
505
512
  If set, the trajectory is saved to this path after each
506
513
  step and when :meth:`Session.end` is called.
514
+ logger:
515
+ Optional generic logger (any object with ``info``/``warn``/
516
+ ``error`` string methods). If set, every major event — user
517
+ message, tool call, tool result, assistant text, thinking,
518
+ turn-end stats — is also emitted as a one-line summary
519
+ through it. See :mod:`caw.logger`.
507
520
  """
508
521
  merged = {**self._kwargs, **kwargs}
509
522
 
510
- # Pop auto_wait and metadata — these are Session concerns, not provider kwargs
523
+ # Pop auto_wait, metadata, logger — these are Session concerns, not provider kwargs
511
524
  auto_wait = merged.pop("auto_wait", True)
512
525
  session_metadata: dict[str, Any] = merged.pop("metadata", {})
526
+ logger: AgentLogger | None = merged.pop("logger", None) or self._logger
513
527
  # Agent-level metadata as base, session kwargs override
514
528
  if self._metadata:
515
529
  session_metadata = {**self._metadata, **session_metadata}
@@ -567,6 +581,7 @@ class Agent:
567
581
  tool_handles=all_handles,
568
582
  auto_wait=auto_wait,
569
583
  metadata=session_metadata,
584
+ logger=logger,
570
585
  )
571
586
 
572
587
  if traj_path is not None:
@@ -259,6 +259,78 @@ class CodexAuthProvider(AgentAuthProvider):
259
259
  return files
260
260
 
261
261
 
262
+ # ---------------------------------------------------------------------------
263
+ # opencode
264
+ # ---------------------------------------------------------------------------
265
+
266
+
267
+ class OpencodeAuthProvider(AgentAuthProvider):
268
+ name = "opencode"
269
+
270
+ def validate(self, src_home: Path) -> list[str]:
271
+ missing: list[str] = []
272
+ if not (src_home / ".local" / "share" / "opencode" / "auth.json").exists():
273
+ missing.append(str(src_home / ".local" / "share" / "opencode" / "auth.json"))
274
+ return missing
275
+
276
+ def describe(self, src_home: Path) -> str:
277
+ try:
278
+ with open(src_home / ".local" / "share" / "opencode" / "auth.json") as f:
279
+ auth_data = json.load(f)
280
+ providers = list(auth_data.keys())
281
+ if not providers:
282
+ return "Auth file present (no providers configured)"
283
+ kinds = []
284
+ for p in providers:
285
+ entry = auth_data.get(p) or {}
286
+ kinds.append(f"{p}({entry.get('type', 'unknown')})")
287
+ return f"Providers: {', '.join(kinds)}"
288
+ except Exception:
289
+ return "Could not read auth info"
290
+
291
+ def collect(self, src_home: Path) -> list[CollectedFile]:
292
+ files: list[CollectedFile] = []
293
+
294
+ # auth.json — credential (bind-mounted for token refresh write-back)
295
+ auth_src = src_home / ".local" / "share" / "opencode" / "auth.json"
296
+ with open(auth_src, "rb") as f:
297
+ auth_content = f.read()
298
+ files.append(
299
+ CollectedFile(
300
+ manifest_file=ManifestFile(
301
+ src="opencode/auth.json",
302
+ container_target=".local/share/opencode/auth.json",
303
+ host_original=".local/share/opencode/auth.json",
304
+ type="credential",
305
+ strategy="bind",
306
+ mode="0600",
307
+ ),
308
+ content=auth_content,
309
+ )
310
+ )
311
+
312
+ # opencode.jsonc / opencode.json — config, copied (no local-project paths to strip)
313
+ for fname in ("opencode.jsonc", "opencode.json"):
314
+ cfg = src_home / ".config" / "opencode" / fname
315
+ if cfg.exists():
316
+ files.append(
317
+ CollectedFile(
318
+ manifest_file=ManifestFile(
319
+ src=f"opencode/{fname}",
320
+ container_target=f".config/opencode/{fname}",
321
+ host_original=f".config/opencode/{fname}",
322
+ type="config",
323
+ strategy="copy",
324
+ mode="0644",
325
+ ),
326
+ content=cfg.read_bytes(),
327
+ )
328
+ )
329
+ break # only one of the two should exist
330
+
331
+ return files
332
+
333
+
262
334
  # ---------------------------------------------------------------------------
263
335
  # Provider registry
264
336
  # ---------------------------------------------------------------------------
@@ -268,5 +340,6 @@ PROVIDERS: dict[str, AgentAuthProvider] = {
268
340
  for p in [
269
341
  ClaudeAuthProvider(),
270
342
  CodexAuthProvider(),
343
+ OpencodeAuthProvider(),
271
344
  ]
272
345
  }
@@ -0,0 +1,89 @@
1
+ """Generic logger protocol for caw.
2
+
3
+ The Agent layer emits a one-line summary for every major event
4
+ (tool call, tool result, assistant text, thinking, turn end) through
5
+ this interface, in addition to the existing rich-console Display.
6
+
7
+ Any object with ``info``/``warn``/``error`` string methods satisfies
8
+ the protocol — including ``logging.Logger`` (after a tiny adapter for
9
+ ``warn``→``warning``) and the project's own ``RedisLogger``. Pass an
10
+ instance via ``Agent(logger=...)`` or ``agent.start_session(logger=...)``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from typing import Protocol, runtime_checkable
17
+
18
+ from caw.models import TextBlock, ThinkingBlock, ToolUse, UsageStats
19
+
20
+
21
+ @runtime_checkable
22
+ class AgentLogger(Protocol):
23
+ """Minimal logger surface used by caw."""
24
+
25
+ def info(self, message: str) -> None: ...
26
+ def warn(self, message: str) -> None: ...
27
+ def error(self, message: str) -> None: ...
28
+
29
+
30
+ def _truncate(text: str, max_len: int = 200) -> str:
31
+ text = text.replace("\n", " ").strip()
32
+ if len(text) <= max_len:
33
+ return text
34
+ return text[: max_len - 1] + "…"
35
+
36
+
37
+ def log_tool_call(logger: AgentLogger | None, block: ToolUse) -> None:
38
+ if logger is None:
39
+ return
40
+ try:
41
+ args = json.dumps(block.arguments, separators=(",", ":"))
42
+ except (TypeError, ValueError):
43
+ args = str(block.arguments)
44
+ logger.info(f"tool_call {block.name} {_truncate(args)}")
45
+
46
+
47
+ def log_tool_result(logger: AgentLogger | None, block: ToolUse) -> None:
48
+ if logger is None:
49
+ return
50
+ tag = "tool_error" if block.is_error else "tool_result"
51
+ logger.info(f"{tag} {block.name} → {_truncate(block.output or '')}")
52
+
53
+
54
+ def log_text(logger: AgentLogger | None, block: TextBlock) -> None:
55
+ if logger is None or not block.text:
56
+ return
57
+ logger.info(f"assistant {_truncate(block.text)}")
58
+
59
+
60
+ def log_thinking(logger: AgentLogger | None, block: ThinkingBlock) -> None:
61
+ if logger is None or not block.text:
62
+ return
63
+ logger.info(f"thinking {_truncate(block.text)}")
64
+
65
+
66
+ def log_turn_end(logger: AgentLogger | None, usage: UsageStats, duration_ms: int) -> None:
67
+ if logger is None:
68
+ return
69
+ parts = [
70
+ f"duration={duration_ms}ms",
71
+ f"tokens={usage.input_tokens}in/{usage.output_tokens}out",
72
+ ]
73
+ if usage.cost_usd:
74
+ parts.append(f"cost=${usage.cost_usd:.4f}")
75
+ logger.info("turn_end " + " ".join(parts))
76
+
77
+
78
+ def log_user_message(logger: AgentLogger | None, message: str) -> None:
79
+ if logger is None:
80
+ return
81
+ logger.info(f"user {_truncate(message)}")
82
+
83
+
84
+ def log_metadata(logger: AgentLogger | None, **kwargs: str) -> None:
85
+ if logger is None:
86
+ return
87
+ pairs = [f"{k}={v}" for k, v in kwargs.items() if v]
88
+ if pairs:
89
+ logger.info("metadata " + " ".join(pairs))
@@ -0,0 +1,32 @@
1
+ {
2
+ "_comment": "Pricing in USD per 1 million tokens, keyed by agent then model",
3
+ "codex": {
4
+ "gpt-5.2-codex": {
5
+ "input": 1.75,
6
+ "cached_input": 0.175,
7
+ "output": 14.0
8
+ },
9
+ "gpt-5.3-codex": {
10
+ "input": 1.75,
11
+ "cached_input": 0.175,
12
+ "output": 14.0
13
+ }
14
+ },
15
+ "opencode": {
16
+ "openai/gpt-5.3-codex": {
17
+ "input": 1.75,
18
+ "cached_input": 0.175,
19
+ "output": 14.0
20
+ },
21
+ "openai/gpt-5.3-codex-spark": {
22
+ "input": 0.25,
23
+ "cached_input": 0.025,
24
+ "output": 2.0
25
+ },
26
+ "openai/gpt-5.2-codex": {
27
+ "input": 1.75,
28
+ "cached_input": 0.175,
29
+ "output": 14.0
30
+ }
31
+ }
32
+ }
@@ -6,6 +6,7 @@ from abc import ABC, abstractmethod
6
6
  from collections.abc import Callable
7
7
  from typing import Any
8
8
 
9
+ from caw.logger import AgentLogger
9
10
  from caw.models import InteractiveResult, MCPServer, ModelTier, ToolGroup, Trajectory, Turn
10
11
 
11
12
 
@@ -51,6 +52,10 @@ class ProviderSession(ABC):
51
52
  """Set callback invoked after each step within send()."""
52
53
  pass # default no-op; concrete providers override
53
54
 
55
+ def set_logger(self, logger: AgentLogger | None) -> None:
56
+ """Attach a generic logger that receives a one-line summary per event."""
57
+ pass # default no-op; concrete providers override
58
+
54
59
 
55
60
  class Provider(ABC):
56
61
  """ABC that each coding agent backend implements."""
@@ -14,6 +14,16 @@ from datetime import datetime, timedelta, timezone
14
14
  from typing import Any
15
15
 
16
16
  from caw.display import Display, get_global_display
17
+ from caw.logger import (
18
+ AgentLogger,
19
+ log_metadata,
20
+ log_text,
21
+ log_thinking,
22
+ log_tool_call,
23
+ log_tool_result,
24
+ log_turn_end,
25
+ log_user_message,
26
+ )
17
27
  from caw.models import (
18
28
  ContentBlock,
19
29
  InteractiveResult,
@@ -167,6 +177,7 @@ class ClaudeCodeSession(ProviderSession):
167
177
  self._mcp_config_path: str | None = None
168
178
  self._last_raw_output: str = ""
169
179
  self._step_callback = None
180
+ self._logger: AgentLogger | None = None
170
181
 
171
182
  # ------------------------------------------------------------------
172
183
  # MCP config helpers
@@ -210,6 +221,14 @@ class ClaudeCodeSession(ProviderSession):
210
221
  session=self._session_id,
211
222
  )
212
223
  display.on_user_message(message)
224
+ if not self._has_sent:
225
+ log_metadata(
226
+ self._logger,
227
+ agent="claude_code",
228
+ model=self._model or "",
229
+ session=self._session_id,
230
+ )
231
+ log_user_message(self._logger, message)
213
232
 
214
233
  cmd = [
215
234
  "claude",
@@ -304,6 +323,7 @@ class ClaudeCodeSession(ProviderSession):
304
323
 
305
324
  if display:
306
325
  display.on_turn_end(turn.result, usage, duration_ms)
326
+ log_turn_end(self._logger, usage, duration_ms)
307
327
 
308
328
  self._turns.append(turn)
309
329
  self._total_usage = self._total_usage + turn.usage
@@ -321,6 +341,9 @@ class ClaudeCodeSession(ProviderSession):
321
341
  def set_step_callback(self, callback):
322
342
  self._step_callback = callback
323
343
 
344
+ def set_logger(self, logger: AgentLogger | None) -> None:
345
+ self._logger = logger
346
+
324
347
  # ------------------------------------------------------------------
325
348
  # Per-event processing
326
349
  # ------------------------------------------------------------------
@@ -340,19 +363,25 @@ class ClaudeCodeSession(ProviderSession):
340
363
  self._model = event.get("model", "")
341
364
  if display and self._model:
342
365
  display.on_metadata(model=self._model)
366
+ if self._model:
367
+ log_metadata(self._logger, model=self._model)
343
368
 
344
369
  elif event_type == "assistant":
345
370
  new_blocks = self._parse_assistant_blocks(event)
346
371
  for block in new_blocks:
347
372
  blocks.append(block)
348
- if display:
349
- if isinstance(block, TextBlock):
373
+ if isinstance(block, TextBlock):
374
+ if display:
350
375
  display.on_text(block)
351
- elif isinstance(block, ThinkingBlock):
376
+ log_text(self._logger, block)
377
+ elif isinstance(block, ThinkingBlock):
378
+ if display:
352
379
  display.on_thinking(block)
353
- elif isinstance(block, ToolUse):
380
+ log_thinking(self._logger, block)
381
+ elif isinstance(block, ToolUse):
382
+ if display:
354
383
  display.on_tool_call(block)
355
- if isinstance(block, ToolUse):
384
+ log_tool_call(self._logger, block)
356
385
  tool_blocks[block.id] = block
357
386
 
358
387
  elif event_type == "user":
@@ -385,6 +414,7 @@ class ClaudeCodeSession(ProviderSession):
385
414
  tool_blocks[tid].is_error = is_error
386
415
  if display:
387
416
  display.on_tool_result(tool_blocks[tid])
417
+ log_tool_result(self._logger, tool_blocks[tid])
388
418
 
389
419
  elif event_type == "result":
390
420
  return self._parse_usage(event), event.get("duration_ms", 0)
@@ -461,6 +491,7 @@ class ClaudeCodeSession(ProviderSession):
461
491
  turns=list(self._turns),
462
492
  usage=self._total_usage,
463
493
  duration_ms=self._total_duration_ms,
494
+ reasoning=self._reasoning or "",
464
495
  metadata={},
465
496
  )
466
497
 
@@ -14,6 +14,16 @@ from pathlib import Path
14
14
  from typing import Any
15
15
 
16
16
  from caw.display import Display, get_global_display
17
+ from caw.logger import (
18
+ AgentLogger,
19
+ log_metadata,
20
+ log_text,
21
+ log_thinking,
22
+ log_tool_call,
23
+ log_tool_result,
24
+ log_turn_end,
25
+ log_user_message,
26
+ )
17
27
  from caw.models import (
18
28
  ContentBlock,
19
29
  MCPServer,
@@ -163,10 +173,14 @@ class CodexSession(ProviderSession):
163
173
  self._total_duration_ms = 0
164
174
  self._last_raw_output: str = ""
165
175
  self._step_callback = None
176
+ self._logger: AgentLogger | None = None
166
177
 
167
178
  def set_step_callback(self, callback):
168
179
  self._step_callback = callback
169
180
 
181
+ def set_logger(self, logger: AgentLogger | None) -> None:
182
+ self._logger = logger
183
+
170
184
  # ------------------------------------------------------------------
171
185
  # MCP config helpers
172
186
  # ------------------------------------------------------------------
@@ -198,6 +212,14 @@ class CodexSession(ProviderSession):
198
212
  session=self._session_id,
199
213
  )
200
214
  display.on_user_message(message)
215
+ if not self._has_sent:
216
+ log_metadata(
217
+ self._logger,
218
+ agent="codex",
219
+ model=self._model or "",
220
+ session=self._session_id,
221
+ )
222
+ log_user_message(self._logger, message)
201
223
 
202
224
  # Build the prompt (prepend system prompt on first turn)
203
225
  prompt = message
@@ -300,6 +322,7 @@ class CodexSession(ProviderSession):
300
322
 
301
323
  if display:
302
324
  display.on_turn_end(turn.result, usage, 0)
325
+ log_turn_end(self._logger, usage, 0)
303
326
 
304
327
  self._turns.append(turn)
305
328
  self._total_usage = self._total_usage + turn.usage
@@ -345,6 +368,7 @@ class CodexSession(ProviderSession):
345
368
  tool_blocks[tool_id] = block
346
369
  if display:
347
370
  display.on_tool_call(block)
371
+ log_tool_call(self._logger, block)
348
372
 
349
373
  elif item_type == "mcp_tool_call":
350
374
  server = item.get("server", "")
@@ -359,6 +383,7 @@ class CodexSession(ProviderSession):
359
383
  tool_blocks[tool_id] = block
360
384
  if display:
361
385
  display.on_tool_call(block)
386
+ log_tool_call(self._logger, block)
362
387
 
363
388
  elif item_type == "file_change":
364
389
  block = ToolUse(
@@ -370,6 +395,7 @@ class CodexSession(ProviderSession):
370
395
  tool_blocks[tool_id] = block
371
396
  if display:
372
397
  display.on_tool_call(block)
398
+ log_tool_call(self._logger, block)
373
399
 
374
400
  elif event_type in ("item.completed", "item.updated"):
375
401
  item = event.get("item", {})
@@ -383,6 +409,8 @@ class CodexSession(ProviderSession):
383
409
  tool_blocks[tool_id].is_error = item.get("exit_code", 0) != 0
384
410
  if display and is_final:
385
411
  display.on_tool_result(tool_blocks[tool_id])
412
+ if is_final:
413
+ log_tool_result(self._logger, tool_blocks[tool_id])
386
414
 
387
415
  elif item_type == "mcp_tool_call":
388
416
  tool_id = item.get("id", "")
@@ -406,6 +434,8 @@ class CodexSession(ProviderSession):
406
434
  tool_blocks[tool_id].is_error = True
407
435
  if display and is_final:
408
436
  display.on_tool_result(tool_blocks[tool_id])
437
+ if is_final:
438
+ log_tool_result(self._logger, tool_blocks[tool_id])
409
439
 
410
440
  elif item_type == "file_change":
411
441
  tool_id = item.get("id", "")
@@ -413,6 +443,8 @@ class CodexSession(ProviderSession):
413
443
  tool_blocks[tool_id].output = item.get("patch", item.get("content", ""))
414
444
  if display and is_final:
415
445
  display.on_tool_result(tool_blocks[tool_id])
446
+ if is_final:
447
+ log_tool_result(self._logger, tool_blocks[tool_id])
416
448
 
417
449
  elif item_type == "reasoning" and is_final:
418
450
  text = item.get("text", "")
@@ -421,6 +453,7 @@ class CodexSession(ProviderSession):
421
453
  blocks.append(block)
422
454
  if display:
423
455
  display.on_thinking(block)
456
+ log_thinking(self._logger, block)
424
457
 
425
458
  elif item_type == "agent_message" and is_final:
426
459
  text = item.get("text", "")
@@ -429,6 +462,7 @@ class CodexSession(ProviderSession):
429
462
  blocks.append(block)
430
463
  if display:
431
464
  display.on_text(block)
465
+ log_text(self._logger, block)
432
466
 
433
467
  elif event_type == "turn.completed":
434
468
  return self._parse_usage(event)
@@ -446,7 +480,10 @@ class CodexSession(ProviderSession):
446
480
  blocks.append(block)
447
481
  if display:
448
482
  display.on_text(block)
483
+ log_text(self._logger, block)
449
484
  else:
485
+ if self._logger:
486
+ self._logger.error(f"codex turn failed: {error_msg}")
450
487
  raise RuntimeError(f"Codex turn failed: {error_msg}")
451
488
 
452
489
  return None