levain-cli 0.1.19__tar.gz → 0.1.21__tar.gz

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 (27) hide show
  1. {levain_cli-0.1.19 → levain_cli-0.1.21}/PKG-INFO +1 -1
  2. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/__init__.py +1 -1
  3. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/harness.py +26 -1
  4. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/models.py +23 -0
  5. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/user_input.py +7 -0
  6. {levain_cli-0.1.19 → levain_cli-0.1.21}/pyproject.toml +1 -1
  7. {levain_cli-0.1.19 → levain_cli-0.1.21}/.gitignore +0 -0
  8. {levain_cli-0.1.19 → levain_cli-0.1.21}/LICENSE +0 -0
  9. {levain_cli-0.1.19 → levain_cli-0.1.21}/README.md +0 -0
  10. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/__main__.py +0 -0
  11. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/_format.py +0 -0
  12. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/_java.py +0 -0
  13. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/auth.py +0 -0
  14. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/cli.py +0 -0
  15. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/codegen.py +0 -0
  16. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/graph.py +0 -0
  17. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/integrations_check.py +0 -0
  18. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/known_recipes.py +0 -0
  19. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/mcp.py +0 -0
  20. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/parser.py +0 -0
  21. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/profile.py +0 -0
  22. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/py.typed +0 -0
  23. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/reducers.py +0 -0
  24. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/source_check.py +0 -0
  25. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/tlc.py +0 -0
  26. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/translate.py +0 -0
  27. {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: levain-cli
3
- Version: 0.1.19
3
+ Version: 0.1.21
4
4
  Summary: Levain recipe toolchain: PlusCal validator, TLC model checking, and GraphState codegen for agent recipes
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -17,4 +17,4 @@ avoid them.
17
17
  # or annotation vocabulary changes, independently of tool
18
18
  # releases. The API's toolchain handshake compares this against
19
19
  # the server's copy.
20
- LANGUAGE_VERSION = "1.1"
20
+ LANGUAGE_VERSION = "1.2"
@@ -18,6 +18,7 @@ A harness recipe has no ``graph.tla`` and no node functions. Its
18
18
  [tool.levain.harness.state]
19
19
  task = { type = "string", description = "What to work on" }
20
20
  summary = "string" # shorthand: type only
21
+ result = { type = "dict", file = "out/result.json" } # read from the workspace
21
22
 
22
23
  Validation errors are prescriptive in the same ``[stage] message``
23
24
  shape as the PlusCal pipeline, so the build agent and the CLI treat
@@ -58,12 +59,13 @@ _KEYS = (
58
59
  "permission_mode",
59
60
  "state",
60
61
  )
61
- _FIELD_KEYS = ("type", "default", "description", "reducer")
62
+ _FIELD_KEYS = ("type", "default", "description", "reducer", "file")
62
63
  _DEFAULTS: dict[str, Any] = {
63
64
  "bool": False,
64
65
  "string": "",
65
66
  "int": 0,
66
67
  "list": [],
68
+ "dict": {},
67
69
  }
68
70
 
69
71
 
@@ -319,6 +321,12 @@ def _state_fields(
319
321
  _err(f"{where}: reducer is only valid on list fields.")
320
322
  )
321
323
  continue
324
+ file = spec.get("file")
325
+ if file is not None:
326
+ file_error = _file_error(where, file, vtype)
327
+ if file_error is not None:
328
+ errors.append(file_error)
329
+ continue
322
330
  default = spec.get("default", _DEFAULTS[vtype])
323
331
  if not _default_fits(default, vtype):
324
332
  errors.append(
@@ -335,11 +343,28 @@ def _state_fields(
335
343
  description=(
336
344
  str(description) if isinstance(description, str) else None
337
345
  ),
346
+ file=file,
338
347
  )
339
348
  )
340
349
  return fields, errors
341
350
 
342
351
 
352
+ def _file_error(where: str, file: Any, vtype: str) -> ValidationError | None:
353
+ if not isinstance(file, str) or not file.strip():
354
+ return _err(f"{where}: file must be a path relative to the workspace.")
355
+ if os.path.isabs(file) or ".." in file.split("/"):
356
+ return _err(
357
+ f"{where}: file = {file!r} must stay inside the workspace."
358
+ )
359
+ if vtype == "list":
360
+ return _err(
361
+ f"{where}: a file holds a whole value, and a list field"
362
+ " accumulates through its reducer; declare a dict and put"
363
+ " the list in it."
364
+ )
365
+ return None
366
+
367
+
343
368
  def _default_fits(value: Any, vtype: str) -> bool:
344
369
  if vtype == "int":
345
370
  return isinstance(value, int) and not isinstance(value, bool)
@@ -5,6 +5,7 @@ and sandbox modules. Other modules should import from here
5
5
  rather than defining their own copies.
6
6
  """
7
7
 
8
+ import dataclasses
8
9
  from dataclasses import dataclass, field
9
10
  from typing import Any
10
11
 
@@ -16,6 +17,9 @@ SPEC_TYPES: dict[str, type] = {
16
17
  "string": str,
17
18
  "int": int,
18
19
  "list": list,
20
+ # A JSON object of the recipe's own shape. Harness recipes only:
21
+ # PlusCal declares no records.
22
+ "dict": dict,
19
23
  }
20
24
 
21
25
  # Built-in node function name for the langgraph interrupt
@@ -67,6 +71,10 @@ class StateField:
67
71
  # and on the API's state-schema endpoint so the FE can render
68
72
  # a per-field helper in the State tab.
69
73
  description: str | None = None
74
+ # Harness recipes only: a path in the workspace the engine writes
75
+ # the value to, read when the turn ends instead of taken through
76
+ # the ``levain-result`` tool.
77
+ file: str | None = None
70
78
 
71
79
 
72
80
  @dataclass(frozen=True)
@@ -190,6 +198,21 @@ def recipe_kind_of(spec: RecipeSpec) -> str:
190
198
  return "harness" if isinstance(spec, HarnessSpec) else "graph"
191
199
 
192
200
 
201
+ def graph_payload(spec: ValidatedSpec) -> dict[str, Any]:
202
+ """The spec in its runtime shape: what the sandbox graph engine builds.
203
+
204
+ Plain JSON (the turn params carry it), so the sandbox needs none of
205
+ this package to run a graph; the spec's source and invariants stay
206
+ behind, they are the validator's.
207
+ """
208
+ return {
209
+ "variables": [dataclasses.asdict(v) for v in spec.variables],
210
+ "nodes": [dataclasses.asdict(n) for n in spec.nodes],
211
+ "routing": [dataclasses.asdict(t) for t in spec.routing],
212
+ "constants": dict(spec.constants),
213
+ }
214
+
215
+
193
216
  def mcp_server_names(spec: RecipeSpec) -> set[str]:
194
217
  """Every MCP server name the recipe declares, whichever its kind."""
195
218
  if isinstance(spec, HarnessSpec):
@@ -40,6 +40,7 @@ FieldKind = Literal[
40
40
  "string",
41
41
  "int",
42
42
  "bool",
43
+ "dict",
43
44
  "messages",
44
45
  "string_list",
45
46
  "dict_list",
@@ -106,6 +107,7 @@ _SCALAR_KIND: dict[str, FieldKind] = {
106
107
  "string": "string",
107
108
  "int": "int",
108
109
  "bool": "bool",
110
+ "dict": "dict",
109
111
  }
110
112
 
111
113
  # Every entry maps a blessed reducer name to the FieldKind the
@@ -207,6 +209,10 @@ def _check_bool(key: str, value: Any) -> str | None:
207
209
  return None if isinstance(value, bool) else _type_err(key, "bool", value)
208
210
 
209
211
 
212
+ def _check_dict(key: str, value: Any) -> str | None:
213
+ return None if isinstance(value, dict) else _type_err(key, "object", value)
214
+
215
+
210
216
  def _check_string_list(key: str, value: Any) -> str | None:
211
217
  if not isinstance(value, list) or not all(
212
218
  isinstance(x, str) for x in value
@@ -254,6 +260,7 @@ _KIND_VALIDATORS.update(
254
260
  "string": _check_string,
255
261
  "int": _check_int,
256
262
  "bool": _check_bool,
263
+ "dict": _check_dict,
257
264
  "messages": _validate_messages,
258
265
  "string_list": _check_string_list,
259
266
  "dict_list": _check_dict_list,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "levain-cli"
3
- version = "0.1.19"
3
+ version = "0.1.21"
4
4
  description = "Levain recipe toolchain: PlusCal validator, TLC model checking, and GraphState codegen for agent recipes"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes
File without changes