Changelog

Breaking changes, new APIs, and migration guide for the latest release.

v0.5.0

Resolvable agent options

Agent options (model, system, activeTools, generation, skills) now accept Resolvable<T> — a static value or a function evaluated fresh on every LLM step. This replaces send-level overrides and session-stored config.

const myAgent = agent("my-agent", {
  model: "anthropic/claude-sonnet-4-6",
  system: async ({ context }) => {
    const rules = await fetchRules(context.projectId);
    return `You are a coding assistant.\n${rules}`;
  },
  activeTools: ({ messages }) => {
    const inPlanMode = messages.some(m =>
      m.parts.some(p => "type" in p && p.type === "tool-EnterPlanMode")
    );
    return inPlanMode ? ["Read", "Grep", "List"] : ["Read", "Write", "Edit", "Grep", "List", "Bash"];
  },
});

Resolvable functions receive { context, sessionId, sandbox, messages } — the same shape as ToolContext.

Simplified session model

Session is now minimal: { id, sandboxId, lastMessageId, createdAt, updatedAt }. Runtime config (model, system, activeTools, generation, skills) is no longer stored on the session — it's resolved from agent options each step.

  • session.update() is scoped to { sandboxId?, lastMessageId? }. To change model, system, or tools dynamically, use resolvable agent options with context.
  • SendOptions no longer accepts model, system, activeTools, generation, or skills overrides. Pass context instead and use resolvable agent options to derive config.

Before

await session.send("hello", {
  model: "anthropic/claude-opus-4.6",
  system: "Be concise.",
  activeTools: ["Read", "Write"],
});

After

const myAgent = agent("my-agent", {
  model: ({ context }) => context.useAdvanced
    ? "anthropic/claude-opus-4.6"
    : "anthropic/claude-sonnet-4-6",
  system: ({ context }) => context.systemPrompt,
  activeTools: ({ context }) => context.tools,
});

await session.send("hello", {
  context: { useAdvanced: true, systemPrompt: "Be concise.", tools: ["Read", "Write"] },
});

ToolContext changes

ToolContext is now ResolvableArgs — the same shape used by resolvable option functions:

BeforeAfter
context.sessioncontext.sessionId
context.storage(removed)
context.sandboxcontext.sandbox (unchanged)
context.contextcontext.context (unchanged)
context.messages (new)

Other changes

  • Resolvable, ResolvableArgs exports — New types for building resolvable option patterns.
  • activeTools accepts readonly arraysas const tool lists work without casts.
  • localStorage() auto-gitignore — Emits a .gitignore in the storage directory on first write to prevent session data from being tracked by git.
  • SystemPromptFn, SystemPromptInput removed — System is now Resolvable<string | string[] | undefined>. Arrays are joined with newlines; falsy values are filtered.
  • Default sandbox setup options — Sandbox bindings accept default setup options applied on first sandbox creation.
  • session.sandbox handle — Access the session's sandbox outside of tool execution without knowing the sandbox ID.
  • Durable message metadata — Type-safe metadata on messages via agent<Metadata>() generic.

v0.4.0

KV2 storage auto-fallback

When no storage is configured, the agent automatically falls back to KV2-backed storage when available.

// explicit storage — nothing changes
const myAgent = agent("my-agent", {
  storage: myCustomStorage(),
});

// no storage — auto-falls back to KV2
const myAgent = agent("my-agent", {});
  • BLOB_READ_WRITE_TOKEN must be present in the environment for it to fallback to KV2
  • Keys are namespaced under the agent name (e.g. "my-agent/")
  • @vercel/kv2 is dynamically imported on first storage access
  • Uses "use step" for workflow durability
  • Falls back to localStorage() when the token is not set (e.g. local dev)

v0.3.0

Redesigned storage and workflow. Storage is now a handler map you provide directly. Workflow is fully opt-in.

Storage

Storage backends ({ type: "vercel" | "local" | "custom" }) are replaced by a storage() function with inline handlers.

Before

agent({ storage: { type: "vercel" } })

After

Storage is a function that receives a store and dispatches to your handlers. To add workflow durability, add "use step" at the top — that's the only difference.

agent("my-agent", {
  async storage(store) {
    "use step"; // remove this line if you don't use workflow

    return await store.on({
      "session.get": async ({ id }) => await db.sessions.findById(id),
      "session.set": async ({ id, value }) => await db.sessions.upsert(id, value),
      "session.update": async ({ id, updates }) => await db.sessions.update(id, updates),
      // ... message, part, sandbox handlers
    });
  },
})

For development, use the built-in localStorage(). For Vercel deployments, use @vercel/kv2 (see the vade app for a full example).

Session

session() is now synchronous. send() accepts the message directly and returns a SendResult.

Before

const session = await myAgent.session(chatId);
await session.send({ input: message });
const stream = await session.stream();

After

const session = myAgent.session(chatId);
await session.send(message, opts);
const stream = await session.stream();

send() returns SendResult { assistantMessageId, done } instead of void | Error.

Workflow

Workflow is no longer always-on. Everything works without it. To add durability, define a workflow function and start it yourself.

Before

The withAgent Next.js plugin in next.config.ts automatically wired workflow durability behind the scenes. You didn't opt in — it was always on.

// next.config.ts
import { withAgent } from "experimental-agent/next";
export default withAgent(nextConfig);

// route.ts — workflow was implicit, handled by the plugin
const session = await myAgent.session(chatId);
await session.send({ input: message });
const stream = await session.stream();

After

// workflow.ts — you define the workflow function
export async function agentWorkflow(
  sessionId: string,
  ...args: SessionSendArgs<typeof myAgent>
) {
  "use workflow";
  return await myAgent.session(sessionId).send(...args);
}

// route.ts — you start it and stream from the result
import { start } from "workflow/api";

const result = await start(agentWorkflow, [sessionId, message, opts]);
const stream = await session.stream(result);
return createUIMessageStreamResponse({ stream });

send() uses "use step" internally, which is a no-op outside a workflow. No code changes needed in the agent itself.

Streaming

session.stream() now accepts an optional WorkflowRunLike to stream from a workflow result.

Before

const stream = await session.stream();

After

// In-memory reconnect (no workflow)
const stream = await session.stream();

// Stream from a workflow result
const result = await start(agentWorkflow, [...]);
const stream = await session.stream(result);

All changes

New APIs:

  • StorageHandlers — flat map of handler functions ("session.get", "message.set", etc.) (27ca29d, 0033ac2)
  • StorageStepFunction(store: StorageStep) => Promise<any> with "use step" for workflow (748b912)
  • StorageStep class — serializable across workflow boundary, dispatches via store.on(handlers) (748b912)
  • localStorage() — built-in filesystem-backed StorageHandlers (0033ac2)
  • SendResult { assistantMessageId, done } — return type of send() (27ca29d)
  • session.stream(result?) — accepts WorkflowRunLike for workflow streaming (7b2e370, 5ff9743)
  • session.history() — returns conversation history from storage (27ca29d, 3827005)
  • agent(name, opts) — name is now a required first positional argument (27ca29d)

Changed APIs:

  • session() is synchronous (no await) (27ca29d)
  • send(input, opts?) — input is a string, UIMessage, parts array, or { type: "approval" | "message", ... }; options include interruptIfStreaming, context, and session updates (27ca29d, 3827005)
  • session.stream() returns ReadableStream<UIMessageChunk> (not AgentStream) (7b2e370)
  • Vercel storage: use @vercel/kv2 to implement your handlers (0033ac2)

Removed APIs:

  • StorageConfig type ({ type: "vercel" | "local" | "custom" }) (0033ac2)
  • handleStorageRpc and Handlers (old RPC type) (0033ac2)
  • session.ui() — use session.history() instead (3827005)
  • session.tag.* and tagsSchema (27ca29d)
  • session.resolveApproval() — use send({ type: "approval", ... }) instead (3827005)
  • myAgent.storage.* — direct storage access from the agent (0033ac2)
  • myAgent.handler() and myAgent.rpc (0033ac2)
  • workflowSend() and workflowInput() on session (e643d88)
  • withAgent next.config.ts wrapper (27ca29d)
  • apps/storage (Vercel hosted storage service) (0033ac2)
  • RPC dispatch layer (0033ac2)