ellements 0.2.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.
- ellements/__init__.py +57 -0
- ellements/agents/__init__.py +45 -0
- ellements/agents/backend.py +100 -0
- ellements/agents/builder.py +303 -0
- ellements/agents/claude_backend.py +200 -0
- ellements/agents/controller.py +237 -0
- ellements/agents/openai_backend.py +187 -0
- ellements/agents/py.typed +0 -0
- ellements/agents/runner.py +358 -0
- ellements/agents/tools.py +30 -0
- ellements/benchmarking/__init__.py +52 -0
- ellements/benchmarking/harness.py +342 -0
- ellements/benchmarking/py.typed +0 -0
- ellements/benchmarking/results.py +173 -0
- ellements/cli/__init__.py +80 -0
- ellements/cli/adapters.py +73 -0
- ellements/cli/agent_tui.py +1178 -0
- ellements/cli/components.py +411 -0
- ellements/cli/printer.py +229 -0
- ellements/cli/py.typed +0 -0
- ellements/core/__init__.py +112 -0
- ellements/core/async_utils.py +42 -0
- ellements/core/budgeting/__init__.py +43 -0
- ellements/core/budgeting/client.py +276 -0
- ellements/core/budgeting/protocol.py +51 -0
- ellements/core/budgeting/trackers.py +177 -0
- ellements/core/caching/__init__.py +51 -0
- ellements/core/caching/cache.py +73 -0
- ellements/core/caching/client.py +300 -0
- ellements/core/caching/disk.py +128 -0
- ellements/core/caching/keys.py +97 -0
- ellements/core/caching/memory.py +104 -0
- ellements/core/chunking.py +262 -0
- ellements/core/config.py +180 -0
- ellements/core/exceptions.py +145 -0
- ellements/core/llm/__init__.py +46 -0
- ellements/core/llm/client.py +1124 -0
- ellements/core/llm/images.py +226 -0
- ellements/core/llm/messages.py +202 -0
- ellements/core/llm/model_params.py +66 -0
- ellements/core/llm/protocol.py +146 -0
- ellements/core/llm/requests.py +100 -0
- ellements/core/llm/structured.py +91 -0
- ellements/core/llm/wrapper.py +79 -0
- ellements/core/observability/__init__.py +39 -0
- ellements/core/observability/events.py +169 -0
- ellements/core/observability/jsonl_logger.py +244 -0
- ellements/core/observability/markdown_formatter.py +197 -0
- ellements/core/observability/observer.py +56 -0
- ellements/core/prompting/__init__.py +14 -0
- ellements/core/prompting/context.py +185 -0
- ellements/core/prompting/guideline.py +133 -0
- ellements/core/prompting/persona.py +267 -0
- ellements/core/prompting/sources.py +92 -0
- ellements/core/py.typed +0 -0
- ellements/core/rate_limit/__init__.py +44 -0
- ellements/core/rate_limit/bucket.py +85 -0
- ellements/core/rate_limit/client.py +216 -0
- ellements/core/rate_limit/protocol.py +27 -0
- ellements/core/templating.py +126 -0
- ellements/core/tools/__init__.py +51 -0
- ellements/core/tools/dialects.py +136 -0
- ellements/core/tools/executor.py +48 -0
- ellements/core/tools/protocol.py +36 -0
- ellements/core/tools/records.py +28 -0
- ellements/core/tools/registry.py +205 -0
- ellements/core/tools/schemas.py +80 -0
- ellements/core/tools/simple.py +119 -0
- ellements/core/tools/spec.py +33 -0
- ellements/domain_specific/__init__.py +7 -0
- ellements/domain_specific/finance/__init__.py +188 -0
- ellements/domain_specific/finance/calculations.py +837 -0
- ellements/domain_specific/finance/charts.py +426 -0
- ellements/domain_specific/finance/portfolio.py +129 -0
- ellements/domain_specific/finance/quant_analysis.py +279 -0
- ellements/domain_specific/finance/risk.py +362 -0
- ellements/domain_specific/finance/technical_indicators.py +241 -0
- ellements/domain_specific/finance/tools.py +483 -0
- ellements/domain_specific/finance/valuation.py +312 -0
- ellements/domain_specific/finance/yahoo_finance.py +1523 -0
- ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
- ellements/domain_specific/py.typed +0 -0
- ellements/execution/__init__.py +56 -0
- ellements/execution/callbacks.py +149 -0
- ellements/execution/catalog.py +70 -0
- ellements/execution/collaborative.py +191 -0
- ellements/execution/config.py +135 -0
- ellements/execution/py.typed +0 -0
- ellements/execution/reflection.py +156 -0
- ellements/execution/self_consistency.py +189 -0
- ellements/execution/single_call.py +48 -0
- ellements/execution/strategies.py +237 -0
- ellements/execution/tree_of_thought.py +541 -0
- ellements/fslm/__init__.py +108 -0
- ellements/fslm/builtins.py +103 -0
- ellements/fslm/cli.py +199 -0
- ellements/fslm/context.py +50 -0
- ellements/fslm/definition.py +163 -0
- ellements/fslm/det.py +173 -0
- ellements/fslm/dsl.py +458 -0
- ellements/fslm/errors.py +38 -0
- ellements/fslm/evaluators.py +141 -0
- ellements/fslm/kernel.py +603 -0
- ellements/fslm/loading.py +123 -0
- ellements/fslm/models.py +623 -0
- ellements/fslm/nl.py +87 -0
- ellements/fslm/observers.py +107 -0
- ellements/fslm/persistence.py +72 -0
- ellements/fslm/py.typed +0 -0
- ellements/fslm/rendering.py +551 -0
- ellements/fslm/visualization.py +25 -0
- ellements/reporting/__init__.py +12 -0
- ellements/reporting/charts.py +364 -0
- ellements/reporting/html_generation.py +132 -0
- ellements/reporting/py.typed +0 -0
- ellements/reporting/visualization.py +73 -0
- ellements/standard_tools/__init__.py +22 -0
- ellements/standard_tools/py.typed +0 -0
- ellements/standard_tools/terminal.py +205 -0
- ellements/standard_tools/web/__init__.py +37 -0
- ellements/standard_tools/web/browser_viewer.py +214 -0
- ellements/standard_tools/web/crawler.py +454 -0
- ellements/standard_tools/web/search.py +399 -0
- ellements/standard_tools/web/youtube.py +1297 -0
- ellements-0.2.0.dist-info/METADATA +368 -0
- ellements-0.2.0.dist-info/RECORD +130 -0
- ellements-0.2.0.dist-info/WHEEL +5 -0
- ellements-0.2.0.dist-info/entry_points.txt +2 -0
- ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
- ellements-0.2.0.dist-info/top_level.txt +1 -0
ellements/fslm/kernel.py
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""Deterministic FSLM kernel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import random
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from typing import Any, Protocol
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from .context import FSLMContext
|
|
12
|
+
from .definition import MachineDefinition, coerce_definition
|
|
13
|
+
from .models import (
|
|
14
|
+
ActionResult,
|
|
15
|
+
ActionSpec,
|
|
16
|
+
DecisionResult,
|
|
17
|
+
FSLMEvent,
|
|
18
|
+
GuardSpec,
|
|
19
|
+
InvariantSpec,
|
|
20
|
+
MachineSnapshot,
|
|
21
|
+
MachineSpec,
|
|
22
|
+
OutputRecord,
|
|
23
|
+
StepResult,
|
|
24
|
+
StepStatus,
|
|
25
|
+
TransitionSpec,
|
|
26
|
+
)
|
|
27
|
+
from .observers import FSLMEventRecord, FSLMObserver
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GuardEvaluator(Protocol):
|
|
31
|
+
"""Evaluates transition guards."""
|
|
32
|
+
|
|
33
|
+
async def evaluate_guard(
|
|
34
|
+
self,
|
|
35
|
+
guard: GuardSpec,
|
|
36
|
+
*,
|
|
37
|
+
spec: MachineSpec | MachineDefinition,
|
|
38
|
+
snapshot: MachineSnapshot,
|
|
39
|
+
event: FSLMEvent,
|
|
40
|
+
) -> DecisionResult:
|
|
41
|
+
"""Return a structured guard decision."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InvariantChecker(Protocol):
|
|
45
|
+
"""Checks state invariants."""
|
|
46
|
+
|
|
47
|
+
async def check_invariant(
|
|
48
|
+
self,
|
|
49
|
+
invariant: InvariantSpec,
|
|
50
|
+
*,
|
|
51
|
+
spec: MachineSpec | MachineDefinition,
|
|
52
|
+
snapshot: MachineSnapshot,
|
|
53
|
+
event: FSLMEvent,
|
|
54
|
+
) -> DecisionResult:
|
|
55
|
+
"""Return a structured invariant decision."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ActionExecutor(Protocol):
|
|
59
|
+
"""Executes action specs selected by a transition."""
|
|
60
|
+
|
|
61
|
+
async def execute_action(
|
|
62
|
+
self,
|
|
63
|
+
action: ActionSpec,
|
|
64
|
+
*,
|
|
65
|
+
spec: MachineSpec | MachineDefinition,
|
|
66
|
+
snapshot: MachineSnapshot,
|
|
67
|
+
event: FSLMEvent,
|
|
68
|
+
) -> ActionResult:
|
|
69
|
+
"""Execute one action and return its result."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FSLMKernel:
|
|
73
|
+
"""Small deterministic FSLM kernel.
|
|
74
|
+
|
|
75
|
+
The kernel owns graph mechanics. Natural-language judgment, tool execution,
|
|
76
|
+
persistence, and coordination plug in through explicit protocols.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
spec: MachineSpec | MachineDefinition,
|
|
82
|
+
*,
|
|
83
|
+
guard_evaluator: GuardEvaluator | None = None,
|
|
84
|
+
invariant_checker: InvariantChecker | None = None,
|
|
85
|
+
action_executor: ActionExecutor | None = None,
|
|
86
|
+
observers: Iterable[FSLMObserver] | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
self.definition = coerce_definition(spec)
|
|
89
|
+
self.spec = self.definition.spec
|
|
90
|
+
self.guard_evaluator = guard_evaluator
|
|
91
|
+
self.invariant_checker = invariant_checker
|
|
92
|
+
self.action_executor = action_executor
|
|
93
|
+
self.observers = list(observers or [])
|
|
94
|
+
|
|
95
|
+
async def step(self, snapshot: MachineSnapshot, event: FSLMEvent) -> StepResult:
|
|
96
|
+
"""Apply one event to one snapshot."""
|
|
97
|
+
await self._emit(
|
|
98
|
+
"EventReceived",
|
|
99
|
+
snapshot.machine_id,
|
|
100
|
+
None,
|
|
101
|
+
{"event_type": event.type, "state": snapshot.current_state},
|
|
102
|
+
)
|
|
103
|
+
candidates = self._candidate_transitions(snapshot, event)
|
|
104
|
+
legal: list[tuple[TransitionSpec, list[DecisionResult]]] = []
|
|
105
|
+
all_guard_results: list[DecisionResult] = []
|
|
106
|
+
for transition in candidates:
|
|
107
|
+
guard_results = await self._evaluate_guards(transition, snapshot, event)
|
|
108
|
+
all_guard_results.extend(guard_results)
|
|
109
|
+
if all(result.allowed for result in guard_results):
|
|
110
|
+
legal.append((transition, guard_results))
|
|
111
|
+
|
|
112
|
+
if not legal:
|
|
113
|
+
result = StepResult(
|
|
114
|
+
event_id=event.id,
|
|
115
|
+
source_state=snapshot.current_state,
|
|
116
|
+
target_state=snapshot.current_state,
|
|
117
|
+
status="no_transition",
|
|
118
|
+
guard_results=all_guard_results,
|
|
119
|
+
new_snapshot=self._advance_snapshot(snapshot, snapshot.current_state),
|
|
120
|
+
trace={"candidate_transitions": [item.name for item in candidates]},
|
|
121
|
+
)
|
|
122
|
+
await self._emit_step_completed(result)
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
selected, random_record = self._select_transition(snapshot, legal)
|
|
126
|
+
selected_transition, selected_guard_results = selected
|
|
127
|
+
await self._emit(
|
|
128
|
+
"TransitionSelected",
|
|
129
|
+
snapshot.machine_id,
|
|
130
|
+
None,
|
|
131
|
+
{
|
|
132
|
+
"transition": selected_transition.name,
|
|
133
|
+
"source": selected_transition.source,
|
|
134
|
+
"target": selected_transition.target,
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
new_snapshot = await self._apply_transition(
|
|
138
|
+
snapshot,
|
|
139
|
+
selected_transition,
|
|
140
|
+
event,
|
|
141
|
+
result_step_id=None,
|
|
142
|
+
)
|
|
143
|
+
invariant_results = await self._check_invariants(new_snapshot, event)
|
|
144
|
+
violations = [
|
|
145
|
+
result.id
|
|
146
|
+
for result in invariant_results
|
|
147
|
+
if not result.allowed
|
|
148
|
+
and self.spec.states[new_snapshot.current_state].invariants
|
|
149
|
+
]
|
|
150
|
+
actions = await self._handle_actions(selected_transition, new_snapshot, event)
|
|
151
|
+
outputs = await self._produce_outputs(selected_transition, new_snapshot, event)
|
|
152
|
+
status: StepStatus = "blocked" if violations else "transitioned"
|
|
153
|
+
result = StepResult(
|
|
154
|
+
event_id=event.id,
|
|
155
|
+
source_state=snapshot.current_state,
|
|
156
|
+
target_state=new_snapshot.current_state,
|
|
157
|
+
status=status,
|
|
158
|
+
selected_transition=selected_transition.name,
|
|
159
|
+
guard_results=selected_guard_results,
|
|
160
|
+
invariant_results=invariant_results,
|
|
161
|
+
outputs=outputs,
|
|
162
|
+
actions=actions,
|
|
163
|
+
violations=violations,
|
|
164
|
+
random=random_record,
|
|
165
|
+
trace={
|
|
166
|
+
"candidate_transitions": [item.name for item in candidates],
|
|
167
|
+
"legal_transitions": [item[0].name for item in legal],
|
|
168
|
+
},
|
|
169
|
+
new_snapshot=new_snapshot,
|
|
170
|
+
)
|
|
171
|
+
await self._emit_step_completed(result)
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
def _candidate_transitions(
|
|
175
|
+
self,
|
|
176
|
+
snapshot: MachineSnapshot,
|
|
177
|
+
event: FSLMEvent,
|
|
178
|
+
) -> list[TransitionSpec]:
|
|
179
|
+
state = self.spec.states[snapshot.current_state]
|
|
180
|
+
names = [*state.transitions, *state.recovery_transitions]
|
|
181
|
+
candidates = []
|
|
182
|
+
for name in names:
|
|
183
|
+
transition = self.spec.transitions[name]
|
|
184
|
+
if transition.trigger.type in (event.type, "*"):
|
|
185
|
+
candidates.append(transition)
|
|
186
|
+
return candidates
|
|
187
|
+
|
|
188
|
+
async def _evaluate_guards(
|
|
189
|
+
self,
|
|
190
|
+
transition: TransitionSpec,
|
|
191
|
+
snapshot: MachineSnapshot,
|
|
192
|
+
event: FSLMEvent,
|
|
193
|
+
) -> list[DecisionResult]:
|
|
194
|
+
results: list[DecisionResult] = []
|
|
195
|
+
for guard in transition.guards:
|
|
196
|
+
if guard.kind == "deterministic" and guard.ref:
|
|
197
|
+
ctx = self._context(snapshot, event, transition)
|
|
198
|
+
result = _decision_from_result(
|
|
199
|
+
guard.id,
|
|
200
|
+
await _invoke(
|
|
201
|
+
self.definition.bindings.resolve(guard.ref, "guard"),
|
|
202
|
+
ctx,
|
|
203
|
+
guard.args,
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
elif self.guard_evaluator is None:
|
|
207
|
+
result = self._default_guard_result(guard, event)
|
|
208
|
+
else:
|
|
209
|
+
result = await self.guard_evaluator.evaluate_guard(
|
|
210
|
+
guard,
|
|
211
|
+
spec=self.spec,
|
|
212
|
+
snapshot=snapshot,
|
|
213
|
+
event=event,
|
|
214
|
+
)
|
|
215
|
+
threshold = (
|
|
216
|
+
guard.min_confidence
|
|
217
|
+
if guard.min_confidence is not None
|
|
218
|
+
else self.spec.policy.confidence.min_guard_confidence
|
|
219
|
+
)
|
|
220
|
+
if result.confidence < threshold:
|
|
221
|
+
result = result.model_copy(
|
|
222
|
+
update={
|
|
223
|
+
"allowed": False,
|
|
224
|
+
"uncertainties": [
|
|
225
|
+
*result.uncertainties,
|
|
226
|
+
f"confidence below threshold {threshold}",
|
|
227
|
+
],
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
results.append(result)
|
|
231
|
+
await self._emit(
|
|
232
|
+
"GuardEvaluated",
|
|
233
|
+
snapshot.machine_id,
|
|
234
|
+
None,
|
|
235
|
+
{
|
|
236
|
+
"id": result.id,
|
|
237
|
+
"allowed": result.allowed,
|
|
238
|
+
"confidence": result.confidence,
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
return results
|
|
242
|
+
|
|
243
|
+
async def _check_invariants(
|
|
244
|
+
self,
|
|
245
|
+
snapshot: MachineSnapshot,
|
|
246
|
+
event: FSLMEvent,
|
|
247
|
+
) -> list[DecisionResult]:
|
|
248
|
+
state = self.spec.states[snapshot.current_state]
|
|
249
|
+
results: list[DecisionResult] = []
|
|
250
|
+
for invariant in state.invariants:
|
|
251
|
+
if invariant.kind == "deterministic" and invariant.ref:
|
|
252
|
+
ctx = self._context(snapshot, event, None)
|
|
253
|
+
result = _decision_from_result(
|
|
254
|
+
invariant.id,
|
|
255
|
+
await _invoke(
|
|
256
|
+
self.definition.bindings.resolve(
|
|
257
|
+
invariant.ref,
|
|
258
|
+
"invariant",
|
|
259
|
+
),
|
|
260
|
+
ctx,
|
|
261
|
+
invariant.args,
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
elif self.invariant_checker is None:
|
|
265
|
+
result = self._default_invariant_result(invariant, snapshot, event)
|
|
266
|
+
else:
|
|
267
|
+
result = await self.invariant_checker.check_invariant(
|
|
268
|
+
invariant,
|
|
269
|
+
spec=self.spec,
|
|
270
|
+
snapshot=snapshot,
|
|
271
|
+
event=event,
|
|
272
|
+
)
|
|
273
|
+
threshold = (
|
|
274
|
+
invariant.min_confidence
|
|
275
|
+
if invariant.min_confidence is not None
|
|
276
|
+
else self.spec.policy.confidence.min_invariant_confidence
|
|
277
|
+
)
|
|
278
|
+
if result.confidence < threshold:
|
|
279
|
+
result = result.model_copy(
|
|
280
|
+
update={
|
|
281
|
+
"allowed": False,
|
|
282
|
+
"uncertainties": [
|
|
283
|
+
*result.uncertainties,
|
|
284
|
+
f"confidence below threshold {threshold}",
|
|
285
|
+
],
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
results.append(result)
|
|
289
|
+
await self._emit(
|
|
290
|
+
"InvariantChecked",
|
|
291
|
+
snapshot.machine_id,
|
|
292
|
+
None,
|
|
293
|
+
{"id": result.id, "allowed": result.allowed},
|
|
294
|
+
)
|
|
295
|
+
return results
|
|
296
|
+
|
|
297
|
+
async def _handle_actions(
|
|
298
|
+
self,
|
|
299
|
+
transition: TransitionSpec,
|
|
300
|
+
snapshot: MachineSnapshot,
|
|
301
|
+
event: FSLMEvent,
|
|
302
|
+
) -> list[ActionResult]:
|
|
303
|
+
results: list[ActionResult] = []
|
|
304
|
+
for action in transition.actions:
|
|
305
|
+
if self.spec.policy.execution == "dry_run" or self.action_executor is None:
|
|
306
|
+
if self.spec.policy.execution == "execute":
|
|
307
|
+
executed = await self._execute_bound_action(action, snapshot, event)
|
|
308
|
+
result = executed or ActionResult(
|
|
309
|
+
action_name=action.name or action.tool or action.ref or "action",
|
|
310
|
+
tool=action.tool or action.ref or "python",
|
|
311
|
+
status="planned",
|
|
312
|
+
message="action planned but no executor/binding was configured",
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
result = ActionResult(
|
|
316
|
+
action_name=action.name or action.tool or action.ref or "action",
|
|
317
|
+
tool=action.tool or action.ref or "python",
|
|
318
|
+
status="planned",
|
|
319
|
+
message="action planned but not executed",
|
|
320
|
+
)
|
|
321
|
+
else:
|
|
322
|
+
result = await self.action_executor.execute_action(
|
|
323
|
+
action,
|
|
324
|
+
spec=self.spec,
|
|
325
|
+
snapshot=snapshot,
|
|
326
|
+
event=event,
|
|
327
|
+
)
|
|
328
|
+
results.append(result)
|
|
329
|
+
return results
|
|
330
|
+
|
|
331
|
+
def _select_transition(
|
|
332
|
+
self,
|
|
333
|
+
snapshot: MachineSnapshot,
|
|
334
|
+
legal: list[tuple[TransitionSpec, list[DecisionResult]]],
|
|
335
|
+
) -> tuple[tuple[TransitionSpec, list[DecisionResult]], dict[str, object]]:
|
|
336
|
+
if len(legal) == 1 and legal[0][0].weight is None:
|
|
337
|
+
return legal[0], {}
|
|
338
|
+
if not any(item[0].weight is not None for item in legal):
|
|
339
|
+
return legal[0], {}
|
|
340
|
+
rng = random.Random(snapshot.random_seed)
|
|
341
|
+
for _ in range(snapshot.random_draws):
|
|
342
|
+
rng.random()
|
|
343
|
+
weights = [item[0].weight or 1.0 for item in legal]
|
|
344
|
+
total = sum(weights)
|
|
345
|
+
draw = rng.random()
|
|
346
|
+
threshold = draw * total
|
|
347
|
+
cumulative = 0.0
|
|
348
|
+
selected_index = len(legal) - 1
|
|
349
|
+
for index, weight in enumerate(weights):
|
|
350
|
+
cumulative += weight
|
|
351
|
+
if threshold <= cumulative:
|
|
352
|
+
selected_index = index
|
|
353
|
+
break
|
|
354
|
+
record: dict[str, object] = {
|
|
355
|
+
"seed": snapshot.random_seed,
|
|
356
|
+
"draw_index": snapshot.random_draws,
|
|
357
|
+
"draw": draw,
|
|
358
|
+
"weights": {
|
|
359
|
+
legal[index][0].name: weights[index] for index in range(len(legal))
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
return legal[selected_index], record
|
|
363
|
+
|
|
364
|
+
async def _execute_bound_action(
|
|
365
|
+
self,
|
|
366
|
+
action: ActionSpec,
|
|
367
|
+
snapshot: MachineSnapshot,
|
|
368
|
+
event: FSLMEvent,
|
|
369
|
+
) -> ActionResult | None:
|
|
370
|
+
ctx = self._context(snapshot, event, None)
|
|
371
|
+
if action.kind == "deterministic" and action.ref:
|
|
372
|
+
payload = await _invoke(
|
|
373
|
+
self.definition.bindings.resolve(action.ref, "action"),
|
|
374
|
+
ctx,
|
|
375
|
+
action.args,
|
|
376
|
+
)
|
|
377
|
+
return _action_from_result(action, payload)
|
|
378
|
+
if action.kind == "tool" and action.tool and self.definition.bindings.tools:
|
|
379
|
+
arguments = dict(action.arguments)
|
|
380
|
+
arguments_ref = action.args.get("arguments_ref")
|
|
381
|
+
if isinstance(arguments_ref, str):
|
|
382
|
+
produced = await _invoke(
|
|
383
|
+
self.definition.bindings.resolve(arguments_ref, "action"),
|
|
384
|
+
ctx,
|
|
385
|
+
action.args,
|
|
386
|
+
)
|
|
387
|
+
if isinstance(produced, dict):
|
|
388
|
+
arguments.update(produced)
|
|
389
|
+
output = await ctx.call_tool(action.tool, **arguments)
|
|
390
|
+
return ActionResult(
|
|
391
|
+
action_name=action.name or action.tool,
|
|
392
|
+
tool=action.tool,
|
|
393
|
+
status="executed",
|
|
394
|
+
output=output if isinstance(output, dict) else {"value": output},
|
|
395
|
+
)
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
async def _produce_outputs(
|
|
399
|
+
self,
|
|
400
|
+
transition: TransitionSpec,
|
|
401
|
+
snapshot: MachineSnapshot,
|
|
402
|
+
event: FSLMEvent,
|
|
403
|
+
) -> list[OutputRecord]:
|
|
404
|
+
records: list[OutputRecord] = []
|
|
405
|
+
for output in transition.emits:
|
|
406
|
+
payload: dict[str, Any] = {}
|
|
407
|
+
if output.kind == "deterministic" and output.ref:
|
|
408
|
+
produced = await _invoke(
|
|
409
|
+
self.definition.bindings.resolve(output.ref, "output"),
|
|
410
|
+
self._context(snapshot, event, transition),
|
|
411
|
+
output.args,
|
|
412
|
+
)
|
|
413
|
+
if isinstance(produced, OutputRecord):
|
|
414
|
+
records.append(produced)
|
|
415
|
+
continue
|
|
416
|
+
if isinstance(produced, dict):
|
|
417
|
+
payload = produced
|
|
418
|
+
else:
|
|
419
|
+
payload = {"value": produced}
|
|
420
|
+
elif output.kind == "nl":
|
|
421
|
+
payload = {"instruction": output.text}
|
|
422
|
+
records.append(
|
|
423
|
+
OutputRecord(
|
|
424
|
+
type=output.type,
|
|
425
|
+
payload=payload,
|
|
426
|
+
description=output.description,
|
|
427
|
+
destination=output.destination,
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
return records
|
|
431
|
+
|
|
432
|
+
async def _apply_transition(
|
|
433
|
+
self,
|
|
434
|
+
snapshot: MachineSnapshot,
|
|
435
|
+
transition: TransitionSpec,
|
|
436
|
+
event: FSLMEvent,
|
|
437
|
+
result_step_id: str | None,
|
|
438
|
+
) -> MachineSnapshot:
|
|
439
|
+
variables = dict(snapshot.variables)
|
|
440
|
+
variables.update(_resolve_direct_effects(transition.effects, variables))
|
|
441
|
+
for effect in transition.effect_calls:
|
|
442
|
+
ctx = self._context(snapshot, event, transition, step_id=result_step_id)
|
|
443
|
+
patch = await _invoke(
|
|
444
|
+
self.definition.bindings.resolve(effect.ref, "effect"),
|
|
445
|
+
ctx,
|
|
446
|
+
effect.args,
|
|
447
|
+
)
|
|
448
|
+
if isinstance(patch, dict):
|
|
449
|
+
variables.update(patch)
|
|
450
|
+
return self._advance_snapshot(
|
|
451
|
+
snapshot,
|
|
452
|
+
transition.target,
|
|
453
|
+
variables=variables,
|
|
454
|
+
consumed_random=transition.weight is not None,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def _context(
|
|
458
|
+
self,
|
|
459
|
+
snapshot: MachineSnapshot,
|
|
460
|
+
event: FSLMEvent,
|
|
461
|
+
transition: TransitionSpec | None,
|
|
462
|
+
*,
|
|
463
|
+
step_id: str | None = None,
|
|
464
|
+
) -> FSLMContext:
|
|
465
|
+
return FSLMContext(
|
|
466
|
+
spec=self.spec,
|
|
467
|
+
definition=self.definition,
|
|
468
|
+
state=self.spec.states[snapshot.current_state],
|
|
469
|
+
transition=transition,
|
|
470
|
+
snapshot=snapshot,
|
|
471
|
+
event=event,
|
|
472
|
+
vars=snapshot.variables,
|
|
473
|
+
step_id=step_id or "",
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
@staticmethod
|
|
477
|
+
def _advance_snapshot(
|
|
478
|
+
snapshot: MachineSnapshot,
|
|
479
|
+
state: str,
|
|
480
|
+
*,
|
|
481
|
+
variables: dict[str, object] | None = None,
|
|
482
|
+
consumed_random: bool = False,
|
|
483
|
+
) -> MachineSnapshot:
|
|
484
|
+
return snapshot.model_copy(
|
|
485
|
+
update={
|
|
486
|
+
"id": uuid4().hex,
|
|
487
|
+
"current_state": state,
|
|
488
|
+
"variables": variables if variables is not None else snapshot.variables,
|
|
489
|
+
"step_index": snapshot.step_index + 1,
|
|
490
|
+
"random_draws": snapshot.random_draws + (1 if consumed_random else 0),
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
@staticmethod
|
|
495
|
+
def _default_guard_result(guard: GuardSpec, event: FSLMEvent) -> DecisionResult:
|
|
496
|
+
if guard.kind == "nl":
|
|
497
|
+
return DecisionResult(
|
|
498
|
+
id=guard.id,
|
|
499
|
+
allowed=False,
|
|
500
|
+
confidence=0.0,
|
|
501
|
+
uncertainties=["no natural-language guard evaluator configured"],
|
|
502
|
+
)
|
|
503
|
+
guard_values = event.payload.get("guards", {})
|
|
504
|
+
allowed = bool(guard_values.get(guard.id, False)) if isinstance(guard_values, dict) else False
|
|
505
|
+
return DecisionResult(id=guard.id, allowed=allowed)
|
|
506
|
+
|
|
507
|
+
@staticmethod
|
|
508
|
+
def _default_invariant_result(
|
|
509
|
+
invariant: InvariantSpec,
|
|
510
|
+
snapshot: MachineSnapshot,
|
|
511
|
+
event: FSLMEvent,
|
|
512
|
+
) -> DecisionResult:
|
|
513
|
+
if invariant.kind == "nl":
|
|
514
|
+
return DecisionResult(
|
|
515
|
+
id=invariant.id,
|
|
516
|
+
allowed=False,
|
|
517
|
+
confidence=0.0,
|
|
518
|
+
uncertainties=["no natural-language invariant checker configured"],
|
|
519
|
+
)
|
|
520
|
+
invariant_values = event.payload.get("invariants")
|
|
521
|
+
if not isinstance(invariant_values, dict):
|
|
522
|
+
invariant_values = snapshot.variables.get("invariants", {})
|
|
523
|
+
allowed = bool(invariant_values.get(invariant.id, True)) if isinstance(invariant_values, dict) else True
|
|
524
|
+
return DecisionResult(id=invariant.id, allowed=allowed)
|
|
525
|
+
|
|
526
|
+
async def _emit(
|
|
527
|
+
self,
|
|
528
|
+
type: str,
|
|
529
|
+
machine_id: str,
|
|
530
|
+
step_id: str | None,
|
|
531
|
+
payload: dict[str, object],
|
|
532
|
+
) -> None:
|
|
533
|
+
if not self.observers:
|
|
534
|
+
return
|
|
535
|
+
record = FSLMEventRecord(
|
|
536
|
+
type=type,
|
|
537
|
+
machine_id=machine_id,
|
|
538
|
+
step_id=step_id,
|
|
539
|
+
payload=payload,
|
|
540
|
+
)
|
|
541
|
+
for observer in self.observers:
|
|
542
|
+
await observer.on_event(record)
|
|
543
|
+
|
|
544
|
+
async def _emit_step_completed(self, result: StepResult) -> None:
|
|
545
|
+
await self._emit(
|
|
546
|
+
"StepCompleted",
|
|
547
|
+
result.new_snapshot.machine_id,
|
|
548
|
+
result.step_id,
|
|
549
|
+
{"status": result.status, "state": result.new_snapshot.current_state},
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
__all__ = [
|
|
554
|
+
"ActionExecutor",
|
|
555
|
+
"FSLMKernel",
|
|
556
|
+
"GuardEvaluator",
|
|
557
|
+
"InvariantChecker",
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
async def _invoke(fn: Any, ctx: FSLMContext, args: dict[str, Any]) -> Any:
|
|
562
|
+
signature = inspect.signature(fn)
|
|
563
|
+
result = fn(ctx, args) if len(signature.parameters) >= 2 else fn(ctx)
|
|
564
|
+
if inspect.isawaitable(result):
|
|
565
|
+
return await result
|
|
566
|
+
return result
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _decision_from_result(id: str, value: Any) -> DecisionResult:
|
|
570
|
+
if isinstance(value, DecisionResult):
|
|
571
|
+
return value
|
|
572
|
+
if isinstance(value, bool):
|
|
573
|
+
return DecisionResult(id=id, allowed=value)
|
|
574
|
+
if isinstance(value, dict):
|
|
575
|
+
return DecisionResult(id=id, allowed=bool(value.get("allowed", True)), metadata=value)
|
|
576
|
+
return DecisionResult(id=id, allowed=bool(value), metadata={"value": value})
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _action_from_result(action: ActionSpec, value: Any) -> ActionResult:
|
|
580
|
+
if isinstance(value, ActionResult):
|
|
581
|
+
return value
|
|
582
|
+
return ActionResult(
|
|
583
|
+
action_name=action.name or action.ref or action.tool or "action",
|
|
584
|
+
tool=action.tool or action.ref or "python",
|
|
585
|
+
status="executed",
|
|
586
|
+
output=value if isinstance(value, dict) else {"value": value},
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _resolve_direct_effects(
|
|
591
|
+
effects: dict[str, Any],
|
|
592
|
+
variables: dict[str, Any],
|
|
593
|
+
) -> dict[str, Any]:
|
|
594
|
+
resolved: dict[str, Any] = {}
|
|
595
|
+
for key, value in effects.items():
|
|
596
|
+
if isinstance(value, dict) and value.get("op") == "increment":
|
|
597
|
+
path = str(value.get("path", key))
|
|
598
|
+
current_key = path.removeprefix("$.snapshot.variables.")
|
|
599
|
+
current = variables.get(current_key, 0)
|
|
600
|
+
resolved[key] = current + value.get("by", 1)
|
|
601
|
+
else:
|
|
602
|
+
resolved[key] = value
|
|
603
|
+
return resolved
|