A small family of MIT packages for driving a local agent (Codex App Server now; Kimi/Qwen/Gemini/Grok over ACP next) and presenting a consistent chat/tool experience behind one neutral schema. Extracted from PwrSnap and PwrAgnt so downstream apps consume it instead of re-implementing the transport, discovery, turn loop, and chat surface.
| Package | Role |
|---|---|
@pwrdrvr/agent-core |
Neutral message/thread/tool-call schema + injected interfaces (Logger, ThreadStore, Clock, OpenExternal). The dependency hub. Zero runtime deps. |
@pwrdrvr/agent-transport |
JSON-RPC 2.0 core + stdio transport. (planned) |
@pwrdrvr/codex-discovery |
Codex binary discovery, version compare, auth profiles, login/relogin. (planned) |
@pwrdrvr/agent-client |
Codex App Server adapter: thread client + chat controller + defineTool, normalizing into agent-core. (planned) |
@pwrdrvr/agent-acp |
ACP adapter for Kimi/Qwen/Gemini/Grok, normalizing into agent-core. (planned) |
@pwrdrvr/agent-chat-react |
Presentational chat UI (React peer dep). (planned) |
The Codex protocol types live in a separate repo,
@pwrdrvr/codex-app-server-protocol,
consumed as a normal versioned dependency.
Codex App Server protocol 0.144.0 represents dynamic tools as a discriminated
union. Use buildToolCatalog() for the dynamicTools/tools value sent when a
thread starts; it emits unnamespaced tools as top-level { type: "function" }
objects and groups tools with the same ToolSpec.namespace into one
{ type: "namespace", tools: [...] } object.
import {
buildToolCatalog,
defineTool,
toDynamicToolFunctionSpec
} from "@pwrdrvr/agent-client";
const tools = [
defineTool({
namespace: "capture_library",
name: "list",
description: "List captures.",
argsSchema,
deferLoading: true,
dispatch
})
];
const codexDynamicTools = buildToolCatalog(tools);
// Flat consumers such as an MCP bridge can retain the owner separately.
const mcpTools = tools.map((tool) => ({
namespace: tool.namespace,
...toDynamicToolFunctionSpec(tool)
}));Do not build the Codex catalog with tools.map(toDynamicToolSpec): that
compatibility helper can only produce a one-tool namespace at a time and cannot
group a namespaced catalog.
@pwrdrvr/agent-acp accepts the ACP v1 stdio, streamable HTTP, and HTTP+SSE MCP
server shapes. Stdio callers remain source-compatible: args may be omitted and
env may remain a Record<string, string>. HTTP/SSE headers and stdio env may
also be supplied as ACP-native { name, value }[] collections; the package
always serializes the exact arrays required by the pinned protocol library.
import type { AcpMcpServerConfig } from "@pwrdrvr/agent-acp";
const threadMcpServers: AcpMcpServerConfig[] = [
{
name: "local-tools",
command: "/opt/local-tools",
env: { SOCKET_PATH: "/tmp/local-tools.sock" }
},
{
name: "remote-tools",
type: "http",
url: "https://mcp.example.com/rpc",
headers: [{ name: "Authorization", value: `Bearer ${token}` }]
}
];
await client.connect();
if (client.supportsHttpMcp()) {
await client.reopenThread({ threadId, mcpServers: threadMcpServers });
}Optional HTTP and SSE transports are rejected unless the agent explicitly
advertises them in initialize.agentCapabilities.mcpCapabilities. Hosts can
gate UI/configuration with supportsHttpMcp() / supportsSseMcp() or the
exported acpRuntimeSupportsHttpMcp() / acpRuntimeSupportsSseMcp() helpers.
Per-thread lists are accepted by startThreadNative, reopenThread, and
loadThreadNative; a pooled client keeps the wire payload and permission-policy
server names isolated to that session. ACP v1 in the pinned library has no
separate session/resume method, so the package does not invent one.
PwrAgent and PwrSnap consumers can replace local stdio/HTTP/SSE unions with
AcpMcpServerConfig, keep their existing { name, value }[] headers unchanged,
and pass the selected list at the per-thread lifecycle call. Credentials belong
only in env/headers; lifecycle errors redact those values and remote URLs.
The design and sequencing live in docs/plans.
nvm use # v24.14.1
pnpm install
pnpm build # tsup per package
pnpm typecheck
pnpm test # vitest
pnpm lint:licenses # MIT-only gate
pnpm lint:deps # single-version guard (zod)@pwrdrvr/agent-acp speaks ACP through
@agentclientprotocol/sdk,
which declares zod@^3.25 || ^4 as a peer from SDK 1.3 onward. First-party
packages here (@pwrdrvr/agent-client, and consumers like PwrAgent / PwrSnap)
use zod@^4. A single resolved copy avoids unnecessary bundle weight and the
cross-instance footgun where a schema built by one copy fails instanceof
against the other.
agent-acp imports nothing from zod directly. This repo resolves the SDK peer and the first-party packages to one zod 4 copy with a root override:
This repo ships that override and a CI guard (pnpm lint:deps,
scripts/check-single-version-deps.mjs)
that fails if a second zod ever creeps back in. The SDK peer works with a
consumer's existing zod 4 installation; add an install-root override only when
your package manager resolves more than one compatible copy.
Versioning is per-package via changesets:
pnpm changeset # record a bumpOn merge to main, the Release workflow opens a "Version Packages" PR; merging
that publishes the changed packages to npm via OIDC trusted publishing
(tokenless, with provenance, gated behind the npm-publish environment). Each
package needs a one-time bootstrap publish + trusted-publisher config the first
time — same flow as the protocol repo.
MIT © PwrDrvr LLC.