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.
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/PKG-INFO +4 -3
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/README.md +3 -2
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/__init__.py +19 -12
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/agent.py +16 -1
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/providers.py +73 -0
- coding_agent_wrapper-0.1.4/caw/logger.py +89 -0
- coding_agent_wrapper-0.1.4/caw/pricing.json +32 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/provider.py +5 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/providers/claude_code.py +36 -5
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/providers/codex.py +37 -0
- coding_agent_wrapper-0.1.4/caw/providers/opencode.py +715 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/viewer/static/index.html +2 -2
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/pyproject.toml +1 -1
- coding_agent_wrapper-0.1.1/caw/pricing.json +0 -15
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/.gitignore +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/LICENSE +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/README.md +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/__init__.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/cli.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/collector.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/manifest.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/auth/status.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/cli.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/display.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/faststats.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/mcp.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/models.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/pricing.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/providers/__init__.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/py.typed +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/storage.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/toolkit.py +0 -0
- {coding_agent_wrapper-0.1.1 → coding_agent_wrapper-0.1.4}/caw/traj_cli.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
28
|
-
|
|
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
|
|
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
|
|
349
|
-
if
|
|
373
|
+
if isinstance(block, TextBlock):
|
|
374
|
+
if display:
|
|
350
375
|
display.on_text(block)
|
|
351
|
-
|
|
376
|
+
log_text(self._logger, block)
|
|
377
|
+
elif isinstance(block, ThinkingBlock):
|
|
378
|
+
if display:
|
|
352
379
|
display.on_thinking(block)
|
|
353
|
-
|
|
380
|
+
log_thinking(self._logger, block)
|
|
381
|
+
elif isinstance(block, ToolUse):
|
|
382
|
+
if display:
|
|
354
383
|
display.on_tool_call(block)
|
|
355
|
-
|
|
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
|