levain-cli 0.1.20__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.20 → levain_cli-0.1.21}/PKG-INFO +1 -1
  2. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/__init__.py +1 -1
  3. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/harness.py +26 -1
  4. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/models.py +7 -0
  5. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/user_input.py +7 -0
  6. {levain_cli-0.1.20 → levain_cli-0.1.21}/pyproject.toml +1 -1
  7. {levain_cli-0.1.20 → levain_cli-0.1.21}/.gitignore +0 -0
  8. {levain_cli-0.1.20 → levain_cli-0.1.21}/LICENSE +0 -0
  9. {levain_cli-0.1.20 → levain_cli-0.1.21}/README.md +0 -0
  10. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/__main__.py +0 -0
  11. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/_format.py +0 -0
  12. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/_java.py +0 -0
  13. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/auth.py +0 -0
  14. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/cli.py +0 -0
  15. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/codegen.py +0 -0
  16. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/graph.py +0 -0
  17. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/integrations_check.py +0 -0
  18. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/known_recipes.py +0 -0
  19. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/mcp.py +0 -0
  20. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/parser.py +0 -0
  21. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/profile.py +0 -0
  22. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/py.typed +0 -0
  23. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/reducers.py +0 -0
  24. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/source_check.py +0 -0
  25. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/tlc.py +0 -0
  26. {levain_cli-0.1.20 → levain_cli-0.1.21}/levain_recipe/translate.py +0 -0
  27. {levain_cli-0.1.20 → 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.20
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)
@@ -17,6 +17,9 @@ SPEC_TYPES: dict[str, type] = {
17
17
  "string": str,
18
18
  "int": int,
19
19
  "list": list,
20
+ # A JSON object of the recipe's own shape. Harness recipes only:
21
+ # PlusCal declares no records.
22
+ "dict": dict,
20
23
  }
21
24
 
22
25
  # Built-in node function name for the langgraph interrupt
@@ -68,6 +71,10 @@ class StateField:
68
71
  # and on the API's state-schema endpoint so the FE can render
69
72
  # a per-field helper in the State tab.
70
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
71
78
 
72
79
 
73
80
  @dataclass(frozen=True)
@@ -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.20"
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