Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
- Work is tracked in Beads (`bd`). Check open Beads before starting follow-up work.
- The examples GitHub Environment now provides `OPENAI_API_KEY`, `GEMINI_API_KEY`, and `ANTHROPIC_API_KEY` to `.github/workflows/examples.yml`.
- Use `createClientFromProfile(profile, store)` for generic LLM profile dispatch. It routes `providerId`/detected provider to Anthropic, Gemini, OpenAI Responses, or OpenAI-compatible chat. Product/REST callers should select `LLMProfile` records; use explicit provider factories such as `createOpenAIChatClientFromProfile` only for advanced SDK tests or provider-specific code.
- `Agent.step()` passes only usable `ToolDefinition` instances to `LLMClient.complete`; OpenAI clients serialize native tools and omit the request field when none are present. `npm run live:openai-tools` proves real read/edit/finish dispatch with `gpt-5-nano`.

- Gemini `thoughtSignature` round-trip is verified live for Gemini 3.x models (`gemini-3.5-flash`, `gemini-3.1-pro-preview`) using `thinkingConfig.thinkingLevel`. Gemini 2.5 `thinkingBudget` support is intentionally closed as wont-fix because those models are old/unavailable for this SDK target.
- `Agent.step()` passes only usable `ToolDefinition` instances to `LLMClient.complete`; provider clients serialize native declarations and omit the request field when none are present. Anthropic uses `tool_use`/`tool_result`; Gemini uses stateless `/v1beta/interactions` step replay; OpenAI-compatible routes use the Chat Completions function dialect.
- `npm run live:openai-tools` proves real read/edit/finish dispatch with `gpt-5-nano`. `npm run live:gemini-tools` proves real `lookup_value`/finish dispatch and was verified with `gemini-3.5-flash-lite`. Anthropic tool tests use recorded shapes only because the key has no credit.
- Gemini signed thought round-trip now uses Interactions `thought` steps with lower-case `generation_config.thinking_level`; the previous GenerateContent `thinkingConfig` path is no longer used by `GeminiClient`.
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Idiomatic TypeScript transpilation of the [OpenHands](https://github.com/OpenHan

## Status

`0.3.3` is the native OpenAI tool-completion parity release of the fresh TypeScript transpilation. It covers the core SDK surfaces needed to build and run agent loops locally, passes usable Agent tools through OpenAI Chat Completions and Responses, and documents the main architecture in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md):
`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`](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
Expand All @@ -18,7 +18,7 @@ Intentional deviations from Python remain: no ACP runtime, security analyzers, r

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 `ToolDefinition` propagation, and provider-owned OpenAI chat completions/Responses, Anthropic, and Gemini request/response mapping
- LLM message/content serialization, Agent-to-LLM `ToolDefinition` propagation, and provider-owned OpenAI chat completions/Responses, Anthropic Messages, and Gemini Interactions request/response mapping
- event schemas and `eventsToMessages` conversion, 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
Expand All @@ -32,11 +32,18 @@ Accepted deviations are deliberate and should not be treated as missing work unl
- **Type enforcement.** Strict TypeScript everywhere; runtime validation via [zod v4](https://github.com/colinhacks/zod) (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-tab` is 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-style `LLM` objects 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 GenerateContent — rather than flattening provider-specific reasoning, caching, tool-call, and replay behavior into a leaky abstraction.
- **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 `LLMProfile` records 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 `openhands` service. 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.

## OpenAI-compatible native tools

`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.


## Tooling

| Concern | Choice |
Expand Down Expand Up @@ -112,12 +119,14 @@ console.log(state.executionStatus);

## Examples

Runnable TypeScript examples live in [`examples/`](examples/) and are checked by `npm run test:examples`. Real-LLM examples use [`examples/_shared/exampleProfile.ts`](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.
Runnable TypeScript examples live in [`examples/`](examples/) and are checked by `npm run test:examples`. Real-LLM examples use [`examples/_shared/exampleProfile.ts`](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`](examples/hello-world.ts) | Real OpenAI profile completion through the shared env-backed example profile helper |
| [`native-openai-tools.ts`](examples/native-openai-tools.ts) | Real OpenAI Responses read/edit/finish function calls through Agent tool dispatch |
| [`native-gemini-tools.ts`](examples/native-gemini-tools.ts) | Credential-gated Gemini Interactions tool dispatch; defaults to `gemini-3.5-flash-lite` |
| [`native-tool-serialization.ts`](examples/native-tool-serialization.ts) | Keyless comparison of one `ToolDefinition` across all four provider wire formats |
| [`tools.ts`](examples/tools.ts) | Concrete terminal, file editor, glob, grep, and task tracker tools |
| [`profiles-and-secrets.ts`](examples/profiles-and-secrets.ts) | Provider/profile-scoped LLM API key references and secret store usage |
| [`agent-settings.ts`](examples/agent-settings.ts) | Agent settings/profile validation and profile-selected raw LLM field cleanup |
Expand Down
10 changes: 6 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Provider clients live next to the neutral model:
- `OpenAIChatClient` for chat completions and compatible proxies.
- `OpenAIResponsesClient` for the Responses API.
- `AnthropicMessagesClient` for Anthropic Messages.
- `GeminiClient` for Gemini.
- `GeminiClient` for the Gemini Interactions API.

The product boundary is profile-first: `createClientFromProfile(profile, secretStore)` resolves a concrete client from an `LLMProfile`. Product and REST callers select profiles; they do not instantiate a raw Python-style `LLM`, pass loose model/provider fields, or rely on implicit default models. Low-level provider clients and provider-specific factories remain exported only as explicit advanced SDK/test building blocks.

Expand All @@ -127,10 +127,12 @@ The four provider APIs are implemented as the APIs they actually are, not hidden

- OpenAI-compatible Chat Completions owns chat-completions request/response shape and compatible proxy behavior.
- OpenAI Responses owns Responses-specific input, tool, reasoning, and replay fields.
- Anthropic Messages owns Anthropic content blocks, prompt caching, and extended-thinking details.
- Gemini owns GenerateContent parts, function-call parts, `thoughtSignature` round-tripping, and Gemini thinking config.
- Anthropic Messages owns Anthropic content blocks, prompt caching, extended-thinking details, `tool_use` calls, and `tool_result` continuation.
- Gemini owns Interactions steps, flat function tools, signed thought replay, `function_call` parsing, and `function_result` continuation.

For OpenAI, Chat Completions wraps the schema produced from `ToolDefinition.toResponsesTool()` in its nested function-tool shape, while Responses uses the helper's native top-level shape. Both omit the wire-level `tools` field when the supplied list is empty. These are provider-client concerns; the shared completion interface carries `ToolDefinition`s without a parallel DTO layer.
Every client receives the same optional `ToolDefinition[]` through `LLMClient.complete()` and derives its provider declaration from `ToolDefinition.toResponsesTool()`. Chat Completions wraps that schema in its nested function shape; Responses uses the top-level shape; Anthropic renames `parameters` to `input_schema`; Gemini uses a flat function declaration and removes JSON Schema fields its API rejects. All clients omit the wire-level `tools` field when the supplied list is empty.

Gemini requests use documented stateless Interactions mode (`store: false`) and reconstruct `user_input`, `thought`, `model_output`, `function_call`, and `function_result` steps from the durable neutral transcript. This keeps conversation restore and forks correct instead of coupling an SDK client instance to server-side `previous_interaction_id` state. Signed thought steps round-trip through `Message.thinking_blocks`.

`oh-tab/packages/agent-sdk` was used as inspiration for product-level profile semantics, key lookup shape, and build/test tooling expectations. It was not copied: the implementation is fresh TypeScript, and the older package remains reference-only.

Expand Down
Loading