Skip to main content

In-Context State Management

This document describes how the OpenProse VM tracks execution state using structured narration in the conversation history. This is one of two state management approaches (the other being file-based state in filesystem.md).

Overview

In-context state uses text-prefixed markers to persist state within the conversation. The VM "thinks aloud" about execution—what you say becomes what you remember.

Key principle: Your conversation history IS the VM's working memory.


When to Use In-Context State

In-context state is appropriate for:

FactorIn-ContextUse File-Based Instead
Statement count< 30 statements>= 30 statements
Parallel branches< 5 concurrent>= 5 concurrent
Imported programs0-2 imports>= 3 imports
Nested depth<= 2 levels> 2 levels
Expected duration< 5 minutes>= 5 minutes

Announce your state mode at program start:

OpenProse Program Start
State mode: in-context (program is small, fits in context)

The Narration Protocol

Use text-prefixed markers for each state change:

MarkerCategoryUsage
[Program]ProgramStart, end, definition collection
[Position]PositionCurrent statement being executed
[Binding]BindingVariable assignment or update
[Input]InputReceiving inputs from caller
[Output]OutputProducing outputs for caller
[Import]ImportFetching and invoking imported programs
[Success]SuccessSession or block completion
[Warning]ErrorFailures and exceptions
[Parallel]ParallelEntering, branch status, joining
[Loop]LoopIteration, condition evaluation
[Pipeline]PipelineStage progress
[Try]Error handlingTry/catch/finally
[Flow]FlowCondition evaluation results
[Frame+]Call StackPush new frame (block invocation)
[Frame-]Call StackPop frame (block completion)

Narration Patterns by Construct

Session Statements

[Position] Executing: session "Research the topic"
[Task tool call]
[Success] Session complete: "Research found that..."
[Binding] let research = <result>

Parallel Blocks

[Parallel] Entering parallel block (3 branches, strategy: all)
- security: pending
- perf: pending
- style: pending
[Multiple Task calls]
[Parallel] Parallel complete:
- security = "No vulnerabilities found..."
- perf = "Performance is acceptable..."
- style = "Code follows conventions..."
[Binding] security, perf, style bound

Loop Blocks

[Loop] Starting loop until **task complete** (max: 5)

[Loop] Iteration 1 of max 5
[Position] session "Work on task"
[Success] Session complete
[Loop] Evaluating: **task complete**
[Flow] Not satisfied, continuing

[Loop] Iteration 2 of max 5
[Position] session "Work on task"
[Success] Session complete
[Loop] Evaluating: **task complete**
[Flow] Satisfied!

[Loop] Loop exited: condition satisfied at iteration 2

Error Handling

[Try] Entering try block
[Position] session "Risky operation"
[Warning] Session failed: connection timeout
[Binding] err = {message: "connection timeout"}
[Try] Executing catch block
[Position] session "Handle error" with context: err
[Success] Recovery complete
[Try] Executing finally block
[Position] session "Cleanup"
[Success] Cleanup complete

Variable Bindings

[Binding] let research = "AI safety research covers..." (mutable)
[Binding] const config = {model: "opus"} (immutable)
[Binding] research = "Updated research..." (reassignment, was: "AI safety...")

Input/Output Bindings

[Input] Inputs received:
topic = "quantum computing" (from caller)
depth = "deep" (from caller)

[Output] output findings = "Research shows..." (will return to caller)
[Output] output sources = ["arxiv:2401.1234", ...] (will return to caller)

Block Invocation and Call Stack

Track block invocations with frame markers:

[Position] do process(data, 5)
[Frame+] Entering block: process (execution_id: 1, depth: 1)
Arguments: chunk=data, depth=5

[Position] session "Split into parts"
[Task tool call]
[Success] Session complete
[Binding] let parts = <result> (execution_id: 1)

[Position] do process(parts[0], 4)
[Frame+] Entering block: process (execution_id: 2, depth: 2)
Arguments: chunk=parts[0], depth=4
Parent: execution_id 1

[Position] session "Split into parts"
[Task tool call]
[Success] Session complete
[Binding] let parts = <result> (execution_id: 2) # Shadows parent's 'parts'

... (continues recursively)

[Frame-] Exiting block: process (execution_id: 2)

[Position] session "Combine results"
[Task tool call]
[Success] Session complete

[Frame-] Exiting block: process (execution_id: 1)

Key points:

  • Each [Frame+] must have a matching [Frame-]
  • execution_id uniquely identifies each invocation
  • depth shows call stack depth (1 = first level)
  • Bindings include (execution_id: N) to indicate scope
  • Nested frames show Parent: execution_id N for the scope chain

Scoped Binding Narration

When inside a block invocation, always include the execution_id:

[Binding] let result = "computed value" (execution_id: 43)

For variable resolution across scopes:

[Binding] Resolving 'config': found in execution_id 41 (parent scope)

Program Imports

[Import] Importing: @alice/research
Fetching from: https://p.prose.md/@alice/research
Inputs expected: [topic, depth]
Outputs provided: [findings, sources]
Registered as: research

[Import] Invoking: research(topic: "quantum computing")
[Input] Passing inputs:
topic = "quantum computing"

[... imported program execution ...]

[Output] Received outputs:
findings = "Quantum computing uses..."
sources = ["arxiv:2401.1234"]

[Import] Import complete: research
[Binding] result = { findings: "...", sources: [...] }

Context Serialization

In-context state passes values, not references. This is the key difference from file-based and PostgreSQL state. The VM holds binding values directly in conversation history.

When passing context to sessions, format appropriately:

Context SizeStrategy
< 2000 charsPass verbatim
2000-8000 charsSummarize to key points
> 8000 charsExtract essentials only

Format:

Context provided:
---
research: "Key findings about AI safety..."
analysis: "Risk assessment shows..."
---

Limitation: In-context state cannot support RLM-style "environment as variable" patterns where agents query arbitrarily large bindings. For programs with large intermediate values, use file-based or PostgreSQL state instead.


Complete Execution Trace Example

agent researcher:
model: sonnet

let research = session: researcher
prompt: "Research AI safety"

parallel:
a = session "Analyze risk A"
b = session "Analyze risk B"

loop until **analysis complete** (max: 3):
session "Synthesize"
context: { a, b, research }

Narration:

[Program] Program Start
Collecting definitions...
- Agent: researcher (model: sonnet)

[Position] Statement 1: let research = session: researcher
Spawning with prompt: "Research AI safety"
Model: sonnet
[Task tool call]
[Success] Session complete: "AI safety research covers alignment..."
[Binding] let research = <result>

[Position] Statement 2: parallel block
[Parallel] Entering parallel (2 branches, strategy: all)
[Task: "Analyze risk A"] [Task: "Analyze risk B"]
[Parallel] Parallel complete:
- a = "Risk A: potential misalignment..."
- b = "Risk B: robustness concerns..."
[Binding] a, b bound

[Position] Statement 3: loop until **analysis complete** (max: 3)
[Loop] Starting loop

[Loop] Iteration 1 of max 3
[Position] session "Synthesize" with context: {a, b, research}
[Task with serialized context]
[Success] Result: "Initial synthesis shows..."
[Loop] Evaluating: **analysis complete**
[Flow] Not satisfied (synthesis is preliminary)

[Loop] Iteration 2 of max 3
[Position] session "Synthesize" with context: {a, b, research}
[Task with serialized context]
[Success] Result: "Comprehensive analysis complete..."
[Loop] Evaluating: **analysis complete**
[Flow] Satisfied!

[Loop] Loop exited: condition satisfied at iteration 2

[Program] Program Complete

State Categories

The VM must track these state categories in narration:

CategoryWhat to TrackExample
Import RegistryImported programs and aliasesresearch: @alice/research
Agent RegistryAll agent definitionsresearcher: {model: sonnet, prompt: "..."}
Block RegistryAll block definitions (hoisted)review: {params: [topic], body: [...]}
Input BindingsInputs received from callertopic = "quantum computing"
Output BindingsOutputs to return to callerfindings = "Research shows..."
Variable BindingsName -> value mapping (with execution_id)result = "..." (execution_id: 3)
Variable MutabilityWhich are let vs const vs outputresearch: let, findings: output
Execution PositionCurrent statement indexStatement 3 of 7
Loop StateCounter, max, conditionIteration 2 of max 5
Parallel StateBranches, results, strategy{a: complete, b: pending}
Error StateException, retry countRetry 2 of 3, error: "timeout"
Call StackStack of execution framesSee below

Call Stack State

For block invocations, track the full call stack:

[CallStack] Current stack (depth: 3):
execution_id: 5 | block: process | depth: 3 | status: executing
execution_id: 3 | block: process | depth: 2 | status: waiting
execution_id: 1 | block: process | depth: 1 | status: waiting

Each frame tracks:

  • execution_id: Unique ID for this invocation
  • block: Name of the block
  • depth: Position in call stack
  • status: executing, waiting, or completed

Independence from File-Based State

In-context state and file-based state (filesystem.md) are independent approaches. You choose one or the other based on program complexity.

  • In-context: State lives in conversation history
  • File-based: State lives in .prose/runs/{id}/

They are not designed to be complementary—pick the appropriate mode at program start.


Summary

In-context state management:

  1. Uses text-prefixed markers to track state changes
  2. Persists state in conversation history
  3. Is appropriate for smaller, simpler programs
  4. Requires consistent narration throughout execution
  5. Makes state visible in the conversation itself

The narration protocol ensures that the VM can recover its execution state by reading its own prior messages. What you say becomes what you remember.