From 14dc08926e56956b2f69afd385fca67d1e247b5b Mon Sep 17 00:00:00 2001 From: fffonion Date: Wed, 12 Aug 2026 13:13:41 +0800 Subject: [PATCH] plan(vm): define invocation item stream contract --- plans/2026-08-09_architecture-plan-index.md | 4 +- ...-08-09_run-outcome-event-error-contract.md | 209 ++++++++++-------- plans/2026-08-09_vm-runtime-decomposition.md | 18 +- 3 files changed, 130 insertions(+), 101 deletions(-) diff --git a/plans/2026-08-09_architecture-plan-index.md b/plans/2026-08-09_architecture-plan-index.md index 8c2f798d..03a289f6 100644 --- a/plans/2026-08-09_architecture-plan-index.md +++ b/plans/2026-08-09_architecture-plan-index.md @@ -55,9 +55,9 @@ Exit gate: module semantics have explicit identities; VM has explicit ownership ### Wave 2: Unified execution lifecycle 1. Unified host resource/operation/cancellation lifecycle after VM ownership exists. -2. RunOutcome/event/error implementation on RunContext, integrating lifecycle cancellation as it becomes available. +2. Invocation item stream and typed-error implementation on RunContext, integrating lifecycle cancellation as it becomes available. -Exit gate: production host subsystems use one lifecycle, and every run has one structured terminal outcome with live bounded events. +Exit gate: production host subsystems use one lifecycle, and every invocation yields bounded `Event` items followed by one `Complete` item or typed error, then ends. ### Wave 3: Specialized consumers diff --git a/plans/2026-08-09_run-outcome-event-error-contract.md b/plans/2026-08-09_run-outcome-event-error-contract.md index c0222507..6e95cf32 100644 --- a/plans/2026-08-09_run-outcome-event-error-contract.md +++ b/plans/2026-08-09_run-outcome-event-error-contract.md @@ -1,127 +1,157 @@ -# Run Outcome, Event Stream, and Runtime Error Contract Plan +# Invocation Item Stream and Runtime Error Contract Plan -**Goal:** Define one structured execution result that keeps return values, events, usage, cancellation, and errors separate and machine-readable. +**Goal:** Expose one small, Rust-like pull stream for each exported RSS invocation so structured arguments, emitted items, the final return value, cancellation, and errors have unambiguous semantics. -**Architecture:** A run produces a terminal `RunOutcome`; events flow during execution through a bounded sink/channel and never replace the function return value. Runtime and host failures retain structured codes and context through the VM embedding boundary. +**Architecture:** The host initializes a VM, resolves an exported callable, and starts an invocation with ordinary `Value` arguments. The invocation behaves like `Stream>`: `Event(Value)` items may arrive during execution, one `Complete(Value)` item carries the function return value, and the stream is fused after that terminal item or one error. `Vm::run()` remains the low-level execution pump; core does not add generator syntax, a second completion future, an embedding callback sink, or event persistence policy. -**Tech Stack:** Rust 2024, VM embedding API, runtime context/events, host errors, agent runner integration tests. +**Tech Stack:** Rust 2024, existing VM callable APIs, `VmStatus`, generic runtime errors, existing async host bridge. --- -## Independence and dependency +## 1. Contract and non-goals -- Contract design can start independently. -- Implementation depends on RunContext ownership from the VM decomposition plan. -- Operation cancellation details depend on the unified host-lifecycle plan. -- The agent run-lifecycle plan consumes this API. +### Public semantics -## Scope boundary +```text +InvocationItem + Event(Value) + Complete(Value) + +Invocation stream item + Result + +poll_next + Pending(wait reason) + Ready(Some(Ok(Event(value)))) + Ready(Some(Ok(Complete(return_value)))) + Ready(Some(Err(runtime_error))) + Ready(None) // only after Complete or Err +``` -### In scope +The concrete API may use a small VM-specific poll enum instead of implementing `futures::Stream`; core must not create an executor or require a Tokio runtime. Its observable behavior must match a fused Rust stream. -- `RunOutcome`, terminal reason, usage, return value, and structured error. -- Bounded event emission during execution. -- Event receipt/sequence semantics at the VM boundary. -- Structured runtime/host error propagation. -- Removal of stack-top/event-last inference in embedding code. +### Required rules -### Out of scope +- Input is passed as ordinary arguments to an exported callable such as `run(input)`. +- `stream::emit(value)` produces one `Event(value)` item and never changes the callable return value. +- A normal callable return produces exactly one `Complete(value)` item. +- Cancellation or failure produces exactly one typed error item and no `Complete` item. +- The next poll after `Complete` or `Err` returns end-of-stream. +- Polling drives execution. When the consumer stops polling, the VM does not continue producing items. +- At most one event item is buffered between polls; this provides natural backpressure without an event queue. +- Core validates only the configured per-item value bound. Run IDs, event names, sequence numbers, durable cursors, retention, replay, and platform delivery belong to the embedding. -- Agent event names, provider protocols, SSE framing, or Telegram rendering. -- Durable event persistence. -- Source-language concurrency syntax. -- Compatibility wrappers for ambiguous prior return behavior. +### Explicit non-goals -## Target contracts +- No source-language `yield`, generator object, `next(value)`, or resume-value semantics. +- No `RunOutcome`, `RunTermination`, `RunUsage`, terminal future, or event receipt type. +- No ambient-input builtin or JSON-specific input/output wrapper. +- No stack-top or event-last result inference. +- No agent/provider/platform event schema in core. -```text -RunOutcome - return_value: optional Value - termination: completed | cancelled | failed | budget_exhausted - error: optional RuntimeError - usage: RunUsage - last_event_sequence - -RuntimeEvent - sequence - value - payload_bytes - -RuntimeError - code - message - subsystem - operation/resource context - retryability where meaningful - source error where meaningful -``` +## 2. Dependency boundary + +- Reuse exported callable identity and `Vm::resolve_exported_callable`. +- Reuse callable execution state from `start_callable`, `run`, and `take_callable_result`. +- Reuse `HostAsyncBridge`; an outstanding host operation maps to `Pending` and is resumed by the embedding-owned driver. +- Reuse unified cancellation tokens, but expose typed invocation cancellation through the public invocation API. +- Preserve the capability-profile and host-binding contracts unchanged. -## Implementation route +## 3. Implementation route -### Milestone 1: Add contract tests +### Milestone 1: Freeze stream behavior with failing tests + +**Files:** +- Create: `tests/invocation_stream_tests.rs` +- Modify: `tests/runtime_context_tests.rs` +- Modify: `tests/runtime_host_tests.rs` Add tests proving: -- a script may emit events and return a different value; -- zero events does not alter the return value; -- event order is monotonic; -- sink rejection/backpressure has a documented terminal behavior; -- cancellation reason survives the public VM API; -- host/runtime codes survive without string equality checks; -- usage is finalized for success, error, cancellation, and budget exhaustion. +1. `run(input)` receives the exact structured `Value` argument. +2. A script that emits `a`, emits `b`, and returns `c` produces `Event(a)`, `Event(b)`, `Complete(c)`, then end-of-stream. +3. A script with no events produces `Complete(value)`, then end-of-stream. +4. An event value never replaces or mutates the return value. +5. One poll exposes at most one event and execution does not advance while polling is paused. +6. A waiting host operation returns `Pending`, resumes through the existing async host driver, and preserves item order. +7. Cancellation produces one typed error item with its reason, then end-of-stream. +8. Fuel exhaustion, deadline expiry, and host failure each produce one typed error item, then end-of-stream. +9. Starting a second invocation on the same VM while one is active is rejected. +10. No public embedder needs to inspect the operand stack or compare error strings. -### Milestone 2: Define terminal and usage types +### Milestone 2: Add the minimal invocation state machine **Files:** +- Create: `src/vm/invocation.rs` +- Modify: `src/vm/mod.rs` +- Modify: `src/vm/instance.rs` - Modify: `src/lib.rs` -- Create: `src/vm/outcome.rs` -- Modify runtime error modules -1. Define `RunOutcome`, `RunTermination`, and `RunUsage`. -2. Make halt/failure/cancellation paths produce exactly one terminal outcome. -3. Stop requiring embedders to inspect stack top, yield reason, and side channels to infer completion. +1. Define `InvocationItem::{Event(Value), Complete(Value)}`. +2. Define one public invocation handle/state with `poll_next` and typed cancellation. +3. Start only from an initialized exported callable plus ordinary arguments. +4. Reuse `start_callable`, `run`, and `take_callable_result`; do not duplicate interpreter or async-host loops. +5. Enforce one active invocation per VM and fused termination. +6. Keep `Vm::run() -> VmResult` unchanged as the low-level pump. -### Milestone 3: Make events live and bounded +### Milestone 3: Turn runtime emit into one stream item **Files:** - Modify: `src/builtins/runtime/context.rs` - Modify: `src/builtins/runtime/event.rs` - Modify: `src/builtins/runtime/context_host.rs` -- Modify: RunContext +- Modify: `src/vm/run_context.rs` + +1. Remove run-scoped ambient input storage and its script-visible builtins. +2. Replace the embedding-owned `EventSink` and cumulative event counters with one pending event slot owned by the active invocation. +3. Add the single script-visible `stream::emit(value)` builtin; it validates the per-item bound, places one pending event, and yields control to the invocation poller. +4. Resume the script after the caller consumes that item; `stream::emit` still evaluates to `()` inside RSS. +5. Remove core sequence assignment, event receipts, cumulative event-byte accounting, and sink rejection wrapping. +6. Do not add a JSON-specific emit builtin; adapters encode or decode JSON outside the VM contract. + +### Milestone 4: Capture the callable return explicitly + +**Files:** +- Modify: `src/vm/mod.rs` +- Modify: `src/vm/instance.rs` +- Test: `tests/invocation_stream_tests.rs` -1. Define a bounded event sink contract. -2. Emit each accepted event during execution. -3. Allocate sequence numbers once at the run boundary. -4. Define overflow policy explicitly: block/yield, return a typed limit error, or drop only where configured with a receipt. Silent loss is prohibited. -5. Keep event values independent from function return storage. +1. On normal callable completion, take the existing host callable result and emit `Complete(value)` once. +2. Never read the operand stack to infer the result. +3. Reject termination that lacks an explicit callable result as an internal frame-state error. +4. After `Complete`, release invocation state and return end-of-stream on every later poll. -### Milestone 4: Preserve structured errors +### Milestone 5: Preserve typed terminal errors and cancellation **Files:** -- Modify: runtime error types -- Modify: `src/vm/host.rs` -- Modify: public VM error surface +- Modify: `src/builtins/runtime/error.rs` +- Modify: `src/builtins/runtime/cancellation.rs` +- Modify: `src/vm/run_context.rs` +- Modify: `src/vm/invocation.rs` +- Modify: public VM error conversion paths -1. Carry `RuntimeErrorCode` through host completion and `RunOutcome`. -2. Include structured cancellation/deadline/resource/operation context. -3. Remove embedding logic that compares error strings such as `"cancelled"`. -4. Define rendering separately from machine-readable fields. +1. Expose typed invocation cancellation without making `RunContext` public. +2. Preserve cancellation reason, deadline, fuel, resource, operation, and host error codes through the stream item. +3. Remove `HostError(String)` flattening from runtime capability paths consumed by the invocation API. +4. Emit one error item, cancel outstanding owned operations, release invocation state, and fuse the stream. +5. Do not add string parsing or dual legacy error contracts. -### Milestone 5: Migrate embedders and remove ambiguous APIs +### Milestone 6: Migrate embedders and remove superseded APIs **Files:** -- Modify examples and tests in `rustscript` -- Coordinate later changes in `rustscript-agent/src/lib.rs` +- Modify: RustScript examples and embedding tests +- Coordinate: `rustscript-agent/src/lib.rs` -1. Consume `RunOutcome.return_value` directly. -2. Subscribe to events through the sink/channel. -3. Remove event-last and stack-last fallback behavior. -4. Remove superseded internal return APIs after migration; no dual long-term contract. +1. Resolve an exported `run` callable and pass structured input as its argument. +2. Consume `Event` and `Complete` items in order. +3. Remove `events.last()`, `stack.last()`, ambient runtime input, and event sink setup. +4. Remove superseded runtime input/event exports after all in-repository consumers migrate. -### Milestone 6: Verification +### Milestone 7: Verification ```bash cargo fmt --all -- --check +cargo test --locked --test invocation_stream_tests cargo test --locked --test runtime_context_tests cargo test --locked --test runtime_host_tests cargo test --locked --workspace --all-features @@ -129,12 +159,13 @@ cargo clippy --locked --workspace --all-targets --all-features -- -D warnings git diff --check ``` -## Target criteria +## 4. Target criteria -- Emitting an event never changes the function return value. -- Events are observable before run completion through a bounded contract. -- Every run produces one structured terminal outcome. -- Cancellation, deadline, resource, and host errors retain machine-readable codes. -- Embedders do not infer results from stack/event ordering. -- String equality is absent from cancellation/error control flow. -- Usage and event sequence metadata are finalized for every terminal path. +- The public invocation surface has one input path: exported callable arguments. +- The invocation yields zero or more `Event` items, then exactly one `Complete` item or one typed error, then ends. +- Events never replace the callable return value. +- Backpressure follows polling; no unbounded or embedding-callback event queue exists in core. +- `Vm::run` remains a low-level pump and no executor is introduced. +- Cancellation and runtime failures remain machine-readable. +- Core carries no event sequence, persistence, replay, or platform policy. +- No generator syntax or compatibility wrapper is introduced. diff --git a/plans/2026-08-09_vm-runtime-decomposition.md b/plans/2026-08-09_vm-runtime-decomposition.md index 2c80b101..0c080ffd 100644 --- a/plans/2026-08-09_vm-runtime-decomposition.md +++ b/plans/2026-08-09_vm-runtime-decomposition.md @@ -2,7 +2,7 @@ **Goal:** Split the current monolithic `Vm` state into explicit engine, program, instance, run-context, and host-runtime ownership layers. -**Architecture:** Immutable compiled artifacts and backend caches live outside per-run execution state. An `Instance` owns interpreter state, a `RunContext` owns one execution's input/budgets/events/cancellation, and `HostRuntime` owns capabilities/resources/operations. The migration preserves observable execution behavior while removing subsystem-specific fields from the central VM object. +**Architecture:** Immutable compiled artifacts and backend caches live outside per-run execution state. An `Instance` owns interpreter state, a `RunContext` owns one invocation's pending stream item, budgets, and cancellation, and `HostRuntime` owns capabilities/resources/operations. Invocation input remains in ordinary callable arguments. The migration preserves observable execution behavior while removing subsystem-specific fields from the central VM object. **Tech Stack:** Rust 2024, `pd-vm` interpreter/JIT/AOT integration, existing compiler and runtime tests. @@ -11,7 +11,7 @@ ## Independence and dependency - Static builtin IDs should land first so decomposition does not move an unstable wire catalog. -- Defines ownership required by the unified host-lifecycle and RunOutcome plans. +- Defines ownership required by the unified host-lifecycle and invocation-item-stream plans. - Independent of agent providers, gateway routes, module semantics, and new host functions. ## Scope boundary @@ -50,10 +50,8 @@ Instance yield/wait state RunContext - input - event channel + one pending invocation item fuel/deadline/cancellation - usage accounting HostRuntime capability profile @@ -75,7 +73,7 @@ The public facade may be renamed or retained, but ownership must follow this mod Prove: - one immutable program can create multiple isolated instances; -- run input/events/budgets never leak between runs; +- pending invocation items and budgets never leak between runs; - backend cache may be shared without sharing stacks/resources; - reset closes run-scoped state and retains only documented reusable state. @@ -104,11 +102,11 @@ Move IP, stack, locals, frames, captures, callbacks, waiting/yield state, and in **Files:** - Create: `src/vm/run_context.rs` -- Move runtime input, event sink, fuel, epoch/deadline, cancellation, and usage state +- Move the pending invocation item, fuel, epoch/deadline, and cancellation state -1. Create a fresh RunContext per execution. +1. Create a fresh RunContext per invocation. 2. Make cancellation and deadline mandatory run-owned data, with explicit unlimited settings where allowed. -3. Remove source injection and embedding-global event ownership from execution paths. +3. Keep invocation input in callable arguments and remove source injection and embedding-global event ownership from execution paths. 4. Make run completion consume/finalize the context. ### Milestone 5: Extract HostRuntime shell @@ -143,7 +141,7 @@ Add behavioral comparison fixtures that run the same program before and after ea - Immutable Program data and backend caches are not owned by per-run state. - Stack/frame/wait state is isolated in Instance. -- Input/events/budget/cancellation are isolated in RunContext. +- Pending invocation item, budget, and cancellation are isolated in RunContext; input stays in callable arguments. - Capabilities/resources/operations are isolated in HostRuntime. - Reset and drop no longer enumerate every runtime subsystem in one central method. - Multiple instances from one program cannot share mutable run or host resources.