agentlisp 2.0.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.
@@ -0,0 +1,145 @@
1
+ """Command Line Interface for Agent Lisp Runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import structlog
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from . import __version__
15
+ from .engine import AgentEngine
16
+ from .loader import AgentLoader
17
+
18
+
19
+ def _version_callback(value: bool) -> None:
20
+ if value:
21
+ typer.echo(__version__)
22
+ raise typer.Exit(code=0)
23
+
24
+
25
+ app = typer.Typer(
26
+ name="agentlisp",
27
+ help="Agent Lisp Runtime - Execute Scheme-defined agent workflows",
28
+ no_args_is_help=True,
29
+ )
30
+ console = Console()
31
+ logger = structlog.get_logger()
32
+
33
+
34
+ @app.callback()
35
+ def main(
36
+ version: bool | None = typer.Option(
37
+ None,
38
+ "--version",
39
+ "-V",
40
+ help="Show the Agent Lisp Runtime version and exit.",
41
+ is_eager=True,
42
+ callback=_version_callback,
43
+ ),
44
+ ) -> None:
45
+ """Agent Lisp Runtime CLI entrypoint."""
46
+ return None
47
+
48
+
49
+ @app.command("validate")
50
+ def validate(
51
+ agent_file: Path = typer.Argument( # noqa: B008
52
+ ..., exists=True, readable=True, help="Agent definition file"
53
+ ),
54
+ format: str = typer.Option("json", "--format", "-f", help="Input format: json|yaml|scheme"),
55
+ ) -> None:
56
+ """Validate an agent definition file."""
57
+ try:
58
+ agent = _load_agent(agent_file, format)
59
+ console.print(
60
+ Panel.fit(
61
+ f"[bold green]✓ Agent '{agent.name}' is valid[/bold green]\n"
62
+ f"Purpose: {agent.purpose or '(none)'}\n"
63
+ f"Tools: {len(agent.tools)}\n"
64
+ f"Workflows: {len(agent.workflows)}",
65
+ title="Validation Result",
66
+ border_style="green",
67
+ )
68
+ )
69
+ if agent.workflows:
70
+ table = Table(title="Workflows")
71
+ table.add_column("Name")
72
+ table.add_column("Steps")
73
+ table.add_column("Triggers")
74
+ for wf in agent.workflows:
75
+ table.add_row(wf.name, str(len(wf.steps)), ", ".join(wf.triggers) or "-")
76
+ console.print(table)
77
+ except Exception as exc:
78
+ console.print(f"[bold red]✗ Validation failed:[/bold red] {exc}")
79
+ raise typer.Exit(code=1) from exc
80
+
81
+
82
+ @app.command("run")
83
+ def run_workflow(
84
+ agent_file: Path = typer.Argument( # noqa: B008
85
+ ..., exists=True, readable=True, help="Agent definition file"
86
+ ),
87
+ workflow: str = typer.Argument(..., help="Workflow name to execute"),
88
+ format: str = typer.Option("json", "--format", "-f", help="Input format: json|yaml|scheme"),
89
+ output: Path | None = typer.Option(None, "--output", "-o", help="Write result to file"), # noqa: B008
90
+ ) -> None:
91
+ """Execute a workflow defined in the agent file."""
92
+ import asyncio
93
+
94
+ async def _run() -> None:
95
+ agent = _load_agent(agent_file, format)
96
+ engine = AgentEngine(agent)
97
+ result = await engine.run_workflow(workflow)
98
+ result_dict = result.model_dump(mode="json")
99
+
100
+ result_text = json.dumps(result_dict, indent=2, ensure_ascii=False, default=str)
101
+
102
+ if output:
103
+ output.write_text(result_text, encoding="utf-8")
104
+ console.print(f"[green]Result written to {output}[/green]")
105
+ else:
106
+ console.print_json(result_text)
107
+
108
+ if not result.success:
109
+ raise typer.Exit(code=2)
110
+
111
+ asyncio.run(_run())
112
+
113
+
114
+ @app.command("export")
115
+ def export(
116
+ scheme_file: Path = typer.Argument(..., exists=True, readable=True, help="Scheme .rkt file"), # noqa: B008
117
+ output: Path = typer.Option(Path("agent.json"), "--output", "-o", help="Output JSON file"), # noqa: B008
118
+ racket_exe: str = typer.Option("racket", "--racket", help="Racket executable path"),
119
+ ) -> None:
120
+ """Export a Scheme agent definition to JSON."""
121
+ try:
122
+ agent = AgentLoader.from_scheme(scheme_file, racket_exe=racket_exe)
123
+ output.write_text(
124
+ agent.model_dump_json(indent=2),
125
+ encoding="utf-8",
126
+ )
127
+ console.print(f"[bold green]✓ Exported agent '{agent.name}' to {output}[/bold green]")
128
+ except Exception as exc:
129
+ console.print(f"[bold red]✗ Export failed:[/bold red] {exc}")
130
+ raise typer.Exit(code=1) from exc
131
+
132
+
133
+ def _load_agent(path: Path, fmt: str):
134
+ fmt = fmt.lower()
135
+ if fmt == "json":
136
+ return AgentLoader.from_json(path)
137
+ if fmt in {"yaml", "yml"}:
138
+ return AgentLoader.from_yaml(path)
139
+ if fmt in {"scheme", "racket", "rkt"}:
140
+ return AgentLoader.from_scheme(path)
141
+ raise typer.BadParameter(f"Unsupported format: {fmt}")
142
+
143
+
144
+ if __name__ == "__main__":
145
+ app()
@@ -0,0 +1,253 @@
1
+ """Agent Engine - core execution engine that runs workflows defined via DSL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ import structlog
11
+
12
+ from .models import (
13
+ Agent,
14
+ AtomicAction,
15
+ ExecutionContext,
16
+ ExecutionResult,
17
+ ExecutionStatus,
18
+ Step,
19
+ StepExecutionResult,
20
+ Workflow,
21
+ )
22
+
23
+ logger = structlog.get_logger()
24
+
25
+
26
+ class AgentEngine:
27
+ def __init__(
28
+ self,
29
+ agent: Agent,
30
+ tool_handlers: dict[str, Callable[..., Any]] | None = None,
31
+ ) -> None:
32
+ self._agent = agent
33
+ self._tool_handlers: dict[str, Callable[..., Any]] = tool_handlers or {}
34
+ self._register_agent_tools()
35
+
36
+ def _register_agent_tools(self) -> None:
37
+ for tool in self._agent.tools:
38
+ if tool.handler is not None and tool.name not in self._tool_handlers:
39
+ self._tool_handlers[tool.name] = tool.handler
40
+
41
+ def register_tool_handler(self, tool_name: str, handler: Callable[..., Any]) -> None:
42
+ self._tool_handlers[tool_name] = handler
43
+
44
+ async def run_workflow(
45
+ self,
46
+ workflow_name: str,
47
+ context: ExecutionContext | None = None,
48
+ ) -> ExecutionResult:
49
+ start_time = time.perf_counter()
50
+ context = context or ExecutionContext()
51
+
52
+ workflow = self._agent.get_workflow(workflow_name)
53
+ if workflow is None:
54
+ return ExecutionResult(
55
+ run_id=context.run_id,
56
+ agent_name=self._agent.name,
57
+ workflow_name=workflow_name,
58
+ status=ExecutionStatus.FAILED,
59
+ error=f"Workflow '{workflow_name}' not found",
60
+ )
61
+
62
+ logger.info(
63
+ "workflow.start",
64
+ agent=self._agent.name,
65
+ workflow=workflow_name,
66
+ run_id=str(context.run_id),
67
+ )
68
+
69
+ result = ExecutionResult(
70
+ run_id=context.run_id,
71
+ agent_name=self._agent.name,
72
+ workflow_name=workflow_name,
73
+ status=ExecutionStatus.RUNNING,
74
+ )
75
+
76
+ try:
77
+ await self._execute_steps(workflow, context, result)
78
+ result.status = ExecutionStatus.SUCCESS
79
+ except Exception as exc:
80
+ logger.exception("workflow.error", error=str(exc))
81
+ result.status = ExecutionStatus.FAILED
82
+ result.error = str(exc)
83
+
84
+ result.total_duration_ms = (time.perf_counter() - start_time) * 1000
85
+ logger.info(
86
+ "workflow.end",
87
+ agent=self._agent.name,
88
+ workflow=workflow_name,
89
+ status=result.status.value,
90
+ duration_ms=result.total_duration_ms,
91
+ )
92
+ return result
93
+
94
+ async def _execute_steps(
95
+ self,
96
+ workflow: Workflow,
97
+ context: ExecutionContext,
98
+ result: ExecutionResult,
99
+ ) -> None:
100
+ step_map: dict[str, Step] = {s.id: s for s in workflow.steps}
101
+
102
+ if not workflow.steps:
103
+ return
104
+
105
+ current_step_id: str | None = workflow.steps[0].id
106
+ visited: set[str] = set()
107
+
108
+ while current_step_id is not None:
109
+ if current_step_id in visited:
110
+ raise RuntimeError(f"Cycle detected in workflow at step: {current_step_id}")
111
+ visited.add(current_step_id)
112
+
113
+ step = step_map.get(current_step_id)
114
+ if step is None:
115
+ raise RuntimeError(f"Step not found: {current_step_id}")
116
+
117
+ step_result = await self._execute_step(step, context)
118
+ result.steps.append(step_result)
119
+
120
+ if step_result.status == ExecutionStatus.FAILED:
121
+ raise RuntimeError(f"Step '{step.id}' failed: {step_result.error}")
122
+
123
+ if step.next is None or step.next is False:
124
+ break
125
+
126
+ current_step_id = step.next if isinstance(step.next, str) else None
127
+
128
+ async def _execute_step(
129
+ self,
130
+ step: Step,
131
+ context: ExecutionContext,
132
+ ) -> StepExecutionResult:
133
+ start_time = time.perf_counter()
134
+
135
+ if not self._evaluate_condition(step.condition, context):
136
+ return StepExecutionResult(
137
+ step_id=step.id,
138
+ status=ExecutionStatus.SKIPPED,
139
+ duration_ms=(time.perf_counter() - start_time) * 1000,
140
+ )
141
+
142
+ logger.debug("step.start", step=step.id)
143
+
144
+ try:
145
+ action = step.action
146
+ if isinstance(action, AtomicAction):
147
+ output = await self._execute_atomic_action(action, context)
148
+ elif isinstance(action, dict):
149
+ atomic = AtomicAction(**action)
150
+ output = await self._execute_atomic_action(atomic, context)
151
+ else:
152
+ output = None
153
+
154
+ context.set_result(step.id, output)
155
+
156
+ return StepExecutionResult(
157
+ step_id=step.id,
158
+ status=ExecutionStatus.SUCCESS,
159
+ output=output,
160
+ duration_ms=(time.perf_counter() - start_time) * 1000,
161
+ )
162
+ except Exception as exc:
163
+ logger.exception("step.error", step=step.id, error=str(exc))
164
+ return StepExecutionResult(
165
+ step_id=step.id,
166
+ status=ExecutionStatus.FAILED,
167
+ error=str(exc),
168
+ duration_ms=(time.perf_counter() - start_time) * 1000,
169
+ )
170
+
171
+ def _evaluate_condition(
172
+ self,
173
+ condition: bool | str,
174
+ context: ExecutionContext,
175
+ ) -> bool:
176
+ if isinstance(condition, bool):
177
+ return condition
178
+ if isinstance(condition, str):
179
+ try:
180
+ return bool(eval(condition, {"context": context}))
181
+ except Exception:
182
+ logger.warning("condition.eval_failed", condition=condition)
183
+ return False
184
+ return True
185
+
186
+ async def _execute_atomic_action(
187
+ self,
188
+ action: AtomicAction,
189
+ context: ExecutionContext,
190
+ ) -> Any:
191
+ handler = self._tool_handlers.get(action.target)
192
+ if handler is None:
193
+ logger.warning(
194
+ "handler.not_found",
195
+ target=action.target,
196
+ method=action.method,
197
+ )
198
+ return {
199
+ "_status": "no_handler",
200
+ "target": action.target,
201
+ "method": action.method,
202
+ "params": action.params,
203
+ }
204
+
205
+ resolved_params = self._resolve_params(action.params, context)
206
+ method_fn = getattr(handler, action.method, None) if not callable(handler) else handler
207
+
208
+ if method_fn is not None and callable(method_fn):
209
+ if asyncio.iscoroutinefunction(method_fn):
210
+ return await method_fn(*resolved_params)
211
+ return method_fn(*resolved_params)
212
+
213
+ if callable(handler):
214
+ if asyncio.iscoroutinefunction(handler):
215
+ return await handler(action.method, *resolved_params)
216
+ return handler(action.method, *resolved_params)
217
+
218
+ raise RuntimeError(
219
+ f"No callable handler for target='{action.target}', method='{action.method}'"
220
+ )
221
+
222
+ def _resolve_params(self, params: list[Any], context: ExecutionContext) -> list[Any]:
223
+ resolved: list[Any] = []
224
+ for p in params:
225
+ if isinstance(p, str) and p.startswith("$."):
226
+ var_path = p[2:]
227
+ value = self._resolve_path(var_path, context)
228
+ resolved.append(value)
229
+ else:
230
+ resolved.append(p)
231
+ return resolved
232
+
233
+ @staticmethod
234
+ def _resolve_path(path: str, context: ExecutionContext) -> Any:
235
+ parts = path.split(".")
236
+ if not parts:
237
+ return None
238
+ root = parts[0]
239
+ rest = parts[1:]
240
+ data: Any = None
241
+ if root == "results":
242
+ data = context.results
243
+ elif root == "vars" or root == "variables":
244
+ data = context.variables
245
+ elif root == "meta" or root == "metadata":
246
+ data = context.metadata
247
+ else:
248
+ return None
249
+ for part in rest:
250
+ data = data.get(part) if isinstance(data, dict) else getattr(data, part, None)
251
+ if data is None:
252
+ break
253
+ return data
@@ -0,0 +1,126 @@
1
+ """Agent Loader - loads agent definitions from JSON/YAML/Scheme sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+ from .models import (
13
+ ActionType,
14
+ Agent,
15
+ AtomicAction,
16
+ Step,
17
+ Tool,
18
+ Workflow,
19
+ )
20
+
21
+
22
+ class AgentLoader:
23
+ @staticmethod
24
+ def _normalize_action(action: dict[str, Any] | AtomicAction | Any) -> AtomicAction | Any:
25
+ if isinstance(action, AtomicAction):
26
+ return action
27
+ if isinstance(action, dict):
28
+ kind = action.get("type")
29
+ if kind in {None, ActionType.ATOMIC, "atomic"}:
30
+ return AtomicAction(
31
+ name=action.get("name", ""),
32
+ target=action.get("target", ""),
33
+ method=action.get("method", ""),
34
+ params=list(action.get("params", [])),
35
+ type=ActionType.ATOMIC,
36
+ )
37
+ return action
38
+ return action
39
+
40
+ @classmethod
41
+ def _build_step(cls, step_data: dict[str, Any]) -> Step:
42
+ normalized = dict(step_data)
43
+ if "action" in normalized:
44
+ normalized["action"] = cls._normalize_action(normalized["action"])
45
+ return Step(**normalized)
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: dict[str, Any]) -> Agent:
49
+ tools = [Tool(**t) for t in data.get("tools", [])]
50
+ workflows = [
51
+ Workflow(
52
+ name=wf["name"],
53
+ triggers=wf.get("triggers", []),
54
+ steps=[cls._build_step(s) for s in wf.get("steps", [])],
55
+ )
56
+ for wf in data.get("workflows", [])
57
+ ]
58
+ return Agent(
59
+ name=data["name"],
60
+ purpose=data.get("purpose", ""),
61
+ tools=tools,
62
+ workflows=workflows,
63
+ )
64
+
65
+ @classmethod
66
+ def from_json(cls, path: str | Path) -> Agent:
67
+ path = Path(path)
68
+ with path.open("r", encoding="utf-8") as f:
69
+ data = json.load(f)
70
+ return cls.from_dict(data)
71
+
72
+ @classmethod
73
+ def from_yaml(cls, path: str | Path) -> Agent:
74
+ path = Path(path)
75
+ with path.open("r", encoding="utf-8") as f:
76
+ data = yaml.safe_load(f)
77
+ return cls.from_dict(data)
78
+
79
+ @classmethod
80
+ def from_scheme(
81
+ cls,
82
+ path: str | Path,
83
+ racket_exe: str = "racket",
84
+ scheme_root: str | Path | None = None,
85
+ ) -> Agent:
86
+ path = Path(path)
87
+ script_dir = path.parent
88
+ script = f"""
89
+ #lang racket/base
90
+ (require json
91
+ racket/runtime-path
92
+ (file "{path.name}"))
93
+ (require (lib "agent-dsl/reader.rkt"))
94
+ (display (jsexpr->string (serialize-agent {cls._infer_agent_name(path)})))
95
+ """
96
+ tmp_script = script_dir / "_agent_export_tmp.rkt"
97
+ tmp_script.write_text(script, encoding="utf-8")
98
+
99
+ try:
100
+ env_cmd: list[str] = [racket_exe, str(tmp_script)]
101
+ cwd = Path(scheme_root) if scheme_root else script_dir
102
+ result = subprocess.run(
103
+ env_cmd,
104
+ cwd=cwd,
105
+ capture_output=True,
106
+ text=True,
107
+ timeout=60,
108
+ )
109
+ if result.returncode != 0:
110
+ raise RuntimeError(
111
+ f"Racket execution failed (exit {result.returncode}):\n"
112
+ f"STDOUT: {result.stdout}\nSTDERR: {result.stderr}"
113
+ )
114
+ data = json.loads(result.stdout.strip())
115
+ return cls.from_dict(data)
116
+ finally:
117
+ if tmp_script.exists():
118
+ tmp_script.unlink()
119
+
120
+ @staticmethod
121
+ def _infer_agent_name(path: Path) -> str:
122
+ stem = path.stem
123
+ if "-" in stem:
124
+ parts = stem.split("-")
125
+ return "".join(p for p in parts)
126
+ return stem
@@ -0,0 +1,129 @@
1
+ """Data models for Agent DSL runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from enum import Enum
7
+ from typing import Any
8
+ from uuid import UUID, uuid4
9
+
10
+ from pydantic import BaseModel, Field, field_validator
11
+
12
+
13
+ class ActionType(str, Enum): # noqa: UP042
14
+ ATOMIC = "atomic"
15
+ COMPOSITE = "composite"
16
+ CUSTOM = "custom"
17
+
18
+
19
+ class ExecutionStatus(str, Enum): # noqa: UP042
20
+ PENDING = "pending"
21
+ RUNNING = "running"
22
+ SUCCESS = "success"
23
+ FAILED = "failed"
24
+ SKIPPED = "skipped"
25
+ CANCELLED = "cancelled"
26
+
27
+
28
+ class AtomicAction(BaseModel):
29
+ name: str
30
+ target: str
31
+ method: str
32
+ params: list[Any] = Field(default_factory=list)
33
+ type: ActionType = ActionType.ATOMIC
34
+
35
+
36
+ class Tool(BaseModel):
37
+ name: str
38
+ description: str = ""
39
+ input_schema: dict[str, Any] = Field(default_factory=dict)
40
+ output_schema: dict[str, Any] = Field(default_factory=dict)
41
+ handler: Callable[..., Awaitable[Any] | Any] | None = Field(default=None, exclude=True)
42
+
43
+ @field_validator("name")
44
+ @classmethod
45
+ def name_not_empty(cls, v: str) -> str:
46
+ if not v.strip():
47
+ raise ValueError("Tool name cannot be empty")
48
+ return v
49
+
50
+
51
+ class Step(BaseModel):
52
+ id: str
53
+ action: AtomicAction | dict[str, Any]
54
+ next: str | None = None
55
+ condition: bool | str = True
56
+
57
+ @field_validator("id")
58
+ @classmethod
59
+ def id_not_empty(cls, v: str) -> str:
60
+ if not v.strip():
61
+ raise ValueError("Step id cannot be empty")
62
+ return v
63
+
64
+
65
+ class Workflow(BaseModel):
66
+ name: str
67
+ steps: list[Step] = Field(default_factory=list)
68
+ triggers: list[str] = Field(default_factory=list)
69
+
70
+
71
+ class Agent(BaseModel):
72
+ name: str
73
+ purpose: str = ""
74
+ tools: list[Tool] = Field(default_factory=list)
75
+ workflows: list[Workflow] = Field(default_factory=list)
76
+
77
+ @field_validator("name")
78
+ @classmethod
79
+ def name_not_empty(cls, v: str) -> str:
80
+ if not v.strip():
81
+ raise ValueError("Agent name cannot be empty")
82
+ return v
83
+
84
+ def get_tool(self, tool_name: str) -> Tool | None:
85
+ for tool in self.tools:
86
+ if tool.name == tool_name:
87
+ return tool
88
+ return None
89
+
90
+ def get_workflow(self, workflow_name: str) -> Workflow | None:
91
+ for wf in self.workflows:
92
+ if wf.name == workflow_name:
93
+ return wf
94
+ return None
95
+
96
+
97
+ class ExecutionContext(BaseModel):
98
+ run_id: UUID = Field(default_factory=uuid4)
99
+ variables: dict[str, Any] = Field(default_factory=dict)
100
+ results: dict[str, Any] = Field(default_factory=dict)
101
+ metadata: dict[str, Any] = Field(default_factory=dict)
102
+
103
+ def set_result(self, step_id: str, value: Any) -> None:
104
+ self.results[step_id] = value
105
+
106
+ def get_result(self, step_id: str, default: Any = None) -> Any:
107
+ return self.results.get(step_id, default)
108
+
109
+
110
+ class StepExecutionResult(BaseModel):
111
+ step_id: str
112
+ status: ExecutionStatus
113
+ output: Any = None
114
+ error: str | None = None
115
+ duration_ms: float = 0.0
116
+
117
+
118
+ class ExecutionResult(BaseModel):
119
+ run_id: UUID
120
+ agent_name: str
121
+ workflow_name: str | None
122
+ status: ExecutionStatus
123
+ steps: list[StepExecutionResult] = Field(default_factory=list)
124
+ total_duration_ms: float = 0.0
125
+ error: str | None = None
126
+
127
+ @property
128
+ def success(self) -> bool:
129
+ return self.status == ExecutionStatus.SUCCESS
host/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """AgentLisp v2 host glue package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: list[str] = []