ziro-core 0.2.6__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.
Files changed (281) hide show
  1. ziro/__init__.py +63 -0
  2. ziro/_hf_bootstrap.py +78 -0
  3. ziro/_version.py +24 -0
  4. ziro/agent.py +511 -0
  5. ziro/agents/.subagents/doc_writer.agent.md +37 -0
  6. ziro/agents/.subagents/fact_checker.agent.md +41 -0
  7. ziro/agents/.subagents/reflector.agent.md +27 -0
  8. ziro/agents/.subagents/reviewer.agent.md +35 -0
  9. ziro/agents/.subagents/scout.agent.md +30 -0
  10. ziro/agents/.subagents/solver.agent.md +37 -0
  11. ziro/agents/.subagents/summarizer.agent.md +36 -0
  12. ziro/agents/.subagents/tester.agent.md +42 -0
  13. ziro/agents/analyst/agent_config.yaml +38 -0
  14. ziro/agents/analyst/hooks.yaml +29 -0
  15. ziro/agents/analyst/meta.yaml +13 -0
  16. ziro/agents/analyst/permissions.yaml +10 -0
  17. ziro/agents/analyst/shell_policy.yaml +46 -0
  18. ziro/agents/analyst/subagent_policy.yaml +16 -0
  19. ziro/agents/analyst/tool_policy.yaml +29 -0
  20. ziro/agents/architect/agent_config.yaml +38 -0
  21. ziro/agents/architect/meta.yaml +13 -0
  22. ziro/agents/architect/permissions.yaml +9 -0
  23. ziro/agents/architect/subagent_policy.yaml +14 -0
  24. ziro/agents/architect/tool_policy.yaml +21 -0
  25. ziro/agents/coder/agent_config.yaml +40 -0
  26. ziro/agents/coder/hooks.yaml +31 -0
  27. ziro/agents/coder/meta.yaml +14 -0
  28. ziro/agents/coder/permissions.yaml +13 -0
  29. ziro/agents/coder/shell_policy.yaml +47 -0
  30. ziro/agents/coder/subagent_policy.yaml +17 -0
  31. ziro/agents/coder/tool_policy.yaml +29 -0
  32. ziro/agents/default/agent_config.yaml +110 -0
  33. ziro/agents/default/attachment_policy.yaml +8 -0
  34. ziro/agents/default/compaction_policy.yaml +24 -0
  35. ziro/agents/default/fs_policy.yaml +54 -0
  36. ziro/agents/default/guardrails_policy.yaml +145 -0
  37. ziro/agents/default/handoff_policy.yaml +12 -0
  38. ziro/agents/default/hooks.yaml +44 -0
  39. ziro/agents/default/mcp_servers.yaml +143 -0
  40. ziro/agents/default/memory_policy.yaml +17 -0
  41. ziro/agents/default/meta.yaml +19 -0
  42. ziro/agents/default/modes_policy.yaml +25 -0
  43. ziro/agents/default/permissions.yaml +27 -0
  44. ziro/agents/default/plugin_config.yaml +61 -0
  45. ziro/agents/default/shell_policy.yaml +29 -0
  46. ziro/agents/default/subagent_policy.yaml +32 -0
  47. ziro/agents/default/tool_policy.yaml +46 -0
  48. ziro/agents/default/voice_policy.yaml +29 -0
  49. ziro/agents/default/webfetch_policy.yaml +14 -0
  50. ziro/agents/default/websearch_policy.yaml +21 -0
  51. ziro/agents/life_coach/agent_config.yaml +49 -0
  52. ziro/agents/life_coach/attachment_policy.yaml +8 -0
  53. ziro/agents/life_coach/compaction_policy.yaml +24 -0
  54. ziro/agents/life_coach/guardrails_policy.yaml +145 -0
  55. ziro/agents/life_coach/handoff_policy.yaml +12 -0
  56. ziro/agents/life_coach/mcp_servers.yaml +53 -0
  57. ziro/agents/life_coach/memory_policy.yaml +17 -0
  58. ziro/agents/life_coach/meta.yaml +19 -0
  59. ziro/agents/life_coach/permissions.yaml +24 -0
  60. ziro/agents/life_coach/shell_policy.yaml +28 -0
  61. ziro/agents/life_coach/subagent_policy.yaml +17 -0
  62. ziro/agents/life_coach/tool_policy.yaml +15 -0
  63. ziro/agents/life_coach/voice_policy.yaml +29 -0
  64. ziro/agents/life_coach/webfetch_policy.yaml +14 -0
  65. ziro/agents/life_coach/websearch_policy.yaml +19 -0
  66. ziro/agents/product_manager/agent_config.yaml +36 -0
  67. ziro/agents/product_manager/meta.yaml +14 -0
  68. ziro/agents/product_manager/permissions.yaml +9 -0
  69. ziro/agents/product_manager/subagent_policy.yaml +15 -0
  70. ziro/agents/product_manager/tool_policy.yaml +22 -0
  71. ziro/agents/registry.yaml +10 -0
  72. ziro/agents/researcher/agent_config.yaml +74 -0
  73. ziro/agents/researcher/attachment_policy.yaml +8 -0
  74. ziro/agents/researcher/compaction_policy.yaml +25 -0
  75. ziro/agents/researcher/guardrails_policy.yaml +39 -0
  76. ziro/agents/researcher/handoff_policy.yaml +12 -0
  77. ziro/agents/researcher/hooks.yaml +71 -0
  78. ziro/agents/researcher/mcp_servers.yaml +56 -0
  79. ziro/agents/researcher/memory_policy.yaml +17 -0
  80. ziro/agents/researcher/meta.yaml +18 -0
  81. ziro/agents/researcher/permissions.yaml +22 -0
  82. ziro/agents/researcher/queue_policy.yaml +13 -0
  83. ziro/agents/researcher/shell_policy.yaml +29 -0
  84. ziro/agents/researcher/subagent_policy.yaml +22 -0
  85. ziro/agents/researcher/tool_policy.yaml +24 -0
  86. ziro/agents/researcher/voice_policy.yaml +29 -0
  87. ziro/agents/researcher/webfetch_policy.yaml +15 -0
  88. ziro/agents/researcher/websearch_policy.yaml +22 -0
  89. ziro/agents/researcher_docker/agent_config.yaml +67 -0
  90. ziro/agents/researcher_docker/attachment_policy.yaml +8 -0
  91. ziro/agents/researcher_docker/compaction_policy.yaml +25 -0
  92. ziro/agents/researcher_docker/guardrails_policy.yaml +39 -0
  93. ziro/agents/researcher_docker/handoff_policy.yaml +12 -0
  94. ziro/agents/researcher_docker/hooks.yaml +71 -0
  95. ziro/agents/researcher_docker/mcp_servers.yaml +18 -0
  96. ziro/agents/researcher_docker/memory_policy.yaml +17 -0
  97. ziro/agents/researcher_docker/meta.yaml +18 -0
  98. ziro/agents/researcher_docker/permissions.yaml +22 -0
  99. ziro/agents/researcher_docker/shell_policy.yaml +36 -0
  100. ziro/agents/researcher_docker/subagent_policy.yaml +20 -0
  101. ziro/agents/researcher_docker/tool_policy.yaml +19 -0
  102. ziro/agents/researcher_docker/webfetch_policy.yaml +15 -0
  103. ziro/agents/researcher_docker/websearch_policy.yaml +22 -0
  104. ziro/agents/solver/meta.yaml +7 -0
  105. ziro/agents/solver/permissions.yaml +11 -0
  106. ziro/agents/tester/meta.yaml +7 -0
  107. ziro/agents/tester/permissions.yaml +12 -0
  108. ziro/agents/writer/agent_config.yaml +40 -0
  109. ziro/agents/writer/meta.yaml +13 -0
  110. ziro/agents/writer/permissions.yaml +10 -0
  111. ziro/agents/writer/subagent_policy.yaml +15 -0
  112. ziro/agents/writer/tool_policy.yaml +24 -0
  113. ziro/agents_md.py +49 -0
  114. ziro/capabilities/__init__.py +40 -0
  115. ziro/capabilities/audit.py +71 -0
  116. ziro/capabilities/builtins.py +143 -0
  117. ziro/capabilities/context.py +28 -0
  118. ziro/capabilities/handle.py +41 -0
  119. ziro/capabilities/manager.py +376 -0
  120. ziro/capabilities/models.py +160 -0
  121. ziro/capabilities/registry.py +57 -0
  122. ziro/clarify/__init__.py +0 -0
  123. ziro/clarify/models.py +30 -0
  124. ziro/clarify/tools.py +70 -0
  125. ziro/compaction/__init__.py +40 -0
  126. ziro/compaction/models.py +87 -0
  127. ziro/compaction/node.py +202 -0
  128. ziro/compaction/summarizer.py +76 -0
  129. ziro/compaction/tokenizer.py +87 -0
  130. ziro/compaction/window.py +192 -0
  131. ziro/core/__init__.py +0 -0
  132. ziro/core/agent_md.py +80 -0
  133. ziro/core/agent_profiles.py +720 -0
  134. ziro/core/config.py +826 -0
  135. ziro/core/embeddings.py +142 -0
  136. ziro/core/flavour_apply.py +83 -0
  137. ziro/core/flavour_runtime.py +50 -0
  138. ziro/core/flavours.py +75 -0
  139. ziro/core/paths.py +289 -0
  140. ziro/core/retry.py +93 -0
  141. ziro/core/skills_catalog.py +179 -0
  142. ziro/core/thread_titles.py +48 -0
  143. ziro/core/trust.py +203 -0
  144. ziro/core/ui_prefs.py +47 -0
  145. ziro/core/update_check.py +110 -0
  146. ziro/flavours/chat/flavour.yaml +6 -0
  147. ziro/flavours/chat/persona.md +16 -0
  148. ziro/flavours/engineering/flavour.yaml +22 -0
  149. ziro/flavours/engineering/persona.md +8 -0
  150. ziro/flavours/research/flavour.yaml +30 -0
  151. ziro/flavours/research/persona.md +3 -0
  152. ziro/flavours/safe/flavour.yaml +9 -0
  153. ziro/flavours/safe/persona.md +3 -0
  154. ziro/fs/__init__.py +0 -0
  155. ziro/fs/models.py +69 -0
  156. ziro/fs/scratch.py +82 -0
  157. ziro/fs/tools.py +430 -0
  158. ziro/graph/__init__.py +0 -0
  159. ziro/graph/graph.py +557 -0
  160. ziro/graph/interrupts.py +327 -0
  161. ziro/graph/nodes.py +926 -0
  162. ziro/graph/state.py +147 -0
  163. ziro/guardrails/__init__.py +34 -0
  164. ziro/guardrails/backends.py +613 -0
  165. ziro/guardrails/evaluator.py +90 -0
  166. ziro/guardrails/models.py +136 -0
  167. ziro/guardrails/nodes.py +71 -0
  168. ziro/guardrails/policy_loader.py +30 -0
  169. ziro/handoff/__init__.py +7 -0
  170. ziro/handoff/models.py +27 -0
  171. ziro/handoff/tools.py +54 -0
  172. ziro/hooks/__init__.py +30 -0
  173. ziro/hooks/callables.py +126 -0
  174. ziro/hooks/guards.py +324 -0
  175. ziro/hooks/models.py +120 -0
  176. ziro/hooks/registry.py +86 -0
  177. ziro/hooks/runner.py +31 -0
  178. ziro/io/__init__.py +1 -0
  179. ziro/io/attachments.py +155 -0
  180. ziro/llm/__init__.py +4 -0
  181. ziro/llm/adapter.py +86 -0
  182. ziro/llm/capabilities.py +74 -0
  183. ziro/llm/custom.py +269 -0
  184. ziro/llm/model_tiers.py +31 -0
  185. ziro/llm/models.py +72 -0
  186. ziro/llm/providers/__init__.py +2 -0
  187. ziro/llm/providers/anthropic/__init__.py +26 -0
  188. ziro/llm/providers/anthropic/adapter.py +108 -0
  189. ziro/llm/providers/anthropic/catalog.py +106 -0
  190. ziro/llm/providers/anthropic/connection.py +15 -0
  191. ziro/llm/providers/openai/__init__.py +29 -0
  192. ziro/llm/providers/openai/adapter.py +123 -0
  193. ziro/llm/providers/openai/catalog.py +127 -0
  194. ziro/llm/providers/openai/connection.py +22 -0
  195. ziro/llm/providers/openrouter/__init__.py +27 -0
  196. ziro/llm/providers/openrouter/adapter.py +127 -0
  197. ziro/llm/providers/openrouter/catalog.py +291 -0
  198. ziro/llm/providers/openrouter/connection.py +24 -0
  199. ziro/llm/registry.py +78 -0
  200. ziro/memory/__init__.py +0 -0
  201. ziro/memory/checkpointer.py +52 -0
  202. ziro/memory/models.py +111 -0
  203. ziro/memory/node.py +63 -0
  204. ziro/memory/project_memory.py +153 -0
  205. ziro/memory/reflection.py +88 -0
  206. ziro/memory/store.py +215 -0
  207. ziro/modes/__init__.py +0 -0
  208. ziro/modes/models.py +50 -0
  209. ziro/modes/tools.py +62 -0
  210. ziro/permissions/__init__.py +43 -0
  211. ziro/permissions/gate.py +49 -0
  212. ziro/permissions/hook.py +67 -0
  213. ziro/permissions/mode.py +74 -0
  214. ziro/permissions/models.py +73 -0
  215. ziro/permissions/policy.py +85 -0
  216. ziro/permissions/store.py +37 -0
  217. ziro/permissions/validate.py +122 -0
  218. ziro/plugins/__init__.py +24 -0
  219. ziro/plugins/adapters/__init__.py +1 -0
  220. ziro/plugins/adapters/import_claude_code.py +190 -0
  221. ziro/plugins/consent.py +129 -0
  222. ziro/plugins/discovery.py +568 -0
  223. ziro/plugins/installer.py +277 -0
  224. ziro/plugins/marketplace.py +291 -0
  225. ziro/plugins/models.py +53 -0
  226. ziro/plugins/registry.py +28 -0
  227. ziro/profiles/__init__.py +208 -0
  228. ziro/profiles/spec_profile.py +277 -0
  229. ziro/py.typed +0 -0
  230. ziro/queue/__init__.py +9 -0
  231. ziro/queue/inflight.py +46 -0
  232. ziro/queue/models.py +75 -0
  233. ziro/queue/worker.py +310 -0
  234. ziro/rag/__init__.py +0 -0
  235. ziro/rag/indexer.py +109 -0
  236. ziro/rag/retriever.py +309 -0
  237. ziro/runtime/__init__.py +4 -0
  238. ziro/runtime/runner.py +1103 -0
  239. ziro/spec.py +116 -0
  240. ziro/state/__init__.py +2 -0
  241. ziro/state/env_store.py +185 -0
  242. ziro/state/fileio.py +69 -0
  243. ziro/state/projects_index.py +48 -0
  244. ziro/state/threads_index.py +309 -0
  245. ziro/state/users.py +87 -0
  246. ziro/subagents/__init__.py +12 -0
  247. ziro/subagents/models.py +85 -0
  248. ziro/subagents/orchestrator.py +452 -0
  249. ziro/subagents/tool.py +147 -0
  250. ziro/tasks/__init__.py +25 -0
  251. ziro/tasks/models.py +21 -0
  252. ziro/tasks/reducer.py +58 -0
  253. ziro/tasks/render.py +36 -0
  254. ziro/tasks/tools.py +126 -0
  255. ziro/tools/__init__.py +0 -0
  256. ziro/tools/bootstrap.py +223 -0
  257. ziro/tools/indexer.py +36 -0
  258. ziro/tools/mcp_client.py +305 -0
  259. ziro/tools/mcp_manager.py +494 -0
  260. ziro/tools/mcp_models.py +73 -0
  261. ziro/tools/meta_tools.py +177 -0
  262. ziro/tools/oauth.py +357 -0
  263. ziro/tools/registry.py +277 -0
  264. ziro/tools/shell.py +499 -0
  265. ziro/tools/shell_audit.py +151 -0
  266. ziro/tools/shell_models.py +142 -0
  267. ziro/types.py +27 -0
  268. ziro/voice/__init__.py +7 -0
  269. ziro/voice/backends.py +170 -0
  270. ziro/voice/models.py +65 -0
  271. ziro/voice/pipeline.py +180 -0
  272. ziro/webfetch/__init__.py +6 -0
  273. ziro/webfetch/models.py +22 -0
  274. ziro/webfetch/tool.py +263 -0
  275. ziro/websearch/__init__.py +11 -0
  276. ziro/websearch/models.py +27 -0
  277. ziro/websearch/tool.py +120 -0
  278. ziro_core-0.2.6.dist-info/METADATA +66 -0
  279. ziro_core-0.2.6.dist-info/RECORD +281 -0
  280. ziro_core-0.2.6.dist-info/WHEEL +4 -0
  281. ziro_core-0.2.6.dist-info/licenses/LICENSE +21 -0
ziro/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """Ziro — the public, semver'd Python SDK.
2
+
3
+ ``import ziro`` is the ONLY supported, documented surface. Everything under
4
+ ``app.*`` is internal and may change without a major-version bump. The SDK is a
5
+ thin facade over the F12 ``TurnRunner`` shared turn core: it resolves a profile,
6
+ builds the graph, and drives turns through the one resume loop the CLI also uses.
7
+
8
+ Imports are **lazy** (PEP 562 module ``__getattr__``): ``import ziro`` pulls no
9
+ torch / graph stack — those load only when you touch ``ziro.Ziro`` (i.e. when you
10
+ actually build an agent). This keeps the SDK cheap to import in a host app.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ __all__ = [
17
+ "Ziro",
18
+ "CapabilitiesHandle",
19
+ "TurnResult",
20
+ "StreamEvent",
21
+ "create_agent",
22
+ "Agent",
23
+ "AgentSpec",
24
+ "ToolConfig",
25
+ "Guardrails",
26
+ "PluginConfig",
27
+ ]
28
+
29
+ try:
30
+ # Written at build time by hatch-vcs from the git tag (see pyproject.toml).
31
+ from ziro._version import __version__
32
+ except ImportError: # raw source tree with no tag / no VCS metadata
33
+ __version__ = "0.0.0+unknown"
34
+
35
+ if TYPE_CHECKING: # for type-checkers / IDEs only — no runtime import cost
36
+ from ziro.agent import Agent, Ziro, create_agent
37
+ from ziro.capabilities.handle import CapabilitiesHandle
38
+ from ziro.spec import AgentSpec, Guardrails, PluginConfig, ToolConfig
39
+ from ziro.types import StreamEvent, TurnResult
40
+
41
+
42
+ def __getattr__(name: str):
43
+ if name in ("Ziro", "Agent", "create_agent"):
44
+ from ziro import agent
45
+
46
+ return getattr(agent, name)
47
+ if name in ("AgentSpec", "ToolConfig", "Guardrails", "PluginConfig"):
48
+ from ziro import spec
49
+
50
+ return getattr(spec, name)
51
+ if name == "CapabilitiesHandle":
52
+ from ziro.capabilities.handle import CapabilitiesHandle
53
+
54
+ return CapabilitiesHandle
55
+ if name in ("TurnResult", "StreamEvent"):
56
+ from ziro import types
57
+
58
+ return getattr(types, name)
59
+ raise AttributeError(f"module 'ziro' has no attribute {name!r}")
60
+
61
+
62
+ def __dir__() -> list[str]:
63
+ return sorted(__all__)
ziro/_hf_bootstrap.py ADDED
@@ -0,0 +1,78 @@
1
+ """F24 — HuggingFace offline bootstrap (runs before any transformers/hub import).
2
+
3
+ ``huggingface_hub`` reads ``HF_HUB_OFFLINE`` into a module constant **once, at
4
+ import time**, and it is imported very early (via
5
+ ``langchain_core.language_models.base`` → ``transformers``). Setting the env var any
6
+ later has no effect (verified during implementation). The only chokepoint that runs
7
+ before that import is ``app/__init__.py``, which calls :func:`configure_hf_offline`
8
+ here.
9
+
10
+ Going offline removes ~9 s of HuggingFace Hub revision/etag checks per launch (a
11
+ cached-model construct drops from ~9.2 s to ~0.3 s) **and** pins the embedding space
12
+ to the cached revision the FAISS index was built against. To stay correct on a fresh
13
+ machine, we only go offline when the model is **already cached** (a pure filesystem
14
+ check, no import) — otherwise the one-time download still runs. This module imports
15
+ nothing but :mod:`os` so it can run before the heavy import web.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+
22
+ _EMB_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
23
+
24
+
25
+ def _hub_cache_dir() -> str:
26
+ """The HuggingFace hub cache directory, honouring the standard env overrides."""
27
+ cache = os.environ.get("HUGGINGFACE_HUB_CACHE")
28
+ if cache:
29
+ return cache
30
+ home = os.environ.get("HF_HOME") or os.path.join(
31
+ os.path.expanduser("~"), ".cache", "huggingface"
32
+ )
33
+ return os.path.join(home, "hub")
34
+
35
+
36
+ def _embeddings_cached(model: str = _EMB_MODEL) -> bool:
37
+ """True iff a usable snapshot of ``model`` is already in the hub cache.
38
+
39
+ A usable snapshot carries ``config.json``; checking for it (rather than just the
40
+ snapshots dir) guards against a half-populated cache.
41
+ """
42
+ folder = "models--" + model.replace("/", "--")
43
+ snapshots = os.path.join(_hub_cache_dir(), folder, "snapshots")
44
+ if not os.path.isdir(snapshots):
45
+ return False
46
+ try:
47
+ for snap in os.listdir(snapshots):
48
+ if os.path.exists(os.path.join(snapshots, snap, "config.json")):
49
+ return True
50
+ except OSError:
51
+ return False
52
+ return False
53
+
54
+
55
+ def configure_hf_offline() -> bool:
56
+ """Set ``HF_HUB_OFFLINE``/``TRANSFORMERS_OFFLINE`` when safe. Returns True iff
57
+ offline is in effect. MUST run before any ``huggingface_hub`` import.
58
+
59
+ Precedence: ``ZIRO_NO_ENV_MUTATION=1`` disables all host-env mutation (for
60
+ embedders — ``import ziro`` must not silently touch the host process env);
61
+ an explicit operator ``HF_HUB_OFFLINE`` is never overridden;
62
+ ``ZIRO_EMBEDDINGS_OFFLINE=1`` forces offline regardless of cache; otherwise
63
+ we go offline only when the embeddings model is already cached.
64
+ """
65
+ if os.environ.get("ZIRO_NO_ENV_MUTATION") == "1":
66
+ # Never write to the host env. Report the current offline state, but leave
67
+ # it to the operator to set HF_HUB_OFFLINE explicitly when embedding Ziro.
68
+ explicit_ro = os.environ.get("HF_HUB_OFFLINE")
69
+ return explicit_ro not in (None, "0", "", "false", "False")
70
+ explicit = os.environ.get("HF_HUB_OFFLINE")
71
+ if explicit is not None: # respect an operator's explicit choice, either way
72
+ return explicit not in ("0", "", "false", "False")
73
+ forced = os.environ.get("ZIRO_EMBEDDINGS_OFFLINE", "") in ("1", "true", "True")
74
+ if forced or _embeddings_cached():
75
+ os.environ["HF_HUB_OFFLINE"] = "1"
76
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
77
+ return True
78
+ return False
ziro/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.2.6'
22
+ __version_tuple__ = version_tuple = (0, 2, 6)
23
+
24
+ __commit_id__ = commit_id = None
ziro/agent.py ADDED
@@ -0,0 +1,511 @@
1
+ """``Agent`` — the public facade over the F12 ``TurnRunner`` shared turn core.
2
+
3
+ No new turn logic: resolve a profile (by ``agent=`` YAML id, or an injected
4
+ ``AgentSpec`` via ``create_agent``), open the store/checkpointer, build the
5
+ graph, and drive turns through ``TurnRunner.run_turn`` — the same path the TUI
6
+ and ``-p`` use. Sync methods bridge to the async core on a private daemon
7
+ loop; async methods await it directly.
8
+
9
+ **One-loop rule (replan C5).** A single ``Agent`` instance is either driven all-sync
10
+ (``chat``/``stream``/``open``/``close``) or all-async (``achat``/``astream`` under
11
+ ``async with``) — never mixed. The sync API funnels *everything* (open, turn,
12
+ close) through one private loop; the async API stays on the caller's loop. Mixing
13
+ them would use the async ``AsyncSqliteSaver`` checkpointer across two event loops,
14
+ which raises. A guard below fails loudly instead of corrupting state.
15
+
16
+ **Torch-free import.** Heavy imports (``TurnRunner``, the graph stack → torch) are
17
+ done inside methods, so ``import ziro`` / ``import ziro.agent`` pull no torch until
18
+ you actually construct (with a ``user=``) or drive an ``Agent``.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import threading
24
+ import warnings
25
+ from contextlib import AsyncExitStack
26
+ from typing import (
27
+ TYPE_CHECKING,
28
+ Any,
29
+ AsyncIterator,
30
+ Callable,
31
+ Iterator,
32
+ Optional,
33
+ Sequence,
34
+ Union,
35
+ cast,
36
+ )
37
+
38
+ from ziro.capabilities.handle import CapabilitiesHandle
39
+ from ziro.spec import AgentSpec, Guardrails, PluginConfig, ToolConfig
40
+ from ziro.types import StreamEvent
41
+
42
+ if TYPE_CHECKING:
43
+ from langchain_core.tools import BaseTool
44
+ from langgraph.checkpoint.base import BaseCheckpointSaver
45
+ from langgraph.store.base import BaseStore
46
+
47
+ from ziro.compaction.models import CompactionConfig
48
+ from ziro.hooks.models import HooksConfig, HookSpec
49
+ from ziro.llm.adapter import LLMAdapter
50
+ from ziro.memory.models import MemoryConfig
51
+ from ziro.permissions.models import PermissionPolicy
52
+ from ziro.runtime.runner import TurnResult, TurnRunner
53
+ from ziro.graph.interrupts import InterruptRequest, InterruptResponse
54
+ from ziro.subagents.models import SpawnPolicy
55
+
56
+ InterruptHandler = Callable[[InterruptRequest], InterruptResponse]
57
+
58
+
59
+ class Agent:
60
+ """A reusable agent session over an ``AgentSpec`` (code-first) or a YAML
61
+ agent id. ``user`` binds at construction OR on the first turn
62
+ (``agent.chat("hi", user="alice")``); one instance = one user.
63
+
64
+ ``use_agent()`` switches to a YAML-registry agent even on a spec-driven
65
+ instance (that is what the underlying switch does).
66
+
67
+ Args:
68
+ spec: an ``AgentSpec`` (code-first, via ``create_agent``), or ``None``
69
+ for the YAML-registry path (``agent=``/``model=``).
70
+ user: user id (scopes long-term memory). May be omitted here and
71
+ supplied on the first ``chat``/``turn``/``stream`` call instead.
72
+ agent: agent id to run (None → the manifest/picker default, non-interactive).
73
+ flavour: reserved (F22); passing it now raises NotImplementedError.
74
+ thread: resume a thread by id (None → a fresh thread).
75
+ model: OpenRouter model override (None → the agent's meta / env default).
76
+ provider: reserved (set the provider in the agent's meta.yaml for now).
77
+ reasoning_effort: high|medium|low|off (None → env / meta default).
78
+ on_interrupt: called for a mid-turn HITL interrupt (permission ``ask`` /
79
+ handoff / clarify). Default = fail-closed deny (the ``chat_once`` posture).
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ spec: AgentSpec | None = None,
85
+ *,
86
+ user: Optional[str] = None,
87
+ agent: Optional[str] = None,
88
+ flavour: Optional[str] = None,
89
+ thread: Optional[str] = None,
90
+ model: Optional[str] = None,
91
+ provider: Optional[str] = None,
92
+ reasoning_effort: Optional[str] = None,
93
+ on_interrupt: "Optional[InterruptHandler]" = None,
94
+ ) -> None:
95
+ if flavour is not None:
96
+ raise NotImplementedError("flavour selection lands in F22; pass agent=… for now")
97
+ if provider is not None:
98
+ raise NotImplementedError(
99
+ "per-instance provider override is not wired yet: set `provider:` in "
100
+ "the agent's meta.yaml (or build a custom LLMAdapter and pass llm=)"
101
+ )
102
+ if spec is not None and (agent is not None or model is not None):
103
+ raise ValueError("pass either spec= (code-first) or agent=/model= (YAML), not both")
104
+ self._spec = spec
105
+ self._agent_arg = agent
106
+ self._model_arg = model
107
+ self._effort_arg = reasoning_effort
108
+ self._thread_arg = thread
109
+ self._user = user
110
+ self._runner: "TurnRunner | None" = None
111
+ self._on_interrupt = on_interrupt
112
+ self._stack: Optional[AsyncExitStack] = None
113
+ self._opened = False
114
+ # A private daemon loop so the SYNC API can drive the async core without the
115
+ # caller being on an event loop (the TUI's pattern).
116
+ self._loop = asyncio.new_event_loop()
117
+ self._loop_thread = threading.Thread(
118
+ target=self._loop.run_forever, name="ziro-sdk", daemon=True
119
+ )
120
+ self._loop_thread.start()
121
+ # The loop the session was opened on — the one-loop guard compares against it.
122
+ self._session_loop: Optional[asyncio.AbstractEventLoop] = None
123
+ if user is not None:
124
+ self._ensure_runner()
125
+
126
+ # ---- runner construction (lazy — this is where torch loads) -----------
127
+
128
+ def _ensure_runner(self) -> "TurnRunner":
129
+ if self._runner is not None:
130
+ return self._runner
131
+ if self._user is None:
132
+ raise RuntimeError(
133
+ "no user bound: pass user= to the constructor or to the first "
134
+ "chat/turn/stream call"
135
+ )
136
+ from ziro.runtime.runner import TurnRunner # lazy: pulls the graph stack (torch)
137
+
138
+ extra: dict = {}
139
+ effort = self._effort_arg
140
+ if self._spec is not None:
141
+ from ziro.memory.models import MemoryConfig
142
+ from ziro.profiles.spec_profile import SpecProfile
143
+
144
+ spec = self._spec
145
+ extra["profile"] = SpecProfile(spec)
146
+ if spec.llm is not None and not isinstance(spec.llm, str):
147
+ extra["llm"] = spec.llm
148
+ if spec.memory is not None and not isinstance(spec.memory, MemoryConfig):
149
+ extra["store"] = spec.memory
150
+ if spec.checkpointer is not None:
151
+ extra["checkpointer"] = spec.checkpointer
152
+ effort = effort or spec.reasoning_effort
153
+ self._runner = TurnRunner(
154
+ self._user,
155
+ agent=self._agent_arg,
156
+ thread_id=self._thread_arg,
157
+ interactive=False,
158
+ model=self._model_arg,
159
+ reasoning_effort=effort,
160
+ **extra,
161
+ )
162
+ return self._runner
163
+
164
+ def _bind_user(self, user: Optional[str]) -> None:
165
+ if user is not None:
166
+ if self._user is None:
167
+ self._user = user
168
+ elif user != self._user:
169
+ raise RuntimeError(
170
+ f"this Agent instance is bound to user '{self._user}'; "
171
+ "create another instance (or a gateway session) for a second user"
172
+ )
173
+ self._ensure_runner()
174
+
175
+ # ---- interrupt renderer ------------------------------------------------
176
+
177
+ def _renderer(self) -> "InterruptHandler":
178
+ if self._on_interrupt is not None:
179
+ return self._on_interrupt
180
+ from ziro.graph.interrupts import render_non_interactive # fail-closed deny
181
+
182
+ return render_non_interactive
183
+
184
+ # ---- lifecycle ---------------------------------------------------------
185
+
186
+ def _submit(self, coro: Any) -> Any:
187
+ """Run a coroutine on the private loop from a sync caller and block."""
188
+ return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
189
+
190
+ def _check_loop(self) -> None:
191
+ """One-loop guard: a turn must run on the same loop that opened the session."""
192
+ if not self._opened:
193
+ return
194
+ try:
195
+ running = asyncio.get_running_loop()
196
+ except RuntimeError:
197
+ return
198
+ if self._session_loop is not None and running is not self._session_loop:
199
+ raise RuntimeError(
200
+ "Agent was opened on a different event loop. Use one instance either "
201
+ "all-sync (chat/stream) or all-async (async with … achat/astream), "
202
+ "never both."
203
+ )
204
+
205
+ async def _aopen(self) -> "Agent":
206
+ if not self._opened:
207
+ runner = self._ensure_runner()
208
+ self._session_loop = asyncio.get_running_loop()
209
+ self._stack = AsyncExitStack()
210
+ await self._stack.enter_async_context(runner.session())
211
+ self._opened = True
212
+ return self
213
+
214
+ async def _aclose(self) -> None:
215
+ if self._stack is not None:
216
+ await self._stack.aclose()
217
+ self._stack = None
218
+ self._opened = False
219
+ self._session_loop = None
220
+
221
+ def open(self) -> "Agent":
222
+ """Sync open: build the graph + open store/checkpointer on the private loop."""
223
+ self._submit(self._aopen())
224
+ return self
225
+
226
+ def close(self) -> None:
227
+ if self._opened:
228
+ self._submit(self._aclose())
229
+ self._loop.call_soon_threadsafe(self._loop.stop)
230
+
231
+ def __enter__(self) -> "Agent":
232
+ return self.open()
233
+
234
+ def __exit__(self, *exc: Any) -> None:
235
+ self.close()
236
+
237
+ async def __aenter__(self) -> "Agent":
238
+ return await self._aopen()
239
+
240
+ async def __aexit__(self, *exc: Any) -> None:
241
+ await self._aclose()
242
+ self._loop.call_soon_threadsafe(self._loop.stop)
243
+
244
+ # ---- turn API (all delegate to TurnRunner) -----------------------------
245
+
246
+ async def achat(
247
+ self, message: str, *, attach: Optional[list[str]] = None, user: Optional[str] = None
248
+ ) -> str:
249
+ result = await self.aturn(message, attach=attach, user=user)
250
+ return result.reply
251
+
252
+ def chat(
253
+ self, message: str, *, attach: Optional[list[str]] = None, user: Optional[str] = None
254
+ ) -> str:
255
+ return self._submit(self.achat(message, attach=attach, user=user))
256
+
257
+ async def aturn(
258
+ self, message: str, *, attach: Optional[list[str]] = None, user: Optional[str] = None
259
+ ) -> "TurnResult":
260
+ """Full ``TurnResult`` (guardrail flags, interrupts, raw state).
261
+ ``to_json()`` is the exact chat_once contract."""
262
+ self._bind_user(user)
263
+ await self._aopen()
264
+ self._check_loop()
265
+ runner = self._ensure_runner()
266
+ human, _ = runner.make_human(message, attach)
267
+ return await runner.run_turn(human, renderer=self._renderer())
268
+
269
+ def turn(
270
+ self, message: str, *, attach: Optional[list[str]] = None, user: Optional[str] = None
271
+ ) -> "TurnResult":
272
+ return self._submit(self.aturn(message, attach=attach, user=user))
273
+
274
+ async def astream(
275
+ self, message: str, *, attach: Optional[list[str]] = None, user: Optional[str] = None
276
+ ) -> AsyncIterator[StreamEvent]:
277
+ """Stream the turn's incremental token/state events, then complete."""
278
+ self._bind_user(user)
279
+ await self._aopen()
280
+ self._check_loop()
281
+ runner = self._ensure_runner()
282
+ human, _ = runner.make_human(message, attach)
283
+ queue: "asyncio.Queue[Optional[StreamEvent]]" = asyncio.Queue()
284
+
285
+ def _sink(event: StreamEvent) -> None:
286
+ queue.put_nowait(event)
287
+
288
+ async def _drive() -> None:
289
+ try:
290
+ await runner.run_turn(human, renderer=self._renderer(), on_event=_sink)
291
+ finally:
292
+ queue.put_nowait(None) # sentinel: turn done
293
+
294
+ task = asyncio.ensure_future(_drive())
295
+ try:
296
+ while True:
297
+ event = await queue.get()
298
+ if event is None:
299
+ break
300
+ yield event
301
+ finally:
302
+ await task
303
+
304
+ def stream(
305
+ self, message: str, *, attach: Optional[list[str]] = None, user: Optional[str] = None
306
+ ) -> Iterator[StreamEvent]:
307
+ """Sync stream: drain the async stream over the private loop."""
308
+ agen = self.astream(message, attach=attach, user=user)
309
+ while True:
310
+ try:
311
+ yield self._submit(agen.__anext__())
312
+ except StopAsyncIteration:
313
+ break
314
+
315
+ # ---- session control -----------------------------------------------------
316
+
317
+ @property
318
+ def thread_id(self) -> str:
319
+ return self._ensure_runner().thread_id
320
+
321
+ @property
322
+ def agent_id(self) -> str:
323
+ return self._ensure_runner().profile.agent_id
324
+
325
+ def use_agent(self, agent_id: str) -> None:
326
+ """Switch agent (fresh thread, rebuilt graph) — TurnRunner.switch_agent."""
327
+ self._submit(self._ensure_runner().switch_agent(agent_id))
328
+
329
+ def use_model(self, model: str) -> None:
330
+ """Override the model and rebuild — TurnRunner.switch_model.
331
+
332
+ No-op on a spec-driven agent whose spec injected an ``LLMAdapter``
333
+ object as ``llm``: the injected adapter keeps winning on rebuild
334
+ (``_build_llm``'s short-circuit), so this call has no effect —
335
+ construct a new ``Agent`` with a different spec instead.
336
+ """
337
+ self._submit(self._ensure_runner().switch_model(model))
338
+
339
+ def use_thread(self, thread_id: str) -> None:
340
+ """Resume a thread by id (no rebuild) — TurnRunner.switch_thread."""
341
+ self._ensure_runner().switch_thread(thread_id)
342
+
343
+ def use_flavour(self, name: str) -> None:
344
+ """STUB (F22): apply a capability-preset bundle + swap the default agent."""
345
+ raise NotImplementedError("flavour switching lands in F22")
346
+
347
+ # ---- capability handle (stub interface; F21 fills it) ------------------
348
+
349
+ @property
350
+ def capabilities(self) -> CapabilitiesHandle:
351
+ return CapabilitiesHandle(self._ensure_runner())
352
+
353
+ # ---- embedding: mount as a sub-graph in a host LangGraph / FastAPI app --
354
+
355
+ @property
356
+ def graph(self) -> Any:
357
+ """The live compiled LangGraph (a Runnable) — includes THIS session's
358
+ checkpointer. Requires the session to be open. For mounting inside a host
359
+ graph that owns its own persistence, prefer ``as_runnable()``.
360
+
361
+ ``plugin_gate`` is pre-bound at build from this agent's resolved plugin
362
+ policy (``SpecProfile``/``AgentProfile`` → off-by-default for a code-first
363
+ agent) — the host must **not** supply it; a host-supplied ``thread_id`` /
364
+ ``user_id`` / ``agent_id`` still merges in normally alongside it."""
365
+ if not self._opened:
366
+ self.open()
367
+ from ziro.plugins.discovery import _resolve_plugin_gate
368
+
369
+ r = self._ensure_runner()
370
+ assert r.graph is not None # self.open() above ran session(), which builds it
371
+ gate = _resolve_plugin_gate(r.profile)
372
+ return r.graph.with_config({"configurable": {"plugin_gate": gate}})
373
+
374
+ def as_runnable(self) -> Any:
375
+ """A compiled LangGraph Runnable with **no checkpointer**, for composition
376
+ into a host graph that provides its own persistence (avoids a
377
+ nested-checkpointer conflict). The host must supply the ``configurable``
378
+ keys the graph reads: ``thread_id`` / ``user_id`` / ``agent_id``.
379
+ ``plugin_gate`` is pre-bound at build from this agent's resolved plugin
380
+ policy — the host does not (and must not) supply it, so an embedded
381
+ code-first agent stays hermetic (zero plugins unless ``plugins=`` asked
382
+ for them) regardless of what configurable keys the host passes."""
383
+ if not self._opened:
384
+ self.open()
385
+ from ziro.graph.graph import build_graph
386
+ from ziro.plugins.discovery import _resolve_plugin_gate
387
+
388
+ r = self._ensure_runner()
389
+ assert r._store is not None # open() ran session(), which sets the store
390
+ gate = _resolve_plugin_gate(r.profile)
391
+ graph = build_graph(
392
+ r._build_llm(), r._store, None, registry=r._registry, profile=r.profile
393
+ )
394
+ return graph.with_config({"configurable": {"plugin_gate": gate}})
395
+
396
+
397
+ def _normalize_plugins(
398
+ plugins: "Union[PluginConfig, bool, Sequence[str], None]",
399
+ ) -> "PluginConfig | None":
400
+ """Normalize the ``plugins=`` shorthand to a ``PluginConfig``, mirroring how
401
+ ``tools=`` normalizes to ``ToolConfig``. ``None`` stays None so ``SpecProfile``
402
+ can apply the SDK's default-off surface default."""
403
+ if plugins is None or isinstance(plugins, PluginConfig):
404
+ return plugins
405
+ if isinstance(plugins, bool):
406
+ return PluginConfig(enabled=plugins)
407
+ if isinstance(plugins, (str, bytes)):
408
+ raise TypeError(
409
+ "plugins= expects a sequence of plugin ids (e.g. ['cc/x']), not a bare "
410
+ f"str/bytes; got {plugins!r}. Wrap it in a list: plugins=[{plugins!r}]"
411
+ )
412
+ allow = tuple(plugins)
413
+ if not allow:
414
+ # An empty sequence means "no plugins" — must stay distinguishable from
415
+ # plugins=True (all installed) and must NOT produce enabled=True with an
416
+ # empty allow, since the gate treats empty allow as "no allowlist, all pass".
417
+ return PluginConfig(enabled=False)
418
+ return PluginConfig(enabled=True, allow=allow)
419
+
420
+
421
+ def create_agent(
422
+ *,
423
+ spec: AgentSpec | None = None,
424
+ llm: "Union[LLMAdapter, str, None]" = None,
425
+ system_prompt: Optional[str] = None,
426
+ soul_prompt: Optional[str] = None,
427
+ tools: "Union[ToolConfig, Sequence[Union[str, BaseTool]], None]" = None,
428
+ guardrails: Optional[Guardrails] = None,
429
+ memory: "Union[BaseStore, MemoryConfig, None]" = None,
430
+ checkpointer: "Optional[BaseCheckpointSaver]" = None,
431
+ compaction: "Optional[CompactionConfig]" = None,
432
+ permissions: "Optional[PermissionPolicy]" = None,
433
+ subagents: "Union[SpawnPolicy, Sequence[str], None]" = None,
434
+ mcp_servers: Optional[dict] = None,
435
+ hooks: "Union[HooksConfig, Sequence[HookSpec], None]" = None,
436
+ reasoning_effort: Optional[str] = None,
437
+ plugins: "Union[PluginConfig, bool, Sequence[str], None]" = None,
438
+ name: str = "custom",
439
+ description: str = "",
440
+ user: Optional[str] = None,
441
+ thread: Optional[str] = None,
442
+ on_interrupt: "Optional[InterruptHandler]" = None,
443
+ ) -> Agent:
444
+ """Build an :class:`Agent` from pluggable objects. Everything omitted keeps
445
+ today's defaults (env model, sqlite store/checkpointer, library policies).
446
+ ``create_agent(spec=spec)`` accepts a prebuilt :class:`AgentSpec` directly
447
+ (mutually exclusive with the per-field kwargs)."""
448
+ field_kwargs = (
449
+ llm, system_prompt, soul_prompt, tools, guardrails, memory, checkpointer,
450
+ compaction, permissions, subagents, mcp_servers, hooks, reasoning_effort,
451
+ plugins,
452
+ )
453
+ if spec is not None:
454
+ if any(v is not None for v in field_kwargs) or name != "custom" or description:
455
+ raise ValueError("pass either spec= or per-field kwargs, not both")
456
+ else:
457
+ if tools is None:
458
+ tool_cfg = ToolConfig()
459
+ elif isinstance(tools, ToolConfig):
460
+ tool_cfg = tools
461
+ else:
462
+ tool_cfg = ToolConfig(core=tuple(tools))
463
+ spawn = subagents
464
+ if spawn is not None and not hasattr(spawn, "allowed_children"):
465
+ from ziro.subagents.models import SpawnPolicy
466
+
467
+ spawn = SpawnPolicy(allowed_children=list(spawn))
468
+ hooks_cfg = hooks
469
+ if hooks_cfg is not None and not hasattr(hooks_cfg, "hooks"):
470
+ from ziro.hooks.models import HooksConfig
471
+
472
+ hooks_cfg = HooksConfig(hooks=list(hooks_cfg))
473
+ spec = AgentSpec(
474
+ name=name, description=description, llm=llm,
475
+ system_prompt=system_prompt, soul_prompt=soul_prompt,
476
+ tools=tool_cfg, guardrails=guardrails, memory=memory,
477
+ checkpointer=checkpointer, compaction=compaction,
478
+ permissions=permissions, subagents=cast("SpawnPolicy | None", spawn),
479
+ mcp_servers=mcp_servers, hooks=cast("HooksConfig | None", hooks_cfg),
480
+ reasoning_effort=reasoning_effort,
481
+ plugins=_normalize_plugins(plugins),
482
+ )
483
+ return Agent(spec, user=user, thread=thread, on_interrupt=on_interrupt)
484
+
485
+
486
+ class Ziro(Agent):
487
+ """Deprecated F20 facade name — use :func:`create_agent` / :class:`Agent`.
488
+ Survives one release."""
489
+
490
+ def __init__(
491
+ self,
492
+ *,
493
+ user: str,
494
+ agent: Optional[str] = None,
495
+ flavour: Optional[str] = None,
496
+ thread: Optional[str] = None,
497
+ model: Optional[str] = None,
498
+ provider: Optional[str] = None,
499
+ reasoning_effort: Optional[str] = None,
500
+ on_interrupt: "Optional[InterruptHandler]" = None,
501
+ ) -> None:
502
+ warnings.warn(
503
+ "ziro.Ziro is deprecated; use ziro.create_agent(...) / ziro.Agent",
504
+ DeprecationWarning,
505
+ stacklevel=2,
506
+ )
507
+ super().__init__(
508
+ user=user, agent=agent, flavour=flavour, thread=thread, model=model,
509
+ provider=provider, reasoning_effort=reasoning_effort,
510
+ on_interrupt=on_interrupt,
511
+ )