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
6 changes: 6 additions & 0 deletions skills/open-prose/guidance/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,9 @@ responsibility, and its persisted state is its world-model.
check and a judged quality pass.
- Over-tiering a one-shot or single-responsibility job — manufacturing
classifier/facet machinery where one render would do.
- Collapsing block invocations in parallel contexts into a single natural-language
subagent task instead of preserving discrete statement-by-statement session
execution and scoped checkpoints.
- Batching intermediate ledger writes or state updates at the end of a parallel
block branch instead of committing them contemporaneously after each statement
completes.
16 changes: 16 additions & 0 deletions skills/open-prose/prose.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,8 @@ spawn_session({ prompt: "Node: critic ..." })
// Wait for all to complete, then continue
```

When executing block invocations in parallel contexts (`parallel:` or `parallel for`), the VM preserves discrete statement-by-statement execution and nested session boundaries across all branches; see [Block Invocation in Parallel Contexts](#block-invocation-in-parallel-contexts).

#### 4e. Apply Manifest Constraints When Present

Current v0 compiled intent does not define a separate pattern-constraint schema.
Expand Down Expand Up @@ -697,6 +699,20 @@ spawn_session({ description: "OpenProse render: fact-checker", prompt: "..." })
// Wait for all to complete
```

### Block Invocation in Parallel Contexts

When a named block (`block name(args): ...`) is invoked inside a parallel construct—such as `parallel: do name(...)` or `parallel for item in items: do name(item)`—the VM and executing agents must preserve discrete statement and session boundaries for each concurrent branch:

1. **Statement-by-statement execution**: Each parallel branch executes the block body statement-by-statement in sequence. The VM or coordinating agent MUST NOT collapse the block statements into a single natural-language summary prompt (e.g., instructing a worker to "process this item through all steps" in one turn).
2. **Discrete nested sessions**: Every `session:` and `call:` statement inside the block body MUST be dispatched as an independent, discrete session with its own prompt, inputs, and completion barrier. For example, if a block logs to a ledger before and after a work step:
- The pre-step `session:` executes and completes (e.g., recording phase start).
- The work `session:` executes and completes.
- The post-step `session:` executes and completes (e.g., recording phase completion).
Collapsing these into a single monolithic prompt breaks nested session semantics and eliminates intermediate checkpointing.
3. **Sub-VM delegation protocol**: When the coordinator delegates a parallel iteration or branch to a worker subagent, it must supply the verbatim block statements and branch bindings, explicitly instructing the worker to act as an OpenProse sub-VM. The worker must evaluate statements sequentially and spawn or invoke discrete sessions for each nested `session:` or `call:`.
4. **Contemporaneous state and ledger updates**: All state writes, ledger appends, event emissions, and receipt updates must occur contemporaneously when each statement finishes, not batched at the end of the parallel branch. This guarantees that event logs reflect accurate temporal ordering and distinct timestamps for each phase.
5. **Scoped execution frames**: Each parallel iteration runs within an isolated execution scope (see [Scoped Execution Frames](state/filesystem.md#scoped-execution-frames)). Local variable bindings (`let`) and working scratch are private to each branch instance (`workspace/{node}/__scope/{execution_id}/`), preventing cross-branch collision.

### What the Subagent Receives

The subagent receives:
Expand Down
29 changes: 29 additions & 0 deletions skills/open-prose/prosescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,29 @@ Validation:
| Branch result read before join | Error |
| Duplicate binding produced by two branches | Error |

### Parallel Block Invocations

When invoking reusable blocks inside `parallel:` or `parallel for`:

```prose
block process-item(item_id):
session: ledger
prompt: "Append 'phase-1-started' for {item_id}"
let step1_result = session "Do step 1"
prompt: "Process step 1 for {item_id}"
session: ledger
prompt: "Append 'phase-1-completed' for {item_id}"

parallel for item in items:
do process-item(item)
```

Invariants:
- **No statement collapsing**: Parallel block invocations execute each statement in the block body sequentially per branch. The VM never condenses the block into a single natural-language instruction or monolithic prompt.
- **Discrete nested sessions**: Each `session:` or `call:` inside the block is invoked as a discrete session with its own prompt, execution turn, and completion boundary.
- **Contemporaneous updates**: Intermediate side-effects, ledger entries, and state transitions are committed immediately upon statement completion, ensuring discrete timestamps and observable intermediate state across concurrent iterations.
- **Isolated scope**: Each iteration or branch evaluates inside an isolated execution frame (`execution_id`), preventing variable binding or scratch collision.

## Loops

Fixed repetition:
Expand Down Expand Up @@ -714,6 +737,12 @@ Block definitions are collected before execution, so a block may be invoked
before its definition. Parameters are immutable within the block call. Each
block invocation has its own scope.

When a block is invoked inside a parallel construct (`parallel:` or `parallel for`),
the block's statement sequence is preserved for every branch. Nested `session:`
or `call:` statements are dispatched as discrete sessions, and intermediate state
or ledger updates occur contemporaneously; parallel execution never collapses
a block into a single monolithic prompt (see [Parallel Block Invocations](#parallel-block-invocations)).

Validation:

| Check | Result |
Expand Down
18 changes: 18 additions & 0 deletions skills/open-prose/state/filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ To resume an interrupted run:
| `sources/*.prose.md` | the compile phase | Before execution |
| `world-model/caller/*.md` | VM | At entry / gateway boot |
| `workspace/{node}/*` | the render | During the render |
| `workspace/{node}/__scope/{execution_id}/*` | the render / sub-VM | Isolated frame for parallel block invocations |
| `workspace/{node}/__delegate/{delegate}/{id}.md` | the render | Before delegation yield |
| `workspace/{node}/__delegate/{delegate}/{id}-response.md` | VM | After delegate completes |
| `world-model/{node}/*` + `.version` | VM (`commit_world_model`) | On a `rendered` receipt with a moved fingerprint |
Expand Down Expand Up @@ -581,6 +582,23 @@ If the render wrote `__error.md` instead:

---

## Scoped Execution Frames

When a render executes blocks or iterations within parallel constructs (`parallel:` or `parallel for`), concurrent branches require isolation for branch-local scratch files, iteration bindings, and sub-VM workspaces:

```
workspace/{node}/__scope/{execution_id}/
├── scratch/ # Branch-local scratch files
└── outputs/ # Intermediate values bound to local bindings
```

### Invariants
1. **Per-Branch Isolation**: `{execution_id}` uniquely identifies the parallel iteration or branch (derived from iteration index or execution UUID). Local variable bindings and temporary working files are contained within this scoped path, preventing concurrent branches from clobbering one another.
2. **Discrete Session and State Progression**: Nested `session:` or `call:` statements executed within the scoped frame commit their outputs and ledger/state entries contemporaneously as each statement finishes, maintaining distinct timestamps and observable progress throughout the branch's lifetime.
3. **No Premature Clobbering**: Scoped working state remains isolated until the parallel construct joins and returns its aggregated result.

---

## Agent Memory Files

Agent memory lives under `runs/{id}/agents/{name}/` for execution-scoped
Expand Down
231 changes: 231 additions & 0 deletions tests/open-prose/primitives/parallel-blocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Conformance test for parallel block invocations and nested session preservation (Issue #17).
//
// In Issue #17 (@MattKotsenas), invoking a block with sequential session: calls inside
// parallel: or parallel for caused nested sessions to collapse into a single monolithic
// subagent prompt, losing discrete session boundaries and batching ledger entries with
// identical timestamps.
//
// This test validates:
// 1. prose.md specifies statement-by-statement parallel execution and forbids collapsing prompts.
// 2. prosescript.md defines parallel block invocation invariants and discrete nested sessions.
// 3. authoring.md catalogs the anti-pattern of collapsing parallel block invocations.
// 4. filesystem.md defines scoped execution frames (__scope/{execution_id}).
// 5. Execution trace simulation verifying discrete session boundaries and advancing timestamps.

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

const repoRoot = fileURLToPath(new URL("../../../", import.meta.url));

function readFile(relPath: string): string {
return readFileSync(join(repoRoot, relPath), "utf8").replace(/\s+/g, " ");
}

describe("Issue #17 - prose.md VM execution specification", () => {
const proseDoc = readFile("skills/open-prose/prose.md");

it("specifies Block Invocation in Parallel Contexts section", () => {
expect(proseDoc).toContain("### Block Invocation in Parallel Contexts");
});

it("mandates statement-by-statement execution per parallel branch", () => {
expect(proseDoc).toMatch(/statement-by-statement execution/i);
});

it("strictly forbids collapsing block statements into a single natural-language summary prompt", () => {
expect(proseDoc).toMatch(/MUST NOT collapse.*summary prompt/i);
});

it("mandates discrete nested sessions with individual prompts and completion barriers", () => {
expect(proseDoc).toMatch(/Discrete nested sessions/i);
expect(proseDoc).toMatch(/independent, discrete session with its own prompt/i);
});

it("defines the Sub-VM delegation protocol for parallel branches", () => {
expect(proseDoc).toMatch(/Sub-VM delegation protocol/i);
expect(proseDoc).toMatch(/act as an OpenProse sub-VM/i);
});

it("requires contemporaneous state and ledger updates rather than batching at termination", () => {
expect(proseDoc).toMatch(/Contemporaneous state and ledger updates/i);
expect(proseDoc).toMatch(/not batched at the end of the parallel branch/i);
});

it("links to Scoped Execution Frames for parallel iteration isolation", () => {
expect(proseDoc).toMatch(/Scoped Execution Frames/i);
expect(proseDoc).toContain("__scope/{execution_id}");
});

it("cross-references block invocations under Section 4d Parallel Execution", () => {
expect(proseDoc).toMatch(/Block Invocation in Parallel Contexts/);
});
});

describe("Issue #17 - prosescript.md language reference", () => {
const scriptDoc = readFile("skills/open-prose/prosescript.md");

it("documents Parallel Block Invocations under ## Parallel Blocks", () => {
expect(scriptDoc).toContain("### Parallel Block Invocations");
expect(scriptDoc).toMatch(/parallel for item in items:\s+do process-item\(item\)/);
});

it("states the invariant of no statement collapsing and discrete nested sessions", () => {
expect(scriptDoc).toMatch(/No statement collapsing/i);
expect(scriptDoc).toMatch(/Discrete nested sessions/i);
expect(scriptDoc).toMatch(/Contemporaneous updates/i);
});

it("documents parallel preservation in ## Blocks And `do`", () => {
expect(scriptDoc).toMatch(/When a block is invoked inside a parallel construct/i);
expect(scriptDoc).toMatch(/statement sequence is preserved for every branch/i);
expect(scriptDoc).toMatch(/never collapses a block into a single monolithic prompt/i);
});
});

describe("Issue #17 - guidance/authoring.md anti-patterns", () => {
const authoringDoc = readFile("skills/open-prose/guidance/authoring.md");

it("catalogs the anti-pattern of collapsing parallel block invocations", () => {
expect(authoringDoc).toMatch(/Collapsing block invocations in parallel contexts into a single natural-language subagent task/i);
});

it("catalogs the anti-pattern of batching intermediate ledger writes", () => {
expect(authoringDoc).toMatch(/Batching intermediate ledger writes or state updates at the end of a parallel block branch/i);
});
});

describe("Issue #17 - state/filesystem.md scoped execution frames", () => {
const fsDoc = readFile("skills/open-prose/state/filesystem.md");

it("includes scoped execution frames in the canonical directory table", () => {
expect(fsDoc).toContain("workspace/{node}/__scope/{execution_id}/*");
});

it("defines ## Scoped Execution Frames with isolation and contemporaneous progression", () => {
expect(fsDoc).toContain("## Scoped Execution Frames");
expect(fsDoc).toMatch(/Per-Branch Isolation/i);
expect(fsDoc).toMatch(/Discrete Session and State Progression/i);
});
});

describe("Issue #17 - Behavioral Simulation: Parallel Block Execution vs Collapsed Anti-Pattern", () => {
interface LedgerEntry {
itemId: string;
event: string;
timestamp: number;
sessionId: string;
}

// Simulates compliant OpenProse VM execution: statement-by-statement with discrete sessions
async function runCompliantParallelBlock(items: string[]): Promise<LedgerEntry[]> {
const ledger: LedgerEntry[] = [];
let virtualClock = 1000;

await Promise.all(
items.map(async (itemId, itemIndex) => {
const executionId = `exec-${itemIndex}`;

// Statement 1: session: ledger (phase-1-started)
const t1 = virtualClock++;
ledger.push({
itemId,
event: `phase-1-started for ${itemId}`,
timestamp: t1,
sessionId: `${executionId}-session-ledger-1`,
});

// Statement 2: let step1_result = session "Do step 1"
virtualClock += 50; // Work step takes time
const step1SessionId = `${executionId}-session-step1`;

// Statement 3: session: ledger (phase-1-completed)
const t2 = virtualClock++;
ledger.push({
itemId,
event: `phase-1-completed for ${itemId}`,
timestamp: t2,
sessionId: `${executionId}-session-ledger-2`,
});

// Statement 4: session: ledger (phase-2-started)
const t3 = virtualClock++;
ledger.push({
itemId,
event: `phase-2-started for ${itemId}`,
timestamp: t3,
sessionId: `${executionId}-session-ledger-3`,
});

// Statement 5: let step2_result = session "Do step 2"
virtualClock += 50; // Work step takes time
const step2SessionId = `${executionId}-session-step2`;

// Statement 6: session: ledger (phase-2-completed)
const t4 = virtualClock++;
ledger.push({
itemId,
event: `phase-2-completed for ${itemId}`,
timestamp: t4,
sessionId: `${executionId}-session-ledger-4`,
});
}),
);

return ledger;
}

// Simulates the collapsed anti-pattern that caused Issue #17
function runCollapsedAntiPattern(items: string[]): LedgerEntry[] {
const ledger: LedgerEntry[] = [];
const finishTime = 5000;

for (const itemId of items) {
const subagentSessionId = `subagent-${itemId}`;
// All events appended at the same time upon subagent task completion
ledger.push(
{ itemId, event: `phase-1-started for ${itemId}`, timestamp: finishTime, sessionId: subagentSessionId },
{ itemId, event: `phase-1-completed for ${itemId}`, timestamp: finishTime, sessionId: subagentSessionId },
{ itemId, event: `phase-2-started for ${itemId}`, timestamp: finishTime, sessionId: subagentSessionId },
{ itemId, event: `phase-2-completed for ${itemId}`, timestamp: finishTime, sessionId: subagentSessionId },
);
}

return ledger;
}

it("produces distinct advancing timestamps and discrete sessions per phase under compliant VM execution", async () => {
const items = ["item-A", "item-B"];
const log = await runCompliantParallelBlock(items);

for (const itemId of items) {
const itemEvents = log.filter((e) => e.itemId === itemId);
expect(itemEvents).toHaveLength(4);

const [p1Start, p1End, p2Start, p2End] = itemEvents;

// Discrete timestamps reflect real elapsed time between phase start and completion
expect(p1End.timestamp).toBeGreaterThan(p1Start.timestamp);
expect(p2Start.timestamp).toBeGreaterThan(p1End.timestamp);
expect(p2End.timestamp).toBeGreaterThan(p2Start.timestamp);

// Each ledger update had a discrete session ID
const sessionIds = new Set(itemEvents.map((e) => e.sessionId));
expect(sessionIds.size).toBe(4);
}
});

it("illustrates the bug in the collapsed anti-pattern with identical timestamps and single session", () => {
const items = ["item-A"];
const collapsedLog = runCollapsedAntiPattern(items);

// In the buggy / collapsed behavior, all timestamps are identical
const timestamps = new Set(collapsedLog.map((e) => e.timestamp));
expect(timestamps.size).toBe(1);

// And all events share the single subagent session ID
const sessionIds = new Set(collapsedLog.map((e) => e.sessionId));
expect(sessionIds.size).toBe(1);
});
});