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.
- {levain_cli-0.1.19 → levain_cli-0.1.21}/PKG-INFO +1 -1
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/__init__.py +1 -1
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/harness.py +26 -1
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/models.py +23 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/user_input.py +7 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/pyproject.toml +1 -1
- {levain_cli-0.1.19 → levain_cli-0.1.21}/.gitignore +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/LICENSE +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/README.md +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/__main__.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/_format.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/_java.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/auth.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/cli.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/codegen.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/graph.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/integrations_check.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/known_recipes.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/mcp.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/parser.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/profile.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/py.typed +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/reducers.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/source_check.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/tlc.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/translate.py +0 -0
- {levain_cli-0.1.19 → levain_cli-0.1.21}/levain_recipe/validate.py +0 -0
|
@@ -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,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|