flatmachines 1.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.
- flatmachines/__init__.py +136 -0
- flatmachines/actions.py +408 -0
- flatmachines/adapters/__init__.py +38 -0
- flatmachines/adapters/flatagent.py +86 -0
- flatmachines/adapters/pi_agent_bridge.py +127 -0
- flatmachines/adapters/pi_agent_runner.mjs +99 -0
- flatmachines/adapters/smolagents.py +125 -0
- flatmachines/agents.py +144 -0
- flatmachines/assets/MACHINES.md +141 -0
- flatmachines/assets/README.md +11 -0
- flatmachines/assets/__init__.py +0 -0
- flatmachines/assets/flatagent.d.ts +219 -0
- flatmachines/assets/flatagent.schema.json +271 -0
- flatmachines/assets/flatagent.slim.d.ts +58 -0
- flatmachines/assets/flatagents-runtime.d.ts +523 -0
- flatmachines/assets/flatagents-runtime.schema.json +281 -0
- flatmachines/assets/flatagents-runtime.slim.d.ts +187 -0
- flatmachines/assets/flatmachine.d.ts +403 -0
- flatmachines/assets/flatmachine.schema.json +620 -0
- flatmachines/assets/flatmachine.slim.d.ts +106 -0
- flatmachines/assets/profiles.d.ts +140 -0
- flatmachines/assets/profiles.schema.json +93 -0
- flatmachines/assets/profiles.slim.d.ts +26 -0
- flatmachines/backends.py +222 -0
- flatmachines/distributed.py +835 -0
- flatmachines/distributed_hooks.py +351 -0
- flatmachines/execution.py +638 -0
- flatmachines/expressions/__init__.py +60 -0
- flatmachines/expressions/cel.py +101 -0
- flatmachines/expressions/simple.py +166 -0
- flatmachines/flatmachine.py +1263 -0
- flatmachines/hooks.py +381 -0
- flatmachines/locking.py +69 -0
- flatmachines/monitoring.py +505 -0
- flatmachines/persistence.py +213 -0
- flatmachines/run.py +117 -0
- flatmachines/utils.py +166 -0
- flatmachines/validation.py +79 -0
- flatmachines-1.0.0.dist-info/METADATA +390 -0
- flatmachines-1.0.0.dist-info/RECORD +41 -0
- flatmachines-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""pi-mono adapter using a Node.js runner (cross-runtime bridge)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..agents import AgentAdapter, AgentAdapterContext, AgentExecutor, AgentRef, AgentResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PiAgentBridgeExecutor(AgentExecutor):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
ref: str,
|
|
17
|
+
config: Optional[Dict[str, Any]],
|
|
18
|
+
runner_path: str,
|
|
19
|
+
node_path: str,
|
|
20
|
+
cwd: str,
|
|
21
|
+
env: Dict[str, str],
|
|
22
|
+
timeout: Optional[float],
|
|
23
|
+
):
|
|
24
|
+
self._ref = ref
|
|
25
|
+
self._config = config or {}
|
|
26
|
+
self._runner_path = runner_path
|
|
27
|
+
self._node_path = node_path
|
|
28
|
+
self._cwd = cwd
|
|
29
|
+
self._env = env
|
|
30
|
+
self._timeout = timeout
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def metadata(self) -> Dict[str, Any]:
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
async def execute(
|
|
37
|
+
self,
|
|
38
|
+
input_data: Dict[str, Any],
|
|
39
|
+
context: Optional[Dict[str, Any]] = None,
|
|
40
|
+
) -> AgentResult:
|
|
41
|
+
request = {
|
|
42
|
+
"ref": self._ref,
|
|
43
|
+
"config": self._config,
|
|
44
|
+
"input": input_data,
|
|
45
|
+
"context": context or {},
|
|
46
|
+
}
|
|
47
|
+
payload = json.dumps(request)
|
|
48
|
+
|
|
49
|
+
proc = await asyncio.create_subprocess_exec(
|
|
50
|
+
self._node_path,
|
|
51
|
+
self._runner_path,
|
|
52
|
+
stdin=asyncio.subprocess.PIPE,
|
|
53
|
+
stdout=asyncio.subprocess.PIPE,
|
|
54
|
+
stderr=asyncio.subprocess.PIPE,
|
|
55
|
+
cwd=self._cwd,
|
|
56
|
+
env=self._env,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(payload.encode()), timeout=self._timeout)
|
|
61
|
+
except asyncio.TimeoutError:
|
|
62
|
+
proc.kill()
|
|
63
|
+
await proc.wait()
|
|
64
|
+
raise TimeoutError("pi-agent runner timed out")
|
|
65
|
+
|
|
66
|
+
if proc.returncode != 0:
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"pi-agent runner failed ({proc.returncode}): {stderr.decode().strip()}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if not stdout:
|
|
72
|
+
return AgentResult()
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
result = json.loads(stdout.decode())
|
|
76
|
+
except json.JSONDecodeError as exc:
|
|
77
|
+
raise ValueError(f"Invalid JSON from pi-agent runner: {stdout.decode()}") from exc
|
|
78
|
+
|
|
79
|
+
return AgentResult(
|
|
80
|
+
output=result.get("output"),
|
|
81
|
+
content=result.get("content"),
|
|
82
|
+
raw=result.get("raw"),
|
|
83
|
+
usage=result.get("usage"),
|
|
84
|
+
cost=result.get("cost"),
|
|
85
|
+
metadata=result.get("metadata"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PiAgentBridgeAdapter(AgentAdapter):
|
|
90
|
+
type_name = "pi-agent"
|
|
91
|
+
|
|
92
|
+
def create_executor(
|
|
93
|
+
self,
|
|
94
|
+
*,
|
|
95
|
+
agent_name: str,
|
|
96
|
+
agent_ref: AgentRef,
|
|
97
|
+
context: AgentAdapterContext,
|
|
98
|
+
) -> AgentExecutor:
|
|
99
|
+
if not agent_ref.ref:
|
|
100
|
+
raise ValueError(f"pi-agent reference missing ref for agent '{agent_name}'")
|
|
101
|
+
|
|
102
|
+
settings = context.settings.get("agent_runners", {}).get("pi_agent", {})
|
|
103
|
+
config = agent_ref.config or {}
|
|
104
|
+
|
|
105
|
+
runner_path = config.get("runner") or settings.get("runner")
|
|
106
|
+
if not runner_path:
|
|
107
|
+
runner_path = os.path.join(os.path.dirname(__file__), "pi_agent_runner.mjs")
|
|
108
|
+
elif not os.path.isabs(runner_path):
|
|
109
|
+
runner_path = os.path.join(context.config_dir, runner_path)
|
|
110
|
+
|
|
111
|
+
node_path = config.get("node") or settings.get("node") or "node"
|
|
112
|
+
timeout = config.get("timeout") or settings.get("timeout")
|
|
113
|
+
cwd = config.get("cwd") or settings.get("cwd") or context.config_dir
|
|
114
|
+
|
|
115
|
+
env = dict(os.environ)
|
|
116
|
+
env.update(settings.get("env", {}) if isinstance(settings.get("env"), dict) else {})
|
|
117
|
+
env.update(config.get("env", {}) if isinstance(config.get("env"), dict) else {})
|
|
118
|
+
|
|
119
|
+
return PiAgentBridgeExecutor(
|
|
120
|
+
ref=agent_ref.ref,
|
|
121
|
+
config=config.get("agent_config") or config.get("config") or {},
|
|
122
|
+
runner_path=runner_path,
|
|
123
|
+
node_path=node_path,
|
|
124
|
+
cwd=cwd,
|
|
125
|
+
env=env,
|
|
126
|
+
timeout=timeout,
|
|
127
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import process from "process";
|
|
6
|
+
|
|
7
|
+
async function readStdin() {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
for await (const chunk of process.stdin) {
|
|
10
|
+
chunks.push(chunk);
|
|
11
|
+
}
|
|
12
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseRef(ref) {
|
|
16
|
+
if (!ref) {
|
|
17
|
+
throw new Error("Missing ref for pi-agent runner");
|
|
18
|
+
}
|
|
19
|
+
const [moduleRef, exportName] = ref.split("#");
|
|
20
|
+
return { moduleRef, exportName: exportName || "buildAgent" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function loadFactory(moduleRef, exportName) {
|
|
24
|
+
const url = moduleRef.startsWith(".") || moduleRef.startsWith("/")
|
|
25
|
+
? pathToFileURL(resolve(moduleRef)).href
|
|
26
|
+
: moduleRef;
|
|
27
|
+
const mod = await import(url);
|
|
28
|
+
const factory = mod[exportName] || mod.default;
|
|
29
|
+
if (!factory) {
|
|
30
|
+
throw new Error(`Factory '${exportName}' not found in ${moduleRef}`);
|
|
31
|
+
}
|
|
32
|
+
return factory;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractText(content) {
|
|
36
|
+
if (!content) return "";
|
|
37
|
+
if (typeof content === "string") return content;
|
|
38
|
+
if (Array.isArray(content)) {
|
|
39
|
+
return content
|
|
40
|
+
.map((part) => {
|
|
41
|
+
if (part.type === "text") return part.text || "";
|
|
42
|
+
return "";
|
|
43
|
+
})
|
|
44
|
+
.join("");
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function run() {
|
|
50
|
+
const raw = await readStdin();
|
|
51
|
+
if (!raw) {
|
|
52
|
+
throw new Error("No input provided to pi-agent runner");
|
|
53
|
+
}
|
|
54
|
+
const request = JSON.parse(raw);
|
|
55
|
+
const { ref, config, input } = request;
|
|
56
|
+
const { moduleRef, exportName } = parseRef(ref);
|
|
57
|
+
|
|
58
|
+
const factory = await loadFactory(moduleRef, exportName);
|
|
59
|
+
const agent = await factory(config || {});
|
|
60
|
+
|
|
61
|
+
if (!agent || typeof agent.prompt !== "function") {
|
|
62
|
+
throw new Error("Factory did not return a pi-mono Agent instance");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const payload = input || {};
|
|
66
|
+
|
|
67
|
+
if (payload.messages) {
|
|
68
|
+
await agent.prompt(payload.messages);
|
|
69
|
+
} else if (payload.message) {
|
|
70
|
+
await agent.prompt(payload.message);
|
|
71
|
+
} else if (payload.task || payload.prompt) {
|
|
72
|
+
const task = payload.task || payload.prompt;
|
|
73
|
+
const images = payload.images || undefined;
|
|
74
|
+
await agent.prompt(task, images);
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error("pi-agent input requires task/prompt or message(s)");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const messages = agent.state?.messages || [];
|
|
80
|
+
const assistant = [...messages].reverse().find((msg) => msg.role === "assistant");
|
|
81
|
+
const content = assistant ? extractText(assistant.content) : "";
|
|
82
|
+
const usage = assistant?.usage || null;
|
|
83
|
+
const cost = usage?.cost?.total ?? null;
|
|
84
|
+
|
|
85
|
+
const response = {
|
|
86
|
+
output: { content },
|
|
87
|
+
content,
|
|
88
|
+
usage,
|
|
89
|
+
cost,
|
|
90
|
+
raw: { messages },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
process.stdout.write(JSON.stringify(response));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
run().catch((err) => {
|
|
97
|
+
console.error(err?.stack || String(err));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""smolagents adapter for FlatMachines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Dict, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from ..agents import AgentAdapter, AgentAdapterContext, AgentExecutor, AgentRef, AgentResult
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from smolagents.agents import MultiStepAgent, RunResult
|
|
15
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
|
16
|
+
raise ImportError("smolagents is required for SmolagentsAdapter") from exc
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SmolagentsExecutor(AgentExecutor):
|
|
20
|
+
def __init__(self, agent: MultiStepAgent, default_kwargs: Optional[Dict[str, Any]] = None):
|
|
21
|
+
self._agent = agent
|
|
22
|
+
self._default_kwargs = default_kwargs or {}
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def metadata(self) -> Dict[str, Any]:
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
async def execute(
|
|
29
|
+
self,
|
|
30
|
+
input_data: Dict[str, Any],
|
|
31
|
+
context: Optional[Dict[str, Any]] = None,
|
|
32
|
+
) -> AgentResult:
|
|
33
|
+
task = input_data.get("task") or input_data.get("prompt")
|
|
34
|
+
if task is None:
|
|
35
|
+
raise ValueError("smolagents adapter requires input.task or input.prompt")
|
|
36
|
+
|
|
37
|
+
run_kwargs = dict(self._default_kwargs)
|
|
38
|
+
additional_args = input_data.get("additional_args")
|
|
39
|
+
if additional_args is not None:
|
|
40
|
+
run_kwargs["additional_args"] = additional_args
|
|
41
|
+
|
|
42
|
+
if input_data.get("max_steps") is not None:
|
|
43
|
+
run_kwargs["max_steps"] = input_data["max_steps"]
|
|
44
|
+
|
|
45
|
+
if input_data.get("return_full_result") is not None:
|
|
46
|
+
run_kwargs["return_full_result"] = input_data["return_full_result"]
|
|
47
|
+
else:
|
|
48
|
+
run_kwargs.setdefault("return_full_result", True)
|
|
49
|
+
|
|
50
|
+
result = await asyncio.to_thread(self._agent.run, task, **run_kwargs)
|
|
51
|
+
|
|
52
|
+
if isinstance(result, RunResult):
|
|
53
|
+
output = result.output
|
|
54
|
+
usage = result.token_usage.dict() if result.token_usage is not None else None
|
|
55
|
+
content = None
|
|
56
|
+
if output is None:
|
|
57
|
+
content = None
|
|
58
|
+
elif isinstance(output, dict):
|
|
59
|
+
content = output.get("content") if isinstance(output.get("content"), str) else None
|
|
60
|
+
else:
|
|
61
|
+
content = str(output)
|
|
62
|
+
output = {"content": content}
|
|
63
|
+
|
|
64
|
+
return AgentResult(
|
|
65
|
+
output=output if isinstance(output, dict) else None,
|
|
66
|
+
content=content,
|
|
67
|
+
raw=result,
|
|
68
|
+
usage=usage,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if isinstance(result, dict):
|
|
72
|
+
return AgentResult(output=result, raw=result)
|
|
73
|
+
|
|
74
|
+
content = None if result is None else str(result)
|
|
75
|
+
return AgentResult(output={"content": content} if content is not None else None, content=content, raw=result)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SmolagentsAdapter(AgentAdapter):
|
|
79
|
+
type_name = "smolagents"
|
|
80
|
+
|
|
81
|
+
def create_executor(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
agent_name: str,
|
|
85
|
+
agent_ref: AgentRef,
|
|
86
|
+
context: AgentAdapterContext,
|
|
87
|
+
) -> AgentExecutor:
|
|
88
|
+
if agent_ref.ref:
|
|
89
|
+
factory = _load_factory(agent_ref.ref, context.config_dir)
|
|
90
|
+
kwargs = agent_ref.config or {}
|
|
91
|
+
agent = factory(**kwargs)
|
|
92
|
+
if not isinstance(agent, MultiStepAgent):
|
|
93
|
+
raise TypeError("smolagents factory did not return MultiStepAgent")
|
|
94
|
+
return SmolagentsExecutor(agent)
|
|
95
|
+
|
|
96
|
+
raise ValueError(f"smolagents reference missing ref for agent '{agent_name}'")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse_ref(ref: str) -> Tuple[str, str]:
|
|
100
|
+
if "#" in ref:
|
|
101
|
+
module_ref, factory_name = ref.split("#", 1)
|
|
102
|
+
else:
|
|
103
|
+
module_ref, factory_name = ref, "build_agent"
|
|
104
|
+
return module_ref, factory_name
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _load_factory(ref: str, config_dir: str):
|
|
108
|
+
module_ref, factory_name = _parse_ref(ref)
|
|
109
|
+
|
|
110
|
+
if module_ref.endswith(".py") or module_ref.startswith(".") or "/" in module_ref:
|
|
111
|
+
module_path = module_ref
|
|
112
|
+
if not os.path.isabs(module_path):
|
|
113
|
+
module_path = os.path.join(config_dir, module_path)
|
|
114
|
+
spec = importlib.util.spec_from_file_location("smolagents_factory", module_path)
|
|
115
|
+
if spec is None or spec.loader is None:
|
|
116
|
+
raise ImportError(f"Unable to load smolagents factory from {module_path}")
|
|
117
|
+
module = importlib.util.module_from_spec(spec)
|
|
118
|
+
spec.loader.exec_module(module)
|
|
119
|
+
else:
|
|
120
|
+
module = importlib.import_module(module_ref)
|
|
121
|
+
|
|
122
|
+
factory = getattr(module, factory_name, None)
|
|
123
|
+
if factory is None:
|
|
124
|
+
raise AttributeError(f"Factory '{factory_name}' not found in {module_ref}")
|
|
125
|
+
return factory
|
flatmachines/agents.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Agent executor interfaces and adapter registry for FlatMachines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, Iterable, Optional, Protocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AgentResult:
|
|
11
|
+
"""Normalized result from an agent execution."""
|
|
12
|
+
|
|
13
|
+
output: Optional[Dict[str, Any]] = None
|
|
14
|
+
content: Optional[str] = None
|
|
15
|
+
raw: Any = None
|
|
16
|
+
usage: Optional[Dict[str, Any]] = None
|
|
17
|
+
cost: Optional[float] = None
|
|
18
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
19
|
+
|
|
20
|
+
def output_payload(self) -> Dict[str, Any]:
|
|
21
|
+
if self.output is not None:
|
|
22
|
+
return self.output
|
|
23
|
+
if self.content is not None:
|
|
24
|
+
return {"content": self.content}
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AgentExecutor(Protocol):
|
|
29
|
+
"""Protocol for running a single agent call."""
|
|
30
|
+
|
|
31
|
+
async def execute(
|
|
32
|
+
self,
|
|
33
|
+
input_data: Dict[str, Any],
|
|
34
|
+
context: Optional[Dict[str, Any]] = None,
|
|
35
|
+
) -> AgentResult:
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def metadata(self) -> Dict[str, Any]: # Optional metadata for execution strategies
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class AgentRef:
|
|
45
|
+
"""Normalized agent reference for adapter resolution."""
|
|
46
|
+
|
|
47
|
+
type: str
|
|
48
|
+
ref: Optional[str] = None
|
|
49
|
+
config: Optional[Dict[str, Any]] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class AgentAdapterContext:
|
|
54
|
+
config_dir: str
|
|
55
|
+
settings: Dict[str, Any]
|
|
56
|
+
machine_name: str
|
|
57
|
+
profiles_file: Optional[str] = None
|
|
58
|
+
profiles_dict: Optional[Dict[str, Any]] = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AgentAdapter(Protocol):
|
|
62
|
+
"""Adapter interface used to build agent executors."""
|
|
63
|
+
|
|
64
|
+
type_name: str
|
|
65
|
+
|
|
66
|
+
def create_executor(
|
|
67
|
+
self,
|
|
68
|
+
*,
|
|
69
|
+
agent_name: str,
|
|
70
|
+
agent_ref: AgentRef,
|
|
71
|
+
context: AgentAdapterContext,
|
|
72
|
+
) -> AgentExecutor:
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AgentAdapterRegistry:
|
|
77
|
+
"""Registry mapping adapter type names to adapter instances."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, adapters: Optional[Iterable[AgentAdapter]] = None):
|
|
80
|
+
self._adapters: Dict[str, AgentAdapter] = {}
|
|
81
|
+
if adapters:
|
|
82
|
+
for adapter in adapters:
|
|
83
|
+
self.register(adapter)
|
|
84
|
+
|
|
85
|
+
def register(self, adapter: AgentAdapter) -> None:
|
|
86
|
+
self._adapters[adapter.type_name] = adapter
|
|
87
|
+
|
|
88
|
+
def get(self, type_name: str) -> AgentAdapter:
|
|
89
|
+
if type_name not in self._adapters:
|
|
90
|
+
raise KeyError(f"No agent adapter registered for type '{type_name}'")
|
|
91
|
+
return self._adapters[type_name]
|
|
92
|
+
|
|
93
|
+
def create_executor(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
agent_name: str,
|
|
97
|
+
agent_ref: AgentRef,
|
|
98
|
+
context: AgentAdapterContext,
|
|
99
|
+
) -> AgentExecutor:
|
|
100
|
+
adapter = self.get(agent_ref.type)
|
|
101
|
+
return adapter.create_executor(
|
|
102
|
+
agent_name=agent_name,
|
|
103
|
+
agent_ref=agent_ref,
|
|
104
|
+
context=context,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
DEFAULT_AGENT_TYPE = "flatagent"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def normalize_agent_ref(raw_ref: Any) -> AgentRef:
|
|
112
|
+
"""Normalize agent reference into AgentRef.
|
|
113
|
+
|
|
114
|
+
Backward compatibility:
|
|
115
|
+
- string -> type=flatagent, ref=string
|
|
116
|
+
- dict with spec: flatagent -> type=flatagent, config=dict
|
|
117
|
+
"""
|
|
118
|
+
if isinstance(raw_ref, str):
|
|
119
|
+
return AgentRef(type=DEFAULT_AGENT_TYPE, ref=raw_ref)
|
|
120
|
+
|
|
121
|
+
if isinstance(raw_ref, dict):
|
|
122
|
+
if "type" in raw_ref:
|
|
123
|
+
return AgentRef(
|
|
124
|
+
type=raw_ref["type"],
|
|
125
|
+
ref=raw_ref.get("ref"),
|
|
126
|
+
config=raw_ref.get("config"),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if raw_ref.get("spec") == "flatagent":
|
|
130
|
+
return AgentRef(type=DEFAULT_AGENT_TYPE, config=raw_ref)
|
|
131
|
+
|
|
132
|
+
raise ValueError(
|
|
133
|
+
"Invalid agent reference. Expected string path or {type, ref/config}."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def coerce_agent_result(value: Any) -> AgentResult:
|
|
138
|
+
if isinstance(value, AgentResult):
|
|
139
|
+
return value
|
|
140
|
+
if isinstance(value, dict):
|
|
141
|
+
return AgentResult(output=value, raw=value)
|
|
142
|
+
if value is None:
|
|
143
|
+
return AgentResult()
|
|
144
|
+
return AgentResult(content=str(value), raw=value)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# FlatAgents + FlatMachines Reference
|
|
2
|
+
|
|
3
|
+
> **Target: <1000 tokens.** LLM-optimized. See `flatagent.d.ts`, `flatmachine.d.ts`, `profiles.d.ts` for schemas.
|
|
4
|
+
>
|
|
5
|
+
> **Versioning:** All specs and SDKs use lockstep versioning.
|
|
6
|
+
|
|
7
|
+
## Concepts
|
|
8
|
+
|
|
9
|
+
**FlatAgent**: Single LLM call. Model + prompts + output schema. No orchestration.
|
|
10
|
+
**FlatMachine**: State machine orchestrating agents. States, transitions, conditions, loops, error handling.
|
|
11
|
+
|
|
12
|
+
| Need | Use |
|
|
13
|
+
|------|-----|
|
|
14
|
+
| Single LLM call | FlatAgent |
|
|
15
|
+
| Multi-step/branching/retry/errors | FlatMachine |
|
|
16
|
+
| Parallel execution | `machine: [a, b, c]` |
|
|
17
|
+
| Dynamic parallelism | `foreach` |
|
|
18
|
+
| Background tasks | `launch` |
|
|
19
|
+
|
|
20
|
+
## Model Profiles
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
# profiles.yml — agents reference by name
|
|
24
|
+
spec: flatprofiles
|
|
25
|
+
spec_version: "0.10.0"
|
|
26
|
+
data:
|
|
27
|
+
model_profiles:
|
|
28
|
+
fast: { provider: cerebras, name: zai-glm-4.6, temperature: 0.6 }
|
|
29
|
+
smart: { provider: anthropic, name: claude-3-opus-20240229 }
|
|
30
|
+
default: fast # Fallback
|
|
31
|
+
# override: smart # Force all
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Agent model field: `"fast"` | `{ profile: "fast", temperature: 0.9 }` | `{ provider: x, name: y }`
|
|
35
|
+
Resolution: default → profile → overrides → override
|
|
36
|
+
|
|
37
|
+
## Agent References
|
|
38
|
+
|
|
39
|
+
`data.agents` values may be:
|
|
40
|
+
- String path to a flatagent config
|
|
41
|
+
- Inline flatagent config (`spec: flatagent`)
|
|
42
|
+
- Typed adapter ref: `{ type: "flatagent" | "smolagents" | "pi-agent", ref?: "...", config?: {...} }`
|
|
43
|
+
|
|
44
|
+
## State Fields
|
|
45
|
+
|
|
46
|
+
| Field | Purpose |
|
|
47
|
+
|-------|---------|
|
|
48
|
+
| `type` | `initial` (entry) / `final` (exit+output) |
|
|
49
|
+
| `agent` | Agent to call |
|
|
50
|
+
| `machine` | Machine(s) — string or `[array]` for parallel |
|
|
51
|
+
| `foreach` | Array expr for dynamic parallelism (`as`: item var, `key`: result key) |
|
|
52
|
+
| `launch` / `launch_input` | Fire-and-forget machine(s) |
|
|
53
|
+
| `input` | Map input to agent/machine |
|
|
54
|
+
| `output_to_context` | Map `output.*` to `context.*` |
|
|
55
|
+
| `execution` | `{ type: retry, backoffs: [2,8,16], jitter: 0.1 }` |
|
|
56
|
+
| `on_error` | State name or `{ default: x, ErrorType: y }` |
|
|
57
|
+
| `transitions` | `[{ condition: "expr", to: state }, { to: default }]` |
|
|
58
|
+
| `mode` | `settled` (all) / `any` (first) for parallel |
|
|
59
|
+
| `timeout` | Seconds (0=forever) |
|
|
60
|
+
|
|
61
|
+
## Patterns
|
|
62
|
+
|
|
63
|
+
**Execution types**: `default` | `retry` (backoffs, jitter) | `parallel` (n_samples) | `mdap_voting` (k_margin, max_candidates)
|
|
64
|
+
|
|
65
|
+
**Transitions**: `condition: "context.score >= 8"` with `to: state`. Last without condition = default.
|
|
66
|
+
|
|
67
|
+
**Loops**: Transition `to: same_state`. Machine has `max_steps` safety.
|
|
68
|
+
|
|
69
|
+
**Errors**: `on_error: state` or per-type. Context gets `last_error`, `last_error_type`.
|
|
70
|
+
|
|
71
|
+
**Parallel machines**:
|
|
72
|
+
```yaml
|
|
73
|
+
machine: [review_a, review_b] # Results keyed by name
|
|
74
|
+
mode: settled # or "any"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Foreach**:
|
|
78
|
+
```yaml
|
|
79
|
+
foreach: "{{ context.items }}"
|
|
80
|
+
as: item
|
|
81
|
+
machine: processor
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Launch** (fire-and-forget):
|
|
85
|
+
```yaml
|
|
86
|
+
launch: background_task
|
|
87
|
+
launch_input: { data: "{{ context.data }}" }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Distributed Worker Pattern
|
|
91
|
+
|
|
92
|
+
Use hook actions (e.g., `DistributedWorkerHooks`) with a `RegistrationBackend` + `WorkBackend` to build worker pools.
|
|
93
|
+
|
|
94
|
+
**Core machines**
|
|
95
|
+
- **Checker**: `get_pool_state` → `calculate_spawn` → `spawn_workers`
|
|
96
|
+
- **Worker**: `register_worker` → `claim_job` → process → `complete_job`/`fail_job` → `deregister_worker`
|
|
97
|
+
- **Reaper**: `list_stale_workers` → `reap_stale_workers`
|
|
98
|
+
|
|
99
|
+
`spawn_workers` expects `worker_config_path` in context (or override hooks to resolve it). Custom queues can compose the base hooks and add actions.
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
context:
|
|
103
|
+
worker_config_path: "./job_worker.yml"
|
|
104
|
+
states:
|
|
105
|
+
check_state: { action: get_pool_state }
|
|
106
|
+
calculate_spawn: { action: calculate_spawn }
|
|
107
|
+
spawn_workers: { action: spawn_workers }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
See `sdk/examples/distributed_worker/` for a full example.
|
|
111
|
+
|
|
112
|
+
## Context Variables
|
|
113
|
+
|
|
114
|
+
`context.*` (all states), `input.*` (initial), `output.*` (in output_to_context), `item`/`as` (foreach)
|
|
115
|
+
|
|
116
|
+
## Hooks
|
|
117
|
+
|
|
118
|
+
`on_machine_start`, `on_machine_end`, `on_state_enter`, `on_state_exit`, `on_transition`, `on_error`, `on_action`
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
class MyHooks(MachineHooks):
|
|
122
|
+
def on_action(self, action: str, context: dict) -> dict:
|
|
123
|
+
if action == "fetch": context["data"] = api_call()
|
|
124
|
+
return context
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Persistence
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
persistence: { enabled: true, backend: local } # local | memory
|
|
131
|
+
```
|
|
132
|
+
Resume: `machine.execute(resume_from=execution_id)`
|
|
133
|
+
|
|
134
|
+
## SDKs
|
|
135
|
+
|
|
136
|
+
### Python SDKs
|
|
137
|
+
- **flatagents** (agents): `pip install flatagents[litellm]`
|
|
138
|
+
- **flatmachines** (orchestration): `pip install flatmachines[flatagents]`
|
|
139
|
+
|
|
140
|
+
### JavaScript SDK
|
|
141
|
+
A single JS SDK lives under [`sdk/js`](./sdk/js). It follows the same specs but is not yet split into separate FlatAgents/FlatMachines packages.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Specification Static Assets
|
|
2
|
+
|
|
3
|
+
**IMPORTANT: The canonical specs are in the repo root.**
|
|
4
|
+
|
|
5
|
+
* `*.d.ts` - copied from canonical spec at repo.
|
|
6
|
+
* `*.slim.d.ts` - No comments, less context.
|
|
7
|
+
* `*.schema.json` - Automated json conversion.
|
|
8
|
+
|
|
9
|
+
These assets are included here for easy availability for machines or to copy to SDKs.
|
|
10
|
+
|
|
11
|
+
It is recommended to directly regenerate these assets in your sdk release with `/scripts/generate-spec-assets.sh`
|
|
File without changes
|