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.
- agentlisp-2.0.0.dist-info/METADATA +482 -0
- agentlisp-2.0.0.dist-info/RECORD +36 -0
- agentlisp-2.0.0.dist-info/WHEEL +4 -0
- agentlisp-2.0.0.dist-info/entry_points.txt +3 -0
- agentlisp_runtime/__init__.py +29 -0
- agentlisp_runtime/cli.py +145 -0
- agentlisp_runtime/engine.py +253 -0
- agentlisp_runtime/loader.py +126 -0
- agentlisp_runtime/models.py +129 -0
- host/__init__.py +5 -0
- host/gateway.py +128 -0
- host/gateway_sse.py +788 -0
- host/sandbox.py +143 -0
- host/sandbox_e2b.py +173 -0
- host/tests/test_infra_docker_e2e.py +247 -0
- host/workflow.py +803 -0
- host/workflow_temporal.py +89 -0
- runtime/__init__.py +29 -0
- runtime/base_agent_harness.py +164 -0
- runtime/base_harness.py +359 -0
- runtime/base_harness_v2.py +1379 -0
- runtime/checker.py +654 -0
- runtime/checkpoint.py +98 -0
- runtime/errors.py +37 -0
- runtime/llm_client.py +644 -0
- runtime/mcp_client.py +86 -0
- runtime/memory_fs.py +90 -0
- runtime/otel_tracer.py +124 -0
- runtime/status_bar.py +86 -0
- runtime/tests/test_ac1_rackunit_18cases_roundtrip.py +255 -0
- runtime/tests/test_cli_if_cli_1_exit_encoding.py +309 -0
- runtime/tests/test_harness_v2.py +2030 -0
- runtime/tests/test_sideeffect_builtin_tools_racket_and_python_ssot_8_items_bitwise_equal.py +132 -0
- runtime/tests/test_srs_acceptance.py +1369 -0
- runtime/tests/test_t2_bench_dataset.py +86 -0
- runtime/tests/test_v2_smoke.py +139 -0
agentlisp_runtime/cli.py
ADDED
|
@@ -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
|