@maniac-ai/agents
v0.4.1
Published
TypeScript port of the Maniac agents SDK.
Readme
@maniac-ai/agents
TypeScript port of the Maniac agents SDK. As of 0.4.0, the TS port is at feature parity with the Python SDK on the inference, runtime, observability, dispatcher, and ACP-server surfaces — native AnthropicModel, MCPToolset, OTelTracer, BackgroundTaskDispatcher, parallel tool execution, budget enforcement, structured output with repair, multimodal Message.content, Anthropic prompt caching, partial-turn persistence on errored / interrupted runs, and the Agent Client Protocol server (with plan primitive, capability-gated builtin tools, and the maniac-agents-acp bin) all ship in the published tarball. 0.4.0 also adds the TypeScript-only @maniac-ai/agents/channels surface for serving a registered agent on Slack / Discord / Telegram / Microsoft Teams / WhatsApp. See docs/reference/typescript/divergences.md for the remaining intentional differences.
The package mirrors the Python SDK's durable JSON contracts while using TypeScript-native ergonomics for tools and model adapters. REPL execution remains Python-backed: PythonSandboxClient speaks the existing subprocess worker protocol so top-level await, persistent namespace state, stdout/stderr capture, and injected callable RPC preserve Python behavior.
Install
npm install @maniac-ai/agentsESM-only. This package ships only the ECMAScript module entrypoint (
"type": "module"with norequirefield inexports). Use it from ESM consumers (import { runAgent } from "@maniac-ai/agents"), from TypeScript with"module": "esnext"or"nodenext", or from CommonJS via the dynamicawait import("@maniac-ai/agents")form. A directrequire("@maniac-ai/agents")call is not supported; track the dual-package roadmap in the repository changelog.
For local development in this repository:
cd typescript/packages/agents
npm install
npm testQuickstart
import { StaticModel, runAgent, tool } from "@maniac-ai/agents";
const lookup = tool({
name: "lookup_order",
description: "Look up an order by id.",
inputSchema: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"]
},
handler: ({ order_id }) => ({ order_id, status: "in_transit" })
});
const model = new StaticModel([
{
content: "",
usage: { prompt: 1, completion: 1 },
finish_reason: "stop",
tool_calls: [
{ id: "call_1", name: "lookup_order", arguments: { order_id: "ord_123" } }
]
},
{
content: "Your order is in transit.",
usage: { prompt: 1, completion: 4 },
finish_reason: "stop",
tool_calls: []
}
]);
const result = await runAgent(
{ id: "support", instructions: "Use tools.", model, tools: [lookup] },
"Where is my order?"
);For a live OpenAI-compatible provider, use the bundled adapter:
import { OpenAICompatibleModel, runAgent } from "@maniac-ai/agents";
const model = new OpenAICompatibleModel({
slug: "gpt-4o-mini"
});
const result = await runAgent(
{ id: "assistant", instructions: "Be concise.", model },
"Summarize the release checks."
);OpenAICompatibleModel reads OPENAI_API_KEY by default. OpenRouterModel is also exported and reads OPENROUTER_API_KEY. VercelGatewayModel targets the Vercel AI Gateway and reads AI_GATEWAY_API_KEY with a VERCEL_OIDC_TOKEN fallback.
Streaming Terminal Chat
For a minimal multi-turn terminal chat backed by a live provider:
cd typescript/packages/agents
OPENAI_API_KEY=... npm run example:chatThe chat uses OpenAICompatibleModel by default with OPENAI_MODEL falling back to gpt-4o-mini. Set OPENAI_BASE_URL for a local or proxy OpenAI-compatible endpoint.
To use OpenRouter instead:
cd typescript/packages/agents
AGENTS_PROVIDER=openrouter OPENROUTER_API_KEY=... npm run example:chatOPENROUTER_MODEL defaults to openai/gpt-4o-mini. Pass --verbose or set AGENTS_CHAT_VERBOSE=1 to print per-turn token usage. The chat supports /help, /clear, /exit, and /quit; /clear resets the in-memory conversation history while the process keeps running.
To verify the CLI starts without making a provider call:
npm run example:chat -- --helpSubagent Stream Contract
Future subagent support should render nested model output inline in the same visible assistant turn. The terminal renderer should consume runAgentStream events in order and print text tokens where they arrive, including tokens from nested agents, so output continues under the same assistant > response block instead of jumping to a separate transcript.
Trace metadata such as depth, principal, span_id, and parent_span_id can route events internally and power verbose/debug annotations. Normal chat mode should keep the user-facing output visually continuous with the main answer, and should not buffer subagent text for replay later except for tiny terminal-smoothing chunks.
Persistent Memory
InMemoryMemory is the default Memory and lives entirely in process. For durable storage on a single host, the package ships a SQLite adapter built on Node's bundled node:sqlite module:
import { SqliteMemory } from "@maniac-ai/agents/memory/sqlite";
import { ConversationStore } from "@maniac-ai/agents/memory";
const memory = new SqliteMemory({ filename: "./agent.db" });
await memory.setup();
const conversations = new ConversationStore(memory);
await conversations.saveTurn("support", "thread-1", [
{ role: "user", content: "hi" }
]);SqliteMemory adds zero npm dependencies and no native build step but requires Node >= 22.5.0 (use --experimental-sqlite on Node 22.x; unflagged from 23.5+). It implements both Memory and the optional MemorySequencer protocol, so it drops into ConversationStore, ObservationStore, RunCheckpointStore, and BackgroundTaskStore without further changes. See docs/sqlite-memory.md for the schema, setup() semantics, BYO DatabaseSync handle, and the "when to reach for Postgres instead" guidance.
Honcho memory (managed reasoning layer)
HonchoMemory opts into Honcho, a hosted reasoning layer that maintains per-peer representations server-side. Wiring it in swaps the default ConversationStore for HonchoConversationStore (loads the prefix via session.context({ tokens, peerTarget }).toOpenAI(...), mirrors writes via session.addMessages) and auto-injects the ask_about_user(query) LM tool that wraps the Dialectic API. @honcho-ai/sdk is an optional peer dependency loaded lazily.
import { HonchoMemory, Maniac } from "@maniac-ai/agents";
const memory = new HonchoMemory({
apiKey: process.env.HONCHO_API_KEY!,
environment: "production"
});
const app = new Maniac({ model, memory });
app.agent({ id: "support", instructions: "Help the user." });
await app.chat("support", "hi", { threadId: "thread-1", resourceId: "user-42" });See the docs/how-to/use-honcho-memory.md recipe for the full surface (peer attribution, working-memory peer-card mirror, observational-memory advisory, demo vs production semantics).
Persistence on errored / interrupted turns
From v0.3 onwards, Maniac.chat / chatStream persist the partial transcript when a run finishes with status: "errored" (budget / output-validation / guardrail block) and when the runner aborts mid-turn by raising. RunCancelledError (caller AbortSignal fired) and RunInterruptedError (LM provider crash, sub-agent throw) both carry the in-flight transcript as error.partial; chat peels the wrapper, persists the new turn delta, and re-throws the original cause for non-cancel throws. (RunCancelledError continues to surface as an errored AgentResult from runAgent, just enriched with messages from the partial.)
Before writing, the persisted turn is sealed: every tool_call.id from the trailing assistant message that didn't get a tool reply gets a synthetic { ok: false, error: "tool call interrupted before completion" } observation. Without sealing, the next request would be rejected by every native tool-calling provider.
Reflectors (ObservationBuffer.afterTurn, WorkingMemoryRunner.afterTurn) still only run on completed turns. Set Maniac({ persistPartialOnError: false }) to opt out and restore the v0.2 "errored runs are not written" behaviour.
Python Worker
PythonSandboxClient starts python -u -m runtime.adapters._subprocess_worker. In this repository it auto-detects .venv/bin/python when available and prepends the repository root to PYTHONPATH.
In a packaged application, pass pythonExecutable and pythonPath if the worker module is shipped separately:
import { PythonSandboxClient } from "@maniac-ai/agents/runtime";
const sandbox = new PythonSandboxClient({
pythonExecutable: "/path/to/python",
pythonPath: ["/path/to/maniac-agents"]
});For sidecar and process-image deployment notes, see docs/repl-python-worker.md.
Streaming Resume
Pause-then-resume runs (e.g. an approval-pending agent that the operator approved out of band) can now be streamed from the resume call:
import { runAgentResumeStream } from "@maniac-ai/agents";
for await (const env of runAgentResumeStream(spec, checkpoint, responses)) {
if (env.type === "event") render(env.event);
else handleResult(env.result);
}runAgentResumeStream mirrors runAgentStream ergonomics (live StreamEnvelope events terminated by a single { type: "result" } envelope) and is also exposed as Maniac.resumeStream(...) on the app entrypoint.
Agent Client Protocol (ACP)
@maniac-ai/agents/acp exposes any registered Maniac agent over the Agent Client Protocol so editors (Zed, Neovim, Gemini CLI) can drive Maniac agents through the same JSON-RPC subprocess transport they already speak. Install the optional @agentclientprotocol/sdk peer dep, then:
import { Maniac } from "@maniac-ai/agents";
import { serveAcpStdio } from "@maniac-ai/agents/acp";
const app = new Maniac({ /* agents, model, conversationStore, ... */ });
await serveAcpStdio(app, "support");The package also ships a maniac-agents-acp CLI bin (registered in package.json's bin field) that mirrors the Python maniac acp subcommand:
npx maniac-agents-acp serve <agent_id> --app ./path/to/app.js:app
npx maniac-agents-acp client ./external-acp-agent --prompt "hello"Plan-aware agents (Agent.plans_enabled = true) auto-inject the LM-facing set_plan tool whose emissions surface as ACP session/update sessionUpdate=plan notifications. Capability-gated builtin tools (acp_read_text_file / acp_write_text_file / acp_run_command) auto-register onto the agent for the duration of every prompt turn whose ACP client advertised the matching capability.
See docs/reference/typescript/acp.md for the full reference and docs/concepts/acp.md for the cross-language design.
Trace Event Correlation IDs
Every trace event carries the standard envelope (run_id, event_id, seq, span_id, parent_span_id) and now optionally surfaces the following correlation IDs:
turn_id— UUID per LM iteration. Set onstep,lm_call_start,token,lm_call, and downstream events emitted within the same iteration.message_id— UUID for the assistant message produced by the LM iteration. Tokens that contributed to the message and the correspondinglm_callevent share this ID.block_id— UUID per contiguous run of the samechunk_kindontokenevents.thread_id— Caller-scoped conversation thread, sourced fromRunOptions.threadIdand propagated through nested runs.
All four fields are optional; consumers that do not need them can safely ignore them. JSON Schema for the trace envelope is published at @maniac-ai/agents/trace-event.schema.json (also written to dist/trace-event.schema.json during the build).
Tool Result Previews
tool completed and failed events now include an optional result_preview describing the underlying ToolResult so trace consumers can render the value without a separate join. The preview is JSON-safe and capped at ~16 KB; oversized values are stringified and elided with result_truncated: true. The raw ToolResult continues to flow through messages unchanged.
Tool Argument Validation
When provider streams emit malformed tool-call arguments (incomplete JSON, non-object payloads, etc.), mergeStreamChunks and the OpenAI-compatible adapter no longer silently coerce to {}. The merged response carries arguments_error plus the raw provider text on the affected ToolCall, and the runner emits a structured tool failed event with reason: "tool_arguments_invalid" instead of invoking the handler with empty arguments.
Reasoning configuration
Reasoning-capable model families (OpenAI gpt-5 / o-series, Anthropic extended thinking, DeepSeek-R1, etc.) accept different parameter names on the wire. The SDK exposes a single normalized ReasoningConfig shape on both InferenceRequest.reasoning and Agent.reasoning, and the adapters translate it to the provider-native field:
| Field | OpenAI-compatible (OpenAICompatibleModel, OpenRouterModel) | Anthropic (AnthropicModel) |
| --- | --- | --- |
| effort: "minimal" \| "low" \| "medium" \| "high" | top-level reasoning_effort | mapped to thinking.budget_tokens (1024 / 4096 / 10000 / 24000) |
| max_tokens: number | (ignored — Responses-API only) | thinking.budget_tokens (exact value) |
| summary: "auto" \| "concise" \| "detailed" | (ignored — Responses-API only) | (no equivalent — ignored) |
Set it once on Agent to apply to every LM call the runner makes for that agent:
import { runAgent, OpenAICompatibleModel, AnthropicModel, type Agent } from "@maniac-ai/agents";
const spec: Agent = {
id: "researcher",
instructions: "Be thorough.",
model: new OpenAICompatibleModel({ slug: "gpt-5", apiKey: process.env.OPENAI_API_KEY }),
reasoning: { effort: "high" }
};
await runAgent(spec, "What are the second-order effects of...?");When Anthropic enables extended thinking the adapter automatically bumps max_tokens above budget_tokens (Anthropic requires max_tokens > budget_tokens). A prepare_step hook can override the spec-level reasoning per turn by setting request.reasoning on the returned StepDecision; the runner does not merge the spec default on top of a hook-supplied request.
Adapters that don't recognize the field (e.g. older OpenAI chat models that reject reasoning_effort) surface the provider error verbatim; the SDK does not strip the field silently.
On the response side, OpenAICompatibleModel surfaces provider reasoning text from both wire variants:
message.reasoning/delta.reasoning— OpenRouter's normalized form, used bymicrosoft/mai-ds-r1,qwen/qwen3-235b-a22b,x-ai/grok-3-mini-beta, etc.message.reasoning_content/delta.reasoning_content— DeepSeek-R1, vLLM, SGLang, Qwen native form.
It populates InferenceResponse.reasoning on infer() and emits StreamChunk(kind: "reasoning") deltas on stream(), so consumers see the reasoning channel on both paths regardless of which provider naming the upstream chose.
Feature surface (0.4.0)
- Inference adapters:
OpenAICompatibleModel,OpenRouterModel,VercelGatewayModel, nativeAnthropicModel(streaming, tool-use, extended-thinking reasoning chunks, prompt-cache passthrough),RetryingModel,FallbackModel,StaticModel. ImplementModeldirectly to integrate any other provider. - Model discovery: adapters that can enumerate their catalog implement
listModels()(feature-detect withsupportsModelCatalog(model)), returning normalizedModelSpec[](context_window, per-tokenModelPricing, modalities,supported_parameters, and the verbatim provider entry). Adapters that cannot list throwModelCatalogUnsupportedError. - Generation parameters:
InferenceRequestaccepts normalized OpenAI-compatible sampling knobs —top_p,top_k,frequency_penalty,presence_penalty,seed,logit_bias,parallel_tool_calls, anduser. Each is omitted from the wire payload unless set; adapters translate or ignore per provider (e.g. Anthropic mapsusertometadata.user_id). - Multimodal & prompt caching:
Message.contentaccepts bothstring(back-compat) andContentPart[](text / image / file with optionalcache_control: { type: "ephemeral" }). Anthropic forwardscache_controlnatively; OAI-compat adapters strip it with a one-time warning. - Toolsets:
BaseToolset,StaticToolset,MCPToolset(under@maniac-ai/agents/tools/adapters; speaksstdio/sse/streamable_http).@modelcontextprotocol/sdkis an optional peer dep. - Compaction:
LMSummarizingCompactorand the lower-levelsafeTailStarthelper, both under@maniac-ai/agents/agents. - Memory:
InMemoryMemory(default), plusSqliteMemory(@maniac-ai/agents/memory/sqlite, zero-dependencynode:sqlite),VectorMemory(semantic-search wrapper with a defaultInMemoryVectorIndex), andHonchoMemory(@maniac-ai/agents/memory/honcho, hosted reasoning layer). Memory-backedConversationStore,ObservationStore,RunCheckpointStore,BackgroundTaskStore, andWorkingMemoryStoreall share the oneMemoryprotocol. - Runtime: parallel tool execution,
AgentBudget(max_tokens/max_cost_usd/max_wall_seconds) enforcement, structured output (Agent.output_model) withoutput_repair_attempts, reasoning stream chunk kind, paused/resume + checkpointing. - Background tasks:
BackgroundTaskDispatcherwith concurrency caps, lifecycle hooks, andManiac.runUntilIdle/runStreamUntilIdle/enqueueBackground.Maniac.backgroundTasksis no longer@deprecated. - Observability:
OTelTracerunder@maniac-ai/agents/observability, mapping everyTraceEventto OTel spans following the GenAI semantic conventions.@opentelemetry/apiand@opentelemetry/sdk-trace-baseare optional peer deps. - Streaming:
runAgentStreamandrunAgentResumeStreamemit a taggedStreamEnvelope<T>({ type: "event" } | { type: "result" }); cancellation viaRunOptions.signal: AbortSignal. - Channels (TypeScript-only):
@maniac-ai/agents/channels(serveChannels+ChatToolset+ framework-agnostic webhook helpers) wraps Vercel's Chat SDK to serve a registered agent on Slack / Discord / Telegram / Microsoft Teams / WhatsApp, with structured multimodal inbound, multi-user attribution, signed-webhook + idempotency hardening, and approval-card round-trips throughManiac.resumeCheckpoint. Behind optionalchat/@chat-adapter/*peers; Python parity is tracked as a divergence. - JSON Schema artifact:
@maniac-ai/agents/trace-event.schema.jsonfor non-TS consumers (Python validators, IDE tooling, fixture generators).
