Skip to content
Merged
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
46 changes: 41 additions & 5 deletions fixtures/react-parity/runtime/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# Installed native runtime consumers

## Thread lifetime

Open `/?threads` on either review URL for the separate conversation-selection
workflow. Both apps use a fixed-thread session for each selected conversation.
The application selects a new session and explicitly disposes the outgoing one;
selecting the same thread is a no-op. Returning to an old thread creates a fresh
local session. Selection performs no history read, submission or server thread
creation. Click **Load selected** to observe persisted data.

React borrows the selected session through `useAgent`. A keyed view also resets
its local command feedback. Angular keys a child component by selection identity
and calls `observeAgent` in that component's injection context once its required
input is available. Destroying the component releases the subscription. The
application owns disposal, independently of view destruction. Binding tests also
cover retaining the old session and reattaching to its completed run.

Follow the thirteen-action sequence shown in the app. The wire fixture requires
four history POSTs and two run POSTs. Selecting B during A's stream aborts local
observation; returning to A starts empty. A's second history request deliberately
stays pending until selecting B disposes it. Each new session starts with no
observed values or history. Disposal permanently closes this example's owner.
The existing main review sequence remains available at `/` on the same server.

The selection owner is fixture-only application code, not a public library
manager, cache or router. URL selection can feed an application's selected ID,
but URL synchronization and thread creation/list CRUD remain migration work.
Disposal stops local ownership, not remote execution or already-started side
effects. Runtime tests cover late history, concurrent sessions and a late durable
tool claim settled under its retired thread. Checkpoint execution is separate
from thread selection. No core, runtime or native binding API changes are added
by this proof.

## Run configuration

The private LangGraph session accepts readonly `config`, `context`, `metadata`
Expand Down Expand Up @@ -218,7 +250,7 @@ are missing. `node scripts/react-parity/review-runtime.mjs --help` prints the
prerequisite and review sequence.

The runner packs those artifacts, installs and strictly type-checks isolated
React and Angular consumers, builds each app, and runs all fifteen browser
React and Angular consumers, builds each app, and runs all twenty-one browser
scenarios on fresh fixture servers. Only after those checks pass does it print
two new, untouched loopback URLs. Open each URL manually; no browser opens
automatically. The review servers have made no SDK requests at that point.
Expand Down Expand Up @@ -404,14 +436,18 @@ React uses a Vite production build. Angular uses the existing consumer template'
installed Angular CLI application builder and real APF linking, with output in
`dist/consumer/browser` and input evidence from `dist/consumer/stats.json`.

Both built apps run the same fifteen browser scenarios in installed Playwright
Both built apps run the same twenty-one browser scenarios in installed Playwright
Chromium: inert mount, explicit history load, equal history refresh, empty history
replacement, successful text, a real local tool handler and exact
two-request result continuation, protected visible server error, held streaming
DOM updates and Stop, the full pause batch retained after Stop, an explicit response
map and second pause, same-message resume completion, known-run premature EOF,
explicit cursor join with exact-run completion, reuse after Stop,
then unmount/dispose/post-disposal commands.
then unmount/dispose/post-disposal commands. The separate thread view adds six
scenario groups covering explicit selection/load, switching during streaming,
same-thread identity, a fresh session on return, switching during history and
permanent owner disposal. Its four history and two run POSTs are counted
separately from the main workflow below.
Seven submissions and two resumes through the component controls make exactly ten
run POSTs (including one tool continuation) and call the handler once. One explicit
reconnect makes one join GET; the original Drop EOF and joined EOF each require
Expand All @@ -425,8 +461,8 @@ the catalog and actual serialized ToolMessage payload.
Values assertions distinguish unobserved from empty state, show loaded application
fields, and verify replacement/deletion across root, tool, held and reused runs.
Separate native component tests make four history reads to cover a values-only
refresh with unchanged messages and interrupts; installed browser scenarios still
make three. History fixtures contain two separate task payloads and show paused
refresh with unchanged messages and interrupts; the main installed workflow
makes three. History fixtures contain two separate task payloads and show paused
delivery. The Pause button sends two separate root controls and renders both
payloads; Stop retains them without another request. Resume clears the old batch
and observes a new pause, then a second explicit resume completes without adding
Expand Down
153 changes: 153 additions & 0 deletions fixtures/react-parity/runtime/angular-threads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {
Component,
inject,
Injector,
input,
runInInjectionContext,
signal,
type OnInit,
type Signal,
} from '@angular/core';
import { observeAgent } from '@threadplane/angular';
import { bootstrapApplication } from '@angular/platform-browser';
import { createFixtureSession } from './runtime-entry.js';
import {
createThreadOwner,
threadInstructions,
type ThreadSelection,
} from './thread-owner';
import { display, type FixtureSnapshot } from './scenarios';

// The application owns execution outside component initialization/destruction.
const owner = createThreadOwner((id) => createFixtureSession('/api', id));

@Component({
selector: 'thread-view',
template: `
<section class="panel" aria-label="Selected conversation">
<h2>Selected conversation</h2>
<div class="controls">
<button (click)="loadSelected()">Load selected</button>
<button (click)="run()">Run selected</button>
</div>
<div class="fields">
<div>
<h3>Thread</h3>
<output data-testid="thread-id">{{ selected().id }}</output>
</div>
<div>
<h3>Session generation</h3>
<output data-testid="thread-generation">{{
selected().generation
}}</output>
</div>
<div>
<h3>Status</h3>
<output data-testid="thread-status">{{ snapshot().status }}</output>
</div>
<div>
<h3>History request</h3>
<output data-testid="thread-load">{{ load() }}</output>
</div>
<div>
<h3>Run outcome</h3>
<output data-testid="thread-outcome">{{ outcome() }}</output>
</div>
<div>
<h3>Transcript</h3>
<output data-testid="thread-text">{{ view().transcript }}</output>
</div>
<div>
<h3>Values</h3>
<output data-testid="thread-values">{{ view().values }}</output>
</div>
<div>
<h3>Checkpoint history</h3>
<output data-testid="thread-history">{{ view().history }}</output>
</div>
</div>
</section>
`,
})
export class ThreadView implements OnInit {
readonly selected = input.required<ThreadSelection>();
private readonly injector = inject(Injector);
snapshot!: Signal<FixtureSnapshot>;
readonly load = signal('unobserved');
readonly outcome = signal('');
// This component is keyed by selection identity. Its input never changes;
// destruction releases observeAgent through this component's DestroyRef.
ngOnInit() {
this.snapshot = runInInjectionContext(this.injector, () =>
observeAgent(this.selected().session)
);
}
readonly view = () => display(this.snapshot());
async loadSelected() {
const selected = this.selected();
this.load.set('loading');
try {
await selected.session.load?.();
this.load.set('loaded');
} catch {
this.load.set('error');
}
}
async run() {
const selected = this.selected();
this.outcome.set('running');
try {
this.outcome.set(
await selected.session.submit(
selected.id === 'thread-a' ? 'Hold A' : 'Send B'
)
);
} catch {
this.outcome.set('error');
}
}
}

@Component({
selector: 'app-root',
imports: [ThreadView],
template: `
<main class="review-shell">
<header>
<p class="eyebrow">Installed package review · Angular</p>
<h1>Thread lifetime</h1>
</header>
<section class="panel">
<h2>Review sequence</h2>
<p>{{ instructions }}</p>
</section>
<section class="panel" aria-label="Thread owner">
<h2>Application selection</h2>
<div class="controls">
<button (click)="select('thread-a')">Select A</button>
<button (click)="select('thread-b')">Select B</button>
<button (click)="dispose()">Dispose selected</button>
</div>
<h3>Owner</h3>
<output data-testid="thread-owner">{{ state() }}</output>
</section>
@for (entry of [selected()]; track entry) {
<thread-view [selected]="entry" />
}
</main>
`,
})
export class ThreadApp {
readonly instructions = threadInstructions;
readonly selected = signal(owner.selected);
readonly state = signal('active');
select(id: ThreadSelection['id']) {
this.selected.set(owner.select(id));
}
async dispose() {
await owner.dispose();
this.state.set('disposed');
}
}

void bootstrapApplication(ThreadApp);
Loading
Loading