zag-agent 0.2.1__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.
zag/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """Python SDK for zag — a unified CLI for AI coding agents."""
2
+
3
+ from .builder import ZagBuilder
4
+ from .types import (
5
+ AgentOutput,
6
+ AssistantMessageEvent,
7
+ ContentBlock,
8
+ ErrorEvent,
9
+ Event,
10
+ InitEvent,
11
+ PermissionRequestEvent,
12
+ ResultEvent,
13
+ TextBlock,
14
+ ToolExecutionEvent,
15
+ ToolResult,
16
+ ToolUseBlock,
17
+ Usage,
18
+ ZagError,
19
+ )
20
+
21
+ __all__ = [
22
+ "ZagBuilder",
23
+ "AgentOutput",
24
+ "Usage",
25
+ "Event",
26
+ "InitEvent",
27
+ "AssistantMessageEvent",
28
+ "ToolExecutionEvent",
29
+ "ResultEvent",
30
+ "ErrorEvent",
31
+ "PermissionRequestEvent",
32
+ "ContentBlock",
33
+ "TextBlock",
34
+ "ToolUseBlock",
35
+ "ToolResult",
36
+ "ZagError",
37
+ ]
zag/builder.py ADDED
@@ -0,0 +1,306 @@
1
+ """Fluent builder for configuring and running zag agent sessions.
2
+
3
+ Example::
4
+
5
+ from zag import ZagBuilder
6
+
7
+ output = await ZagBuilder() \\
8
+ .provider("claude") \\
9
+ .model("sonnet") \\
10
+ .auto_approve() \\
11
+ .exec("write a hello world program")
12
+
13
+ print(output.result)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from collections.abc import AsyncGenerator
20
+
21
+ from .process import default_bin, exec_zag, run_zag, stream_zag, stream_with_input
22
+ from .types import AgentOutput, Event
23
+
24
+
25
+ class ZagBuilder:
26
+ """Fluent builder for configuring and running zag agent sessions."""
27
+
28
+ def __init__(self) -> None:
29
+ self._bin: str = default_bin()
30
+ self._provider: str | None = None
31
+ self._model: str | None = None
32
+ self._system_prompt: str | None = None
33
+ self._root: str | None = None
34
+ self._auto_approve: bool = False
35
+ self._add_dirs: list[str] = []
36
+ self._json: bool = False
37
+ self._json_schema: dict | None = None
38
+ self._json_stream: bool = False
39
+ self._worktree: str | bool | None = None
40
+ self._sandbox: str | bool | None = None
41
+ self._verbose: bool = False
42
+ self._quiet: bool = False
43
+ self._debug: bool = False
44
+ self._session_id: str | None = None
45
+ self._output_format: str | None = None
46
+ self._input_format: str | None = None
47
+ self._replay_user_messages: bool = False
48
+ self._include_partial_messages: bool = False
49
+ self._max_turns: int | None = None
50
+ self._show_usage: bool = False
51
+ self._size: str | None = None
52
+
53
+ # -- Configuration methods -----------------------------------------------
54
+
55
+ def bin(self, path: str) -> ZagBuilder:
56
+ """Override the zag binary path (default: ``ZAG_BIN`` env or ``"zag"``)."""
57
+ self._bin = path
58
+ return self
59
+
60
+ def provider(self, p: str) -> ZagBuilder:
61
+ """Set the provider (e.g., ``"claude"``, ``"codex"``, ``"gemini"``)."""
62
+ self._provider = p
63
+ return self
64
+
65
+ def model(self, m: str) -> ZagBuilder:
66
+ """Set the model (e.g., ``"sonnet"``, ``"opus"``, ``"small"``)."""
67
+ self._model = m
68
+ return self
69
+
70
+ def system_prompt(self, p: str) -> ZagBuilder:
71
+ """Set a system prompt to configure agent behavior."""
72
+ self._system_prompt = p
73
+ return self
74
+
75
+ def root(self, r: str) -> ZagBuilder:
76
+ """Set the root directory for the agent to operate in."""
77
+ self._root = r
78
+ return self
79
+
80
+ def auto_approve(self, a: bool = True) -> ZagBuilder:
81
+ """Enable auto-approve mode (skip permission prompts)."""
82
+ self._auto_approve = a
83
+ return self
84
+
85
+ def add_dir(self, d: str) -> ZagBuilder:
86
+ """Add an additional directory for the agent to include."""
87
+ self._add_dirs.append(d)
88
+ return self
89
+
90
+ def json_mode(self) -> ZagBuilder:
91
+ """Request JSON output from the agent."""
92
+ self._json = True
93
+ return self
94
+
95
+ def json_schema(self, s: dict) -> ZagBuilder:
96
+ """Set a JSON schema for structured output validation. Implies ``json_mode()``."""
97
+ self._json_schema = s
98
+ self._json = True
99
+ return self
100
+
101
+ def json_stream(self) -> ZagBuilder:
102
+ """Enable streaming JSON output (NDJSON format)."""
103
+ self._json_stream = True
104
+ return self
105
+
106
+ def worktree(self, name: str | None = None) -> ZagBuilder:
107
+ """Enable worktree mode with an optional name."""
108
+ self._worktree = name if name is not None else True
109
+ return self
110
+
111
+ def sandbox(self, name: str | None = None) -> ZagBuilder:
112
+ """Enable sandbox mode with an optional name."""
113
+ self._sandbox = name if name is not None else True
114
+ return self
115
+
116
+ def verbose(self, v: bool = True) -> ZagBuilder:
117
+ """Enable verbose output."""
118
+ self._verbose = v
119
+ return self
120
+
121
+ def quiet(self, q: bool = True) -> ZagBuilder:
122
+ """Enable quiet mode."""
123
+ self._quiet = q
124
+ return self
125
+
126
+ def debug(self, d: bool = True) -> ZagBuilder:
127
+ """Enable debug logging."""
128
+ self._debug = d
129
+ return self
130
+
131
+ def session_id(self, id: str) -> ZagBuilder:
132
+ """Pre-set a session ID (UUID)."""
133
+ self._session_id = id
134
+ return self
135
+
136
+ def output_format(self, f: str) -> ZagBuilder:
137
+ """Set the output format (e.g., ``"text"``, ``"json"``, ``"stream-json"``)."""
138
+ self._output_format = f
139
+ return self
140
+
141
+ def input_format(self, f: str) -> ZagBuilder:
142
+ """Set the input format (Claude only)."""
143
+ self._input_format = f
144
+ return self
145
+
146
+ def replay_user_messages(self, r: bool = True) -> ZagBuilder:
147
+ """Re-emit user messages from stdin on stdout (Claude only)."""
148
+ self._replay_user_messages = r
149
+ return self
150
+
151
+ def include_partial_messages(self, i: bool = True) -> ZagBuilder:
152
+ """Include partial message chunks in streaming output (Claude only)."""
153
+ self._include_partial_messages = i
154
+ return self
155
+
156
+ def max_turns(self, n: int) -> ZagBuilder:
157
+ """Set the maximum number of agentic turns."""
158
+ self._max_turns = n
159
+ return self
160
+
161
+ def show_usage(self, s: bool = True) -> ZagBuilder:
162
+ """Show token usage statistics (only applies to JSON output mode)."""
163
+ self._show_usage = s
164
+ return self
165
+
166
+ def size(self, s: str) -> ZagBuilder:
167
+ """Set the Ollama model parameter size (e.g., ``"2b"``, ``"9b"``, ``"35b"``)."""
168
+ self._size = s
169
+ return self
170
+
171
+ # -- Arg building --------------------------------------------------------
172
+
173
+ def _global_args(self) -> list[str]:
174
+ args: list[str] = []
175
+ if self._provider:
176
+ args.extend(["-p", self._provider])
177
+ if self._model:
178
+ args.extend(["--model", self._model])
179
+ if self._system_prompt:
180
+ args.extend(["--system-prompt", self._system_prompt])
181
+ if self._root:
182
+ args.extend(["--root", self._root])
183
+ if self._auto_approve:
184
+ args.append("--auto-approve")
185
+ for d in self._add_dirs:
186
+ args.extend(["--add-dir", d])
187
+ if self._worktree is True:
188
+ args.append("-w")
189
+ elif isinstance(self._worktree, str):
190
+ args.extend(["-w", self._worktree])
191
+ if self._sandbox is True:
192
+ args.append("--sandbox")
193
+ elif isinstance(self._sandbox, str):
194
+ args.extend(["--sandbox", self._sandbox])
195
+ if self._verbose:
196
+ args.append("--verbose")
197
+ if self._quiet:
198
+ args.append("--quiet")
199
+ if self._debug:
200
+ args.append("--debug")
201
+ if self._session_id:
202
+ args.extend(["--session", self._session_id])
203
+ if self._max_turns is not None:
204
+ args.extend(["--max-turns", str(self._max_turns)])
205
+ if self._show_usage:
206
+ args.append("--show-usage")
207
+ if self._size:
208
+ args.extend(["--size", self._size])
209
+ return args
210
+
211
+ def _exec_args(self, prompt: str, *, streaming: bool = False) -> list[str]:
212
+ args = self._global_args()
213
+ args.append("exec")
214
+ if self._json:
215
+ args.append("--json")
216
+ if self._json_schema:
217
+ args.extend(["--json-schema", json.dumps(self._json_schema)])
218
+ if self._json_stream or streaming:
219
+ args.append("--json-stream")
220
+ if self._output_format:
221
+ args.extend(["-o", self._output_format])
222
+ if self._input_format:
223
+ args.extend(["-i", self._input_format])
224
+ if self._replay_user_messages:
225
+ args.append("--replay-user-messages")
226
+ if self._include_partial_messages:
227
+ args.append("--include-partial-messages")
228
+ # Default to json output for structured parsing
229
+ if not streaming and not self._output_format and not self._json_stream:
230
+ args.extend(["-o", "json"])
231
+ args.append(prompt)
232
+ return args
233
+
234
+ # -- Terminal methods ----------------------------------------------------
235
+
236
+ async def exec(self, prompt: str) -> AgentOutput:
237
+ """Run the agent non-interactively and return structured output.
238
+
239
+ Example::
240
+
241
+ output = await ZagBuilder().provider("claude").exec("say hello")
242
+ print(output.result)
243
+ """
244
+ args = self._exec_args(prompt)
245
+ return await exec_zag(self._bin, args)
246
+
247
+ async def exec_streaming(self, prompt: str) -> "StreamingSession":
248
+ """Run the agent with streaming input and output (Claude only).
249
+
250
+ Returns a StreamingSession for bidirectional communication.
251
+
252
+ Example::
253
+
254
+ session = await ZagBuilder().provider("claude").exec_streaming("hello")
255
+ await session.send_user_message("do something")
256
+ async for event in session.events():
257
+ print(event.type)
258
+ await session.wait()
259
+ """
260
+ from .process import StreamingSession as _StreamingSession
261
+
262
+ args = self._global_args()
263
+ args.append("exec")
264
+ args.extend(["-i", "stream-json"])
265
+ args.extend(["-o", "stream-json"])
266
+ args.append("--replay-user-messages")
267
+ if self._include_partial_messages:
268
+ args.append("--include-partial-messages")
269
+ args.append(prompt)
270
+ return await _StreamingSession.create(self._bin, args)
271
+
272
+ async def stream(self, prompt: str) -> AsyncGenerator[Event, None]:
273
+ """Run the agent in streaming mode, yielding events as they arrive.
274
+
275
+ Example::
276
+
277
+ async for event in await ZagBuilder().provider("claude").stream("analyze"):
278
+ print(event.type)
279
+ """
280
+ args = self._exec_args(prompt, streaming=True)
281
+ async for event in stream_zag(self._bin, args):
282
+ yield event
283
+
284
+ async def run(self, prompt: str | None = None) -> None:
285
+ """Start an interactive agent session (inherits stdio)."""
286
+ args = self._global_args()
287
+ args.append("run")
288
+ if self._json:
289
+ args.append("--json")
290
+ if self._json_schema:
291
+ args.extend(["--json-schema", json.dumps(self._json_schema)])
292
+ if prompt:
293
+ args.append(prompt)
294
+ await run_zag(self._bin, args)
295
+
296
+ async def resume(self, session_id: str) -> None:
297
+ """Resume a previous session by ID."""
298
+ args = self._global_args()
299
+ args.extend(["run", "--resume", session_id])
300
+ await run_zag(self._bin, args)
301
+
302
+ async def continue_last(self) -> None:
303
+ """Resume the most recent session."""
304
+ args = self._global_args()
305
+ args.extend(["run", "--continue"])
306
+ await run_zag(self._bin, args)
zag/process.py ADDED
@@ -0,0 +1,176 @@
1
+ """Subprocess helpers for invoking the zag CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ from collections.abc import AsyncGenerator
9
+
10
+ from .types import AgentOutput, Event, ZagError, parse_event
11
+
12
+
13
+ def default_bin() -> str:
14
+ """Return the zag binary path (``ZAG_BIN`` env or ``"zag"``)."""
15
+ return os.environ.get("ZAG_BIN", "zag")
16
+
17
+
18
+ async def exec_zag(bin: str, args: list[str]) -> AgentOutput:
19
+ """Run ``zag`` and return parsed :class:`AgentOutput`.
20
+
21
+ Raises :class:`ZagError` on non-zero exit.
22
+ """
23
+ proc = await asyncio.create_subprocess_exec(
24
+ bin,
25
+ *args,
26
+ stdout=asyncio.subprocess.PIPE,
27
+ stderr=asyncio.subprocess.PIPE,
28
+ )
29
+ stdout_bytes, stderr_bytes = await proc.communicate()
30
+ stdout = stdout_bytes.decode()
31
+ stderr = stderr_bytes.decode()
32
+
33
+ if proc.returncode != 0:
34
+ raise ZagError(
35
+ f"zag exited with code {proc.returncode}: {stderr or stdout}",
36
+ proc.returncode,
37
+ stderr,
38
+ )
39
+
40
+ try:
41
+ data = json.loads(stdout)
42
+ except json.JSONDecodeError as exc:
43
+ raise ZagError(
44
+ f"Failed to parse zag JSON output: {stdout[:200]}",
45
+ proc.returncode,
46
+ stderr,
47
+ ) from exc
48
+
49
+ return AgentOutput.from_dict(data)
50
+
51
+
52
+ async def stream_zag(bin: str, args: list[str]) -> AsyncGenerator[Event, None]:
53
+ """Run ``zag`` in streaming mode and yield :class:`Event` objects (NDJSON)."""
54
+ proc = await asyncio.create_subprocess_exec(
55
+ bin,
56
+ *args,
57
+ stdout=asyncio.subprocess.PIPE,
58
+ stderr=asyncio.subprocess.PIPE,
59
+ )
60
+
61
+ assert proc.stdout is not None
62
+ assert proc.stderr is not None
63
+
64
+ while True:
65
+ line = await proc.stdout.readline()
66
+ if not line:
67
+ break
68
+ text = line.decode().strip()
69
+ if not text:
70
+ continue
71
+ try:
72
+ data = json.loads(text)
73
+ yield parse_event(data)
74
+ except (json.JSONDecodeError, ValueError):
75
+ continue
76
+
77
+ stderr_bytes = await proc.stderr.read()
78
+ await proc.wait()
79
+
80
+ if proc.returncode != 0:
81
+ stderr = stderr_bytes.decode()
82
+ raise ZagError(
83
+ f"zag exited with code {proc.returncode}",
84
+ proc.returncode,
85
+ stderr,
86
+ )
87
+
88
+
89
+ class StreamingSession:
90
+ """A live streaming session with piped stdin and stdout.
91
+
92
+ Send NDJSON messages via :meth:`send`, read events via :meth:`events`,
93
+ then call :meth:`wait` when done.
94
+ """
95
+
96
+ def __init__(self, proc: asyncio.subprocess.Process) -> None:
97
+ self._proc = proc
98
+
99
+ @classmethod
100
+ async def create(cls, bin: str, args: list[str]) -> "StreamingSession":
101
+ proc = await asyncio.create_subprocess_exec(
102
+ bin,
103
+ *args,
104
+ stdin=asyncio.subprocess.PIPE,
105
+ stdout=asyncio.subprocess.PIPE,
106
+ stderr=asyncio.subprocess.PIPE,
107
+ )
108
+ return cls(proc)
109
+
110
+ async def send(self, message: str) -> None:
111
+ """Send a raw NDJSON line to the agent's stdin."""
112
+ assert self._proc.stdin is not None
113
+ self._proc.stdin.write((message + "\n").encode())
114
+ await self._proc.stdin.drain()
115
+
116
+ async def send_user_message(self, content: str) -> None:
117
+ """Send a user message to the agent."""
118
+ msg = json.dumps({"type": "user_message", "content": content})
119
+ await self.send(msg)
120
+
121
+ def close_input(self) -> None:
122
+ """Close stdin to signal no more input."""
123
+ if self._proc.stdin is not None:
124
+ self._proc.stdin.close()
125
+
126
+ async def events(self) -> AsyncGenerator[Event, None]:
127
+ """Async iterator over parsed Event objects from stdout."""
128
+ assert self._proc.stdout is not None
129
+ while True:
130
+ line = await self._proc.stdout.readline()
131
+ if not line:
132
+ break
133
+ text = line.decode().strip()
134
+ if not text:
135
+ continue
136
+ try:
137
+ data = json.loads(text)
138
+ yield parse_event(data)
139
+ except (json.JSONDecodeError, ValueError):
140
+ continue
141
+
142
+ async def wait(self) -> None:
143
+ """Wait for the process to exit. Raises ZagError on non-zero exit."""
144
+ self.close_input()
145
+ assert self._proc.stderr is not None
146
+ stderr_bytes = await self._proc.stderr.read()
147
+ await self._proc.wait()
148
+ if self._proc.returncode != 0:
149
+ stderr = stderr_bytes.decode()
150
+ raise ZagError(
151
+ f"zag exited with code {self._proc.returncode}",
152
+ self._proc.returncode,
153
+ stderr,
154
+ )
155
+
156
+
157
+ def stream_with_input(bin: str, args: list[str]) -> StreamingSession:
158
+ """Create a StreamingSession (alias for StreamingSession.create)."""
159
+ # This is a sync wrapper that returns the coroutine; callers should await it
160
+ raise NotImplementedError("Use StreamingSession.create() directly")
161
+
162
+
163
+ async def run_zag(bin: str, args: list[str]) -> None:
164
+ """Run ``zag`` interactively with inherited stdio.
165
+
166
+ Raises :class:`ZagError` on non-zero exit.
167
+ """
168
+ proc = await asyncio.create_subprocess_exec(bin, *args)
169
+ await proc.wait()
170
+
171
+ if proc.returncode != 0:
172
+ raise ZagError(
173
+ f"zag exited with code {proc.returncode}",
174
+ proc.returncode,
175
+ "",
176
+ )
zag/types.py ADDED
@@ -0,0 +1,302 @@
1
+ """Type definitions for zag agent output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ class ZagError(Exception):
10
+ """Error raised when the zag process fails."""
11
+
12
+ def __init__(self, message: str, exit_code: int | None, stderr: str) -> None:
13
+ super().__init__(message)
14
+ self.exit_code = exit_code
15
+ self.stderr = stderr
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Usage
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ @dataclass
24
+ class Usage:
25
+ """Token usage statistics for an agent session."""
26
+
27
+ input_tokens: int = 0
28
+ output_tokens: int = 0
29
+ cache_read_tokens: int | None = None
30
+ cache_creation_tokens: int | None = None
31
+ web_search_requests: int | None = None
32
+ web_fetch_requests: int | None = None
33
+
34
+ @classmethod
35
+ def from_dict(cls, data: dict[str, Any]) -> Usage:
36
+ return cls(
37
+ input_tokens=data.get("input_tokens", 0),
38
+ output_tokens=data.get("output_tokens", 0),
39
+ cache_read_tokens=data.get("cache_read_tokens"),
40
+ cache_creation_tokens=data.get("cache_creation_tokens"),
41
+ web_search_requests=data.get("web_search_requests"),
42
+ web_fetch_requests=data.get("web_fetch_requests"),
43
+ )
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Tool Result
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ @dataclass
52
+ class ToolResult:
53
+ """Result from a tool execution."""
54
+
55
+ success: bool
56
+ output: str | None = None
57
+ error: str | None = None
58
+ data: Any = None
59
+
60
+ @classmethod
61
+ def from_dict(cls, data: dict[str, Any]) -> ToolResult:
62
+ return cls(
63
+ success=data.get("success", False),
64
+ output=data.get("output"),
65
+ error=data.get("error"),
66
+ data=data.get("data"),
67
+ )
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Content Blocks
72
+ # ---------------------------------------------------------------------------
73
+
74
+
75
+ @dataclass
76
+ class TextBlock:
77
+ """Plain text content block."""
78
+
79
+ type: str = "text"
80
+ text: str = ""
81
+
82
+ @classmethod
83
+ def from_dict(cls, data: dict[str, Any]) -> TextBlock:
84
+ return cls(text=data.get("text", ""))
85
+
86
+
87
+ @dataclass
88
+ class ToolUseBlock:
89
+ """Tool invocation content block."""
90
+
91
+ type: str = "tool_use"
92
+ id: str = ""
93
+ name: str = ""
94
+ input: Any = None
95
+
96
+ @classmethod
97
+ def from_dict(cls, data: dict[str, Any]) -> ToolUseBlock:
98
+ return cls(
99
+ id=data.get("id", ""),
100
+ name=data.get("name", ""),
101
+ input=data.get("input"),
102
+ )
103
+
104
+
105
+ ContentBlock = TextBlock | ToolUseBlock
106
+
107
+
108
+ def _parse_content_block(data: dict[str, Any]) -> ContentBlock:
109
+ if data.get("type") == "tool_use":
110
+ return ToolUseBlock.from_dict(data)
111
+ return TextBlock.from_dict(data)
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Events (tagged union on "type")
116
+ # ---------------------------------------------------------------------------
117
+
118
+
119
+ @dataclass
120
+ class InitEvent:
121
+ """Session initialization event."""
122
+
123
+ type: str = "init"
124
+ model: str = ""
125
+ tools: list[str] = field(default_factory=list)
126
+ working_directory: str | None = None
127
+ metadata: dict[str, Any] = field(default_factory=dict)
128
+
129
+ @classmethod
130
+ def from_dict(cls, data: dict[str, Any]) -> InitEvent:
131
+ return cls(
132
+ model=data.get("model", ""),
133
+ tools=data.get("tools", []),
134
+ working_directory=data.get("working_directory"),
135
+ metadata=data.get("metadata", {}),
136
+ )
137
+
138
+
139
+ @dataclass
140
+ class UserMessageEvent:
141
+ """User message (replayed via --replay-user-messages)."""
142
+
143
+ type: str = "user_message"
144
+ content: list[ContentBlock] = field(default_factory=list)
145
+
146
+ @classmethod
147
+ def from_dict(cls, data: dict[str, Any]) -> UserMessageEvent:
148
+ content = [_parse_content_block(b) for b in data.get("content", [])]
149
+ return cls(content=content)
150
+
151
+
152
+ @dataclass
153
+ class AssistantMessageEvent:
154
+ """Message from the assistant."""
155
+
156
+ type: str = "assistant_message"
157
+ content: list[ContentBlock] = field(default_factory=list)
158
+ usage: Usage | None = None
159
+
160
+ @classmethod
161
+ def from_dict(cls, data: dict[str, Any]) -> AssistantMessageEvent:
162
+ content = [_parse_content_block(b) for b in data.get("content", [])]
163
+ usage_data = data.get("usage")
164
+ usage = Usage.from_dict(usage_data) if usage_data else None
165
+ return cls(content=content, usage=usage)
166
+
167
+
168
+ @dataclass
169
+ class ToolExecutionEvent:
170
+ """Tool execution event."""
171
+
172
+ type: str = "tool_execution"
173
+ tool_name: str = ""
174
+ tool_id: str = ""
175
+ input: Any = None
176
+ result: ToolResult = field(default_factory=lambda: ToolResult(success=False))
177
+
178
+ @classmethod
179
+ def from_dict(cls, data: dict[str, Any]) -> ToolExecutionEvent:
180
+ return cls(
181
+ tool_name=data.get("tool_name", ""),
182
+ tool_id=data.get("tool_id", ""),
183
+ input=data.get("input"),
184
+ result=ToolResult.from_dict(data.get("result", {})),
185
+ )
186
+
187
+
188
+ @dataclass
189
+ class ResultEvent:
190
+ """Final session result event."""
191
+
192
+ type: str = "result"
193
+ success: bool = False
194
+ message: str | None = None
195
+ duration_ms: int | None = None
196
+ num_turns: int | None = None
197
+
198
+ @classmethod
199
+ def from_dict(cls, data: dict[str, Any]) -> ResultEvent:
200
+ return cls(
201
+ success=data.get("success", False),
202
+ message=data.get("message"),
203
+ duration_ms=data.get("duration_ms"),
204
+ num_turns=data.get("num_turns"),
205
+ )
206
+
207
+
208
+ @dataclass
209
+ class ErrorEvent:
210
+ """Error event."""
211
+
212
+ type: str = "error"
213
+ message: str = ""
214
+ details: Any = None
215
+
216
+ @classmethod
217
+ def from_dict(cls, data: dict[str, Any]) -> ErrorEvent:
218
+ return cls(
219
+ message=data.get("message", ""),
220
+ details=data.get("details"),
221
+ )
222
+
223
+
224
+ @dataclass
225
+ class PermissionRequestEvent:
226
+ """Permission request event."""
227
+
228
+ type: str = "permission_request"
229
+ tool_name: str = ""
230
+ description: str = ""
231
+ granted: bool = False
232
+
233
+ @classmethod
234
+ def from_dict(cls, data: dict[str, Any]) -> PermissionRequestEvent:
235
+ return cls(
236
+ tool_name=data.get("tool_name", ""),
237
+ description=data.get("description", ""),
238
+ granted=data.get("granted", False),
239
+ )
240
+
241
+
242
+ Event = (
243
+ InitEvent
244
+ | UserMessageEvent
245
+ | AssistantMessageEvent
246
+ | ToolExecutionEvent
247
+ | ResultEvent
248
+ | ErrorEvent
249
+ | PermissionRequestEvent
250
+ )
251
+
252
+ _EVENT_PARSERS: dict[str, type] = {
253
+ "init": InitEvent,
254
+ "user_message": UserMessageEvent,
255
+ "assistant_message": AssistantMessageEvent,
256
+ "tool_execution": ToolExecutionEvent,
257
+ "result": ResultEvent,
258
+ "error": ErrorEvent,
259
+ "permission_request": PermissionRequestEvent,
260
+ }
261
+
262
+
263
+ def parse_event(data: dict[str, Any]) -> Event:
264
+ """Parse a raw dict into the appropriate Event subtype."""
265
+ event_type = data.get("type", "")
266
+ parser = _EVENT_PARSERS.get(event_type)
267
+ if parser is None:
268
+ raise ValueError(f"Unknown event type: {event_type}")
269
+ return parser.from_dict(data) # type: ignore[union-attr]
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # AgentOutput
274
+ # ---------------------------------------------------------------------------
275
+
276
+
277
+ @dataclass
278
+ class AgentOutput:
279
+ """Unified output from an agent session."""
280
+
281
+ agent: str = ""
282
+ session_id: str = ""
283
+ events: list[Event] = field(default_factory=list)
284
+ result: str | None = None
285
+ is_error: bool = False
286
+ total_cost_usd: float | None = None
287
+ usage: Usage | None = None
288
+
289
+ @classmethod
290
+ def from_dict(cls, data: dict[str, Any]) -> AgentOutput:
291
+ events = [parse_event(e) for e in data.get("events", [])]
292
+ usage_data = data.get("usage")
293
+ usage = Usage.from_dict(usage_data) if usage_data else None
294
+ return cls(
295
+ agent=data.get("agent", ""),
296
+ session_id=data.get("session_id", ""),
297
+ events=events,
298
+ result=data.get("result"),
299
+ is_error=data.get("is_error", False),
300
+ total_cost_usd=data.get("total_cost_usd"),
301
+ usage=usage,
302
+ )
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: zag-agent
3
+ Version: 0.2.1
4
+ Summary: Python SDK for zag — a unified CLI for AI coding agents
5
+ Author: Niclas Lindstedt
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/niclaslindstedt/zag
8
+ Project-URL: Repository, https://github.com/niclaslindstedt/zag
9
+ Keywords: zag,ai,agent,claude,codex,gemini,copilot,ollama
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Zag Python Binding
22
+
23
+ Python binding for [zag](https://github.com/niclaslindstedt/zag) — a unified CLI for AI coding agents.
24
+
25
+ ## Prerequisites
26
+
27
+ - Python 3.10+
28
+ - The `zag` CLI binary installed and on your `PATH` (or set via `ZAG_BIN` env var)
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install zag-agent
34
+ ```
35
+
36
+ For development from source:
37
+
38
+ ```bash
39
+ cd bindings/python
40
+ pip install -e .
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ from zag import ZagBuilder
47
+
48
+ output = await ZagBuilder() \
49
+ .provider("claude") \
50
+ .model("sonnet") \
51
+ .auto_approve() \
52
+ .exec("write a hello world program")
53
+
54
+ print(output.result)
55
+ ```
56
+
57
+ ## Streaming
58
+
59
+ ```python
60
+ from zag import ZagBuilder
61
+
62
+ async for event in await ZagBuilder().provider("claude").stream("analyze code"):
63
+ print(event.type, event)
64
+ ```
65
+
66
+ ## Builder methods
67
+
68
+ | Method | Description |
69
+ |--------|-------------|
70
+ | `.provider(name)` | Set provider: `"claude"`, `"codex"`, `"gemini"`, `"copilot"`, `"ollama"` |
71
+ | `.model(name)` | Set model name or size alias (`"small"`, `"medium"`, `"large"`) |
72
+ | `.system_prompt(text)` | Set a system prompt |
73
+ | `.root(path)` | Set the working directory |
74
+ | `.auto_approve()` | Skip permission prompts |
75
+ | `.add_dir(path)` | Add an additional directory (chainable) |
76
+ | `.json_mode()` | Request JSON output |
77
+ | `.json_schema(schema)` | Validate output against a JSON schema (implies `.json_mode()`) |
78
+ | `.json_stream()` | Enable streaming NDJSON output |
79
+ | `.worktree(name=None)` | Run in an isolated git worktree |
80
+ | `.sandbox(name=None)` | Run in a Docker sandbox |
81
+ | `.session_id(uuid)` | Use a specific session ID |
82
+ | `.output_format(fmt)` | Set output format (`"text"`, `"json"`, `"json-pretty"`, `"stream-json"`) |
83
+ | `.input_format(fmt)` | Set input format (`"text"`, `"stream-json"` — Claude only) |
84
+ | `.replay_user_messages()` | Re-emit user messages on stdout (Claude only) |
85
+ | `.include_partial_messages()` | Include partial message chunks (Claude only) |
86
+ | `.max_turns(n)` | Set the maximum number of agentic turns |
87
+ | `.show_usage()` | Show token usage statistics (JSON output mode) |
88
+ | `.size(size)` | Set Ollama model parameter size (e.g., `"2b"`, `"9b"`, `"35b"`) |
89
+ | `.verbose()` | Enable verbose output |
90
+ | `.quiet()` | Suppress non-essential output |
91
+ | `.debug()` | Enable debug logging |
92
+ | `.bin(path)` | Override the `zag` binary path |
93
+
94
+ ## Terminal methods
95
+
96
+ | Method | Returns | Description |
97
+ |--------|---------|-------------|
98
+ | `.exec(prompt)` | `AgentOutput` | Run non-interactively, return structured output |
99
+ | `.stream(prompt)` | `AsyncGenerator[Event]` | Stream NDJSON events |
100
+ | `.exec_streaming(prompt)` | `StreamingSession` | Bidirectional streaming (Claude only) |
101
+ | `.run(prompt=None)` | `None` | Start an interactive session (inherits stdio) |
102
+ | `.resume(session_id)` | `None` | Resume a previous session by ID |
103
+ | `.continue_last()` | `None` | Resume the most recent session |
104
+
105
+ ## How it works
106
+
107
+ The SDK spawns the `zag` CLI as a subprocess (`zag exec -o json` or `-o stream-json`) and parses the JSON/NDJSON output into typed dataclasses. Zero external dependencies — only the Python standard library.
108
+
109
+ ## Testing
110
+
111
+ ```bash
112
+ pip install pytest pytest-asyncio
113
+ pytest
114
+ ```
115
+
116
+ ## See also
117
+
118
+ - [TypeScript SDK](../typescript/README.md)
119
+ - [C# SDK](../csharp/README.md)
120
+ - [Rust API (zag-agent)](../../zag-agent/README.md)
121
+ - [All bindings](../README.md)
122
+
123
+ ## License
124
+
125
+ [MIT](../../LICENSE)
@@ -0,0 +1,8 @@
1
+ zag/__init__.py,sha256=FWgja1DP4aO2vLdJHB7V12rUr4v1diHMCJC7sd3IJpc,676
2
+ zag/builder.py,sha256=TQGYJlt8OX9WBFK-dFc8Mo8n_rdW6jpd9lYcTAeuPmc,10728
3
+ zag/process.py,sha256=mc_zsJGscEgjTVIRi_9_xDhg6yIIRbG3VvPpopExL-A,5389
4
+ zag/types.py,sha256=5OJYQpJJuyotp_HQP1L2BGaiBW-5rokMrQig6kZYKgA,8365
5
+ zag_agent-0.2.1.dist-info/METADATA,sha256=_oM1P44MeuNO-lCZBqg8jHPNORfEs-0ivwW7IFPt6Ps,4182
6
+ zag_agent-0.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ zag_agent-0.2.1.dist-info/top_level.txt,sha256=yf3HgUUit2iipiJk8Hw-xOXYQ0TV0Pm3ycYXzNWAHK8,4
8
+ zag_agent-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ zag