React AWS Strands meeting preview
Review a proposed meeting with a Strands agent, then choose whether to continue. The application combines ordinary conversation rendering with a decision tied to the backend’s current pause.
The native React APIs on this page are experimental, private source-build packages. Run the repository example to use them; they are not an npm installation recipe.
Read the pause and confirm the result
The Strands bridge sends an AG-UI interrupt whose payload includes the proposed topic and slot. The application preserves its identity, submits the selected response, and waits for the resumed tool result. A schedule snapshot and assistant prose are useful display data, but neither replaces confirmation of the requested operation. The calendar in this example is fictional process-local data.
The React conversation walkthrough connects this capability to requests, tools, UI state, and saved history.
Request and review a meeting
Choose Run, then Q3 roadmap review to fill an editable message. Only Send creates a native Session and thread and starts the request. A draft, assistant prose, or observed schedule cannot authorize a decision. Approve and Decline appear only for the current native generic interrupt for book_meeting. Its metadata.reason carries the topic and slot; the interrupt has no tool-call id or approval schema. The application validates the current root run and exact meeting before enabling the controls.
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated consumer copies this internal example layout.
import { ExampleFooter, ExampleLayout } from '../../../../shared/react-example';
import { useEffect, useLayoutEffect, useState } from 'react';
import { projectTextTranscript } from '@threadplane/ag-ui';
import { useAgent } from '@threadplane/react';
import {
ApprovalCard,
ChatInput,
TextTranscript,
} from '@threadplane/react/chat';
import {
createConnectedApplication,
type strandsConnection,
} from './connection';
import type { Meeting } from './approval-policy';
export const MEETING_PRESET =
'This is a fictional demo meeting. Check availability for Tuesday, then book Q3 roadmap review for Tuesday 10:00 after my approval.';
function MeetingDetails({
meeting,
}: {
readonly meeting: Readonly<Partial<Meeting>>;
}) {
return (
<dl className="meeting-details">
<dt>Topic</dt>
<dd>{meeting.topic ?? 'Not observed'}</dd>
<dt>Slot</dt>
<dd>{meeting.slot ?? 'Not observed'}</dd>
</dl>
);
}
export function StrandsDemo({
connection,
onReady,
}: {
readonly connection: ReturnType<typeof strandsConnection>;
readonly onReady: () => void;
}) {
// Applications are lazy: construction allocates neither a thread nor a Session.
// The stable owner survives StrictMode effect rehearsal; disposed applications do not.
type Application = ReturnType<typeof createConnectedApplication>;
const [owner] = useState<{
current?: Application;
connection: typeof connection;
}>(() => ({ connection }));
const [application, setApplication] = useState<Application | null>(null);
useLayoutEffect(() => {
if (!owner.current || owner.current.getSnapshot().status === 'disposed')
owner.current = createConnectedApplication(owner.connection);
const acquired = owner.current;
setApplication(acquired);
return () => {
void acquired.dispose().catch(() => undefined);
};
}, [owner]);
return application ? (
<StrandsPresentation
application={application}
label={owner.connection.label}
onReady={onReady}
/>
) : (
<p role="status">Starting meeting preview…</p>
);
}
function StrandsPresentation({
application,
label,
onReady,
}: {
readonly application: ReturnType<typeof createConnectedApplication>;
readonly label: ReturnType<typeof strandsConnection>['label'];
readonly onReady: () => void;
}) {
const [draft, setDraft] = useState('');
const snapshot = useAgent(application);
const generation = snapshot.generation;
const approval = snapshot.approval;
useEffect(() => {
if (application.getSnapshot().status !== 'disposed') onReady();
}, [application, onReady]);
useEffect(() => {
setDraft('');
}, [generation]);
const status = {
ready: 'Ready.',
running: 'Checking the meeting…',
pending: 'Awaiting your decision.',
approved: 'Fictional meeting booked.',
declined: 'Meeting declined.',
unconfirmed: 'Meeting unconfirmed. Start a new conversation.',
'cleanup-failed': 'Conversation could not close. Reload the preview.',
replacing: 'Starting new conversation…',
disposed: 'Preview closed.',
}[snapshot.status];
return (
<ExampleLayout className="strands-demo">
<header>
<div>
<h1>AWS Strands meetings</h1>
</div>
<button
type="button"
disabled={
snapshot.status === 'replacing' ||
snapshot.status === 'disposed' ||
snapshot.status === 'cleanup-failed'
}
onClick={() => {
void application.newConversation();
}}
>
New conversation
</button>
</header>
<p>
Review a fictional meeting, then approve or decline it. No real meeting
is booked.
</p>
<p className="preview-note">Source-build preview · {label}</p>
<button
type="button"
className="preset"
disabled={!snapshot.canSend}
onClick={() => setDraft(MEETING_PRESET)}
>
Q3 roadmap review
</button>
<TextTranscript
messages={projectTextTranscript(snapshot.rows)}
label="Conversation"
/>
<section
aria-label="Last observed schedule"
className="observed-schedule"
>
<h2>Last observed schedule</h2>
<p>
The last snapshot can still show pending after the meeting tool
finishes. See the separate backend result for the confirmed decision.
</p>
{snapshot.schedule?.availability ? (
<p>
Availability · {snapshot.schedule.availability.day}:{' '}
{snapshot.schedule.availability.slots.join(', ') ||
'No slots observed'}
</p>
) : (
<p>No availability observed yet.</p>
)}
{snapshot.schedule?.booking ? (
<>
<MeetingDetails meeting={snapshot.schedule.booking} />
<p>
Observed booking status:{' '}
{snapshot.schedule.booking.status ?? 'Not observed'}
</p>
</>
) : (
<p>No booking observed yet.</p>
)}
</section>
{approval && (
<ApprovalCard
title="Meeting approval"
disabled={snapshot.busy || !snapshot.canRespond}
actions={[
{
id: 'approve',
label: 'Approve',
onSelect: () => {
void application.decide(approval, 'approve', generation);
},
},
{
id: 'decline',
label: 'Decline',
onSelect: () => {
void application.decide(approval, 'decline', generation);
},
},
]}
>
<MeetingDetails meeting={approval.meeting} />
<p>This decision applies only to the fictional meeting shown here.</p>
</ApprovalCard>
)}
{snapshot.result !== null && (
<section
aria-label="Observed backend result"
className="backend-result"
>
<h2>Observed backend result</h2>
<p>The meeting tool returned this exact text:</p>
<pre>{snapshot.result}</pre>
</section>
)}
<ExampleFooter>
{snapshot.error && <p role="alert">{snapshot.error}</p>}
<section aria-label="Compose message">
<ChatInput
label="Message"
placeholder="Ask about a fictional meeting…"
value={draft}
onValueChange={setDraft}
disabled={!snapshot.canSend}
busy={snapshot.busy}
onStop={
snapshot.busy &&
snapshot.status !== 'replacing' &&
snapshot.status !== 'cleanup-failed'
? () => {
void application.stop();
}
: undefined
}
onSubmit={(text) => {
void application.submit(text, generation);
const current = application.getSnapshot();
return (
current.generation === generation &&
current.busy &&
current.status === 'running'
);
}}
/>
</section>
<p role="status">{status}</p>
</ExampleFooter>
</ExampleLayout>
);
}import type { Session } from '@threadplane/ag-ui';
export type NativeSnapshot = ReturnType<Session['getSnapshot']>;
type Transcript = NativeSnapshot['transcript'];
export interface Meeting {
readonly topic: string;
readonly slot: string;
}
export interface OwnedTurn {
readonly threadId: string;
readonly runId: string;
readonly human: Transcript[number];
readonly prefix: Transcript;
}
export interface MeetingApproval {
readonly threadId: string;
readonly sourceRunId: string;
/** Keep the original opaque branded occurrence, never decode an interrupt identifier. */
readonly pause: Parameters<Session['resume']>[0];
readonly interruptId: string;
readonly toolCallId: string;
readonly meeting: Meeting;
readonly transcript: Transcript;
readonly observation: NativeSnapshot;
}
export function record(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
/** Reject accessors/cycles and bound both traversal and retained text. Recheck after every proxy trap. */
export function ownData(value: unknown, valid = () => true): unknown {
let nodes = 100000,
characters = 1000000;
const seen = new Set<object>();
function copy(input: unknown, depth: number): unknown {
if (!valid() || --nodes < 0 || depth > 32)
throw Error('Revoked or oversized observation');
if (typeof input === 'string') {
characters -= input.length;
if (characters < 0) throw Error('Oversized text');
return input;
}
if (input === null || input === undefined || typeof input === 'boolean')
return input;
if (typeof input === 'number' && Number.isFinite(input)) return input;
if (typeof input !== 'object' || seen.has(input))
throw Error('Not plain data');
const array = Array.isArray(input),
prototype = Object.getPrototypeOf(input);
if (
!valid() ||
(!array && prototype !== Object.prototype && prototype !== null)
)
throw Error('Not plain data');
let arrayLength: number | undefined;
if (array) {
const descriptor = Object.getOwnPropertyDescriptor(input, 'length');
if (
!valid() ||
!descriptor ||
!('value' in descriptor) ||
!Number.isSafeInteger(descriptor.value) ||
descriptor.value < 0 ||
descriptor.value > nodes
)
throw Error('Revoked or oversized array');
arrayLength = descriptor.value as number;
}
seen.add(input);
const output: Record<string, unknown> | unknown[] = array ? [] : {};
const keys = Reflect.ownKeys(input);
if (!valid() || keys.length > nodes)
throw Error('Revoked or oversized keys');
if (arrayLength !== undefined) {
// Reject sparse arrays instead of retaining uncharged logical slots.
// Check bounds too: a proxy may under-report its writable length.
let slots = 0;
for (const key of keys) {
if (typeof key !== 'string') throw Error('Not plain data');
const index = Number(key);
if (
Number.isInteger(index) &&
index >= 0 &&
index < 4294967295 &&
String(index) === key
) {
if (index >= arrayLength) throw Error('Array index exceeds length');
slots++;
}
}
if (slots !== arrayLength) throw Error('Sparse array');
}
for (const key of keys) {
if (!valid() || typeof key !== 'string') throw Error('Not plain data');
characters -= key.length;
if (characters < 0) throw Error('Oversized keys');
if (array && key === 'length') continue;
const descriptor = Object.getOwnPropertyDescriptor(input, key);
if (!valid() || !descriptor || !('value' in descriptor))
throw Error('Accessor data');
Object.defineProperty(output, key, {
value: copy(descriptor.value, depth + 1),
enumerable: true,
});
}
seen.delete(input);
if (!valid()) throw Error('Revoked');
return Object.freeze(output);
}
return copy(value, 0);
}
export function ownSnapshot(
value: NativeSnapshot,
valid = () => true
): NativeSnapshot | undefined {
try {
return ownData(value, valid) as NativeSnapshot;
} catch {
return undefined;
}
}
export function same(left: unknown, right: unknown): boolean {
function canonical(value: unknown): unknown {
if (Array.isArray(value)) return value.map(canonical);
const r = record(value);
return r
? Object.fromEntries(
Object.keys(r)
.sort()
.map((k) => [k, canonical(r[k])])
)
: value;
}
return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right));
}
function rootRows(s: NativeSnapshot): Transcript | undefined {
if (!Array.isArray(s.transcript) || !Array.isArray(s.subagents)) return;
const ids = new Set<string>(),
calls = new Set<string>();
for (const message of s.transcript) {
const m = record(message);
if (!m || typeof m['id'] !== 'string' || !m['id'] || ids.has(m['id']))
return;
ids.add(m['id']);
if (
m['subagentRunId'] !== undefined &&
(typeof m['subagentRunId'] !== 'string' || !m['subagentRunId'])
)
return;
const roles =
typeof m['subagentRunId'] === 'string'
? [
'user',
'assistant',
'tool',
'developer',
'system',
'activity',
'reasoning',
]
: ['user', 'assistant', 'tool'];
if (!roles.includes(m['role'] as string)) return;
if (m['toolCalls'] !== undefined) {
if (m['role'] !== 'assistant' || !Array.isArray(m['toolCalls'])) return;
for (const call of m['toolCalls']) {
const c = record(call),
f = record(c?.['function']);
if (
!c ||
typeof c['id'] !== 'string' ||
!c['id'] ||
calls.has(c['id']) ||
c['type'] !== 'function' ||
!f ||
typeof f['name'] !== 'string' ||
typeof f['arguments'] !== 'string'
)
return;
calls.add(c['id']);
}
}
}
const rows = s.transcript.filter((m) => m.subagentRunId === undefined);
const rootCalls = new Set<string>(),
results = new Set<string>();
for (const m of rows) {
if (m.role === 'assistant')
for (const call of m.toolCalls ?? []) rootCalls.add(call.id);
else if (m.role === 'tool') {
// A result can only belong to a root declaration already observed before it.
if (!rootCalls.has(m.toolCallId) || results.has(m.toolCallId)) return;
results.add(m.toolCallId);
}
}
return Object.freeze(rows);
}
function semantic(message: Transcript[number]) {
if (message.role !== 'assistant') return message;
return {
...message,
content: message.content ?? '',
toolCalls: message.toolCalls?.map((c) => ({
...c,
function: { ...c.function, arguments: parse(c.function.arguments) },
})),
};
}
function history(past: Transcript, current: Transcript) {
let next = 0;
for (const message of current) {
if (next === past.length) break;
const retained = past[next];
if (message.id !== retained.id) continue;
if (!same(semantic(retained), semantic(message))) return false;
next++;
}
// New observations may interleave retained rows, but never reorder them.
return next === past.length;
}
export function captureTurn(
snapshot: NativeSnapshot,
threadId: string,
previous: string | undefined,
prefix: Transcript,
valid = () => true
): OwnedTurn | undefined {
const s = ownSnapshot(snapshot, valid),
rows = s && rootRows(s);
if (
!s ||
!rows ||
s.status !== 'running' ||
s.decision ||
typeof threadId !== 'string' ||
!threadId ||
typeof s.run?.id !== 'string' ||
!s.run.id ||
s.run.id === previous ||
s.run.outcome !== undefined ||
rows.length !== prefix.length + 1 ||
!history(prefix, rows)
)
return;
const human = rows.at(-1);
if (
!human ||
human.role !== 'user' ||
typeof human.content !== 'string' ||
!human.content.trim() ||
!valid()
)
return;
return Object.freeze({
threadId,
runId: s.run.id,
human,
prefix: Object.freeze([...prefix]),
});
}
function parse(value: string): unknown {
try {
return JSON.parse(value);
} catch {
return undefined;
}
}
export function boundedText(value: unknown, maximum = 500): value is string {
return typeof value === 'string' && !!value.trim() && value.length <= maximum;
}
export function meetingValue(value: unknown): Meeting | undefined {
const m = record(value);
if (
!m ||
Object.keys(m).length !== 2 ||
!boundedText(m['topic']) ||
!boundedText(m['slot'])
)
return;
return Object.freeze({ topic: m['topic'], slot: m['slot'] });
}
function lookupArgs(value: unknown) {
const a = record(value);
return !!a && Object.keys(a).length === 1 && boundedText(a['day']);
}
function submitCalls(rows: Transcript) {
return rows.flatMap((m) => (m.role === 'assistant' ? m.toolCalls ?? [] : []));
}
function nativeInterrupt(value: unknown, now: number) {
const i = record(value);
if (
!i ||
!boundedText(i['id'], 2000) ||
i['reason'] !== 'book_meeting' ||
i['toolCallId'] !== undefined ||
i['responseSchema'] !== undefined ||
i['subagentRunId'] !== undefined
)
return;
const expiry = i['expiresAt'];
if (
expiry !== undefined &&
(typeof expiry !== 'string' ||
!Number.isFinite(Date.parse(expiry)) ||
Date.parse(expiry) <= now)
)
return;
return i;
}
export function captureApproval(
snapshot: NativeSnapshot,
turn: OwnedTurn,
valid = () => true
): MeetingApproval | undefined {
const s = ownSnapshot(snapshot, valid),
rows = s && rootRows(s);
if (!s || !rows || !turn) return;
const d = s.decision,
r = s.run,
t = r?.terminal;
if (
s.status !== 'idle' ||
r?.id !== turn.runId ||
r.outcome !== 'paused' ||
d?.kind !== 'native' ||
!boundedText(d.id, 4000) ||
d.attempt ||
d.sourceRunId !== turn.runId ||
!Array.isArray(d.interrupts) ||
d.interrupts.length !== 1 ||
t?.type !== 'RUN_FINISHED' ||
record(t)?.['subagentRunId'] !== undefined ||
t.threadId !== turn.threadId ||
t.runId !== turn.runId ||
t.outcome?.type !== 'interrupt' ||
!Array.isArray(t.outcome.interrupts) ||
t.outcome.interrupts.length !== 1 ||
!history([...turn.prefix, turn.human], rows)
)
return;
const i = nativeInterrupt(d.interrupts[0], Date.now()),
end = nativeInterrupt(t.outcome.interrupts[0], Date.now());
if (!i || !end || !same(i, end)) return;
const metadata = record(i['metadata']),
meeting = meetingValue(metadata?.['reason']);
if (!meeting) return;
const previousCalls = submitCalls(turn.prefix),
calls = submitCalls(rows).filter(
(c) => !previousCalls.some((p) => p.id === c.id)
),
books = calls.filter((c) => c.function.name === 'book_meeting'),
lookups = calls.filter((c) => c.function.name === 'check_availability');
const humanIndex = rows.findIndex((m) => m.id === turn.human.id);
if (
humanIndex < 0 ||
submitCalls(rows.slice(0, humanIndex + 1)).some((c) =>
calls.some((current) => current.id === c.id)
)
)
return;
if (
books.length !== 1 ||
lookups.length > 1 ||
calls.length !== books.length + lookups.length ||
!same(parse(books[0].function.arguments), meeting) ||
lookups.some((c) => !lookupArgs(parse(c.function.arguments))) ||
rows.some(
(m) =>
m.role === 'tool' && m.error && calls.some((c) => c.id === m.toolCallId)
) ||
rows.some((m) => m.role === 'tool' && m.toolCallId === books[0].id) ||
!same(
rows.filter((m) => m.role === 'user'),
[...turn.prefix.filter((m) => m.role === 'user'), turn.human]
) ||
!valid()
)
return;
try {
const original = Object.getOwnPropertyDescriptor(
snapshot,
'decision'
)?.value;
if (!valid()) return;
const pause =
original &&
(Object.getOwnPropertyDescriptor(original, 'id')?.value as Parameters<
Session['resume']
>[0]);
if (!valid() || pause !== d.id) return;
return Object.freeze({
threadId: turn.threadId,
sourceRunId: turn.runId,
pause,
interruptId: i['id'] as string,
toolCallId: books[0].id,
meeting,
transcript: rows,
observation: s,
});
} catch {
return;
}
}
export function sameApproval(a: MeetingApproval, b: MeetingApproval): boolean {
return (
a.pause === b.pause &&
a.threadId === b.threadId &&
a.sourceRunId === b.sourceRunId &&
a.interruptId === b.interruptId &&
a.toolCallId === b.toolCallId &&
same(a.meeting, b.meeting) &&
same(a.transcript.map(semantic), b.transcript.map(semantic)) &&
same(a.observation.decision, b.observation.decision) &&
same(a.observation.run, b.observation.run)
);
}
export function confirmResume(
snapshot: NativeSnapshot,
approval: MeetingApproval,
runId: string,
approved: boolean,
valid = () => true
): { readonly transcript: Transcript; readonly result: string } | undefined {
const s = ownSnapshot(snapshot, valid),
rows = s && rootRows(s);
if (
!s ||
!rows ||
!approval ||
!runId ||
runId === approval.sourceRunId ||
s.status !== 'idle' ||
s.decision ||
s.run?.id !== runId ||
s.run.outcome !== 'success'
)
return;
const t = s.run.terminal;
if (
t?.type !== 'RUN_FINISHED' ||
record(t)?.['subagentRunId'] !== undefined ||
t.threadId !== approval.threadId ||
t.runId !== runId ||
(t.outcome !== undefined && t.outcome.type !== 'success') ||
(t.outcome?.type === 'success' && !!t.outcome.pendingToolCallIds?.length) ||
!history(approval.transcript, rows)
)
return;
const ownedCalls = submitCalls(approval.transcript),
calls = submitCalls(rows),
results = rows.filter(
(m) => m.role === 'tool' && m.toolCallId === approval.toolCallId
),
pending = ownedCalls.filter(
(c) =>
c.id !== approval.toolCallId &&
!approval.transcript.some(
(m) => m.role === 'tool' && m.toolCallId === c.id
)
);
if (pending.length > 1) return;
for (const call of pending) {
const lookup = rows.filter(
(m) => m.role === 'tool' && m.toolCallId === call.id
);
if (
call.function.name !== 'check_availability' ||
!lookupArgs(parse(call.function.arguments)) ||
lookup.length !== 1 ||
lookup[0].role !== 'tool' ||
lookup[0].error ||
!boundedText(lookup[0].content, 100000)
)
return;
}
const expected = approved
? `Meeting booked for ${approval.meeting.slot}: ${approval.meeting.topic}`
: `The human declined. Meeting NOT booked: ${approval.meeting.topic}`;
if (
results.length !== 1 ||
results[0].role !== 'tool' ||
results[0].error ||
typeof results[0].content !== 'string' ||
parse(results[0].content) !== expected ||
rows.some(
(m) =>
m.role === 'user' && !approval.transcript.some((p) => p.id === m.id)
) ||
rows.some(
(m) =>
m.role === 'tool' &&
m.toolCallId !== approval.toolCallId &&
!approval.transcript.some((p) => p.id === m.id) &&
!pending.some((c) => c.id === m.toolCallId)
) ||
calls.some((c) => !ownedCalls.some((p) => p.id === c.id)) ||
!valid()
)
return;
return Object.freeze({ transcript: rows, result: expected });
}Resume and confirm
The application preserves the original opaque pause id and calls native session.resume(pauseId, [{ interruptId, status: 'resolved', payload: { approved } }], { signal }). Decline sends approved: false and resumes the tool; it is separate from Stop. A local decision never confirms a booking.
import type { Session } from '@threadplane/ag-ui';
import {
captureApproval,
captureTurn,
confirmResume,
ownSnapshot,
same,
sameApproval,
type MeetingApproval,
type NativeSnapshot,
type OwnedTurn,
} from './approval-policy';
import { projectSchedule, type ScheduleSnapshot } from './schedule-projection';
export interface StrandsSnapshot {
readonly generation: number;
readonly threadId: string | null;
readonly busy: boolean;
readonly canSend: boolean;
readonly canRespond: boolean;
readonly rows: NativeSnapshot['transcript'];
readonly schedule: ScheduleSnapshot | undefined;
readonly approval: MeetingApproval | undefined;
readonly result: string | null;
readonly status:
| 'ready'
| 'running'
| 'pending'
| 'approved'
| 'declined'
| 'unconfirmed'
| 'cleanup-failed'
| 'replacing'
| 'disposed';
readonly error: string | null;
}
type Methods = { [K in keyof Session]: Session[K] };
type Owner = {
readonly candidate: Session;
readonly threadId: string;
readonly methods: Methods;
readonly seenRuns: Set<string>;
closed: boolean;
release?: () => void;
turn?: OwnedTurn;
approval?: MeetingApproval;
transcript: NativeSnapshot['transcript'];
previous?: string;
intent?: 'submit' | 'resume';
resumeRunId?: string;
approved?: boolean;
unsafe: boolean;
};
/** Owns admission, observation and lifetime. Native Session alone executes the protocol. */
export function createStrandsApplication(options: {
readonly sessionFactory: (threadId: string) => Session | Promise<Session>;
readonly threadIdFactory?: () => string;
}) {
const listeners = new Set<() => void>(),
cleanups = new Set<Promise<void>>();
let primary: Owner | undefined,
operation: AbortController | undefined,
disposed = false,
cleanupFailed = false,
revoking = false,
disposal: Promise<void> | undefined;
let snapshot: StrandsSnapshot = Object.freeze({
generation: 0,
threadId: null,
busy: false,
canSend: true,
canRespond: false,
rows: Object.freeze([]),
schedule: undefined,
approval: undefined,
result: null,
status: 'ready',
error: null,
});
const cleanupFailure: Partial<StrandsSnapshot> = Object.freeze({
canSend: false,
canRespond: false,
approval: undefined,
result: null,
status: 'cleanup-failed',
error:
'The conversation could not close safely. Reload the preview to continue.',
});
function failCleanup() {
// Failure is permanent even if a later best-effort teardown succeeds.
cleanupFailed = true;
publish(cleanupFailure);
}
function publish(update: Partial<StrandsSnapshot>) {
if (disposed) return;
snapshot = Object.freeze({
...snapshot,
...update,
...(cleanupFailed ? cleanupFailure : {}),
});
for (const notify of [...listeners]) {
try {
notify();
} catch {
/* An observer cannot prevent revocation. */
}
}
}
function track(promise: Promise<unknown>) {
const done = promise.then(
() => undefined,
() => undefined
);
cleanups.add(done);
void done.then(() => cleanups.delete(done));
return done;
}
async function drain() {
while (cleanups.size) await Promise.all([...cleanups]);
}
function current(p: Owner) {
return !disposed && !cleanupFailed && primary === p && !p.closed;
}
function cleanupCandidate(
candidate: unknown,
methods?: Partial<Methods>,
release?: () => void
) {
// Authority and the rendered controls have already been revoked by the caller.
const done = track(
Promise.resolve()
.then(async () => {
let dispose = methods?.dispose;
try {
dispose ??= (candidate as Session)?.dispose;
} catch {
failCleanup();
}
if (typeof dispose === 'function') {
try {
await dispose.call(candidate);
return;
} catch {
failCleanup();
}
}
let stop = methods?.stop;
try {
stop ??= (candidate as Session)?.stop;
} catch {
failCleanup();
}
if (typeof stop === 'function') {
try {
await stop.call(candidate);
} catch {
failCleanup();
}
} else failCleanup();
})
.catch(() => {
// Retain unexpected cleanup rejection before the settlement-only barrier.
failCleanup();
})
);
try {
release?.();
} catch {
failCleanup();
}
return done;
}
function close(p: Owner) {
p.closed = true;
const release = p.release;
p.release = undefined;
return cleanupCandidate(p.candidate, p.methods, release);
}
function fail(p?: Owner, bootstrap = false) {
if (p && !current(p)) return;
let prepared: (() => void) | undefined;
if (p) {
p.unsafe = true;
p.approval = undefined;
if (bootstrap) {
primary = undefined;
p.closed = true;
// Observers may dispose after detachment but before release. Keep every
// disposal behind preparation until candidate cleanup is registered.
track(
new Promise<void>((resolve) => {
prepared = resolve;
})
);
}
}
publish({
canSend: false,
canRespond: false,
approval: undefined,
result: null,
status: 'unconfirmed',
error:
'The meeting outcome could not be confirmed. Start a new conversation.',
});
if (p && bootstrap) {
try {
void close(p);
} finally {
prepared?.();
}
}
}
function read(p: Owner, valid: () => boolean) {
if (!valid()) return;
const native = p.methods.getSnapshot.call(p.candidate);
if (!valid()) return;
return ownSnapshot(native, valid);
}
function capture(p: Owner, valid: () => boolean) {
const native = read(p, valid);
if (
!native ||
!valid() ||
!Array.isArray(native.transcript) ||
!Array.isArray(native.subagents)
)
return;
if (native.status === 'error') p.unsafe = true;
if (p.intent === 'submit' && !p.turn) {
const turn = captureTurn(
native,
p.threadId,
p.previous,
p.transcript,
valid
);
if (turn) {
if (p.seenRuns.has(turn.runId)) p.unsafe = true;
else {
p.turn = turn;
p.seenRuns.add(turn.runId);
}
}
}
if (
p.intent === 'resume' &&
!p.resumeRunId &&
p.approval &&
native.status === 'running' &&
typeof native.run?.id === 'string' &&
native.run.id !== p.approval.sourceRunId &&
native.decision?.kind === 'native' &&
native.decision.id === p.approval.pause &&
native.decision.sourceRunId === p.approval.sourceRunId &&
native.decision.attempt?.runId === native.run?.id
) {
if (p.seenRuns.has(native.run.id)) p.unsafe = true;
else {
p.resumeRunId = native.run.id;
p.seenRuns.add(native.run.id);
}
}
const approval =
p.intent === 'submit' && p.turn && !p.unsafe
? captureApproval(native, p.turn, valid)
: undefined;
const confirmed =
p.intent === 'resume' &&
p.approval &&
p.resumeRunId &&
typeof p.approved === 'boolean' &&
!p.unsafe
? confirmResume(native, p.approval, p.resumeRunId, p.approved, valid)
: undefined;
return valid()
? {
native,
rows: Object.freeze(
native.transcript.filter((m) => m.subagentRunId === undefined)
),
schedule: projectSchedule(native.state),
approval,
confirmed,
}
: undefined;
}
function observe(p: Owner) {
const valid = () => current(p) && !operation?.signal.aborted;
try {
const result = capture(p, valid);
if (!valid()) return;
if (!result) {
fail(p);
return;
}
if (
snapshot.canRespond &&
(!result.approval ||
!p.approval ||
!sameApproval(result.approval, p.approval))
) {
fail(p);
return;
}
publish({ rows: result.rows, schedule: result.schedule });
} catch {
if (valid()) fail(p);
}
}
function claim(status: 'running' | 'replacing') {
const controller = new AbortController();
operation = controller;
const valid = () =>
!disposed &&
!cleanupFailed &&
operation === controller &&
!controller.signal.aborted;
publish({
busy: true,
canSend: false,
canRespond: false,
approval: undefined,
result: null,
status,
error: null,
});
return { controller, valid };
}
function finish(controller: AbortController) {
if (!disposed && operation === controller) {
operation = undefined;
publish({ busy: false });
}
}
async function install(valid: () => boolean): Promise<Owner | undefined> {
if (!valid()) return;
const threadIdFactory =
options.threadIdFactory ?? (() => crypto.randomUUID());
if (!valid()) return;
const threadId = threadIdFactory();
if (!valid()) return;
if (typeof threadId !== 'string' || !threadId || threadId.length > 500)
throw TypeError('A thread identifier is required');
const factory = options.sessionFactory;
if (!valid()) return;
// Track late bootstrap so New/dispose cannot finish ahead of candidate cleanup.
let resolveCandidate!: (candidate: Session | Promise<Session>) => void,
rejectCandidate!: (error: unknown) => void;
const candidatePromise = new Promise<Session>((resolve, reject) => {
resolveCandidate = resolve;
rejectCandidate = reject;
});
track(
candidatePromise.then((candidate) => {
if (!valid()) return cleanupCandidate(candidate);
return undefined;
})
);
try {
resolveCandidate(factory(threadId));
} catch (error) {
rejectCandidate(error);
}
const candidate = await candidatePromise;
if (!valid()) return;
const methods: Partial<Methods> = {};
let release: (() => void) | undefined,
p: Owner | undefined,
cleanupAccessFailed = false;
try {
if (
candidate === null ||
(typeof candidate !== 'object' && typeof candidate !== 'function')
)
throw TypeError('A native Session is required');
for (const name of [
'getSnapshot',
'subscribe',
'submit',
'resume',
'stop',
'dispose',
] as const) {
let method: unknown;
try {
method = candidate[name];
} catch (error) {
cleanupAccessFailed = name === 'stop' || name === 'dispose';
throw error;
}
if (!valid()) {
void cleanupCandidate(candidate, methods);
return;
}
if (typeof method !== 'function')
throw TypeError('A complete native Session is required');
Object.defineProperty(methods, name, {
value: method,
enumerable: true,
});
}
p = {
candidate,
threadId,
methods: methods as Methods,
seenRuns: new Set(),
closed: false,
transcript: Object.freeze([]),
unsafe: false,
};
const selected = p;
const result = p.methods.subscribe.call(candidate, () => {
if (primary === selected) observe(selected);
});
if (typeof result !== 'function')
throw TypeError('A callable release is required');
release = result;
if (!valid()) {
p.closed = true;
void cleanupCandidate(candidate, methods, release);
return;
}
// Only a complete callable candidate with its release in hand can become authoritative.
p.release = release;
primary = p;
return p;
} catch (error) {
if (p) p.closed = true;
void cleanupCandidate(candidate, methods, release);
// Register owned cleanup before notifying reentrant observers. A later
// successful disposal cannot erase an observed cleanup accessor failure.
if (cleanupAccessFailed) failCleanup();
throw error;
}
}
async function submit(message: string, generation = snapshot.generation) {
if (
disposed ||
cleanupFailed ||
revoking ||
snapshot.busy ||
!snapshot.canSend ||
generation !== snapshot.generation ||
typeof message !== 'string' ||
!message.trim() ||
message.length > 16000
)
return;
const { controller, valid } = claim('running');
let p = primary;
let bootstrapping = true;
try {
if (!valid()) return;
await drain();
if (!valid()) return;
p ??= await install(valid);
if (!p || !valid() || !current(p)) return;
const owner = p,
ownedValid = () => valid() && current(owner);
publish({ threadId: p.threadId });
if (!ownedValid()) return;
const initial = read(p, ownedValid);
if (!ownedValid()) return;
if (
!initial ||
initial.status !== 'idle' ||
initial.decision ||
p.unsafe ||
!same(
initial.transcript.filter((m) => m.subagentRunId === undefined),
p.transcript
)
) {
fail(p, true);
return;
}
bootstrapping = false;
p.previous = initial.run?.id;
p.turn = undefined;
p.intent = 'submit';
p.approval = undefined;
const pending = p.methods.submit.call(p.candidate, message, {
signal: controller.signal,
});
void pending.catch(() => undefined);
if (!ownedValid()) return;
observe(p);
const outcome = await pending;
if (!ownedValid()) return;
const result = capture(p, ownedValid);
if (!ownedValid()) return;
if (outcome !== 'paused' || !result?.approval || p.unsafe) {
fail(p);
return;
}
p.approval = result.approval;
publish({
rows: result.rows,
schedule: result.schedule,
approval: p.approval,
canRespond: true,
canSend: false,
status: 'pending',
});
} catch {
if (valid()) fail(p, bootstrapping);
} finally {
finish(controller);
}
}
async function decide(
approval: MeetingApproval,
action: 'approve' | 'decline',
generation = snapshot.generation
) {
const p = primary;
if (
disposed ||
cleanupFailed ||
revoking ||
snapshot.busy ||
!snapshot.canRespond ||
generation !== snapshot.generation ||
!p ||
p.closed ||
p.unsafe ||
p.approval !== approval ||
snapshot.approval !== approval ||
!p.turn ||
(action !== 'approve' && action !== 'decline')
)
return;
// Claim synchronously before getters, native execution or observer notification.
const { controller, valid } = claim('running'),
ownedValid = () => valid() && current(p);
try {
if (!ownedValid()) return;
const native = read(p, ownedValid),
fresh = native && captureApproval(native, p.turn, ownedValid);
if (!ownedValid()) return;
if (!fresh || !sameApproval(fresh, approval)) {
fail(p);
return;
}
p.intent = 'resume';
p.resumeRunId = undefined;
p.approved = action === 'approve';
const pending = p.methods.resume.call(
p.candidate,
approval.pause,
[
{
interruptId: approval.interruptId,
status: 'resolved',
payload: { approved: p.approved },
},
],
{ signal: controller.signal }
);
void pending.catch(() => undefined);
if (!ownedValid()) return;
observe(p);
const outcome = await pending;
if (!ownedValid()) return;
const result = capture(p, ownedValid);
if (!ownedValid()) return;
if (outcome !== 'success' || !result?.confirmed || p.unsafe) {
fail(p);
return;
}
p.transcript = result.confirmed.transcript;
p.approval = undefined;
publish({
rows: result.rows,
schedule: result.schedule,
approval: undefined,
canRespond: false,
canSend: true,
status: p.approved ? 'approved' : 'declined',
result: result.confirmed.result,
error: null,
});
} catch {
if (ownedValid()) fail(p);
} finally {
finish(controller);
}
}
async function revoke(replacing: boolean) {
if (disposed || cleanupFailed || revoking) return;
revoking = true;
const previous = operation,
p = primary;
operation = undefined;
primary = undefined;
if (p) {
p.closed = true;
p.approval = undefined;
}
// A view notification can dispose while the old owner is already detached.
// Keep that disposal behind preparation until the old cleanup is registered.
let prepared!: () => void;
track(
new Promise<void>((resolve) => {
prepared = resolve;
})
);
// Clear controls and result before abort/release/cleanup can inspect or reenter.
const { controller, valid } = claim(replacing ? 'replacing' : 'running');
publish({
generation: snapshot.generation + 1,
threadId: replacing ? null : snapshot.threadId,
rows: replacing ? Object.freeze([]) : snapshot.rows,
schedule: replacing ? undefined : snapshot.schedule,
approval: undefined,
result: null,
canRespond: false,
canSend: false,
status: replacing ? 'replacing' : 'unconfirmed',
error: replacing
? null
: 'The operation stopped. Start a new conversation.',
});
try {
previous?.abort();
if (p) void close(p);
} finally {
prepared();
}
try {
await drain();
if (valid() && replacing) publish({ canSend: true, status: 'ready' });
} finally {
revoking = false;
finish(controller);
}
}
function dispose(): Promise<void> {
if (disposal) return disposal;
let settled!: () => void;
disposal = new Promise<void>((resolve) => {
settled = resolve;
});
disposed = true;
const previous = operation,
p = primary;
operation = undefined;
primary = undefined;
if (p) {
p.closed = true;
p.approval = undefined;
}
snapshot = Object.freeze({
...snapshot,
generation: snapshot.generation + 1,
busy: false,
canSend: false,
canRespond: false,
approval: undefined,
result: null,
status: 'disposed',
});
listeners.clear();
previous?.abort();
if (p) void close(p);
void drain().then(settled);
return disposal;
}
return {
getSnapshot: () => snapshot,
subscribe(notify: () => void) {
if (disposed) return () => undefined;
listeners.add(notify);
return () => {
listeners.delete(notify);
};
},
submit,
decide,
newConversation: () => revoke(true),
stop: () => revoke(false),
dispose,
};
}Confirmation requires the distinct resumed run, its matching normal root RUN_FINISHED, a settled continuation, and the matching canonical tool result decoded through exactly one JSON layer. Observed backend result displays the tool's exact text separately from assistant prose. HTTP completion, assistant text, a wrong run, an error terminal, or an absent result cannot confirm a meeting. An unconfirmed outcome requires New conversation with no automatic business retry or replay.
Last observed schedule is a separate read-only snapshot. It may remain pending after a sequential booking, or become final when a later batch availability lookup publishes state. Neither snapshot shape is required confirmation evidence. The fictional calendar is process-global, shared across conversations; a new thread does not isolate or reset it.
import { boundedText, ownData, record, type Meeting } from './approval-policy';
export interface ScheduleSnapshot {
readonly availability?: {
readonly day: string;
readonly slots: readonly string[];
};
readonly booking?: Readonly<Partial<Meeting>> & {
readonly status?: 'pending' | 'booked' | 'declined';
};
}
/** Observed shared state is display only; never infer a booking outcome from it. */
export function projectSchedule(state: unknown): ScheduleSnapshot | undefined {
try {
const s = record(ownData(state));
if (!s) return;
const availability = record(s['availability']),
booking = record(s['booking']);
const output: {
availability?: ScheduleSnapshot['availability'];
booking?: ScheduleSnapshot['booking'];
} = {};
if (
availability &&
boundedText(availability['day']) &&
Array.isArray(availability['slots']) &&
availability['slots'].length <= 100 &&
availability['slots'].every((slot) => boundedText(slot))
)
output.availability = Object.freeze({
day: availability['day'],
slots: Object.freeze([...availability['slots']]),
});
if (booking) {
const b: {
topic?: string;
slot?: string;
status?: 'pending' | 'booked' | 'declined';
} = {};
if (boundedText(booking['topic'])) b.topic = booking['topic'];
if (boundedText(booking['slot'])) b.slot = booking['slot'];
if (
['pending', 'booked', 'declined'].includes(booking['status'] as string)
)
b.status = booking['status'] as 'pending' | 'booked' | 'declined';
if (Object.keys(b).length) output.booking = Object.freeze(b);
}
return Object.keys(output).length ? Object.freeze(output) : undefined;
} catch {
return;
}
}Stop and start fresh
Stop revokes current controls. New conversation clears local state and waits for owned cleanup before another Send. Docs, Code, and Run navigation preserves the mounted iframe and editable draft. Page exit or switching frontend disposes the Session. If cleanup fails, controls remain revoked, New conversation stays disabled, and the preview requires a reload. There is no automatic reconnect, decision replay, saved-history restoration, or durable cross-reload guarantee.
Use the shared service
The shared AG-UI route is /runtimes/aws-strands/agent; this frontend is /runtimes/aws-strands/react/. A configured AG-UI Developer runtime is supported. Runtime addresses and credentials stay outside the visible preview.
import { createSession } from '@threadplane/ag-ui';
import { createStrandsApplication } from './application';
// eslint-disable-next-line @nx/enforce-module-boundaries -- The installed build copies this neutral internal bridge.
import type { RuntimeBridgeConfiguration } from '../../../../../libs/cockpit-runtime-bridge/src/index';
export function strandsConnection(configuration: RuntimeBridgeConfiguration): {
readonly url: string;
readonly label: 'Shared runtime' | 'Developer runtime';
} {
if (
configuration.status === 'error' ||
(configuration.status === 'configured' &&
configuration.target.kind === 'langsmith')
)
throw new Error('Runtime configuration failed.');
const target =
configuration.status === 'configured' &&
configuration.target.kind === 'ag-ui'
? configuration.target
: null;
return {
url:
target?.endpoint ??
new URL('/runtimes/aws-strands/agent', window.location.origin).href,
label: target ? 'Developer runtime' : 'Shared runtime',
};
}
export function createConnectedApplication(
connection: ReturnType<typeof strandsConnection>
) {
return createStrandsApplication({
sessionFactory: (threadId) =>
createSession({ threadId, url: connection.url }),
});
}"""Meeting scheduler copilot — AWS Strands backend.
A genuinely non-LangGraph AG-UI backend exercising the neutral Agent
contract surfaces measured in the 2026-08-31 runtime-portability matrix:
- messages: streamed assistant text.
- tool calls: ``check_availability`` executes server-side with no pause.
- shared state: SNAPSHOT-only, and honestly so. The Strands bridge never
emits STATE_DELTA; outbound state exists only where a tool opts in via
per-tool ``ToolBehavior`` hooks (``state_from_result`` on
``check_availability``, ``state_from_args`` on ``book_meeting``), each of
which emits a full STATE_SNAPSHOT. Because our adapter applies snapshots
with replacement semantics, a PARTIAL snapshot would clobber sibling
state keys — so every hook here returns the COMPLETE state object.
- interrupts: ``book_meeting`` parks in ``tool_context.interrupt(...)``;
the bridge finishes the run with the protocol-standard
``RUN_FINISHED.outcome = {type: 'interrupt', interrupts: [...]}`` and
resumes from the client's top-level ``resume`` entries keyed by
``interruptId`` (never the LangGraph bridge's CUSTOM ``on_interrupt``).
- subagents: ``research_availability`` delegates to a tool-less specialist
``availability_researcher`` Agent via an async-generator ``@tool`` that
re-yields the specialist's ``stream_async`` events; a per-tool
``ToolBehavior.tool_stream_event_handler`` (src/subagent_emitter.py)
translates them into standard ``SUBAGENT_*`` + child ``TEXT_MESSAGE_*``
wire events. (The bridge natively drops inner text deltas and would
otherwise route delegation through CUSTOM MultiAgentHandoff + STEP_*;
multi-agent routes also crash the stale PyPI ``ag-ui-strands`` 0.3.0
wheel — which is why this example installs ``ag-ui-strands`` 0.4.1 from PyPI, see
pyproject.toml and docs/wire-capture-subagents.md.)
Model: Strands' native OpenAI provider on plain ``OPENAI_API_KEY`` — no
AWS credentials involved. ``OPENAI_BASE_URL`` is honored, which is how the
aimock e2e harness intercepts model calls. See .env.example.
"""
import json
import os
# Strands wires OpenTelemetry unconditionally and logs exporter noise when
# no collector is listening. Disable the SDK before strands imports. (Env
# override still wins because this is a setdefault; see .env.example.)
os.environ.setdefault("OTEL_SDK_DISABLED", "true")
os.environ.setdefault("OTEL_PYTHON_DISABLED_INSTRUMENTATIONS", "all")
from strands import Agent, tool
from strands.models.openai import OpenAIModel
from strands.types.tools import ToolContext
from ag_ui_strands import StrandsAgent, StrandsAgentConfig, ToolBehavior
from .subagent_emitter import emit_subagent_events
_SLOTS = {
"monday": ["09:00", "13:30"],
"tuesday": ["10:00", "15:00"],
"wednesday": ["11:00"],
"thursday": ["09:30", "14:00", "16:30"],
"friday": ["10:30"],
}
# region demo-state
# Per-process demo state. The Strands bridge is SNAPSHOT-only: every
# outbound state emission replaces the whole frontend state object, so each
# ToolBehavior hook below composes and returns this COMPLETE object rather
# than just the key it changed — a partial return would wipe the sibling
# key. (Module-level state keeps the demo honest and simple; a real
# deployment would key this per thread.)
_state: dict = {"availability": None, "booking": None}
def _complete_state() -> dict:
return {"availability": _state["availability"], "booking": _state["booking"]}
# endregion
# region availability-tool
@tool
def check_availability(day: str) -> dict:
"""Look up the open meeting slots for a weekday.
Args:
day: Weekday name, e.g. 'Tuesday'.
Returns:
A dict with the day and its open slots.
"""
slots = _SLOTS.get(day.strip().lower(), [])
return {"day": day, "slots": slots}
async def availability_state(context) -> dict | None:
"""state_from_result hook: mirror the availability lookup into state."""
result = context.result_data
if isinstance(result, str):
try:
result = json.loads(result)
except ValueError:
return None
if not isinstance(result, dict):
return None
_state["availability"] = {"day": result.get("day"), "slots": result.get("slots", [])}
return _complete_state()
# endregion
# region book-meeting
@tool(context=True)
def book_meeting(topic: str, slot: str, tool_context: ToolContext) -> str:
"""Book a meeting after a human approves it.
Args:
topic: Short description of the meeting purpose.
slot: The chosen slot, e.g. 'Tuesday 10:00'.
Returns:
A confirmation (or rejection) sentence.
"""
answer = tool_context.interrupt(
"book_meeting",
reason={"topic": topic, "slot": slot},
)
payload = answer.get("response") or {}
approved = bool(payload.get("approved")) and not (
answer.get("cancelled") or payload.get("cancelled")
)
_state["booking"] = {"topic": topic, "slot": slot, "status": "booked" if approved else "declined"}
if not approved:
return f"The human declined. Meeting NOT booked: {topic}"
return f"Meeting booked for {slot}: {topic}"
# endregion
# region booking-state
async def booking_state(context) -> dict | None:
"""state_from_args hook: mirror the pending booking into state as the
tool-call arguments finish streaming (before the interrupt pauses the
run), so the approval UI can render from shared state."""
tool_input = context.tool_input
if isinstance(tool_input, str):
try:
tool_input = json.loads(tool_input)
except ValueError:
return None
if not isinstance(tool_input, dict):
return None
_state["booking"] = {
"topic": tool_input.get("topic"),
"slot": tool_input.get("slot"),
"status": "pending",
}
return _complete_state()
# endregion
_RESEARCHER_INSTRUCTIONS = (
"You are an availability researcher. Given attendee names and a date "
"range, produce a short bullet summary of likely availability windows. "
"Be concise: 3 bullets max."
)
# region delegation-tool
@tool
async def research_availability(attendees: str, date_range: str):
"""Delegate availability research for the given attendees to a specialist.
Args:
attendees: Comma-separated attendee names, e.g. 'Ada, Grace'.
date_range: The window to research, e.g. 'next week'.
Returns:
The specialist's bullet summary of likely availability windows.
"""
chunks: list[str] = []
try:
async for event in availability_researcher.stream_async(
f"Attendees: {attendees}\nDate range: {date_range}"
):
if isinstance(event, dict) and isinstance(event.get("data"), str):
chunks.append(event["data"])
yield event
except Exception as exc: # pragma: no cover - not reachable without a live model failure
# Surface the failure to the emitter (which owns the SUBAGENT_ERROR
# wire event), then let the tool error propagate to Strands normally.
yield {"delegation_error": str(exc)}
raise
# Strands takes the LAST yielded value as the tool result.
yield "".join(chunks)
# endregion
_INSTRUCTIONS = """You are a meeting scheduling copilot.
When the user asks to book a meeting:
1. FIRST call `check_availability` with the requested weekday.
2. THEN call `book_meeting` with the meeting topic and ONE chosen slot in
the form '<Day> <time>' (e.g. 'Tuesday 10:00'). Do not output prose
alongside the `book_meeting` call — the user will be shown an approval
card.
3. After the human approves and the tool returns, confirm in one short
sentence what was booked, naming the topic and slot.
If the human declines, acknowledge in one short sentence that nothing was
booked.
Keep every response brief and factual. Never invent availability — use the
tool.
When the user asks about attendees' availability, delegate that research to
the `research_availability` tool before proposing meeting slots.
"""
# region model
def build_model() -> OpenAIModel:
"""Strands' native OpenAI provider — plain OPENAI_API_KEY, no AWS creds.
A placeholder key fails properly at request time (401) instead of at
module import. OPENAI_BASE_URL redirects the client (used by the aimock
e2e harness to replay fixtures).
"""
client_args: dict = {"api_key": os.environ.get("OPENAI_API_KEY", "unset-openai-api-key")}
base_url = os.environ.get("OPENAI_BASE_URL")
if base_url:
client_args["base_url"] = base_url
return OpenAIModel(client_args=client_args, model_id=os.environ.get("OPENAI_CHAT_MODEL", "gpt-4o-mini"))
# endregion
# region specialist
# Tool-less specialist the orchestrator delegates availability research to
# via the `research_availability` async-generator tool above. Its streamed
# events cross the bridge as tool_stream_events and are translated into
# SUBAGENT_* wire events by the emitter registered in ToolBehavior below.
availability_researcher = Agent(
model=build_model(),
system_prompt=_RESEARCHER_INSTRUCTIONS,
name="availability_researcher",
tools=[],
)
# endregion
# region agent-config
agent = StrandsAgent(
agent=Agent(
model=build_model(),
system_prompt=_INSTRUCTIONS,
tools=[check_availability, book_meeting, research_availability],
),
name="aws-strands",
description="Books meetings with availability lookup, shared state, and human approval.",
config=StrandsAgentConfig(
tool_behaviors={
"check_availability": ToolBehavior(state_from_result=availability_state),
"book_meeting": ToolBehavior(state_from_args=booking_state),
"research_availability": ToolBehavior(
tool_stream_event_handler=emit_subagent_events,
),
},
),
)
# endregionfrom fastapi import FastAPI
from ag_ui_strands import add_strands_fastapi_endpoint
from .agent import agent
app = FastAPI(title="cockpit-runtimes-aws-strands")
add_strands_fastapi_endpoint(app, agent, "/agent")
@app.get("/ok")
def ok() -> dict:
return {"ok": True}The required backend helper is included for source completeness; its research functionality is outside this React slice.
"""SUBAGENT_* emitter for the `research_availability` delegation tool.
The Strands bridge natively drops a child agent's text deltas: an
async-generator `@tool` re-yielding a specialist's ``stream_async`` events
produces ``tool_stream_event``s, but `_forward_inner_agent_events` forwards
only the inner TOOL-CALL lifecycle and never inner text (measured in
docs/wire-capture-subagents.md). Registering this
``ToolBehavior.tool_stream_event_handler`` claims the whole child stream and
re-emits it as standard AG-UI subagent wire events:
SUBAGENT_STARTED (first inner event)
TEXT_MESSAGE_START/CONTENT.../END (inner ``data`` deltas, streamed)
SUBAGENT_FINISHED outcome=success (inner terminal ``result`` event)
or ``SUBAGENT_ERROR`` when the delegation fails (the tool yields a
``delegation_error`` sentinel before re-raising, or the inner stream
force-stops). Ids derive from the wire ``toolCallId`` (``ctx.tool_use_id``)
so the client can key the subagent card on ``parentToolCallId`` with zero
bookkeeping.
The bridge instantiates this handler ONCE PER EVENT (a fresh async generator
per ``tool_stream_event``), so per-invocation lifecycle state lives in a
module-level dict keyed by ``tool_use_id``. The encoder requires pydantic
``BaseEvent`` instances — raw dicts crash the stream — so only typed
``ag_ui.core`` events are yielded.
"""
from dataclasses import dataclass
from ag_ui.core import (
EventType,
SubagentErrorEvent,
SubagentFinishedEvent,
SubagentFinishedSuccessOutcome,
SubagentStartedEvent,
TextMessageContentEvent,
TextMessageEndEvent,
TextMessageStartEvent,
)
from ag_ui_strands import ToolStreamEventContext
SPECIALIST_NAME = "availability_researcher"
@dataclass
class _DelegationState:
"""Lifecycle of one delegation call, keyed by tool_use_id."""
started: bool = False
message_open: bool = False
finished: bool = False
generation: int = 1
"""Bumped when a reused tool_use_id starts a fresh inner stream, so the
re-run's message id (-m2, -m3, ...) never collides with an already-emitted
one."""
# Finished entries are kept (not popped) so the tool's trailing result-string
# yield and any stragglers stay suppressed. Growth is capped at _MAX_SESSIONS
# (dict insertion order = age; eviction prefers finished sessions, see
# _eviction_candidate; each entry is a short key string plus a 4-field
# dataclass, ~414 bytes measured with sys.getsizeof — the cap bounds the dict
# at ~200 KiB).
_sessions: dict[str, _DelegationState] = {}
_MAX_SESSIONS = 512
def _eviction_candidate(current: str) -> str:
"""Pick the session to drop when the cap is exceeded: the oldest FINISHED
session first (its only job is straggler suppression); only when every
other session is still in flight, the oldest in-flight one — evicting an
unfinished session makes its next event re-emit SUBAGENT_STARTED, so that
is the last resort that keeps the cap a hard memory bound. Never the
session being registered right now."""
oldest_unfinished: str | None = None
for key, state in _sessions.items():
if key == current:
continue
if state.finished:
return key
if oldest_unfinished is None:
oldest_unfinished = key
assert oldest_unfinished is not None # cap > 1, so another key exists
return oldest_unfinished
def _subagent_run_id(tool_use_id: str) -> str:
return f"{tool_use_id}-sub"
def _message_id(tool_use_id: str, generation: int) -> str:
return f"{tool_use_id}-sub-m{generation}"
async def emit_subagent_events(ctx: ToolStreamEventContext):
"""tool_stream_event_handler translating child events to SUBAGENT_* wire
events. Async generator, called once per inner event."""
state = _sessions.get(ctx.tool_use_id)
if state is None:
state = _DelegationState()
_sessions[ctx.tool_use_id] = state
while len(_sessions) > _MAX_SESSIONS:
del _sessions[_eviction_candidate(ctx.tool_use_id)]
run_id = _subagent_run_id(ctx.tool_use_id)
data = ctx.stream_data
if state.finished and isinstance(data, dict) and "init_event_loop" in data:
# Reused tool_use_id (real for some Strands providers — see the
# bridge's _reused_frontend_tool_identity_error): a fresh inner
# stream always opens with init_event_loop, so reset for a second
# full SUBAGENT_* sequence under the same subagent_run_id (the
# adapter treats an identity-unchanged re-announce as content-only).
# Non-init stragglers after the terminal stay swallowed below.
state = _DelegationState(generation=state.generation + 1)
_sessions[ctx.tool_use_id] = state
message_id = _message_id(ctx.tool_use_id, state.generation)
try:
if state.finished:
# The tool's final yield (the result string) and any stragglers
# arrive after the inner terminal event — nothing left to emit.
return
if not state.started:
state.started = True
yield SubagentStartedEvent(
type=EventType.SUBAGENT_STARTED,
subagent_run_id=run_id,
name=SPECIALIST_NAME,
parent_tool_call_id=ctx.tool_use_id,
)
if isinstance(data, str):
# Terminal-success fallback: the tool's final result-string yield
# arriving on an UNFINISHED session means the inner stream ended
# without a {"result": ...} event — close out rather than leaving
# the subagent card open forever.
state.finished = True
if state.message_open:
state.message_open = False
yield TextMessageEndEvent(
type=EventType.TEXT_MESSAGE_END,
message_id=message_id,
subagent_run_id=run_id,
)
yield SubagentFinishedEvent(
type=EventType.SUBAGENT_FINISHED,
subagent_run_id=run_id,
outcome=SubagentFinishedSuccessOutcome(),
)
return
if not isinstance(data, dict):
return
if "delegation_error" in data or data.get("force_stop"):
message = str(
data.get("delegation_error")
or data.get("force_stop_reason")
or "subagent stream force-stopped"
)
state.finished = True
if state.message_open:
state.message_open = False
yield TextMessageEndEvent(
type=EventType.TEXT_MESSAGE_END,
message_id=message_id,
subagent_run_id=run_id,
)
yield SubagentErrorEvent(
type=EventType.SUBAGENT_ERROR,
subagent_run_id=run_id,
message=message,
)
elif isinstance(data.get("data"), str) and data["data"]:
if not state.message_open:
state.message_open = True
yield TextMessageStartEvent(
type=EventType.TEXT_MESSAGE_START,
message_id=message_id,
role="assistant",
subagent_run_id=run_id,
)
yield TextMessageContentEvent(
type=EventType.TEXT_MESSAGE_CONTENT,
message_id=message_id,
delta=data["data"],
subagent_run_id=run_id,
)
elif "result" in data:
state.finished = True
if state.message_open:
state.message_open = False
yield TextMessageEndEvent(
type=EventType.TEXT_MESSAGE_END,
message_id=message_id,
subagent_run_id=run_id,
)
yield SubagentFinishedEvent(
type=EventType.SUBAGENT_FINISHED,
subagent_run_id=run_id,
outcome=SubagentFinishedSuccessOutcome(),
)
except Exception as exc: # pragma: no cover - defensive: never crash the run
state.finished = True
yield SubagentErrorEvent(
type=EventType.SUBAGENT_ERROR,
subagent_run_id=run_id,
message=str(exc),
)Build and verify from source
npx nx build cockpit-runtimes-aws-strands-react
npx nx e2e cockpit-runtimes-aws-strands-reactThe e2e target prepares frozen Python 3.12 dependencies before running the unchanged generated Strands service against a deterministic offline OpenAI model seam. The owned fixture checks actual HTTP/SSE native pause/resume and canonical results and closes its temporary processes and dependencies. This proves local interoperability, without proving a live model-provider request. Docs, Code, and Run select this same authored implementation.
{
"name": "cockpit-runtimes-aws-strands-react",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "cockpit/runtimes/aws-strands/react/src",
"projectType": "application",
"tags": ["scope:cockpit-examples", "scope:cockpit-e2e", "framework:react"],
"targets": {
"prepare-fixture": {
"executor": "nx:run-commands",
"cache": false,
"options": {
"command": "node scripts/react-cockpit/prepare-native-strands-fixture.mjs"
}
},
"e2e": {
"executor": "@nx/playwright:playwright",
"cache": false,
"inputs": ["default", "awsStrandsFixture"],
"dependsOn": ["prepare-fixture", "build"],
"options": {
"config": "cockpit/runtimes/aws-strands/react/e2e/playwright.config.ts",
"skipInstall": true
}
},
"test": {
"executor": "@nx/vitest:test",
"options": {
"configFile": "cockpit/runtimes/aws-strands/react/vite.config.mts"
}
},
"lint": { "executor": "@nx/eslint:lint" },
"build": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": [
{ "target": "build", "projects": ["core", "content", "react"] },
{ "target": "react-ag-ui", "projects": ["website-runtime-preparation"] }
],
"outputs": ["{workspaceRoot}/dist/cockpit/runtimes/aws-strands/react"],
"options": {
"command": "node scripts/react-cockpit/build.mjs runtimes-aws-strands"
}
}
},
"namedInputs": {
"awsStrandsFixture": [
"{workspaceRoot}/fixtures/react-parity/native-strands-react/**/*",
"{workspaceRoot}/scripts/react-cockpit/native-strands-fixture.mjs",
"{workspaceRoot}/scripts/react-cockpit/native-strands-fixture.spec.mjs",
"{workspaceRoot}/scripts/react-cockpit/prepare-native-strands-fixture.mjs",
"{workspaceRoot}/scripts/react-cockpit/prepare-native-strands-fixture.spec.mjs"
]
}
}