SOLWYN

TypeScript SDK Runs

Attribute Node.js async work, reuse a run identity, understand run-scoped leases, and stop work cooperatively with the Solwyn TypeScript SDK.

Agent runs group related model calls under one identity in Solwyn Cloud. In Node.js, import the run APIs from @solwyn/sdk/node; this entry uses AsyncLocalStorage to carry the active run across ordinary asynchronous work. Importing the entry does not enable a process-wide async hook: run lifetime tracking starts with the first callback run or detached handle and stops when the last run lifetime is collected. Active run lifetimes still add promise overhead.

import OpenAI from "openai";
import { Solwyn } from "@solwyn/sdk";
import { currentRun, run } from "@solwyn/sdk/node";

const client = new Solwyn(new OpenAI(), {
  apiKey: process.env.SOLWYN_API_KEY!,
});

try {
  await run("nightly-summary", async () => {
    const context = currentRun();
    console.log(context?.agentRunId);

    await client.chat.completions.create({
      model: "gpt-4o",
      messages: [{ role: "user", content: "Summarize today's activity." }],
    });
  });
} finally {
  await client.close();
}

Every call admitted inside the callback receives the generated agentRunId and the name nightly-summary. Calls made outside a run still work, but do not receive a caller-defined run identity.

Callback-scoped runs

run() has two overloads and returns the callback's exact return value:

declare function run<T>(name: string, fn: () => T): T;
declare function run<T>(name: string, options: RunOptions, fn: () => T): T;

interface RunOptions {
  tags?: Record<string, string> | null;
  inheritTags?: boolean;
}

The scope propagates through await, promises, timers, and other asynchronous resources created inside the callback. Concurrent runs stay isolated. Return or await all work that belongs to the run; worker_threads do not inherit an AsyncLocalStorage context.

Each call to run() creates a fresh ID, even when names match. Names must be non-empty strings, may have a JavaScript string.length of at most 255, and may not contain control or formatting characters.

Use currentRun() when you only need identity. Use currentRunContext() for a defensive snapshot that also includes tags and the parent run:

import { currentRunContext, run } from "@solwyn/sdk/node";

await run("parent", { tags: { environment: "production" } }, async () => {
  await run("child", { tags: { task: "summarize" } }, async () => {
    const context = currentRunContext();
    // {
    //   agentRunId: "run_...",
    //   agentRunName: "child",
    //   tags: { task: "summarize", environment: "production" },
    //   parentAgentRunId: "run_..."
    // }
  });
});

Nested runs inherit parent tags by default. A child value wins when the same key exists in both layers. Pass inheritTags: false to keep the parent relationship while starting with a fresh tag set. Across all attribution layers, per-call solwyn_tags override run tags, which override client defaults.

Detached run handles

Use createRun() when separately scheduled callbacks belong to one durable workflow. Creating a handle does not change the current context. Each activate() call reuses the handle's fixed runId.

import { createRun } from "@solwyn/sdk/node";

const workflow = createRun("invoice-reconciliation", {
  tags: { queue: "billing" },
});

await workflow.activate(async () => {
  await reconcileFirstBatch();
});

await workflow.activate(async () => {
  await reconcileRemainingBatches();
});

workflow.finish();

activate(fn) tracks the exact value or promise returned by fn. Return or await background work whose lifetime belongs to the activation. finish() is permanent: it fails while an activation is active, and later calls to activate() or finish() throw SolwynError. A handle cannot activate itself recursively in its own context.

Run-scoped leases

Run scopes also enable local token leases. With leaseEnabled: true (the default), an untagged text call inside run() or RunHandle.activate() can reserve estimated input tokens plus an output bound from one run-scoped lease. Later eligible calls can be admitted locally while that lease is valid, avoiding a control-plane round trip for each call.

Lease grants and renewals are generation-fenced. Renewal happens away from the provider-call path. When a run finishes, its remaining lease authority is surrendered through a bounded per-client dispatcher; client.close() drains outstanding surrenders within a one-second budget.

These calls use the ordinary per-call budget check instead:

  • Calls outside a Node run scope
  • Calls with client, run, or per-call tags
  • Media calls
  • Model chains the lease does not yet cover; an allowed per-call check widens the lease in the background so later calls on that chain can use it
  • Models that the control plane marks lease-ineligible
  • Calls made with leaseEnabled: false

When a request does not expose a usable output limit, leaseOutputBoundDefault supplies the reservation bound; its default is 4096. See TypeScript SDK Budgets for the complete admission and outage behavior.

Cooperative run stops

Run control is scoped to the exact run ID. A server directive or a local velocity rule can mark one run as stopped without affecting a similarly named or similarly shaped run. The next intercepted provider dispatch for that ID throws RunStoppedError. Local velocity history is structural and kept for up to 4096 runs; when it must discard live history, the SDK logs a rate-limited velocity.history_evicted warning.

import {
  RunStoppedError,
  currentRunTerminated,
  runTermination,
} from "@solwyn/sdk";
import { currentRun, run } from "@solwyn/sdk/node";

await run("crawler", async () => {
  const runId = currentRun()?.agentRunId;
  if (runId === undefined) throw new Error("missing run context");

  while (!currentRunTerminated()) {
    try {
      await crawlNextPage();
    } catch (error) {
      if (!(error instanceof RunStoppedError)) throw error;
      break;
    }
  }

  const stopped = runTermination(runId);
  if (stopped !== undefined) {
    console.log(stopped.source, stopped.reason, stopped.atMonotonic);
  }
});

RunStoppedError derives directly from SolwynError, not BudgetExceededError. Handle it separately from a budget denial so retry logic does not restart an explicitly stopped run. Its agentRunId, source, and reason fields are structural and content-free.

Stops are cooperative:

  • A buffered provider request already in flight is allowed to finish and settles normally.
  • The drop-in client's active stream stops at the next raw provider-chunk boundary. The triggering chunk is not yielded, observed usage settles once, and iteration throws RunStoppedError.
  • Future dispatches for the exact stopped ID are blocked before they reach the provider.

The process-wide registry retains exact stop state for the 256 most recently touched run IDs. It never infers identity from a run name or request fingerprint. Active streams keep their own first observed stop until they are released, even if the registry entry is later evicted.

Clearing stop state

Clear a termination only when your application has intentionally recovered and is prepared to start new work:

import { clearRunTermination, runTermination } from "@solwyn/sdk";

const stopped = runTermination(workflow.runId);
if (stopped?.source === "local_velocity") {
  clearRunTermination(workflow.runId);
}

clearRunTermination(runId) is forward-looking. New calls and newly created streams re-enter normal admission; velocity history, budget authority, or a later live check can stop or deny them again. A stream that already latched the stop continues to abort with its original termination, so restart that stream after clearing. Clearing local process state also does not revoke a server-side stop.

On this page