agentabi 0.2.2__tar.gz → 0.3.0__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.
- {agentabi-0.2.2/src/agentabi.egg-info → agentabi-0.3.0}/PKG-INFO +3 -2
- {agentabi-0.2.2 → agentabi-0.3.0}/README.md +2 -1
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/__init__.py +1 -1
- agentabi-0.3.0/src/agentabi/providers/pi_native.py +347 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/registry.py +2 -0
- {agentabi-0.2.2 → agentabi-0.3.0/src/agentabi.egg-info}/PKG-INFO +3 -2
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi.egg-info/SOURCES.txt +1 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/LICENSE +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/pyproject.toml +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/setup.cfg +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/auto_detect.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/middleware/__init__.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/middleware/logging.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/middleware/timeout.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/middleware/usage.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/__init__.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/base.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/claude_native.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/claude_sdk.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/codex_native.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/codex_sdk.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/gemini_native.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/gemini_sdk.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/providers/opencode_native.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/py.typed +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/session.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/types/__init__.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/types/ir/__init__.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/types/ir/capabilities.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/types/ir/events.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/types/ir/helpers.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/types/ir/permissions.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/types/ir/session.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/types/ir/task.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi/types/ir/type_guards.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi.egg-info/dependency_links.txt +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi.egg-info/requires.txt +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/src/agentabi.egg-info/top_level.txt +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/tests/test_ir_types.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/tests/test_middleware.py +0 -0
- {agentabi-0.2.2 → agentabi-0.3.0}/tests/test_session.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentabi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Unified async Python API for driving coding agent CLIs — Claude Code, Codex, Gemini CLI, and more
|
|
5
5
|
Author-email: Peng Ding <pding.dp@foxmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -67,6 +67,7 @@ One interface. Any coding agent.
|
|
|
67
67
|
| [Codex](https://github.com/openai/codex) | OpenAI | Implemented |
|
|
68
68
|
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Google | Implemented |
|
|
69
69
|
| [OpenCode](https://opencode.ai) | Open Source | Implemented |
|
|
70
|
+
| [Pi](https://pi.dev/) | Open Source | Implemented |
|
|
70
71
|
|
|
71
72
|
## Installation
|
|
72
73
|
|
|
@@ -83,7 +84,7 @@ pip install agentabi[gemini] # Gemini CLI SDK support
|
|
|
83
84
|
pip install agentabi[all] # All optional SDKs
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
> **Note:** Each agent's CLI must be installed separately (e.g., `claude`, `codex`, `gemini`, `opencode`).
|
|
87
|
+
> **Note:** Each agent's CLI must be installed separately (e.g., `claude`, `codex`, `gemini`, `opencode`, `pi`).
|
|
87
88
|
|
|
88
89
|
## Quick Start
|
|
89
90
|
|
|
@@ -21,6 +21,7 @@ One interface. Any coding agent.
|
|
|
21
21
|
| [Codex](https://github.com/openai/codex) | OpenAI | Implemented |
|
|
22
22
|
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Google | Implemented |
|
|
23
23
|
| [OpenCode](https://opencode.ai) | Open Source | Implemented |
|
|
24
|
+
| [Pi](https://pi.dev/) | Open Source | Implemented |
|
|
24
25
|
|
|
25
26
|
## Installation
|
|
26
27
|
|
|
@@ -37,7 +38,7 @@ pip install agentabi[gemini] # Gemini CLI SDK support
|
|
|
37
38
|
pip install agentabi[all] # All optional SDKs
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
> **Note:** Each agent's CLI must be installed separately (e.g., `claude`, `codex`, `gemini`, `opencode`).
|
|
41
|
+
> **Note:** Each agent's CLI must be installed separately (e.g., `claude`, `codex`, `gemini`, `opencode`, `pi`).
|
|
41
42
|
|
|
42
43
|
## Quick Start
|
|
43
44
|
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentabi - Pi Coding Agent Native Provider
|
|
3
|
+
|
|
4
|
+
Native subprocess provider for Pi CLI.
|
|
5
|
+
Runs `pi --print --mode json <prompt>` and parses JSONL output.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from ..types.ir.capabilities import AgentCapabilities
|
|
18
|
+
from ..types.ir.events import (
|
|
19
|
+
IREvent,
|
|
20
|
+
MessageDeltaEvent,
|
|
21
|
+
MessageEndEvent,
|
|
22
|
+
MessageStartEvent,
|
|
23
|
+
SessionEndEvent,
|
|
24
|
+
SessionStartEvent,
|
|
25
|
+
ToolResultEvent,
|
|
26
|
+
ToolUseEvent,
|
|
27
|
+
UsageEvent,
|
|
28
|
+
UsageInfo,
|
|
29
|
+
)
|
|
30
|
+
from ..types.ir.session import SessionResult
|
|
31
|
+
from ..types.ir.task import TaskConfig
|
|
32
|
+
from .base import collect_subprocess_errors
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PiNativeProvider:
|
|
36
|
+
"""Native subprocess provider for Pi coding agent CLI.
|
|
37
|
+
|
|
38
|
+
Runs `pi --print --mode json <prompt>` as a subprocess
|
|
39
|
+
and parses JSONL events into IR events.
|
|
40
|
+
|
|
41
|
+
Pi CLI JSONL event types:
|
|
42
|
+
- session — session metadata (id, cwd)
|
|
43
|
+
- agent_start — agent begins processing (skipped, no IR mapping)
|
|
44
|
+
- turn_start — new LLM turn begins
|
|
45
|
+
- message_start — message begins (user messages skipped)
|
|
46
|
+
- message_update — streaming delta (text_delta mapped;
|
|
47
|
+
thinking_start/delta/end skipped, no IR type)
|
|
48
|
+
- message_end — message completed with full content + usage
|
|
49
|
+
- tool_execution_start — tool call begins
|
|
50
|
+
- tool_execution_end — tool call completed with result
|
|
51
|
+
- turn_end — turn ends with usage stats
|
|
52
|
+
- agent_end — agent finishes, contains full message history
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self._pending_text: list[str] = []
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def is_available() -> bool:
|
|
60
|
+
"""Check if `pi` CLI is available."""
|
|
61
|
+
return shutil.which("pi") is not None
|
|
62
|
+
|
|
63
|
+
def capabilities(self) -> AgentCapabilities:
|
|
64
|
+
"""Declare Pi capabilities."""
|
|
65
|
+
return {
|
|
66
|
+
"name": "Pi",
|
|
67
|
+
"agent_type": "pi",
|
|
68
|
+
"supports_streaming": True,
|
|
69
|
+
"supports_mcp": False,
|
|
70
|
+
"supports_session_resume": True,
|
|
71
|
+
"supports_system_prompt": True,
|
|
72
|
+
"supports_tool_filtering": True,
|
|
73
|
+
"supports_permissions": False,
|
|
74
|
+
"supports_multi_turn": True,
|
|
75
|
+
"transport": "subprocess",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async def stream(self, task: TaskConfig) -> AsyncIterator[IREvent]:
|
|
79
|
+
"""Run task via pi CLI and yield IR events."""
|
|
80
|
+
cmd = self._build_command(task)
|
|
81
|
+
merged_env = {**os.environ, **(task.get("env") or {})}
|
|
82
|
+
|
|
83
|
+
proc = await asyncio.create_subprocess_exec(
|
|
84
|
+
*cmd,
|
|
85
|
+
stdout=asyncio.subprocess.PIPE,
|
|
86
|
+
stderr=asyncio.subprocess.PIPE,
|
|
87
|
+
env=merged_env,
|
|
88
|
+
cwd=task.get("working_dir"),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
timed_out = False
|
|
92
|
+
timeout = task.get("timeout")
|
|
93
|
+
kill_task: asyncio.Task[None] | None = None
|
|
94
|
+
|
|
95
|
+
if timeout is not None and timeout > 0:
|
|
96
|
+
|
|
97
|
+
async def _kill_after(secs: float) -> None:
|
|
98
|
+
await asyncio.sleep(secs)
|
|
99
|
+
nonlocal timed_out
|
|
100
|
+
timed_out = True
|
|
101
|
+
proc.kill()
|
|
102
|
+
|
|
103
|
+
kill_task = asyncio.create_task(_kill_after(timeout))
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
assert proc.stdout is not None
|
|
107
|
+
async for line_bytes in proc.stdout:
|
|
108
|
+
line = line_bytes.decode().rstrip("\n").rstrip("\r")
|
|
109
|
+
if not line:
|
|
110
|
+
continue
|
|
111
|
+
try:
|
|
112
|
+
raw = json.loads(line)
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
continue
|
|
115
|
+
for event in self._parse_event(raw):
|
|
116
|
+
yield event
|
|
117
|
+
|
|
118
|
+
await proc.wait()
|
|
119
|
+
for err_event in await collect_subprocess_errors(
|
|
120
|
+
proc, timed_out=timed_out, timeout_seconds=timeout
|
|
121
|
+
):
|
|
122
|
+
yield err_event
|
|
123
|
+
finally:
|
|
124
|
+
if kill_task is not None:
|
|
125
|
+
kill_task.cancel()
|
|
126
|
+
if proc.returncode is None:
|
|
127
|
+
proc.terminate()
|
|
128
|
+
try:
|
|
129
|
+
await asyncio.wait_for(proc.wait(), timeout=5)
|
|
130
|
+
except asyncio.TimeoutError:
|
|
131
|
+
proc.kill()
|
|
132
|
+
await proc.wait()
|
|
133
|
+
|
|
134
|
+
async def run(self, task: TaskConfig) -> SessionResult:
|
|
135
|
+
"""Run task and return aggregated result."""
|
|
136
|
+
from .base import default_run
|
|
137
|
+
|
|
138
|
+
return await default_run(self, task)
|
|
139
|
+
|
|
140
|
+
# ========== Private: command building ==========
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _build_command(task: TaskConfig) -> list[str]:
|
|
144
|
+
"""Convert TaskConfig to pi CLI arguments."""
|
|
145
|
+
cmd = ["pi", "--print", "--mode", "json"]
|
|
146
|
+
|
|
147
|
+
if "model" in task:
|
|
148
|
+
cmd.extend(["--model", task["model"]])
|
|
149
|
+
|
|
150
|
+
if "system_prompt" in task:
|
|
151
|
+
cmd.extend(["--system-prompt", task["system_prompt"]])
|
|
152
|
+
if "append_system_prompt" in task:
|
|
153
|
+
cmd.extend(["--append-system-prompt", task["append_system_prompt"]])
|
|
154
|
+
|
|
155
|
+
if "session_id" in task and task.get("resume"):
|
|
156
|
+
cmd.extend(["--session", task["session_id"]])
|
|
157
|
+
|
|
158
|
+
if "allowed_tools" in task:
|
|
159
|
+
cmd.extend(["--tools", ",".join(task["allowed_tools"])])
|
|
160
|
+
elif task.get("permissions") and "allowed_tools" in task["permissions"]:
|
|
161
|
+
cmd.extend(["--tools", ",".join(task["permissions"]["allowed_tools"])])
|
|
162
|
+
|
|
163
|
+
if "disallowed_tools" in task:
|
|
164
|
+
cmd.extend(["--exclude-tools", ",".join(task["disallowed_tools"])])
|
|
165
|
+
elif task.get("permissions") and "disallowed_tools" in task["permissions"]:
|
|
166
|
+
cmd.extend(
|
|
167
|
+
[
|
|
168
|
+
"--exclude-tools",
|
|
169
|
+
",".join(task["permissions"]["disallowed_tools"]),
|
|
170
|
+
]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
cmd.append(task["prompt"])
|
|
174
|
+
return cmd
|
|
175
|
+
|
|
176
|
+
# ========== Private: event parsing ==========
|
|
177
|
+
|
|
178
|
+
def _parse_event(self, raw: dict[str, Any]) -> list[IREvent]:
|
|
179
|
+
"""Convert a Pi CLI JSONL event to IR events."""
|
|
180
|
+
event_type = raw.get("type")
|
|
181
|
+
|
|
182
|
+
if event_type == "session":
|
|
183
|
+
return self._handle_session(raw)
|
|
184
|
+
elif event_type == "turn_start":
|
|
185
|
+
return self._handle_turn_start()
|
|
186
|
+
elif event_type == "message_update":
|
|
187
|
+
return self._handle_message_update(raw)
|
|
188
|
+
elif event_type == "message_end":
|
|
189
|
+
return self._handle_message_end(raw)
|
|
190
|
+
elif event_type == "tool_execution_start":
|
|
191
|
+
return self._handle_tool_start(raw)
|
|
192
|
+
elif event_type == "tool_execution_end":
|
|
193
|
+
return self._handle_tool_end(raw)
|
|
194
|
+
elif event_type == "turn_end":
|
|
195
|
+
return self._handle_turn_end(raw)
|
|
196
|
+
elif event_type == "agent_end":
|
|
197
|
+
return self._handle_agent_end(raw)
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _handle_session(raw: dict[str, Any]) -> list[IREvent]:
|
|
202
|
+
"""Handle session event — emits SessionStartEvent."""
|
|
203
|
+
start: SessionStartEvent = {
|
|
204
|
+
"type": "session_start",
|
|
205
|
+
"session_id": raw.get("id", ""),
|
|
206
|
+
"agent": "pi",
|
|
207
|
+
}
|
|
208
|
+
cwd = raw.get("cwd")
|
|
209
|
+
if cwd:
|
|
210
|
+
start["working_dir"] = cwd
|
|
211
|
+
return [start]
|
|
212
|
+
|
|
213
|
+
def _handle_turn_start(self) -> list[IREvent]:
|
|
214
|
+
"""Handle turn_start — emits MessageStartEvent.
|
|
215
|
+
|
|
216
|
+
Also defensively clears pending text to prevent bleed from
|
|
217
|
+
a previous turn if message_end was missed.
|
|
218
|
+
"""
|
|
219
|
+
self._pending_text = []
|
|
220
|
+
msg_start: MessageStartEvent = {
|
|
221
|
+
"type": "message_start",
|
|
222
|
+
"role": "assistant",
|
|
223
|
+
}
|
|
224
|
+
return [msg_start]
|
|
225
|
+
|
|
226
|
+
def _handle_message_update(self, raw: dict[str, Any]) -> list[IREvent]:
|
|
227
|
+
"""Handle message_update — emits MessageDeltaEvent for text deltas."""
|
|
228
|
+
assistant_event = raw.get("assistantMessageEvent", {})
|
|
229
|
+
event_subtype = assistant_event.get("type", "")
|
|
230
|
+
|
|
231
|
+
if event_subtype == "text_delta":
|
|
232
|
+
delta_text = assistant_event.get("delta", "")
|
|
233
|
+
if delta_text:
|
|
234
|
+
self._pending_text.append(delta_text)
|
|
235
|
+
delta: MessageDeltaEvent = {
|
|
236
|
+
"type": "message_delta",
|
|
237
|
+
"text": delta_text,
|
|
238
|
+
}
|
|
239
|
+
return [delta]
|
|
240
|
+
# Skip thinking_start, thinking_delta, thinking_end, text_start, text_end
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
def _handle_message_end(self, raw: dict[str, Any]) -> list[IREvent]:
|
|
244
|
+
"""Handle message_end — emits MessageEndEvent with accumulated text."""
|
|
245
|
+
message = raw.get("message", {})
|
|
246
|
+
role = message.get("role", "")
|
|
247
|
+
|
|
248
|
+
# Skip user message_end events
|
|
249
|
+
if role != "assistant":
|
|
250
|
+
return []
|
|
251
|
+
|
|
252
|
+
# Extract full text from content blocks
|
|
253
|
+
content = message.get("content", [])
|
|
254
|
+
full_text = ""
|
|
255
|
+
for block in content:
|
|
256
|
+
if block.get("type") == "text":
|
|
257
|
+
full_text += block.get("text", "")
|
|
258
|
+
|
|
259
|
+
end: MessageEndEvent = {
|
|
260
|
+
"type": "message_end",
|
|
261
|
+
"stop_reason": message.get("stopReason", ""),
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Prefer full text from content blocks, fallback to pending
|
|
265
|
+
if full_text:
|
|
266
|
+
end["text"] = full_text
|
|
267
|
+
self._pending_text = []
|
|
268
|
+
elif self._pending_text:
|
|
269
|
+
end["text"] = "".join(self._pending_text)
|
|
270
|
+
self._pending_text = []
|
|
271
|
+
|
|
272
|
+
return [end]
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def _handle_tool_start(raw: dict[str, Any]) -> list[IREvent]:
|
|
276
|
+
"""Handle tool_execution_start — emits ToolUseEvent."""
|
|
277
|
+
tool_use: ToolUseEvent = {
|
|
278
|
+
"type": "tool_use",
|
|
279
|
+
"tool_use_id": raw.get("toolCallId", ""),
|
|
280
|
+
"tool_name": raw.get("toolName", ""),
|
|
281
|
+
"tool_input": raw.get("args", {}),
|
|
282
|
+
}
|
|
283
|
+
return [tool_use]
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def _handle_tool_end(raw: dict[str, Any]) -> list[IREvent]:
|
|
287
|
+
"""Handle tool_execution_end — emits ToolResultEvent."""
|
|
288
|
+
result = raw.get("result", {})
|
|
289
|
+
content_blocks = result.get("content", [])
|
|
290
|
+
content_text = ""
|
|
291
|
+
for block in content_blocks:
|
|
292
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
293
|
+
content_text += block.get("text", "")
|
|
294
|
+
|
|
295
|
+
tool_result: ToolResultEvent = {
|
|
296
|
+
"type": "tool_result",
|
|
297
|
+
"tool_use_id": raw.get("toolCallId", ""),
|
|
298
|
+
"content": content_text,
|
|
299
|
+
}
|
|
300
|
+
if raw.get("isError"):
|
|
301
|
+
tool_result["is_error"] = True
|
|
302
|
+
return [tool_result]
|
|
303
|
+
|
|
304
|
+
def _handle_turn_end(self, raw: dict[str, Any]) -> list[IREvent]:
|
|
305
|
+
"""Handle turn_end — emits UsageEvent with token counts and cost."""
|
|
306
|
+
results: list[IREvent] = []
|
|
307
|
+
message = raw.get("message", {})
|
|
308
|
+
|
|
309
|
+
# Extract usage
|
|
310
|
+
raw_usage = message.get("usage", {})
|
|
311
|
+
usage: UsageInfo = {}
|
|
312
|
+
if raw_usage.get("input"):
|
|
313
|
+
usage["input_tokens"] = raw_usage["input"]
|
|
314
|
+
if raw_usage.get("output"):
|
|
315
|
+
usage["output_tokens"] = raw_usage["output"]
|
|
316
|
+
total = raw_usage.get("totalTokens", 0)
|
|
317
|
+
if total:
|
|
318
|
+
usage["total_tokens"] = total
|
|
319
|
+
if raw_usage.get("cacheRead"):
|
|
320
|
+
usage["cache_read_tokens"] = raw_usage["cacheRead"]
|
|
321
|
+
if raw_usage.get("cacheWrite"):
|
|
322
|
+
usage["cache_creation_tokens"] = raw_usage["cacheWrite"]
|
|
323
|
+
|
|
324
|
+
usage_event: UsageEvent = {"type": "usage", "usage": usage}
|
|
325
|
+
|
|
326
|
+
# Extract cost
|
|
327
|
+
cost = raw_usage.get("cost", {})
|
|
328
|
+
total_cost = cost.get("total")
|
|
329
|
+
if total_cost:
|
|
330
|
+
usage_event["cost_usd"] = total_cost
|
|
331
|
+
|
|
332
|
+
# Extract model
|
|
333
|
+
model = message.get("model")
|
|
334
|
+
if model:
|
|
335
|
+
usage_event["model"] = model
|
|
336
|
+
|
|
337
|
+
results.append(usage_event)
|
|
338
|
+
return results
|
|
339
|
+
|
|
340
|
+
@staticmethod
|
|
341
|
+
def _handle_agent_end(raw: dict[str, Any]) -> list[IREvent]:
|
|
342
|
+
"""Handle agent_end — emits SessionEndEvent."""
|
|
343
|
+
end: SessionEndEvent = {"type": "session_end"}
|
|
344
|
+
return [end]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
__all__ = ["PiNativeProvider"]
|
|
@@ -35,12 +35,14 @@ def _build_provider_chain() -> dict[str, list[type[Provider]]]:
|
|
|
35
35
|
from .gemini_native import GeminiNativeProvider
|
|
36
36
|
from .gemini_sdk import GeminiSDKProvider
|
|
37
37
|
from .opencode_native import OpenCodeNativeProvider
|
|
38
|
+
from .pi_native import PiNativeProvider
|
|
38
39
|
|
|
39
40
|
return {
|
|
40
41
|
"claude_code": [ClaudeNativeProvider, ClaudeSDKProvider],
|
|
41
42
|
"codex": [CodexNativeProvider, CodexSDKProvider],
|
|
42
43
|
"gemini_cli": [GeminiNativeProvider, GeminiSDKProvider],
|
|
43
44
|
"opencode": [OpenCodeNativeProvider],
|
|
45
|
+
"pi": [PiNativeProvider],
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentabi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Unified async Python API for driving coding agent CLIs — Claude Code, Codex, Gemini CLI, and more
|
|
5
5
|
Author-email: Peng Ding <pding.dp@foxmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -67,6 +67,7 @@ One interface. Any coding agent.
|
|
|
67
67
|
| [Codex](https://github.com/openai/codex) | OpenAI | Implemented |
|
|
68
68
|
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Google | Implemented |
|
|
69
69
|
| [OpenCode](https://opencode.ai) | Open Source | Implemented |
|
|
70
|
+
| [Pi](https://pi.dev/) | Open Source | Implemented |
|
|
70
71
|
|
|
71
72
|
## Installation
|
|
72
73
|
|
|
@@ -83,7 +84,7 @@ pip install agentabi[gemini] # Gemini CLI SDK support
|
|
|
83
84
|
pip install agentabi[all] # All optional SDKs
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
> **Note:** Each agent's CLI must be installed separately (e.g., `claude`, `codex`, `gemini`, `opencode`).
|
|
87
|
+
> **Note:** Each agent's CLI must be installed separately (e.g., `claude`, `codex`, `gemini`, `opencode`, `pi`).
|
|
87
88
|
|
|
88
89
|
## Quick Start
|
|
89
90
|
|
|
@@ -23,6 +23,7 @@ src/agentabi/providers/codex_sdk.py
|
|
|
23
23
|
src/agentabi/providers/gemini_native.py
|
|
24
24
|
src/agentabi/providers/gemini_sdk.py
|
|
25
25
|
src/agentabi/providers/opencode_native.py
|
|
26
|
+
src/agentabi/providers/pi_native.py
|
|
26
27
|
src/agentabi/providers/registry.py
|
|
27
28
|
src/agentabi/types/__init__.py
|
|
28
29
|
src/agentabi/types/ir/__init__.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|