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.
Files changed (130) hide show
  1. ellements/__init__.py +57 -0
  2. ellements/agents/__init__.py +45 -0
  3. ellements/agents/backend.py +100 -0
  4. ellements/agents/builder.py +303 -0
  5. ellements/agents/claude_backend.py +200 -0
  6. ellements/agents/controller.py +237 -0
  7. ellements/agents/openai_backend.py +187 -0
  8. ellements/agents/py.typed +0 -0
  9. ellements/agents/runner.py +358 -0
  10. ellements/agents/tools.py +30 -0
  11. ellements/benchmarking/__init__.py +52 -0
  12. ellements/benchmarking/harness.py +342 -0
  13. ellements/benchmarking/py.typed +0 -0
  14. ellements/benchmarking/results.py +173 -0
  15. ellements/cli/__init__.py +80 -0
  16. ellements/cli/adapters.py +73 -0
  17. ellements/cli/agent_tui.py +1178 -0
  18. ellements/cli/components.py +411 -0
  19. ellements/cli/printer.py +229 -0
  20. ellements/cli/py.typed +0 -0
  21. ellements/core/__init__.py +112 -0
  22. ellements/core/async_utils.py +42 -0
  23. ellements/core/budgeting/__init__.py +43 -0
  24. ellements/core/budgeting/client.py +276 -0
  25. ellements/core/budgeting/protocol.py +51 -0
  26. ellements/core/budgeting/trackers.py +177 -0
  27. ellements/core/caching/__init__.py +51 -0
  28. ellements/core/caching/cache.py +73 -0
  29. ellements/core/caching/client.py +300 -0
  30. ellements/core/caching/disk.py +128 -0
  31. ellements/core/caching/keys.py +97 -0
  32. ellements/core/caching/memory.py +104 -0
  33. ellements/core/chunking.py +262 -0
  34. ellements/core/config.py +180 -0
  35. ellements/core/exceptions.py +145 -0
  36. ellements/core/llm/__init__.py +46 -0
  37. ellements/core/llm/client.py +1124 -0
  38. ellements/core/llm/images.py +226 -0
  39. ellements/core/llm/messages.py +202 -0
  40. ellements/core/llm/model_params.py +66 -0
  41. ellements/core/llm/protocol.py +146 -0
  42. ellements/core/llm/requests.py +100 -0
  43. ellements/core/llm/structured.py +91 -0
  44. ellements/core/llm/wrapper.py +79 -0
  45. ellements/core/observability/__init__.py +39 -0
  46. ellements/core/observability/events.py +169 -0
  47. ellements/core/observability/jsonl_logger.py +244 -0
  48. ellements/core/observability/markdown_formatter.py +197 -0
  49. ellements/core/observability/observer.py +56 -0
  50. ellements/core/prompting/__init__.py +14 -0
  51. ellements/core/prompting/context.py +185 -0
  52. ellements/core/prompting/guideline.py +133 -0
  53. ellements/core/prompting/persona.py +267 -0
  54. ellements/core/prompting/sources.py +92 -0
  55. ellements/core/py.typed +0 -0
  56. ellements/core/rate_limit/__init__.py +44 -0
  57. ellements/core/rate_limit/bucket.py +85 -0
  58. ellements/core/rate_limit/client.py +216 -0
  59. ellements/core/rate_limit/protocol.py +27 -0
  60. ellements/core/templating.py +126 -0
  61. ellements/core/tools/__init__.py +51 -0
  62. ellements/core/tools/dialects.py +136 -0
  63. ellements/core/tools/executor.py +48 -0
  64. ellements/core/tools/protocol.py +36 -0
  65. ellements/core/tools/records.py +28 -0
  66. ellements/core/tools/registry.py +205 -0
  67. ellements/core/tools/schemas.py +80 -0
  68. ellements/core/tools/simple.py +119 -0
  69. ellements/core/tools/spec.py +33 -0
  70. ellements/domain_specific/__init__.py +7 -0
  71. ellements/domain_specific/finance/__init__.py +188 -0
  72. ellements/domain_specific/finance/calculations.py +837 -0
  73. ellements/domain_specific/finance/charts.py +426 -0
  74. ellements/domain_specific/finance/portfolio.py +129 -0
  75. ellements/domain_specific/finance/quant_analysis.py +279 -0
  76. ellements/domain_specific/finance/risk.py +362 -0
  77. ellements/domain_specific/finance/technical_indicators.py +241 -0
  78. ellements/domain_specific/finance/tools.py +483 -0
  79. ellements/domain_specific/finance/valuation.py +312 -0
  80. ellements/domain_specific/finance/yahoo_finance.py +1523 -0
  81. ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
  82. ellements/domain_specific/py.typed +0 -0
  83. ellements/execution/__init__.py +56 -0
  84. ellements/execution/callbacks.py +149 -0
  85. ellements/execution/catalog.py +70 -0
  86. ellements/execution/collaborative.py +191 -0
  87. ellements/execution/config.py +135 -0
  88. ellements/execution/py.typed +0 -0
  89. ellements/execution/reflection.py +156 -0
  90. ellements/execution/self_consistency.py +189 -0
  91. ellements/execution/single_call.py +48 -0
  92. ellements/execution/strategies.py +237 -0
  93. ellements/execution/tree_of_thought.py +541 -0
  94. ellements/fslm/__init__.py +108 -0
  95. ellements/fslm/builtins.py +103 -0
  96. ellements/fslm/cli.py +199 -0
  97. ellements/fslm/context.py +50 -0
  98. ellements/fslm/definition.py +163 -0
  99. ellements/fslm/det.py +173 -0
  100. ellements/fslm/dsl.py +458 -0
  101. ellements/fslm/errors.py +38 -0
  102. ellements/fslm/evaluators.py +141 -0
  103. ellements/fslm/kernel.py +603 -0
  104. ellements/fslm/loading.py +123 -0
  105. ellements/fslm/models.py +623 -0
  106. ellements/fslm/nl.py +87 -0
  107. ellements/fslm/observers.py +107 -0
  108. ellements/fslm/persistence.py +72 -0
  109. ellements/fslm/py.typed +0 -0
  110. ellements/fslm/rendering.py +551 -0
  111. ellements/fslm/visualization.py +25 -0
  112. ellements/reporting/__init__.py +12 -0
  113. ellements/reporting/charts.py +364 -0
  114. ellements/reporting/html_generation.py +132 -0
  115. ellements/reporting/py.typed +0 -0
  116. ellements/reporting/visualization.py +73 -0
  117. ellements/standard_tools/__init__.py +22 -0
  118. ellements/standard_tools/py.typed +0 -0
  119. ellements/standard_tools/terminal.py +205 -0
  120. ellements/standard_tools/web/__init__.py +37 -0
  121. ellements/standard_tools/web/browser_viewer.py +214 -0
  122. ellements/standard_tools/web/crawler.py +454 -0
  123. ellements/standard_tools/web/search.py +399 -0
  124. ellements/standard_tools/web/youtube.py +1297 -0
  125. ellements-0.2.0.dist-info/METADATA +368 -0
  126. ellements-0.2.0.dist-info/RECORD +130 -0
  127. ellements-0.2.0.dist-info/WHEEL +5 -0
  128. ellements-0.2.0.dist-info/entry_points.txt +2 -0
  129. ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
  130. ellements-0.2.0.dist-info/top_level.txt +1 -0
@@ -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