rote-cli 0.12.1__tar.gz → 0.14.0__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.
- {rote_cli-0.12.1 → rote_cli-0.14.0}/PKG-INFO +43 -23
- {rote_cli-0.12.1 → rote_cli-0.14.0}/README.md +36 -16
- {rote_cli-0.12.1 → rote_cli-0.14.0}/pyproject.toml +10 -6
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/__init__.py +1 -1
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/__init__.py +10 -2
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/_common.py +76 -1
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/_py_common.py +80 -59
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/_ts_common.py +413 -3
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/cloudflare.py +240 -52
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/dbos.py +5 -1
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/dbos_ts.py +46 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/inngest.py +72 -7
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/python.py +48 -6
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/adapters/temporal.py +87 -47
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/cli.py +219 -23
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/compiler/__init__.py +139 -2
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/compiler/drivers/__init__.py +9 -0
- rote_cli-0.14.0/src/rote/compiler/drivers/_heartbeat.py +52 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/compiler/drivers/anthropic_api.py +161 -22
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/compiler/drivers/claude.py +129 -56
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/compiler/drivers/openai_api.py +218 -19
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/compiler/events.py +2 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/deploy_rote_cloud.py +10 -6
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/eval/baseline.py +5 -6
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/eval/pricing.py +70 -2
- rote_cli-0.14.0/src/rote/mcp/live_tools.py +303 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/.gitignore +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/LICENSE +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/skills/rote-compile/SKILL.md +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/skills/rote-compile/references/crystallization-heuristics.md +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/skills/rote-compile/references/eval-estimates.md +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/skills/rote-compile/references/implementation.md +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/skills/rote-compile/references/ir-schema.md +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/skills/rote-compile/references/llm-judge-extraction.md +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/skills/rote-compile/references/node-kinds.md +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/__main__.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/_dbos.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/app_registry.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/cloud_auth.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/cloud_compile.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/compiler/drivers/_fs_tools.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/compiler/drivers/codex.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/compiler/update.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/config.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/contracts.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/deploy.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/eval/__init__.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/eval/empirical.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/eval/estimate.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/eval/priors.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/eval/scorecard.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/eval/sidecar.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/eval/tokens.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/inference/__init__.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/inference/_runtime_helper.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/init_wizard.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/ir.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/mcp/__init__.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/mcp/_runtime_helper.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/mcp/auth.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/mcp/registry.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/mcp/release.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/mcp/tokens.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/probe.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/py.typed +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/runners/__init__.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/runners/_node.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/runners/cloudflare.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/runners/dbos_ts.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/runners/inngest.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/runners/temporal.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/serve/__init__.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/serve/backends.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/serve/registry.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/serve/server.py +0 -0
- {rote_cli-0.12.1 → rote_cli-0.14.0}/src/rote/skill_source.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: rote-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Compile AI agent skills into cheap, fast, deterministic pipelines that run without an LLM in the loop
|
|
5
5
|
Project-URL: Homepage, https://roteskills.com
|
|
6
6
|
Project-URL: Repository, https://github.com/trevhud/rote
|
|
@@ -29,11 +29,11 @@ Requires-Dist: pyyaml>=6.0
|
|
|
29
29
|
Provides-Extra: api
|
|
30
30
|
Requires-Dist: anthropic>=0.89; extra == 'api'
|
|
31
31
|
Provides-Extra: dbos
|
|
32
|
-
Requires-Dist: dbos
|
|
32
|
+
Requires-Dist: dbos<3,>=2.26; extra == 'dbos'
|
|
33
33
|
Provides-Extra: dev
|
|
34
34
|
Requires-Dist: anthropic>=0.89; extra == 'dev'
|
|
35
|
-
Requires-Dist: dbos
|
|
36
|
-
Requires-Dist: fastmcp
|
|
35
|
+
Requires-Dist: dbos<3,>=2.26; extra == 'dev'
|
|
36
|
+
Requires-Dist: fastmcp<4,>=3.4.2; extra == 'dev'
|
|
37
37
|
Requires-Dist: httpx>=0.28; extra == 'dev'
|
|
38
38
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
39
39
|
Requires-Dist: openai>=2.45; extra == 'dev'
|
|
@@ -43,12 +43,12 @@ Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
|
43
43
|
Requires-Dist: temporalio>=1.7; extra == 'dev'
|
|
44
44
|
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
45
45
|
Provides-Extra: mcp
|
|
46
|
-
Requires-Dist: fastmcp
|
|
46
|
+
Requires-Dist: fastmcp<4,>=3.4.2; extra == 'mcp'
|
|
47
47
|
Requires-Dist: httpx>=0.28; extra == 'mcp'
|
|
48
48
|
Provides-Extra: openai-api
|
|
49
49
|
Requires-Dist: openai>=2.45; extra == 'openai-api'
|
|
50
50
|
Provides-Extra: serve
|
|
51
|
-
Requires-Dist: fastmcp
|
|
51
|
+
Requires-Dist: fastmcp<4,>=3.4.2; extra == 'serve'
|
|
52
52
|
Requires-Dist: httpx>=0.28; extra == 'serve'
|
|
53
53
|
Provides-Extra: temporal
|
|
54
54
|
Requires-Dist: temporalio>=1.7; extra == 'temporal'
|
|
@@ -257,6 +257,24 @@ override with `--agent`. The output directory splits into `compiled/`
|
|
|
257
257
|
(the agent's `pipeline.yaml`, `extracted/`, `signatures/`, eval seeds,
|
|
258
258
|
and a `compile-report.md`) and `runtime/<runtime>/` (the adapter's
|
|
259
259
|
emitted code + a README on how to run, signal gates, and deploy).
|
|
260
|
+
`compiled/compile-metrics.json` preserves final token counts, session ID,
|
|
261
|
+
and the driver's cost estimate before emission starts, so an adapter
|
|
262
|
+
failure does not lose the paid run's metrics. Missing final output stays
|
|
263
|
+
null with `usage_complete: false`; unchanged updates retain prior metrics.
|
|
264
|
+
|
|
265
|
+
Local `api` and `openai-api` compilation automatically discovers your
|
|
266
|
+
registered, authenticated MCP servers and exposes their read-only tools.
|
|
267
|
+
Pass `--no-mcp` to skip that discovery and compile without live MCP calls:
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
rote compile examples/bdr-outreach/skill --local --agent api --no-mcp --out /tmp/bdr-compiled
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
This leaves the emitted pipeline's MCP bindings intact. The flag also works
|
|
274
|
+
with `rote analyze` and with an API driver selected through config or
|
|
275
|
+
auto-detection. It rejects cloud compilation, subprocess drivers, and
|
|
276
|
+
`--baseline`, whose raw skill run uses its own MCP tools. API drivers bill
|
|
277
|
+
through your API credentials.
|
|
260
278
|
|
|
261
279
|
### Other commands
|
|
262
280
|
|
|
@@ -434,9 +452,8 @@ plain-Python subprocess); those need a Node toolchain / Docker, so they
|
|
|
434
452
|
run locally with `pytest tests/ -m slow`, not in CI.
|
|
435
453
|
|
|
436
454
|
Known gaps: the extracted modules are `NotImplementedError` stubs
|
|
437
|
-
you fill in with real API-client code, a Restate adapter is planned
|
|
438
|
-
|
|
439
|
-
invocation (per-element dispatch is a planned enhancement). Published on
|
|
455
|
+
you fill in with real API-client code, and a Restate adapter is planned.
|
|
456
|
+
Published on
|
|
440
457
|
PyPI as [`rote-cli`](https://pypi.org/project/rote-cli/) via tag-driven
|
|
441
458
|
Trusted Publishing ([docs/releasing.md](docs/releasing.md)).
|
|
442
459
|
|
|
@@ -502,6 +519,9 @@ rote/
|
|
|
502
519
|
archetype: a bounded browser-automation loop stays one agent node
|
|
503
520
|
while the date math, filtering, and reporting around it compile to
|
|
504
521
|
code, plus the measured runs that forced the loop-aware cost model
|
|
522
|
+
- [`examples/rote-compile/`](examples/rote-compile/): the compiler compiling
|
|
523
|
+
its own core skill, with preserved artifacts and an offline audit of
|
|
524
|
+
the generated workflow's runtime and data-flow failures
|
|
505
525
|
|
|
506
526
|
---
|
|
507
527
|
|
|
@@ -509,19 +529,19 @@ rote/
|
|
|
509
529
|
|
|
510
530
|
In rough priority order:
|
|
511
531
|
|
|
512
|
-
1. **
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
532
|
+
1. **Pre-filter as a `pure_function` node**: today hard thresholds are
|
|
533
|
+
lifted into a judge's `forward()`, which works for the Python
|
|
534
|
+
runtimes but not the TypeScript ones; a separate node makes the
|
|
535
|
+
short-circuit uniform across all six.
|
|
536
|
+
2. **Make self-compilation executable**: the
|
|
537
|
+
[preserved core-skill run](examples/rote-compile/) produced valid IR,
|
|
538
|
+
and runtime emission now succeeds. Its offline audit still has
|
|
539
|
+
data-flow and agent-result contract failures. Close those gaps before
|
|
540
|
+
treating it as a working replacement compiler.
|
|
541
|
+
3. **More example skills**: four archetypes are covered today (BDR
|
|
542
|
+
outreach, 100%-roteness, data-heavy, agent-loop). Research-heavy,
|
|
543
|
+
retrieval-heavy, and code-review skills each stress the IR in ways
|
|
544
|
+
none of those four do.
|
|
525
545
|
|
|
526
546
|
---
|
|
527
547
|
|
|
@@ -201,6 +201,24 @@ override with `--agent`. The output directory splits into `compiled/`
|
|
|
201
201
|
(the agent's `pipeline.yaml`, `extracted/`, `signatures/`, eval seeds,
|
|
202
202
|
and a `compile-report.md`) and `runtime/<runtime>/` (the adapter's
|
|
203
203
|
emitted code + a README on how to run, signal gates, and deploy).
|
|
204
|
+
`compiled/compile-metrics.json` preserves final token counts, session ID,
|
|
205
|
+
and the driver's cost estimate before emission starts, so an adapter
|
|
206
|
+
failure does not lose the paid run's metrics. Missing final output stays
|
|
207
|
+
null with `usage_complete: false`; unchanged updates retain prior metrics.
|
|
208
|
+
|
|
209
|
+
Local `api` and `openai-api` compilation automatically discovers your
|
|
210
|
+
registered, authenticated MCP servers and exposes their read-only tools.
|
|
211
|
+
Pass `--no-mcp` to skip that discovery and compile without live MCP calls:
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
rote compile examples/bdr-outreach/skill --local --agent api --no-mcp --out /tmp/bdr-compiled
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
This leaves the emitted pipeline's MCP bindings intact. The flag also works
|
|
218
|
+
with `rote analyze` and with an API driver selected through config or
|
|
219
|
+
auto-detection. It rejects cloud compilation, subprocess drivers, and
|
|
220
|
+
`--baseline`, whose raw skill run uses its own MCP tools. API drivers bill
|
|
221
|
+
through your API credentials.
|
|
204
222
|
|
|
205
223
|
### Other commands
|
|
206
224
|
|
|
@@ -378,9 +396,8 @@ plain-Python subprocess); those need a Node toolchain / Docker, so they
|
|
|
378
396
|
run locally with `pytest tests/ -m slow`, not in CI.
|
|
379
397
|
|
|
380
398
|
Known gaps: the extracted modules are `NotImplementedError` stubs
|
|
381
|
-
you fill in with real API-client code, a Restate adapter is planned
|
|
382
|
-
|
|
383
|
-
invocation (per-element dispatch is a planned enhancement). Published on
|
|
399
|
+
you fill in with real API-client code, and a Restate adapter is planned.
|
|
400
|
+
Published on
|
|
384
401
|
PyPI as [`rote-cli`](https://pypi.org/project/rote-cli/) via tag-driven
|
|
385
402
|
Trusted Publishing ([docs/releasing.md](docs/releasing.md)).
|
|
386
403
|
|
|
@@ -446,6 +463,9 @@ rote/
|
|
|
446
463
|
archetype: a bounded browser-automation loop stays one agent node
|
|
447
464
|
while the date math, filtering, and reporting around it compile to
|
|
448
465
|
code, plus the measured runs that forced the loop-aware cost model
|
|
466
|
+
- [`examples/rote-compile/`](examples/rote-compile/): the compiler compiling
|
|
467
|
+
its own core skill, with preserved artifacts and an offline audit of
|
|
468
|
+
the generated workflow's runtime and data-flow failures
|
|
449
469
|
|
|
450
470
|
---
|
|
451
471
|
|
|
@@ -453,19 +473,19 @@ rote/
|
|
|
453
473
|
|
|
454
474
|
In rough priority order:
|
|
455
475
|
|
|
456
|
-
1. **
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
476
|
+
1. **Pre-filter as a `pure_function` node**: today hard thresholds are
|
|
477
|
+
lifted into a judge's `forward()`, which works for the Python
|
|
478
|
+
runtimes but not the TypeScript ones; a separate node makes the
|
|
479
|
+
short-circuit uniform across all six.
|
|
480
|
+
2. **Make self-compilation executable**: the
|
|
481
|
+
[preserved core-skill run](examples/rote-compile/) produced valid IR,
|
|
482
|
+
and runtime emission now succeeds. Its offline audit still has
|
|
483
|
+
data-flow and agent-result contract failures. Close those gaps before
|
|
484
|
+
treating it as a working replacement compiler.
|
|
485
|
+
3. **More example skills**: four archetypes are covered today (BDR
|
|
486
|
+
outreach, 100%-roteness, data-heavy, agent-loop). Research-heavy,
|
|
487
|
+
retrieval-heavy, and code-review skills each stress the IR in ways
|
|
488
|
+
none of those four do.
|
|
469
489
|
|
|
470
490
|
---
|
|
471
491
|
|
|
@@ -69,14 +69,14 @@ openai-api = [
|
|
|
69
69
|
"openai>=2.45",
|
|
70
70
|
]
|
|
71
71
|
dbos = [
|
|
72
|
-
"dbos>=2.26",
|
|
72
|
+
"dbos>=2.26,<3",
|
|
73
73
|
]
|
|
74
74
|
serve = [
|
|
75
|
-
"fastmcp>=3.4.2",
|
|
75
|
+
"fastmcp>=3.4.2,<4",
|
|
76
76
|
"httpx>=0.28",
|
|
77
77
|
]
|
|
78
78
|
mcp = [
|
|
79
|
-
"fastmcp>=3.4.2",
|
|
79
|
+
"fastmcp>=3.4.2,<4",
|
|
80
80
|
"httpx>=0.28",
|
|
81
81
|
]
|
|
82
82
|
dev = [
|
|
@@ -88,8 +88,8 @@ dev = [
|
|
|
88
88
|
"temporalio>=1.7",
|
|
89
89
|
"anthropic>=0.89",
|
|
90
90
|
"openai>=2.45",
|
|
91
|
-
"dbos>=2.26",
|
|
92
|
-
"fastmcp>=3.4.2",
|
|
91
|
+
"dbos>=2.26,<3",
|
|
92
|
+
"fastmcp>=3.4.2,<4",
|
|
93
93
|
"httpx>=0.28",
|
|
94
94
|
]
|
|
95
95
|
|
|
@@ -138,7 +138,11 @@ extend-exclude = ["examples"]
|
|
|
138
138
|
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
139
139
|
|
|
140
140
|
[tool.pytest.ini_options]
|
|
141
|
-
|
|
141
|
+
# -rs: always print the REASON for every skip. A conditional skip whose
|
|
142
|
+
# condition silently becomes permanent is invisible behind a bare
|
|
143
|
+
# "1 skipped" count — that is how `test_per_tool_override_changes_payload`
|
|
144
|
+
# went from a real test to dead weight without anyone noticing.
|
|
145
|
+
addopts = "-m 'not slow' -rs"
|
|
142
146
|
markers = [
|
|
143
147
|
"slow: tests that need an external toolchain (Node/npm/tsc) or take >5s. Run with `pytest -m slow` to include.",
|
|
144
148
|
]
|
|
@@ -56,9 +56,17 @@ def _cloudflare_adapter_factory(**options: Any) -> Adapter:
|
|
|
56
56
|
from rote.adapters.cloudflare import CloudflareAdapter, CloudflareAdapterConfig
|
|
57
57
|
|
|
58
58
|
backend = _validated_backend(options)
|
|
59
|
-
|
|
59
|
+
mcp_client = options.get("mcp_client")
|
|
60
|
+
if mcp_client not in (None, "direct", "binding"):
|
|
61
|
+
raise ValueError(f"mcp_client must be 'direct' or 'binding', got {mcp_client!r}")
|
|
62
|
+
if backend is None and mcp_client is None:
|
|
60
63
|
return CloudflareAdapter()
|
|
61
|
-
return CloudflareAdapter(
|
|
64
|
+
return CloudflareAdapter(
|
|
65
|
+
CloudflareAdapterConfig(
|
|
66
|
+
external_backend=backend or "mcp",
|
|
67
|
+
mcp_client=mcp_client or "direct",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
62
70
|
|
|
63
71
|
|
|
64
72
|
def _source_dir(options: dict[str, Any]) -> Path | None:
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import hashlib
|
|
15
15
|
import json
|
|
16
16
|
import re
|
|
17
|
+
from collections.abc import Sequence
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
19
20
|
from rote.ir import Node, NodeKind, Pipeline, parse_input_ref
|
|
@@ -305,12 +306,20 @@ def ir_duration_to_human(s: str) -> str:
|
|
|
305
306
|
Cloudflare's step config accepts directly. Strings that don't match
|
|
306
307
|
the IR shorthand pattern are passed through unchanged (they're
|
|
307
308
|
assumed to already be in an acceptable form).
|
|
309
|
+
|
|
310
|
+
A value of exactly 1 is singularized ('1 hour', not '1 hours').
|
|
311
|
+
Cloudflare's ``WorkflowDuration`` accepts either spelling, so this
|
|
312
|
+
is legibility rather than correctness — but emitted code is read by
|
|
313
|
+
humans, and '1 hours' in a reviewed artifact looks like a bug.
|
|
308
314
|
"""
|
|
309
315
|
s = s.strip()
|
|
310
316
|
m = _IR_DURATION_RE.fullmatch(s)
|
|
311
317
|
if not m:
|
|
312
318
|
return s
|
|
313
|
-
|
|
319
|
+
value, unit = m.group(1), _UNIT_TO_HUMAN[m.group(2)]
|
|
320
|
+
if float(value) == 1:
|
|
321
|
+
unit = unit.removesuffix("s")
|
|
322
|
+
return f"{value} {unit}"
|
|
314
323
|
|
|
315
324
|
|
|
316
325
|
_UNIT_TO_SECONDS = {
|
|
@@ -422,6 +431,72 @@ def check_input_refs_available(node: Node, available: set[str]) -> None:
|
|
|
422
431
|
)
|
|
423
432
|
|
|
424
433
|
|
|
434
|
+
def fan_out_element_param(node: Node, pipeline: Pipeline) -> str:
|
|
435
|
+
"""Which ``inputs`` param carries the list a ``fan_out`` node fans over.
|
|
436
|
+
|
|
437
|
+
Every other input is shared verbatim by all invocations. Which param
|
|
438
|
+
is the list, in precedence order:
|
|
439
|
+
|
|
440
|
+
1. the param bound to the source of an incoming ``fan_out: true``
|
|
441
|
+
edge (the IR's explicit marker);
|
|
442
|
+
2. else the param bound to a node with any incoming edge — inputs
|
|
443
|
+
may also reference nodes *without* an edge (shared context, e.g.
|
|
444
|
+
BDR's ``intel``), which is what makes "the only node-bound
|
|
445
|
+
param" too naive;
|
|
446
|
+
3. else the only node-bound param.
|
|
447
|
+
|
|
448
|
+
Ambiguity after all three is an emit-time error, never a guess:
|
|
449
|
+
dispatching over the wrong list would silently judge the wrong
|
|
450
|
+
things.
|
|
451
|
+
|
|
452
|
+
This lives in the language-neutral common module because the answer
|
|
453
|
+
is a property of the IR, not of the target language — every adapter
|
|
454
|
+
must fan over the *same* input or the same pipeline would mean
|
|
455
|
+
different things on different runtimes.
|
|
456
|
+
"""
|
|
457
|
+
if not node.inputs:
|
|
458
|
+
raise ValueError(f"fan_out node {node.id!r} has no inputs: nothing to fan over")
|
|
459
|
+
node_bound = {
|
|
460
|
+
param: parsed.node_id
|
|
461
|
+
for param, ref in node.inputs.items()
|
|
462
|
+
if (parsed := parse_input_ref(ref)).node_id is not None
|
|
463
|
+
}
|
|
464
|
+
edge_sources = {e.from_ for e in pipeline.edges if e.to == node.id}
|
|
465
|
+
fan_edge_sources = {e.from_ for e in pipeline.edges if e.to == node.id and e.fan_out}
|
|
466
|
+
|
|
467
|
+
# The tiers are tried narrowest-first, but the ORDER is provably
|
|
468
|
+
# unobservable: fan_edge_sources ⊆ edge_sources by construction, so
|
|
469
|
+
# matching(fan) ⊆ matching(edge), and a tier is accepted only when it
|
|
470
|
+
# singles out exactly one param. If the broader tier singles one out,
|
|
471
|
+
# it is necessarily the same one. (Mutation testing flags swapping
|
|
472
|
+
# these as a surviving mutant — it is equivalent, not a test gap.)
|
|
473
|
+
# The tiers still earn their place: they express which signal the
|
|
474
|
+
# author gave, and tier 2 is what handles a marker pointing at a node
|
|
475
|
+
# no input references.
|
|
476
|
+
for sources in (fan_edge_sources, edge_sources):
|
|
477
|
+
matching = sorted(p for p, src in node_bound.items() if src in sources)
|
|
478
|
+
if len(matching) == 1:
|
|
479
|
+
return matching[0]
|
|
480
|
+
if len(node_bound) != 1:
|
|
481
|
+
raise ValueError(
|
|
482
|
+
f"fan_out node {node.id!r}: cannot identify the element param — "
|
|
483
|
+
f"node-bound inputs {sorted(node_bound)} and incoming edges "
|
|
484
|
+
f"{sorted(edge_sources)} don't single one out; mark the list edge "
|
|
485
|
+
f"with `fan_out: true`"
|
|
486
|
+
)
|
|
487
|
+
return next(iter(node_bound))
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def fan_out_nodes(wave: Sequence[Node]) -> tuple[list[Node], list[Node]]:
|
|
491
|
+
"""Split a wave into ``(fan_out_nodes, plain_nodes)``, order preserved.
|
|
492
|
+
|
|
493
|
+
A ``fan_out`` node dispatches once per element of its bound list, so
|
|
494
|
+
it never shares the single/parallel payload shapes an adapter emits
|
|
495
|
+
for plain nodes.
|
|
496
|
+
"""
|
|
497
|
+
return [n for n in wave if n.fan_out], [n for n in wave if not n.fan_out]
|
|
498
|
+
|
|
499
|
+
|
|
425
500
|
# ───────── MCP-capability refusal ─────────
|
|
426
501
|
|
|
427
502
|
|
|
@@ -30,6 +30,7 @@ from rote.adapters._common import (
|
|
|
30
30
|
DEFAULT_AGENT_MAX_ITERATIONS,
|
|
31
31
|
EmitWriter,
|
|
32
32
|
_to_pascal_case,
|
|
33
|
+
fan_out_element_param,
|
|
33
34
|
safe_docstring_line,
|
|
34
35
|
)
|
|
35
36
|
from rote.ir import LLMSignature, Node, NodeKind, Pipeline, parse_input_ref
|
|
@@ -93,52 +94,65 @@ def _payload_literal(node: Node, indent: str) -> str:
|
|
|
93
94
|
return "\n".join(lines)
|
|
94
95
|
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
_FAN_OUT_LIST_HELPER_PY = '''\
|
|
98
|
+
def _fan_out_list(value: object, node_id: str, ref: str) -> list:
|
|
99
|
+
"""The list a fan_out node dispatches over.
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
2. else the param bound to a node with any incoming edge — inputs
|
|
106
|
-
may also reference nodes *without* an edge (shared context, e.g.
|
|
107
|
-
BDR's ``intel``), which is what makes "the only node-bound
|
|
108
|
-
param" too naive;
|
|
109
|
-
3. else the only node-bound param.
|
|
110
|
-
|
|
111
|
-
Ambiguity after all three is an emit-time error, never a guess:
|
|
112
|
-
dispatching over the wrong list would silently judge the wrong
|
|
113
|
-
things.
|
|
101
|
+
Iterating a missing key raises "'NoneType' object is not iterable",
|
|
102
|
+
which names neither the node nor the reference — in generated code
|
|
103
|
+
that is close to undebuggable, and the cause is almost always an
|
|
104
|
+
upstream node that didn't return the key the IR says it does. The
|
|
105
|
+
TypeScript runtimes emit the same guard (`fanOutList`), so a missing
|
|
106
|
+
key reports identically on every target.
|
|
114
107
|
"""
|
|
115
|
-
if not
|
|
116
|
-
raise
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
108
|
+
if not isinstance(value, list):
|
|
109
|
+
raise TypeError(
|
|
110
|
+
f"fan_out node {node_id!r} expected a list from {ref!r}, got "
|
|
111
|
+
f"{type(value).__name__}. The upstream node must return that "
|
|
112
|
+
f"key as a list."
|
|
113
|
+
)
|
|
114
|
+
return value
|
|
115
|
+
'''
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def fan_out_list_helper() -> str:
|
|
119
|
+
"""The ``_fan_out_list`` guard emitted into Python runtimes that fan out.
|
|
120
|
+
|
|
121
|
+
Behavioral emitted code, like ``_serialize``: if the runtimes' guards
|
|
122
|
+
drift, the same broken pipeline reports differently per target.
|
|
123
|
+
"""
|
|
124
|
+
return _FAN_OUT_LIST_HELPER_PY
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def fan_out_binding(
|
|
128
|
+
node: Node, pipeline: Pipeline, indent: str = ""
|
|
129
|
+
) -> tuple[str, str, dict[str, str]]:
|
|
130
|
+
"""``(element_param, list_expr, scalar_exprs)`` for a ``fan_out`` node.
|
|
131
|
+
|
|
132
|
+
The Python rendering of :func:`fan_out_element_param` — which input
|
|
133
|
+
is the list is a language-neutral property of the IR, so it is
|
|
134
|
+
resolved there and only the expressions are built here.
|
|
139
135
|
|
|
136
|
+
``indent`` is the column the ``_fan_out_list(`` token sits at; the
|
|
137
|
+
guard call wraps against it rather than running long on one line.
|
|
138
|
+
"""
|
|
139
|
+
element_param = fan_out_element_param(node, pipeline)
|
|
140
|
+
# Narrowing only: fan_out_element_param raises on a node with no
|
|
141
|
+
# inputs, so reaching here means there is something to fan over.
|
|
142
|
+
assert node.inputs is not None
|
|
140
143
|
scalars = {p: _ref_to_python_expr(r) for p, r in node.inputs.items() if p != element_param}
|
|
141
|
-
|
|
144
|
+
ref = node.inputs[element_param]
|
|
145
|
+
inner = indent + " "
|
|
146
|
+
# json.dumps, not !r: emitted Python uses double quotes throughout
|
|
147
|
+
# (the repo's ruff-format style), same as _py_literal.
|
|
148
|
+
list_expr = (
|
|
149
|
+
"_fan_out_list(\n"
|
|
150
|
+
f"{inner}{_ref_to_python_expr(ref)},\n"
|
|
151
|
+
f"{inner}{json.dumps(node.id)},\n"
|
|
152
|
+
f"{inner}{json.dumps(ref)},\n"
|
|
153
|
+
f"{indent})"
|
|
154
|
+
)
|
|
155
|
+
return element_param, list_expr, scalars
|
|
142
156
|
|
|
143
157
|
|
|
144
158
|
# ───────── JSON Schema → Pydantic source ─────────
|
|
@@ -218,38 +232,47 @@ class _SchemaToPydantic:
|
|
|
218
232
|
|
|
219
233
|
The Python analog of the Cloudflare adapter's ``json_schema_to_zod``.
|
|
220
234
|
Named ``$defs`` become named classes / ``Literal`` aliases; inline
|
|
221
|
-
object schemas get synthesized names.
|
|
222
|
-
|
|
223
|
-
|
|
235
|
+
object schemas get synthesized names. Each root schema resolves its
|
|
236
|
+
own definitions; generated names and dependency-ordered blocks are
|
|
237
|
+
shared across the module.
|
|
224
238
|
"""
|
|
225
239
|
|
|
226
|
-
def __init__(self) -> None:
|
|
240
|
+
def __init__(self, *, reserved_names: tuple[str, ...] = ()) -> None:
|
|
227
241
|
self._defs: dict[str, dict[str, Any]] = {}
|
|
228
242
|
self._blocks: list[str] = []
|
|
229
243
|
self._emitted: dict[str, str] = {} # $def name -> python name
|
|
230
244
|
self._in_progress: set[str] = set()
|
|
231
|
-
self._used_names: set[str] =
|
|
245
|
+
self._used_names: set[str] = {
|
|
246
|
+
*keyword.kwlist,
|
|
247
|
+
*reserved_names,
|
|
248
|
+
"Any",
|
|
249
|
+
"Literal",
|
|
250
|
+
"BaseModel",
|
|
251
|
+
"ConfigDict",
|
|
252
|
+
"Field",
|
|
253
|
+
"PROMPT",
|
|
254
|
+
"MODEL",
|
|
255
|
+
"TEMPERATURE",
|
|
256
|
+
}
|
|
232
257
|
self.uses_literal = False
|
|
233
258
|
self.uses_config_dict = False
|
|
234
259
|
self.uses_field = False
|
|
235
260
|
|
|
236
261
|
# ── public API ──
|
|
237
262
|
|
|
238
|
-
def add_defs(self, defs: dict[str, Any]) -> None:
|
|
239
|
-
for name, schema in defs.items():
|
|
240
|
-
if name in self._defs and self._defs[name] != schema:
|
|
241
|
-
raise ValueError(
|
|
242
|
-
f"signature emission: $defs entry {name!r} appears in both input "
|
|
243
|
-
f"and output schemas with different definitions"
|
|
244
|
-
)
|
|
245
|
-
self._defs[name] = schema
|
|
246
|
-
|
|
247
263
|
def emit_root(self, class_name: str, schema: dict[str, Any]) -> None:
|
|
248
264
|
if schema.get("type") != "object":
|
|
249
265
|
raise ValueError(
|
|
250
266
|
f"signature emission: root signature schema for {class_name} must be "
|
|
251
267
|
f"an object schema, got type={schema.get('type')!r}"
|
|
252
268
|
)
|
|
269
|
+
# A #/$defs reference belongs to its root document. Sharing this
|
|
270
|
+
# cache across input/output can bind even identical wrappers to the
|
|
271
|
+
# wrong nested definition. Only Python symbol allocation is global.
|
|
272
|
+
self._defs = schema.get("$defs", {})
|
|
273
|
+
self._emitted.clear()
|
|
274
|
+
self._in_progress.clear()
|
|
275
|
+
self._used_names.add(class_name)
|
|
253
276
|
self._emit_object_class(class_name, schema)
|
|
254
277
|
|
|
255
278
|
@property
|
|
@@ -484,9 +507,7 @@ def emit_signature_module(
|
|
|
484
507
|
spec = node.signature_spec
|
|
485
508
|
pascal = _to_pascal_case(node.id)
|
|
486
509
|
|
|
487
|
-
converter = _SchemaToPydantic()
|
|
488
|
-
converter.add_defs(spec.input_schema.get("$defs", {}))
|
|
489
|
-
converter.add_defs(spec.output_schema.get("$defs", {}))
|
|
510
|
+
converter = _SchemaToPydantic(reserved_names=(pascal, f"{pascal}Input", f"{pascal}Output"))
|
|
490
511
|
converter.emit_root(f"{pascal}Input", spec.input_schema)
|
|
491
512
|
converter.emit_root(f"{pascal}Output", spec.output_schema)
|
|
492
513
|
|