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.