Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plans/2026-08-09_architecture-plan-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
209 changes: 120 additions & 89 deletions plans/2026-08-09_run-outcome-event-error-contract.md
Original file line number Diff line number Diff line change
@@ -1,140 +1,171 @@
# 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<Item = Result<InvocationItem, RuntimeError>>`: `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<InvocationItem, RuntimeError>

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<VmStatus>` 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
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.
18 changes: 8 additions & 10 deletions plans/2026-08-09_vm-runtime-decomposition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -50,10 +50,8 @@ Instance
yield/wait state

RunContext
input
event channel
one pending invocation item
fuel/deadline/cancellation
usage accounting

HostRuntime
capability profile
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading