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
11 changes: 11 additions & 0 deletions .changeset/effect-state-kernel-stage1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@agent-bundle/runtime": patch
---

Rewrite the state-kernel driver internals on Effect v4 behind the unchanged
public API: `Scope`/`Layer` own sqlite connection and BEGIN IMMEDIATE
transaction lifecycles (`acquireUseRelease` commit/rollback), and the kernel's
fail-closed states ride a typed `AgentStateError` error channel mapped back at
the boundary module. `defineState`/`dispatch`/`read`/`changes`/`reset` still
return the same Promise shapes and reject with the same typed errors; root and
plugin entries still ship zero kernel or effect bytes.
18 changes: 18 additions & 0 deletions agent-patterns/effect-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ const connection = Effect.acquireRelease(

Finalizers run in reverse acquire order. Interruption still runs them.

## Transactions (stage-1 kernel idiom)

`Effect.acquireUseRelease` when begin/commit/rollback are one unit and the
release step branches on the exit:

```ts
Effect.acquireUseRelease(
begin, // BEGIN IMMEDIATE
() => work,
(db, exit) => Exit.isFailure(exit)
? rollback
: commit.pipe(Effect.catch((e) => rollback.pipe(Effect.andThen(Effect.fail(e))))),
);
```

A failed COMMIT must still roll back before re-raising, or the connection
holds the transaction open for the next caller.

## Layers

- `Layer.effect` for a service with no finalizer.
Expand Down
19 changes: 12 additions & 7 deletions examples/rsc-agent-runtime/tests/host-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,18 +313,23 @@ test('runs each packaged native hook from one shell argv path when its plugin ro

test('keeps the published Agent Bundle package free of the supplemental RSC runtime', async () => {
const packageRoot = join(exampleRoot, '../../packages/agent-bundle');
const packageJson = await readJson<{ dependencies?: Record<string, string>; optionalDependencies?: Record<string, string>; peerDependencies?: Record<string, string> }>(
const packageJson = await readJson<{ dependencies?: Record<string, string>; optionalDependencies?: Record<string, string>; peerDependencies?: Record<string, string>; peerDependenciesMeta?: Record<string, { optional?: boolean }> }>(
join(packageRoot, 'package.json'),
);
const allDependencies = {
// Install-cost guard: hook-only consumers must never be forced to install
// the RSC runtime stack. Optional peers (declared for the #103 test
// harness) add no install cost, so they are allowed only when
// peerDependenciesMeta marks them optional.
const requiredDependencies = {
...packageJson.dependencies,
...packageJson.optionalDependencies,
...packageJson.peerDependencies,
};

expect(allDependencies).not.toHaveProperty('react');
expect(allDependencies).not.toHaveProperty('react-server-dom-rspack');
expect(allDependencies).not.toHaveProperty('rsbuild-plugin-rsc');
for (const name of ['react', 'react-server-dom-rspack', 'rsbuild-plugin-rsc']) {
expect(requiredDependencies).not.toHaveProperty(name);
if (packageJson.peerDependencies?.[name] !== undefined) {
expect(packageJson.peerDependenciesMeta?.[name]?.optional, `${name} peer must be optional`).toBe(true);
}
}

const sourceRoot = join(packageRoot, 'src');
const sourceFiles = await readdir(sourceRoot, { recursive: true });
Expand Down
22 changes: 22 additions & 0 deletions packages/rsc-runtime/src/state/effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Effect } from 'effect';

import { AgentStateError } from './contract.js';

/**
* Lifts synchronous kernel work into the typed state error channel.
* Expected fail-closed conditions remain AgentStateError failures; any other
* throw is an implementation defect and stays in Effect's defect channel.
*/
export const stateEffect = <A>(
evaluate: () => A,
): Effect.Effect<A, AgentStateError> =>
Effect.try({
catch: (error) => error,
try: evaluate,
}).pipe(
Effect.catch((error) =>
error instanceof AgentStateError
? Effect.fail(error)
: Effect.die(error),
),
);
Loading
Loading