@anthropic-ai/sdk
Version:
The official TypeScript library for the Anthropic API
798 lines • 51.2 kB
JavaScript
"use strict";
var _IdleClock_maxIdleMs, _IdleClock_onExpire, _IdleClock_blockers, _IdleClock_armPending, _IdleClock_timer, _SessionToolRunner_instances, _SessionToolRunner_consumed, _SessionToolRunner_controller, _SessionToolRunner_detachExternal, _SessionToolRunner_requestOpts, _SessionToolRunner_toolByName, _SessionToolRunner_logger, _SessionToolRunner_seen, _SessionToolRunner_answered, _SessionToolRunner_confirmationVerdicts, _SessionToolRunner_awaitingConfirmation, _SessionToolRunner_results, _SessionToolRunner_inFlightCount, _SessionToolRunner_sendRetryWindowMs, _SessionToolRunner_onIdle, _SessionToolRunner_idleClock, _SessionToolRunner_otelContext, _SessionToolRunner_primaryThreadId, _SessionToolRunner_historyRead, _SessionToolRunner_requestOptions, _SessionToolRunner_streamLoop, _SessionToolRunner_reconcile, _SessionToolRunner_ingestHistory, _SessionToolRunner_notePrimaryThread, _SessionToolRunner_handleStreamEvent, _SessionToolRunner_routeToolEvent, _SessionToolRunner_noteConfirmation, _SessionToolRunner_applyVerdict, _SessionToolRunner_surfaceCall, _SessionToolRunner_execute, _SessionToolRunner_sendResult, _SessionToolRunner_drain;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionToolRunner = exports.DEFAULT_MAX_IDLE_MS = exports.MANAGED_AGENTS_BETA = void 0;
const tslib_1 = require("../../internal/tslib.js");
const api_1 = require("@opentelemetry/api");
const error_1 = require("../../core/error.js");
const log_1 = require("../../internal/utils/log.js");
const sleep_1 = require("../../internal/utils/sleep.js");
const backoff_1 = require("../../internal/utils/backoff.js");
const abort_1 = require("../../internal/utils/abort.js");
const async_queue_1 = require("../../internal/utils/async-queue.js");
const headers_1 = require("../../internal/headers.js");
const stainless_helper_header_1 = require("../../internal/stainless-helper-header.js");
const BetaRunnableTool_1 = require("./BetaRunnableTool.js");
const tracing_1 = require("../internal/tracing.js");
/** Beta header for the managed-agents API. */
exports.MANAGED_AGENTS_BETA = 'managed-agents-2026-04-01';
const STREAM_BACKOFF_START_MS = 500;
const STREAM_BACKOFF_CAP_MS = 10000;
const TOOL_TIMEOUT_MS = 120000;
const DRAIN_TIMEOUT_MS = 30000;
const SEND_BACKOFF_START_MS = 1000;
const SEND_BACKOFF_CAP_MS = 30000;
/**
* How long a transiently failing tool-result send keeps retrying when the
* runner is used on its own: the server's default work-item lease TTL.
* `EnvironmentWorker` overrides it with the live TTL from each lease heartbeat
* (`_setSendRetryWindow`), so a send is only abandoned once the lease can no
* longer be ours.
*/
const SEND_RETRY_WINDOW_MS = 5 * 60000;
/** Default {@link SessionToolRunnerOptions.maxIdleMs}: 60 seconds. */
exports.DEFAULT_MAX_IDLE_MS = 60000;
/**
* Whether `ev` is a `session.status_idle` whose turn is over. Only
* `requires_action` leaves the turn open: the session resumes once a client
* resolves the events it names. Every other stop reason ends the turn, including
* one newer than this SDK's types.
*/
function endsTurn(ev) {
if (ev.type !== 'session.status_idle')
return false;
return ev.stop_reason?.type !== 'requires_action';
}
var IdleClock = /* @__PURE__ */ (() => {
/**
* The `maxIdleMs` stop-countdown, including its deferral. {@link noteEvent}
* arms on an idle that ends the turn and disarms on anything else. Gated tool
* work registered via {@link block} — a call held for user confirmation, or a
* user-approved call still dispatching — keeps
* {@link arm} pending until {@link unblock} retires the last blocker, at which
* point the countdown starts. Event-driven — there is no polling watchdog.
*/
class IdleClock {
constructor(maxIdleMs, onExpire) {
_IdleClock_maxIdleMs.set(this, void 0);
_IdleClock_onExpire.set(this, void 0);
_IdleClock_blockers.set(this, new Set());
// Set when arm() found blockers outstanding; the unblock that retires the
// last blocker applies it. Cleared by any disarm.
_IdleClock_armPending.set(this, false);
_IdleClock_timer.set(this, void 0);
tslib_1.__classPrivateFieldSet(this, _IdleClock_maxIdleMs, maxIdleMs, "f");
tslib_1.__classPrivateFieldSet(this, _IdleClock_onExpire, onExpire, "f");
}
/**
* Arm on an idle that ends the turn; disarm otherwise. `user.tool_confirmation`
* is neutral: it signals neither agent activity nor an idle, and its effect
* on the clock flows through {@link block} / {@link unblock} instead —
* disarming here would discard the pending arm the verdict is about to
* settle.
*/
noteEvent(ev) {
if (ev.type === 'user.tool_confirmation')
return;
if (endsTurn(ev))
this.arm();
else
this.disarm();
}
/** Register gated work that must resolve before an idle countdown starts. */
block(toolUseId) {
tslib_1.__classPrivateFieldGet(this, _IdleClock_blockers, "f").add(toolUseId);
if (tslib_1.__classPrivateFieldGet(this, _IdleClock_timer, "f") !== undefined) {
// Defensive: every caller disarms first (any event that routes a tool
// call is itself a disarming event), but a countdown running when gated
// work appears is stale evidence — the session is not idly waiting to
// stop. Convert it into a pending arm rather than let it fire over the
// gated call.
tslib_1.__classPrivateFieldSet(this, _IdleClock_armPending, true, "f");
clearTimeout(tslib_1.__classPrivateFieldGet(this, _IdleClock_timer, "f"));
tslib_1.__classPrivateFieldSet(this, _IdleClock_timer, undefined, "f");
}
}
/**
* Retire gated work (a no-op for ids never blocked); applies a pending arm —
* with a fresh full `maxIdleMs` window — once the last blocker retires.
*/
unblock(toolUseId) {
tslib_1.__classPrivateFieldGet(this, _IdleClock_blockers, "f").delete(toolUseId);
if (tslib_1.__classPrivateFieldGet(this, _IdleClock_blockers, "f").size === 0 && tslib_1.__classPrivateFieldGet(this, _IdleClock_armPending, "f"))
this.arm();
}
/**
* (Re)start the idle countdown — or, while blockers are outstanding, hold
* the arm pending instead. Stopping then would drop a held call when its
* verdict later arrives, or cut the runner off before a released call's
* result can drive the next turn.
*/
arm() {
if (tslib_1.__classPrivateFieldGet(this, _IdleClock_maxIdleMs, "f") <= 0)
return;
if (tslib_1.__classPrivateFieldGet(this, _IdleClock_blockers, "f").size > 0) {
tslib_1.__classPrivateFieldSet(this, _IdleClock_armPending, true, "f");
return;
}
tslib_1.__classPrivateFieldSet(this, _IdleClock_armPending, false, "f");
if (tslib_1.__classPrivateFieldGet(this, _IdleClock_timer, "f") !== undefined)
clearTimeout(tslib_1.__classPrivateFieldGet(this, _IdleClock_timer, "f"));
tslib_1.__classPrivateFieldSet(this, _IdleClock_timer, setTimeout(tslib_1.__classPrivateFieldGet(this, _IdleClock_onExpire, "f"), tslib_1.__classPrivateFieldGet(this, _IdleClock_maxIdleMs, "f")), "f");
}
/**
* Cancel the idle countdown and any pending arm. Blockers persist — they
* track real outstanding work, retired only by {@link unblock}.
*/
disarm() {
tslib_1.__classPrivateFieldSet(this, _IdleClock_armPending, false, "f");
if (tslib_1.__classPrivateFieldGet(this, _IdleClock_timer, "f") !== undefined) {
clearTimeout(tslib_1.__classPrivateFieldGet(this, _IdleClock_timer, "f"));
tslib_1.__classPrivateFieldSet(this, _IdleClock_timer, undefined, "f");
}
}
}
_IdleClock_maxIdleMs = new WeakMap(), _IdleClock_onExpire = new WeakMap(), _IdleClock_blockers = new WeakMap(), _IdleClock_armPending = new WeakMap(), _IdleClock_timer = new WeakMap();
return IdleClock;
})();
var SessionToolRunner = /* @__PURE__ */ (() => {
/**
* The sessions-side counterpart to `client.beta.messages.toolRunner`: an
* async-iterable that attaches to a managed-agents session, executes every
* incoming `agent.tool_use` and `agent.custom_tool_use` event against a local
* tool registry, posts the matching result back (`user.tool_result` for the
* former, `user.custom_tool_result` for the latter), and yields one
* {@link DispatchedToolCall} per completed call. Server-side `agent.mcp_tool_use`
* calls are not dispatched. Internally drives event-stream reconnect and result
* posting.
*
* A call the server gated with `evaluated_permission: "ask"` (the `always_ask`
* policy — or any value this SDK doesn't recognize, which fails closed) is held
* until its `user.tool_confirmation` arrives: only an explicit `allow` runs it;
* `deny` — or any verdict this SDK doesn't recognize, failing closed — is never
* executed and posts nothing (the denial resolves the call server-side), but is
* still yielded (`confirmation="deny"`, `posted=false`, `result=undefined`) so
* the consumer can observe it. A held call — and a user-approved one still
* dispatching — defers the `maxIdleMs` countdown, so an idle observed in the
* meantime cannot stop the runner: it waits until the verdict arrives, the
* session terminates, or the abort signal fires — pass
* `AbortSignal.timeout(...)` for a wall-clock bound.
*
* Iteration ends when the session terminates (`session.status_terminated` /
* `session.deleted`), when the consumer `break`s out of the loop or aborts the
* supplied signal, or — once the session has gone idle with any `stop_reason.type`
* but `"requires_action"` — when `maxIdleMs` elapses with no new event (any
* new event resets that countdown; it re-arms on the next such idle;
* `maxIdleMs <= 0` disables it). The `finally` branch drains any in-flight
* tool calls and runs each tool's `close()` cleanup hook. It does *not* touch
* the work-item lease — wrap it in an `EnvironmentWorker` if you need
* heartbeating / force-stop.
*
* @example
* ```ts
* import { betaAgentToolset20260401 } from '@anthropic-ai/sdk/tools/agent-toolset/node';
*
* for await (const call of client.beta.sessions.events.toolRunner(work.data.id, {
* tools: [...betaAgentToolset20260401({ workdir }), myTool],
* })) {
* console.log(`${call.name} -> ${call.isError ? 'error' : 'ok'}`);
* }
* ```
*/
class SessionToolRunner {
constructor(sessionId, opts) {
_SessionToolRunner_instances.add(this);
_SessionToolRunner_consumed.set(this, false);
_SessionToolRunner_controller.set(this, void 0);
_SessionToolRunner_detachExternal.set(this, void 0);
_SessionToolRunner_requestOpts.set(this, void 0);
_SessionToolRunner_toolByName.set(this, void 0);
_SessionToolRunner_logger.set(this, void 0);
_SessionToolRunner_seen.set(this, new Set());
_SessionToolRunner_answered.set(this, new Set());
// Confirmation gating (`always_ask` tools): `#confirmationVerdicts` records
// every `user.tool_confirmation` verdict by `tool_use_id`;
// `#awaitingConfirmation` holds the tool-call events whose
// `evaluated_permission` is `ask` and whose verdict has not arrived —
// released (or resolved as denied) by `#noteConfirmation` / the next
// reconcile pass. Like `#seen` and `#answered`, `#confirmationVerdicts` is
// per-session O(tool calls): recorded verdicts persist for the life of the run.
_SessionToolRunner_confirmationVerdicts.set(this, new Map());
_SessionToolRunner_awaitingConfirmation.set(this, new Map());
_SessionToolRunner_results.set(this, new async_queue_1.AsyncQueue());
_SessionToolRunner_inFlightCount.set(this, 0);
_SessionToolRunner_sendRetryWindowMs.set(this, SEND_RETRY_WINDOW_MS);
_SessionToolRunner_onIdle.set(this, null);
_SessionToolRunner_idleClock.set(this, void 0);
/** The OpenTelemetry `Context` that was active where the loop started, the parent of each tool span. */
_SessionToolRunner_otelContext.set(this, void 0);
/**
* The ID of the Session's primary thread, whose tool call events have no thread ID. It is the thread
* of the Session's first thread status event: a child thread starts after the primary thread has.
*/
_SessionToolRunner_primaryThreadId.set(this, void 0);
/** Whether the last read of the history succeeded, so that the stream's events continue it. */
_SessionToolRunner_historyRead.set(this, false);
this.client = opts.client;
this.sessionId = sessionId;
this.tools = opts.tools;
this.maxIdleMs = opts.maxIdleMs ?? exports.DEFAULT_MAX_IDLE_MS;
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_logger, (0, log_1.loggerFor)(opts.client), "f");
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_toolByName, new Map(opts.tools.map((t) => [(0, BetaRunnableTool_1.toolName)(t), t])), "f");
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_controller, new AbortController(), "f");
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_detachExternal, (0, abort_1.linkAbort)(opts.signal, tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f")), "f");
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_requestOpts, opts.requestOptions, "f");
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_idleClock, new IdleClock(this.maxIdleMs, () => {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").info('session idle after its turn ended; stopping', {
component: 'session-tool-runner',
session_id: this.sessionId,
max_idle_ms: this.maxIdleMs,
});
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").abort();
}), "f");
}
/** Read-only view of this runner's abort signal. */
get signal() {
return tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").signal;
}
/** Abort the runner. Background tasks will wind down and `for await` will exit cleanly. */
abort() {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").abort();
}
/**
* @internal
* `EnvironmentWorker` keeps this equal to the lease TTL each heartbeat
* reports; applies to a send already retrying.
*/
_setSendRetryWindow(ms) {
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_sendRetryWindowMs, ms, "f");
}
async *[(_SessionToolRunner_consumed = new WeakMap(), _SessionToolRunner_controller = new WeakMap(), _SessionToolRunner_detachExternal = new WeakMap(), _SessionToolRunner_requestOpts = new WeakMap(), _SessionToolRunner_toolByName = new WeakMap(), _SessionToolRunner_logger = new WeakMap(), _SessionToolRunner_seen = new WeakMap(), _SessionToolRunner_answered = new WeakMap(), _SessionToolRunner_confirmationVerdicts = new WeakMap(), _SessionToolRunner_awaitingConfirmation = new WeakMap(), _SessionToolRunner_results = new WeakMap(), _SessionToolRunner_inFlightCount = new WeakMap(), _SessionToolRunner_sendRetryWindowMs = new WeakMap(), _SessionToolRunner_onIdle = new WeakMap(), _SessionToolRunner_idleClock = new WeakMap(), _SessionToolRunner_otelContext = new WeakMap(), _SessionToolRunner_primaryThreadId = new WeakMap(), _SessionToolRunner_historyRead = new WeakMap(), _SessionToolRunner_instances = new WeakSet(), Symbol.asyncIterator)]() {
if (tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_consumed, "f")) {
throw new error_1.AnthropicError('Cannot iterate over a consumed SessionToolRunner');
}
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_consumed, true, "f");
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_otelContext, this.client._tracer ? api_1.context.active() : undefined, "f");
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").info('session tool runner starting', {
component: 'session-tool-runner',
session_id: this.sessionId,
});
// The one background promise: drives the event stream and dispatches tools.
// Its `.catch` aborts the controller so the main loop unwinds.
const streamPromise = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_streamLoop).call(this).catch((e) => {
if (!tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").signal.aborted) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").error('stream loop failed', { error: String(e) });
}
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").abort();
});
try {
// Phase 1: yield results as they arrive. `next(signal)` resolves
// `done: true` when the controller aborts — cancellation is handled in
// the queue read, no outer `Promise.race` needed.
while (true) {
const next = await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_results, "f").next(tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").signal);
if (next.done)
break;
yield next.value;
}
// Phase 2: let the stream loop settle (and push any final results), then
// drain whatever is still queued before closing.
await streamPromise;
let pending;
while ((pending = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_results, "f").tryShift()) !== undefined) {
yield pending;
}
}
finally {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").abort();
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").disarm();
// Re-await defensively in case the consumer broke out of phase 1 before
// phase 2 ran — a no-op if it already settled.
await streamPromise;
try {
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_drain).call(this);
}
catch (e) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").warn('drain failed', { error: String(e) });
}
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_results, "f").close();
for (const t of this.tools) {
try {
// `close` is typed `() => Promisable<void>`, so a single `await`
// covers both the sync and async return.
await t.close?.();
}
catch (e) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").warn('tool.close failed', { tool: (0, BetaRunnableTool_1.toolName)(t), error: String(e) });
}
}
// Detach from the external signal so the consumer can drop their signal
// reference without leaking this iterator instance.
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_detachExternal, "f").call(this);
}
}
}
_SessionToolRunner_requestOptions = function _SessionToolRunner_requestOptions() {
return {
...tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_requestOpts, "f"),
headers: (0, headers_1.buildHeaders)([(0, stainless_helper_header_1.helperHeader)('session-tool-runner'), tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_requestOpts, "f")?.headers]),
signal: tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").signal,
};
}, _SessionToolRunner_streamLoop =
// ===== event stream =====
async function _SessionToolRunner_streamLoop() {
const ctrl = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f");
let backoff = STREAM_BACKOFF_START_MS;
while (!ctrl.signal.aborted) {
try {
// Establish the event stream *before* reconciling history, so an event
// emitted in the gap between listing and attaching is buffered on the
// stream rather than lost. `seen`/`answered` dedup any event that shows
// up both in the reconcile pass and on the live stream.
const stream = await this.client.beta.sessions.events.stream(this.sessionId, {}, tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_requestOptions).call(this));
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_reconcile).call(this);
for await (const ev of stream) {
backoff = STREAM_BACKOFF_START_MS;
if (await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_handleStreamEvent).call(this, ev))
return;
}
}
catch (e) {
// An abort throws to unwind the caller (the iterator's `streamPromise`
// `.catch`) rather than returning early and letting it carry on.
ctrl.signal.throwIfAborted();
if ((0, backoff_1.isFatal4xx)(e)) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").error('permanent stream failure, shutting down', { error: String(e) });
ctrl.abort();
throw e;
}
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").warn('stream disconnected, reconnecting', {
error: String(e),
backoff_ms: backoff,
});
}
ctrl.signal.throwIfAborted();
await (0, sleep_1.sleep)(backoff, ctrl.signal);
backoff = Math.min(backoff * 2, STREAM_BACKOFF_CAP_MS);
}
}, _SessionToolRunner_reconcile =
/**
* Read full history before dispatching so a `tool_use` whose result appears
* later in the same history is not re-executed. Runs after the live stream is
* already attached (see {@link SessionToolRunner.#streamLoop}).
*/
async function _SessionToolRunner_reconcile() {
const ctrl = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f");
const pending = [];
let lastEndedTurn = false;
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_historyRead, false, "f");
try {
for await (const ev of this.client.beta.sessions.events.list(this.sessionId, { limit: 1000 }, tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_requestOptions).call(this))) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_ingestHistory).call(this, ev, pending);
// Neutral here as in `IdleClock.noteEvent`: a verdict is neither agent
// activity nor an idle.
if (ev.type !== 'user.tool_confirmation')
lastEndedTurn = endsTurn(ev);
}
}
catch (e) {
// An abort throws to unwind the caller; a real list failure is
// non-fatal — undo the speculative `seen` entries and let `#streamLoop`
// carry on with the live stream.
ctrl.signal.throwIfAborted();
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").warn('reconcile list failed', { error: String(e) });
// If list itself failed, undo the speculative `seen` entries so the next
// reconcile pass (or the live stream) can pick them up. Leave the idle
// timer untouched — the history we read may be incomplete.
for (const ev of pending)
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_seen, "f").delete(ev.id);
return;
}
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_historyRead, true, "f");
const unanswered = pending.filter((ev) => !tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").has(ev.id));
// Disarm before routing: `#execute` runs inline here, so a timer left armed
// from before the reconnect could fire over an in-flight tool.
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").disarm();
for (const ev of unanswered)
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_routeToolEvent).call(this, ev);
// A held call's verdict is normally applied by the routing pass above; if
// its tool_use fell outside the listed window the pass never saw it, so
// apply the verdict to the held copy here.
for (const held of [...tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_awaitingConfirmation, "f").values()]) {
const verdict = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_confirmationVerdicts, "f").get(held.id);
if (verdict !== undefined)
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_applyVerdict).call(this, held, verdict);
}
// Routing resolves denied calls in place (marking them answered) and holds
// ask-gated calls for their `user.tool_confirmation`. If the most recent
// event in history ends the turn and no tool work is outstanding,
// the session is done — arm the idle clock so the runner stops even if that
// idle arrived during a disconnect. A held call is not outstanding
// here: it blocks the clock, so this arm stays pending until the verdict
// (and, for an allow, the dispatch it releases) resolves it.
const outstanding = unanswered.filter((ev) => !tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").has(ev.id) && !tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_awaitingConfirmation, "f").has(ev.id));
if (lastEndedTurn && outstanding.length === 0)
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").arm();
else
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").disarm();
}, _SessionToolRunner_ingestHistory = function _SessionToolRunner_ingestHistory(ev, pending) {
// The history starts with the Session's first event.
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_notePrimaryThread).call(this, ev);
if (ev.type === 'agent.tool_use' || ev.type === 'agent.custom_tool_use') {
// Mark the event seen so a replay on the live stream is not dispatched
// twice, but decide whether it still needs executing from `answered`, not
// `seen`: a call whose result post failed is seen-but-unanswered, and must
// be retried on the next reconcile pass rather than silently dropped.
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_seen, "f").add(ev.id);
if (!tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").has(ev.id))
pending.push(ev);
}
else if (ev.type === 'user.tool_result') {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").add(ev.tool_use_id);
}
else if (ev.type === 'user.custom_tool_result') {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").add(ev.custom_tool_use_id);
}
else if (ev.type === 'user.tool_confirmation') {
// Record the verdict only, before the pending pass, so a call whose
// confirmation appears later in the same history routes with its verdict
// already known. Releasing a held call here as well would dispatch it a
// second time when the routing pass reaches its tool_use event.
if (!tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").has(ev.tool_use_id))
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_confirmationVerdicts, "f").set(ev.tool_use_id, ev.result);
}
}, _SessionToolRunner_notePrimaryThread = function _SessionToolRunner_notePrimaryThread(ev) {
// Only the spans have a use for it.
if (!this.client._tracer || tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_primaryThreadId, "f") !== undefined)
return;
switch (ev.type) {
case 'session.thread_status_running':
case 'session.thread_status_idle':
case 'session.thread_status_rescheduled':
case 'session.thread_status_terminated':
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_primaryThreadId, ev.session_thread_id, "f");
}
}, _SessionToolRunner_handleStreamEvent =
/** Returns true when the runner should exit. */
async function _SessionToolRunner_handleStreamEvent(ev) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").noteEvent(ev);
// Without the history, the stream may have begun after the Session's first thread status event.
if (tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_historyRead, "f"))
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_notePrimaryThread).call(this, ev);
switch (ev.type) {
case 'agent.tool_use':
case 'agent.custom_tool_use':
if (!tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_seen, "f").has(ev.id)) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_seen, "f").add(ev.id);
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_routeToolEvent).call(this, ev);
}
return false;
case 'user.tool_confirmation':
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_noteConfirmation).call(this, ev);
return false;
case 'user.tool_result':
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").add(ev.tool_use_id);
return false;
case 'user.custom_tool_result':
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").add(ev.custom_tool_use_id);
return false;
case 'session.status_terminated':
case 'session.deleted':
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").info('session terminated', {
component: 'session-tool-runner',
session_id: this.sessionId,
});
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").abort();
return true;
default:
return false;
}
}, _SessionToolRunner_routeToolEvent =
// ===== confirmation gating (always_ask tools) =====
/**
* Dispatch `ev`, honoring its evaluated permission. A call the server gated
* (`evaluated_permission == "ask"`) is held until its `user.tool_confirmation`
* arrives. Fails closed: only an explicit `allow` verdict releases a gated
* call; a server-side `deny` overrides any recorded verdict; an unrecognized
* permission is held like `ask` and an unrecognized verdict is denied.
*/
async function _SessionToolRunner_routeToolEvent(ev) {
// `getattr`-style read: today only `agent.tool_use` carries
// `evaluated_permission`, but if it ever lands on `agent.custom_tool_use`
// the gate must keep failing closed rather than dispatch by event type.
const permission = ev.evaluated_permission;
// A server-side `deny` overrides any (stray) recorded verdict.
const verdict = permission === 'deny' ? 'deny' : tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_confirmationVerdicts, "f").get(ev.id);
if (verdict === undefined) {
if (permission === undefined || permission === 'allow') {
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_execute).call(this, ev, undefined);
}
else if (!tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_awaitingConfirmation, "f").has(ev.id)) {
// "ask" — or a permission this SDK does not recognize, which must not
// dispatch unconfirmed — waits for the user's verdict. (Already-held: a
// reconcile after reconnect re-routes the call; keep the existing hold.)
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").info('tool call awaiting confirmation; holding', {
component: 'session-tool-runner',
session_id: this.sessionId,
tool: ev.name,
tool_use_id: ev.id,
});
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_awaitingConfirmation, "f").set(ev.id, ev);
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").block(ev.id);
}
return;
}
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_applyVerdict).call(this, ev, verdict);
}, _SessionToolRunner_noteConfirmation =
/** Record an allow/deny verdict and release the held call it gates, if any. */
async function _SessionToolRunner_noteConfirmation(ev) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_confirmationVerdicts, "f").set(ev.tool_use_id, ev.result);
const held = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_awaitingConfirmation, "f").get(ev.tool_use_id);
// Nothing held: the verdict gates a call this runner has not seen yet (or
// one it never gates, e.g. an `agent.mcp_tool_use`). Keeping it in
// `#confirmationVerdicts` lets a later route of that call resolve instantly.
if (held === undefined)
return;
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_applyVerdict).call(this, held, ev.result);
}, _SessionToolRunner_applyVerdict =
/**
* Dispatch or resolve a gated call according to its verdict.
*
* The idle-clock blocker accounting lives here: a denial retires the held
* call's blocker, while an allow keeps one on the call — taking it now if the
* verdict was already known when the call was routed, so it was never held —
* until `#execute` has finished with it. The countdown must not run over
* gated work that is still in flight.
*/
async function _SessionToolRunner_applyVerdict(ev, verdict) {
const wasHeld = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_awaitingConfirmation, "f").delete(ev.id);
if (verdict === 'allow') {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").info('tool call confirmed', {
component: 'session-tool-runner',
session_id: this.sessionId,
tool: ev.name,
tool_use_id: ev.id,
});
if (!wasHeld)
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").block(ev.id);
try {
await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_execute).call(this, ev, 'allow');
}
finally {
// The approved call is fully disposed of (executed, or moot because it
// was answered elsewhere) — the sole place an allow's blocker retires.
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").unblock(ev.id);
}
return;
}
// "deny" — or any value other than an explicit "allow" (fail closed). The
// denial resolves the call server-side, so mark it answered and yield it
// (nothing ran, nothing posted).
if (wasHeld)
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").unblock(ev.id);
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").add(ev.id);
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").info('tool call denied; not executing', {
component: 'session-tool-runner',
session_id: this.sessionId,
tool: ev.name,
tool_use_id: ev.id,
});
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_surfaceCall).call(this, {
event: ev,
toolUseId: ev.id,
name: ev.name,
isError: false,
posted: false,
confirmation: 'deny',
});
}, _SessionToolRunner_surfaceCall = function _SessionToolRunner_surfaceCall(call) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_results, "f").push(call);
}, _SessionToolRunner_execute =
// ===== tool execution =====
async function _SessionToolRunner_execute(ev, confirmation) {
var _a, _b;
if (tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").has(ev.id))
return;
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").info('executing tool', {
component: 'session-tool-runner',
session_id: this.sessionId,
tool: ev.name,
tool_use_id: ev.id,
});
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_inFlightCount, (_a = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_inFlightCount, "f"), _a++, _a), "f");
try {
const tool = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_toolByName, "f").get(ev.name);
if (!tool) {
// Skip (split-client partial fulfilment): a name this runner
// is not registered for belongs to the other client servicing this
// session (typically the customer's app backend handling custom tools).
// Post NO result, do not mark it answered, and leave the tool_use_id
// pending for its owner — claiming it would corrupt the conversation.
// Still yield the call so the consumer can observe the unowned
// dispatch; nothing was sent, so `posted`/`isError` stay false and no
// `result` event is populated. The id stays unanswered, so reconcile
// keeps it out of the idle/end-turn accounting and re-surfaces it after
// a reconnect until its owner answers it.
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").info('tool not owned by this runner; leaving the tool_use_id pending for its owner', {
component: 'session-tool-runner',
session_id: this.sessionId,
tool: ev.name,
tool_use_id: ev.id,
});
// The approval kept the idle countdown pending on this call. Drop it
// instead of starting it: the owner still has to answer.
if (confirmation === 'allow')
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_idleClock, "f").disarm();
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_surfaceCall).call(this, {
event: ev,
toolUseId: ev.id,
name: ev.name,
isError: false,
posted: false,
confirmation,
});
return;
}
let content;
let isError;
// The default is the Session thread ID, which is the conversation ID of the platform's spans.
// Only a child thread's call event has it, so a call without one is the primary thread's.
const conversationId = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_requestOpts, "f")?.openTelemetry?.conversationId ?? (ev.session_thread_id || tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_primaryThreadId, "f"));
// Per-tool controller: aborts on the runner's own signal *or* the
// per-tool timeout, so an in-flight tool stops promptly when the runner
// is aborted instead of running until the timeout.
const toolCtrl = new AbortController();
const detachTool = (0, abort_1.linkAbort)(tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").signal, toolCtrl);
const timer = setTimeout(() => toolCtrl.abort(), TOOL_TIMEOUT_MS);
try {
// Pass the source `agent.tool_use` / `agent.custom_tool_use` event
// straight through as the run context's `toolUse` — it is a union
// member of `BetaToolUse`, no Messages-block adapter needed.
const spanOptions = {
signal: tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f").signal,
conversationId,
sessionID: this.sessionId,
parentContext: tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_otelContext, "f"),
};
const outcome = await (0, tracing_1.withToolUseSpan)(this.client, tool, ev, spanOptions, async () => {
const toolOutcome = await (0, BetaRunnableTool_1.runRunnableTool)(tool, ev.input, {
toolUse: ev,
toolUseBlock: ev,
signal: toolCtrl.signal,
});
const sent = toSessionContent(toolOutcome.content);
// A span keeps a text block whole, so it gets a block that is sent as its JSON as the block itself.
const recorded = !this.client._tracer ?
sent
: sent.map((block, i) => {
const original = typeof toolOutcome.content === 'string' ? undefined : toolOutcome.content[i];
return original && original.type !== 'text' && block.type === 'text' ? original : block;
});
return { sent, content: recorded, isError: toolOutcome.isError };
});
content = outcome.sent;
isError = outcome.isError;
}
finally {
clearTimeout(timer);
detachTool();
}
// Answer with the result event that matches the call kind: a
// `user.tool_result` for an `agent.tool_use`, a `user.custom_tool_result`
// for an `agent.custom_tool_use`. Posting the wrong one leaves the call
// unanswered and the session stuck.
const result = buildResultEvent(ev, isError, content);
const posted = await tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_sendResult).call(this, result, ev.id, conversationId);
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_surfaceCall).call(this, {
event: ev,
result,
toolUseId: ev.id,
name: ev.name,
isError,
posted,
confirmation,
});
}
finally {
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_inFlightCount, (_b = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_inFlightCount, "f"), _b--, _b), "f");
if (tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_inFlightCount, "f") === 0)
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_onIdle, "f")?.call(this);
}
}, _SessionToolRunner_sendResult = async function _SessionToolRunner_sendResult(result, toolUseId, conversationId) {
const ctrl = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_controller, "f");
const start = Date.now();
let lastErr;
let attempt = 0;
while (true) {
attempt++;
// An abort throws to unwind the caller rather than returning a
// `posted: false` result the iterator would carry on past.
ctrl.signal.throwIfAborted();
try {
await this.client.beta.sessions.events.send(this.sessionId, { events: [result] }, { ...tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_instances, "m", _SessionToolRunner_requestOptions).call(this), openTelemetry: { conversationId } });
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_answered, "f").add(toolUseId);
return true;
}
catch (e) {
lastErr = e;
// Only short-circuit on a permanent 4xx; 408/409/429 deserve the
// remaining retries (aligned with the core client's retry policy).
if ((0, backoff_1.isFatal4xx)(e))
break;
const remainingMs = tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_sendRetryWindowMs, "f") - (Date.now() - start);
if (remainingMs <= 0)
break;
const waitMs = Math.min((0, backoff_1.applyJitter)((0, backoff_1.backoff)(attempt - 1, SEND_BACKOFF_START_MS, SEND_BACKOFF_CAP_MS)), remainingMs);
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").warn('tool result send failed; retrying', {
tool_use_id: toolUseId,
attempt,
backoff_ms: waitMs,
error: String(e),
});
await (0, sleep_1.sleep)(waitMs, ctrl.signal);
}
}
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").error('failed to send tool result', {
tool_use_id: toolUseId,
attempts: attempt,
error: String(lastErr),
});
return false;
}, _SessionToolRunner_drain =
/** Wait (bounded) for in-flight tool executions to finish during teardown. */
async function _SessionToolRunner_drain() {
if (tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_inFlightCount, "f") === 0)
return;
await Promise.race([new Promise((r) => (tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_onIdle, r, "f"))), (0, sleep_1.sleep)(DRAIN_TIMEOUT_MS)]);
tslib_1.__classPrivateFieldSet(this, _SessionToolRunner_onIdle, null, "f");
if (tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_inFlightCount, "f") > 0) {
tslib_1.__classPrivateFieldGet(this, _SessionToolRunner_logger, "f").warn('drain timeout exceeded');
}
};
return SessionToolRunner;
})();
exports.SessionToolRunner = SessionToolRunner;
/**
* Build the result event that answers `ev`: a `user.tool_result` for a builtin
* `agent.tool_use`, a `user.custom_tool_result` for a custom
* `agent.custom_tool_use`. The two `(use, result)` pairs are distinct API event
* types and must be matched exactly — a `user.tool_result` does not answer a
* custom tool call.
*/
function buildResultEvent(ev, isError, content) {
if (ev.type === 'agent.custom_tool_use') {
return { type: 'user.custom_tool_result', custom_tool_use_id: ev.id, is_error: isError, content };
}
return { type: 'user.tool_result', tool_use_id: ev.id, is_error: isError, content };
}
// The Messages-API tool-result block union is wider than the Sessions-API
// tool_result content union; pass through text/image/document and stringify
// anything else so a BetaRunnableTool authored for toolRunner still works here.
function toSessionContent(content) {
if (typeof content === 'string')
return [{ type: 'text', text: content || '(no output)' }];
const out = content.map((b) => {
if (b.type === 'text')
return { type: 'text', text: b.text || '(no output)' };
if (b.type === 'image' || b.type === 'document')
return b;
if (b.type === 'search_result') {
// The Messages `search_result` block param maps field-for-field onto the
// Sessions `BetaManagedAgentsSearchResultBlock`; map it explicitly rather
// than letting it fall through to the JSON.stringify branch (which would
// bury a structured result inside a text block). `citations` is required
// on the Sessions side and optional on the Messages side — default the
// flag to `false` when the producer left it unset.
return {
type: 'search_result',
source: b.source,
title: b.title,
content: b.content.map((c) => ({ type: 'text', text: c.text })),
citations: { enabled: b.citations?.enabled ?? false },
};
}
return { type: 'text', text: JSON.stringify(b) };
});
return out.length > 0 ? out : [{ type: 'text', text: '(no output)' }];
}
//# sourceMappingURL=SessionToolRunner.js.map