zu-runtime 0.2.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.
zu/__init__.py ADDED
@@ -0,0 +1,203 @@
1
+ """Zu — the embed facade. ``import zu`` and run an agent in one line.
2
+
3
+ This is the batteries-included entry point for *using* Zu from your own code.
4
+ It wires the same path the CLI and the HTTP server use — config in, a typed
5
+ ``Result`` out — so embedding, ``zu run``, and ``zu serve`` are one runtime, not
6
+ three.
7
+
8
+ import zu
9
+
10
+ # a self-contained agent: one agent.yaml, or a bundle dir (agent.yaml + tools/)
11
+ result = zu.run_agent("agent.yaml") # or zu.run_agent("my-agent/")
12
+
13
+ # the programmatic form — a config plus a task (config + many tasks):
14
+ result = zu.run(
15
+ {"query": "Extract the title and price.", "target": "https://example.com",
16
+ "output_schema": {"type": "object", "properties": {"title": {"type": "string"}}}},
17
+ config={"provider": {"name": "anthropic", "model": "claude-sonnet-4-6",
18
+ "api_key_env": "ANTHROPIC_API_KEY"},
19
+ "plugins": {"tools": ["http_fetch", "html_parse"], "validators": ["schema"]}},
20
+ )
21
+
22
+ print(result.status, result.value)
23
+
24
+ # a reusable, configured runner (load config once, run many tasks)
25
+ agent = zu.Zu(config="zu.yaml")
26
+ r1 = agent.run({"query": "..."})
27
+ r2, events = agent.run_with_events({"query": "..."}) # also get the event log
28
+
29
+ Credentials are never passed here: config names the *environment variable*
30
+ holding a key (``api_key_env``), resolved inside the adapter at call time.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import asyncio
36
+ from typing import Any
37
+
38
+ from zu_cli.config import (
39
+ ConfigError,
40
+ RunConfig,
41
+ assemble,
42
+ coerce_config,
43
+ coerce_task,
44
+ )
45
+ from zu_core.contracts import Budget, Event, Result, Status, TaskSpec
46
+ from zu_core.loop import run_task
47
+ from zu_core.registry import (
48
+ backend,
49
+ detector,
50
+ provider,
51
+ sink,
52
+ tool,
53
+ validator,
54
+ )
55
+
56
+ from .pipeline import Pipeline, PipelineResult
57
+
58
+ __version__ = "0.1.0"
59
+
60
+ # In-process plugin registration decorators, re-exported from the core registry
61
+ # so the documented ``@zu.tool`` / ``@zu.detector`` / … surface (see
62
+ # the architecture docs and AGENTS.md) actually resolves on ``import zu``. They
63
+ # register onto the process-wide REGISTRY the loop reads, so a decorator-
64
+ # registered plugin is visible to ``zu.run`` and ``zu plugins`` alike.
65
+ __all__ = [
66
+ "Zu",
67
+ "Pipeline",
68
+ "PipelineResult",
69
+ "run_agent",
70
+ "run_agent_with_events",
71
+ "run",
72
+ "arun",
73
+ "run_with_events",
74
+ "ConfigError",
75
+ "RunConfig",
76
+ "TaskSpec",
77
+ "Result",
78
+ "Status",
79
+ "Budget",
80
+ "Event",
81
+ "create_app",
82
+ "tool",
83
+ "detector",
84
+ "validator",
85
+ "provider",
86
+ "backend",
87
+ "sink",
88
+ "__version__",
89
+ ]
90
+
91
+
92
+ # Config/task coercion is shared with the CLI surfaces (see zu_cli.config). The
93
+ # embed facade accepts a str task as a *path* (``allow_paths=True``): you're
94
+ # running in-process on your own host, so reading a task file you point at — the
95
+ # same affordance as ``zu run`` — is intended.
96
+
97
+
98
+ class Zu:
99
+ """A configured runner. Load a config once, run many tasks against it.
100
+
101
+ ``config`` is a path, a dict, a ``RunConfig``, or None (``./zu.yaml``). The
102
+ config is parsed eagerly so a bad config fails here, not on the first run.
103
+ """
104
+
105
+ def __init__(self, config: Any = None) -> None:
106
+ self.config: RunConfig = coerce_config(config)
107
+
108
+ async def arun_with_events(self, task: Any) -> tuple[Result, list[Event]]:
109
+ """Async: run one task, returning the Result and the run's event log."""
110
+ spec = coerce_task(task, self.config.budget, allow_paths=True)
111
+ provider, registry, bus, providers = assemble(self.config)
112
+ # The same observability hook the CLI uses: an embedded agent queues a
113
+ # blocked attempt to the review queue too (no console trace by default).
114
+ from zu_cli.observe import attach_observability
115
+
116
+ attach_observability(bus, self.config.observability)
117
+ try:
118
+ result = await run_task(
119
+ spec, provider, registry, bus,
120
+ providers=providers, containment=self.config.containment,
121
+ max_observation_chars=self.config.max_observation_chars,
122
+ observation_strategy=self.config.observation_strategy,
123
+ max_context_chars=self.config.max_context_chars,
124
+ )
125
+ events = await bus.query()
126
+ return result, events
127
+ finally:
128
+ # ``assemble`` builds a fresh bus (and its canonical/trace sinks) per
129
+ # run; release them here so a long-lived, reused ``Zu`` instance does
130
+ # not leak one sqlite connection per ``run()``.
131
+ await bus.aclose()
132
+
133
+ async def arun(self, task: Any) -> Result:
134
+ """Async: run one task, returning just the Result."""
135
+ result, _ = await self.arun_with_events(task)
136
+ return result
137
+
138
+ def run_with_events(self, task: Any) -> tuple[Result, list[Event]]:
139
+ """Run one task synchronously, returning the Result and the event log."""
140
+ return asyncio.run(self.arun_with_events(task))
141
+
142
+ def run(self, task: Any) -> Result:
143
+ """Run one task synchronously, returning just the Result."""
144
+ return asyncio.run(self.arun(task))
145
+
146
+
147
+ def run_agent(source: Any = None) -> Result:
148
+ """Run a self-contained agent to a Result — the embed equivalent of
149
+ ``zu run``. ``source`` is an ``agent.yaml`` path, a **bundle directory**
150
+ (agent.yaml + a tools/ package, auto-loaded), a dict, or None (``./agent.yaml``
151
+ or ``./``)."""
152
+ result, _ = run_agent_with_events(source)
153
+ return result
154
+
155
+
156
+ def run_agent_with_events(source: Any = None) -> tuple[Result, list[Event]]:
157
+ """Run a self-contained agent, returning the Result *and* its event log."""
158
+ return asyncio.run(_arun_agent(source))
159
+
160
+
161
+ async def _arun_agent(source: Any) -> tuple[Result, list[Event]]:
162
+ from zu_cli.config import load_agent
163
+ from zu_cli.observe import attach_observability
164
+
165
+ spec, cfg = load_agent(source)
166
+ provider, registry, bus, providers = assemble(cfg)
167
+ attach_observability(bus, cfg.observability)
168
+ try:
169
+ result = await run_task(
170
+ spec, provider, registry, bus,
171
+ providers=providers, containment=cfg.containment,
172
+ max_observation_chars=cfg.max_observation_chars,
173
+ observation_strategy=cfg.observation_strategy,
174
+ max_context_chars=cfg.max_context_chars,
175
+ )
176
+ return result, await bus.query()
177
+ finally:
178
+ await bus.aclose()
179
+
180
+
181
+ def run(task: Any, config: Any = None) -> Result:
182
+ """Run one task against a config — the programmatic form (config + many tasks).
183
+ ``task`` and ``config`` may each be a path, a dict, the typed object, or None.
184
+ For a single self-contained ``agent.yaml``/bundle, use :func:`run_agent`."""
185
+ return Zu(config).run(task)
186
+
187
+
188
+ async def arun(task: Any, config: Any = None) -> Result:
189
+ """Async one-shot — the coroutine behind :func:`run`."""
190
+ return await Zu(config).arun(task)
191
+
192
+
193
+ def run_with_events(task: Any, config: Any = None) -> tuple[Result, list[Event]]:
194
+ """Run one task to a Result *and* its event log (the queryable provenance)."""
195
+ return Zu(config).run_with_events(task)
196
+
197
+
198
+ def create_app(config: Any = None, **kwargs: Any) -> Any:
199
+ """The ASGI app for ``zu serve``. Re-exported here so an embedder can mount
200
+ Zu in their own ASGI stack. Needs the 'serve' extra (FastAPI)."""
201
+ from zu_cli.server import create_app as _create_app
202
+
203
+ return _create_app(config, **kwargs)
zu/pipeline.py ADDED
@@ -0,0 +1,92 @@
1
+ """``zu.Pipeline`` — the config-driven wrapper over the core orchestration.
2
+
3
+ The orchestration itself (shared-trace chaining, gating, resume) lives in
4
+ ``zu_core.pipeline`` — pure, SDK-free core logic over ``run_task``. This wrapper
5
+ adds the ergonomics that match ``zu.run``: a YAML/dict **config** (one provider
6
+ block, the plugins, the sink) and **dict phase specs**. It assembles the config
7
+ once and shares the resulting bus across every phase.
8
+
9
+ pipe = zu.Pipeline(config="zu.yaml")
10
+ pipe.phase("extract", {"query": "...", "output_schema": {...}})
11
+ pipe.phase("summarize", lambda prev: {"query": f"...{prev.value['name']}...", "output_schema": {...}})
12
+ result = pipe.run() # PipelineResult: status, value, phases, events, id
13
+
14
+ A phase's task is a dict, or a callable ``(prev_result) -> dict`` that consumes
15
+ the previous phase's validated value. Point the config at the ``scripted``
16
+ provider to run the whole pipeline offline (no model, no network).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ from collections.abc import Callable
23
+ from typing import Any
24
+ from uuid import UUID, uuid4
25
+
26
+ from zu_cli.config import assemble, coerce_config, coerce_task
27
+ from zu_core.contracts import Result, TaskSpec
28
+ from zu_core.pipeline import Phase, PipelineResult, run_pipeline
29
+
30
+ __all__ = ["Pipeline", "PipelineResult"]
31
+
32
+ # A phase's task: a static spec dict, or a builder that consumes the prior result.
33
+ PhaseTask = dict | Callable[[Result | None], dict]
34
+
35
+
36
+ class Pipeline:
37
+ """A deterministic sequence of phases sharing one trace, log, and budget gate.
38
+
39
+ ``config`` is a path, dict, ``RunConfig``, or None (``./zu.yaml``) — the same
40
+ as ``zu.run``. ``pipeline_id`` defaults to a fresh id; pass a stable one (with
41
+ a durable ``event_sink`` in the config) to make the pipeline resumable across
42
+ process restarts.
43
+ """
44
+
45
+ def __init__(self, config: Any = None, *, pipeline_id: UUID | str | None = None) -> None:
46
+ self.config = coerce_config(config)
47
+ self._id = _as_uuid(pipeline_id) if pipeline_id is not None else uuid4()
48
+ self._phases: list[tuple[str, PhaseTask]] = []
49
+
50
+ @property
51
+ def id(self) -> UUID:
52
+ return self._id
53
+
54
+ def phase(self, name: str, task: PhaseTask) -> Pipeline:
55
+ """Append a phase. ``task`` is a spec dict or ``(prev_result) -> dict``.
56
+ Returns self, so calls chain. Names must be unique (they key resume)."""
57
+ if any(n == name for n, _ in self._phases):
58
+ raise ValueError(f"duplicate phase name: {name!r}")
59
+ self._phases.append((name, task))
60
+ return self
61
+
62
+ async def arun(self) -> PipelineResult:
63
+ cfg = self.config
64
+ provider, registry, bus, providers = assemble(cfg)
65
+ try:
66
+ phases = [Phase(name, self._build(task)) for name, task in self._phases]
67
+ return await run_pipeline(
68
+ phases, provider, registry, bus,
69
+ providers=providers, containment=cfg.containment, pipeline_id=self._id,
70
+ max_observation_chars=cfg.max_observation_chars,
71
+ observation_strategy=cfg.observation_strategy,
72
+ max_context_chars=cfg.max_context_chars,
73
+ )
74
+ finally:
75
+ # ``assemble`` built the bus + its sink(s); release them after the run.
76
+ await bus.aclose()
77
+
78
+ def run(self) -> PipelineResult:
79
+ """Run the pipeline synchronously."""
80
+ return asyncio.run(self.arun())
81
+
82
+ def _build(self, task: PhaseTask) -> Callable[[Result | None], TaskSpec]:
83
+ """Turn a dict / callable phase task into a core TaskSpec builder, coercing
84
+ the dict and inheriting the config's default budget."""
85
+ def build(prev: Result | None) -> TaskSpec:
86
+ task_dict = task(prev) if callable(task) else dict(task)
87
+ return coerce_task(task_dict, self.config.budget, allow_paths=True)
88
+ return build
89
+
90
+
91
+ def _as_uuid(value: UUID | str) -> UUID:
92
+ return value if isinstance(value, UUID) else UUID(str(value))
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: zu-runtime
3
+ Version: 0.2.0
4
+ Summary: An opinionated, backend-agnostic runtime for agents that work in production — deterministic, auditable, injection-resistant. Import as `zu`.
5
+ Project-URL: Homepage, https://github.com/k3-mt/zu
6
+ Project-URL: Repository, https://github.com/k3-mt/zu
7
+ License-Expression: Apache-2.0
8
+ Keywords: agents,ai-agents,event-sourcing,llm,runtime,sandbox,web-scraping
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: zu-backends==0.2.0
19
+ Requires-Dist: zu-checks==0.2.0
20
+ Requires-Dist: zu-cli==0.2.0
21
+ Requires-Dist: zu-providers==0.2.0
22
+ Requires-Dist: zu-tools==0.2.0
23
+ Provides-Extra: all
24
+ Requires-Dist: zu-backends[docker]==0.2.0; extra == 'all'
25
+ Requires-Dist: zu-cli[mcp]==0.2.0; extra == 'all'
26
+ Requires-Dist: zu-cli[serve]==0.2.0; extra == 'all'
27
+ Requires-Dist: zu-cli[test]==0.2.0; extra == 'all'
28
+ Requires-Dist: zu-providers[anthropic]==0.2.0; extra == 'all'
29
+ Requires-Dist: zu-providers[openai]==0.2.0; extra == 'all'
30
+ Requires-Dist: zu-tools==0.2.0; extra == 'all'
31
+ Provides-Extra: anthropic
32
+ Requires-Dist: zu-providers[anthropic]==0.2.0; extra == 'anthropic'
33
+ Provides-Extra: demo
34
+ Requires-Dist: zu-tools==0.2.0; extra == 'demo'
35
+ Provides-Extra: docker
36
+ Requires-Dist: zu-backends[docker]==0.2.0; extra == 'docker'
37
+ Provides-Extra: mcp
38
+ Requires-Dist: zu-cli[mcp]==0.2.0; extra == 'mcp'
39
+ Provides-Extra: openai
40
+ Requires-Dist: zu-providers[openai]==0.2.0; extra == 'openai'
41
+ Provides-Extra: serve
42
+ Requires-Dist: zu-cli[serve]==0.2.0; extra == 'serve'
43
+ Provides-Extra: test
44
+ Requires-Dist: zu-cli[test]==0.2.0; extra == 'test'
45
+ Provides-Extra: web
46
+ Requires-Dist: zu-tools==0.2.0; extra == 'web'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # Zu
50
+
51
+ **An opinionated, backend-agnostic runtime for agents that work in production** —
52
+ deterministic, auditable, and injection-resistant by construction.
53
+
54
+ New here? One line:
55
+
56
+ ```bash
57
+ pip install 'zu-runtime[all]' # everything: web tools, both model SDKs, server, Docker, MCP
58
+ ```
59
+
60
+ Prefer a lean install? `pip install zu-runtime` gives you `import zu`, the `zu`
61
+ command, the **web tools** (http_fetch/html_parse/render_dom), the model-provider adapters,
62
+ detectors, validators, and a SQLite event sink. Add the heavy/situational bits as extras:
63
+
64
+ ```bash
65
+ pip install 'zu-runtime[anthropic]' # + the Anthropic SDK to call a real model (also: [openai])
66
+ pip install 'zu-runtime[serve]' # + the HTTP server (zu serve)
67
+ pip install 'zu-runtime[docker]' # + the Docker sandbox (tier-2 browser)
68
+ pip install 'zu-runtime[mcp]' # + the MCP server (zu mcp)
69
+ ```
70
+
71
+ The `zu-*` packages are also standalone on PyPI, but you rarely install them individually —
72
+ that's for plugin authors depending on just `zu-core`.
73
+
74
+ ## Embed it
75
+
76
+ ```python
77
+ import zu
78
+
79
+ result = zu.run(
80
+ {"query": "Extract the product name and price.",
81
+ "target": "https://example.com/product/123",
82
+ "output_schema": {"type": "object",
83
+ "properties": {"name": {"type": "string"}, "price": {"type": "string"}},
84
+ "required": ["name", "price"]}},
85
+ config={"provider": {"name": "anthropic", "model": "claude-sonnet-4-6",
86
+ "api_key_env": "ANTHROPIC_API_KEY"},
87
+ "plugins": {"tools": ["http_fetch", "html_parse", "render_dom"],
88
+ "detectors": ["empty", "error", "js-shell", "bot-wall"],
89
+ "validators": ["schema", "grounding"]}},
90
+ )
91
+ print(result.status, result.value)
92
+ ```
93
+
94
+ Swapping the model is a one-line edit to the `provider` block — Anthropic,
95
+ OpenRouter, OpenAI, or a local model (Ollama / vLLM) — because the runtime only
96
+ ever speaks to a `ModelProvider` port. Credentials are named by environment
97
+ variable (`api_key_env`), never passed in code or config.
98
+
99
+ ## Run it from the command line, or as a service
100
+
101
+ ```bash
102
+ zu run agent.yaml # one-shot
103
+ zu run agent.yaml --every 5m # scheduled worker
104
+ zu serve -c agent.yaml # HTTP: POST /run (needs the [serve] extra)
105
+ ```
106
+
107
+ ## What it is
108
+
109
+ A small, stable core (the loop, registry, contracts, event bus) surrounded by
110
+ six swappable ports. Every capability that can vary is a plugin behind a port,
111
+ so the production system is reached by adding adapters — never by reopening the
112
+ core. Full source, architecture, and examples:
113
+ **https://github.com/k3-mt/zu**
114
+
115
+ Apache-2.0.
@@ -0,0 +1,5 @@
1
+ zu/__init__.py,sha256=JpeAJGFgcfYQyjDh2GQBLTNVMq1HLUyWyiECbOacHF0,7491
2
+ zu/pipeline.py,sha256=1Ya7fDT5QFf_2NpNPyEVkLlcgHm4Zvy8TzrqjbHLt9g,4015
3
+ zu_runtime-0.2.0.dist-info/METADATA,sha256=QZBtUDvQ6V5uQ5J4QX9aQ0EymYWsFJql33AalJAswsQ,4718
4
+ zu_runtime-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
5
+ zu_runtime-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any