@archships/dim-agent-sdk
v0.0.75
Published
An agent-first TypeScript SDK with provider adapters, sessions, hooks, plugins, and runtime gateways.
Readme
@archships/dim-agent-sdk
An agent-first TypeScript SDK with canonical multi-provider contracts, session/tool loop, hook-based plugins, runtime gateways, and builtin local coding tools.
Install
Recommended runtime: Node.js >=18.
npm install @archships/dim-agent-sdkQuick start
import { createAgent, createModel, createOpenAIAdapter } from '@archships/dim-agent-sdk'
const model = createModel(
createOpenAIAdapter({
apiKey: process.env.OPENAI_API_KEY,
baseUrl: 'https://api.openai.com/v1',
defaultModel: 'gpt-4o-mini',
}),
)
const agent = createAgent({
model,
cwd: process.cwd(),
})
const session = await agent.createSession({
systemPrompt: 'You are a coding agent. Use tools when needed.',
})
const itemId = session.send(
'Create hello.txt in the current directory and write hello world into it.',
)
for await (const event of session.receive()) {
if (event.itemId !== itemId) continue
if (event.type === 'text_delta') process.stdout.write(event.delta)
if (event.type === 'done') console.log(event.message.content)
}When a session has tools available, the runtime also injects an internal tool-call JSON contract into the effective system prompt.
Models are told to emit one complete JSON object per tool call.
If a streamed tool call still arrives with malformed pre-execution JSON, the SDK now surfaces it as a failed recoverable tool_result, stores that failed result in session history when the tool name is known, and keeps the run alive instead of failing the whole item immediately.
The SDK also owns transcript tool-call consistency. Snapshot restore normalizes legacy/OpenAI-shaped assistant.tool_calls and tool.tool_call_id / call_id / callId fields, preserves assistant messages whose only meaningful content is toolCalls, repairs contiguous orphan tool results by backfilling synthetic assistant tool calls, and compensates missing tool results with recovered failed tool messages. These recovered results mark unknown historical failures; the real tool execution state remains unknown. Before provider dispatch, unrepairable tool history raises InvalidToolMessageHistoryError locally with message index, ids, and request/session/turn metadata, so OpenAI-compatible providers do not receive malformed messages.
Usage ledger
Session.usage remains the compatibility mirror of total session usage.
The canonical source now lives in usageLedger, with request-, turn-, and session-level views.
const itemId = session.send('Summarize the latest tool results.')
for await (const event of session.receive()) {
if (event.itemId !== itemId) continue
if (event.type === 'turn_usage') {
console.log(event.summary.totalUsage.totalTokens)
}
}
console.log(session.getUsageSummary().totalUsage.totalTokens)
console.log(session.getTurnUsage(itemId)?.breakdown)
console.log(session.listRequestUsages({ kind: 'auto_compaction' }))Direct gateway calls can also opt into the same ledger:
for await (const event of agent.services.model.stream(request, {
usage: {
sessionId: session.id,
kind: 'manual_compaction',
},
})) {
console.log(event.type)
}Persistence
SessionSnapshot stays as the full export/restore shape, including usageLedger.
Built-in persistence now stores main session state and usage ledger separately:
FileStateStorewrites<encodeURIComponent(sessionId)>.jsonfor the main snapshotFileStateStorewrites<encodeURIComponent(sessionId)>.usage.jsonfor the session usage ledgertransformSnapshotonly rewrites the main snapshot pathload()hydrates the sidecar ledger back into the returnedSessionSnapshot
Custom stores now implement the split persistence contract:
import type { StateStoreSaveInput } from '@archships/dim-agent-sdk'
async function save(input: StateStoreSaveInput) {
console.log(input.snapshot.sessionId)
console.log(input.usageLedger.summary.totalUsage.totalTokens)
}Observability JSONL logging
Agent-level observability records SDK public APIs, state transitions, hooks, model requests, provider and plugin network requests, tool calls, usage finalization, persistence, approvals, notifications, services, and process subagent IPC as JSONL. Each queued user item carries traceId and turnId, and operation rows use paired start plus success or error phases.
import path from 'node:path'
import { createAgent, createModel, createOpenAIAdapter } from '@archships/dim-agent-sdk'
const model = createModel(
createOpenAIAdapter({
apiKey: process.env.OPENAI_API_KEY,
baseUrl: 'https://api.openai.com/v1',
defaultModel: 'gpt-4o-mini',
}),
)
const agent = createAgent({
model,
cwd: process.cwd(),
observability: {
logFilePath: path.join(process.cwd(), 'logs/dim-sdk.jsonl'),
sanitizer: {
maxStringLength: 4000,
maxArrayItems: 128,
},
streamBodies: {
enabled: true,
directory: path.join(process.cwd(), 'logs/streams'),
},
},
})
agent.setObservabilityEnabled(false, { reason: 'maintenance window' })
agent.setObservabilityEnabled(true, { reason: 'resume diagnostics' })The first row is sdk.meta, including SDK/package, Node/process, OS, and allowlisted environment metadata. Model streams emit two rows by default: model.request start with stream metadata, then model.request success / error with stream metadata plus a summary. api.session.receive.event keeps streaming delta event boundaries while replacing delta text with deltaLength by default. Set streamEvents.detail: true to add sanitized model.event rows and keep receive-event delta payloads. Adapter-level debug.logFilePath remains available for provider-only compatibility logs; new host integrations should prefer createAgent({ observability }).
Runtime inspection
The SDK exposes a read-only inspection service at agent.services.inspect for host diagnostics.
Manual
- Developer manual: docs/manual/README.md
- Chinese guide: docs/manual/zh/README.md
- English guide: docs/manual/en/README.md
- SDK API reference (zh): docs/manual/zh/sdk-api-reference.md
- Plugin API reference (zh): docs/manual/zh/plugin-api-reference.md
- SDK API reference (en): docs/manual/en/sdk-api-reference.md
- Plugin API reference (en): docs/manual/en/plugin-api-reference.md
- Host integration cookbook: start in docs/manual/en/sdk.md or docs/manual/zh/sdk.md
Included core capabilities
- Canonical content / message / tool / model / state contracts
- Canonical message content aligned to AI SDK V3 prompt parts:
textandfile createAgent()->Agent->Session- Queue-first session flow:
send(),sendBatch(),steer(),receive(),getQueueStatus() - Long-lived host hygiene:
Session.dispose()releases session-scoped runtime registrations and controller-owned resources when a session is no longer needed - Session events:
text_delta, optionalthinking_delta,tool_call_start,tool_call_args_delta,tool_call_end,tool_call,plugin_event, recoverable and regulartool_result,usage_recorded,turn_usage,done,error - Provider adapters:
openai-compatible,openai-responses,anthropic,gemini,zenmux,aihubmix,aihubmix-responses,moonshotai,deepseek,xai,xai-responses - Builtin tools:
read,write,edit,exec - Builtin
readreturns UTF-8 text as line-numbered windows with full-file revision tracking, locally paginates oversized text windows with continuation notices instead of using the SDK-global<persisted-output>path, and can also project common raster images into syntheticuserfile context when the active model advertises image input support - Hook-first plugin integration
- Host-layer ordered subagents with
SubagentOrchestrator,InProcessSessionSubagentExecutor,ProcessSessionSubagentExecutor, andSubagentProcessRegistry - Runtime gateways: file system, git, exec, network, model
- Host inspection:
agent.services.inspectfor agent/session/persistence snapshots - Namespaced plugin session state with snapshot restore support
- In-memory and file-based persistence
Hook support
Supported public hooks in the current runtime:
run.starttool.beforeExecutetool.afterExecutecontext.compact.beforenotify.messagerun.stoprun.endsession.error
Reserved / experimental hook names that are typed but not wired into the runtime yet:
subagent.stop
Current failure policy:
- Sync middleware is blocking and fail-fast
- Observers are best-effort
mode: 'async'is observer-onlytimeoutMsapplies per hook handler
Official plugin packages
| Package | Support level | Notes |
| ------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- |
| @archships/dim-plugin-auto-compact | supported | Official auto compaction plugin; requires compaction.compactorPluginId: 'auto-compact' |
| @archships/dim-plugin-grep-glob | supported | Registers grep and glob filesystem tools with relative and absolute path overrides |
| @archships/dim-plugin-mcp-client | supported | Controller-driven MCP client for session-scoped server connections and tool injection; use @archships/dim-agent-sdk >= 0.0.23 and @archships/dim-plugin-api >= 0.0.9; host-only prompt/context/tool injection remains compatible |
| @archships/dim-plugin-skills | supported | Metadata-first file-backed skills plugin with <skills_instructions> catalog metadata, skill: name trigger guidance, a model-callable skill loader tool, and a catalog session controller |
| @archships/dim-plugin-plan-mode | supported | Session-scoped planning guardrail with host-data-backed drafts, restricted exec, and plan_read / plan_write; use @archships/dim-agent-sdk >= 0.0.23 and @archships/dim-plugin-api >= 0.0.9 |
| @archships/dim-plugin-memory | experimental | Placeholder package; not part of the current supported surface yet |
| @archships/dim-plugin-web | experimental | Placeholder package; not part of the current supported surface yet |
| @archships/dim-plugin-scheduler | experimental | Placeholder package; not part of the current supported surface yet |
| @archships/dim-plugin-research-mode | experimental | Evidence-first research guardrail with a session controller, staged prompt injection, research state, and delegate_tasks guidance |
Compaction and state model
session.messagesalways keeps the full original history for UI and restoresystemPromptis text-only; user messages can carry mixedtext+filecontent- Canonical request projection is controlled by SDK compaction state:
cursor,systemSegments,checkpoints - Once compaction is active, the request projection realigns
cursorto a real user boundary, injects the retained anchor user first, and replayssystemSegmentsas a synthetic summaryusermessage instead of a tailsystemmessage SubagentProcessRuntimeProfile.compactionconfigures compaction for child-ownedprocesssessions; profiles that setcompactorPluginIdshould also install the matching compactor pluginprocesssubagents inherit parent runtime model capabilities only when the child process factory returns the same provider / model id and leaves that capability field unset; different-model profiles should expose image input and context-window support from their own model factory optionsSession.getStatus()returns the canonical read-only session status snapshot used by hook runtime contextSession.getUsageSummary(),getUsageLedger(),getTurnUsage(),listTurnUsages(), andlistRequestUsages()expose semantic usage queries without reconstructing totals fromdone.usageSession.getPlugin(pluginId)returns a session-scoped plugin controller when the plugin exposes oneagent.services.inspectexposes read-only agent config, live session state, and persisted snapshot summaries without leaking mutable runtime internalsSession.dispose()unregisters the session from compaction and plugin-state services, and also disposes session-scoped plugin controllers such asmcp-client- Plugins can persist their own namespaced session state through
pluginState - If
compaction.compactorPluginIdis configured, only that plugin can write canonical compaction through plugin services Session.compact()remains available as an app-level overriderun.endkeeps final message rewrite support; usage rewrite is retired and hooks now observeusageSummarybuildCompactionBudget(options, estimatedInputTokens, { contextWindow, plannedOutput })andcalculateThresholdTokens(options, targets)are exported for hosts that want to compute the samethreshold = contextWindow − max(effectiveO, contextWindow × safetyRatio)the SDK uses (safetyRatiodefaults toDEFAULT_COMPACTION_SAFETY_RATIO = 0.2;effectiveOcapsplannedOutputand falls back to the safety floor when it claims more than 60% of the window)Session.getCurrentContextSize(),Session.ensureCurrentContextSize(),Session.countTokens(request), andSession.getCompactionBudget({ plannedOutput? })let hosts observe the SDK's own post-hook budgeting without rebuilding the mathisContextWindowExceededError(error)is a provider-agnostic classifier for context-window-exceeded errors surfaced byModelGateway.stream- If threshold compaction still cannot fit the next request, the runtime can do one bounded recovery pass that rewrites only the newest oversized result payloads (
tooloutputs and subagent parent-commit packages) into short overflow summaries before finally raisingcontext_compaction_required - Hook handlers receive the same canonical status through
context.status, without exposing full message history or other plugins' state Session.getStatus().capabilitiesmirrors runtime model input capabilities, and builtinreaduses that signal to decide whether a local image should stay metadata-only or be injected as syntheticuserimage context; same-modelprocesssubagents inherit missing capabilities for this path
Provider notes
createOpenAIAdapter(): OpenAI-compatible Chat Completions style; mapsreasoning_content/reasoningintothinking_deltaand replays assistantthinkingback upstreamcreateOpenAIResponsesAdapter(): official OpenAI Responses API; maps reasoning summaries intothinking_delta, sends full canonical history by default, only reusespreviousResponseIdwhenusePreviousResponseId: true, and accepts request-levelproviderOptions.openaitransport fields forprompt_cache_keyand custom headerscreateAnthropicAdapter(): maps Claude thinking blocks intothinking_delta; when overridingbaseUrl, the adapter appends/v1if it is missing; Anthropic-compatible routes now auto-inject explicit prompt caching unlesscache.modeis set to'off'createGeminiAdapter(): maps Gemini thought parts intothinking_deltacreateZenMuxAdapter(): ZenMux adapter; routesanthropic/*models throughhttps://zenmux.ai/api/anthropic/v1/messagesand sends every other model through the official ZenMux OpenAI-compatible endpoint athttps://zenmux.ai/api/v1/chat/completions.baseUrlis kept for compatibility but ignored at runtime.createAihubmixAdapter()/createAihubmixResponsesAdapter(): AIHubMix chat + responses adapters; chat forwards reasoning effort into AIHubMix-compatible request bodies, and theresponsesvariant sends full canonical history by default, only reusespreviousResponseIdwhenusePreviousResponseId: true, and honors customfetchoverrides for transport/auth wiring. If you omitapiKey, you must inject the real upstream authentication inside customfetch.createMoonshotAIAdapter(): MoonshotAI language model adapter with thinking budget mappingcreateDeepSeekAdapter(): DeepSeek chat adapter with reasoning ->thinking_deltacreateXaiAdapter()/createXaiResponsesAdapter(): xAI chat + responses adapters; theresponsesvariant now sends full canonical history by default and only reusespreviousResponseIdwhenusePreviousResponseId: true- Builtin
execnow publishes a flat top-level object schema and rejects action-incompatible fields as request errors. Anthropic-compatible adapters remove top-leveloneOf/anyOf/allOfduring transport projection; OpenAI-compatible and Responses families for OpenAI-compatible, OpenAI Responses, ZenMux non-Anthropic, AIHubMix chat / responses, xAI chat / responses, DeepSeek, and MoonshotAI also remove top-levelenum/not; Gemini keeps the original schema unchanged. - Builtin
execforeground commands wait up to 5 seconds before moving still-running commands to background managed-task messaging and completion notifications. - Official adapters use realtime upstream streaming by default. Set adapter
streamMode: 'buffered'or requeststreamMode: 'buffered'to replay a completed response instead. - Session runtime fills missing
maxOutputTokenswith4000aftermodel.requestmiddleware runs. Directmodel.stream()calls still pass through whatever the caller provides. - The SDK no longer injects implicit workspace context into model requests; if the model needs file or Git state, let it call tools explicitly.
- Builtin
readreturns line-numbered, revision-tracked text windows for UTF-8 files, whilepng/jpg/jpeg/gif/webpusefileSystem.readBytes()and, when the active model advertisescapabilities.modalities.input: ['image'], append a syntheticuserfile message after the tool receipt so providers receive a normal image input instead of tool-result media. Inprocesssubagents, identical child provider / model ids inherit missing parent capabilities before this decision runs. - Builtin providers are also available from public subpaths such as
@archships/dim-agent-sdk/providers/openaiand@archships/dim-agent-sdk/providers/xai-responses - Published packages ship these public entrypoints directly; consumers should not import
dist/src/*or patchnode_modulesafter install - Other deep internal imports are not part of the public API, even though package-local tests use path aliases against
src/* - Custom providers can follow the same factory pattern via
@archships/dim-agent-sdk/providers/core; implementstream()for realtime deltas,generate()for buffered results, or both
OpenAI Responses request transport:
providerOptions.openai.promptCacheKeymaps to the/responsesbody fieldprompt_cache_keyproviderOptions.openai.requestHeadersmerge into the outgoing HTTP headers for that request, so gateways can receive values such assession_id
import {
createModel,
createOpenAIResponsesAdapter,
type OpenAIResponsesRequestProviderOptions,
} from '@archships/dim-agent-sdk'
const model = createModel(
createOpenAIResponsesAdapter({
apiKey: process.env.OPENAI_API_KEY,
defaultModel: 'o4-mini',
}),
)
const requestProviderOptions = {
promptCacheKey: runtimePromptCacheKey,
requestHeaders: {
session_id: runtimeSessionId,
},
} satisfies OpenAIResponsesRequestProviderOptions
for await (const event of model.stream({
messages,
providerOptions: {
openai: requestProviderOptions,
},
})) {
console.log(event.type)
}Anthropic-compatible prompt caching defaults:
cache.modedefaults to'auto'cache.ttldefaults to'5m'cache: { mode: 'off' }disables SDK-managedcache_controlinjection- explicit
providerOptions.anthropic.cacheControlon a message, content block, or tool definition is forwarded unchanged and disables auto injection for that request
import { createProviderFactory } from '@archships/dim-agent-sdk/providers/core'Demo
Provider-free demos:
pnpm run demo:host: approval + notification host control-plane walkthroughpnpm run demo:persistence:FileStateStore+session.save()+restoreSession()walkthroughpnpm run demo:plan-mode: official session-scoped plugin controller +hostDataDirwalkthroughpnpm run demo:compaction: scripted compaction runtime walkthroughpnpm run demo:auto-compact: scripted official auto compact plugin demopnpm run demo:subagents: scripted ordered subagent walkthrough forin-processand child-ownedprocessmodes
Provider-backed demos:
pnpm run demo:openai: builtin tools smoke demopnpm run demo:hooks: Hook v2 scenario runner
Repo demo files:
packages/dim-agent-sdk/demo/host-control-plane-scripted.tspackages/dim-agent-sdk/demo/persistence-scripted.tspackages/dim-agent-sdk/demo/plan-mode-plugin.tspackages/dim-agent-sdk/demo/compaction-scripted.tspackages/dim-agent-sdk/demo/auto-compact-plugin.tspackages/dim-agent-sdk/demo/subagents-scripted.tspackages/dim-agent-sdk/demo/openai-tools.tspackages/dim-agent-sdk/demo/openai-hooks.ts
demo:hooks currently runs these provider-backed scenarios:
lifecycleapproval-denysynthetic-resultnotification-controlstop-finalize
Provider-backed demos use createOpenAIAdapter() against the OpenAI-compatible Chat API and require DIM_TEST_API_KEY, DIM_TEST_BASE_URL, and optional DIM_TEST_MODEL_ID.
Plan mode v2 notes:
- plan drafts live under
<hostDataDir>/plans/<sessionId>/plan.md - host applications drive plan mode through
session.getPlugin('plan-mode') plan_writeis the only writable artifact exposed to the model while plan mode is activeenable()/disable()changes affect the next run, not the current in-flight runagent.deleteSession(sessionId)removes both the persisted snapshot and the matching plan draft directory
Testing
Local repository verification is split into three layers:
- Local workspace development expects Node.js
20.19+or22.12+because repository verification uses officialVite 7 + Vitest + Oxlint. pnpm run test: full local regression, including deterministictest/e2e/*.e2e.test.tspnpm run test:e2e: deterministic end-to-end workflows forplan-mode, code-agent tool loops, auto-compact restore, and approval / permission boundariespnpm run test:plugins: focused plugin contract and integration testspnpm run test:smoke: env-gated provider smoke forprovider-tools,provider-hooks,provider-plan-mode, andprovider-subagent-process; excludes long-taskpnpm run test:smoke:providers: focused multi-provider builtin-tool smoke intest/smoke/provider-tools.smoke.tspnpm run test:smoke:long-task: default manual OpenAI-compatible long-task smoke; it currently aliases theguidescase without subagentspnpm run test:smoke:long-task:subagents: the same defaultguidescase with host-owned delegation plus child-ownedprocesssubagents enabled throughDIM_TEST_LONG_TASK_SUBAGENTS=1pnpm run test:smoke:long-task:guides/pnpm run test:smoke:long-task:guides:subagents: explicit beginner-guide generation case formodelinfo-clipnpm run test:smoke:long-task:rust-port/pnpm run test:smoke:long-task:rust-port:subagents: review-oriented Rust port case for the same reference repo
The provider smoke layer reuses:
DIM_TEST_API_KEYDIM_TEST_BASE_URLDIM_TEST_BASE_URL_ANTHROPIC(optional; enables the Anthropic smoke lane)DIM_TEST_MODEL_ID(optional)DIM_TEST_LONG_TASK_CASE(optional;guidesby default, also supportsrust-port)DIM_TEST_LONG_TASK_SUBAGENTS(optional; only1enables the subagent mode for the long-task smoke)
The focused multi-provider tools smoke loads repo-root .env.smoke automatically. Start from .env.smoke.example, fill only the providers you want to run, and use per-provider keys such as DIM_TEST_OPENAI_API_KEY, DIM_TEST_OPENAI_MODEL_ID, and optional DIM_TEST_OPENAI_BASE_URL. ZenMux is the exception: DIM_TEST_ZENMUX_BASE_URL is ignored because createZenMuxAdapter() now pins the official ZenMux endpoints, and you can optionally add DIM_TEST_ZENMUX_ANTHROPIC_MODEL_ID to create a second ZenMux smoke lane that exercises the Anthropic-compatible route. That smoke keeps its temporary workdirs under os.tmpdir()/dim-sdk-provider-smoke/<provider-id>/run-* and removes each run directory after the test finishes.
Smoke tests assert stable invariants such as tool calls, plugin_event, notifications, and on-disk side effects. They intentionally avoid exact natural-language output matching unless the smoke is explicitly about artifact structure.
The long-task smoke is tuned for diagnosis as much as pass/fail: it runs only through the OpenAI-compatible adapter, reuses a generic harness plus named cases, and preserves both the final workspace and debug logs for inspection. The default guides case generates beginner-friendly Markdown guides for every tracked file; the rust-port case asks the model to translate the same reference repo into a reviewable Rust cargo project. Every run now prints and writes a unified summary with host rounds, model turns, tool-call count, total tokens, cache/token detail breakdowns when the provider exposes them, delegation/process-batch counts, and case-specific stats. It also writes a full-fidelity session timeline array to logs/long-task-session.json, alongside the staged debug logs in logs/long-task-smoke.jsonl, so parent and delegated child context can be replayed during debugging. Those staged diagnostics now include explicit compaction_notification and compaction_state_after_hook entries, which makes it much easier to see whether auto compact triggered and why it still failed to bring the request back under budget. Ordered subagents remain opt-in through pnpm run test:smoke:long-task:subagents or DIM_TEST_LONG_TASK_SUBAGENTS=1.
