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
10 changes: 10 additions & 0 deletions .changeset/request-store-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@agent-bundle/rsc-runtime": minor
---

Add the versioned realm-singleton request store, `await agent()`, and
Observed identities (#95 Wave 1). MCP and CLI entrypoints install a closed
request lease so `execute` can read the same `AgentInvocation` (kind
`tool` | `event` | `cli` | `script` | `workbench`) without a daemon or
durable state. `state`, `notices`, and `providers` are reserved extension
slots; a captured handle throws after the request completes.
1 change: 0 additions & 1 deletion docs/architecture/rsc-runtime-workbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ examples/
src/rsc/routes.tsx
src/rsc/worker.tsx
src/runtime/contracts.ts
src/runtime/request-context.ts
src/runtime/state-file-core.ts
src/runtime/state-file-test-support.ts
src/runtime/state-file.ts
Expand Down
13 changes: 7 additions & 6 deletions examples/rsc-agent-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ Native hooks are fresh requests: a process normalizes one host event, invokes th

```tsx
// A Hook JSX route reads request-scoped context.
import { Hook } from '@agent-bundle/rsc-runtime';
import { useEdit, useRuntimeSnapshot } from '../runtime/request-context.js';
import { Hook, agent } from '@agent-bundle/rsc-runtime';
import type { CanonicalPostToolUse, RuntimeSnapshot } from '../runtime/contracts.js';

export function AfterFileEdit() {
const edit = useEdit();
const snapshot = useRuntimeSnapshot();
export async function AfterFileEdit() {
const context = await agent();
const edit = context.services.edit as CanonicalPostToolUse;
const snapshot = context.services.snapshot as RuntimeSnapshot;
return (
<Hook.Result>
<Hook.AdditionalContext>
Recorded {edit.path}; {snapshot.edits.length} edits exist.
{`Recorded ${edit.path}; ${snapshot.edits.length} edits exist.`}
</Hook.AdditionalContext>
</Hook.Result>
);
Expand Down
23 changes: 17 additions & 6 deletions examples/rsc-agent-runtime/src/rsc/components.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { basename } from 'node:path';

import { Hook, Mcp } from '@agent-bundle/rsc-runtime';
import type { RuntimeSnapshot } from '../runtime/contracts.js';
import { useEdit, useRuntimeSnapshot } from '../runtime/request-context.js';
import { Hook, Mcp, agent } from '@agent-bundle/rsc-runtime';
import type { CanonicalPostToolUse, RuntimeSnapshot } from '../runtime/contracts.js';

export const AfterFileEdit = () => {
const edit = useEdit();
const snapshot = useRuntimeSnapshot();
const hookServices = async (): Promise<{ edit: CanonicalPostToolUse; snapshot: RuntimeSnapshot }> => {
const context = await agent();
const edit = context.services.edit;
const snapshot = context.services.snapshot;
if (edit === undefined || snapshot === undefined) {
throw new Error('Hook render requires edit and snapshot services');
}
return {
edit: edit as CanonicalPostToolUse,
snapshot: snapshot as RuntimeSnapshot,
};
};

export const AfterFileEdit = async () => {
const { edit, snapshot } = await hookServices();
const editCount = snapshot.stateVersion;
const editNoun = editCount === 1 ? 'edit' : 'edits';

Expand Down
22 changes: 19 additions & 3 deletions examples/rsc-agent-runtime/src/rsc/worker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { finished } from 'node:stream/promises';
import { resolve } from 'node:path';
import { writeSync } from 'node:fs';

import { available, runAgentRequest } from '@agent-bundle/rsc-runtime';
import { renderToReadableStream } from 'react-server-dom-rspack/server.node';

import type { CanonicalPostToolUse, RenderRequest, RuntimeSnapshot } from '../runtime/contracts.js';
import { withRenderContext } from '../runtime/request-context.js';
import { createFileRuntimeKernel } from '../runtime/state-file.js';
import { renderRoute } from './routes.js';

Expand Down Expand Up @@ -135,9 +135,25 @@ const render = async (): Promise<void> => {
};

if (request.type === 'hook/after-file-edit') {
await withRenderContext({ edit: request.event, snapshot }, renderFlight);
await runAgentRequest({
host: available({ name: request.event.host }, 'native'),
invocation: {
id: request.event.idempotencyKey,
kind: 'event',
surface: request.type,
},
session: available({ sessionId: request.event.sessionId }, 'native'),
services: { edit: request.event, snapshot },
workspace: available({ root: request.event.cwd }, 'native'),
}, renderFlight);
} else {
await renderFlight();
await runAgentRequest({
invocation: {
kind: 'tool',
surface: request.type,
},
services: { snapshot },
}, renderFlight);
}
writeSnapshotMetadata();
};
Expand Down
17 changes: 0 additions & 17 deletions examples/rsc-agent-runtime/src/runtime/request-context.ts

This file was deleted.

16 changes: 12 additions & 4 deletions packages/rsc-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,18 @@ const result = lowerMcpResult(
);
```

The package exports `Hook`, `Mcp`, `lowerHookResult`, `lowerMcpResult`, and
`createRscRequestContext`. It does not own an RSC renderer, application state,
transport, persistence, or host packaging. React 19 is a peer dependency and Node
22.19 or newer is required.
The package exports `Hook`, `Mcp`, `lowerHookResult`, `lowerMcpResult`,
`createRscRequestContext`, `agent`, `runAgentRequest`, and `AgentRequestError`. It does not own an
RSC renderer, application state, transport, persistence, or host packaging.
React 19 is a peer dependency and Node 22.19 or newer is required.

Async server utilities and Server Components read the framework request store
with `const context = await agent()`. The store is a versioned realm singleton
installed at MCP and CLI entrypoints (and at any other real invocation via
`runAgentRequest`). Identities are `Observed<T>` — unavailable host, session,
actor, or workspace is a typed reason, never a fabricated string. The context
handle throws after the request completes. `state`, `notices`, and `providers`
are reserved extension slots; provider discovery and `useAgent()` arrive later.

Structured MCP metadata and content are copied through a strict finite-JSON
boundary before being returned, so later caller mutations do not alter a result.
Expand Down
Loading
Loading