zurvan 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. zurvan-0.1.0/.gitignore +35 -0
  2. zurvan-0.1.0/LICENSE +21 -0
  3. zurvan-0.1.0/PKG-INFO +140 -0
  4. zurvan-0.1.0/README.md +112 -0
  5. zurvan-0.1.0/examples/capabilities/README.md +21 -0
  6. zurvan-0.1.0/pyproject.toml +51 -0
  7. zurvan-0.1.0/src/zurvan/__init__.py +75 -0
  8. zurvan-0.1.0/src/zurvan/action.py +64 -0
  9. zurvan-0.1.0/src/zurvan/action_context.py +14 -0
  10. zurvan-0.1.0/src/zurvan/action_registry.py +54 -0
  11. zurvan-0.1.0/src/zurvan/action_transaction.py +38 -0
  12. zurvan-0.1.0/src/zurvan/agent.py +306 -0
  13. zurvan-0.1.0/src/zurvan/agent_function_calling_action_language.py +276 -0
  14. zurvan-0.1.0/src/zurvan/agent_function_calling_action_language_gemini.py +146 -0
  15. zurvan-0.1.0/src/zurvan/agent_function_calling_action_language_groq.py +147 -0
  16. zurvan-0.1.0/src/zurvan/agent_function_calling_action_language_openai.py +27 -0
  17. zurvan-0.1.0/src/zurvan/agent_json_action_language.py +116 -0
  18. zurvan-0.1.0/src/zurvan/agent_language.py +46 -0
  19. zurvan-0.1.0/src/zurvan/agent_registry.py +11 -0
  20. zurvan-0.1.0/src/zurvan/capabilities/__init__.py +2 -0
  21. zurvan-0.1.0/src/zurvan/capabilities/canary_capability.py +130 -0
  22. zurvan-0.1.0/src/zurvan/capabilities/enhanced_time_aware_capability.py +35 -0
  23. zurvan-0.1.0/src/zurvan/capabilities/time_aware_capability.py +64 -0
  24. zurvan-0.1.0/src/zurvan/capability.py +95 -0
  25. zurvan-0.1.0/src/zurvan/decorators.py +157 -0
  26. zurvan-0.1.0/src/zurvan/environment.py +40 -0
  27. zurvan-0.1.0/src/zurvan/goal.py +8 -0
  28. zurvan-0.1.0/src/zurvan/logger/local_text_file_logger.py +30 -0
  29. zurvan-0.1.0/src/zurvan/logger/logger.py +13 -0
  30. zurvan-0.1.0/src/zurvan/memory.py +62 -0
  31. zurvan-0.1.0/src/zurvan/progress_reporter.py +105 -0
  32. zurvan-0.1.0/src/zurvan/prompt.py +9 -0
  33. zurvan-0.1.0/src/zurvan/reversible_action.py +24 -0
  34. zurvan-0.1.0/src/zurvan/token_tracker.py +135 -0
@@ -0,0 +1,35 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+
8
+ # Virtual environments
9
+ .venv/
10
+ venv/
11
+ env/
12
+
13
+ # Build artefacts
14
+ build/
15
+ dist/
16
+ *.egg-info/
17
+ *.egg
18
+
19
+ # Editor / OS
20
+ .DS_Store
21
+ .vscode/
22
+ .idea/
23
+ *.swp
24
+
25
+ # Tests / coverage
26
+ .pytest_cache/
27
+ .coverage
28
+ htmlcov/
29
+ .tox/
30
+
31
+ # Logs
32
+ .logs/
33
+ .venv
34
+ .vscode
35
+ .env
zurvan-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ali Afsahnoudeh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
zurvan-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: zurvan
3
+ Version: 0.1.0
4
+ Summary: A small, composable Python framework for building goal-directed LLM agents (GAME loop + Capability hooks).
5
+ Project-URL: Homepage, https://github.com/aliafsahnoudeh/zurvan
6
+ Project-URL: Repository, https://github.com/aliafsahnoudeh/zurvan
7
+ Project-URL: Issues, https://github.com/aliafsahnoudeh/zurvan/issues
8
+ Author-email: Ali Afsah-Noudeh <aliafsah1988@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,capability,framework,game-loop,llm
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: litellm>=1.50
22
+ Requires-Dist: pydantic>=2
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # zurvan
30
+
31
+ A small, composable Python framework for building goal-directed LLM agents.
32
+
33
+ ## What it is
34
+
35
+ `zurvan` runs a **GAME loop** — Goals, Actions, Memory, Environment — over
36
+ an LLM. Behaviour like plan-first, time-aware, prompt-injection-resistant,
37
+ or progress-tracking is composed by passing a list of `Capability`
38
+ instances rather than by subclassing `Agent`. With no capabilities it
39
+ reduces to a plain Goals → Actions → Memory cycle.
40
+
41
+ LLM providers are abstracted behind `AgentLanguage`. Built-in subclasses
42
+ (via [LiteLLM](https://github.com/BerriAI/litellm)) cover OpenAI, Gemini,
43
+ and Groq.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install zurvan
49
+ ```
50
+
51
+ Requires Python 3.11+.
52
+
53
+ ## Quick start
54
+
55
+ ```python
56
+ from zurvan import (
57
+ Action,
58
+ ActionRegistry,
59
+ Agent,
60
+ AgentFunctionCallingActionLanguageOpenAI,
61
+ Environment,
62
+ Goal,
63
+ )
64
+
65
+ def terminate(message: str) -> str:
66
+ return message
67
+
68
+ actions = ActionRegistry()
69
+ actions.register(Action(
70
+ name="terminate",
71
+ function=terminate,
72
+ description="End the conversation with a final message.",
73
+ parameters={
74
+ "type": "object",
75
+ "properties": {"message": {"type": "string"}},
76
+ "required": ["message"],
77
+ },
78
+ terminal=True,
79
+ ))
80
+
81
+ agent = Agent(
82
+ goals=[Goal(priority=1, name="Greet", description="Greet the user warmly.")],
83
+ agent_language=AgentFunctionCallingActionLanguageOpenAI(model="openai/gpt-4o-mini"),
84
+ action_registry=actions,
85
+ environment=Environment(),
86
+ )
87
+
88
+ memory = agent.run("Say hi.")
89
+ ```
90
+
91
+ ## Pydantic-validated tool inputs
92
+
93
+ `Action` accepts either a JSON-Schema dict (`parameters=`) or a Pydantic
94
+ model (`input_model=`). With a model, the schema sent to the LLM is
95
+ derived from the model and the model's args are **validated before the
96
+ function runs** — a `ValidationError` is caught by `Environment` and
97
+ fed back to the LLM as a failed-tool result, so the model gets a chance
98
+ to retry with corrected args.
99
+
100
+ ```python
101
+ from typing import Literal
102
+ from pydantic import BaseModel
103
+ from zurvan import Action
104
+
105
+ class GetWeatherArgs(BaseModel):
106
+ city: str
107
+ unit: Literal["c", "f"] = "c"
108
+
109
+ def get_weather(city: str, unit: str = "c") -> str:
110
+ return f"It's nice in {city} ({unit}°)"
111
+
112
+ action = Action(
113
+ name="get_weather",
114
+ function=get_weather,
115
+ description="Look up the weather for a city.",
116
+ input_model=GetWeatherArgs,
117
+ )
118
+ ```
119
+
120
+ `parameters=` and `input_model=` are mutually exclusive — pass exactly one.
121
+
122
+ ## Capabilities
123
+
124
+ Capabilities hook into every loop phase (`init`, `start_agent_loop`,
125
+ `process_prompt`, `process_response`, `process_action`, `process_result`,
126
+ `process_new_memories`, `end_agent_loop`, `should_terminate`,
127
+ `terminate`). Compose them — don't subclass `Agent`.
128
+
129
+ ```python
130
+ from zurvan import Agent, CanaryCapability, TimeAwareCapability
131
+
132
+ agent = Agent(
133
+ ...,
134
+ capabilities=[CanaryCapability(), TimeAwareCapability()],
135
+ )
136
+ ```
137
+
138
+ ## License
139
+
140
+ MIT.
zurvan-0.1.0/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # zurvan
2
+
3
+ A small, composable Python framework for building goal-directed LLM agents.
4
+
5
+ ## What it is
6
+
7
+ `zurvan` runs a **GAME loop** — Goals, Actions, Memory, Environment — over
8
+ an LLM. Behaviour like plan-first, time-aware, prompt-injection-resistant,
9
+ or progress-tracking is composed by passing a list of `Capability`
10
+ instances rather than by subclassing `Agent`. With no capabilities it
11
+ reduces to a plain Goals → Actions → Memory cycle.
12
+
13
+ LLM providers are abstracted behind `AgentLanguage`. Built-in subclasses
14
+ (via [LiteLLM](https://github.com/BerriAI/litellm)) cover OpenAI, Gemini,
15
+ and Groq.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install zurvan
21
+ ```
22
+
23
+ Requires Python 3.11+.
24
+
25
+ ## Quick start
26
+
27
+ ```python
28
+ from zurvan import (
29
+ Action,
30
+ ActionRegistry,
31
+ Agent,
32
+ AgentFunctionCallingActionLanguageOpenAI,
33
+ Environment,
34
+ Goal,
35
+ )
36
+
37
+ def terminate(message: str) -> str:
38
+ return message
39
+
40
+ actions = ActionRegistry()
41
+ actions.register(Action(
42
+ name="terminate",
43
+ function=terminate,
44
+ description="End the conversation with a final message.",
45
+ parameters={
46
+ "type": "object",
47
+ "properties": {"message": {"type": "string"}},
48
+ "required": ["message"],
49
+ },
50
+ terminal=True,
51
+ ))
52
+
53
+ agent = Agent(
54
+ goals=[Goal(priority=1, name="Greet", description="Greet the user warmly.")],
55
+ agent_language=AgentFunctionCallingActionLanguageOpenAI(model="openai/gpt-4o-mini"),
56
+ action_registry=actions,
57
+ environment=Environment(),
58
+ )
59
+
60
+ memory = agent.run("Say hi.")
61
+ ```
62
+
63
+ ## Pydantic-validated tool inputs
64
+
65
+ `Action` accepts either a JSON-Schema dict (`parameters=`) or a Pydantic
66
+ model (`input_model=`). With a model, the schema sent to the LLM is
67
+ derived from the model and the model's args are **validated before the
68
+ function runs** — a `ValidationError` is caught by `Environment` and
69
+ fed back to the LLM as a failed-tool result, so the model gets a chance
70
+ to retry with corrected args.
71
+
72
+ ```python
73
+ from typing import Literal
74
+ from pydantic import BaseModel
75
+ from zurvan import Action
76
+
77
+ class GetWeatherArgs(BaseModel):
78
+ city: str
79
+ unit: Literal["c", "f"] = "c"
80
+
81
+ def get_weather(city: str, unit: str = "c") -> str:
82
+ return f"It's nice in {city} ({unit}°)"
83
+
84
+ action = Action(
85
+ name="get_weather",
86
+ function=get_weather,
87
+ description="Look up the weather for a city.",
88
+ input_model=GetWeatherArgs,
89
+ )
90
+ ```
91
+
92
+ `parameters=` and `input_model=` are mutually exclusive — pass exactly one.
93
+
94
+ ## Capabilities
95
+
96
+ Capabilities hook into every loop phase (`init`, `start_agent_loop`,
97
+ `process_prompt`, `process_response`, `process_action`, `process_result`,
98
+ `process_new_memories`, `end_agent_loop`, `should_terminate`,
99
+ `terminate`). Compose them — don't subclass `Agent`.
100
+
101
+ ```python
102
+ from zurvan import Agent, CanaryCapability, TimeAwareCapability
103
+
104
+ agent = Agent(
105
+ ...,
106
+ capabilities=[CanaryCapability(), TimeAwareCapability()],
107
+ )
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT.
@@ -0,0 +1,21 @@
1
+ # Example capabilities (illustrative — not tested, not shipped)
2
+
3
+ The capabilities in this directory are **sketches**, not part of the
4
+ installed package. They reference helpers (`prompt_llm`,
5
+ `action_context.get_action_registry()`) that don't exist in zurvan and
6
+ will not run as-is.
7
+
8
+ They live here as design references for users writing their own
9
+ capabilities — patterns for "plan first" and "track progress after
10
+ each iteration" — not as drop-in code.
11
+
12
+ If you want a working version, you'll need to:
13
+
14
+ 1. Replace `prompt_llm(...)` calls with `agent.agent_language.generate_response(Prompt(messages=[...]))`.
15
+ 2. Get the `ActionRegistry` from the agent (`agent.actions`) rather
16
+ than via `action_context.get_action_registry()` (no such method).
17
+ 3. Decide where the plan/progress prompt should run — `init` for
18
+ plan-first, `end_agent_loop` for progress-tracking — and wire it
19
+ end-to-end.
20
+
21
+ Tested, working capabilities live in [src/zurvan/capabilities/](../../src/zurvan/capabilities/).
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "zurvan"
7
+ version = "0.1.0"
8
+ description = "A small, composable Python framework for building goal-directed LLM agents (GAME loop + Capability hooks)."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ authors = [{ name = "Ali Afsah-Noudeh", email = "aliafsah1988@gmail.com" }]
14
+ keywords = ["agent", "llm", "framework", "game-loop", "capability"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+ dependencies = ["litellm>=1.50", "pydantic>=2"]
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/aliafsahnoudeh/zurvan"
32
+ Repository = "https://github.com/aliafsahnoudeh/zurvan"
33
+ Issues = "https://github.com/aliafsahnoudeh/zurvan/issues"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/zurvan"]
37
+
38
+ [tool.hatch.build.targets.sdist]
39
+ include = ["src/zurvan", "README.md", "LICENSE", "pyproject.toml"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ addopts = "-ra --strict-markers"
44
+ filterwarnings = [
45
+ "ignore::DeprecationWarning:pydantic.*",
46
+ "ignore::DeprecationWarning:litellm.*",
47
+ ]
48
+
49
+ [tool.ruff]
50
+ line-length = 100
51
+ target-version = "py311"
@@ -0,0 +1,75 @@
1
+ """zurvan — composable Python framework for goal-directed LLM agents.
2
+
3
+ The package implements the **GAME loop** (Goals, Actions, Memory,
4
+ Environment) and a :class:`Capability` hook system. Behaviour is
5
+ composed by passing a list of capabilities to the :class:`Agent`,
6
+ not by subclassing.
7
+
8
+ LLM providers are abstracted behind :class:`AgentLanguage`. The
9
+ function-calling subclasses (Gemini, Groq, OpenAI) talk to providers
10
+ via LiteLLM.
11
+ """
12
+
13
+ from zurvan.action import Action
14
+ from zurvan.action_context import ActionContext
15
+ from zurvan.action_registry import ActionRegistry
16
+ from zurvan.agent import Agent
17
+ from zurvan.agent_function_calling_action_language import (
18
+ AgentFunctionCallingActionLanguage,
19
+ )
20
+ from zurvan.agent_function_calling_action_language_gemini import (
21
+ AgentFunctionCallingActionLanguageGemini,
22
+ )
23
+ from zurvan.agent_function_calling_action_language_groq import (
24
+ AgentFunctionCallingActionLanguageGroq,
25
+ )
26
+ from zurvan.agent_function_calling_action_language_openai import (
27
+ AgentFunctionCallingActionLanguageOpenAI,
28
+ )
29
+ from zurvan.agent_language import AgentLanguage
30
+ from zurvan.capabilities.canary_capability import (
31
+ CanaryCapability,
32
+ PromptInjectionDetected,
33
+ )
34
+ from zurvan.capabilities.time_aware_capability import TimeAwareCapability
35
+ from zurvan.capability import Capability
36
+ from zurvan.decorators import register_tool
37
+ from zurvan.environment import Environment
38
+ from zurvan.goal import Goal
39
+ from zurvan.logger.logger import LogLevel, Logger
40
+ from zurvan.memory import Memory
41
+ from zurvan.prompt import Prompt
42
+ from zurvan.token_tracker import TokenTracker
43
+
44
+ __version__ = "0.1.0"
45
+
46
+ __all__ = [
47
+ "__version__",
48
+ # Core GAME primitives
49
+ "Action",
50
+ "ActionContext",
51
+ "ActionRegistry",
52
+ "Agent",
53
+ "Capability",
54
+ "Environment",
55
+ "Goal",
56
+ "Memory",
57
+ "Prompt",
58
+ # Logging
59
+ "LogLevel",
60
+ "Logger",
61
+ # Tool decorator
62
+ "register_tool",
63
+ # Token tracking
64
+ "TokenTracker",
65
+ # Agent languages
66
+ "AgentLanguage",
67
+ "AgentFunctionCallingActionLanguage",
68
+ "AgentFunctionCallingActionLanguageGemini",
69
+ "AgentFunctionCallingActionLanguageGroq",
70
+ "AgentFunctionCallingActionLanguageOpenAI",
71
+ # Capabilities
72
+ "CanaryCapability",
73
+ "PromptInjectionDetected",
74
+ "TimeAwareCapability",
75
+ ]
@@ -0,0 +1,64 @@
1
+ """Action — the interface definition of what an agent can do.
2
+
3
+ Two equivalent shapes are supported:
4
+
5
+ 1. **Dict / JSON Schema** (the original): pass a JSON-Schema dict as
6
+ ``parameters=``. Nothing validates the model's args before
7
+ ``function`` runs — it's on the LLM to send the right shape.
8
+ 2. **Pydantic model**: pass a ``BaseModel`` subclass as
9
+ ``input_model=``. The schema sent to the LLM is derived from the
10
+ model and the model's args are **validated before** ``function``
11
+ runs. ``pydantic.ValidationError`` is allowed to propagate and is
12
+ trapped by ``Environment.execute_action`` like any action exception
13
+ — the failure goes back to the LLM as a tool result, giving the
14
+ model a chance to retry with corrected args.
15
+
16
+ The two paths are mutually exclusive — pass exactly one.
17
+ """
18
+
19
+ from typing import Any, Callable, Dict, Optional, Type
20
+
21
+ from pydantic import BaseModel
22
+
23
+
24
+ class Action:
25
+ def __init__(
26
+ self,
27
+ name: str,
28
+ function: Callable,
29
+ description: str,
30
+ parameters: Optional[Dict] = None,
31
+ input_model: Optional[Type[BaseModel]] = None,
32
+ terminal: bool = False,
33
+ ):
34
+ if parameters is None and input_model is None:
35
+ raise ValueError(
36
+ "Action requires either `parameters` (a JSON-Schema dict) "
37
+ "or `input_model` (a Pydantic BaseModel subclass)."
38
+ )
39
+ if parameters is not None and input_model is not None:
40
+ raise ValueError(
41
+ "Action accepts either `parameters` or `input_model`, not both."
42
+ )
43
+
44
+ self.name = name
45
+ self.function = function
46
+ self.description = description
47
+ self.terminal = terminal
48
+ self.input_model = input_model
49
+ self.parameters = (
50
+ input_model.model_json_schema() if input_model is not None else parameters
51
+ )
52
+
53
+ def execute(self, **args) -> Any:
54
+ """Execute the action's function.
55
+
56
+ With ``input_model`` set, args are first validated through the
57
+ model. ``pydantic.ValidationError`` propagates on failure —
58
+ ``Environment.execute_action`` catches it and surfaces the
59
+ failure to the LLM so the model can retry with corrected args.
60
+ """
61
+ if self.input_model is not None:
62
+ validated = self.input_model(**args)
63
+ return self.function(**validated.model_dump())
64
+ return self.function(**args)
@@ -0,0 +1,14 @@
1
+ from typing import Dict
2
+ import uuid
3
+
4
+
5
+ class ActionContext:
6
+ def __init__(self, properties: Dict = None):
7
+ self.context_id = str(uuid.uuid4())
8
+ self.properties = properties or {}
9
+
10
+ def get(self, key: str, default=None):
11
+ return self.properties.get(key, default)
12
+
13
+ def get_memory(self):
14
+ return self.properties.get("memory", None)
@@ -0,0 +1,54 @@
1
+ from typing import List
2
+
3
+ from zurvan.action import Action
4
+ from zurvan.decorators import tools, tools_by_tag # the global dicts
5
+
6
+
7
+ class ActionRegistry:
8
+ def __init__(self, tags: list[str] | None = None):
9
+ self.actions = {}
10
+ if tags:
11
+ self._load_from_tags(tags)
12
+
13
+ def _load_from_tags(self, tags: list[str]):
14
+ """Load tools matching any of the given tags via tools_by_tag."""
15
+ seen = set()
16
+ for tag in tags:
17
+ for tool_name in tools_by_tag.get(tag, []):
18
+ if tool_name not in seen:
19
+ seen.add(tool_name)
20
+ desc = tools[tool_name]
21
+ self.register(
22
+ Action(
23
+ name=tool_name,
24
+ function=desc["function"],
25
+ description=desc["description"],
26
+ parameters=desc.get("parameters", {}),
27
+ terminal=desc.get("terminal", False),
28
+ )
29
+ )
30
+
31
+ def register(self, action: Action):
32
+ self.actions[action.name] = action
33
+
34
+ def get_action(self, name: str) -> Action | None:
35
+ return self.actions.get(name, None)
36
+
37
+ def get_actions(self) -> List[Action]:
38
+ """Get all registered actions"""
39
+ return list(self.actions.values())
40
+
41
+ def register_from_tools(self, tags: list[str] | None = None):
42
+ """Load decorated tools into this registry, optionally filtered by tags."""
43
+ for name, desc in tools.items():
44
+ if tags and not set(tags) & set(desc.get("tags", [])):
45
+ continue
46
+ self.register(
47
+ Action(
48
+ name=name,
49
+ function=desc["function"],
50
+ description=desc["description"],
51
+ parameters=desc.get("parameters", {}),
52
+ terminal=desc.get("terminal", False),
53
+ )
54
+ )
@@ -0,0 +1,38 @@
1
+ import uuid
2
+
3
+ from zurvan.reversible_action import ReversibleAction
4
+
5
+
6
+ class ActionTransaction:
7
+ def __init__(self):
8
+ self.actions = []
9
+ self.executed = []
10
+ self.committed = False
11
+ self.transaction_id = str(uuid.uuid4())
12
+
13
+ def add(self, action: ReversibleAction, **args):
14
+ """Queue an action for execution."""
15
+ if self.committed:
16
+ raise ValueError("Transaction already committed")
17
+ self.actions.append((action, args))
18
+
19
+ async def execute(self):
20
+ """Execute all actions in the transaction."""
21
+ try:
22
+ for action, args in self.actions:
23
+ result = action.run(**args)
24
+ self.executed.append(action)
25
+ except Exception as e:
26
+ # If any action fails, reverse everything done so far
27
+ await self.rollback()
28
+ raise e
29
+
30
+ async def rollback(self):
31
+ """Reverse all executed actions in reverse order."""
32
+ for action in reversed(self.executed):
33
+ await action.undo()
34
+ self.executed = []
35
+
36
+ def commit(self):
37
+ """Mark transaction as committed."""
38
+ self.committed = True