npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dex-ai/sdk

v0.1.35

Published

Agent SDK: interfaces + createAgent() + generate loop. One package.

Readme

@dex-ai/sdk

An agent SDK for TypeScript + Bun. One package: interfaces, Agent.create(), and the generate loop.

Layout

The SDK lives in its own repo (dex-ai-sdk). Provider implementations live in a sibling monorepo (dex-ai-providers):

workplace/
├── dex-ai-sdk/                           (this repo — @dex-ai/sdk)
│   ├── src/                              # types + define* helpers + Agent.create + generate loop
│   ├── examples/                         # 8 runnable .ts examples
│   └── docs/                             # SVG diagrams
└── dex-ai-providers/                     (separate repo — providers monorepo)
    └── providers/
        └── openai/                       # @dex-ai/openai — OpenAI Chat Completions

@dex-ai/openai is referenced from examples/package.json via file:../../dex-ai-providers/providers/openai. Clone both side by side for local dev.

Usage

import { Agent, Tool } from '@dex-ai/sdk';
import { openai } from '@dex-ai/openai';
import { z } from 'zod';

const getWeather = Tool.define({
  name: 'get_weather',
  description: 'Look up current weather.',
  parameters: z.object({ city: z.string() }),
  async execute({ city }) {
    return { type: 'json', value: { city, tempC: 18 } };
  },
});

const agent = await Agent.create({
  name: 'demo',
  provider: openai({ modelId: 'gpt-4.1' }),
  extensions: [{ name: 'tools', tools: [getWeather] }],
});

// Non-streaming:
const r = await agent.generate({
  input: [{ role: 'user', content: [{ type: 'text', text: 'Weather in Paris?' }] }],
}).result;

// Streaming:
const h = agent.generate({ input: [/* ... */] });
for await (const part of h) {
  if (part.type === 'text-delta') process.stdout.write(part.delta);
  if (part.type === 'message-committed') saveToDb(part.message);
}
const result = await h.result;

Design decisions

One package, two shapes

@dex-ai/sdk ships both the types (so third-party providers and extensions can depend on only the type surface via import type) and the runtime (Agent.create + the loop). There is no separate types-only package.

Content model (flat, ai-sdk aligned)

type Content =
  | { type: 'text'; text }
  | { type: 'image'; image: string|Uint8Array|URL; mediaType? }
  | { type: 'file';  data: string|Uint8Array|URL; mediaType; name? }
  | { type: 'reasoning'; text }
  | { type: 'tool-call'; toolCallId; toolName; input }
  | { type: 'tool-result'; toolCallId; toolName; output: ToolOutput };

type ToolOutput =
  | { type: 'content';    value: ContentPayload[] }   // text / image / file
  | { type: 'json';       value: unknown }
  | { type: 'text';       value: string }
  | { type: 'error-text'; value: string }
  | { type: 'error-json'; value: unknown };

Two contexts, two lifetimes

AgentContext is persistent — created by Agent.create(), owns messages: Message[], dies with agent.dispose(). GenerateContext is fresh per generate() call, owns the in-flight assistant content: Content[], lives across all iterations of one generate.

One generate() method

agent.generate(opts) returns an AgentStream: async-iterable AND has a .result: Promise<GenerateResult>. Same run drives both.

Approval via onToolCall

No dedicated approval surface. An approver extension implements onToolCall: return a ToolResult (with error-text) to reject, a rewritten ToolCall to modify, or void to pass through. Rejections flow back to the LLM as data.

Schema-agnostic

Tool.parameters takes any Standard Schema implementation — zod, valibot, arktype. The provider converts to JSON Schema.

Core Loop Architecture

The generate loop is event-driven. Extensions subscribe to events via ext.on['event-name']. The loop orchestrates model calls, tool dispatch, and context injection purely through events.

Agent Creation (Agent.create)

  1. Collect extensions from options
  2. Run ext.init(actx) on each extension (registration order)
  3. Set up tool result cache (if configured)
  4. Assemble system prompt into a single system message:
    • opts.systemPrompt (base identity/instructions)
    • Skills from extensions: evaluate skill.when(), render skill.content, build catalog
    • session-start event: extensions yield AsyncIterable<Content> for system prompt
  5. Reorder messages: system first, then conversation history
  6. Append seed messages

Generate Loop Phases

Each agent.generate(opts) call runs:

Phase A: Input Processing

| Step | Action | Event | |------|--------|-------| | A1 | Extensions augment input messages | generate-input | | A2 | Commit input to actx.messages | message-committed (stream) | | A3 | Collect ephemeral turn context | generate-startAsyncIterable<Content> | | A4 | Build ephemeral context message (step 0 only, never persisted) | — |

Phase B: Iteration Loop (one model call + tool execution)

| Step | Action | Event | |------|--------|-------| | B1 | Build ModelRequest (messages + tools + params) | — | | B2 | Inject ephemeral context (step 0 only) | — | | B3 | Extensions transform the request (reducer) | model-startModelRequest \| void | | B4 | Compute cache breakpoints (Anthropic-style) | — | | B5 | Iteration begins | message-start | | B6 | Stream from model — consume model.stream(req) | text-delta, reasoning-delta, tool-call-delta, tool-call, message-stop, finish | | B7 | Commit assistant message | message-committed (stream) | | B8 | Assistant output finalized | message-stop | | B9 | Update actx.tokenCount | — | | B10 | Model call complete | model-stop |

Phase C: Tool Dispatch (per tool call)

| Step | Action | Event | |------|--------|-------| | C1 | Extensions intercept/modify/short-circuit | tool-startToolCall \| ToolResult \| void | | C2 | Validate input against tool schema | — | | C3 | Execute tool (raced with timeout + abort) | tool-execute-start (stream) | | C4 | Extensions transform result (reducer) | tool-stopToolResult \| void | | C5 | Commit tool-result message | tool-execute-finish + message-committed (stream) |

Phase D: Continue or Stop

shouldContinue = finishReason == "tool-calls"
                 && pendingToolCalls > 0
                 && step + 1 < maxSteps

If continuing → increment step, go back to Phase B.

Phase E: Finalization

| Step | Action | Event | |------|--------|-------| | E1 | Turn complete | generate-stop (always fires, even on error) | | E2 | Build GenerateResult | — | | E3 | Close stream | generate-finish (stream) |

Event Hook Summary

┌─ Session Lifecycle ─────────────────────────────────────────────┐
│  session-start     → yield Content for system prompt            │
│  session-stop      → cleanup on agent.dispose()                 │
└─────────────────────────────────────────────────────────────────┘

┌─ Per generate() call ───────────────────────────────────────────┐
│  generate-input    → augment input messages                     │
│  generate-start    → yield ephemeral turn context               │
│                                                                 │
│  ┌─ Per iteration (model call) ──────────────────────────────┐  │
│  │  model-start       → transform ModelRequest (reducer)     │  │
│  │  message-start     → observe                              │  │
│  │  text-delta        → observe streaming text               │  │
│  │  reasoning-delta   → observe reasoning tokens             │  │
│  │  tool-call-delta   → observe tool JSON streaming          │  │
│  │  message-stop      → observe committed message            │  │
│  │  model-stop        → observe                              │  │
│  │                                                           │  │
│  │  ┌─ Per tool call ─────────────────────────────────────┐  │  │
│  │  │  tool-start  → intercept / modify / short-circuit   │  │  │
│  │  │  tool-stop   → transform result (reducer)           │  │  │
│  │  └─────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  generate-stop     → finalize (always fires)                    │
└─────────────────────────────────────────────────────────────────┘

┌─ Cross-cutting ─────────────────────────────────────────────────┐
│  error             → observe any error + ErrorSource context    │
└─────────────────────────────────────────────────────────────────┘

Extension Interaction Patterns

| Pattern | Events | Description | |---------|--------|-------------| | Reducer | model-start, tool-start, tool-stop | Return transformed value or void (no-op) | | Observer | text-delta, message-stop, generate-stop | Observe but don't modify | | Context injection | session-start, generate-start, generate-input | Yield content for system prompt or turn context | | Short-circuit | tool-start | Return a ToolResult to skip tool execution | | Ephemeral context | generate-start | Content injected into step 0 request but never persisted |

Stream Parts

The AgentStream merges provider parts with loop-synthesized parts:

  • Provider: response-start, text-delta, reasoning-delta, tool-call-delta, tool-call, message-stop, finish, response-stop, error, abort
  • Loop: generate-start, generate-finish, iteration-start, iteration-finish, tool-execute-start, tool-execute-finish, message-committed, extension-info

Message Integrity (Messages class)

The conversation history is protected by the Messages class — a guarded container that prevents extensions from corrupting the message sequence.

The problem

Extensions previously cast actx.messages as Message[] to bypass the ReadonlyArray type and directly mutate the array (splice, push, etc.). This caused multiple corruption bugs where tool-calls had no matching tool-results, or role alternation was violated.

The solution

actx.messages is now backed by a Proxy that traps all direct mutations at runtime:

// These all throw at runtime — even with `as any` casts:
actx.messages.push(msg);        // ❌ "Direct .push() is not allowed"
actx.messages.splice(0, 1);     // ❌ "Direct .splice() is not allowed"
actx.messages[0] = msg;         // ❌ "Direct index assignment is not allowed"
actx.messages.length = 0;       // ❌ "Direct array mutation is not allowed"

Extensions MUST use actx.messageStore for controlled mutations:

const store = actx.messageStore;

// Append (validates after)
store.append(message);

// Splice with validation
store.splice(start, deleteCount, ...replacements);

// Replace a range
store.replaceRange(start, end, newMessages);

// Replace single message
store.replace(index, newMessage);

// Batch mode — defer validation for multi-step mutations
store.batch();
store.splice(0, 10, ...compressed);
store.append(summary);
store.commit(); // validates here, throws if invalid

// Or use transaction() for auto-commit:
store.transaction(() => {
  store.splice(warmStart, warmCount, ...summarized);
});

Invariants enforced

Every mutation (or commit() in batch mode) validates:

  1. System messages at front — contiguous prefix only
  2. First non-system is user — conversation must start with a user turn
  3. Role alternationuserassistanttool* → user (tool only after assistant)
  4. Tool-call/tool-result pairing — every tool-call in an assistant message must have a matching tool-result
  5. No orphaned tool-results — every tool-result must reference a preceding tool-call

Violations throw with a detailed error listing the indices and what went wrong.

Error Handling

  • reportError(err, source) — structured logging + emits extension-info error part + calls ext.on['error'] on all extensions
  • Orphaned tool-call repair — if the loop throws mid-tool-dispatch, synthesizes error tool-result messages so history stays valid
  • generate-stop always fires — even in catch path, so session persistence can flush

Diagrams

| File | Shows | |------|-------| | docs/architecture.svg | Top-level layers: App → Agent → {Provider, Extensions → Tools}. | | docs/packages.svg | Package graph: @dex-ai/sdk, @dex-ai/openai, examples, deps. | | docs/interfaces.svg | Every exported interface on one canvas. | | docs/contexts.svg | AgentContext vs GenerateContext — lifetimes, mutation, integrity. | | docs/core-loop-sequence.svg | Sequence diagram of one generate() call with hook fire points. | | docs/extension-interface.svg | All hooks grouped by lifetime and kind. | | docs/stream-parts.svg | AgentStreamPart timeline: provider + loop-synthesized parts. |

Dev

bun install
bun run typecheck
bun run examples/01-non-streaming.ts