@nuwax-ai/agent-kit
v0.3.3
Published
Shared agent/ACP logic for nuwa-cli and nuwaclaw (engine resolution, agent install).
Readme
@nuwax-ai/agent-kit
Canonical source lives in the nuwaclaw workspace. nuwa-cli consumes published versions; during the 0.3.0 migration its previous workspace copy remains only as a release bridge and must be removed after 0.3.0 is published.
Shared logic for @nuwax-ai/nuwa-cli and @nuwax-ai/nuwaclaw. The single place
to maintain the agent/ACP behaviour both hosts need, so they stay in lockstep.
Boundary principle. agent-kit holds isomorphic primitives + shared-package
adapters. Hosts keep process model + env strategy + lifecycle + product
extensions. Concretely: agent-kit depends on no host runtime package — the
codex adapter is a require.resolve'd peerDependency, and the MCP bridge is
injected (@nuwax-ai/mcp-proxy-ts is never imported here).
Status
Shared slices, dual-format (ESM + CJS) build:
- codex engine resolution
- file-server / lanproxy health polling
PersistentMcpBridgesingleton lifecycle- MCP npx cache warmup state machine
- ACP permission decision primitives
Exports
Engine resolution (src/index.ts)
resolvePackageEntry(packageName, entrySpecifier)—require.resolvea dependency entry (ESM+CJS safe viacreateRequire(import.meta.url)+ tsup shims).resolveNodePackage({ packageName, entrySpecifier, entryOverride? })— turn either a bundled override or installed package entry into the canonicalnode <entry>spawn target.resolveCodexAcp({ entryOverride? })— resolve the codex ACP adapter (@nuwax-ai/nuwax-codex-acp-ts) to a spawn target{ command, args }.entryOverrideis for hosts that bundle the adapter by a non-require.resolvemechanism (e.g. nuwaclaw's Electronresources/); defaults torequire.resolvefor npm-installed hosts (nuwa-cli).resolveClaudeAcp({ entryOverride? })— the same spawn-target contract forclaude-code-acp-ts; hosts should supply their installed or bundled entry so the package stays an optional integration.EngineResolution— type ({ command; args; envOverlay? }), structurally compatible with nuwa-cli'sResolvedEngine.
Health primitives (src/health.ts)
waitForFileServerHealth({ port, fetchImpl?, signal?, … })— pollGET /healthuntil ok / timeout / abort. Default timeoutDEFAULT_FILE_SERVER_HEALTH_TIMEOUT_MS(20s) for Windows cold start.waitForLanproxyTunnel({ domain, configKey, fetchImpl?, signal?, … })— poll the cloud tunnel health endpoint.isLanproxyTunnelEnvelopeHealthy(envelope)— pure predicate;LANPROXY_OK_CODE("0000") exported for the magic code.confirmProcessHealthy({ pid, isAlive, … })— process liveness across a stabilize window.delay(ms, signal?)— abortable sleep.
Host differences (fetch vs http.request; isPidAlive vs process.kill(0)) are injected via fetchImpl / isAlive.
Start retry (src/startRetry.ts)
withStartRetry(attemptFn, { label, maxAttempts?, backoffMs?, logger?, signal? })— isomorphic full-start retry (default 3 attempts, 1s/2s/4s backoff). Hosts injectattemptFn(spawn → health → cleanup on failure) and an optional logger; agent-kit never owns process lifecycle.
Persistent bridge (src/proxyBridge.ts)
createPersistentBridge({ create, logger, … })— manage one bridge across config changes. Returns a handle withensureStarted(servers)/stop()/isRunning().
Contract: the injected bridge's
start(servers)MUST be idempotent / diff-aware.ensureStartedforwards every call tostart(no internal dedup); the host calls it on every MCP rewrite, so change-detection lives in the bridge. nuwa-cli'sPersistentMcpBridgesatisfies this; a host whosestartis not idempotent must diff before callingensureStarted.
createPersistentBridge is generic over the concrete bridge type, so ensureStarted's
parameter is type-checked against exactly what the injected bridge's start accepts —
no any / host-side casts, and agent-kit still names no host type.
MCP cache warmup (src/mcpCacheWarmup.ts)
runMcpCacheWarmup({ version, npxDir, env, spawnNpx, readState, writeState, … })— shared marker/cache idempotency, serial warming, timeout and TERM/KILL cleanup.packageNameFromSpec(spec)— remove a version suffix while preserving scoped package names.isPackageInNpxCache(npxDir, packageName)— scan npm's_npx/<hash>/node_modulescache without depending on npm's hash algorithm.MCP_WARMUP_SPECSand timeout constants — shared defaults consumed by both hosts.
Hosts retain command discovery, environment policy, state-file schema and logging as adapters. The warmup module never imports Electron or either host runtime.
ACP permission primitives (src/permissions/)
parseComputerPermissionResolveRequest— sharedSelected.option_id/ legacyoptionIdwire parsing; hosts adapt HTTP response envelopes.toComputerPermissionProgressData— shared SSE payload mapping with host-ownedmetadataandextensions(for example nuwaclaw'ssave_rule). Its request contract is structural so ACP SDK enum drift does not leak into hosts.matchToolApprovalRules/ normalization and target extraction — the canonical glob matching implementation used by both hosts.createPendingService— duplicate-key supersession, option validation, timeout/cancel, optional resolved retention, host-provided IDs and revision lookup.retentionMs: 0keeps no resolved entries.
nuwaclaw keeps Electron events, revision response policy, strict sandbox decisions and product audit logs in its host adapters. nuwa-cli keeps its interactive/serve policy.
Build
npm run build # tsup → dist/index.js (esm) + dist/index.cjs (cjs) + dist/index.d.tsThe dual-format build is guarded by tests/agentKit.test.ts, which require()s
dist/index.cjs — that is nuwaclaw's consumption path. A vitest globalSetup
builds the artifact if missing.
Requirements
- Node
>= 20.3(usesAbortSignal.any, available since 20.3). Both hosts run Node 22+. - ACP SDK and adapter packages declared as peerDependencies are provided by the host.
