One TypeScript API for local coding-agent runtimes.
@ouim/agentkit is a host-side SDK for builders creating apps on top of local agent CLIs and runtimes like Codex and Claude.
If you're building an agent UI, review bot, desktop app, internal tool, or automation runner, this package is the layer that should sit between your app and the provider runtime. It gives you one API for sessions, streaming, approvals, user input, discovery, and resume handles so you can spend your time building product behavior instead of provider glue.
- one API across Codex and Claude
- proper session lifecycle: open, resume, stream, close
- resumable handles for cross-process recovery
- normalized agent events, approvals, and user-input prompts
- discovery APIs for installed providers, models, and skills
- Codex compatibility entrypoint for existing integrations
This library is for the kind of app that needs controls like:
- model pickers
- reasoning / thinking-level selectors
- access-mode toggles such as supervised vs fuller autonomy
- approval prompts for tool calls and dangerous actions
- streaming turns with tool activity, reasoning, and completion state
- resumable sessions that survive process boundaries
Those apps are usually built today by stitching directly against vendor CLIs and SDKs. That means re-implementing session lifecycle, approval flows, provider discovery, model catalogs, runtime probing, and every provider quirk by hand.
@ouim/agentkit exists to be that control layer.
@ouim/agentkit is intentionally a runtime substrate, not the orchestration engine.
It should help you build:
- your own chat or plan-style agent UI
- your own approval and supervision flows
- your own persistence, replay, and websocket layers
- your own canonical event model if your product needs one
It is not trying to replace your app architecture. It is trying to replace the low-level Codex/Claude runtime glue that usually sits underneath it.
Sessions still belong to their provider runtime. Resume handles are structured and durable across process restarts, but they are not portable across providers.
npm install @ouim/agentkitRequirements:
- Node.js
>=22.6 - at least one local provider runtime:
- Codex:
codexCLI - Claude: runtime requirements for
@anthropic-ai/claude-agent-sdk
- Codex:
import { createSession } from '@ouim/agentkit'
const session = await createSession({
provider: 'codex',
defaults: {
cwd: process.cwd(),
interactionMode: 'chat',
accessMode: 'supervised',
},
name: 'demo',
})
const result = await session.run('Summarize this repository in 3 bullets')
console.log(result.status)
console.log(result.handle)
await session.close()createSession(...) is the ergonomic path: it creates a private agent client, opens one session, and automatically closes that private client when you call session.close().
- choose a provider and create or resume a session
- switch shared
interactionModebetweenchatandplan - switch shared
accessModebetweensupervised,auto-edit, andfull-access - set
reasoningEffortwhen the provider/runtime supports it - switch models through one API
- stream turns and inspect final run results
- handle approvals and user-input prompts
- interrupt active work
- inspect provider capabilities before rendering UI controls
- query provider inventory, models, and skills
The shared API is provider-neutral first. When a builder needs richer provider-native behavior, the library keeps explicit escape hatches available.
The shared API is now explicit about the control bar most apps want:
const agent = await createAgent({
provider: 'codex',
defaults: { cwd: process.cwd() },
})
const session = await agent.openSession({
name: 'repo-ui',
model: 'gpt-5.4',
interactionMode: 'plan',
accessMode: 'auto-edit',
})
const result = await session.run('Plan the next refactor and make the first safe edit.', {
reasoningEffort: 'high',
})Shared builder controls:
interactionMode?: 'chat' | 'plan'accessMode?: 'supervised' | 'auto-edit' | 'full-access'reasoningEffort?: string
Legacy permissionMode still exists as a low-level escape hatch, but shared APIs reject mixed accessMode + permissionMode input instead of silently guessing.
Use agent.getCapabilities() plus listModels() to decide which controls to render:
| Capability | Codex | Claude |
|---|---|---|
modelSwitch |
turn |
session |
interactionModeSwitch |
turn |
session |
accessModeSwitch |
turn |
session |
Shared runtime reasoningEffort |
Yes | Not yet |
Normalized plan.delta / item.* activity |
Yes | Partial |
listModels() is the source of truth for model-level reasoningEfforts. Claude discovery can advertise reasoning metadata before the shared runtime setter is enabled.
- maintainers building review bots and internal automation on local agent CLIs
- app builders who need resumable sessions and event streams without provider-specific glue
- teams migrating from Codex-only integrations toward a provider-neutral SDK
Local agent tooling is fragmented:
- provider-specific session identifiers and resume rules
- different auth/runtime checks per CLI
- non-portable event streams
- inconsistent approval and user-prompt wiring
This package normalizes those concerns into one API while preserving provider-native escape hatches.
import { createSession, safety } from '@ouim/agentkit'
const session = await createSession({
provider: 'codex',
defaults: { cwd: process.cwd() },
name: 'safe-hello',
})
const result = await session.run('Read this repo and suggest a small cleanup.', {
handlers: safety.confirmDangerous({
allowReadOnly: true,
allowFileEdits: true,
allowCommands: false,
}),
})
console.log(result.text)
await session.close()safety presets are provider-neutral, best-effort handler factories. They classify approval requests by normalized request.kind only, default unknown kinds to deny, and leave provider-specific details available in request.payload.
createSession(options)createAgent(options)agent.openSession(options?)agent.resumeSession(handle, options?)agent.session(name, options?)local cached convenience handleagent.clearSession(name)andagent.clearSessions()clear only local cache entriessession.close()always closes the local session object; remote/runtime impact is provider-specific todayagent.close()closes client-owned runtime resources in the current process
run() returns an AgentRunResult with optional handle:
type AgentSessionHandle = {
version: 1
provider: 'codex' | 'claude'
sessionId: string | null
name?: string
state?: {
resumeKey?: string
resumeAt?: string
raw?: unknown
}
}sessionId, state.resumeKey, and state.resumeAt are resumable provider data. Local session(name) cache state is not remote state and is not enough by itself for cross-process resume.
getAvailableProviders(options?)getProviderAvailability(provider, options?)getProviderInventory(options?)getProviderInventoryEntry(provider, options?)listModels(provider?, options?)listSkills(provider, options?)
session.stream(...) emits provider-neutral AgentEvent values:
run.started: canonical lifecycle start event keyed byrunIdmessage.delta: incremental assistant text deltamessage.completed: completed assistant message payloadreasoning.delta: incremental reasoning summary delta where availableplan.delta: normalized plan activity item for UI plan panesitem.started: normalized tool/file/other activity start eventitem.completed: normalized tool/file/message/other activity completion eventstatus.updated: canonical lifecycle status update keyed byrunIdapproval.tool: request that needs allow/deny approvaluser.input: request for structured user answersrun.completed: normalized final run result for the streamed turnprovider.notification: passthrough provider notification for advanced useerror: normalized stream error event
Current event boundary:
- event order is the live adapter/runtime emission order for the current stream attachment
run.completedcarries the normalized finalAgentRunResultfor that turn- there are no stable event IDs, replay cursors, or live-vs-replay markers yet
For plan.delta, item.started, and item.completed, the normalized item payload includes:
idkind- optional
text - optional
toolName - optional
path - optional
status raw
Use the shared safety helper when you want simple approval defaults without introducing a policy engine:
safety.readOnly()denies all approval requests.safety.acceptEditsOnly()allows file-edit-like kinds and denies the rest.safety.confirmDangerous({ allowReadOnly, allowFileEdits, allowCommands })allows only the explicitly enabled categories and denies unknown kinds conservatively.
These presets are intentionally thin convenience helpers over AgentHandlers. They do not normalize provider-specific payloads beyond the shared request.kind string.
Handlers are passed through createAgent(...).openSession(...).stream(...) (or run(...)) via options.handlers.
import { createAgent } from '@ouim/agentkit'
const agent = await createAgent({ provider: 'codex', defaults: { cwd: process.cwd() } })
const session = await agent.openSession({ name: 'handlers-demo' })
const stream = await session.stream('Inspect package.json and summarize scripts.', {
handlers: {
onToolApproval: async request => {
console.log('Approval request:', request.kind)
return 'allow'
},
onUserInput: async request => {
console.log('User input request:', request.question)
return ['example answer']
},
},
})
for await (const event of stream) {
if (event.type === 'message.delta') process.stdout.write(event.text)
}See runnable example: npm run example:handlers.
If you want explicit client ownership, local named-session caching, or multiple sessions on one runtime client, use createAgent(...) directly:
import { createAgent } from '@ouim/agentkit'
const agent = await createAgent({
provider: 'codex',
defaults: { cwd: process.cwd() },
})
const session = await agent.openSession({ name: 'demo' })
const result = await session.run('Summarize this repository in 3 bullets')
console.log(result.status)
console.log(result.handle)
await session.close()
await agent.close()Use shared API first. Drop down only when you need provider-specific functionality:
agent.asCodex()returnsCodexClient | nullagent.asClaude()returnsClaudeProviderHandle | null
Use escape hatches when your UI needs provider-specific controls or richer runtime detail than the normalized surface exposes today.
Codex compatibility exports now live under an explicit entrypoint:
import { createCodex, CodexClient } from '@ouim/agentkit/compat/codex'Compatibility entrypoint exports include:
createCodex(options?)CodexClientCodexAuthCodexSessionCodexThreadwriteCodexSkillConfig(options)
Use this for direct Codex runtime control. Prefer shared createAgent for new multi-provider code.
All README-listed examples exist and map to npm scripts:
npm run example->examples/basic.ts(provider-neutral Codex smoke)npm run smoke->examples/basic.tsnpm run example:agent:codex->examples/agent-codex.tsnpm run example:agent:claude->examples/agent-claude.tsnpm run example:lifecycle->examples/agent-lifecycle.tsnpm run example:handlers->examples/handlers.tsnpm run example:safe-session->examples/safe-session.tsnpm run example:inventory->examples/provider-inventory.tsnpm run example:models->examples/models.tsnpm run example:skills->examples/skills.tsnpm run example:ci->examples/ci.ts
For precise lifecycle, stream-ordering, interrupt, and close semantics, use docs/RUNTIME_CONTRACT.md as the source of truth.
- Use
npm run example:inventoryand check each providerstatus,installed,runnable, andexecutablePath. - If Codex is missing, install
codexand ensure it is on PATH or passcodexPathin options. - If Claude is missing, ensure Claude runtime dependencies are installed and
pathToClaudeCodeExecutableis set if needed.
- Call
agent.getAccountState()or inspectauthenticatedin provider inventory. - For Codex browser login, use the login URL printed by examples and wait for completion.
- For Codex device auth, run with
CODEXKIT_LOGIN=device-code(or PowerShell$env:CODEXKIT_LOGIN='device-code') in examples that support it. - If device auth completes but account still appears null, restart process/session and re-check inventory/account state.
- Use cheap probe first:
getProviderInventory({ probeMode: 'cheap' }). - Deep probes can fail due to startup/auth/runtime issues; inspect
diagnostics.failureReasonanddiagnostics.probeStrategy. - Bound probe time using
probeTimeoutMsand treatdegradedas a recoverable state.
npm install
npm run typecheck
npm testThe shared API is intentionally a runtime substrate, not an orchestration framework.
- Current normative behavior and boundaries are documented in docs/RUNTIME_CONTRACT.md.
- Persist
AgentSessionHandleif you need cross-process resume. - Treat
agent.session(name)as local cache convenience only. - Treat
raw,resumeState, and provider escape hatches as provider-specific data.
Non-goals today:
- no event replay or cursor model
- no durable pending-request recovery
- no orchestration, websocket, or UI-state abstraction layer