pysolated 0.1.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.
- pysolated/__init__.py +177 -0
- pysolated/agents/__init__.py +45 -0
- pysolated/agents/_parsing.py +55 -0
- pysolated/agents/_registry.py +85 -0
- pysolated/agents/claude_code.py +161 -0
- pysolated/agents/codex.py +193 -0
- pysolated/cli.py +672 -0
- pysolated/completion.py +28 -0
- pysolated/core.py +268 -0
- pysolated/display.py +109 -0
- pysolated/errors.py +120 -0
- pysolated/init.py +361 -0
- pysolated/orchestrator.py +805 -0
- pysolated/prompts.py +206 -0
- pysolated/py.typed +0 -0
- pysolated/sandboxes/__init__.py +69 -0
- pysolated/sandboxes/_images.py +28 -0
- pysolated/sandboxes/_mounts.py +84 -0
- pysolated/sandboxes/_streaming.py +94 -0
- pysolated/sandboxes/docker.py +330 -0
- pysolated/sandboxes/no_sandbox.py +77 -0
- pysolated/sandboxes/podman.py +279 -0
- pysolated/structured_output.py +235 -0
- pysolated/worktrees.py +504 -0
- pysolated-0.1.0.dist-info/METADATA +706 -0
- pysolated-0.1.0.dist-info/RECORD +29 -0
- pysolated-0.1.0.dist-info/WHEEL +4 -0
- pysolated-0.1.0.dist-info/entry_points.txt +2 -0
- pysolated-0.1.0.dist-info/licenses/LICENSE +21 -0
pysolated/__init__.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""pysolated — orchestrate AI coding agents inside sandboxes via `run()`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .agents import (
|
|
6
|
+
ClaudeCode,
|
|
7
|
+
Codex,
|
|
8
|
+
CodexEffort,
|
|
9
|
+
PermissionMode,
|
|
10
|
+
claude_code,
|
|
11
|
+
codex,
|
|
12
|
+
parse_codex_session_usage,
|
|
13
|
+
parse_codex_stream_line,
|
|
14
|
+
parse_session_usage,
|
|
15
|
+
parse_stream_line,
|
|
16
|
+
)
|
|
17
|
+
from .completion import match_completion_signal
|
|
18
|
+
from .core import (
|
|
19
|
+
AgentCommandOptions,
|
|
20
|
+
AgentProvider,
|
|
21
|
+
Command,
|
|
22
|
+
Display,
|
|
23
|
+
ExecResult,
|
|
24
|
+
ResultEvent,
|
|
25
|
+
RunResult,
|
|
26
|
+
Sandbox,
|
|
27
|
+
SandboxProvider,
|
|
28
|
+
SessionIdEvent,
|
|
29
|
+
Severity,
|
|
30
|
+
StreamEvent,
|
|
31
|
+
TextEvent,
|
|
32
|
+
ToolCallEvent,
|
|
33
|
+
Usage,
|
|
34
|
+
)
|
|
35
|
+
from .display import FileDisplay, TerminalDisplay
|
|
36
|
+
from .errors import (
|
|
37
|
+
AgentExecutionError,
|
|
38
|
+
BranchAlreadyCheckedOutError,
|
|
39
|
+
IdleTimeoutError,
|
|
40
|
+
MergeConflictError,
|
|
41
|
+
PysolatedError,
|
|
42
|
+
)
|
|
43
|
+
from .orchestrator import (
|
|
44
|
+
DEFAULT_COMPLETION_SIGNAL,
|
|
45
|
+
DEFAULT_COMPLETION_TIMEOUT_SECONDS,
|
|
46
|
+
DEFAULT_IDLE_TIMEOUT_SECONDS,
|
|
47
|
+
DEFAULT_IDLE_WARNING_INTERVAL_SECONDS,
|
|
48
|
+
run,
|
|
49
|
+
)
|
|
50
|
+
from .prompts import (
|
|
51
|
+
PromptArgumentError,
|
|
52
|
+
PromptError,
|
|
53
|
+
PromptExecutor,
|
|
54
|
+
PromptExpansionError,
|
|
55
|
+
expand_shell_expressions,
|
|
56
|
+
resolve_prompt,
|
|
57
|
+
substitute_arguments,
|
|
58
|
+
)
|
|
59
|
+
from .sandboxes import (
|
|
60
|
+
Docker,
|
|
61
|
+
DockerHandle,
|
|
62
|
+
DockerImageNotFoundError,
|
|
63
|
+
DockerImageUidMismatchError,
|
|
64
|
+
DockerLaunchError,
|
|
65
|
+
Mount,
|
|
66
|
+
NoSandbox,
|
|
67
|
+
NoSandboxHandle,
|
|
68
|
+
Podman,
|
|
69
|
+
PodmanHandle,
|
|
70
|
+
PodmanImageNotFoundError,
|
|
71
|
+
PodmanLaunchError,
|
|
72
|
+
docker,
|
|
73
|
+
no_sandbox,
|
|
74
|
+
podman,
|
|
75
|
+
)
|
|
76
|
+
from .structured_output import (
|
|
77
|
+
Output,
|
|
78
|
+
OutputDefinition,
|
|
79
|
+
OutputObject,
|
|
80
|
+
OutputString,
|
|
81
|
+
StructuredOutputError,
|
|
82
|
+
extract_structured_output,
|
|
83
|
+
)
|
|
84
|
+
from .worktrees import (
|
|
85
|
+
BranchStrategy,
|
|
86
|
+
FinalizedRun,
|
|
87
|
+
HeadStrategy,
|
|
88
|
+
MergeToHeadStrategy,
|
|
89
|
+
NamedBranchStrategy,
|
|
90
|
+
PreparedRun,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
__all__ = [
|
|
94
|
+
# Entry point
|
|
95
|
+
"run",
|
|
96
|
+
# Providers
|
|
97
|
+
"claude_code",
|
|
98
|
+
"ClaudeCode",
|
|
99
|
+
"codex",
|
|
100
|
+
"Codex",
|
|
101
|
+
"CodexEffort",
|
|
102
|
+
"no_sandbox",
|
|
103
|
+
"NoSandbox",
|
|
104
|
+
"NoSandboxHandle",
|
|
105
|
+
"podman",
|
|
106
|
+
"Podman",
|
|
107
|
+
"PodmanHandle",
|
|
108
|
+
"docker",
|
|
109
|
+
"Docker",
|
|
110
|
+
"DockerHandle",
|
|
111
|
+
"Mount",
|
|
112
|
+
"PermissionMode",
|
|
113
|
+
# Display
|
|
114
|
+
"TerminalDisplay",
|
|
115
|
+
"FileDisplay",
|
|
116
|
+
# Seams (Protocols)
|
|
117
|
+
"AgentProvider",
|
|
118
|
+
"SandboxProvider",
|
|
119
|
+
"Sandbox",
|
|
120
|
+
"Display",
|
|
121
|
+
# Pure parsers / matchers
|
|
122
|
+
"parse_stream_line",
|
|
123
|
+
"parse_session_usage",
|
|
124
|
+
"parse_codex_stream_line",
|
|
125
|
+
"parse_codex_session_usage",
|
|
126
|
+
"match_completion_signal",
|
|
127
|
+
# Prompt pipeline
|
|
128
|
+
"resolve_prompt",
|
|
129
|
+
"substitute_arguments",
|
|
130
|
+
"expand_shell_expressions",
|
|
131
|
+
"PromptExecutor",
|
|
132
|
+
# Structured output
|
|
133
|
+
"Output",
|
|
134
|
+
"OutputDefinition",
|
|
135
|
+
"OutputObject",
|
|
136
|
+
"OutputString",
|
|
137
|
+
"extract_structured_output",
|
|
138
|
+
# Defaults
|
|
139
|
+
"DEFAULT_COMPLETION_SIGNAL",
|
|
140
|
+
"DEFAULT_IDLE_TIMEOUT_SECONDS",
|
|
141
|
+
"DEFAULT_COMPLETION_TIMEOUT_SECONDS",
|
|
142
|
+
"DEFAULT_IDLE_WARNING_INTERVAL_SECONDS",
|
|
143
|
+
# Branch strategies
|
|
144
|
+
"BranchStrategy",
|
|
145
|
+
"HeadStrategy",
|
|
146
|
+
"MergeToHeadStrategy",
|
|
147
|
+
"NamedBranchStrategy",
|
|
148
|
+
"PreparedRun",
|
|
149
|
+
"FinalizedRun",
|
|
150
|
+
# Value types
|
|
151
|
+
"RunResult",
|
|
152
|
+
"Usage",
|
|
153
|
+
"Command",
|
|
154
|
+
"ExecResult",
|
|
155
|
+
"AgentCommandOptions",
|
|
156
|
+
"StreamEvent",
|
|
157
|
+
"TextEvent",
|
|
158
|
+
"ToolCallEvent",
|
|
159
|
+
"SessionIdEvent",
|
|
160
|
+
"ResultEvent",
|
|
161
|
+
"Severity",
|
|
162
|
+
# Errors
|
|
163
|
+
"PysolatedError",
|
|
164
|
+
"AgentExecutionError",
|
|
165
|
+
"BranchAlreadyCheckedOutError",
|
|
166
|
+
"IdleTimeoutError",
|
|
167
|
+
"MergeConflictError",
|
|
168
|
+
"PromptError",
|
|
169
|
+
"PromptArgumentError",
|
|
170
|
+
"PromptExpansionError",
|
|
171
|
+
"StructuredOutputError",
|
|
172
|
+
"PodmanImageNotFoundError",
|
|
173
|
+
"PodmanLaunchError",
|
|
174
|
+
"DockerImageNotFoundError",
|
|
175
|
+
"DockerImageUidMismatchError",
|
|
176
|
+
"DockerLaunchError",
|
|
177
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Agent providers — command building and stream parsing.
|
|
2
|
+
|
|
3
|
+
v1 ships one provider, `claude_code`. The stream parser and usage parser are
|
|
4
|
+
pure module-level functions (the provider delegates to them) so they can be
|
|
5
|
+
table-tested directly without constructing a provider.
|
|
6
|
+
|
|
7
|
+
The package layout mirrors `sandboxes/` (see issue #24):
|
|
8
|
+
|
|
9
|
+
- `claude_code.py` — the Claude Code provider plus its pure parsers.
|
|
10
|
+
- `_parsing.py` — shared stream-parsing helpers (the tool-input allowlist and
|
|
11
|
+
the assistant-content block parser) that Claude — and the later Copilot
|
|
12
|
+
provider — both use.
|
|
13
|
+
- `_registry.py` — empty scaffold for the registry + `build_agent` to land in
|
|
14
|
+
a follow-up slice (issue #32).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from .claude_code import (
|
|
20
|
+
ClaudeCode,
|
|
21
|
+
PermissionMode,
|
|
22
|
+
claude_code,
|
|
23
|
+
parse_session_usage,
|
|
24
|
+
parse_stream_line,
|
|
25
|
+
)
|
|
26
|
+
from .codex import (
|
|
27
|
+
Codex,
|
|
28
|
+
CodexEffort,
|
|
29
|
+
codex,
|
|
30
|
+
parse_codex_session_usage,
|
|
31
|
+
parse_codex_stream_line,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"ClaudeCode",
|
|
36
|
+
"Codex",
|
|
37
|
+
"CodexEffort",
|
|
38
|
+
"PermissionMode",
|
|
39
|
+
"claude_code",
|
|
40
|
+
"codex",
|
|
41
|
+
"parse_codex_session_usage",
|
|
42
|
+
"parse_codex_stream_line",
|
|
43
|
+
"parse_session_usage",
|
|
44
|
+
"parse_stream_line",
|
|
45
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Shared stream-parsing helpers used by multiple agent providers.
|
|
2
|
+
|
|
3
|
+
Claude Code — and the later Copilot provider — both emit stream-json with an
|
|
4
|
+
`assistant` content-block shape; this module centralises that parsing and the
|
|
5
|
+
allowlist of tool names whose input fields we are willing to surface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ..core import StreamEvent, TextEvent, ToolCallEvent
|
|
11
|
+
|
|
12
|
+
# Allowlisted tools, mapped to the input field carrying the display arg.
|
|
13
|
+
# Anything not listed here is dropped — we never surface arbitrary tool input.
|
|
14
|
+
TOOL_ARG_FIELDS: dict[str, str] = {
|
|
15
|
+
"Bash": "command",
|
|
16
|
+
"WebSearch": "query",
|
|
17
|
+
"WebFetch": "url",
|
|
18
|
+
"Agent": "description",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_assistant_content_blocks(content: list[object]) -> list[StreamEvent]:
|
|
23
|
+
"""Decode an `assistant` message's `content` blocks into stream events.
|
|
24
|
+
|
|
25
|
+
Pure. Text blocks concatenate into `TextEvent`s; allowlisted `tool_use`
|
|
26
|
+
blocks become `ToolCallEvent`s. Pending text is flushed before each tool
|
|
27
|
+
call so events stay in source order. Unknown / wrong-typed blocks are
|
|
28
|
+
silently skipped.
|
|
29
|
+
"""
|
|
30
|
+
events: list[StreamEvent] = []
|
|
31
|
+
texts: list[str] = []
|
|
32
|
+
for block in content:
|
|
33
|
+
if not isinstance(block, dict):
|
|
34
|
+
continue
|
|
35
|
+
block_type = block.get("type")
|
|
36
|
+
if block_type == "text" and isinstance(block.get("text"), str):
|
|
37
|
+
texts.append(block["text"])
|
|
38
|
+
elif (
|
|
39
|
+
block_type == "tool_use"
|
|
40
|
+
and isinstance(block.get("name"), str)
|
|
41
|
+
and isinstance(block.get("input"), dict)
|
|
42
|
+
):
|
|
43
|
+
arg_field = TOOL_ARG_FIELDS.get(block["name"])
|
|
44
|
+
if arg_field is None:
|
|
45
|
+
continue # not allowlisted
|
|
46
|
+
arg_value = block["input"].get(arg_field)
|
|
47
|
+
if not isinstance(arg_value, str):
|
|
48
|
+
continue # missing / wrong-typed arg field
|
|
49
|
+
if texts:
|
|
50
|
+
events.append(TextEvent(text="".join(texts)))
|
|
51
|
+
texts = []
|
|
52
|
+
events.append(ToolCallEvent(name=block["name"], args=arg_value))
|
|
53
|
+
if texts:
|
|
54
|
+
events.append(TextEvent(text="".join(texts)))
|
|
55
|
+
return events
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Agent registry + CLI-builder.
|
|
2
|
+
|
|
3
|
+
A name → factory map keyed on each provider's ``.name``, plus ``build_agent``
|
|
4
|
+
— the CLI's one resolver. Library callers construct providers directly via
|
|
5
|
+
their typed factories and never touch this module; it exists only for the
|
|
6
|
+
string-name boundary the CLI (and later init/config) sits behind.
|
|
7
|
+
|
|
8
|
+
``build_agent`` applies provider-specific option handling here so the CLI
|
|
9
|
+
grows no ``if name == …`` ladder as agents are added. Argument errors raise
|
|
10
|
+
``ValueError``; the CLI translates that into ``typer.Exit(2)``, consistent
|
|
11
|
+
with the existing ``--prompt`` / ``--prompt-arg`` rejections.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
from ..core import AgentProvider
|
|
19
|
+
from .claude_code import PermissionMode, claude_code
|
|
20
|
+
from .codex import codex
|
|
21
|
+
|
|
22
|
+
_CLAUDE_CODE_DEFAULT_MODEL = "claude-opus-4-7"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_claude_code(
|
|
26
|
+
*,
|
|
27
|
+
model: str | None,
|
|
28
|
+
effort: str | None,
|
|
29
|
+
permission_mode: str | None,
|
|
30
|
+
) -> AgentProvider:
|
|
31
|
+
if effort is not None:
|
|
32
|
+
raise ValueError("--effort is not supported by the claude-code agent.")
|
|
33
|
+
resolved_model = model if model is not None else _CLAUDE_CODE_DEFAULT_MODEL
|
|
34
|
+
return claude_code(
|
|
35
|
+
resolved_model,
|
|
36
|
+
permission_mode=permission_mode, # type: ignore[arg-type]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _build_codex(
|
|
41
|
+
*,
|
|
42
|
+
model: str | None,
|
|
43
|
+
effort: str | None,
|
|
44
|
+
permission_mode: str | None,
|
|
45
|
+
) -> AgentProvider:
|
|
46
|
+
if permission_mode is not None:
|
|
47
|
+
raise ValueError("--permission-mode is not supported by the codex agent.")
|
|
48
|
+
if model is None:
|
|
49
|
+
raise ValueError("--model is required for the codex agent.")
|
|
50
|
+
return codex(model, effort=effort) # type: ignore[arg-type]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Keyed on the provider's ``.name``. Each entry resolves CLI options into a
|
|
54
|
+
# concrete provider; provider-specific rejections live here, not in the CLI.
|
|
55
|
+
_REGISTRY: dict[
|
|
56
|
+
str,
|
|
57
|
+
Callable[..., AgentProvider],
|
|
58
|
+
] = {
|
|
59
|
+
"claude-code": _build_claude_code,
|
|
60
|
+
"codex": _build_codex,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def agent_names() -> list[str]:
|
|
65
|
+
"""The registered agent names, in insertion order — for error messages."""
|
|
66
|
+
return list(_REGISTRY)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_agent(
|
|
70
|
+
name: str,
|
|
71
|
+
*,
|
|
72
|
+
model: str | None,
|
|
73
|
+
effort: str | None = None,
|
|
74
|
+
permission_mode: PermissionMode | None = None,
|
|
75
|
+
) -> AgentProvider:
|
|
76
|
+
"""Resolve a CLI agent name to a configured ``AgentProvider``.
|
|
77
|
+
|
|
78
|
+
Raises ``ValueError`` for an unknown name, a model that's required but
|
|
79
|
+
missing for the chosen agent, or a flag the chosen agent does not accept.
|
|
80
|
+
"""
|
|
81
|
+
factory = _REGISTRY.get(name)
|
|
82
|
+
if factory is None:
|
|
83
|
+
valid = ", ".join(agent_names())
|
|
84
|
+
raise ValueError(f"Unknown --agent {name!r}. Valid agents: {valid}.")
|
|
85
|
+
return factory(model=model, effort=effort, permission_mode=permission_mode)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""The Claude Code agent provider.
|
|
2
|
+
|
|
3
|
+
The stream parser and usage parser are pure module-level functions (the
|
|
4
|
+
provider delegates to them) so they can be table-tested directly without
|
|
5
|
+
constructing a provider.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from ..core import (
|
|
15
|
+
AgentCommandOptions,
|
|
16
|
+
Command,
|
|
17
|
+
SessionIdEvent,
|
|
18
|
+
StreamEvent,
|
|
19
|
+
Usage,
|
|
20
|
+
)
|
|
21
|
+
from ._parsing import parse_assistant_content_blocks
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_stream_line(line: str) -> list[StreamEvent]:
|
|
25
|
+
"""Decode one Claude `stream-json` JSONL line into zero or more events.
|
|
26
|
+
|
|
27
|
+
Pure. Non-JSON / unknown / malformed lines yield no events.
|
|
28
|
+
|
|
29
|
+
- `assistant` lines yield `text` events (content text blocks concatenated)
|
|
30
|
+
and `tool_call` events for allowlisted tools, in source order.
|
|
31
|
+
- `system`/`init` lines yield a single `session_id` event.
|
|
32
|
+
"""
|
|
33
|
+
if not line.startswith("{"):
|
|
34
|
+
return []
|
|
35
|
+
try:
|
|
36
|
+
obj = json.loads(line)
|
|
37
|
+
except (json.JSONDecodeError, ValueError):
|
|
38
|
+
return []
|
|
39
|
+
if not isinstance(obj, dict):
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
message = obj.get("message")
|
|
43
|
+
if (
|
|
44
|
+
obj.get("type") == "assistant"
|
|
45
|
+
and isinstance(message, dict)
|
|
46
|
+
and isinstance(message.get("content"), list)
|
|
47
|
+
):
|
|
48
|
+
return parse_assistant_content_blocks(message["content"])
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
obj.get("type") == "system"
|
|
52
|
+
and obj.get("subtype") == "init"
|
|
53
|
+
and isinstance(obj.get("session_id"), str)
|
|
54
|
+
):
|
|
55
|
+
return [SessionIdEvent(session_id=obj["session_id"])]
|
|
56
|
+
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_USAGE_FIELDS = (
|
|
61
|
+
"input_tokens",
|
|
62
|
+
"cache_creation_input_tokens",
|
|
63
|
+
"cache_read_input_tokens",
|
|
64
|
+
"output_tokens",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_session_usage(content: str) -> Usage | None:
|
|
69
|
+
"""Extract the session's authoritative token usage from streamed content.
|
|
70
|
+
|
|
71
|
+
Pure. Scans the accumulated stream-json content from the end and returns the
|
|
72
|
+
first complete usage block it finds, or `None` when no usage was emitted.
|
|
73
|
+
|
|
74
|
+
The authoritative totals live on the terminal `result` line: an `assistant`
|
|
75
|
+
line's usage is the `message_start` snapshot, captured before the response
|
|
76
|
+
streamed, so its `output_tokens` is only a partial count (often 1). Scanning
|
|
77
|
+
from the end reaches the `result` line first, so its totals win; an
|
|
78
|
+
`assistant` line is the fallback for truncated streams that never reached a
|
|
79
|
+
`result`. The usage block sits at the top level on a `result` line and under
|
|
80
|
+
`message` on an `assistant` line.
|
|
81
|
+
"""
|
|
82
|
+
for line in reversed(content.split("\n")):
|
|
83
|
+
if not line.startswith("{"):
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
obj = json.loads(line)
|
|
87
|
+
except (json.JSONDecodeError, ValueError):
|
|
88
|
+
continue
|
|
89
|
+
if not isinstance(obj, dict):
|
|
90
|
+
continue
|
|
91
|
+
if obj.get("type") == "result":
|
|
92
|
+
usage = obj.get("usage")
|
|
93
|
+
elif obj.get("type") == "assistant":
|
|
94
|
+
message = obj.get("message")
|
|
95
|
+
usage = message.get("usage") if isinstance(message, dict) else None
|
|
96
|
+
else:
|
|
97
|
+
continue
|
|
98
|
+
if not isinstance(usage, dict):
|
|
99
|
+
continue
|
|
100
|
+
if all(isinstance(usage.get(name), int) for name in _USAGE_FIELDS):
|
|
101
|
+
return Usage(
|
|
102
|
+
input_tokens=usage["input_tokens"],
|
|
103
|
+
cache_creation_input_tokens=usage["cache_creation_input_tokens"],
|
|
104
|
+
cache_read_input_tokens=usage["cache_read_input_tokens"],
|
|
105
|
+
output_tokens=usage["output_tokens"],
|
|
106
|
+
)
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Maps directly to Claude's `--permission-mode` flag. Mutually exclusive with
|
|
111
|
+
# `--dangerously-skip-permissions` on Claude's CLI.
|
|
112
|
+
PermissionMode = Literal[
|
|
113
|
+
"default", "acceptEdits", "plan", "auto", "dontAsk", "bypassPermissions"
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class ClaudeCode:
|
|
119
|
+
"""The Claude Code agent provider.
|
|
120
|
+
|
|
121
|
+
Build it via `claude_code(...)` rather than constructing directly.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
model: str
|
|
125
|
+
permission_mode: PermissionMode | None = None
|
|
126
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
127
|
+
name: str = "claude-code"
|
|
128
|
+
|
|
129
|
+
def build_command(self, options: AgentCommandOptions) -> Command:
|
|
130
|
+
"""Build the print-mode argv, with the prompt delivered on stdin.
|
|
131
|
+
|
|
132
|
+
`permission_mode` and `--dangerously-skip-permissions` are mutually
|
|
133
|
+
exclusive: an explicit mode replaces the default skip-permissions flag.
|
|
134
|
+
"""
|
|
135
|
+
argv = ["claude", "--print", "--verbose"]
|
|
136
|
+
if self.permission_mode is not None:
|
|
137
|
+
argv += ["--permission-mode", self.permission_mode]
|
|
138
|
+
else:
|
|
139
|
+
argv.append("--dangerously-skip-permissions")
|
|
140
|
+
argv += ["--output-format", "stream-json", "--model", self.model, "-p", "-"]
|
|
141
|
+
return Command(argv=argv, stdin=options.prompt)
|
|
142
|
+
|
|
143
|
+
def parse_stream_line(self, line: str) -> list[StreamEvent]:
|
|
144
|
+
return parse_stream_line(line)
|
|
145
|
+
|
|
146
|
+
def parse_session_usage(self, content: str) -> Usage | None:
|
|
147
|
+
return parse_session_usage(content)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def claude_code(
|
|
151
|
+
model: str,
|
|
152
|
+
*,
|
|
153
|
+
permission_mode: PermissionMode | None = None,
|
|
154
|
+
env: dict[str, str] | None = None,
|
|
155
|
+
) -> ClaudeCode:
|
|
156
|
+
"""Create a Claude Code agent provider.
|
|
157
|
+
|
|
158
|
+
`model` selects the Claude model. `permission_mode`, when given, replaces the
|
|
159
|
+
default `--dangerously-skip-permissions` flag (the two are mutually exclusive).
|
|
160
|
+
"""
|
|
161
|
+
return ClaudeCode(model=model, permission_mode=permission_mode, env=env or {})
|