Idiomatic TypeScript transpilation of the OpenHands Python agent-sdk.
0.3.3 established native OpenAI tool-completion parity. The current development line extends the same ToolDefinition flow through Anthropic Messages, Gemini Interactions, OpenRouter, LiteLLM-compatible endpoints, and custom OpenAI-compatible gateways. The main architecture is documented in docs/ARCHITECTURE.md:
- zod-backed event, tool, settings, profile, and serialization models
- profile-first LLM clients for OpenAI chat completions, OpenAI Responses, Anthropic, Gemini, and OpenAI-compatible profiles
- local/remote conversation state, disk-backed event logs, agent loop, pending tool-call queue, parallel execution, restore, and stuck detection
- context, condensers, skills, hooks, critics, file-based subagents, git helpers, MCP wrappers
- concrete tools: terminal, file editor, glob, grep, task tracker, and injectable browser adapter
Intentional deviations from Python remain: no ACP runtime, security analyzers, risk scoring, confirmation gates, Python Cipher, or Python secret-storage split. The Python SecretRegistry surface maps to the current TypeScript SecretStore/keyring model.
This package is tracking the Python agent-sdk architecture while staying idiomatic TypeScript. The implemented surfaces currently include focused parity coverage for:
- LLM message/content serialization, Agent-to-LLM
ToolDefinitionpropagation, and provider-owned OpenAI chat completions/Responses, Anthropic Messages, and Gemini Interactions request/response mapping - event schemas and
eventsToMessagesconversion, including parallel tool-call batching behavior - conversation state, local/remote conversations, pause/resume, restore, parallel execution, and stuck detection
- settings/profiles, profile-selected LLM field hygiene, provider/profile-scoped API key references, and keyring-backed secret storage
- tools, workspace abstractions, git helpers, hooks, skills/context, MCP wrappers, critics, and file-based subagents
Accepted deviations are deliberate and should not be treated as missing work unless the product needs them later: ACP runtime execution, security/confirmation policy execution, and Python's older SecretRegistry API.
- Idiomatic TypeScript. Not a literal line-by-line port. We respect the Python SDK's architectural choices and adapt them to TS conventions.
- Type enforcement. Strict TypeScript everywhere; runtime validation via zod v4 (replacing pydantic), using its native
z.toJSONSchema()for tool/settings schema generation. - Fresh transpilation. We do not copy existing code. The earlier TS attempt in
oh-tabis outdated and serves only as a reference for product-level profile semantics, tooling, and tests. - Profile-first product LLM boundary. Product and REST callers select an
LLMProfile; they do not pass raw Python-styleLLMobjects or loose model/provider fields. Low-level provider clients remain available as explicit advanced SDK/test building blocks. - API-native provider clients. Implement provider APIs as they actually are — OpenAI-compatible Chat Completions, OpenAI Responses, Anthropic Messages, and Gemini Interactions — rather than flattening provider-specific reasoning, caching, tool-call, and replay behavior into a leaky abstraction.
- Host-owned profile persistence. This package validates and consumes
LLMProfilerecords but does not choose a global local profile database/path. Host products persist profile JSON in their own settings stores and pass selected profiles to the SDK. - Lower-risk secret handling. Do not port Python's plaintext/local plus encrypted-at-rest remote secret stack. Persist secret references only; store actual secret values in the OS keyring under the
openhandsservice. LLM API keys are provider-scoped by default, with per-profile overrides for cases like multiple proxy profiles for the same provider. - Tooling parity with
oh-tab. Same npm/build/test stack (tsup, vitest, eslint type-checked) unless there's a good reason to diverge.
providerId: 'openrouter', providerId: 'litellm_proxy', and custom OpenAI-compatible baseUrl profiles use OpenAIChatClient. Native tools are sent with the standard Chat Completions function shape, and tool calls/results use assistant tool_calls plus role: 'tool' messages. OpenRouter's standard endpoint and configurable LiteLLM-compatible/custom base URLs are covered by transport tests.
Compatibility here means the endpoint accepts the OpenAI Chat Completions dialect at <baseUrl>/chat/completions with bearer authentication. The SDK does not translate tools into an upstream provider's native Anthropic or Gemini dialect when that provider sits behind a proxy, and it does not guess nonstandard proxy payloads. Configure such gateways to expose the Chat Completions function-tool contract or provide a provider-specific adapter.
| Concern | Choice |
|---|---|
| Language | TypeScript 5.9, strict + extra safety flags |
| Runtime validation | zod v4 (pydantic equivalent; native JSON Schema) |
| Bundler | tsup (ESM + CJS) |
| Tests | vitest |
| Lint | eslint with recommended-type-checked |
npm install @smolpaws/openhands-agentFor local development in this repo:
npm install
npm run typecheck
npm run lint
npm test
npm run build
npm run test:examplesimport {
Agent,
ConversationState,
FinishTool,
LocalConversation,
llmProfileSchema,
messageSchema,
type LLMClient,
} from '@smolpaws/openhands-agent';
const llm: LLMClient = {
profile: llmProfileSchema.parse({ profileId: 'example', providerId: 'mock', model: 'mock' }),
async complete() {
return {
message: messageSchema.parse({
role: 'assistant',
content: null,
tool_calls: [
{
id: 'finish-1',
name: 'finish',
arguments: JSON.stringify({ message: 'Hello from TypeScript OpenHands.' }),
origin: 'completion',
},
],
}),
usage: null,
raw: {},
};
},
};
const state = new ConversationState();
const conversation = new LocalConversation({
agent: new Agent({ llm, tools: [FinishTool.create()] }),
state,
});
conversation.sendMessage('Say hello and finish.');
await conversation.run();
console.log(state.executionStatus);Runnable TypeScript examples live in examples/ and are checked by npm run test:examples. Real-LLM examples use examples/_shared/exampleProfile.ts: by default set OPENAI_API_KEY to run them against an OpenAI LLM profile, or set LLM_PROVIDER_ID/LLM_PROVIDER and the matching <PROVIDER>_API_KEY env var to exercise another provider. The helper stores keys under llmProviderSecretRef(profile.providerId), optionally overrides the model with OPENAI_MODEL or LLM_MODEL, and skips gracefully when no provider key is present. npm run live:gemini-tools is the opt-in Gemini native-tool smoke; Anthropic tool coverage is recorded-shape/unit-only and makes no live request by default.
| Example | Covers |
|---|---|
hello-world.ts |
Real OpenAI profile completion through the shared env-backed example profile helper |
native-openai-tools.ts |
Real OpenAI Responses read/edit/finish function calls through Agent tool dispatch |
native-gemini-tools.ts |
Credential-gated Gemini Interactions tool dispatch; defaults to gemini-3.5-flash-lite |
native-tool-serialization.ts |
Keyless comparison of one ToolDefinition across all four provider wire formats |
tools.ts |
Concrete terminal, file editor, glob, grep, and task tracker tools |
profiles-and-secrets.ts |
Provider/profile-scoped LLM API key references and secret store usage |
agent-settings.ts |
Agent settings/profile validation and profile-selected raw LLM field cleanup |
conversation-patterns.ts |
Real profile completion, pause/resume status, parallel tool execution, manual observation parsing, and stuck detection |
skills-and-context.ts |
Agent context, static skills, and keyword-triggered skill suffixes |
hooks.ts |
Hook config and pre-tool-use hook execution |
mcp.ts |
MCP tool definitions, action argument sanitization, and observations |
remote-workspace.ts |
Guarded remote workspace usage against an agent-server |
Work is tracked with Beads (bd). The source of truth is .beads/issues.jsonl.
bd list --status open
bd show openhands-agent-1MIT