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.
Files changed (41) hide show
  1. flatmachines/__init__.py +136 -0
  2. flatmachines/actions.py +408 -0
  3. flatmachines/adapters/__init__.py +38 -0
  4. flatmachines/adapters/flatagent.py +86 -0
  5. flatmachines/adapters/pi_agent_bridge.py +127 -0
  6. flatmachines/adapters/pi_agent_runner.mjs +99 -0
  7. flatmachines/adapters/smolagents.py +125 -0
  8. flatmachines/agents.py +144 -0
  9. flatmachines/assets/MACHINES.md +141 -0
  10. flatmachines/assets/README.md +11 -0
  11. flatmachines/assets/__init__.py +0 -0
  12. flatmachines/assets/flatagent.d.ts +219 -0
  13. flatmachines/assets/flatagent.schema.json +271 -0
  14. flatmachines/assets/flatagent.slim.d.ts +58 -0
  15. flatmachines/assets/flatagents-runtime.d.ts +523 -0
  16. flatmachines/assets/flatagents-runtime.schema.json +281 -0
  17. flatmachines/assets/flatagents-runtime.slim.d.ts +187 -0
  18. flatmachines/assets/flatmachine.d.ts +403 -0
  19. flatmachines/assets/flatmachine.schema.json +620 -0
  20. flatmachines/assets/flatmachine.slim.d.ts +106 -0
  21. flatmachines/assets/profiles.d.ts +140 -0
  22. flatmachines/assets/profiles.schema.json +93 -0
  23. flatmachines/assets/profiles.slim.d.ts +26 -0
  24. flatmachines/backends.py +222 -0
  25. flatmachines/distributed.py +835 -0
  26. flatmachines/distributed_hooks.py +351 -0
  27. flatmachines/execution.py +638 -0
  28. flatmachines/expressions/__init__.py +60 -0
  29. flatmachines/expressions/cel.py +101 -0
  30. flatmachines/expressions/simple.py +166 -0
  31. flatmachines/flatmachine.py +1263 -0
  32. flatmachines/hooks.py +381 -0
  33. flatmachines/locking.py +69 -0
  34. flatmachines/monitoring.py +505 -0
  35. flatmachines/persistence.py +213 -0
  36. flatmachines/run.py +117 -0
  37. flatmachines/utils.py +166 -0
  38. flatmachines/validation.py +79 -0
  39. flatmachines-1.0.0.dist-info/METADATA +390 -0
  40. flatmachines-1.0.0.dist-info/RECORD +41 -0
  41. 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