rocket-engine 0.0.2b1__py3-none-any.whl
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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# execution — run lifecycle, isolation, and verdicts
|
|
2
|
+
|
|
3
|
+
`execution/` provides run-state services and execution helpers used by the engine composition root. It covers admission/backpressure, solver process isolation, the Python execution approval surface, normalized evidence, and verdict evaluation. It contains no UI code and owns no solver-specific parsing.
|
|
4
|
+
|
|
5
|
+
## Main pieces
|
|
6
|
+
|
|
7
|
+
- `service.py`: in-memory run and approval records, cancellation state, evidence/findings access, and verdict evaluation. Run records live for the lifetime of the `Engine` instance.
|
|
8
|
+
- `apig_pool.py`, `coalesce.py`, and `block_runs_scope.py`: bounded dispatch, duplicate-submit coalescing, and block-scoped run input handling.
|
|
9
|
+
- `runner.py`: subprocess helpers and `BwrapRunner` for OS-level tool isolation when the host supports the required sandbox.
|
|
10
|
+
- `replay_safe.py`: reports interrupted runs after restart and requires an explicit resume or discard decision; it does not silently re-execute survivors.
|
|
11
|
+
- `python_exec.py` and `console.py`: project-script/console request handling and approval-binding semantics. `python_execute` requests admission; the caller cannot approve its own code.
|
|
12
|
+
- `dispatch.py`: shared operation-call parsing and dispatch used by the CLI and console paths.
|
|
13
|
+
|
|
14
|
+
## Run flow
|
|
15
|
+
|
|
16
|
+
`Engine.submit` validates and admits a solver operation, captures its project context, dispatches the selected adapter, and normally returns the completed result inline. `run_job` is available for deferred work, pool draining, batch pumping, and idempotent retrieval of an admitted run's completion. The two surfaces share the same run IDs and verdict rules.
|
|
17
|
+
|
|
18
|
+
Solver adapters return structured findings, measurements, and artifacts. The engine evaluates those results into `PASS`, `FAIL`, `NOT_RUN`, or `UNSUPPORTED`; a zero process exit does not establish `PASS`. Missing isolation support disables isolated work instead of falling back to an unrestricted host process.
|
|
19
|
+
|
|
20
|
+
## Persistence boundary
|
|
21
|
+
|
|
22
|
+
The project ledger can persist when `engine_root` is configured, but `ExecutionService` run state and captured logs are in-memory per engine instance. The MCP launcher can write completed run evidence under the bound project's `results/engine-runs/`. These are separate lifecycles; do not assume every queue item or log survives an engine restart.
|
|
23
|
+
|
|
24
|
+
## Routing
|
|
25
|
+
|
|
26
|
+
- Project revisions/events → [`project/`](../project/README.md).
|
|
27
|
+
- Result and command records → [`interfaces/`](../interfaces/README.md).
|
|
28
|
+
- Tool invocation and parsing → [`adapters/`](../adapters/README.md).
|
|
29
|
+
- Process ordering and the fully-verified gate → [`workflows/`](../workflows/README.md).
|
|
30
|
+
- Isolation and console contracts → [Python console spec](../../../PRD/tools-research/python-console-spec.md).
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Admission, queue, dispatch, isolated workers, cancel, evidence, evaluation. Owns execution."""
|
|
2
|
+
|
|
3
|
+
from .apig.pool import (
|
|
4
|
+
ApigPool,
|
|
5
|
+
pending_from_events,
|
|
6
|
+
request_for_run,
|
|
7
|
+
stored_completion_from_events,
|
|
8
|
+
)
|
|
9
|
+
from .apig.submit import SubmitImmediate, merge_ack_completion
|
|
10
|
+
from .block_runs_scope import (
|
|
11
|
+
BLOCK_RUN_OPERATIONS,
|
|
12
|
+
BLOCK_SCOPE,
|
|
13
|
+
PROJECT_SCOPE,
|
|
14
|
+
SUPPORTED_SCOPES,
|
|
15
|
+
BlockScopeError,
|
|
16
|
+
build_scope_record,
|
|
17
|
+
check_operation_scope,
|
|
18
|
+
closure_hash,
|
|
19
|
+
narrow_job_for_block,
|
|
20
|
+
parse_target,
|
|
21
|
+
resolve_block,
|
|
22
|
+
scope_provenance,
|
|
23
|
+
snapshot_closure,
|
|
24
|
+
stamp_verdict,
|
|
25
|
+
)
|
|
26
|
+
from .coalesce import (
|
|
27
|
+
Coalescer,
|
|
28
|
+
content_key,
|
|
29
|
+
deck_hash,
|
|
30
|
+
normalize_arguments,
|
|
31
|
+
)
|
|
32
|
+
from .console.apply_session import ConsoleSession, get_help
|
|
33
|
+
from .routing.calls import (
|
|
34
|
+
CONSOLE_TOOL_VERBS,
|
|
35
|
+
TIER_2_OPERATIONS,
|
|
36
|
+
admission_envelope,
|
|
37
|
+
build_effects_report,
|
|
38
|
+
collect_run_ids,
|
|
39
|
+
entity_doc_path,
|
|
40
|
+
format_address,
|
|
41
|
+
is_tier2,
|
|
42
|
+
parse_address,
|
|
43
|
+
report_completions,
|
|
44
|
+
stamp_tier2_call,
|
|
45
|
+
tier_for,
|
|
46
|
+
tool_call,
|
|
47
|
+
)
|
|
48
|
+
from .routing.namespace import ConsoleNamespace
|
|
49
|
+
from .routing.tools import ConsoleTools
|
|
50
|
+
from .dispatch import OpDispatcher, parse_op_call, parse_op_tokens
|
|
51
|
+
from .errors import ExecutionError, UsageError
|
|
52
|
+
from .python_exec import (
|
|
53
|
+
MCP_INPUT_SCHEMA,
|
|
54
|
+
PythonExecRunner,
|
|
55
|
+
PythonExecService,
|
|
56
|
+
build_worker_source,
|
|
57
|
+
not_run_verdict,
|
|
58
|
+
parse_exception,
|
|
59
|
+
prelude_digest_for,
|
|
60
|
+
resolve_script_path,
|
|
61
|
+
)
|
|
62
|
+
from .replay_safe import (
|
|
63
|
+
AMBIGUOUS,
|
|
64
|
+
POISON,
|
|
65
|
+
POISON_THRESHOLD,
|
|
66
|
+
REPLAY_SAFE_VERSION,
|
|
67
|
+
RETRYABLE,
|
|
68
|
+
ReplaySafe,
|
|
69
|
+
boot_message,
|
|
70
|
+
content_key_for,
|
|
71
|
+
interrupt_counts_from_events,
|
|
72
|
+
survivors_from_events,
|
|
73
|
+
user_message_for,
|
|
74
|
+
)
|
|
75
|
+
from .runner import BwrapRunner
|
|
76
|
+
from .service import ExecutionService
|
|
77
|
+
from .worker import CellRunner, CredentialGate, worker_profile
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"AMBIGUOUS",
|
|
81
|
+
"BLOCK_RUN_OPERATIONS",
|
|
82
|
+
"BLOCK_SCOPE",
|
|
83
|
+
"POISON",
|
|
84
|
+
"POISON_THRESHOLD",
|
|
85
|
+
"PROJECT_SCOPE",
|
|
86
|
+
"REPLAY_SAFE_VERSION",
|
|
87
|
+
"RETRYABLE",
|
|
88
|
+
"SUPPORTED_SCOPES",
|
|
89
|
+
"ApigPool",
|
|
90
|
+
"BlockScopeError",
|
|
91
|
+
"BwrapRunner",
|
|
92
|
+
"CellRunner",
|
|
93
|
+
"Coalescer",
|
|
94
|
+
"CONSOLE_TOOL_VERBS",
|
|
95
|
+
"ConsoleNamespace",
|
|
96
|
+
"ConsoleSession",
|
|
97
|
+
"ConsoleTools",
|
|
98
|
+
"CredentialGate",
|
|
99
|
+
"ExecutionError",
|
|
100
|
+
"ExecutionService",
|
|
101
|
+
"MCP_INPUT_SCHEMA",
|
|
102
|
+
"OpDispatcher",
|
|
103
|
+
"PythonExecRunner",
|
|
104
|
+
"PythonExecService",
|
|
105
|
+
"ReplaySafe",
|
|
106
|
+
"SubmitImmediate",
|
|
107
|
+
"TIER_2_OPERATIONS",
|
|
108
|
+
"UsageError",
|
|
109
|
+
"admission_envelope",
|
|
110
|
+
"boot_message",
|
|
111
|
+
"build_effects_report",
|
|
112
|
+
"build_scope_record",
|
|
113
|
+
"build_worker_source",
|
|
114
|
+
"check_operation_scope",
|
|
115
|
+
"closure_hash",
|
|
116
|
+
"collect_run_ids",
|
|
117
|
+
"content_key",
|
|
118
|
+
"deck_hash",
|
|
119
|
+
"content_key_for",
|
|
120
|
+
"entity_doc_path",
|
|
121
|
+
"format_address",
|
|
122
|
+
"get_help",
|
|
123
|
+
"interrupt_counts_from_events",
|
|
124
|
+
"is_tier2",
|
|
125
|
+
"merge_ack_completion",
|
|
126
|
+
"narrow_job_for_block",
|
|
127
|
+
"normalize_arguments",
|
|
128
|
+
"not_run_verdict",
|
|
129
|
+
"parse_address",
|
|
130
|
+
"parse_exception",
|
|
131
|
+
"parse_op_call",
|
|
132
|
+
"parse_op_tokens",
|
|
133
|
+
"prelude_digest_for",
|
|
134
|
+
"report_completions",
|
|
135
|
+
"resolve_script_path",
|
|
136
|
+
"parse_target",
|
|
137
|
+
"pending_from_events",
|
|
138
|
+
"request_for_run",
|
|
139
|
+
"resolve_block",
|
|
140
|
+
"scope_provenance",
|
|
141
|
+
"snapshot_closure",
|
|
142
|
+
"stamp_tier2_call",
|
|
143
|
+
"stamp_verdict",
|
|
144
|
+
"stored_completion_from_events",
|
|
145
|
+
"survivors_from_events",
|
|
146
|
+
"tier_for",
|
|
147
|
+
"tool_call",
|
|
148
|
+
"user_message_for",
|
|
149
|
+
"worker_profile",
|
|
150
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Admission pool: backlog, backpressure, and submit-immediate."""
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""Worker pool (API-GENERALIZE): in-order drain, backpressure, boot replay.
|
|
2
|
+
|
|
3
|
+
The durable request log (ProjectService) owns admission truth; the pool
|
|
4
|
+
owns execution order around it. `submit_request` delegates admission to
|
|
5
|
+
the ledger (same ack bytes, same `request_id` dedupe), then holds the
|
|
6
|
+
admitted run in a FIFO backlog until `drain`/`drain_ids` runs it through
|
|
7
|
+
the caller-supplied runner. The backlog is bounded: a full pool refuses
|
|
8
|
+
new admissions with a typed `pool_full` error BEFORE touching the
|
|
9
|
+
ledger, so a refusal never advances the revision and nothing is ever
|
|
10
|
+
silently dropped. Replays (same `request_id`, same text) return the
|
|
11
|
+
stored ack without re-enqueueing and are never refused, even when full.
|
|
12
|
+
|
|
13
|
+
Boot replay rebuilds the backlog from ledger events alone: `replay`
|
|
14
|
+
takes `job_queued` run ids in seq order, minus every run id with a
|
|
15
|
+
`run_completed`/`run_failed` event, resolves each survivor through the
|
|
16
|
+
caller, and drains them — so a kill mid-queue still completes every
|
|
17
|
+
request exactly once after reboot.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import threading
|
|
21
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
22
|
+
|
|
23
|
+
from ruamel.yaml import YAML
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from ..errors import ExecutionError
|
|
27
|
+
except ImportError: # standalone verification before install
|
|
28
|
+
from rocket_engine.execution.errors import ExecutionError
|
|
29
|
+
|
|
30
|
+
POOL_VERSION = 1
|
|
31
|
+
DEFAULT_MAX_DEPTH = 64
|
|
32
|
+
DEFAULT_MAX_WORKERS = 4
|
|
33
|
+
|
|
34
|
+
_yaml = YAML(typ="safe")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def pending_from_events(events):
|
|
38
|
+
"""Order admitted-but-uncompleted requests oldest first.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
events: Ledger events with seq, event, request_id, and
|
|
42
|
+
data.run_id entries.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
(request_id, run_id) pairs for every `job_queued` event
|
|
46
|
+
without a later `run_completed`/`run_failed` event for
|
|
47
|
+
the same run id, in seq order.
|
|
48
|
+
"""
|
|
49
|
+
queued = {}
|
|
50
|
+
order = []
|
|
51
|
+
completed = set()
|
|
52
|
+
for event in sorted(events, key=lambda e: e.get("seq", 0)):
|
|
53
|
+
data = event.get("data") or {}
|
|
54
|
+
run_id = data.get("run_id")
|
|
55
|
+
if run_id is None:
|
|
56
|
+
continue
|
|
57
|
+
if event.get("event") == "job_queued":
|
|
58
|
+
if run_id not in queued:
|
|
59
|
+
order.append(run_id)
|
|
60
|
+
queued[run_id] = event.get("request_id")
|
|
61
|
+
elif event.get("event") in ("run_completed", "run_failed"):
|
|
62
|
+
completed.add(run_id)
|
|
63
|
+
return [
|
|
64
|
+
(queued[run_id], run_id) for run_id in order if run_id not in completed
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def stored_completion_from_events(events, run_id):
|
|
69
|
+
"""Read a run's stored completion from ledger events, if any.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
events: Ledger events with event and data entries.
|
|
73
|
+
run_id: Run to look up.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Completion mapping (run_id, completed state, verdict,
|
|
77
|
+
operation) from the run's `run_completed`/`run_failed`
|
|
78
|
+
event, or None when the run never completed.
|
|
79
|
+
"""
|
|
80
|
+
for event in events:
|
|
81
|
+
if event.get("event") not in ("run_completed", "run_failed"):
|
|
82
|
+
continue
|
|
83
|
+
data = event.get("data") or {}
|
|
84
|
+
if data.get("run_id") != run_id:
|
|
85
|
+
continue
|
|
86
|
+
return {
|
|
87
|
+
"run_id": run_id,
|
|
88
|
+
"state": "completed",
|
|
89
|
+
"verdict": data.get("verdict"),
|
|
90
|
+
"operation": data.get("operation"),
|
|
91
|
+
}
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def request_for_run(events, run_id):
|
|
96
|
+
"""Find the admitting request id for a run, if any.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
events: Ledger events with event, request_id, and
|
|
100
|
+
data.run_id entries.
|
|
101
|
+
run_id: Run to trace back to its admission.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Request id from the run's `job_queued` event, or None
|
|
105
|
+
when no admission event names the run.
|
|
106
|
+
"""
|
|
107
|
+
for event in events:
|
|
108
|
+
if event.get("event") != "job_queued":
|
|
109
|
+
continue
|
|
110
|
+
data = event.get("data") or {}
|
|
111
|
+
if data.get("run_id") == run_id:
|
|
112
|
+
return event.get("request_id")
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ApigPool:
|
|
117
|
+
"""Bounded FIFO execution pool over a durable request ledger.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
project: Project port with `submit(command_text,
|
|
121
|
+
request_id, expected_revision)`, `events_since(cursor)`,
|
|
122
|
+
and `command_for(request_id)` (ProjectService serves).
|
|
123
|
+
runner: Callable `(operation, arguments, snapshot_id,
|
|
124
|
+
request_id, run_id)` returning a completion mapping.
|
|
125
|
+
Runner exceptions are recorded as failed completions,
|
|
126
|
+
never raised, and the drain continues.
|
|
127
|
+
max_depth: Maximum undrained backlog; admissions past it
|
|
128
|
+
are refused with `pool_full`, never queued, never
|
|
129
|
+
dropped silently.
|
|
130
|
+
max_workers: Drain fan-out for independent items
|
|
131
|
+
(BATCH-PARALLEL). Reads and sims run concurrently;
|
|
132
|
+
same-entity mutations serialize upstream through
|
|
133
|
+
ENTITY-CAS. 1 keeps the legacy sequential drain.
|
|
134
|
+
|
|
135
|
+
Caution: The bound applies to new admissions only — replay
|
|
136
|
+
re-enqueues crash survivors past the bound rather than
|
|
137
|
+
abandoning admitted work.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, project, runner, max_depth=DEFAULT_MAX_DEPTH,
|
|
141
|
+
max_workers=1):
|
|
142
|
+
self._project = project
|
|
143
|
+
self._runner = runner
|
|
144
|
+
self._max_depth = max_depth
|
|
145
|
+
self._max_workers = max(1, int(max_workers))
|
|
146
|
+
self._lock = threading.Lock()
|
|
147
|
+
self._backlog = []
|
|
148
|
+
self._queued = {}
|
|
149
|
+
self._acks = {}
|
|
150
|
+
self._completions = {}
|
|
151
|
+
self._completed_runs = None
|
|
152
|
+
self._discarded = set()
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def max_depth(self):
|
|
156
|
+
"""Backlog bound refused past, not queued past.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Maximum undrained backlog.
|
|
160
|
+
"""
|
|
161
|
+
return self._max_depth
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def depth(self):
|
|
165
|
+
"""Current undrained backlog size.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Number of admitted runs awaiting drain.
|
|
169
|
+
"""
|
|
170
|
+
with self._lock:
|
|
171
|
+
return len(self._backlog)
|
|
172
|
+
|
|
173
|
+
def _seed_completed(self):
|
|
174
|
+
if self._completed_runs is None:
|
|
175
|
+
self._completed_runs = {
|
|
176
|
+
run_id
|
|
177
|
+
for _request_id, run_id in ()
|
|
178
|
+
}
|
|
179
|
+
for event in self._project.events_since(0):
|
|
180
|
+
if event.get("event") in ("run_completed", "run_failed"):
|
|
181
|
+
data = event.get("data") or {}
|
|
182
|
+
if data.get("run_id") is not None:
|
|
183
|
+
self._completed_runs.add(data["run_id"])
|
|
184
|
+
|
|
185
|
+
def submit_request(self, command_text, request_id, expected_revision=None):
|
|
186
|
+
"""Admit one request through the ledger, then enqueue it.
|
|
187
|
+
|
|
188
|
+
Replays return the stored ack without re-enqueueing and are
|
|
189
|
+
never refused. A full pool raises before admission, so the
|
|
190
|
+
ledger never advances for a refused request.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
command_text: YAML command mapping with an operation.
|
|
194
|
+
request_id: Client envelope id; resubmitting the
|
|
195
|
+
identical text returns the identical ack.
|
|
196
|
+
expected_revision: Revision the writer read.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Admission ack with run_id, snapshot_id, queued state,
|
|
200
|
+
current revision, and event cursor.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
ExecutionError: pool_full when the undrained backlog
|
|
204
|
+
is at its bound — names the request, the depth,
|
|
205
|
+
and the bound.
|
|
206
|
+
ProjectError: invalid_yaml, revision_conflict, or
|
|
207
|
+
request_conflict from the ledger, untouched.
|
|
208
|
+
"""
|
|
209
|
+
with self._lock:
|
|
210
|
+
if request_id in self._acks:
|
|
211
|
+
return dict(self._acks[request_id])
|
|
212
|
+
if len(self._backlog) >= self._max_depth:
|
|
213
|
+
raise ExecutionError(
|
|
214
|
+
"pool_full",
|
|
215
|
+
f"Pool saturated: {request_id} refused at "
|
|
216
|
+
f"depth {len(self._backlog)}/{self._max_depth}; "
|
|
217
|
+
"drain before submitting more.",
|
|
218
|
+
fields=[{"depth": len(self._backlog),
|
|
219
|
+
"max_depth": self._max_depth}],
|
|
220
|
+
)
|
|
221
|
+
ack = self._project.submit(
|
|
222
|
+
command_text, request_id, expected_revision=expected_revision
|
|
223
|
+
)
|
|
224
|
+
self._seed_completed()
|
|
225
|
+
command = _yaml.load(command_text)
|
|
226
|
+
with self._lock:
|
|
227
|
+
self._acks[request_id] = dict(ack)
|
|
228
|
+
if ack["run_id"] in self._completed_runs:
|
|
229
|
+
return dict(ack)
|
|
230
|
+
self._enqueue_locked(
|
|
231
|
+
request_id, ack["run_id"], command, ack["snapshot_id"]
|
|
232
|
+
)
|
|
233
|
+
return dict(ack)
|
|
234
|
+
|
|
235
|
+
def adopt(self, request_id, run_id, command, snapshot_id):
|
|
236
|
+
"""Enqueue an already-admitted request without touching the ledger.
|
|
237
|
+
|
|
238
|
+
Batch pumping and boot replay admit through this: the
|
|
239
|
+
request was admitted before (its ack lives in the ledger),
|
|
240
|
+
so only the backlog bound and dedupe apply here.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
request_id: Admitting envelope id.
|
|
244
|
+
run_id: Admitted run id.
|
|
245
|
+
command: Parsed command mapping with operation and
|
|
246
|
+
arguments.
|
|
247
|
+
snapshot_id: Evidence snapshot the run pins.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True when the run was enqueued, False when it was
|
|
251
|
+
already queued or drained.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ExecutionError: pool_full when the undrained backlog
|
|
255
|
+
is at its bound.
|
|
256
|
+
"""
|
|
257
|
+
with self._lock:
|
|
258
|
+
if request_id in self._queued or request_id in self._completions:
|
|
259
|
+
return False
|
|
260
|
+
self._seed_completed()
|
|
261
|
+
if run_id in self._completed_runs:
|
|
262
|
+
return False
|
|
263
|
+
if len(self._backlog) >= self._max_depth:
|
|
264
|
+
raise ExecutionError(
|
|
265
|
+
"pool_full",
|
|
266
|
+
f"Pool saturated: {request_id} refused at "
|
|
267
|
+
f"depth {len(self._backlog)}/{self._max_depth}; "
|
|
268
|
+
"drain before submitting more.",
|
|
269
|
+
fields=[{"depth": len(self._backlog),
|
|
270
|
+
"max_depth": self._max_depth}],
|
|
271
|
+
)
|
|
272
|
+
self._enqueue_locked(request_id, run_id, command, snapshot_id)
|
|
273
|
+
return True
|
|
274
|
+
|
|
275
|
+
def _enqueue(self, request_id, run_id, command, snapshot_id):
|
|
276
|
+
with self._lock:
|
|
277
|
+
self._enqueue_locked(request_id, run_id, command, snapshot_id)
|
|
278
|
+
|
|
279
|
+
def _enqueue_locked(self, request_id, run_id, command, snapshot_id):
|
|
280
|
+
entry = {
|
|
281
|
+
"request_id": request_id,
|
|
282
|
+
"run_id": run_id,
|
|
283
|
+
"operation": command.get("operation"),
|
|
284
|
+
"arguments": dict(command.get("arguments") or {}),
|
|
285
|
+
"snapshot_id": snapshot_id,
|
|
286
|
+
}
|
|
287
|
+
self._backlog.append(entry)
|
|
288
|
+
self._queued[request_id] = entry
|
|
289
|
+
|
|
290
|
+
def pending(self):
|
|
291
|
+
"""List undrained run ids, oldest first.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Run ids in drain order.
|
|
295
|
+
"""
|
|
296
|
+
with self._lock:
|
|
297
|
+
return [entry["run_id"] for entry in self._backlog]
|
|
298
|
+
|
|
299
|
+
def drain(self, limit=None):
|
|
300
|
+
"""Run undrained entries oldest first.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
limit: Maximum completions; the whole backlog when
|
|
304
|
+
omitted.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Completion mappings in drain order. A raising runner
|
|
308
|
+
becomes a failed completion carrying the error,
|
|
309
|
+
and the drain continues past it.
|
|
310
|
+
"""
|
|
311
|
+
with self._lock:
|
|
312
|
+
run_ids = [entry["run_id"] for entry in self._backlog]
|
|
313
|
+
if limit is not None:
|
|
314
|
+
run_ids = run_ids[: max(0, limit)]
|
|
315
|
+
return self.drain_ids(run_ids)
|
|
316
|
+
|
|
317
|
+
def discard(self, run_id):
|
|
318
|
+
"""Drop one undrained backlog entry without running it.
|
|
319
|
+
|
|
320
|
+
Cancel-before-drain uses this: the admission stands in the
|
|
321
|
+
ledger, but the run never executes here. A later drain of
|
|
322
|
+
the same id returns a cancelled (NOT_RUN) completion; a
|
|
323
|
+
drain of an id never admitted still raises
|
|
324
|
+
unknown_request.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
run_id: Backlog run id to drop.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
True when an entry was dropped, False when no backlog
|
|
331
|
+
entry names the id.
|
|
332
|
+
"""
|
|
333
|
+
with self._lock:
|
|
334
|
+
for entry in self._backlog:
|
|
335
|
+
if entry["run_id"] == run_id:
|
|
336
|
+
self._backlog.remove(entry)
|
|
337
|
+
del self._queued[entry["request_id"]]
|
|
338
|
+
self._discarded.add(run_id)
|
|
339
|
+
return True
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
def drain_ids(self, run_ids):
|
|
343
|
+
"""Run named backlog entries in the given order.
|
|
344
|
+
|
|
345
|
+
With max_workers > 1 the runners overlap (BATCH-PARALLEL);
|
|
346
|
+
results still return in the given order.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
run_ids: Backlog run ids to drain, in order.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Completion mappings in the given order.
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ExecutionError: unknown_request for a run id with no
|
|
356
|
+
backlog entry.
|
|
357
|
+
"""
|
|
358
|
+
cancelled_ids = []
|
|
359
|
+
with self._lock:
|
|
360
|
+
entries = []
|
|
361
|
+
for run_id in run_ids:
|
|
362
|
+
entry = None
|
|
363
|
+
for candidate in self._backlog:
|
|
364
|
+
if candidate["run_id"] == run_id:
|
|
365
|
+
entry = candidate
|
|
366
|
+
break
|
|
367
|
+
if entry is None:
|
|
368
|
+
if run_id in self._discarded:
|
|
369
|
+
cancelled_ids.append(run_id)
|
|
370
|
+
continue
|
|
371
|
+
raise ExecutionError(
|
|
372
|
+
"unknown_request", f"No queued request: {run_id}."
|
|
373
|
+
)
|
|
374
|
+
self._backlog.remove(entry)
|
|
375
|
+
del self._queued[entry["request_id"]]
|
|
376
|
+
entries.append(entry)
|
|
377
|
+
by_id = {}
|
|
378
|
+
for run_id in cancelled_ids:
|
|
379
|
+
by_id[run_id] = {
|
|
380
|
+
"run_id": run_id,
|
|
381
|
+
"state": "cancelled",
|
|
382
|
+
"reason": "cancelled before dispatch",
|
|
383
|
+
"verdict": {
|
|
384
|
+
"status": "NOT_RUN",
|
|
385
|
+
"reason": "cancelled before dispatch",
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
if entries:
|
|
389
|
+
if self._max_workers <= 1 or len(entries) <= 1:
|
|
390
|
+
drained = [self._drain_one(entry) for entry in entries]
|
|
391
|
+
else:
|
|
392
|
+
with ThreadPoolExecutor(
|
|
393
|
+
max_workers=min(self._max_workers, len(entries)),
|
|
394
|
+
thread_name_prefix="apig-drain",
|
|
395
|
+
) as pool:
|
|
396
|
+
drained = list(pool.map(self._drain_one, entries))
|
|
397
|
+
for entry, completion in zip(entries, drained):
|
|
398
|
+
by_id.setdefault(entry["run_id"], completion)
|
|
399
|
+
return [by_id[run_id] for run_id in run_ids]
|
|
400
|
+
|
|
401
|
+
def _drain_one(self, entry):
|
|
402
|
+
"""Run one backlog entry, recording its completion."""
|
|
403
|
+
try:
|
|
404
|
+
completion = self._runner(
|
|
405
|
+
entry["operation"],
|
|
406
|
+
dict(entry["arguments"]),
|
|
407
|
+
entry["snapshot_id"],
|
|
408
|
+
entry["request_id"],
|
|
409
|
+
entry["run_id"],
|
|
410
|
+
)
|
|
411
|
+
except Exception as exc: # noqa: BLE001 — recorded, never raised
|
|
412
|
+
code = getattr(exc, "code", type(exc).__name__)
|
|
413
|
+
completion = {
|
|
414
|
+
"run_id": entry["run_id"],
|
|
415
|
+
"state": "failed",
|
|
416
|
+
"error": f"{code}: {exc}",
|
|
417
|
+
"operation": entry["operation"],
|
|
418
|
+
}
|
|
419
|
+
with self._lock:
|
|
420
|
+
self._completions[entry["request_id"]] = completion
|
|
421
|
+
self._seed_completed()
|
|
422
|
+
self._completed_runs.add(entry["run_id"])
|
|
423
|
+
return completion
|
|
424
|
+
|
|
425
|
+
def replay(self, events, resolve):
|
|
426
|
+
"""Re-enqueue crash survivors from ledger events, then drain them.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
events: Ledger events; survivors are `job_queued` run
|
|
430
|
+
ids without a `run_completed`/`run_failed` event.
|
|
431
|
+
resolve: Callable `request_id -> (command, ack)` for
|
|
432
|
+
each survivor (ProjectService.command_for serves).
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Report with replayed run ids (seq order), their
|
|
436
|
+
completions, and skipped entries (each with
|
|
437
|
+
request_id, run_id, and reason) for survivors the
|
|
438
|
+
resolver cannot serve. Skips are reported, never
|
|
439
|
+
silent, and the drain continues past them.
|
|
440
|
+
"""
|
|
441
|
+
replayed = []
|
|
442
|
+
skipped = []
|
|
443
|
+
for request_id, run_id in pending_from_events(events):
|
|
444
|
+
if request_id in self._queued or request_id in self._completions:
|
|
445
|
+
continue
|
|
446
|
+
try:
|
|
447
|
+
command, ack = resolve(request_id)
|
|
448
|
+
except Exception as exc: # noqa: BLE001 — reported per request
|
|
449
|
+
skipped.append({
|
|
450
|
+
"request_id": request_id,
|
|
451
|
+
"run_id": run_id,
|
|
452
|
+
"reason": f"{getattr(exc, 'code', type(exc).__name__)}: {exc}",
|
|
453
|
+
})
|
|
454
|
+
continue
|
|
455
|
+
self._acks[request_id] = dict(ack)
|
|
456
|
+
self._enqueue(request_id, run_id, command, ack["snapshot_id"])
|
|
457
|
+
replayed.append(run_id)
|
|
458
|
+
completions = self.drain_ids(replayed)
|
|
459
|
+
return {
|
|
460
|
+
"replayed": replayed,
|
|
461
|
+
"completions": completions,
|
|
462
|
+
"skipped": skipped,
|
|
463
|
+
}
|