rote-cli 0.7.0__tar.gz → 0.9.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.
Files changed (53) hide show
  1. {rote_cli-0.7.0 → rote_cli-0.9.0}/PKG-INFO +7 -1
  2. {rote_cli-0.7.0 → rote_cli-0.9.0}/pyproject.toml +8 -0
  3. {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/SKILL.md +13 -0
  4. {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/eval-estimates.md +41 -20
  5. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/__init__.py +1 -1
  6. rote_cli-0.9.0/src/rote/__main__.py +12 -0
  7. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/__init__.py +28 -11
  8. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/_common.py +47 -0
  9. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/_ts_common.py +573 -0
  10. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/cloudflare.py +146 -15
  11. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/dbos.py +29 -16
  12. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/dbos_ts.py +27 -2
  13. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/inngest.py +27 -2
  14. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/cli.py +403 -66
  15. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/__init__.py +4 -0
  16. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/empirical.py +182 -2
  17. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/estimate.py +20 -6
  18. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/priors.py +13 -0
  19. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/scorecard.py +69 -2
  20. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/sidecar.py +30 -3
  21. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/__init__.py +141 -4
  22. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/drivers/__init__.py +22 -0
  23. rote_cli-0.9.0/src/rote/graduator/drivers/_fs_tools.py +280 -0
  24. rote_cli-0.9.0/src/rote/graduator/drivers/anthropic_api.py +466 -0
  25. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/drivers/claude.py +197 -41
  26. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/drivers/codex.py +13 -2
  27. rote_cli-0.9.0/src/rote/graduator/drivers/openai_api.py +431 -0
  28. rote_cli-0.9.0/src/rote/graduator/events.py +255 -0
  29. rote_cli-0.9.0/src/rote/mcp/__init__.py +48 -0
  30. rote_cli-0.9.0/src/rote/mcp/_runtime_helper.py +247 -0
  31. rote_cli-0.9.0/src/rote/mcp/auth.py +166 -0
  32. rote_cli-0.9.0/src/rote/mcp/registry.py +124 -0
  33. rote_cli-0.9.0/src/rote/mcp/tokens.py +246 -0
  34. rote_cli-0.7.0/src/rote/graduator/drivers/anthropic_api.py +0 -423
  35. {rote_cli-0.7.0 → rote_cli-0.9.0}/.gitignore +0 -0
  36. {rote_cli-0.7.0 → rote_cli-0.9.0}/LICENSE +0 -0
  37. {rote_cli-0.7.0 → rote_cli-0.9.0}/README.md +0 -0
  38. {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/crystallization-heuristics.md +0 -0
  39. {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/ir-schema.md +0 -0
  40. {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/llm-judge-extraction.md +0 -0
  41. {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/node-kinds.md +0 -0
  42. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/_py_common.py +0 -0
  43. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/python.py +0 -0
  44. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/temporal.py +0 -0
  45. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/pricing.py +0 -0
  46. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/tokens.py +0 -0
  47. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/update.py +0 -0
  48. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/ir.py +0 -0
  49. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/serve/__init__.py +0 -0
  50. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/serve/backends.py +0 -0
  51. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/serve/registry.py +0 -0
  52. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/serve/server.py +0 -0
  53. {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/skill_source.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rote-cli
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Graduate fuzzy AI skills into deterministic, reliable workflows
5
5
  Project-URL: Homepage, https://github.com/trevhud/rote
6
6
  Project-URL: Repository, https://github.com/trevhud/rote
@@ -32,11 +32,17 @@ Requires-Dist: dbos>=2.26; extra == 'dev'
32
32
  Requires-Dist: fastmcp>=3.4.2; extra == 'dev'
33
33
  Requires-Dist: httpx>=0.28; extra == 'dev'
34
34
  Requires-Dist: mypy>=1.10; extra == 'dev'
35
+ Requires-Dist: openai>=2.45; extra == 'dev'
35
36
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
36
37
  Requires-Dist: pytest>=8.0; extra == 'dev'
37
38
  Requires-Dist: ruff>=0.6; extra == 'dev'
38
39
  Requires-Dist: temporalio>=1.7; extra == 'dev'
39
40
  Requires-Dist: types-pyyaml; extra == 'dev'
41
+ Provides-Extra: mcp
42
+ Requires-Dist: fastmcp>=3.4.2; extra == 'mcp'
43
+ Requires-Dist: httpx>=0.28; extra == 'mcp'
44
+ Provides-Extra: openai-api
45
+ Requires-Dist: openai>=2.45; extra == 'openai-api'
40
46
  Provides-Extra: serve
41
47
  Requires-Dist: fastmcp>=3.4.2; extra == 'serve'
42
48
  Requires-Dist: httpx>=0.28; extra == 'serve'
@@ -50,6 +50,9 @@ temporal = [
50
50
  api = [
51
51
  "anthropic>=0.89",
52
52
  ]
53
+ openai-api = [
54
+ "openai>=2.45",
55
+ ]
53
56
  dbos = [
54
57
  "dbos>=2.26",
55
58
  ]
@@ -57,6 +60,10 @@ serve = [
57
60
  "fastmcp>=3.4.2",
58
61
  "httpx>=0.28",
59
62
  ]
63
+ mcp = [
64
+ "fastmcp>=3.4.2",
65
+ "httpx>=0.28",
66
+ ]
60
67
  dev = [
61
68
  "pytest>=8.0",
62
69
  "pytest-asyncio>=0.23",
@@ -65,6 +72,7 @@ dev = [
65
72
  "types-pyyaml",
66
73
  "temporalio>=1.7",
67
74
  "anthropic>=0.89",
75
+ "openai>=2.45",
68
76
  "dbos>=2.26",
69
77
  "fastmcp>=3.4.2",
70
78
  "httpx>=0.28",
@@ -46,6 +46,19 @@ re-deriving a known-fixed procedure at runtime is waste.
46
46
 
47
47
  ## Pipeline
48
48
 
49
+ **Progress markers.** At the START of each Phase N, rewrite
50
+ `progress.ndjson` in the work directory so it holds one JSON line per
51
+ phase entered so far, in order:
52
+
53
+ ```
54
+ {"phase": 1, "name": "Intake"}
55
+ {"phase": 2, "name": "Node Classification"}
56
+ ```
57
+
58
+ Rewrite the whole file each time (append the new phase's line to the
59
+ ones already there). This file is machine-read for live progress
60
+ display; it is not a deliverable and never affects the IR.
61
+
49
62
  ### Phase 1: Intake
50
63
 
51
64
  Read the target directory exhaustively:
@@ -25,6 +25,13 @@ Ground rules:
25
25
  (agents batch parallel calls). If it says "repeat until you have 25
26
26
  qualified contacts", estimate the realistic iteration count from any
27
27
  hints (quota sizes, batch sizes, typical yields), not the worst case.
28
+ - **Steps that repeat per item MUST declare `iterations`.** A "process
29
+ each row" step costs its per-row turns *times the row count* — this
30
+ is almost always the dominant term, and flattening it understates the
31
+ whole run by 5–20×. Put the per-iteration turns in `estimated_turns`
32
+ and the realistic item-count range in `iterations` (page sizes,
33
+ pagination labels, batch quotas, and filter windows in the skill text
34
+ are your evidence). The loader multiplies them; never pre-multiply.
28
35
  - **Include the overhead the prose hides.** Reading reference files,
29
36
  re-checking rubric sections, correcting malformed tool calls — a
30
37
  realistic agent spends turns on these. A step whose happy path is 2
@@ -54,9 +61,14 @@ steps:
54
61
  phase: "1"
55
62
  estimated_turns: {low: 1, high: 2}
56
63
  estimated_tool_calls: {low: 4, high: 4}
57
- totals: # optional — omit if the sum is right
58
- low: 28
59
- high: 45
64
+ - description: "Push each eligible invoice and capture its sent date"
65
+ node_id: process_invoices_loop
66
+ phase: "Processing"
67
+ estimated_turns: {low: 3, high: 6} # PER ROW —
68
+ iterations: {low: 20, high: 90} # — × realistic row count
69
+ totals: # optional — omit if the sum is right;
70
+ low: 88 # must be WHOLE-RUN (post-iteration)
71
+ high: 602 # when any step declares iterations
60
72
  notes: >-
61
73
  Lead generation dominates: the loop re-queries ZoomInfo with adjusted
62
74
  filters until the quota is met; typical campaigns converge in 3-5
@@ -72,30 +84,39 @@ Field reference:
72
84
  | `steps[].description` | yes | The step, in the skill's own vocabulary |
73
85
  | `steps[].node_id` | no | IR node id, only when the mapping is clean 1:1 |
74
86
  | `steps[].phase` | no | Source skill phase, same convention as the IR |
75
- | `steps[].estimated_turns` | yes | `{low, high}` agent turns |
87
+ | `steps[].estimated_turns` | yes | `{low, high}` agent turns (per iteration when `iterations` is set) |
76
88
  | `steps[].estimated_tool_calls` | no | `{low, high}` tool calls, when distinct from turns |
77
- | `totals` | no | Whole-run `{low, high}`; omit to mean "sum the steps" |
89
+ | `steps[].iterations` | when the step repeats | `{low, high}` repeats per run (rows, pages, items); whole-run cost = turns × iterations |
90
+ | `totals` | no | Whole-run `{low, high}`; omit to mean "sum the steps (× iterations)" |
78
91
  | `notes` | no | The one paragraph a reader needs to trust the numbers |
79
92
 
80
93
  Validated by `rote.eval.sidecar.EvalEstimates` — if you can run Python,
81
94
  `load_eval_estimates("eval.yaml")` is the dry check, same pattern as
82
95
  `load_pipeline`.
83
96
 
84
- ## Calibration anchors (BDR reference points)
85
-
86
- From real runs of BDR-scale skills (7 phases, ~20 distinct steps,
87
- heavy tool use):
88
-
89
- - Whole-run turn counts landed in the **30–57 turn** range.
90
- - Wall clock was **~13 seconds per turn** including tool execution.
91
- - A single fixed API call step: 1–2 turns. A batch-and-review step:
92
- 2–4. A bounded search loop: 6–15. A genuinely open research step:
93
- 4–10.
94
-
95
- If your step estimates sum to something wildly outside the anchor range
96
- for a comparably sized skill, re-examine before writing — either the
97
- skill really is bigger/smaller (fine, say so in `notes`) or a step
98
- estimate is off.
97
+ ## Calibration anchors (measured production runs)
98
+
99
+ Whole-run turn counts are regime-dependent — anchor to the skill's
100
+ *shape*, not to a universal range:
101
+
102
+ - **Sequential tool-heavy skills** (BDR-scale: 7 phases, ~20 distinct
103
+ steps, no per-item loop): **30–57 turns** measured. ~13 seconds per
104
+ turn including tool execution.
105
+ - **Per-item loop skills** (browser automation over a table, per-record
106
+ processing): measured production runs landed at **184 and 730 turns**
107
+ — the loop multiplier dominates everything else in the file. If a
108
+ skill says "for each row…", expect hundreds of turns, not tens, and
109
+ express it via `iterations`, never by inflating per-step turns.
110
+ - Per-step anchors: a single fixed API call step: 1–2 turns. A
111
+ batch-and-review step: 2–4. A bounded search loop: 6–15. A genuinely
112
+ open research step: 4–10. One iteration of a per-row browser cycle
113
+ (locate, act, confirm, refresh): 2–6.
114
+
115
+ If your estimate lands wildly outside the anchor for the skill's shape,
116
+ re-examine before writing — either the skill really is bigger/smaller
117
+ (fine, say so in `notes`) or a step estimate is off. A per-item skill
118
+ whose whole-run total sums to under ~100 turns almost certainly has a
119
+ missing `iterations` block.
99
120
 
100
121
  ## What NOT to do
101
122
 
@@ -1,3 +1,3 @@
1
1
  """rote — graduate fuzzy AI skills into deterministic workflows."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.9.0"
@@ -0,0 +1,12 @@
1
+ """``python -m rote`` — the same CLI as the ``rote`` console script.
2
+
3
+ Exists so generated configs can invoke rote through a known
4
+ interpreter (``sys.executable -m rote``) instead of guessing at PATH —
5
+ the eval harness's ``headersHelper`` wiring depends on this.
6
+ """
7
+
8
+ import sys
9
+
10
+ from rote.cli import main
11
+
12
+ sys.exit(main())
@@ -31,7 +31,17 @@ class Adapter(Protocol):
31
31
  #: Options a factory may receive (forwarded from ``get_adapter``). Factories
32
32
  #: swallow unknown keys via ``**options`` — mirrors the driver-registry
33
33
  #: convention — so a runtime that doesn't support an option just ignores it.
34
- #: ``external_backend`` ("mcp" | "api") is understood by the DBOS adapter.
34
+ #: ``external_backend`` ("mcp" | "api") is understood by the DBOS, DBOS-TS,
35
+ #: and Inngest adapters.
36
+
37
+
38
+ def _validated_backend(options: dict[str, Any]) -> Literal["mcp", "api"] | None:
39
+ backend = options.get("external_backend")
40
+ if backend is None:
41
+ return None
42
+ if backend not in ("mcp", "api"):
43
+ raise ValueError(f"external_backend must be 'mcp' or 'api', got {backend!r}")
44
+ return cast(Literal["mcp", "api"], backend)
35
45
 
36
46
 
37
47
  def _temporal_adapter_factory(**options: Any) -> Adapter:
@@ -43,32 +53,39 @@ def _temporal_adapter_factory(**options: Any) -> Adapter:
43
53
 
44
54
 
45
55
  def _cloudflare_adapter_factory(**options: Any) -> Adapter:
46
- from rote.adapters.cloudflare import CloudflareAdapter
56
+ from rote.adapters.cloudflare import CloudflareAdapter, CloudflareAdapterConfig
47
57
 
48
- return CloudflareAdapter()
58
+ backend = _validated_backend(options)
59
+ if backend is None:
60
+ return CloudflareAdapter()
61
+ return CloudflareAdapter(CloudflareAdapterConfig(external_backend=backend))
49
62
 
50
63
 
51
64
  def _dbos_adapter_factory(**options: Any) -> Adapter:
52
65
  from rote.adapters.dbos import DbosAdapter, DbosAdapterConfig
53
66
 
54
- backend = options.get("external_backend")
67
+ backend = _validated_backend(options)
55
68
  if backend is None:
56
69
  return DbosAdapter()
57
- if backend not in ("mcp", "api"):
58
- raise ValueError(f"external_backend must be 'mcp' or 'api', got {backend!r}")
59
- return DbosAdapter(DbosAdapterConfig(external_backend=cast(Literal["mcp", "api"], backend)))
70
+ return DbosAdapter(DbosAdapterConfig(external_backend=backend))
60
71
 
61
72
 
62
73
  def _dbos_ts_adapter_factory(**options: Any) -> Adapter:
63
- from rote.adapters.dbos_ts import DbosTsAdapter
74
+ from rote.adapters.dbos_ts import DbosTsAdapter, DbosTsAdapterConfig
64
75
 
65
- return DbosTsAdapter()
76
+ backend = _validated_backend(options)
77
+ if backend is None:
78
+ return DbosTsAdapter()
79
+ return DbosTsAdapter(DbosTsAdapterConfig(external_backend=backend))
66
80
 
67
81
 
68
82
  def _inngest_adapter_factory(**options: Any) -> Adapter:
69
- from rote.adapters.inngest import InngestAdapter
83
+ from rote.adapters.inngest import InngestAdapter, InngestAdapterConfig
70
84
 
71
- return InngestAdapter()
85
+ backend = _validated_backend(options)
86
+ if backend is None:
87
+ return InngestAdapter()
88
+ return InngestAdapter(InngestAdapterConfig(external_backend=backend))
72
89
 
73
90
 
74
91
  def _python_adapter_factory(**options: Any) -> Adapter:
@@ -194,6 +194,27 @@ class EmitWriter:
194
194
  )
195
195
 
196
196
 
197
+ class EmitResult(dict[str, Path]):
198
+ """An adapter emit mapping that also carries preserved-conflict paths.
199
+
200
+ It *is* the ``label -> written path`` dict every ``emit()`` has always
201
+ returned (the path is the ``.new`` sibling when a user-edited file was
202
+ preserved), so existing consumers that iterate it keep working
203
+ unchanged. The extra :attr:`preserved` attribute exposes the
204
+ :class:`EmitWriter`'s ``rel -> .new path`` map directly, for callers
205
+ (the cloud runner) that want the conflict set without sniffing ``.new``
206
+ suffixes off the values.
207
+ """
208
+
209
+ def __init__(
210
+ self,
211
+ written: dict[str, Path] | None = None,
212
+ preserved: dict[str, Path] | None = None,
213
+ ) -> None:
214
+ super().__init__(written or {})
215
+ self.preserved: dict[str, Path] = dict(preserved or {})
216
+
217
+
197
218
  # ───────── Case conversion ─────────
198
219
 
199
220
 
@@ -238,6 +259,32 @@ def _pipeline_hash(pipeline: Pipeline) -> str:
238
259
  return hashlib.sha256(payload.encode()).hexdigest()[:8]
239
260
 
240
261
 
262
+ def workflow_class_name(pipeline: Pipeline) -> str:
263
+ """The generated workflow class name: ``<PascalName>Workflow``.
264
+
265
+ The Cloudflare and Temporal adapters both name their emitted
266
+ workflow class this way; centralizing it here keeps the manifest the
267
+ cloud consumes and the emitted TypeScript from drifting apart.
268
+ """
269
+ return f"{_to_pascal_case(pipeline.name)}Workflow"
270
+
271
+
272
+ def pipeline_identity(pipeline: Pipeline) -> dict[str, str]:
273
+ """Stable identity fields for a pipeline, shared by emitters and manifests.
274
+
275
+ Returns ``{name, version, pipeline_hash, class_name}`` — the same four
276
+ values the Cloudflare adapter bakes into the workflow.ts header and
277
+ writes into ``manifest.json`` for the cloud runner. Single source of
278
+ truth so the two can't disagree.
279
+ """
280
+ return {
281
+ "name": pipeline.name,
282
+ "version": pipeline.version,
283
+ "pipeline_hash": _pipeline_hash(pipeline),
284
+ "class_name": workflow_class_name(pipeline),
285
+ }
286
+
287
+
241
288
  # ───────── Duration strings ─────────
242
289
 
243
290
  _IR_DURATION_RE = re.compile(r"^(\d+(?:\.\d+)?)(ms|s|m|h|d)$")