rote-cli 0.13.0__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.13.0 → rote_cli-0.14.0}/PKG-INFO +41 -18
- {rote_cli-0.13.0 → rote_cli-0.14.0}/README.md +34 -11
- {rote_cli-0.13.0 → rote_cli-0.14.0}/pyproject.toml +5 -5
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/__init__.py +1 -1
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/__init__.py +10 -2
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/_py_common.py +24 -17
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/_ts_common.py +351 -3
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/cloudflare.py +141 -46
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/cli.py +213 -23
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/__init__.py +139 -2
- {rote_cli-0.13.0 → 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.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/anthropic_api.py +161 -22
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/claude.py +129 -56
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/openai_api.py +218 -19
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/events.py +2 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/deploy_rote_cloud.py +10 -6
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/baseline.py +5 -6
- {rote_cli-0.13.0 → 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.13.0 → rote_cli-0.14.0}/.gitignore +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/LICENSE +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/SKILL.md +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/crystallization-heuristics.md +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/eval-estimates.md +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/implementation.md +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/ir-schema.md +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/llm-judge-extraction.md +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/node-kinds.md +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/__main__.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/_dbos.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/_common.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/dbos.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/dbos_ts.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/inngest.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/python.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/temporal.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/app_registry.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/cloud_auth.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/cloud_compile.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/_fs_tools.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/codex.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/update.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/config.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/contracts.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/deploy.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/__init__.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/empirical.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/estimate.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/priors.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/scorecard.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/sidecar.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/tokens.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/inference/__init__.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/inference/_runtime_helper.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/init_wizard.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/ir.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/__init__.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/_runtime_helper.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/auth.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/registry.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/release.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/tokens.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/probe.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/py.typed +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/__init__.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/_node.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/cloudflare.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/dbos_ts.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/inngest.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/temporal.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/serve/__init__.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/serve/backends.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/serve/registry.py +0 -0
- {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/serve/server.py +0 -0
- {rote_cli-0.13.0 → 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
|
|
|
@@ -501,6 +519,9 @@ rote/
|
|
|
501
519
|
archetype: a bounded browser-automation loop stays one agent node
|
|
502
520
|
while the date math, filtering, and reporting around it compile to
|
|
503
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
|
|
504
525
|
|
|
505
526
|
---
|
|
506
527
|
|
|
@@ -508,17 +529,19 @@ rote/
|
|
|
508
529
|
|
|
509
530
|
In rough priority order:
|
|
510
531
|
|
|
511
|
-
1. **
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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.
|
|
522
545
|
|
|
523
546
|
---
|
|
524
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
|
|
|
@@ -445,6 +463,9 @@ rote/
|
|
|
445
463
|
archetype: a bounded browser-automation loop stays one agent node
|
|
446
464
|
while the date math, filtering, and reporting around it compile to
|
|
447
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
|
|
448
469
|
|
|
449
470
|
---
|
|
450
471
|
|
|
@@ -452,17 +473,19 @@ rote/
|
|
|
452
473
|
|
|
453
474
|
In rough priority order:
|
|
454
475
|
|
|
455
|
-
1. **
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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.
|
|
466
489
|
|
|
467
490
|
---
|
|
468
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
|
|
|
@@ -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:
|
|
@@ -232,38 +232,47 @@ class _SchemaToPydantic:
|
|
|
232
232
|
|
|
233
233
|
The Python analog of the Cloudflare adapter's ``json_schema_to_zod``.
|
|
234
234
|
Named ``$defs`` become named classes / ``Literal`` aliases; inline
|
|
235
|
-
object schemas get synthesized names.
|
|
236
|
-
|
|
237
|
-
|
|
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.
|
|
238
238
|
"""
|
|
239
239
|
|
|
240
|
-
def __init__(self) -> None:
|
|
240
|
+
def __init__(self, *, reserved_names: tuple[str, ...] = ()) -> None:
|
|
241
241
|
self._defs: dict[str, dict[str, Any]] = {}
|
|
242
242
|
self._blocks: list[str] = []
|
|
243
243
|
self._emitted: dict[str, str] = {} # $def name -> python name
|
|
244
244
|
self._in_progress: set[str] = set()
|
|
245
|
-
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
|
+
}
|
|
246
257
|
self.uses_literal = False
|
|
247
258
|
self.uses_config_dict = False
|
|
248
259
|
self.uses_field = False
|
|
249
260
|
|
|
250
261
|
# ── public API ──
|
|
251
262
|
|
|
252
|
-
def add_defs(self, defs: dict[str, Any]) -> None:
|
|
253
|
-
for name, schema in defs.items():
|
|
254
|
-
if name in self._defs and self._defs[name] != schema:
|
|
255
|
-
raise ValueError(
|
|
256
|
-
f"signature emission: $defs entry {name!r} appears in both input "
|
|
257
|
-
f"and output schemas with different definitions"
|
|
258
|
-
)
|
|
259
|
-
self._defs[name] = schema
|
|
260
|
-
|
|
261
263
|
def emit_root(self, class_name: str, schema: dict[str, Any]) -> None:
|
|
262
264
|
if schema.get("type") != "object":
|
|
263
265
|
raise ValueError(
|
|
264
266
|
f"signature emission: root signature schema for {class_name} must be "
|
|
265
267
|
f"an object schema, got type={schema.get('type')!r}"
|
|
266
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)
|
|
267
276
|
self._emit_object_class(class_name, schema)
|
|
268
277
|
|
|
269
278
|
@property
|
|
@@ -498,9 +507,7 @@ def emit_signature_module(
|
|
|
498
507
|
spec = node.signature_spec
|
|
499
508
|
pascal = _to_pascal_case(node.id)
|
|
500
509
|
|
|
501
|
-
converter = _SchemaToPydantic()
|
|
502
|
-
converter.add_defs(spec.input_schema.get("$defs", {}))
|
|
503
|
-
converter.add_defs(spec.output_schema.get("$defs", {}))
|
|
510
|
+
converter = _SchemaToPydantic(reserved_names=(pascal, f"{pascal}Input", f"{pascal}Output"))
|
|
504
511
|
converter.emit_root(f"{pascal}Input", spec.input_schema)
|
|
505
512
|
converter.emit_root(f"{pascal}Output", spec.output_schema)
|
|
506
513
|
|
|
@@ -1699,7 +1699,9 @@ function isUnauthorizedWorkers(err: unknown): boolean {
|
|
|
1699
1699
|
"""
|
|
1700
1700
|
|
|
1701
1701
|
|
|
1702
|
-
def emit_workers_mcp_call_module(
|
|
1702
|
+
def emit_workers_mcp_call_module(
|
|
1703
|
+
node: Node, *, generated_by: str, mcp_client: str = "direct"
|
|
1704
|
+
) -> str:
|
|
1703
1705
|
"""Emit ``src/extracted/<id>.ts`` as a working Workers MCP call.
|
|
1704
1706
|
|
|
1705
1707
|
Same module path and function name as the stub, but the function
|
|
@@ -1735,8 +1737,23 @@ def emit_workers_mcp_call_module(node: Node, *, generated_by: str) -> str:
|
|
|
1735
1737
|
f" * {desc_first}",
|
|
1736
1738
|
" *",
|
|
1737
1739
|
f" * Auto-generated by {generated_by}: invokes tool {binding.tool!r} on MCP",
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
+
# The provisioning sentence has to match the helper this build
|
|
1741
|
+
# actually emitted. Binding mode reads no per-server secrets at
|
|
1742
|
+
# all, and this header is the file an operator opens when a call
|
|
1743
|
+
# fails, so naming the wrong mechanism sends them to configure
|
|
1744
|
+
# secrets nothing will ever read.
|
|
1745
|
+
*(
|
|
1746
|
+
[
|
|
1747
|
+
f" * server {binding.server!r} through the platform's ROTE_MCP service",
|
|
1748
|
+
" * binding. The platform provisions the connection; this build reads",
|
|
1749
|
+
" * no per-server secrets.",
|
|
1750
|
+
]
|
|
1751
|
+
if mcp_client == "binding"
|
|
1752
|
+
else [
|
|
1753
|
+
f" * server {binding.server!r} over Streamable HTTP, authenticated from",
|
|
1754
|
+
f" * Worker secrets provisioned by `rote mcp export {binding.server}`.",
|
|
1755
|
+
]
|
|
1756
|
+
),
|
|
1740
1757
|
" * Swap to a direct vendor-SDK call with `rote emit --backend api`.",
|
|
1741
1758
|
*mandatory_lines,
|
|
1742
1759
|
" */",
|
|
@@ -1762,6 +1779,337 @@ def emit_workers_mcp_call_module(node: Node, *, generated_by: str) -> str:
|
|
|
1762
1779
|
return "\n".join(lines)
|
|
1763
1780
|
|
|
1764
1781
|
|
|
1782
|
+
#: Source of the BINDING variant of ``src/extracted/_roteMcp.ts`` —
|
|
1783
|
+
#: emitted when the Cloudflare adapter runs with ``mcp_client="binding"``.
|
|
1784
|
+
#: Same exported surface as :data:`ROTE_MCP_WORKERS_HELPER_TS` (the node
|
|
1785
|
+
#: modules and agent loops import both identically), but every operation
|
|
1786
|
+
#: is delegated to a platform-provisioned ``ROTE_MCP`` service binding:
|
|
1787
|
+
#: no token store, no OAuth refresh, no per-server secrets, and no MCP
|
|
1788
|
+
#: SDK dependency. Every proxy call carries a signed caller-auth object
|
|
1789
|
+
#: built from dispatcher-injected env vars (``ROTE_TENANT_ID`` /
|
|
1790
|
+
#: ``ROTE_PIPELINE`` / ``ROTE_RUN_ID`` / ``ROTE_MCP_SIG``) so tenant
|
|
1791
|
+
#: isolates cannot impersonate each other. The platform proxy signals a
|
|
1792
|
+
#: credential problem with a plain Error whose message starts with
|
|
1793
|
+
#: ``ROTE_MCP_AUTH_NEEDED:`` (RPC boundaries strip class identity, so
|
|
1794
|
+
#: the signal rides in the message); the helper rethrows that as
|
|
1795
|
+
#: RoteMcpAuthNeeded so the emitted park-on-auth loop works unchanged.
|
|
1796
|
+
ROTE_MCP_BINDING_HELPER_TS = """/**
|
|
1797
|
+
* MCP helper for platform-managed connections (generated by rote — do
|
|
1798
|
+
* not edit; the source of truth is
|
|
1799
|
+
* rote.adapters._ts_common.ROTE_MCP_BINDING_HELPER_TS).
|
|
1800
|
+
*
|
|
1801
|
+
* Every MCP operation is delegated to the `ROTE_MCP` service binding —
|
|
1802
|
+
* an RPC stub the hosting platform provisions on this Worker. The
|
|
1803
|
+
* platform owns endpoint resolution, credentials, and token refresh, so
|
|
1804
|
+
* this variant carries no token store, no OAuth, and no per-server
|
|
1805
|
+
* secrets. Every call carries a signed caller-auth object built from
|
|
1806
|
+
* the dispatcher-injected ROTE_TENANT_ID / ROTE_PIPELINE / ROTE_RUN_ID /
|
|
1807
|
+
* ROTE_MCP_SIG variables, so tenant isolates cannot impersonate each
|
|
1808
|
+
* other. When the platform proxy reports a missing or dead credential
|
|
1809
|
+
* (a plain Error whose message starts with "ROTE_MCP_AUTH_NEEDED:"),
|
|
1810
|
+
* the helper rethrows RoteMcpAuthNeeded so the workflow's park-on-auth
|
|
1811
|
+
* loop suspends the instance until the connection is re-authorized.
|
|
1812
|
+
*/
|
|
1813
|
+
|
|
1814
|
+
/** One tool as the server advertises it. */
|
|
1815
|
+
export interface McpToolSpec {
|
|
1816
|
+
name: string;
|
|
1817
|
+
description: string;
|
|
1818
|
+
inputSchema: Record<string, unknown>;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/** ROTE_MCP_SIG authenticates tenant + pipeline; run_id is attribution. */
|
|
1822
|
+
export interface RoteMcpAuth {
|
|
1823
|
+
tenant_id: string;
|
|
1824
|
+
pipeline: string;
|
|
1825
|
+
run_id?: string;
|
|
1826
|
+
sig: string;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/** One call's outcome, as the proxy reports it. */
|
|
1830
|
+
export interface RoteMcpCallResult {
|
|
1831
|
+
/** Text string, or the tool's structured object. */
|
|
1832
|
+
content: unknown;
|
|
1833
|
+
is_error: boolean;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/** One tool as the proxy advertises it (snake_case wire shape). */
|
|
1837
|
+
export interface RoteMcpToolInfo {
|
|
1838
|
+
name: string;
|
|
1839
|
+
description: string | null;
|
|
1840
|
+
input_schema: unknown;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/** The platform's MCP proxy, provisioned as an RPC service binding. */
|
|
1844
|
+
export interface RoteMcpBinding {
|
|
1845
|
+
call(
|
|
1846
|
+
auth: RoteMcpAuth,
|
|
1847
|
+
server: string,
|
|
1848
|
+
tool: string,
|
|
1849
|
+
args: Record<string, unknown>,
|
|
1850
|
+
): Promise<RoteMcpCallResult>;
|
|
1851
|
+
listTools(auth: RoteMcpAuth, server: string): Promise<RoteMcpToolInfo[]>;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
export interface RoteMcpEnv {
|
|
1855
|
+
ROTE_MCP: RoteMcpBinding;
|
|
1856
|
+
[key: string]: unknown;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* The MCP server's platform connection needs to be re-authorized.
|
|
1861
|
+
* Detect with isRoteMcpAuthNeeded(), never `instanceof`: Workflows
|
|
1862
|
+
* serialize errors across hibernation and replay boundaries — class
|
|
1863
|
+
* identity is lost, `name` survives.
|
|
1864
|
+
*/
|
|
1865
|
+
export class RoteMcpAuthNeeded extends Error {
|
|
1866
|
+
readonly server: string;
|
|
1867
|
+
readonly reason: string;
|
|
1868
|
+
|
|
1869
|
+
constructor(server: string, reason: string) {
|
|
1870
|
+
super(
|
|
1871
|
+
`MCP server '${server}' needs (re)authentication — ${reason}. ` +
|
|
1872
|
+
`Re-authorize the connection on the platform hosting this worker.`,
|
|
1873
|
+
);
|
|
1874
|
+
this.name = "RoteMcpAuthNeeded";
|
|
1875
|
+
this.server = server;
|
|
1876
|
+
this.reason = reason;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/** True if a RoteMcpAuthNeeded hides anywhere in the error tree. */
|
|
1881
|
+
export function isRoteMcpAuthNeeded(err: unknown): boolean {
|
|
1882
|
+
let node: unknown = err;
|
|
1883
|
+
for (let depth = 0; node != null && depth < 16; depth++) {
|
|
1884
|
+
const e = node as { name?: unknown; errors?: unknown; cause?: unknown };
|
|
1885
|
+
if (e.name === "RoteMcpAuthNeeded") return true;
|
|
1886
|
+
if (Array.isArray(e.errors) && e.errors.some((sub) => isRoteMcpAuthNeeded(sub))) {
|
|
1887
|
+
return true;
|
|
1888
|
+
}
|
|
1889
|
+
node = e.cause;
|
|
1890
|
+
}
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// Concrete JSON types — same rationale as the direct helper: step.do
|
|
1895
|
+
// constrains callback returns to Rpc.Serializable, which `unknown`
|
|
1896
|
+
// members do not satisfy.
|
|
1897
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
|
|
1898
|
+
export type JsonObject = { [key: string]: JsonValue };
|
|
1899
|
+
|
|
1900
|
+
/** The platform proxy's auth-failure contract: errors arrive as plain
|
|
1901
|
+
* Error (the RPC boundary strips class identity), so the signal rides
|
|
1902
|
+
* in the message prefix. */
|
|
1903
|
+
const AUTH_PREFIX = "ROTE_MCP_AUTH_NEEDED:";
|
|
1904
|
+
|
|
1905
|
+
function translate(err: unknown, server: string): unknown {
|
|
1906
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1907
|
+
if (msg.startsWith(AUTH_PREFIX)) {
|
|
1908
|
+
let reason = msg.slice(AUTH_PREFIX.length).trim();
|
|
1909
|
+
// The proxy's detail leads with the server name (its parse token);
|
|
1910
|
+
// RoteMcpAuthNeeded's own message already names the server, so drop
|
|
1911
|
+
// the leading token rather than saying it twice.
|
|
1912
|
+
if (reason === server) {
|
|
1913
|
+
reason = "";
|
|
1914
|
+
} else if (reason.startsWith(`${server} `)) {
|
|
1915
|
+
reason = reason.slice(server.length + 1).trim();
|
|
1916
|
+
}
|
|
1917
|
+
return new RoteMcpAuthNeeded(
|
|
1918
|
+
server,
|
|
1919
|
+
reason || "the platform reports the connection needs re-authorization",
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
return err;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function requireBinding(env: RoteMcpEnv): void {
|
|
1926
|
+
if (!env.ROTE_MCP) {
|
|
1927
|
+
throw new Error(
|
|
1928
|
+
"the ROTE_MCP service binding is not configured — this build " +
|
|
1929
|
+
"delegates MCP calls to the hosting platform, which must " +
|
|
1930
|
+
"provision the binding",
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function requireVar(env: RoteMcpEnv, name: string): string {
|
|
1936
|
+
const value = env[name];
|
|
1937
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1938
|
+
throw new Error(
|
|
1939
|
+
`the ${name} variable is not set — binding-mode MCP runs only on ` +
|
|
1940
|
+
"the platform, whose dispatcher injects it into the isolate",
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
return value;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/** The signed caller identity sent on every proxy call. */
|
|
1947
|
+
function buildAuth(env: RoteMcpEnv): RoteMcpAuth {
|
|
1948
|
+
// ROTE_MCP_SIG first: its absence is the clearest sign this build is
|
|
1949
|
+
// running off-platform, and the config error should say so.
|
|
1950
|
+
const sig = requireVar(env, "ROTE_MCP_SIG");
|
|
1951
|
+
const auth: RoteMcpAuth = {
|
|
1952
|
+
tenant_id: requireVar(env, "ROTE_TENANT_ID"),
|
|
1953
|
+
pipeline: requireVar(env, "ROTE_PIPELINE"),
|
|
1954
|
+
sig,
|
|
1955
|
+
};
|
|
1956
|
+
const runId = env.ROTE_RUN_ID;
|
|
1957
|
+
if (typeof runId === "string" && runId.length > 0) {
|
|
1958
|
+
auth.run_id = runId;
|
|
1959
|
+
}
|
|
1960
|
+
return auth;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* Call one tool on an MCP server through the platform proxy.
|
|
1965
|
+
*
|
|
1966
|
+
* `is_error` results throw exactly the way the direct helper treats
|
|
1967
|
+
* `result.isError`, and string content is JSON-parsed the way the
|
|
1968
|
+
* direct helper parses text blocks — so a workflow step behaves the
|
|
1969
|
+
* same under either client mode.
|
|
1970
|
+
*/
|
|
1971
|
+
export async function callMcpTool(
|
|
1972
|
+
env: RoteMcpEnv,
|
|
1973
|
+
server: string,
|
|
1974
|
+
_pipelineUrl: string | null,
|
|
1975
|
+
tool: string,
|
|
1976
|
+
args: Record<string, unknown>,
|
|
1977
|
+
): Promise<JsonObject> {
|
|
1978
|
+
requireBinding(env);
|
|
1979
|
+
const auth = buildAuth(env);
|
|
1980
|
+
let result: RoteMcpCallResult;
|
|
1981
|
+
try {
|
|
1982
|
+
result = await env.ROTE_MCP.call(auth, server, tool, args);
|
|
1983
|
+
} catch (err) {
|
|
1984
|
+
throw translate(err, server);
|
|
1985
|
+
}
|
|
1986
|
+
if (result.is_error) {
|
|
1987
|
+
throw new Error(
|
|
1988
|
+
`MCP tool '${tool}' on '${server}' returned an error: ` +
|
|
1989
|
+
JSON.stringify(result.content),
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
const content = result.content;
|
|
1993
|
+
if (typeof content === "string") {
|
|
1994
|
+
return (content ? JSON.parse(content) : {}) as JsonObject;
|
|
1995
|
+
}
|
|
1996
|
+
return (content ?? {}) as JsonObject;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* Every tool `server` advertises, fetched through the platform proxy.
|
|
2001
|
+
*
|
|
2002
|
+
* The proxy's snake_case wire shape (`input_schema`, nullable
|
|
2003
|
+
* description) is normalized into McpToolSpec so bindAgentTools and the
|
|
2004
|
+
* agent loops see the same shape under either client mode.
|
|
2005
|
+
*/
|
|
2006
|
+
export async function listMcpTools(
|
|
2007
|
+
env: RoteMcpEnv,
|
|
2008
|
+
server: string,
|
|
2009
|
+
_pipelineUrl: string | null,
|
|
2010
|
+
): Promise<McpToolSpec[]> {
|
|
2011
|
+
requireBinding(env);
|
|
2012
|
+
const auth = buildAuth(env);
|
|
2013
|
+
let tools: RoteMcpToolInfo[];
|
|
2014
|
+
try {
|
|
2015
|
+
tools = await env.ROTE_MCP.listTools(auth, server);
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
throw translate(err, server);
|
|
2018
|
+
}
|
|
2019
|
+
return tools.map((tool) => ({
|
|
2020
|
+
name: tool.name,
|
|
2021
|
+
description: tool.description ?? "",
|
|
2022
|
+
inputSchema: (tool.input_schema ?? {
|
|
2023
|
+
type: "object",
|
|
2024
|
+
properties: {},
|
|
2025
|
+
}) as Record<string, unknown>,
|
|
2026
|
+
}));
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/** A tool bound to a callable — structurally a BoundTool for the agent loop. */
|
|
2030
|
+
export interface BoundMcpTool {
|
|
2031
|
+
name: string;
|
|
2032
|
+
description: string;
|
|
2033
|
+
parameters: Record<string, unknown>;
|
|
2034
|
+
run: (args: Record<string, unknown>) => Promise<JsonObject>;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* Candidate servers for tool discovery — same contract as the direct
|
|
2039
|
+
* helper: the pipeline's declared servers, with ROTE_MCP_SERVERS
|
|
2040
|
+
* replacing the list wholesale when an operator overrides it.
|
|
2041
|
+
*/
|
|
2042
|
+
function agentServerNames(env: RoteMcpEnv, declared: string[]): string[] {
|
|
2043
|
+
const override = env.ROTE_MCP_SERVERS;
|
|
2044
|
+
if (typeof override === "string" && override.length > 0) {
|
|
2045
|
+
return override
|
|
2046
|
+
.split(",")
|
|
2047
|
+
.map((name) => name.trim())
|
|
2048
|
+
.filter(Boolean);
|
|
2049
|
+
}
|
|
2050
|
+
return [...new Set(declared)].sort();
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* Bind the tools an agent_loop declared to callables the loop can invoke.
|
|
2055
|
+
*
|
|
2056
|
+
* Same contract as the direct helper: discovery is best-effort per server
|
|
2057
|
+
* (a server that is down may not be one this loop needed), a declared
|
|
2058
|
+
* tool no reachable server provides is fatal, and an auth failure that
|
|
2059
|
+
* plausibly explains the gap is rethrown so the Workflow parks on it.
|
|
2060
|
+
*/
|
|
2061
|
+
export async function bindAgentTools(
|
|
2062
|
+
env: RoteMcpEnv,
|
|
2063
|
+
allowed: string[],
|
|
2064
|
+
declaredServers: string[],
|
|
2065
|
+
serverUrls: Record<string, string | null> = {},
|
|
2066
|
+
toolServers: Record<string, string> = {},
|
|
2067
|
+
): Promise<BoundMcpTool[]> {
|
|
2068
|
+
const wanted = new Set(allowed);
|
|
2069
|
+
const bound = new Map<string, BoundMcpTool>();
|
|
2070
|
+
const failures: unknown[] = [];
|
|
2071
|
+
|
|
2072
|
+
for (const server of agentServerNames(env, declaredServers)) {
|
|
2073
|
+
const url = serverUrls[server] ?? null;
|
|
2074
|
+
let specs: McpToolSpec[];
|
|
2075
|
+
try {
|
|
2076
|
+
specs = await listMcpTools(env, server, url);
|
|
2077
|
+
} catch (err) {
|
|
2078
|
+
failures.push(err);
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
for (const spec of specs) {
|
|
2082
|
+
if (!wanted.has(spec.name) || bound.has(spec.name)) continue;
|
|
2083
|
+
// Resolved tools bind only from their own server — see the
|
|
2084
|
+
// direct helper's twin for why first-wins is not good enough.
|
|
2085
|
+
const pinned = toolServers[spec.name];
|
|
2086
|
+
if (pinned !== undefined && pinned !== server) continue;
|
|
2087
|
+
bound.set(spec.name, {
|
|
2088
|
+
name: spec.name,
|
|
2089
|
+
description: spec.description,
|
|
2090
|
+
parameters: spec.inputSchema,
|
|
2091
|
+
run: (args) => callMcpTool(env, server, url, spec.name, args),
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
const missing = allowed.filter((name) => !bound.has(name));
|
|
2097
|
+
if (missing.length > 0) {
|
|
2098
|
+
const authFailure = failures.find((err) => isRoteMcpAuthNeeded(err));
|
|
2099
|
+
if (authFailure) throw authFailure;
|
|
2100
|
+
const detail = failures
|
|
2101
|
+
.map((err) => (err instanceof Error ? err.message : String(err)))
|
|
2102
|
+
.join("; ");
|
|
2103
|
+
throw new Error(
|
|
2104
|
+
`agent tools not provided by any reachable MCP server: ${missing.join(", ")}` +
|
|
2105
|
+
(detail ? ` (unreachable: ${detail})` : ""),
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
return [...bound.values()];
|
|
2109
|
+
}
|
|
2110
|
+
"""
|
|
2111
|
+
|
|
2112
|
+
|
|
1765
2113
|
# ───────── signatures/_roteInference.ts (agent loops) ─────────
|
|
1766
2114
|
|
|
1767
2115
|
#: The default Workers AI model for an agent loop. Tool-capable and the
|