baserun-cli 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.
@@ -0,0 +1,83 @@
1
+ """Bash Agent stream-json parser.
2
+
3
+ Format: `ccagent --output stream-json` (bash-agent C implementation).
4
+ All text/thinking is token-level delta only; no complete message blocks
5
+ and no result event with final text. The delta accumulator in cli.py
6
+ handles flushing complete events.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from ..base import ConnectorEvent, ConnectorEventType
11
+ from .base import BaseParser
12
+
13
+
14
+ class BashAgentParser(BaseParser):
15
+ schema = "bash_agent"
16
+
17
+ @staticmethod
18
+ def extract_session_id(obj: dict) -> str | None:
19
+ if obj.get("type") == "session_start":
20
+ return obj.get("session_id")
21
+ return None
22
+
23
+ @staticmethod
24
+ def parse(obj: dict) -> list[ConnectorEvent]:
25
+ out: list[ConnectorEvent] = []
26
+ t = obj.get("type", "")
27
+
28
+ if t in ("session_start", "user_input"):
29
+ return out
30
+
31
+ if t == "thinking":
32
+ out.append(ConnectorEvent(
33
+ ConnectorEventType.THINKING,
34
+ {"delta": obj.get("content", ""), "is_delta": True},
35
+ ))
36
+ elif t == "text":
37
+ out.append(ConnectorEvent(
38
+ ConnectorEventType.MESSAGE,
39
+ {"delta": obj.get("content", ""), "is_delta": True},
40
+ ))
41
+ elif t == "tool_call":
42
+ out.append(ConnectorEvent(
43
+ ConnectorEventType.TOOL_CALL,
44
+ {"tool": obj.get("name", ""), "args": obj.get("input", {}), "call_id": obj.get("id", "")},
45
+ ))
46
+ elif t == "tool_result":
47
+ out.append(ConnectorEvent(
48
+ ConnectorEventType.TOOL_RESULT,
49
+ {"call_id": obj.get("tool_use_id", ""), "result": obj.get("content", "")},
50
+ ))
51
+ elif t == "usage":
52
+ _in = obj.get("input_tokens", 0)
53
+ _out = obj.get("output_tokens", 0)
54
+ _cache_read = obj.get("cache_read_input_tokens", 0) or 0
55
+ _cache_creation = obj.get("cache_creation_input_tokens", 0) or 0
56
+ raw = {k: v for k, v in obj.items() if k != "type"}
57
+ out.append(ConnectorEvent(
58
+ ConnectorEventType.USAGE,
59
+ {
60
+ "input": _in,
61
+ "output": _out,
62
+ "cache_read_input_tokens": _cache_read,
63
+ "cache_creation_input_tokens": _cache_creation,
64
+ "total": _in + _out + _cache_read + _cache_creation,
65
+ "raw": raw,
66
+ },
67
+ ))
68
+ elif t == "error":
69
+ out.append(ConnectorEvent(
70
+ ConnectorEventType.ERROR,
71
+ {"message": obj.get("message", str(obj))},
72
+ ))
73
+ elif t == "stop":
74
+ if obj.get("reason") == "end_turn":
75
+ out.append(ConnectorEvent(
76
+ ConnectorEventType.FINAL,
77
+ {"text": "", "session_id": None},
78
+ ))
79
+ # tool_use → non-terminal, skip
80
+ else:
81
+ out.append(BaseParser.catch_all(f"bash_agent:unhandled:{t}", obj))
82
+
83
+ return out
@@ -0,0 +1,97 @@
1
+ """Claude Code stream-json parser.
2
+
3
+ Format: `claude -p --output-format stream-json --verbose --include-partial-messages`
4
+ --verbose outputs stream_event (token-level delta) + complete assistant messages.
5
+ text/thinking arrive as stream_event deltas (per-token); tool_use arrives in
6
+ assistant blocks (with full args).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from ..base import ConnectorEvent, ConnectorEventType
11
+ from .base import BaseParser
12
+
13
+
14
+ class ClaudeParser(BaseParser):
15
+ schema = "claude"
16
+
17
+ @staticmethod
18
+ def extract_session_id(obj: dict) -> str | None:
19
+ if obj.get("type") == "system" and obj.get("subtype") == "init":
20
+ return obj.get("session_id")
21
+ return None
22
+
23
+ @staticmethod
24
+ def parse(obj: dict) -> list[ConnectorEvent]:
25
+ out: list[ConnectorEvent] = []
26
+ t = obj.get("type")
27
+
28
+ if t == "stream_event":
29
+ event = obj.get("event", {})
30
+ et = event.get("type")
31
+ if et == "content_block_delta":
32
+ delta = event.get("delta", {})
33
+ dt = delta.get("type")
34
+ if dt == "text_delta":
35
+ out.append(ConnectorEvent(
36
+ ConnectorEventType.MESSAGE,
37
+ {"delta": delta.get("text", ""), "is_delta": True},
38
+ ))
39
+ elif dt == "thinking_delta":
40
+ out.append(ConnectorEvent(
41
+ ConnectorEventType.THINKING,
42
+ {"delta": delta.get("thinking", ""), "is_delta": True},
43
+ ))
44
+ # input_json_delta (tool args delta) — partial JSON, skip
45
+ elif t == "assistant":
46
+ for block in obj.get("message", {}).get("content", []):
47
+ bt = block.get("type")
48
+ if bt == "thinking":
49
+ out.append(ConnectorEvent(
50
+ ConnectorEventType.THINKING,
51
+ {"delta": block.get("thinking", ""), "is_delta": False},
52
+ ))
53
+ elif bt == "text":
54
+ out.append(ConnectorEvent(
55
+ ConnectorEventType.MESSAGE,
56
+ {"delta": block.get("text", ""), "is_delta": False},
57
+ ))
58
+ elif bt == "tool_use":
59
+ out.append(ConnectorEvent(
60
+ ConnectorEventType.TOOL_CALL,
61
+ {"tool": block.get("name"), "args": block.get("input"), "call_id": block.get("id")},
62
+ ))
63
+ elif t == "user":
64
+ for block in obj.get("message", {}).get("content", []):
65
+ if block.get("type") == "tool_result":
66
+ out.append(ConnectorEvent(
67
+ ConnectorEventType.TOOL_RESULT,
68
+ {"call_id": block.get("tool_use_id"), "result": block.get("content")},
69
+ ))
70
+ elif t == "result":
71
+ usage = obj.get("usage", {}) or {}
72
+ if usage:
73
+ _in = usage.get("input_tokens", 0)
74
+ _out = usage.get("output_tokens", 0)
75
+ _cache_read = usage.get("cache_read_input_tokens", 0) or 0
76
+ _cache_creation = usage.get("cache_creation_input_tokens", 0) or 0
77
+ out.append(ConnectorEvent(
78
+ ConnectorEventType.USAGE,
79
+ {
80
+ "input": _in,
81
+ "output": _out,
82
+ "cache_read_input_tokens": _cache_read,
83
+ "cache_creation_input_tokens": _cache_creation,
84
+ "total": _in + _out + _cache_read + _cache_creation,
85
+ "raw": dict(usage),
86
+ },
87
+ ))
88
+ out.append(ConnectorEvent(
89
+ ConnectorEventType.FINAL,
90
+ {"text": obj.get("result", ""), "session_id": None},
91
+ ))
92
+ elif t == "system":
93
+ pass # system/init handled by extract_session_id
94
+ else:
95
+ out.append(BaseParser.catch_all(f"claude:unhandled:{t}", obj))
96
+
97
+ return out
@@ -0,0 +1,202 @@
1
+ """Codex CLI JSONL parser.
2
+
3
+ Format: `codex exec --json --full-auto`
4
+ Validated against real CLI output + binary analysis of codex-cli 0.125.0.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from ..base import ConnectorEvent, ConnectorEventType
9
+ from .base import BaseParser
10
+
11
+ # Item types that are pure lifecycle noise — skipped entirely.
12
+ _SKIP_ITEMS = frozenset({
13
+ "context_compaction",
14
+ "hook_prompt",
15
+ "user_message",
16
+ "entered_review_mode",
17
+ "exited_review_mode",
18
+ })
19
+
20
+
21
+ def _extract_plan_text(item: dict) -> str:
22
+ fragments = item.get("fragments") or []
23
+ parts = []
24
+ for frag in fragments:
25
+ if isinstance(frag, dict):
26
+ md = frag.get("plan_markdown") or frag.get("markdown") or ""
27
+ if md:
28
+ parts.append(md)
29
+ if not parts:
30
+ md = item.get("plan_markdown", "")
31
+ if md:
32
+ return md
33
+ import json as _json
34
+ return f"[plan] {_json.dumps(item, ensure_ascii=False)[:500]}"
35
+ return "\n".join(parts)
36
+
37
+
38
+ def _extract_reasoning_text(item: dict) -> str:
39
+ text = item.get("summary_text") or item.get("raw_content") or ""
40
+ if text:
41
+ return text
42
+ for key in ("summary", "text", "content"):
43
+ val = item.get(key)
44
+ if isinstance(val, str) and val:
45
+ return val
46
+ import json as _json
47
+ return f"[reasoning] {_json.dumps(item, ensure_ascii=False)[:500]}"
48
+
49
+
50
+ class CodexParser(BaseParser):
51
+ schema = "codex"
52
+
53
+ @staticmethod
54
+ def extract_session_id(obj: dict) -> str | None:
55
+ if obj.get("type") == "thread.started":
56
+ return obj.get("thread_id")
57
+ return None
58
+
59
+ @staticmethod
60
+ def parse(obj: dict) -> list[ConnectorEvent]:
61
+ out: list[ConnectorEvent] = []
62
+ t = obj.get("type", "")
63
+
64
+ if t in ("thread.started", "turn.started"):
65
+ return out
66
+
67
+ if t == "item.started":
68
+ return CodexParser._item_started(obj.get("item", {}))
69
+
70
+ if t == "item.completed":
71
+ return CodexParser._item_completed(obj.get("item", {}))
72
+
73
+ if t == "turn.completed":
74
+ usage = obj.get("usage", {}) or {}
75
+ if usage:
76
+ _in = usage.get("input_tokens", 0)
77
+ _cached = usage.get("cached_input_tokens", 0)
78
+ _out = usage.get("output_tokens", 0)
79
+ fresh_in = max(0, _in - _cached)
80
+ _total = _in + _out
81
+ out.append(ConnectorEvent(
82
+ ConnectorEventType.USAGE,
83
+ {
84
+ "input": fresh_in,
85
+ "output": _out,
86
+ "cached_input_tokens": _cached,
87
+ "total": _total,
88
+ "raw": dict(usage),
89
+ },
90
+ ))
91
+ out.append(ConnectorEvent(
92
+ ConnectorEventType.FINAL,
93
+ {"text": "", "session_id": None},
94
+ ))
95
+ return out
96
+
97
+ if t in ("error", "turn.failed"):
98
+ if t == "error":
99
+ msg = obj.get("message", "")
100
+ else:
101
+ err = obj.get("error", {})
102
+ msg = err.get("message", "") if isinstance(err, dict) else str(err)
103
+ out.append(ConnectorEvent(
104
+ ConnectorEventType.ERROR,
105
+ {"message": msg},
106
+ ))
107
+ if t == "turn.failed":
108
+ out.append(ConnectorEvent(
109
+ ConnectorEventType.FINAL,
110
+ {"text": "", "session_id": None},
111
+ ))
112
+ return out
113
+
114
+ out.append(BaseParser.catch_all(f"codex:unhandled:{t}", obj))
115
+ return out
116
+
117
+ @staticmethod
118
+ def _item_started(item: dict) -> list[ConnectorEvent]:
119
+ item_type = item.get("type")
120
+ item_id = item.get("id")
121
+
122
+ if item_type in _SKIP_ITEMS:
123
+ return []
124
+
125
+ tool_map = {
126
+ "command_execution": ("shell", {"command": item.get("command", "")}),
127
+ "file_change": ("file_change", {"changes": item.get("changes", [])}),
128
+ "web_search": ("web_search", {"queries": item.get("queries", [])}),
129
+ "image_generation": ("image_generation", item.get("input", {})),
130
+ "image_view": ("image_view", {"path": item.get("path", "")}),
131
+ "collab_agent_tool_call": ("collab_agent", {"prompt": item.get("prompt", "")}),
132
+ }
133
+ if item_type in tool_map:
134
+ name, args = tool_map[item_type]
135
+ return [ConnectorEvent(
136
+ ConnectorEventType.TOOL_CALL,
137
+ {"tool": name, "args": args, "call_id": item_id},
138
+ )]
139
+ if item_type in ("mcp_tool_call", "dynamic_tool_call"):
140
+ prefix = "mcp" if item_type == "mcp_tool_call" else "dynamic"
141
+ return [ConnectorEvent(
142
+ ConnectorEventType.TOOL_CALL,
143
+ {"tool": f"{prefix}:{item.get('name', 'unknown')}", "args": item.get("arguments"), "call_id": item_id},
144
+ )]
145
+ if item_type == "plan":
146
+ return [ConnectorEvent(
147
+ ConnectorEventType.THINKING,
148
+ {"delta": _extract_plan_text(item), "is_delta": False},
149
+ )]
150
+ if item_type == "reasoning":
151
+ return [ConnectorEvent(
152
+ ConnectorEventType.THINKING,
153
+ {"delta": _extract_reasoning_text(item), "is_delta": False},
154
+ )]
155
+
156
+ return [BaseParser.catch_all(f"codex:item.started:{item_type}", item)]
157
+
158
+ @staticmethod
159
+ def _item_completed(item: dict) -> list[ConnectorEvent]:
160
+ item_type = item.get("type")
161
+ item_id = item.get("id")
162
+
163
+ if item_type in _SKIP_ITEMS:
164
+ return []
165
+
166
+ if item_type == "agent_message":
167
+ return [ConnectorEvent(
168
+ ConnectorEventType.MESSAGE,
169
+ {"delta": item.get("text", ""), "is_delta": False},
170
+ )]
171
+
172
+ # all tool-like completions → TOOL_RESULT
173
+ tool_result_types = {
174
+ "command_execution", "file_change", "web_search",
175
+ "mcp_tool_call", "dynamic_tool_call",
176
+ "collab_agent_tool_call", "image_generation", "image_view",
177
+ }
178
+ if item_type in tool_result_types:
179
+ result_val = (
180
+ item.get("aggregated_output")
181
+ or item.get("output")
182
+ or item.get("result")
183
+ or item.get("changes")
184
+ or item
185
+ )
186
+ return [ConnectorEvent(
187
+ ConnectorEventType.TOOL_RESULT,
188
+ {"call_id": item_id, "result": result_val},
189
+ )]
190
+
191
+ if item_type == "plan":
192
+ return [ConnectorEvent(
193
+ ConnectorEventType.THINKING,
194
+ {"delta": _extract_plan_text(item), "is_delta": False},
195
+ )]
196
+ if item_type == "reasoning":
197
+ return [ConnectorEvent(
198
+ ConnectorEventType.THINKING,
199
+ {"delta": _extract_reasoning_text(item), "is_delta": False},
200
+ )]
201
+
202
+ return [BaseParser.catch_all(f"codex:item.completed:{item_type}", item)]