npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kstackz/ai-toolkit

v0.0.0

Published

Effect-native runtime for stateful coding-agent harnesses whose Messages live in a StdTable

Readme

@kstackz/ai-toolkit

Effect-native runtime for stateful coding-agent harnesses whose Messages live in a StdTable

Big picture

Claude Code and Codex turns run for minutes, pause for questions and approvals, and must outlive the browser tab that started them. Streaming the answer back over the request that started it ties the Run to one connection. ai-toolkit removes that coupling: a client starts a Run over RPC and gets nothing back but void; every fact about the Run lands as an immutable Message row in the AI Table, and clients read the table through whatever feed they already have.

The package builds on @kstackz/std-toolkit. The AI Table is a StdTable with threads, runs, and messages entities, so any std-toolkit database adapter (memory, SQLite, DynamoDB, IndexedDB) can store it, and @kstackz/std-toolkit/sync can mirror it into a browser. The playground uses @kstackz/rpc-toolkit/rpc/websocket-client to keep its subscriptions alive; a production app can pair the same RPC with its own transport and feed.

Vocabulary is in CONTEXT.md, the decisions are in docs/adr/, and the lifecycle of one turn, the table layout, and the harness SDK boundaries are in docs/turn-flow.md.

Install

pnpm add @kstackz/ai-toolkit effect
  • effect (peer, required): the RPC contract, layers, schemas, and the StdTable service all come from Effect.

@kstackz/std-toolkit, @anthropic-ai/claude-agent-sdk, @openai/codex, and @effect/platform-node are regular dependencies and install with the package. Node 24 or newer is required.

Exports

@kstackz/ai-toolkit/rpc

| Export | What it does | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------- | | AiRpc | The public RPC group: cancelRun, claudeStart, claudeRespond, codexStart, codexRespond. | | CommonRpc | Group holding cancelRun alone. | | ClaudeRpc | Group holding claudeStart and claudeRespond. | | CodexRpc | Group holding codexStart and codexRespond. | | AiRpcError | Schema union of every error the RPC group can return. | | RunConflict | Error: a Run with this runId already exists with different input. | | ThreadBusy | Error: the Thread already has an Active Run. | | ThreadNotFound | Error: no Thread with this id. | | RunNotFound | Error: no Run with this id. | | RequestNotFound | Error: the Run has no pending Interaction Request with this id. | | HarnessFailed | Error: the harness could not start or ended abnormally. | | CLAUDE_MODELS | Tuple of Claude model ids accepted by claudeStart. | | CODEX_MODELS | Tuple of Codex model ids accepted by codexStart. | | RUN_STATUSES | Tuple of Run statuses: running, waiting, completed, failed, cancelled. | | THREAD_STATUSES | Tuple of Thread statuses: idle, running, waiting-question, waiting-approval, cancelled, failed. | | COMMON_PARTS | Names of custom parts shared by every harness. | | COMMON_PARTS.ERROR | Part name agent.error, an error surfaced during the Run. | | COMMON_PARTS.PERMISSION_REQUEST | Part name agent.permission.request, an approval the harness is waiting on. | | COMMON_PARTS.PERMISSION_RESOLVED | Part name agent.permission.resolved, the Resolution to an approval. | | COMMON_PARTS.QUESTION | Part name agent.question, a question the harness asked the user. | | COMMON_PARTS.FILE_CHANGED | Part name agent.file.changed, a file the harness edited. | | CLAUDE_PARTS | Names of Claude-only custom parts. | | CLAUDE_PARTS.SUBAGENT | Part name claude.subagent, a subagent's activity. | | CLAUDE_PARTS.COMPACTION | Part name claude.compaction, a context compaction event. | | CODEX_PARTS | Names of Codex-only custom parts. | | CODEX_PARTS.PLAN | Part name codex.plan, a plan update. | | CODEX_PARTS.COMMAND | Part name codex.command, a shell command Codex ran or wants to run. | | CODEX_PARTS.MCP | Part name codex.mcp, an MCP tool call. | | CODEX_PARTS.REQUEST_RESOLVED | Part name codex.request.resolved, the Resolution to a Codex request. | | CUSTOM_PART_NAMES | Tuple of every custom part name across the three maps above. |

@kstackz/ai-toolkit/rpc/live

| Export | What it does | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | AiRpcLive | Server-side handler bundle for AiRpc. | | AiRpcLive.layer | Layer of AiRpc handlers; takes an optional HarnessHostConfig and requires the AI Table's StdTableService<'ai-toolkit'>. | | HarnessHost | Service with start, cancel, resolveClaude, and resolveCodex, for callers that skip RPC. | | HarnessHost.layer | Layer that builds the host, runs the Bootstrap Sweep, and cancels live Runs on shutdown; accepts a config and optional harness runners. |

@kstackz/ai-toolkit/table

| Export | What it does | | --------------- | ---------------------------------------------------------------------------------------- | | aiTable | The StdTable named @kstackz/ai-toolkit with its primary key and GSI1 to GSI5. | | threads | Thread entity with a byUpdate feed on GSI2. | | runs | Run entity keyed under its Thread, with byThreadUpdate on GSI1 and byUpdate on GSI2. | | messages | Message entity keyed under its Run, with byThreadUpdate on GSI1. | | ThreadSchema | Entity schema for a Thread row. | | RunSchema | Entity schema for a Run row. | | MessageSchema | Entity schema for a Message row. | | MESSAGE_ROLES | Tuple of Message roles: system, user, assistant. |

@kstackz/ai-toolkit/client

| Export | What it does | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | toUiMessages | Folds ordered Message rows of one Run and role into turns and joins streamed text and thinking. | | toUiConversation | Folds rows into turns, pairs each question with its Resolution and tool activity, and groups non-prose parts into activity blocks. |

@kstackz/ai-toolkit/playground

| Export | What it does | | ----------------------- | -------------------------------------------------------------------------------------------------------------- | | AiPlaygroundRpc | Demo-only group: createThread, subscribeThreads, subscribeMessages. | | AiPlaygroundServerRpc | AiRpc merged with AiPlaygroundRpc; the group the Playground Server serves. | | PlaygroundFailed | Error returned by the playground calls. | | makePlaygroundSync | Builds threads and messages synced collections over a playground client using @kstackz/std-toolkit/sync. |

The @kstackz/ai-toolkit binary exposes ai-toolkit serve --port 3001, which runs the Playground Server against the current directory.

Usage

Serve the AI RPC over HTTP with in-memory storage

AiRpcLive.layer() needs one thing from the application: the AI Table's StdTable service, built from aiTable with any std-toolkit adapter. Trimmed from src/cli/playground-server.ts.

import { createServer } from 'node:http';
import { Layer } from 'effect';
import { HttpRouter } from 'effect/unstable/http';
import { RpcSerialization, RpcServer } from 'effect/unstable/rpc';
import { NodeHttpServer, NodeServices } from '@effect/platform-node';
import { defaultBroadcaster } from '@kstackz/std-toolkit/core';
import { Memory } from '@kstackz/std-toolkit/db/memory';
import { AiRpc } from '@kstackz/ai-toolkit/rpc';
import { AiRpcLive } from '@kstackz/ai-toolkit/rpc/live';
import { aiTable } from '@kstackz/ai-toolkit/table';

const storage = Layer.merge(Memory.make(aiTable).layer, defaultBroadcaster);

const handlers = AiRpcLive.layer({ hostId: 'my-host' }).pipe(
  Layer.provide(storage),
);

const rpc = RpcServer.layerHttp({ group: AiRpc, path: '/rpc' }).pipe(
  Layer.provide(handlers),
  Layer.provide(RpcSerialization.layerJson),
);

export const server = HttpRouter.serve(rpc).pipe(
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3001 })),
  Layer.provide(NodeServices.layer),
);
  • Every RPC returns void. Threads, Runs, and Messages appear in the table; nothing is observed through the RPC.
  • Thread creation stays with the application: insert into threads yourself. The playground adds createThread only for the demo.
  • Building the layer runs the Bootstrap Sweep, and closing its scope cancels every live Run.
  • Codex needs the codex binary on PATH, or HarnessHostConfig.codexCommand.

Run a turn and read its Messages from the table

HarnessHost can be driven without RPC. Insert a Thread, start a Run, then read what the Transcript persisted. Lifted from src/runtime/host/host.test.ts.

import { Effect, Layer } from 'effect';
import { defaultBroadcaster } from '@kstackz/std-toolkit/core';
import { Memory } from '@kstackz/std-toolkit/db/memory';
import { HarnessHost } from '@kstackz/ai-toolkit/rpc/live';
import { aiTable, messages, runs, threads } from '@kstackz/ai-toolkit/table';

const storage = Layer.merge(Memory.make(aiTable).layer, defaultBroadcaster);
const host = HarnessHost.layer({ hostId: 'test' }).pipe(
  Layer.provideMerge(storage),
);

const program = Effect.gen(function* () {
  const harness = yield* HarnessHost;
  yield* threads.insert({
    id: 't1',
    harness: 'claude',
    cwd: '/tmp',
    status: 'idle',
    activeRunId: null,
    data: { type: 'claude', sessionId: null },
  });
  yield* harness.start({
    harness: 'claude',
    threadId: 't1',
    runId: 'r1',
    message: { id: 'u1', content: 'hi' },
    model: 'claude-sonnet-4-6',
    options: {},
  });
  const run = yield* runs.get({ threadId: 't1', id: 'r1' });
  const stored = yield* messages.query(
    'byThreadUpdate',
    { pk: { threadId: 't1' }, '>=': null },
    { limit: 10 },
  );
  return { run: run?.value, rows: stored.items.map((item) => item.value) };
}).pipe(Effect.provide(host), Effect.scoped);
  • start returns as soon as the Run and user Message are inserted; poll runs.get until finishedAt is set, or watch messages.byThreadUpdate.
  • Run configuration (model, thinking, permission mode) is stored on each Run, so later turns on the same Thread may pick different settings.
  • The test swaps the real runners for a stub through HarnessHost.layer(config, { claude, codex }).

Mirror the playground into a browser and render turns

The docs demo connects over WebSocket, syncs threads and messages into live collections, and folds rows into a conversation. Trimmed from apps/docs/src/demos/ai-toolkit/client/client.ts.

import { Context, Effect, Layer, Scope } from 'effect';
import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
import { toUiConversation } from '@kstackz/ai-toolkit/client';
import {
  AiPlaygroundServerRpc,
  makePlaygroundSync,
} from '@kstackz/ai-toolkit/playground';
import {
  layerWebSocketProtocol,
  RpcConnection,
} from '@kstackz/rpc-toolkit/rpc/websocket-client';

const boot = Effect.gen(function* () {
  const context = yield* Layer.build(
    layerWebSocketProtocol({
      url: 'ws://127.0.0.1:3001/rpc',
      serialization: RpcSerialization.layerJson,
    }),
  );
  const connection = Context.get(context, RpcConnection);
  const api = yield* RpcClient.make(AiPlaygroundServerRpc).pipe(
    Effect.provide(
      Layer.succeed(
        RpcClient.Protocol,
        Context.get(context, RpcClient.Protocol),
      ),
    ),
  );
  const sync = makePlaygroundSync({
    api,
    keepSubscribed: connection.keepSubscribed,
    name: 'ai-toolkit-playground',
  });
  return { api, sync };
});

const { api, sync } = await Effect.runPromise(
  boot.pipe(Effect.provideService(Scope.Scope, Scope.makeUnsafe())),
);

const runId = crypto.randomUUID();
await Effect.runPromise(
  api.claudeStart({
    threadId,
    runId,
    message: { id: crypto.randomUUID(), content: 'hello' },
    model: 'claude-sonnet-4-6',
    options: {},
  }),
);

// later, with rows from sync.messages ordered by createdAt
const conversation = toUiConversation(rows);
  • subscribeThreads and subscribeMessages page from a cursor, so a reconnect resumes where the collection left off.
  • keepSubscribed from rpc-toolkit restarts each subscription after the socket reconnects.
  • toUiConversation joins streamed text split across rows and pairs questions with their Resolutions; sync.messages rows must be ordered by createdAt first.
  • sync.dispose() tears the collections down.