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 +37 -0
- zag/builder.py +306 -0
- zag/process.py +176 -0
- zag/types.py +302 -0
- zag_agent-0.2.1.dist-info/METADATA +125 -0
- zag_agent-0.2.1.dist-info/RECORD +8 -0
- zag_agent-0.2.1.dist-info/WHEEL +5 -0
- zag_agent-0.2.1.dist-info/top_level.txt +1 -0
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 @@
|
|
|
1
|
+
zag
|