'use strict'; var chunkD37QL5LB_cjs = require('../chunk-D37QL5LB.cjs'); var chunkER5YO3AZ_cjs = require('../chunk-ER5YO3AZ.cjs'); var chunkRS7FSLKM_cjs = require('../chunk-RS7FSLKM.cjs'); var chunkXB4FLS7A_cjs = require('../chunk-XB4FLS7A.cjs'); var chunk2TATDSHU_cjs = require('../chunk-2TATDSHU.cjs'); var chunkXSOONORA_cjs = require('../chunk-XSOONORA.cjs'); var chunkPJIAL3WK_cjs = require('../chunk-PJIAL3WK.cjs'); var crypto = require('crypto'); var v4 = require('zod/v4'); // src/harness/types.ts function createEmptyTokenUsage() { return { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedInputTokens: 0, cacheCreationInputTokens: 0 }; } function defaultDisplayState() { return { isRunning: false, currentMessage: null, queuedFollowUps: 0, tokenUsage: createEmptyTokenUsage(), activeTools: /* @__PURE__ */ new Map(), toolInputBuffers: /* @__PURE__ */ new Map(), pendingApproval: null, pendingSuspensions: /* @__PURE__ */ new Map(), activeSubagents: /* @__PURE__ */ new Map(), omProgress: defaultOMProgressState(), bufferingMessages: false, bufferingObservations: false, modifiedFiles: /* @__PURE__ */ new Map(), tasks: [], previousTasks: [] }; } function defaultOMProgressState() { return { status: "idle", pendingTokens: 0, threshold: 3e4, thresholdPercent: 0, observationTokens: 0, reflectionThreshold: 4e4, reflectionThresholdPercent: 0, buffered: { observations: { status: "idle", chunks: 0, messageTokens: 0, projectedMessageRemoval: 0, observationTokens: 0 }, reflection: { status: "idle", inputObservationTokens: 0, observationTokens: 0 } }, generationCount: 0, stepNumber: 0, preReflectionTokens: 0 }; } // src/harness/session.ts function addOptionalUsageField(usage, key, value) { if (value !== void 0) { usage[key] = (usage[key] ?? 0) + value; } } var MODE_ID_KEY = "currentModeId"; var modeModelKey = (modeId) => `modeModelId_${modeId}`; var SessionIdentity = class { /** The memory resourceId the session currently reads/writes under. */ #resourceId; /** The resourceId the session started with, retained across resource switches. */ #defaultResourceId; constructor({ resourceId }) { this.#resourceId = resourceId; this.#defaultResourceId = resourceId; } /** The resourceId the session currently reads/writes under. */ getResourceId() { return this.#resourceId; } /** The resourceId the session started with. */ getDefaultResourceId() { return this.#defaultResourceId; } /** Point the session at a different resourceId (the default is unchanged). */ setResourceId({ resourceId }) { this.#resourceId = resourceId; } }; var SessionThread = class { /** The active thread id, or null when the session is not bound to a thread. */ #threadId = null; /** Gateway to the host's shared thread storage, injected via {@link connect}. */ #store; /** Reads the session's current resourceId (sibling identity state). */ #getResourceId; constructor(getResourceId) { this.#getResourceId = getResourceId; } /** * Attach the shared-host storage gateway the thread domain reads/writes * through. The Harness calls this once storage is available; without it the * data methods degrade gracefully. */ connect(store) { this.#store = store; } /** The active thread id, or null when the session is not bound to a thread. */ getId() { return this.#threadId; } /** Whether the session is currently bound to a thread. */ isSet() { return this.#threadId !== null; } /** The active thread id, throwing when the session is not bound to a thread. */ requireId() { if (this.#threadId === null) { throw new Error("No active thread on this session"); } return this.#threadId; } /** Bind the session to a thread. */ set({ threadId }) { this.#threadId = threadId; } /** Clear the session's thread binding. */ clear() { this.#threadId = null; } // --------------------------------------------------------------------------- // Data domain: reads/queries scoped to this session, backed by host storage. // --------------------------------------------------------------------------- /** List this session's threads (its own resource by default, or all resources). */ async list(options) { if (!this.#store) return []; return this.#store.listThreads({ resourceId: options?.allResources ? void 0 : this.#getResourceId(), includeForkedSubagents: options?.includeForkedSubagents }); } /** Fetch a single thread by id, or null when it doesn't exist / no storage. */ async getById({ threadId }) { if (!this.#store) return null; return this.#store.getById({ threadId }); } /** List messages for a thread (newest-`limit`, returned oldest-first), or all. */ async listMessages({ threadId, limit }) { if (!this.#store) return []; return this.#store.listMessages({ threadId, limit }); } /** List messages for the session's active thread (empty when not bound). */ async listActiveMessages({ limit } = {}) { if (this.#threadId === null) return []; return this.listMessages({ threadId: this.#threadId, limit }); } /** The first user message for a single thread, or null. */ async firstUserMessage({ threadId }) { const messages = await this.firstUserMessages({ threadIds: [threadId] }); return messages.get(threadId) ?? null; } /** The first user message for each given thread id. */ async firstUserMessages({ threadIds }) { if (!this.#store || threadIds.length === 0) return /* @__PURE__ */ new Map(); return this.#store.firstUserMessages({ threadIds }); } /** Read a setting (metadata value) for the active thread. */ async getSetting({ key }) { if (!this.#store || this.#threadId === null) return void 0; return this.#store.getMetadata({ threadId: this.#threadId, key }); } /** Persist a setting (metadata value) for the active thread. */ async setSetting({ key, value }) { if (!this.#store || this.#threadId === null) return; await this.#store.setMetadata({ threadId: this.#threadId, key, value }); } /** Delete a setting (metadata value) for the active thread. */ async deleteSetting({ key }) { if (!this.#store || this.#threadId === null) return; await this.#store.deleteMetadata({ threadId: this.#threadId, key }); } }; var SessionStream = class { /** The live subscription to the active thread, or null when none is open. */ #subscription = null; /** Dedup key (`agentId:resourceId:threadId`) for the open subscription, or null. */ #key = null; /** Build the dedup key identifying a subscription to `threadId` for `agent`. */ static keyFor({ agent, resourceId, threadId }) { return `${agent.id}:${resourceId}:${threadId}`; } /** Whether the open subscription already targets `key` (so it can be reused). */ matches({ key }) { return this.#key === key && this.#subscription !== null; } /** Adopt `subscription` as the live one, recording its dedup `key`. */ attach({ subscription, key }) { this.#subscription = subscription; this.#key = key; } /** Whether a subscription is currently open. */ isOpen() { return this.#subscription !== null; } /** Whether `subscription` is the one currently adopted (identity check). */ isCurrent({ subscription }) { return this.#subscription === subscription; } /** The run id the live subscription reports as active, or null when none/idle. */ activeRunId() { return this.#subscription?.activeRunId() ?? null; } /** Whether the live subscription currently has a run in flight. */ isActive() { return this.activeRunId() !== null; } /** Abort the live subscription's in-flight run, if any. Swallows errors. */ abort() { try { this.#subscription?.abort(); } catch { } } /** Detach the live subscription without aborting (e.g. on stream error). */ detach() { this.#subscription?.unsubscribe(); this.#subscription = null; this.#key = null; } /** Fully tear down the live subscription: abort, unsubscribe, and clear. */ cleanup() { this.#subscription?.abort(); this.#subscription?.unsubscribe(); this.#subscription = null; this.#key = null; } }; var SessionSuspensions = class { /** Parked tool calls awaiting a resume, keyed by `toolCallId`. */ #pending = /* @__PURE__ */ new Map(); /** Park `toolCallId` as awaiting a resume on `runId` for `toolName`. */ register({ toolCallId, runId, toolName }) { this.#pending.set(toolCallId, { runId, toolName }); } /** The parked suspension for `toolCallId`, or undefined when none. */ get({ toolCallId }) { return this.#pending.get(toolCallId); } /** Whether `toolCallId` is currently parked. */ has({ toolCallId }) { return this.#pending.has(toolCallId); } /** Drop `toolCallId` from the parked set (e.g. once resumed). */ delete({ toolCallId }) { this.#pending.delete(toolCallId); } /** Drop all parked suspensions (e.g. on abort or thread switch). */ clear() { this.#pending.clear(); } /** Whether any tool calls are parked awaiting a resume. */ hasPending() { return this.#pending.size > 0; } /** * Resolve which parked suspension to act on. With an explicit `toolCallId` it * must match a parked suspension; without one it returns the single parked * suspension (or undefined when there are zero or several). */ resolveToolCallId(toolCallId) { if (toolCallId) { return this.#pending.has(toolCallId) ? toolCallId : void 0; } if (this.#pending.size === 1) { return this.#pending.keys().next().value; } return void 0; } }; var SessionFollowUps = class { /** Messages waiting to be sent after the current run, in arrival order. */ #queue = []; /** Number of messages currently queued. */ count() { return this.#queue.length; } /** Whether the queue is empty. */ isEmpty() { return this.#queue.length === 0; } /** Append a follow-up to the back of the queue. */ enqueue(followUp) { this.#queue.push(followUp); } /** Remove and return the next follow-up, or undefined when empty. */ dequeue() { return this.#queue.shift(); } /** Put a follow-up back at the front (e.g. when draining it failed). */ requeue(followUp) { this.#queue.unshift(followUp); } /** Drop all queued follow-ups (e.g. on steer or thread switch). */ clear() { this.#queue = []; } }; var SessionApproval = class { /** Resolver for the parked approval promise, or null when nothing is gated. */ #resolve = null; /** Name of the tool currently awaiting approval, or null when none. */ #toolName = null; /** * Park a new approval for `toolName` and return a promise that resolves once * {@link resolve} is called with the user's decision. The caller awaits this * while the run is suspended on the gate. */ arm({ toolName }) { this.#toolName = toolName; return new Promise((resolve) => { this.#resolve = resolve; }); } /** Whether an approval is currently parked awaiting a decision. */ isArmed() { return this.#resolve !== null; } /** * Apply a user's {@link ApprovalResponse} to the parked gate. A no-op when * nothing is armed. `always_allow_category` runs `onAlwaysAllow` with the * gated tool name (so the caller can grant the tool's category — a lookup that * needs Harness config) and then approves; `approve`/`decline` resolve as-is. */ respond({ decision, requestContext, onAlwaysAllow }) { if (!this.isArmed()) return; if (decision === "always_allow_category" && this.#toolName) { onAlwaysAllow?.(this.#toolName); } const resolved = { decision: decision === "decline" ? "decline" : "approve", requestContext }; this.#resolve?.(resolved); this.#resolve = null; this.#toolName = null; } /** * Release a parked gate without a user decision — used when the run is * aborted. Resolves the awaiting producer as a `decline` so the gated tool is * rejected (not run) and the run can finalize. A no-op when nothing is armed. */ cancel() { if (!this.isArmed()) return; this.#resolve?.({ decision: "decline" }); this.#resolve = null; this.#toolName = null; } /** Clear the gated tool name once a parked approval has been consumed. */ clearToolName() { this.#toolName = null; } }; var SessionRun = class { /** Id of the run currently streaming on the active thread, or null when idle. */ #runId = null; /** Trace id for the current run, or null when unset. */ #traceId = null; /** Monotonic counter bumped at the start of each operation. */ #operationId = 0; /** Controller whose signal cancels the active run; null when no run is armed. */ #abortController = null; /** Whether an abort has been requested for the current run. */ #abortRequested = false; /** The current run id (null when idle). */ getRunId() { return this.#runId; } /** Set the current run id. */ setRunId({ runId }) { this.#runId = runId; } /** The current trace id (null when unset). */ getTraceId() { return this.#traceId; } /** Set the current trace id. */ setTraceId({ traceId }) { this.#traceId = traceId; } /** * Clear all run state (run id, trace id, abort controller + requested flag) * when a run ends or is reset. Does not touch the operation counter. */ reset() { this.#runId = null; this.#traceId = null; this.#abortController = null; this.#abortRequested = false; } /** Bump and return the operation counter at the start of a new operation. */ nextOperation() { this.#operationId += 1; return this.#operationId; } /** * Lazily create (if needed) and return the AbortController for the current * run. Callers pass its `.signal` into the underlying stream. */ ensureAbortController() { this.#abortController ??= new AbortController(); return this.#abortController; } /** Signal for the current run's AbortController, or undefined when none is armed. */ getAbortSignal() { return this.#abortController?.signal; } /** * Whether a run is currently in progress. A run is armed with an * AbortController for its duration, so the presence of one is what "running" * means; this is the semantic accessor callers should use. */ isRunning() { return this.#abortController !== null; } /** * Whether an AbortController is currently armed. Equivalent to * {@link isRunning} today; kept for callers that assert on the controller's * lifecycle specifically (e.g. that it was cleared after an abort). */ hasAbortController() { return this.#abortController !== null; } /** Clear the abort-requested flag at the start of a fresh run. */ clearAbortRequested() { this.#abortRequested = false; } /** Whether an abort has been requested for the current run. */ isAbortRequested() { return this.#abortRequested; } /** * Request an abort: mark the run as aborting and fire the AbortController (if * armed), then drop the controller. Leaves the requested flag set so the * run-end path can resolve its reason as 'aborted'; {@link reset} clears it. */ requestAbort() { this.#abortRequested = true; if (this.#abortController) { try { this.#abortController.abort(); } catch { } this.#abortController = null; } } }; var SessionModel = class { #id = ""; #store; constructor(store) { this.#store = store; } /** The currently-selected model id ('' when none selected yet). */ get() { return this.#id; } /** Whether a model is currently selected. */ hasSelection() { return this.#id !== ""; } /** Set the in-memory selected model id (no persistence). */ set({ modelId }) { this.#id = modelId; } /** Persist `modelId` as the last-used model for `modeId`. */ async saveForMode({ modeId, modelId }) { await this.#store()?.set(modeModelKey(modeId), modelId); } /** * Resolve the model for `modeId`: the persisted per-mode model if present, * else `defaultModelId`, else null. */ async resolveForMode({ modeId, defaultModelId }) { const stored = await this.#store()?.get(modeModelKey(modeId)); if (stored) return stored; return defaultModelId ?? null; } }; var SessionMode = class { /** Id of the currently-selected mode. Empty until the Harness resolves its default mode. */ #id = ""; /** * Monotonically increasing counter bumped on each switch. A slower in-flight * switch detects it was superseded by a newer one and bails. */ #switchVersion = 0; #store; #model; /** * Resolves a mode id to its full definition. Injected by the Harness via * {@link setResolver}, since the mode *catalog* (`config.modes`) is host config. */ #resolveMode; constructor(store, model) { this.#store = store; this.#model = model; } /** * Attach the resolver that maps a mode id to its definition. The Harness owns * the mode catalog (`config.modes`) and injects this once. */ setResolver(resolve) { this.#resolveMode = resolve; } /** The currently-selected mode id. */ get() { return this.#id; } /** * Resolve the currently-selected mode id to its full definition against the * host's mode catalog. Throws if the selected mode id isn't in the catalog. */ resolve() { const mode = this.#resolveMode?.(this.#id) ?? null; if (!mode) { throw new Error(`Mode not found: ${this.#id}`); } return mode; } /** Set the currently-selected mode id (on default resolution or hydration). */ set({ modeId }) { this.#id = modeId; } /** * Switch to `modeId`, coordinating the selected model and persistence. * * The Harness handles aborting in-flight work and emitting events; this owns * the version-guarded sequence: remember the outgoing mode's model, persist * the new mode, then resolve and apply the incoming mode's model. A newer * switch starting mid-flight supersedes this one, which then bails. * * Returns the resolved model id for the new mode (or null), so the caller can * emit `model_changed` after applying it. */ async switch({ modeId, defaultModelId }) { const previousModeId = this.#id; const previousModelId = this.#model.get(); const version = ++this.#switchVersion; this.#id = modeId; if (previousModelId) { await this.#model.saveForMode({ modeId: previousModeId, modelId: previousModelId }); } if (this.#switchVersion !== version) return { modelId: null }; await this.#store()?.set(MODE_ID_KEY, modeId); if (this.#switchVersion !== version) return { modelId: null }; const modelId = await this.#model.resolveForMode({ modeId, defaultModelId }); if (this.#switchVersion !== version) return { modelId: null }; if (modelId) { this.#model.set({ modelId }); } return { modelId }; } }; var SessionState = class { #state; #updateQueue = Promise.resolve(); #schema; #emit; constructor({ initialState, stateSchema, emit }) { this.#schema = stateSchema ? chunkXB4FLS7A_cjs.toStandardSchema(stateSchema) : void 0; this.#state = { ...this.getSchemaDefaults(), ...initialState }; this.#emit = emit; } get() { return { ...this.#state }; } getSchemaDefaults() { if (!this.#schema) return {}; const defaults = {}; try { const jsonSchema = this.#schema["~standard"].jsonSchema.output({ target: "draft-07" }); if (jsonSchema?.properties) { for (const [key, prop] of Object.entries(jsonSchema.properties)) { if (prop.default !== void 0) { defaults[key] = prop.default; } } } } catch { } return defaults; } async apply(updates) { const changedKeys = Object.keys(updates); const newState = { ...this.#state, ...updates }; if (this.#schema) { const result = await this.#schema["~standard"].validate(newState); if (result.issues) { const messages = result.issues.map((i) => i.message).join("; "); throw new Error(`Invalid state update: ${messages}`); } this.#state = result.value; } else { this.#state = newState; } this.#emit?.({ type: "state_changed", state: this.get(), changedKeys }); } set(updates) { const updateSnapshot = { ...updates }; const run = this.#updateQueue.then(() => this.apply(updateSnapshot)); this.#updateQueue = run.then( () => void 0, () => void 0 ); return run; } update(updater) { const run = this.#updateQueue.then(async () => { const update = await updater(this.get()); if (update.updates && Object.keys(update.updates).length > 0) { await this.apply(update.updates); } for (const event of update.events ?? []) { this.#emit?.(event); } return update.result; }); this.#updateQueue = run.then( () => void 0, () => void 0 ); return run; } }; var SessionDisplayState = class { constructor(deps) { this.deps = deps; } deps; #state = defaultDisplayState(); /** * A read-only snapshot of the canonical display state. UIs should render from * this instead of building state up from raw events. */ get() { return this.#state; } /** * Drop the display mirror of every parked tool suspension. Used on abort, * which abandons the run's parked suspensions; the caller dispatches * `display_state_changed`. */ clearPendingSuspensions() { this.#state.pendingSuspensions.clear(); } /** * Clear the modified-files tally without touching the rest of the snapshot. * Used after a clone, which starts the cloned thread with a clean working set * while the surrounding UI reset handles tasks/tools explicitly. */ clearModifiedFiles() { this.#state.modifiedFiles.clear(); } /** * Drop the display mirror of a single parked tool suspension once it has been * resumed, so the UI stops rendering only the resolved prompt while any other * parked suspensions stay visible. */ deletePendingSuspension(toolCallId) { this.#state.pendingSuspensions.delete(toolCallId); } /** * Restore task display state after a UI replays persisted task-tool history. * Updates the snapshot without emitting a live `task_updated` event, since no * task tool just ran. The caller dispatches `display_state_changed`. */ restoreTasks(tasks) { this.#state.previousTasks = [...this.#state.tasks]; this.#state.tasks = [...tasks]; } /** * Reset display fields scoped to a thread. Called on thread switch/creation. * Also clears the session's follow-up queue (mirrored by `queuedFollowUps`). */ resetThread() { const ds = this.#state; ds.activeTools = /* @__PURE__ */ new Map(); ds.toolInputBuffers = /* @__PURE__ */ new Map(); ds.pendingApproval = null; ds.pendingSuspensions = /* @__PURE__ */ new Map(); ds.activeSubagents = /* @__PURE__ */ new Map(); ds.currentMessage = null; this.deps.clearFollowUps(); ds.queuedFollowUps = 0; ds.modifiedFiles = /* @__PURE__ */ new Map(); ds.tasks = []; ds.previousTasks = []; ds.omProgress = defaultOMProgressState(); ds.bufferingMessages = false; ds.bufferingObservations = false; } /** * Apply a display-state update based on an incoming event. The centralized * state machine that keeps {@link HarnessDisplayState} in sync with every * event the Harness emits. */ apply(event) { const ds = this.#state; switch (event.type) { // ── Agent lifecycle ──────────────────────────────────────────────── case "agent_start": ds.isRunning = true; ds.activeTools = /* @__PURE__ */ new Map(); ds.toolInputBuffers = /* @__PURE__ */ new Map(); ds.currentMessage = null; ds.pendingApproval = null; break; case "agent_end": ds.isRunning = false; ds.pendingApproval = null; if (event.reason !== "suspended") { ds.pendingSuspensions.clear(); } for (const [, tool] of ds.activeTools) { if (tool.status === "running" || tool.status === "streaming_input") { tool.status = "error"; } } ds.activeSubagents = /* @__PURE__ */ new Map(); break; // ── Message streaming ────────────────────────────────────────────── case "message_start": ds.currentMessage = event.message; break; case "message_update": ds.currentMessage = event.message; break; case "message_end": ds.currentMessage = event.message; break; // ── Tool lifecycle ───────────────────────────────────────────────── case "tool_input_start": { ds.toolInputBuffers.set(event.toolCallId, { text: "", toolName: event.toolName }); const existing = ds.activeTools.get(event.toolCallId); if (existing) { existing.status = "streaming_input"; } else { ds.activeTools.set(event.toolCallId, { name: event.toolName, args: {}, status: "streaming_input" }); } break; } case "tool_input_delta": { const buf = ds.toolInputBuffers.get(event.toolCallId); if (buf) { buf.text += event.argsTextDelta; } break; } case "tool_input_end": ds.toolInputBuffers.delete(event.toolCallId); break; case "tool_start": { const existingTool = ds.activeTools.get(event.toolCallId); if (existingTool) { existingTool.name = event.toolName; existingTool.args = event.args; existingTool.status = "running"; } else { ds.activeTools.set(event.toolCallId, { name: event.toolName, args: event.args, status: "running" }); } break; } case "tool_update": { const tool = ds.activeTools.get(event.toolCallId); if (tool) { tool.partialResult = typeof event.partialResult === "string" ? event.partialResult : chunkER5YO3AZ_cjs.safeStringify(event.partialResult); } break; } case "tool_end": { const endedTool = ds.activeTools.get(event.toolCallId); if (endedTool) { endedTool.status = event.isError ? "error" : "completed"; endedTool.result = event.result; endedTool.isError = event.isError; } if (!event.isError) { const FILE_TOOLS = ["string_replace_lsp", "write_file", "ast_smart_edit"]; const toolState = ds.activeTools.get(event.toolCallId); if (toolState && FILE_TOOLS.includes(toolState.name)) { const toolArgs = toolState.args; const filePath = toolArgs?.path; if (filePath) { const existing = ds.modifiedFiles.get(filePath); if (existing) { existing.operations.push(toolState.name); } else { ds.modifiedFiles.set(filePath, { operations: [toolState.name], firstModified: /* @__PURE__ */ new Date() }); } } } } break; } case "shell_output": { const shellTool = ds.activeTools.get(event.toolCallId); if (shellTool) { shellTool.shellOutput = (shellTool.shellOutput ?? "") + event.output; } break; } case "tool_approval_required": ds.pendingApproval = { toolCallId: event.toolCallId, toolName: event.toolName, args: event.args }; break; case "tool_suspended": ds.pendingSuspensions.set(event.toolCallId, { toolCallId: event.toolCallId, toolName: event.toolName, args: event.args, suspendPayload: event.suspendPayload, resumeSchema: event.resumeSchema }); break; // ── Subagent tracking ────────────────────────────────────────────── case "subagent_start": { const displayName = this.deps.getSubagentDisplayName(event.agentType); ds.activeSubagents.set(event.toolCallId, { agentType: event.agentType, ...displayName !== void 0 ? { displayName } : {}, task: event.task, modelId: event.modelId, forked: event.forked, toolCalls: [], textDelta: "", status: "running" }); break; } case "subagent_text_delta": { const sub = ds.activeSubagents.get(event.toolCallId); if (sub) { sub.textDelta += event.textDelta; } break; } case "subagent_tool_start": { const subAgent = ds.activeSubagents.get(event.toolCallId); if (subAgent) { subAgent.toolCalls.push({ name: event.subToolName, isError: false }); } break; } case "subagent_tool_end": { const subTool = ds.activeSubagents.get(event.toolCallId); if (subTool) { const tc = subTool.toolCalls.find((t) => t.name === event.subToolName && !t.isError); if (tc) { tc.isError = event.isError; } } break; } case "subagent_end": { const endedSub = ds.activeSubagents.get(event.toolCallId); if (endedSub) { endedSub.status = event.isError ? "error" : "completed"; endedSub.durationMs = event.durationMs; endedSub.result = event.result; } break; } // ── Observational Memory ─────────────────────────────────────────── case "om_status": { const w = event.windows; ds.omProgress.pendingTokens = w.active.messages.tokens; ds.omProgress.threshold = w.active.messages.threshold; ds.omProgress.thresholdPercent = w.active.messages.threshold > 0 ? w.active.messages.tokens / w.active.messages.threshold * 100 : 0; ds.omProgress.observationTokens = w.active.observations.tokens; ds.omProgress.reflectionThreshold = w.active.observations.threshold; ds.omProgress.reflectionThresholdPercent = w.active.observations.threshold > 0 ? w.active.observations.tokens / w.active.observations.threshold * 100 : 0; ds.omProgress.buffered = { observations: { ...w.buffered.observations }, reflection: { ...w.buffered.reflection } }; ds.omProgress.generationCount = event.generationCount; ds.omProgress.stepNumber = event.stepNumber; ds.bufferingMessages = w.buffered.observations.status === "running"; ds.bufferingObservations = w.buffered.reflection.status === "running"; break; } case "om_observation_start": ds.omProgress.status = "observing"; ds.omProgress.cycleId = event.cycleId; ds.omProgress.startTime = Date.now(); break; case "om_observation_end": ds.omProgress.status = "idle"; ds.omProgress.cycleId = void 0; ds.omProgress.startTime = void 0; ds.omProgress.observationTokens = event.observationTokens; ds.omProgress.pendingTokens = 0; ds.omProgress.thresholdPercent = 0; break; case "om_observation_failed": ds.omProgress.status = "idle"; ds.omProgress.cycleId = void 0; ds.omProgress.startTime = void 0; break; case "om_reflection_start": ds.omProgress.status = "reflecting"; ds.omProgress.cycleId = event.cycleId; ds.omProgress.startTime = Date.now(); ds.omProgress.preReflectionTokens = ds.omProgress.observationTokens; ds.omProgress.observationTokens = event.tokensToReflect; ds.omProgress.reflectionThresholdPercent = ds.omProgress.reflectionThreshold > 0 ? event.tokensToReflect / ds.omProgress.reflectionThreshold * 100 : 0; break; case "om_reflection_end": ds.omProgress.status = "idle"; ds.omProgress.cycleId = void 0; ds.omProgress.startTime = void 0; ds.omProgress.observationTokens = event.compressedTokens; ds.omProgress.reflectionThresholdPercent = ds.omProgress.reflectionThreshold > 0 ? event.compressedTokens / ds.omProgress.reflectionThreshold * 100 : 0; break; case "om_reflection_failed": ds.omProgress.status = "idle"; ds.omProgress.cycleId = void 0; ds.omProgress.startTime = void 0; break; case "om_buffering_start": if (event.operationType === "observation") { ds.bufferingMessages = true; } else { ds.bufferingObservations = true; } break; case "om_buffering_end": if (event.operationType === "observation") { ds.bufferingMessages = false; } else { ds.bufferingObservations = false; } break; case "om_buffering_failed": if (event.operationType === "observation") { ds.bufferingMessages = false; } else { ds.bufferingObservations = false; } break; case "om_activation": if (event.operationType === "observation") { ds.bufferingMessages = false; } else { ds.bufferingObservations = false; } break; // ── Token usage ──────────────────────────────────────────────────── case "usage_update": ds.tokenUsage = this.deps.getTokenUsage(); break; // ── Tasks ────────────────────────────────────────────────────────── case "task_updated": ds.previousTasks = [...ds.tasks]; ds.tasks = event.tasks; break; // ── Follow-up queue ──────────────────────────────────────────────── case "follow_up_queued": ds.queuedFollowUps = event.count; break; // ── Thread lifecycle ─────────────────────────────────────────────── case "thread_changed": this.resetThread(); ds.tokenUsage = this.deps.getTokenUsage(); break; case "thread_created": this.resetThread(); ds.tokenUsage = createEmptyTokenUsage(); break; case "thread_deleted": if (!this.deps.getThreadId()) { this.resetThread(); ds.tokenUsage = createEmptyTokenUsage(); } break; // ── State changes (for OM threshold overrides) ────────────────────── case "state_changed": { const keys = event.changedKeys; if (keys.includes("observationThreshold")) { const value = event.state.observationThreshold; if (typeof value === "number") { ds.omProgress.threshold = value; ds.omProgress.thresholdPercent = value > 0 ? ds.omProgress.pendingTokens / value * 100 : 0; } } if (keys.includes("reflectionThreshold")) { const value = event.state.reflectionThreshold; if (typeof value === "number") { ds.omProgress.reflectionThreshold = value; ds.omProgress.reflectionThresholdPercent = value > 0 ? ds.omProgress.observationTokens / value * 100 : 0; } } break; } } } }; var Session = class { /** Tool categories the user has granted "allow" for the lifetime of this session. */ #grantedCategories = /* @__PURE__ */ new Set(); /** Individual tool names the user has granted "allow" for the lifetime of this session. */ #grantedTools = /* @__PURE__ */ new Set(); /** Running token-usage tally for the active thread. */ #tokenUsage = createEmptyTokenUsage(); /** Thread-settings persistence handle, injected by the Harness via {@link setStore}. */ #store; /** Resolves a tool name to its category, injected by the Harness via {@link setCategoryResolver} (the category map is Harness config). */ #resolveCategory; /** Resolves a subagent's display name from Harness config, injected via {@link setSubagentNameResolver}. */ #resolveSubagentName; /** The session's currently-selected model (source of truth) + per-mode memory. */ model = new SessionModel(() => this.#store); /** The session's currently-selected mode and switch sequence. */ mode = new SessionMode(() => this.#store, this.model); /** Transient run identity (run id, trace id, operation counter) for the active run. */ run = new SessionRun(); /** Live subscription to the active thread's agent event stream. */ stream = new SessionStream(); /** Tool calls parked awaiting a resume (the resume data, keyed by toolCallId). */ suspensions = new SessionSuspensions(); /** Messages queued to send after the active run finishes. */ followUps = new SessionFollowUps(); /** The interactive tool-approval gate the current run parks on. */ approval = new SessionApproval(); /** The session's identity: the memory resourceId it reads/writes under. */ identity; /** The session's thread domain: current binding + reads scoped to it. */ thread; /** The canonical display state a UI renders, plus the reducer that maintains it. */ displayState; /** The session-owned Harness state domain. */ state; constructor({ resourceId, state }) { this.identity = new SessionIdentity({ resourceId }); this.thread = new SessionThread(() => this.identity.getResourceId()); this.displayState = new SessionDisplayState({ getTokenUsage: () => this.getTokenUsage(), getSubagentDisplayName: (agentType) => this.#resolveSubagentName?.(agentType), getThreadId: () => this.thread.getId(), clearFollowUps: () => this.followUps.clear() }); this.state = new SessionState(state ?? { initialState: {} }); } /** * Attach the thread-settings store the Session persists mode/model through. * The Harness calls this once storage is available; without it, mode/model * state lives purely in memory. */ setStore(store) { this.#store = store; } /** * Attach the tool→category resolver used when a user picks "always allow * category". The category map is Harness config, so the Harness injects this * once; without it, an "always_allow_category" decision simply approves. */ setCategoryResolver(resolveCategory) { this.#resolveCategory = resolveCategory; } /** * Attach the subagent display-name resolver the display-state reducer uses to * label active subagents. The subagent catalog is Harness config, so the * Harness injects this once; without it, subagents render without a name. */ setSubagentNameResolver(resolveSubagentName) { this.#resolveSubagentName = resolveSubagentName; } /** * The id of the run currently active on this session: the live subscription's * active run id when it is streaming, falling back to the last run id the run * tracker observed. Null when the session is idle. */ getCurrentRunId() { return this.stream.activeRunId() ?? this.run.getRunId(); } /** * Abort the session's active run: drop any parked tool suspensions, abort the * live subscription's in-flight run, and mark the run as aborting so the * run-end path resolves its reason as 'aborted'. * * Dropping the parked suspensions matters because a run sitting in a tool * `suspend()` (e.g. `ask_user` / `request_access`) is not actively streaming, * so aborting the controller alone would leave it orphaned. The Harness still * clears its own display-state mirror of those suspensions separately. * * Releasing a parked tool-approval gate matters for the same reason: a run * awaiting `approval.arm()` is not streaming, so we resolve it as a decline so * the gated tool is rejected and the run can finalize rather than hang. */ abortRun() { this.suspensions.clear(); this.approval.cancel(); this.stream.abort(); this.run.requestAbort(); } /** * Respond to the parked tool-approval gate with the user's decision. A no-op * when nothing is awaiting approval. "always_allow_category" grants the gated * tool's category for the rest of the session (resolved via the injected * {@link setCategoryResolver}) and then approves; "approve"/"decline" release * the run as-is. */ respondToToolApproval({ decision, requestContext }) { this.approval.respond({ decision, requestContext, onAlwaysAllow: (toolName) => { const category = this.#resolveCategory?.(toolName); if (category) this.grantCategory(category); } }); } /** Grant a tool category "allow" for the remainder of the session. */ grantCategory(category) { this.#grantedCategories.add(category); } /** Grant an individual tool "allow" for the remainder of the session. */ grantTool(toolName) { this.#grantedTools.add(toolName); } /** Whether the given tool category has been granted for the session. */ hasCategoryGrant(category) { return this.#grantedCategories.has(category); } /** Whether the given tool has been granted for the session. */ hasToolGrant(toolName) { return this.#grantedTools.has(toolName); } /** Snapshot of all session-scoped grants. */ getGrants() { return { categories: [...this.#grantedCategories], tools: [...this.#grantedTools] }; } /** A copy of the running token-usage tally for the active thread. */ getTokenUsage() { return { ...this.#tokenUsage }; } /** * Replace the running tally, e.g. when hydrating from persisted thread * metadata on thread switch. */ setTokenUsage(usage) { this.#tokenUsage = { ...usage }; } /** Reset the running tally to zero, e.g. on a new/empty thread. */ resetTokenUsage() { this.#tokenUsage = createEmptyTokenUsage(); } /** Fold a single step's usage into the running tally. */ addUsage(stepUsage) { this.#tokenUsage.promptTokens += stepUsage.promptTokens; this.#tokenUsage.completionTokens += stepUsage.completionTokens; this.#tokenUsage.totalTokens += stepUsage.totalTokens; addOptionalUsageField(this.#tokenUsage, "reasoningTokens", stepUsage.reasoningTokens); addOptionalUsageField(this.#tokenUsage, "cachedInputTokens", stepUsage.cachedInputTokens); addOptionalUsageField(this.#tokenUsage, "cacheCreationInputTokens", stepUsage.cacheCreationInputTokens); if (stepUsage.raw !== void 0) { this.#tokenUsage.raw = stepUsage.raw; } } }; var FORKED_SUBAGENT_NESTING_NOTICE = "Do not call the `subagent` tool. You are currently running inside a forked subagent, and this is the maximum allowed subagent nesting level. Further subagent calls will return an error. Answer the task directly using the conversation history and the other tools available to you."; var FORKED_SUBAGENT_TASK_NOTICE = "Do not call `task_write`, `task_update`, `task_complete`, or `task_check` inside a forked subagent. Forked subagents keep the parent task-tool schemas for prompt-cache stability, but the parent agent owns the visible task list. Track forked subagent work in your final answer instead."; function createSubagentTool(opts) { const { subagents, resolveModel, harnessTools, fallbackModelId } = opts; const subagentIds = subagents.map((s) => s.id); const typeDescriptions = subagents.map((s) => `- **${s.id}** (${s.name}): ${s.description}`).join("\n"); return chunkRS7FSLKM_cjs.createTool({ id: "subagent", description: `Delegate a focused task to a specialized subagent. The subagent runs independently with a constrained toolset, then returns its findings as text. Available agent types: ${typeDescriptions} By default the subagent runs in its own context \u2014 it does NOT see the parent conversation history. Write a clear, self-contained task description. Set \`forked: true\` for context-dependent parallel work that needs the parent conversation, prior tool results, or the parent tool environment. Omit it for self-contained delegation. A forked subagent reuses the parent agent's instructions and tools so the prompt prefix stays cache-friendly. Use this tool when: - You want to run multiple investigations in parallel - The task is self-contained and can be delegated`, inputSchema: v4.z.object({ agentType: v4.z.enum(subagentIds).describe("Type of subagent to spawn"), task: v4.z.string().describe( "Clear, self-contained description of what the subagent should do. For non-forked subagents include all relevant context \u2014 the subagent cannot see the parent conversation." ), modelId: v4.z.string().optional().describe( "Optional model ID override for this task. Ignored when `forked: true` (the parent agent's model is used)." ), forked: v4.z.boolean().optional().describe( "If true, fork the parent conversation: clone the parent thread and run with the parent agent's instructions/tools so prompt cache is preserved. Requires memory to be configured on the Harness. Defaults to the subagent definition's `forked` setting." ) }), execute: async (input, context) => { const { agentType, modelId, forked } = input; let { task } = input; const displayTask = task; const definition = subagents.find((s) => s.id === agentType); if (!definition) { return { content: `Unknown agent type: ${agentType}. Valid types: ${subagentIds.join(", ")}`, isError: true }; } const harnessCtx = context?.requestContext?.get("harness"); const emitEvent = harnessCtx?.emitEvent; const abortSignal = harnessCtx?.abortSignal; const toolCallId = context?.agent?.toolCallId ?? "unknown"; const workspace = context?.workspace; const runAsForked = forked ?? definition.forked ?? false; let subagentToRun; let resolvedModelId; let subagentRequestContext; let streamMemory; let streamMaxSteps; let streamStopWhen; let streamPrepareStep; let forkedToolsets; if (runAsForked) { const parentAgent = opts.getParentAgent?.(); if (!parentAgent) { return { content: "Forked subagent requires a parent agent. None is configured on this Harness.", isError: true }; } const parentThreadId = harnessCtx?.threadId; if (!parentThreadId) { return { content: "Forked subagent requires an active parent thread; none is set on the Harness.", isError: true }; } if (!opts.cloneThreadForFork) { return { content: "Forked subagent requires memory to be configured on the Harness so the parent thread can be cloned.", isError: true }; } await context?.agent?.flushMessages?.().catch(() => { }); let forkedThread; try { forkedThread = await opts.cloneThreadForFork({ sourceThreadId: parentThreadId, resourceId: harnessCtx?.resourceId, title: `Fork: ${definition.name} subagent` }); } catch (err) { return { content: `Failed to clone parent thread for forked subagent: ${err instanceof Error ? err.message : String(err)}`, isError: true }; } subagentToRun = parentAgent; resolvedModelId = opts.getParentModelId?.() || "parent-agent"; task = `${task} ${FORKED_SUBAGENT_NESTING_NOTICE} ${FORKED_SUBAGENT_TASK_NOTICE}`; streamMemory = { thread: forkedThread.id, resource: forkedThread.resourceId }; streamMaxSteps = 1e3; streamStopWhen = void 0; streamPrepareStep = void 0; if (context?.requestContext) { subagentRequestContext = new chunkPJIAL3WK_cjs.RequestContext(context.requestContext.entries()); if (harnessCtx) { subagentRequestContext.set("harness", { ...harnessCtx, threadId: forkedThread.id, resourceId: forkedThread.resourceId }); } } const inheritedToolsets = await opts.getParentToolsets?.(subagentRequestContext); if (inheritedToolsets) { forkedToolsets = {}; for (const [setName, setTools] of Object.entries(inheritedToolsets)) { const patched = {}; for (const [toolId, tool] of Object.entries(setTools)) { if (toolId === "subagent") { patched[toolId] = patchSubagentToolForFork(tool); } else if (isForkedTaskToolId(toolId)) { patched[toolId] = patchTaskToolForFork(tool, toolId); } else { patched[toolId] = tool; } } forkedToolsets[setName] = patched; } } } else { const mergedTools = { ...definition.tools }; if (definition.allowedHarnessTools && harnessTools) { for (const toolId of definition.allowedHarnessTools) { if (harnessTools[toolId] && !mergedTools[toolId]) { mergedTools[toolId] = harnessTools[toolId]; } } } const harnessModelId = harnessCtx?.getSubagentModelId?.({ agentType }) ?? void 0; const maybeModelId = modelId ?? harnessModelId ?? definition.defaultModelId ?? fallbackModelId; if (!maybeModelId) { return { content: "No model ID available for subagent. Configure defaultModelId.", isError: true }; } resolvedModelId = maybeModelId; let model; try { model = resolveModel(resolvedModelId); } catch (err) { return { content: `Failed to resolve model "${resolvedModelId}": ${err instanceof Error ? err.message : String(err)}`, isError: true }; } subagentToRun = new chunkD37QL5LB_cjs.Agent({ id: `subagent-${definition.id}`, name: `${definition.name} Subagent`, instructions: definition.instructions, model, tools: mergedTools, workspace }); const allowedWs = definition.allowedWorkspaceTools ? new Set(definition.allowedWorkspaceTools) : void 0; const allWorkspaceToolNames = workspace && allowedWs ? new Set( Object.keys( await chunkD37QL5LB_cjs.createWorkspaceTools(workspace, { requestContext: context?.requestContext ?? {}, workspace }) ) ) : void 0; streamMaxSteps = definition.maxSteps ?? (definition.stopWhen ? void 0 : 50); streamStopWhen = definition.stopWhen; streamPrepareStep = allowedWs && allWorkspaceToolNames ? ({ tools }) => ({ activeTools: Object.keys(tools ?? {}).filter((k) => !allWorkspaceToolNames.has(k) || allowedWs.has(k)) }) : void 0; if (context?.requestContext) { subagentRequestContext = new chunkPJIAL3WK_cjs.RequestContext(context.requestContext.entries()); if (harnessCtx) { subagentRequestContext.set("harness", { ...harnessCtx, threadId: null, resourceId: "" }); } } } const startTime = Date.now(); emitEvent?.({ type: "subagent_start", toolCallId, agentType, task: displayTask, modelId: resolvedModelId, forked: runAsForked }); let partialText = ""; try { const response = await subagentToRun.stream(task, { maxSteps: streamMaxSteps, stopWhen: streamStopWhen, abortSignal, requireToolApproval: false, requestContext: subagentRequestContext, ...streamMemory && { memory: streamMemory }, ...forkedToolsets && { toolsets: forkedToolsets }, ...context?.tracingContext && { tracingContext: context.tracingContext }, prepareStep: streamPrepareStep }); for await (const chunk of response.fullStream) { switch (chunk.type) { case "text-delta": partialText += chunk.payload.text; emitEvent?.({ type: "subagent_text_delta", toolCallId, agentType, textDelta: chunk.payload.text }); break; case "tool-call": if (!(runAsForked && chunk.payload.toolName === "subagent")) { emitEvent?.({ type: "subagent_tool_start", toolCallId, agentType, subToolName: chunk.payload.toolName, subToolArgs: chunk.payload.args }); } break; case "tool-result": { const isErr = chunk.payload.isError ?? false; if (!(runAsForked && chunk.payload.toolName === "subagent")) { emitEvent?.({ type: "subagent_tool_end", toolCallId, agentType, subToolName: chunk.payload.toolName, subToolResult: chunk.payload.result, isError: isErr }); } break; } } } if (abortSignal?.aborted) { const durationMs2 = Date.now() - startTime; const abortResult = partialText ? `[Aborted by user] Partial output: ${partialText}` : "[Aborted by user]"; emitEvent?.({ type: "subagent_end", toolCallId, agentType, result: abortResult, isError: false, durationMs: durationMs2 }); return { content: abortResult, isError: false }; } const fullOutput = await response.getFullOutput(); const resultText = fullOutput.text || partialText; const durationMs = Date.now() - startTime; emitEvent?.({ type: "subagent_end", toolCallId, agentType, result: resultText, isError: false, durationMs }); return { content: resultText, isError: false }; } catch (err) { const isAbort = err instanceof Error && (err.name === "AbortError" || err.message?.includes("abort") || err.message?.includes("cancel")); const durationMs = Date.now() - startTime; if (isAbort) { const abortResult = partialText ? `[Aborted by user] Partial output: ${partialText}` : "[Aborted by user]"; emitEvent?.({ type: "subagent_end", toolCallId, agentType, result: abortResult, isError: false, durationMs }); return { content: abortResult, isError: false }; } const message = err instanceof Error ? err.message : String(err); emitEvent?.({ type: "subagent_end", toolCallId, agentType, result: message, isError: true, durationMs }); return { content: `Subagent "${definition.name}" failed: ${message}`, isError: true }; } } }); } function patchSubagentToolForFork(tool) { const stubExecute = async () => ({ content: FORKED_SUBAGENT_NESTING_NOTICE, isError: true }); return Object.assign({}, tool, { execute: stubExecute }); } function isForkedTaskToolId(toolId) { return toolId === "task_write" || toolId === "task_update" || toolId === "task_complete" || toolId === "task_check"; } function patchTaskToolForFork(tool, toolId) { const stubExecute = async () => { const baseResult = { content: FORKED_SUBAGENT_TASK_NOTICE, tasks: [], isError: true }; if (toolId === "task_check") { const taskCheck = chunkD37QL5LB_cjs.summarizeTaskCheck([]); return { ...baseResult, summary: taskCheck.summary, incompleteTasks: taskCheck.incompleteTasks }; } return baseResult; }; return Object.assign({}, tool, { execute: stubExecute }); } function parseSubagentMeta(content) { const match = content.match(/\n$/); if (!match) return { text: content }; const text = content.slice(0, match.index); const modelId = match[1]; const durationMs = parseInt(match[2], 10); const toolCalls = match[3] ? match[3].split(",").filter(Boolean).map((entry) => { const [name, status] = entry.split(":"); return { name, isError: status === "err" }; }) : []; return { text, modelId, durationMs, toolCalls }; } // src/harness/harness.ts function validateModes(modes) { const modeIds = /* @__PURE__ */ new Set(); for (const mode of modes) { if (modeIds.has(mode.id)) { throw new Error(`Duplicate mode id "${mode.id}" found when creating the Harness`); } modeIds.add(mode.id); const modeRecord = mode; if (modeRecord.tools && modeRecord.additionalTools) { throw new Error( `Mode "${modeRecord.id}" cannot set both "tools" and "additionalTools" - choose replace OR augment` ); } } for (const mode of modes) { if (mode.transitionsTo === mode.id) { throw new Error(`Mode "${mode.id}" transitionsTo cannot reference itself`); } if (mode.transitionsTo && !modeIds.has(mode.transitionsTo)) { throw new Error(`Mode "${mode.id}" transitionsTo references unknown mode "${mode.transitionsTo}"`); } } } function describeNonSuccessFinishReason(reason, providerMetadata) { switch (reason) { case "content-filter": { const stopDetails = providerMetadata?.anthropic?.stopDetails; const explanation = stopDetails && typeof stopDetails.explanation === "string" ? stopDetails.explanation : void 0; const category = stopDetails && typeof stopDetails.category === "string" ? stopDetails.category : void 0; const detail = explanation ?? (category ? `category: ${category}` : void 0); return detail ? `The model stopped on a content filter (${detail}).` : "The model stopped on a content filter."; } case "error": return "The model stream ended with an error before producing a final response."; case "length": return "The model stopped because it reached its maximum output length before finishing."; default: return void 0; } } var FABLE_FALLBACK_MODEL = "claude-opus-4-8"; var HARNESS_MAX_STEPS = 1e3; function buildFableFallbackProviderOptions(modelId) { if (!/(^|\/)claude-fable-5$/.test(modelId)) { return void 0; } return { anthropic: { fallbacks: [{ model: FABLE_FALLBACK_MODEL }] } }; } function describeServerSideFallback(providerMetadata) { const fallback = chunkD37QL5LB_cjs.getServerSideFallbackInfo(providerMetadata); if (!fallback) { return void 0; } return fallback.model ? `The selected model declined this turn; the response was generated by fallback model ${fallback.model}.` : "The selected model declined this turn; the response was generated by a fallback model."; } function getUsageNumber(usage, key) { const value = usage[key]; if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string" && value.trim() !== "") { const numericValue = Number(value); if (Number.isFinite(numericValue)) { return numericValue; } } return void 0; } function addOptionalUsageField2(usage, key, value) { if (value !== void 0) { usage[key] = (usage[key] ?? 0) + value; } } function getDisplayTransform(metadata, phase, fallback) { const transform = chunk2TATDSHU_cjs.getTransformedToolPayload(metadata, "display", phase); return chunk2TATDSHU_cjs.hasTransformedToolPayload(transform) ? transform.transformed : fallback; } function getStringValue(value) { return typeof value === "string" ? value : void 0; } function getRecordValue(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : void 0; } function signalContentsToHarnessContent(contents) { if (typeof contents === "string") return [{ type: "text", text: contents }]; return contents.flatMap((part) => { if (part.type === "text") { return [{ type: "text", text: part.text }]; } if (typeof part.data !== "string") return []; if (part.mediaType.startsWith("image/")) { return [{ type: "image", data: part.data, mimeType: part.mediaType }]; } return [ { type: "file", data: part.data, mediaType: part.mediaType, filename: part.filename } ]; }); } function toSystemReminderContent(payload) { const attributes = getRecordValue(payload.attributes); const metadata = getRecordValue(payload.metadata); const message = signalContentsToText(payload.contents); if (!message) return void 0; return { type: "system_reminder", message, reminderType: getStringValue(payload.reminderType) ?? getStringValue(attributes?.type) ?? getStringValue(payload.type), path: getStringValue(payload.path) ?? getStringValue(attributes?.path), precedesMessageId: getStringValue(payload.precedesMessageId) ?? getStringValue(attributes?.precedesMessageId), gapText: getStringValue(payload.gapText) ?? getStringValue(attributes?.gapText), gapMs: typeof payload.gapMs === "number" ? payload.gapMs : typeof attributes?.gapMs === "number" ? attributes.gapMs : void 0, timestamp: getStringValue(payload.timestamp) ?? getStringValue(attributes?.timestamp), goalMaxTurns: typeof payload.goalMaxTurns === "number" ? payload.goalMaxTurns : typeof metadata?.goalMaxTurns === "number" ? metadata.goalMaxTurns : void 0, judgeModelId: getStringValue(payload.judgeModelId) ?? getStringValue(metadata?.judgeModelId), goalEvaluation: getRecordValue(metadata?.goalEvaluation) }; } function toUserSignalMessage(payload) { const id = getStringValue(payload.id); const rawContents = payload.contents; if (!id || rawContents === void 0) return void 0; const signal = chunk2TATDSHU_cjs.createSignal({ id, type: "user", tagName: "user", contents: rawContents, attributes: getRecordValue(payload.attributes), createdAt: getStringValue(payload.createdAt) }); const content = signalContentsToHarnessContent(signal.contents); if (content.length === 0) return void 0; return { id: signal.id, role: "user", content, createdAt: signal.createdAt, attributes: signal.attributes }; } function signalContentsToText(contents) { if (typeof contents === "string") return contents; if (!Array.isArray(contents)) return ""; return contents.filter((part) => getRecordValue(part)?.type === "text").map((part) => part.text).join("\n"); } function toStateSignalContent(payload) { const stateMetadata = getRecordValue(getRecordValue(payload.metadata)?.state); const stateId = getStringValue(stateMetadata?.id) ?? getStringValue(payload.tagName) ?? "state"; return { type: "state_signal", id: getStringValue(payload.id), stateId, mode: stateMetadata?.mode === "delta" ? "delta" : "snapshot", cacheKey: getStringValue(stateMetadata?.cacheKey), version: typeof stateMetadata?.version === "number" ? stateMetadata.version : void 0, message: signalContentsToText(payload.contents) }; } function toNotificationSummaryContent(payload) { const metadataSummary = getRecordValue(getRecordValue(payload.metadata)?.notificationSummary); const bySource = getRecordValue(metadataSummary?.bySource) ?? {}; const byPriority = getRecordValue(metadataSummary?.byPriority) ?? {}; const notificationIds = Array.isArray(metadataSummary?.notificationIds) ? metadataSummary.notificationIds.filter((id) => typeof id === "string") : []; const pending = typeof metadataSummary?.pending === "number" ? metadataSummary.pending : void 0; return { type: "notification_summary", id: getStringValue(payload.id), message: signalContentsToText(payload.contents), pending: pending ?? notificationIds.length, bySource: Object.fromEntries( Object.entries(bySource).filter((entry) => typeof entry[1] === "number") ), byPriority: Object.fromEntries( Object.entries(byPriority).filter((entry) => typeof entry[1] === "number") ), notificationIds }; } function toReactiveSignalContent(payload) { const tagName = getStringValue(payload.tagName); if (!tagName) return void 0; return { type: "reactive_signal", id: getStringValue(payload.id), tagName, message: signalContentsToText(payload.contents), attributes: getRecordValue(payload.attributes), metadata: getRecordValue(payload.metadata) }; } function toNotificationContent(payload) { const attributes = getRecordValue(payload.attributes) ?? {}; const metadata = getRecordValue(payload.metadata) ?? {}; const notificationMetadata = getRecordValue(metadata.notification); const message = signalContentsToText(payload.contents); if (!message) return void 0; return { type: "notification", id: getStringValue(payload.id), notificationId: getStringValue(attributes.id) ?? getStringValue(notificationMetadata?.recordId), message, source: getStringValue(attributes.source) ?? getStringValue(notificationMetadata?.source), kind: getStringValue(attributes.kind) ?? getStringValue(attributes.type) ?? getStringValue(notificationMetadata?.kind), priority: getStringValue(attributes.priority) ?? getStringValue(notificationMetadata?.priority), status: getStringValue(attributes.status) ?? getStringValue(notificationMetadata?.status), attributes, metadata }; } var Harness = class { id; config; listeners = []; workspace = void 0; workspaceFn = void 0; workspaceInitialized = false; browser = void 0; browserFn = void 0; heartbeatTimers = /* @__PURE__ */ new Map(); #session; availableModelsCache = null; availableModelsCacheTime = 0; #instructions; #internalMastra = void 0; #legacyAgentMode = {}; constructor(config) { validateModes(config.modes); this.id = config.id; this.config = config; this.#instructions = config.instructions; this.#session = new Session({ resourceId: config.resourceId ?? config.id, state: { initialState: config.initialState, stateSchema: config.stateSchema, emit: (event) => this.emit(event) } }); const defaultMode = config.defaultModeId ? config.modes.find((mode) => mode.id === config.defaultModeId) : config.modes.find((mode) => mode.default || mode.metadata?.default === true) ?? config.modes[0]; if (!defaultMode) { throw new Error( config.defaultModeId ? `Default mode not found: ${config.defaultModeId}` : "Harness requires at least one agent mode" ); } this.#session.mode.set({ modeId: defaultMode.id }); this.#session.setStore({ get: (key) => this.#session.thread.getSetting({ key }), set: (key, value) => this.#session.thread.setSetting({ key, value }) }); this.#session.setCategoryResolver((toolName) => this.getToolCategory({ toolName })); this.#session.setSubagentNameResolver((agentType) => this.getSubagentDisplayName(agentType)); this.#session.mode.setResolver((modeId) => this.config.modes.find((m) => m.id === modeId) ?? null); this.#session.thread.connect(this.createThreadDataStore()); if (config.workspace instanceof chunkD37QL5LB_cjs.Workspace) { this.workspace = config.workspace; } else if (typeof config.workspace === "function") { this.workspaceFn = config.workspace; } if (config.browser && typeof config.browser !== "function") { this.browser = config.browser; } else if (typeof config.browser === "function") { this.browserFn = config.browser; } const initialModelId = config.initialState?.currentModelId; if (initialModelId) { this.#session.model.set({ modelId: initialModelId }); } else if (defaultMode.defaultModelId) { this.#session.model.set({ modelId: defaultMode.defaultModelId }); } } // =========================================================================== // Accessors // =========================================================================== /** * Access the internal Mastra instance. * Available after `init()` when storage is configured. * Useful for scorer registration, observability access, and eval tooling. */ getMastra() { return this.#internalMastra; } /** * The current harness session. Owns per-session runtime state such as * session-scoped permission grants. Prefer `harness.session.*` over the * (removed) re-exposed grant helpers on the Harness. */ get session() { return this.#session; } /** * Sets or updates the harness-level browser and propagates it to mode agents. */ setBrowser(browser) { this.browser = browser; this.browserFn = void 0; const agents = /* @__PURE__ */ new Set(); if (this.config.agent) { agents.add(this.config.agent); } for (const mode of this.config.modes) { if (mode.agent || !this.config.agent) { agents.add(this.getAgentForMode(mode)); } } for (const agent of agents) { agent.setBrowser(browser); } } // =========================================================================== // Initialization // =========================================================================== /** * Initialize the harness — loads storage and workspace. * Must be called before using the harness. */ async init() { if (this.config.storage) { const enabledGateways = this.config.gateways?.filter((gateway) => gateway.shouldEnable?.() ?? true); const gateways = enabledGateways?.length ? Object.fromEntries(enabledGateways.map((gateway) => [gateway.id, gateway])) : void 0; this.#internalMastra = new chunkD37QL5LB_cjs.Mastra({ logger: false, storage: this.config.storage, ...this.config.pubsub ? { pubsub: this.config.pubsub } : {}, ...this.config.observability ? { observability: this.config.observability } : {}, ...gateways ? { gateways } : {} }); await this.#internalMastra.getStorage().init(); } if (this.config.workspace && !this.workspaceInitialized && !this.workspaceFn) { try { if (!this.workspace) { this.workspace = new chunkD37QL5LB_cjs.Workspace(this.config.workspace); } this.emit({ type: "workspace_status_changed", status: "initializing" }); await this.workspace.init(); this.workspaceInitialized = true; this.emit({ type: "workspace_status_changed", status: "ready" }); this.emit({ type: "workspace_ready", workspaceId: this.workspace.id, workspaceName: this.workspace.name }); } catch (error) { const err = chunkXSOONORA_cjs.getErrorFromUnknown(error); this.workspace = void 0; this.workspaceInitialized = false; this.emit({ type: "workspace_status_changed", status: "error", error: err }); this.emit({ type: "workspace_error", error: err }); } } const agents = /* @__PURE__ */ new Set(); if (this.config.agent) { agents.add(this.config.agent); } for (const mode of this.config.modes) { if (mode.agent || !this.config.agent) { agents.add(this.getAgentForMode(mode)); } } for (const agent of agents) { this.propagateRuntimeServicesToAgent(agent); } this.startHeartbeats(); } /** * Select the most recent thread, or create one if none exist. */ async selectOrCreateThread() { const threads = await this.#session.thread.list(); if (threads.length === 0) { return await this.createThread(); } const sortedThreads = [...threads].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); const mostRecent = sortedThreads[0]; await this.config.threadLock?.acquire(mostRecent.id); this.#session.thread.set({ threadId: mostRecent.id }); await this.loadThreadMetadata(); await this.ensureCurrentAgentThreadSubscription(); return mostRecent; } async getMemoryStorage() { if (!this.config.storage) { throw new Error("Storage is not configured on this Harness"); } const memoryStorage = await this.config.storage.getStore("memory"); if (!memoryStorage) { throw new Error("Storage does not have a memory domain configured"); } return memoryStorage; } /** * The shared-host storage gateway the Session's thread domain reads/writes * through. The Session owns the thread-domain logic; this adapter just maps * raw storage rows to Harness types — it does not call back into Session. */ createThreadDataStore() { return { listThreads: ({ resourceId, includeForkedSubagents }) => this.queryThreads({ resourceId, includeForkedSubagents }), getById: ({ threadId }) => this.queryThreadById({ threadId }), listMessages: ({ threadId, limit }) => this.queryThreadMessages({ threadId, limit }), firstUserMessages: ({ threadIds }) => this.queryFirstUserMessages({ threadIds }), getMetadata: ({ threadId, key }) => this.readThreadMetadataValue({ threadId, key }), setMetadata: ({ threadId, key, value }) => this.writeThreadMetadataValue({ threadId, key, value }), deleteMetadata: ({ threadId, key }) => this.removeThreadMetadataValue({ threadId, key }) }; } async readThreadMetadataValue({ threadId, key }) { if (!this.config.storage) return void 0; try { const memoryStorage = await this.getMemoryStorage(); const thread = await memoryStorage.getThreadById({ threadId }); const metadata = thread?.metadata; return metadata?.[key]; } catch { return void 0; } } async writeThreadMetadataValue({ threadId, key, value }) { if (!this.config.storage) return; try { const memoryStorage = await this.getMemoryStorage(); const thread = await memoryStorage.getThreadById({ threadId }); if (thread) { await memoryStorage.saveThread({ thread: { ...thread, metadata: { ...thread.metadata, [key]: value }, updatedAt: /* @__PURE__ */ new Date() } }); } } catch { } } async removeThreadMetadataValue({ threadId, key }) { if (!this.config.storage) return; try { const memoryStorage = await this.getMemoryStorage(); const thread = await memoryStorage.getThreadById({ threadId }); if (thread && thread.metadata) { const metadata = { ...thread.metadata }; delete metadata[key]; await memoryStorage.saveThread({ thread: { ...thread, metadata: Object.keys(metadata).length > 0 ? metadata : void 0, updatedAt: /* @__PURE__ */ new Date() } }); } } catch { } } async queryThreadById({ threadId }) { if (!this.config.storage) return null; const memoryStorage = await this.getMemoryStorage(); const thread = await memoryStorage.getThreadById({ threadId }); if (!thread) return null; return { id: thread.id, resourceId: thread.resourceId, title: thread.title, createdAt: thread.createdAt, updatedAt: thread.updatedAt, metadata: thread.metadata }; } async queryThreads({ resourceId, includeForkedSubagents }) { if (!this.config.storage) return []; const memoryStorage = await this.getMemoryStorage(); const filter = resourceId === void 0 ? void 0 : { resourceId }; const result = await memoryStorage.listThreads({ filter, perPage: false }); const threads = includeForkedSubagents ? result.threads : result.threads.filter((thread) => { const metadata = thread.metadata; return metadata?.forkedSubagent !== true; }); return threads.map((thread) => ({ id: thread.id, resourceId: thread.resourceId, title: thread.title, createdAt: thread.createdAt, updatedAt: thread.updatedAt, metadata: thread.metadata })); } async queryThreadMessages({ threadId, limit }) { if (!this.config.storage) return []; const memoryStorage = await this.getMemoryStorage(); if (limit) { const result2 = await memoryStorage.listMessages({ threadId, perPage: limit, page: 0, orderBy: { field: "createdAt", direction: "DESC" } }); return result2.messages.map((msg) => this.convertToHarnessMessage(msg)).reverse(); } const result = await memoryStorage.listMessages({ threadId, perPage: false }); return result.messages.map((msg) => this.convertToHarnessMessage(msg)); } async queryFirstUserMessages({ threadIds }) { if (!this.config.storage || threadIds.length === 0) return /* @__PURE__ */ new Map(); const memoryStorage = await this.getMemoryStorage(); const result = await memoryStorage.listMessages({ threadId: threadIds, perPage: false, orderBy: { field: "createdAt", direction: "ASC" } }); const firstUserMessages = /* @__PURE__ */ new Map(); for (const message of result.messages) { if (message.role !== "user" || !message.threadId || firstUserMessages.has(message.threadId)) continue; firstUserMessages.set(message.threadId, this.convertToHarnessMessage(message)); if (firstUserMessages.size === threadIds.length) { break; } } return firstUserMessages; } // =========================================================================== // State Management // =========================================================================== /** * Get current harness state (read-only snapshot). * @deprecated Prefer `harness.session.state.get()`. */ getState() { return this.#session.state.get(); } /** * Update harness state. Validates against schema if provided. * Emits state_changed event. * @deprecated Prefer `harness.session.state.set(...)`. */ async setState(updates) { return this.#session.state.set(updates); } async updateState(updater) { return this.#session.state.update(updater); } // =========================================================================== // Mode Management // =========================================================================== listModes() { return this.config.modes; } /** * Switch to a different mode. * Aborts any in-progress generation and switches to the mode's default model. */ async switchMode({ modeId }) { const mode = this.config.modes.find((m) => m.id === modeId); if (!mode) { throw new Error(`Mode not found: ${modeId}`); } this.abort(); const previousModeId = this.#session.mode.get(); this.emit({ type: "mode_changed", modeId, previousModeId }); const { modelId } = await this.#session.mode.switch({ modeId, defaultModelId: mode.defaultModelId }); if (modelId) { this.emit({ type: "model_changed", modelId }); } } propagateRuntimeServicesToAgent(agent) { const alreadyHasMastra = !!agent.getMastraInstance(); const workspaceForAgents = this.workspaceFn ?? this.workspace; const browserForAgents = this.browserFn ?? this.browser; if (this.config.memory && !agent.hasOwnMemory()) { agent.__setMemory(this.config.memory); } if (workspaceForAgents && !agent.hasOwnWorkspace()) { agent.__setWorkspace(workspaceForAgents); } if (browserForAgents && !agent.hasOwnBrowser()) { agent.setBrowser(browserForAgents); } if (this.config.pubsub && !agent.hasOwnPubSub()) { agent.__setPubSub(this.config.pubsub); } if (this.#internalMastra && !alreadyHasMastra) { this.#internalMastra.addAgent(agent); } return agent; } getAgentForMode(mode) { if (mode.agent) { if (!this.#legacyAgentMode[mode.id]) { this.#legacyAgentMode[mode.id] = mode.agent; } return this.#legacyAgentMode[mode.id]; } if (this.config.agent) { return this.config.agent; } if (!this.#legacyAgentMode[mode.id]) { if (!mode.defaultModelId) { throw new Error(`Mode ${mode.id} requires a defaultModelId when no backing agent is configured`); } const instructions = [this.#instructions ?? "", mode.instructions].filter(Boolean).join("\n"); const modeTools = { ...mode.tools, ...mode.additionalTools }; const model = this.config.resolveModel ? this.config.resolveModel(mode.defaultModelId) : mode.defaultModelId; this.#legacyAgentMode[mode.id] = new chunkD37QL5LB_cjs.Agent({ id: `${this.id}-agent`, name: `Harness ${this.id} agent`, model, instructions, tools: modeTools }); } return this.#legacyAgentMode[mode.id]; } /** * Resolve the combined instructions for the current mode: harness-level * instructions + mode-specific instructions. Passed at call time via * `buildAgentMessageStreamOptions` so the agent's own instructions are * never mutated. */ resolveCurrentModeInstructions() { const mode = this.#session.mode.resolve(); const combined = [this.#instructions ?? "", mode?.instructions ?? ""].filter(Boolean).join("\n"); return combined || void 0; } /** * Convert AgentInstructions (string | string[] | system message objects) to * a plain string for combining with mode instructions. */ instructionsToString(instructions) { if (typeof instructions === "string") return instructions; if (Array.isArray(instructions)) { return instructions.map((msg) => typeof msg === "string" ? msg : typeof msg.content === "string" ? msg.content : "").filter(Boolean).join("\n\n"); } return typeof instructions.content === "string" ? instructions.content : ""; } /** * Get the agent for the current mode. */ /** * Resolve the Agent backing the current mode, with runtime services (storage, * pubsub, telemetry) propagated. Public so consumers like MastraCode's * GoalManager can drive the agent's native objective methods * (`setObjective`/`getObjective`/`clearObjective`/`updateObjectiveOptions`), * which read/write the durable `threadState` `'goal'` slot. */ getCurrentAgent() { const mode = this.#session.mode.resolve(); return this.propagateRuntimeServicesToAgent(this.getAgentForMode(mode)); } /** * Get a short display name from the current model ID. */ getModelName() { const modelId = this.#session.model.get(); if (!modelId || modelId === "unknown") return modelId || "unknown"; const parts = modelId.split("/"); return parts[parts.length - 1] || modelId; } /** * Get the full model ID (e.g., "anthropic/claude-sonnet-4"). */ getFullModelId() { return this.#session.model.get(); } /** * Switch to a different model at runtime. */ async switchModel({ modelId, scope = "thread", modeId }) { const targetModeId = modeId ?? this.#session.mode.get(); if (targetModeId === this.#session.mode.get()) { this.#session.model.set({ modelId }); } if (scope === "thread") { await this.#session.model.saveForMode({ modeId: targetModeId, modelId }); } try { await Promise.resolve(this.config.modelUseCountTracker?.(modelId)); } catch (error) { console.error("Failed to track model usage count", error); } this.emit({ type: "model_changed", modelId, scope, modeId: targetModeId }); } /** * Check if the current model's provider has authentication configured. * Uses app-provided catalog/auth hooks; Harness does not resolve gateway auth itself. */ async getCurrentModelAuthStatus() { const modelId = this.#session.model.get(); if (!modelId) return { hasAuth: true }; try { const availableModels = await this.listAvailableModels(); const currentModel = availableModels.find((model) => model.id === modelId); if (currentModel) { return { hasAuth: currentModel.hasApiKey, apiKeyEnvVar: currentModel.hasApiKey ? void 0 : currentModel.apiKeyEnvVar }; } } catch { } const provider = modelId.split("/", 1)[0]; if (this.config.modelAuthChecker && provider) { const result = this.config.modelAuthChecker(provider); if (result !== void 0) return { hasAuth: result }; } return { hasAuth: true }; } /** * Get available models from the app-provided catalog hook with use counts applied. */ async listAvailableModels() { const now = Date.now(); if (this.availableModelsCache && now - this.availableModelsCacheTime < 1e4) { return this.availableModelsCache; } const useCounts = this.config.modelUseCountProvider?.() ?? {}; const modelsById = /* @__PURE__ */ new Map(); const upsertModel = (model) => { if (!model.id || !model.provider || !model.modelName) return; modelsById.set(model.id, { ...model, useCount: useCounts[model.id] ?? 0 }); }; if (this.config.customModelCatalogProvider) { try { const customModels = await Promise.resolve(this.config.customModelCatalogProvider()); for (const model of customModels) { upsertModel({ id: model.id, provider: model.provider, modelName: model.modelName, hasApiKey: model.hasApiKey, apiKeyEnvVar: model.apiKeyEnvVar }); } } catch (error) { console.warn("Failed to load available models:", error); } } const result = [...modelsById.values()]; this.availableModelsCache = result; this.availableModelsCacheTime = Date.now(); return result; } invalidateAvailableModelsCache() { this.availableModelsCache = null; this.availableModelsCacheTime = 0; } // =========================================================================== // Thread Management // =========================================================================== async getResolvedMemory() { if (!this.config.memory) return null; return this.resolveMemory(); } /** * Point the session at a different memory resourceId. The resourceId itself * lives on the session (`session.identity`); the Harness orchestrates the * surrounding teardown — dropping the current thread subscription and clearing * the active thread — since those are Harness-owned. */ setResourceId({ resourceId }) { this.cleanupAgentThreadSubscription(); this.#session.identity.setResourceId({ resourceId }); this.#session.thread.clear(); } async getKnownResourceIds() { const threads = await this.#session.thread.list({ allResources: true }); const ids = new Set(threads.map((t) => t.resourceId)); return [...ids].sort(); } async createThread({ title } = {}) { this.cleanupAgentThreadSubscription(); const now = /* @__PURE__ */ new Date(); const thread = { id: this.generateId(), resourceId: this.#session.identity.getResourceId(), title: title || "", createdAt: now, updatedAt: now }; const currentStateModel = this.#session.model.get(); const currentMode = this.#session.mode.resolve(); const modelId = currentStateModel || currentMode.defaultModelId; const metadata = {}; if (modelId) { metadata.currentModelId = modelId; metadata[`modeModelId_${this.#session.mode.get()}`] = modelId; } const projectPath = this.#session.state.get().projectPath; if (projectPath) { metadata.projectPath = projectPath; } const oldThreadId = this.#session.thread.getId(); if (this.config.threadLock) { try { await this.config.threadLock.acquire(thread.id); } catch (err) { if (oldThreadId) { try { await this.config.threadLock.acquire(oldThreadId); } catch { } } throw err; } if (oldThreadId) { await this.config.threadLock.release(oldThreadId); } } if (this.config.storage) { const memoryStorage = await this.getMemoryStorage(); try { await memoryStorage.saveThread({ thread: { id: thread.id, resourceId: thread.resourceId, title: thread.title, createdAt: thread.createdAt, updatedAt: thread.updatedAt, metadata: Object.keys(metadata).length > 0 ? metadata : void 0 } }); } catch (err) { let reacquired = false; if (this.config.threadLock) { try { await this.config.threadLock.release(thread.id); } catch { } if (oldThreadId) { try { await this.config.threadLock.acquire(oldThreadId); reacquired = true; } catch { } } } if (reacquired && oldThreadId) { this.#session.thread.set({ threadId: oldThreadId }); } else { this.#session.thread.clear(); } throw err; } } this.#session.thread.set({ threadId: thread.id }); if (modelId && !currentStateModel) { this.#session.model.set({ modelId }); } this.#session.resetTokenUsage(); this.emit({ type: "thread_created", thread }); await this.ensureCurrentAgentThreadSubscription(); return thread; } /** * Returns a memory accessor with thread and message management methods. */ get memory() { return { createThread: this.createThread.bind(this), switchThread: this.switchThread.bind(this), listThreads: (options) => this.#session.thread.list(options), renameThread: this.renameThread.bind(this), deleteThread: this.deleteThread.bind(this) }; } async deleteThread({ threadId }) { if (!this.config.storage) return; const memoryStorage = await this.getMemoryStorage(); const thread = await memoryStorage.getThreadById({ threadId }); if (!thread) { throw new Error(`Thread not found: ${threadId}`); } const isDeletingCurrentThread = this.#session.thread.getId() === threadId; await memoryStorage.deleteThread({ threadId }); if (isDeletingCurrentThread) { try { await this.config.threadLock?.release(threadId); } catch { } this.cleanupAgentThreadSubscription(); this.#session.thread.clear(); this.#session.resetTokenUsage(); } this.emit({ type: "thread_deleted", threadId }); } async renameThread({ title }) { const threadId = this.#session.thread.getId(); if (!threadId || !this.config.storage) return; const memoryStorage = await this.getMemoryStorage(); const thread = await memoryStorage.getThreadById({ threadId }); if (thread) { await memoryStorage.saveThread({ thread: { ...thread, title, updatedAt: /* @__PURE__ */ new Date() } }); } } async cloneThread({ sourceThreadId, title, resourceId } = {}) { const sourceId = sourceThreadId ?? this.#session.thread.getId(); if (!sourceId) { throw new Error("No source thread to clone"); } if (!this.config.memory) { throw new Error("Memory is not configured on this Harness"); } const memory = await this.resolveMemory(); const result = await memory.cloneThread({ sourceThreadId: sourceId, resourceId: resourceId ?? this.#session.identity.getResourceId(), title }); const clonedThread = { id: result.thread.id, resourceId: result.thread.resourceId, title: result.thread.title ?? "Cloned Thread", createdAt: result.thread.createdAt, updatedAt: result.thread.updatedAt, metadata: result.thread.metadata }; const oldThreadId = this.#session.thread.getId(); if (this.config.threadLock) { try { await this.config.threadLock.acquire(clonedThread.id); } catch (err) { if (oldThreadId) { try { await this.config.threadLock.acquire(oldThreadId); } catch { } } throw err; } if (oldThreadId) { await this.config.threadLock.release(oldThreadId); } } this.cleanupAgentThreadSubscription(); this.#session.thread.set({ threadId: clonedThread.id }); await this.loadThreadMetadata(); this.#session.resetTokenUsage(); this.emit({ type: "thread_created", thread: clonedThread }); await this.ensureCurrentAgentThreadSubscription(); return clonedThread; } async switchThread({ threadId }) { this.abort(); this.cleanupAgentThreadSubscription(); await this.config.threadLock?.acquire(threadId); const previousThreadId = this.#session.thread.getId(); if (previousThreadId) { await this.config.threadLock?.release(previousThreadId); } if (this.config.storage) { const memoryStorage = await this.getMemoryStorage(); const thread = await memoryStorage.getThreadById({ threadId }); if (!thread) { throw new Error(`Thread not found: ${threadId}`); } } this.#session.thread.set({ threadId }); await this.loadThreadMetadata(); this.emit({ type: "thread_changed", threadId, previousThreadId }); await this.ensureCurrentAgentThreadSubscription(); } async loadThreadMetadata() { const threadId = this.#session.thread.getId(); if (!threadId || !this.config.storage) { this.#session.resetTokenUsage(); return; } try { const memoryStorage = await this.getMemoryStorage(); const thread = await memoryStorage.getThreadById({ threadId }); const savedUsage = thread?.metadata?.tokenUsage; if (savedUsage) { this.#session.setTokenUsage({ ...createEmptyTokenUsage(), ...savedUsage, promptTokens: savedUsage.promptTokens ?? 0, completionTokens: savedUsage.completionTokens ?? 0, totalTokens: savedUsage.totalTokens ?? 0, cachedInputTokens: savedUsage.cachedInputTokens ?? 0, cacheCreationInputTokens: savedUsage.cacheCreationInputTokens ?? 0 }); } else { this.#session.resetTokenUsage(); } const meta = thread?.metadata; const updates = {}; let previousModeIdForEmit; if (meta?.currentModeId) { const savedModeId = meta.currentModeId; const modeExists = this.config.modes.some((m) => m.id === savedModeId); if (modeExists && savedModeId !== this.#session.mode.get()) { previousModeIdForEmit = this.#session.mode.get(); this.#session.mode.set({ modeId: savedModeId }); } } const currentModeId = this.#session.mode.get(); const modeModelKey2 = `modeModelId_${currentModeId}`; if (meta?.[modeModelKey2]) { this.#session.model.set({ modelId: meta[modeModelKey2] }); } else { const currentMode = this.config.modes.find((m) => m.id === currentModeId); if (currentMode?.defaultModelId) { this.#session.model.set({ modelId: currentMode.defaultModelId }); } else if (meta?.currentModelId) { this.#session.model.set({ modelId: meta.currentModelId }); } } if (previousModeIdForEmit !== void 0) { this.emit({ type: "mode_changed", modeId: this.#session.mode.get(), previousModeId: previousModeIdForEmit }); } if (meta?.observerModelId) { updates.observerModelId = meta.observerModelId; } if (meta?.reflectorModelId) { updates.reflectorModelId = meta.reflectorModelId; } const hasObservationThreshold = typeof meta?.observationThreshold === "number"; const hasReflectionThreshold = typeof meta?.reflectionThreshold === "number"; if (hasObservationThreshold) { updates.observationThreshold = meta.observationThreshold; } if (hasReflectionThreshold) { updates.reflectionThreshold = meta.reflectionThreshold; } if (Object.keys(updates).length > 0) { await this.setState(updates); } if (!hasObservationThreshold) { const observationThreshold = this.getObservationThreshold(); if (observationThreshold !== void 0) { await this.#session.thread.setSetting({ key: "observationThreshold", value: observationThreshold }); } } if (!hasReflectionThreshold) { const reflectionThreshold = this.getReflectionThreshold(); if (reflectionThreshold !== void 0) { await this.#session.thread.setSetting({ key: "reflectionThreshold", value: reflectionThreshold }); } } } catch { this.#session.resetTokenUsage(); } } // =========================================================================== // Observational Memory // =========================================================================== /** * Load observational memory progress for the current thread. * Reads the OM record and recent messages to reconstruct status, * then emits an `om_status` event for the UI. */ async loadOMProgress() { const threadId = this.#session.thread.getId(); if (!threadId) return; try { const memoryStorage = await this.getMemoryStorage(); const record = await memoryStorage.getObservationalMemory(threadId, this.#session.identity.getResourceId()); if (!record) return; const config = record.config; const getThreshold = (val, fallback) => { if (!val) return fallback; if (typeof val === "number") return val; return val.max; }; let observationThreshold = getThreshold(config?.observationThreshold, 3e4); let reflectionThreshold = getThreshold(config?.reflectionThreshold, 4e4); let messageTokens = record.pendingMessageTokens ?? 0; let observationTokens = record.observationTokenCount ?? 0; let bufferedObs = { status: "idle", chunks: 0, messageTokens: 0, projectedMessageRemoval: 0, observationTokens: 0 }; let bufferedRef = { status: "idle", inputObservationTokens: 0, observationTokens: 0 }; let generationCount = 0; let stepNumber = 0; const messagesResult = await memoryStorage.listMessages({ threadId, perPage: 70, page: 0, orderBy: { field: "createdAt", direction: "DESC" } }); const messages = messagesResult.messages; let foundStatus = false; for (const msg of messages) { if (msg.role !== "assistant") continue; const content = msg.content; if (typeof content === "string" || !content?.parts) continue; for (let i = content.parts.length - 1; i >= 0; i--) { const part = content.parts[i]; if (part.type === "data-om-status" && part.data?.windows) { const w = part.data.windows; messageTokens = w.active?.messages?.tokens ?? messageTokens; observationTokens = w.active?.observations?.tokens ?? observationTokens; const msgThresh = w.active?.messages?.threshold; const obsThresh = w.active?.observations?.threshold; if (msgThresh) observationThreshold = msgThresh; if (obsThresh) reflectionThreshold = obsThresh; const bo = w.buffered?.observations; if (bo) { bufferedObs = { status: bo.status ?? "idle", chunks: bo.chunks ?? 0, messageTokens: bo.messageTokens ?? 0, projectedMessageRemoval: bo.projectedMessageRemoval ?? 0, observationTokens: bo.observationTokens ?? 0 }; } const br = w.buffered?.reflection; if (br) { bufferedRef = { status: br.status ?? "idle", inputObservationTokens: br.inputObservationTokens ?? 0, observationTokens: br.observationTokens ?? 0 }; } generationCount = part.data.generationCount ?? 0; stepNumber = part.data.stepNumber ?? 0; foundStatus = true; break; } } if (foundStatus) break; } this.emit({ type: "om_status", windows: { active: { messages: { tokens: messageTokens, threshold: observationThreshold }, observations: { tokens: observationTokens, threshold: reflectionThreshold } }, buffered: { observations: bufferedObs, reflection: bufferedRef } }, recordId: record.id ?? "", threadId, stepNumber, generationCount }); } catch { } } async getObservationalMemoryRecord() { if (!this.#session.thread.getId()) return null; try { const memoryStorage = await this.getMemoryStorage(); return await memoryStorage.getObservationalMemory( this.#session.thread.getId(), this.#session.identity.getResourceId() ); } catch { return null; } } /** * Returns the observer model ID from state, falling back to omConfig defaults. */ getObserverModelId() { return this.#session.state.get().observerModelId ?? this.config.omConfig?.defaultObserverModelId; } /** * Returns the reflector model ID from state, falling back to omConfig defaults. */ getReflectorModelId() { return this.#session.state.get().reflectorModelId ?? this.config.omConfig?.defaultReflectorModelId; } /** * Returns the observation threshold from state, falling back to omConfig defaults. */ getObservationThreshold() { return this.#session.state.get().observationThreshold ?? this.config.omConfig?.defaultObservationThreshold; } /** * Returns the reflection threshold from state, falling back to omConfig defaults. */ getReflectionThreshold() { return this.#session.state.get().reflectionThreshold ?? this.config.omConfig?.defaultReflectionThreshold; } /** * Resolves the observer model ID to a language model instance via the configured resolver. */ getResolvedObserverModel() { const modelId = this.getObserverModelId(); if (!modelId || !this.config.resolveModel) return void 0; return this.config.resolveModel(modelId); } /** * Resolves the reflector model ID to a language model instance via the configured resolver. */ getResolvedReflectorModel() { const modelId = this.getReflectorModelId(); if (!modelId || !this.config.resolveModel) return void 0; return this.config.resolveModel(modelId); } /** * Switch the Observer model. */ async switchObserverModel({ modelId }) { void this.setState({ observerModelId: modelId }); await this.#session.thread.setSetting({ key: "observerModelId", value: modelId }); this.emit({ type: "om_model_changed", role: "observer", modelId }); } /** * Switch the Reflector model. */ async switchReflectorModel({ modelId }) { void this.setState({ reflectorModelId: modelId }); await this.#session.thread.setSetting({ key: "reflectorModelId", value: modelId }); this.emit({ type: "om_model_changed", role: "reflector", modelId }); } // =========================================================================== // Subagent Model Management // =========================================================================== getSubagentModelId({ agentType } = {}) { const state = this.#session.state.get(); if (agentType) { const perType = state[`subagentModelId_${agentType}`]; if (typeof perType === "string") return perType; } const global = state.subagentModelId; return typeof global === "string" ? global : null; } async setSubagentModelId({ modelId, agentType }) { const key = agentType ? `subagentModelId_${agentType}` : "subagentModelId"; void this.setState({ [key]: modelId }); await this.#session.thread.setSetting({ key, value: modelId }); this.emit({ type: "subagent_model_changed", modelId, scope: "thread", agentType }); } // =========================================================================== // Permissions // =========================================================================== getToolCategory({ toolName }) { return this.config.toolCategoryResolver?.(toolName) ?? null; } setPermissionForCategory({ category, policy }) { const rules = this.getPermissionRules(); rules.categories[category] = policy; void this.setState({ permissionRules: rules }); } setPermissionForTool({ toolName, policy }) { const rules = this.getPermissionRules(); rules.tools[toolName] = policy; void this.setState({ permissionRules: rules }); } getPermissionRules() { const state = this.#session.state.get(); const rules = state.permissionRules; return rules ?? { categories: {}, tools: {} }; } /** * Resolve whether a tool call should be auto-approved, denied, or asked. * Resolution chain: per-tool deny → yolo → per-tool policy → session tool grant → * session category grant → category policy → "ask" */ resolveToolApproval(toolName) { const state = this.#session.state.get(); const rules = this.getPermissionRules(); const toolPolicy = rules.tools[toolName]; if (toolPolicy === "deny") return "deny"; if (state.yolo === true) return "allow"; if (toolPolicy) return toolPolicy; if (this.#session.hasToolGrant(toolName)) return "allow"; const category = this.getToolCategory({ toolName }); if (category) { if (this.#session.hasCategoryGrant(category)) return "allow"; const categoryPolicy = rules.categories[category]; if (categoryPolicy) return categoryPolicy; } return "ask"; } // =========================================================================== // Message Handling // =========================================================================== cleanupAgentThreadSubscription() { this.#session.stream.cleanup(); this.#session.run.reset(); } async ensureAgentThreadSubscription(agent, threadId) { const key = SessionStream.keyFor({ agent, resourceId: this.#session.identity.getResourceId(), threadId }); if (this.#session.stream.matches({ key })) return; this.cleanupAgentThreadSubscription(); const subscription = await agent.subscribeToThread({ resourceId: this.#session.identity.getResourceId(), threadId }); this.#session.stream.attach({ subscription, key }); void this.processSubscribedThreadStream(subscription); } async ensureCurrentAgentThreadSubscription() { const threadId = this.#session.thread.getId(); if (!threadId) return; await this.ensureAgentThreadSubscription(this.getCurrentAgent(), threadId); } createMessageInput({ content, files }) { if (!files?.length) return content; const fileParts = files.map((f) => { const isText = f.mediaType.startsWith("text/") || f.mediaType === "application/json"; if (isText) { let textContent = f.data; const base64Match = f.data.match(/^data:[^;]*;base64,(.*)$/); if (base64Match) { try { textContent = Buffer.from(base64Match[1], "base64").toString("utf-8"); } catch { } } const label = f.filename ? `[File: ${f.filename}]` : "[Attached file]"; const maxBacktickRun = Math.max(0, ...Array.from(textContent.matchAll(/`+/g), (match) => match[0].length)); const fence = "`".repeat(Math.max(3, maxBacktickRun + 1)); return { type: "text", text: `${label} ${fence} ${textContent} ${fence}` }; } return { type: "file", data: f.data, mediaType: f.mediaType, ...f.filename ? { filename: f.filename } : {} }; }); return [{ type: "text", text: content }, ...fileParts]; } async buildAgentMessageStreamOptions({ requestContext: requestContextInput, tracingContext, tracingOptions }) { if (!this.#session.thread.getId()) { throw new Error("Cannot build stream options without a current thread"); } this.#session.run.clearAbortRequested(); const requestContext = await this.buildRequestContext(requestContextInput); let callTimeInstructions; if (this.config.agent) { const modeInstructions = this.resolveCurrentModeInstructions(); if (modeInstructions) { const agent = this.getCurrentAgent(); const agentInstructions = await agent.getInstructions({ requestContext }); const agentStr = this.instructionsToString(agentInstructions); callTimeInstructions = [agentStr, modeInstructions].filter(Boolean).join("\n") || void 0; } } const streamOptions = { ...this.buildSharedRunOptions(), memory: { thread: this.#session.thread.getId(), resource: this.#session.identity.getResourceId() }, abortSignal: this.#session.run.ensureAbortController().signal, requestContext, ...tracingContext && { tracingContext }, ...tracingOptions && { tracingOptions }, ...callTimeInstructions && { instructions: callTimeInstructions } }; streamOptions.toolsets = await this.buildToolsets(requestContext); return streamOptions; } /** * Options that every harness-driven agent run must carry — the initial stream * AND every `resumeStream`. Centralized so the two paths can't drift: a * missing `maxSteps` on resume silently caps the resumed run at the agent's * small default and ends it mid-task (see {@link HARNESS_MAX_STEPS}). */ buildSharedRunOptions() { const isYolo = this.#session.state.get().yolo === true; const shared = { maxSteps: HARNESS_MAX_STEPS, savePerStep: false, requireToolApproval: !isYolo, modelSettings: { temperature: 1 } }; const fableFallback = buildFableFallbackProviderOptions(this.#session.model.get()); if (fableFallback) { shared.providerOptions = { anthropic: { ...fableFallback.anthropic } }; } return shared; } async drainFollowUpQueue(options) { if (this.#session.followUps.isEmpty()) return false; const next = this.#session.followUps.dequeue(); const threadId = this.#session.thread.getId(); try { if (this.#session.stream.isOpen() && threadId) { const agent = this.getCurrentAgent(); const streamOptions = await this.buildAgentMessageStreamOptions({ requestContext: next.requestContext, tracingContext: options?.tracingContext, tracingOptions: options?.tracingOptions }); const result = agent.queueMessage(this.createMessageInput({ content: next.content }), { resourceId: this.#session.identity.getResourceId(), threadId, ifIdle: { streamOptions } }); this.emit({ type: "follow_up_queued", count: this.#session.followUps.count(), runId: result.runId }); } else { this.emit({ type: "follow_up_queued", count: this.#session.followUps.count() }); await this.sendMessage({ content: next.content, requestContext: next.requestContext, tracingContext: options?.tracingContext, tracingOptions: options?.tracingOptions }); } return true; } catch (error) { this.#session.followUps.requeue(next); this.emit({ type: "follow_up_queued", count: this.#session.followUps.count() }); throw error; } } async finishSubscribedStreamRun({ suspended, error, aborted }) { const reason = error ? "error" : suspended ? "suspended" : aborted || this.#session.run.isAbortRequested() ? "aborted" : "complete"; this.emit({ type: "agent_end", reason }); this.#session.run.reset(); await this.drainFollowUpQueue(); } async handleSubscribedStreamError(error) { if (error instanceof Error && error.name === "AbortError") { this.emit({ type: "agent_end", reason: "aborted" }); } else { this.emit({ type: "error", error: chunkXSOONORA_cjs.getErrorFromUnknown(error) }); this.emit({ type: "agent_end", reason: "error" }); } this.#session.stream.detach(); this.#session.run.reset(); await this.drainFollowUpQueue(); } async processSubscribedThreadStream(subscription) { const requestContext = await this.buildRequestContext(); let currentRun; let lastFinishedRunId = null; try { for await (const chunk of subscription.stream) { if (!this.#session.stream.isCurrent({ subscription })) { subscription.unsubscribe(); break; } const chunkRunId = "runId" in chunk ? chunk.runId : null; if (lastFinishedRunId && chunkRunId === lastFinishedRunId) { continue; } if (!currentRun) { currentRun = this.createStreamState(); this.#session.run.nextOperation(); this.#session.run.ensureAbortController(); this.#session.run.setRunId({ runId: subscription.activeRunId() ?? ("runId" in chunk ? chunk.runId : null) }); this.#session.run.setTraceId({ traceId: null }); this.emit({ type: "agent_start" }); } if (chunk.type === "start") { continue; } try { const streamResult = await this.processStreamChunk(currentRun, chunk, requestContext); if (streamResult || chunk.type === "finish" || chunk.type === "error" || chunk.type === "abort" || chunk.type === "tool-call-suspended") { const finishedRunId = chunkRunId ?? this.#session.run.getRunId(); const suspended = chunk.type === "tool-call-suspended" || (streamResult ?? this.finishStreamState(currentRun)).suspended || void 0; const aborted = chunk.type === "abort"; let isError = chunk.type === "error"; if (currentRun.terminalError && !isError && !aborted && !this.#session.run.isAbortRequested() && !suspended) { isError = true; this.emit({ type: "error", error: new Error(currentRun.terminalError) }); } await this.finishSubscribedStreamRun({ suspended, error: isError, aborted }); lastFinishedRunId = finishedRunId; currentRun = void 0; } } catch (error) { await this.handleSubscribedStreamError(error); currentRun = void 0; } } } catch (error) { if (this.#session.stream.isCurrent({ subscription })) { await this.handleSubscribedStreamError(error); } } } /** * Send a signal to the current agent/thread. */ sendSignal(input) { const { tracingContext, tracingOptions, requestContext: requestContextInput } = "content" in input ? input : {}; const ifActive = "content" in input ? input.ifActive : void 0; const ifIdle = "content" in input ? input.ifIdle : void 0; const signal = chunk2TATDSHU_cjs.createSignal( "content" in input ? { type: "user", tagName: "user", contents: input.content } : input ); const accepted = Promise.resolve().then(async () => { if (!this.#session.thread.getId()) { const thread = await this.createThread(); this.#session.thread.set({ threadId: thread.id }); } const threadId = this.#session.thread.getId(); const agent = this.getCurrentAgent(); await this.ensureAgentThreadSubscription(agent, threadId); if (this.#session.run.getRunId() && this.#session.stream.activeRunId()) { const result2 = agent.sendSignal(signal, { resourceId: this.#session.identity.getResourceId(), threadId, ifActive, ifIdle }); return { accepted: result2.accepted, runId: result2.runId }; } const streamOptions = await this.buildAgentMessageStreamOptions({ requestContext: requestContextInput, tracingContext, tracingOptions }); const result = agent.sendSignal(signal, { resourceId: this.#session.identity.getResourceId(), threadId, ifActive, ifIdle: { ...ifIdle, streamOptions } }); return { accepted: result.accepted, runId: result.runId }; }); return { id: signal.id, type: signal.type, accepted }; } /** * Send a notification signal to the current agent/thread. */ async sendNotificationSignal(input, options = {}) { const { ifActive, ifIdle, requestContext: requestContextInput, tracingContext, tracingOptions } = options; if (!this.#session.thread.getId()) { const thread = await this.createThread(); this.#session.thread.set({ threadId: thread.id }); } const threadId = this.#session.thread.getId(); const agent = this.getCurrentAgent(); await this.ensureAgentThreadSubscription(agent, threadId); if (this.#session.run.getRunId() && this.#session.stream.activeRunId()) { return agent.sendNotificationSignal(input, { resourceId: this.#session.identity.getResourceId(), threadId, ifActive, ifIdle }); } const streamOptions = await this.buildAgentMessageStreamOptions({ requestContext: requestContextInput, tracingContext, tracingOptions }); return agent.sendNotificationSignal(input, { resourceId: this.#session.identity.getResourceId(), threadId, ifActive, ifIdle: { ...ifIdle, streamOptions } }); } /** * Send a message to the current agent. * Streams the response and emits events. */ async sendMessage({ content, files, tracingContext, tracingOptions, requestContext: requestContextInput }) { const messageInput = this.createMessageInput({ content, files }); const wasActive = this.#session.stream.isActive(); let emittedAgentEnd = false; const unsubscribeAgentEnd = wasActive ? void 0 : this.subscribe((event) => { if (event.type === "agent_end") emittedAgentEnd = true; }); const signal = this.sendSignal({ content: messageInput, tracingContext, tracingOptions, requestContext: requestContextInput }); await signal.accepted; if (!wasActive) { await new Promise((resolve) => setTimeout(resolve, 0)); await this.waitForCurrentThreadStreamIdle(); unsubscribeAgentEnd?.(); if (!emittedAgentEnd && !this.#session.suspensions.hasPending()) { this.emit({ type: "agent_end", reason: "complete" }); } } return; } async saveSystemReminderMessage({ message, reminderType, role = "user", metadata }) { const threadId = this.#session.thread.getId(); if (!threadId || !this.config.storage) return null; const memoryStorage = await this.getMemoryStorage(); const dbMessage = { id: crypto.randomUUID(), role, threadId, resourceId: this.#session.identity.getResourceId(), createdAt: /* @__PURE__ */ new Date(), content: { format: 2, parts: [], content: "", metadata: { systemReminder: { type: reminderType, message, ...metadata } } } }; const result = await memoryStorage.saveMessages({ messages: [dbMessage] }); const saved = result.messages[0] ?? dbMessage; return this.convertToHarnessMessage(saved); } convertToHarnessMessage(msg) { const content = []; const systemReminder = getRecordValue(msg.content.metadata?.systemReminder); if (systemReminder && typeof systemReminder.type === "string") { const reminder = toSystemReminderContent({ ...systemReminder, contents: typeof systemReminder.message === "string" ? systemReminder.message : "", reminderType: systemReminder.type }); if (reminder) { content.push(reminder); } return { id: msg.id, role: msg.role === "signal" ? "user" : msg.role, content, createdAt: msg.createdAt }; } if (msg.role === "signal") { const signal = chunk2TATDSHU_cjs.mastraDBMessageToSignal(msg); if (signal.type === "user") { const signalContent = signalContentsToHarnessContent(signal.contents); if (signalContent.length > 0) { return { id: msg.id, role: "user", content: signalContent, createdAt: msg.createdAt, attributes: signal.attributes }; } } if (signal.type === "state") { const stateSignal = toStateSignalContent({ id: signal.id, tagName: signal.tagName, contents: signal.contents, metadata: signal.metadata }); if (stateSignal) { content.push(stateSignal); } return { id: msg.id, role: "user", content, createdAt: msg.createdAt }; } if (signal.type === "reactive" && signal.tagName === "system-reminder") { const reminder = toSystemReminderContent({ type: signal.type, contents: signalContentsToText(signal.contents), attributes: signal.attributes ?? msg.content.metadata, metadata: signal.metadata }); if (reminder) { content.push(reminder); } return { id: msg.id, role: "user", content, createdAt: msg.createdAt }; } if (signal.type === "notification" && signal.tagName === "notification-summary") { const notificationSummary = toNotificationSummaryContent({ id: signal.id, contents: signal.contents, metadata: signal.metadata }); if (notificationSummary) { content.push(notificationSummary); } return { id: msg.id, role: "user", content, createdAt: msg.createdAt }; } if (signal.type === "notification" && signal.tagName === "notification") { const notification = toNotificationContent({ id: signal.id, contents: signal.contents, attributes: signal.attributes, metadata: signal.metadata }); if (notification) { content.push(notification); } return { id: msg.id, role: "user", content, createdAt: msg.createdAt }; } if (signal.type === "reactive") { const reactiveSignal = toReactiveSignalContent({ id: signal.id, tagName: signal.tagName, contents: signal.contents, attributes: signal.attributes, metadata: signal.metadata }); if (reactiveSignal) { content.push(reactiveSignal); } return { id: msg.id, role: "user", content, createdAt: msg.createdAt }; } } for (const part of msg.content.parts) { switch (part.type) { case "text": if (part.text) { content.push({ type: "text", text: part.text }); } break; case "reasoning": if (part.reasoning) { content.push({ type: "thinking", thinking: part.reasoning }); } break; case "tool-invocation": if (part.toolInvocation) { const inv = part.toolInvocation; content.push({ type: "tool_call", id: inv.toolCallId, name: inv.toolName, args: inv.args }); if (inv.state === "result" && inv.result !== void 0) { const partProviderMetadata = part.providerMetadata; content.push({ type: "tool_result", id: inv.toolCallId, name: inv.toolName, result: inv.result, isError: inv.isError ?? false, ...partProviderMetadata ? { providerMetadata: partProviderMetadata } : {} }); } } else if (part.toolCallId && part.toolName) { content.push({ type: "tool_call", id: part.toolCallId, name: part.toolName, args: part.args }); } break; case "tool-call": if (part.toolCallId && part.toolName) { content.push({ type: "tool_call", id: part.toolCallId, name: part.toolName, args: part.args }); } break; case "tool-result": if (part.toolCallId && part.toolName) { const resultProviderMetadata = part.providerMetadata; content.push({ type: "tool_result", id: part.toolCallId, name: part.toolName, result: part.result, isError: part.isError ?? false, ...resultProviderMetadata ? { providerMetadata: resultProviderMetadata } : {} }); } break; case "data-om-observation-start": { const data = part.data ?? {}; content.push({ type: "om_observation_start", tokensToObserve: data.tokensToObserve ?? 0, operationType: data.operationType ?? "observation" }); break; } case "data-om-observation-end": { const data = part.data ?? {}; content.push({ type: "om_observation_end", tokensObserved: data.tokensObserved ?? 0, observationTokens: data.observationTokens ?? 0, durationMs: data.durationMs ?? 0, operationType: data.operationType ?? "observation", observations: data.observations ?? void 0, currentTask: data.currentTask ?? void 0, suggestedResponse: data.suggestedResponse ?? void 0 }); break; } case "data-om-observation-failed": { const data = part.data ?? {}; content.push({ type: "om_observation_failed", error: data.error ?? "Unknown error", tokensAttempted: data.tokensAttempted ?? 0, operationType: data.operationType ?? "observation" }); break; } case "data-signal": { const data = part.data ?? {}; if (data.type === "state") { const stateSignal = toStateSignalContent(data); if (stateSignal) content.push(stateSignal); } else if (data.type === "reactive" && data.tagName === "system-reminder") { const reminder = toSystemReminderContent(data); if (reminder) content.push(reminder); } else if (data.type === "notification" && data.tagName === "notification-summary") { const notificationSummary = toNotificationSummaryContent(data); if (notificationSummary) content.push(notificationSummary); } else if (data.type === "notification" && data.tagName === "notification") { const notification = toNotificationContent(data); if (notification) content.push(notification); } else if (data.type === "reactive") { const reactiveSignal = toReactiveSignalContent(data); if (reactiveSignal) content.push(reactiveSignal); } break; } case "data-user-message": { const data = part.data ?? {}; const message = toUserSignalMessage(data); if (message) { content.push(...message.content); } break; } // Back-compat: persisted streams may still contain data-system-reminder parts case "data-system-reminder": { const data = part.data ?? {}; const reminder = toSystemReminderContent(data); if (reminder) { content.push(reminder); } break; } case "file": if (typeof part.data !== "string") { console.warn("[Harness] Skipping file part with non-string data:", typeof part.data); break; } content.push({ type: "file", data: part.data, mediaType: part.mediaType ?? part.mimeType ?? "application/octet-stream", ...part.filename ? { filename: part.filename } : {} }); break; case "image": { const imgData = typeof part.data === "string" ? part.data : typeof part.image === "string" ? part.image : ""; content.push({ type: "image", data: imgData, mimeType: part.mimeType ?? part.mediaType ?? "image/png" }); break; } case "data-om-thread-update": { const data = part.data ?? {}; if (data.newTitle) { content.push({ type: "om_thread_title_updated", threadId: data.threadId ?? "", oldTitle: data.oldTitle ?? void 0, newTitle: data.newTitle }); } break; } } } return { id: msg.id, role: msg.role === "signal" ? "user" : msg.role, content, createdAt: msg.createdAt }; } createEmptyAssistantMessage() { return { id: this.generateId(), role: "assistant", content: [], createdAt: /* @__PURE__ */ new Date() }; } hasCurrentMessageContent(state) { return state.currentMessage.content.length > 0 || Boolean(state.currentMessage.stopReason); } finishCurrentMessageAndRotate(state) { if (!this.hasCurrentMessageContent(state)) return; this.emit({ type: "message_end", message: state.currentMessage }); state.lastFinishedMessage = state.currentMessage; state.currentMessage = this.createEmptyAssistantMessage(); state.textContentById.clear(); state.thinkingContentById.clear(); } /** * Process a stream response (shared between sendMessage and tool approval). */ createStreamState() { return { currentMessage: this.createEmptyAssistantMessage(), isSuspended: false, textContentById: /* @__PURE__ */ new Map(), thinkingContentById: /* @__PURE__ */ new Map() }; } abortForOmFailure({ operationType, stage, error }) { this.emit({ type: "error", error: new Error(`Observational memory ${operationType} ${stage} failed: ${error}`) }); this.abort(); } async processStream(response, requestContextInput) { const state = this.createStreamState(); const requestContext = await this.buildRequestContext(requestContextInput); this.#session.run.nextOperation(); this.emit({ type: "agent_start" }); let result; let error = false; let aborted = false; for await (const chunk of response.fullStream) { result = await this.processStreamChunk(state, chunk, requestContext); if (chunk.type === "error") { error = true; } if (chunk.type === "abort") { aborted = true; } if (result || chunk.type === "finish" || chunk.type === "error" || chunk.type === "abort" || chunk.type === "tool-call-suspended" || this.#session.run.isAbortRequested()) { result ??= this.finishStreamState(state); break; } } result ??= this.finishStreamState(state); if (state.terminalError && !error && !aborted && !this.#session.run.isAbortRequested() && !result.suspended) { error = true; this.emit({ type: "error", error: new Error(state.terminalError) }); } this.emit({ type: "agent_end", reason: error ? "error" : result.suspended ? "suspended" : aborted || this.#session.run.isAbortRequested() ? "aborted" : "complete" }); this.#session.run.reset(); await this.drainFollowUpQueue(); return result; } async processStreamChunk(state, chunk, requestContext) { if ("runId" in chunk && chunk.runId) { this.#session.run.setRunId({ runId: chunk.runId }); } switch (chunk.type) { case "text-start": { const textIndex = state.currentMessage.content.length; state.currentMessage.content.push({ type: "text", text: "" }); state.textContentById.set(chunk.payload.id, { index: textIndex, text: "" }); this.emit({ type: "message_start", message: { ...state.currentMessage } }); break; } case "text-delta": { const textState = state.textContentById.get(chunk.payload.id); if (textState) { textState.text += chunk.payload.text; const textContent = state.currentMessage.content[textState.index]; if (textContent && textContent.type === "text") { textContent.text = textState.text; } this.emit({ type: "message_update", message: { ...state.currentMessage } }); } break; } case "reasoning-start": { const thinkingIndex = state.currentMessage.content.length; state.currentMessage.content.push({ type: "thinking", thinking: "" }); state.thinkingContentById.set(chunk.payload.id, { index: thinkingIndex, text: "" }); this.emit({ type: "message_update", message: { ...state.currentMessage } }); break; } case "reasoning-delta": { const thinkingState = state.thinkingContentById.get(chunk.payload.id); if (thinkingState) { thinkingState.text += chunk.payload.text; const thinkingContent = state.currentMessage.content[thinkingState.index]; if (thinkingContent && thinkingContent.type === "thinking") { thinkingContent.thinking = thinkingState.text; } this.emit({ type: "message_update", message: { ...state.currentMessage } }); } break; } case "tool-call-input-streaming-start": { const { toolCallId, toolName } = chunk.payload; this.emit({ type: "tool_input_start", toolCallId, toolName }); break; } case "tool-call-delta": { const { toolCallId, argsTextDelta, toolName } = chunk.payload; const transform = chunk2TATDSHU_cjs.getTransformedToolPayload(chunk.metadata, "display", "input-delta"); if (!transform?.suppress) { this.emit({ type: "tool_input_delta", toolCallId, argsTextDelta: chunk2TATDSHU_cjs.hasTransformedToolPayload(transform) ? transform.transformed : argsTextDelta, toolName }); } break; } case "tool-call-input-streaming-end": { const { toolCallId } = chunk.payload; this.emit({ type: "tool_input_end", toolCallId }); break; } case "tool-call": { const toolCall = chunk.payload; const args = getDisplayTransform(chunk.metadata, "input-available", toolCall.args); state.currentMessage.content.push({ type: "tool_call", id: toolCall.toolCallId, name: toolCall.toolName, args }); this.emit({ type: "tool_start", toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, args }); this.emit({ type: "message_update", message: { ...state.currentMessage } }); break; } case "tool-result": { const toolResult = chunk.payload; const providerMetadata = toolResult.providerMetadata; const result = getDisplayTransform(chunk.metadata, "output-available", toolResult.result); state.currentMessage.content.push({ type: "tool_result", id: toolResult.toolCallId, name: toolResult.toolName, result, isError: toolResult.isError ?? false, ...providerMetadata ? { providerMetadata } : {} }); this.emit({ type: "tool_end", toolCallId: toolResult.toolCallId, result, isError: toolResult.isError ?? false, ...providerMetadata ? { providerMetadata } : {} }); this.emit({ type: "message_update", message: { ...state.currentMessage } }); break; } case "tool-error": { const toolError = chunk.payload; this.emit({ type: "tool_end", toolCallId: toolError.toolCallId, result: getDisplayTransform(chunk.metadata, "error", toolError.error), isError: true }); break; } case "tool-call-approval": { const toolCallId = chunk.payload.toolCallId; const toolName = chunk.payload.toolName; const approvalTransform = chunk2TATDSHU_cjs.getTransformedToolPayload(chunk.metadata, "display", "approval"); const toolArgs = chunk2TATDSHU_cjs.hasTransformedToolPayload(approvalTransform) ? approvalTransform.transformed : getDisplayTransform(chunk.metadata, "input-available", chunk.payload.args); const policy = this.resolveToolApproval(toolName); if (policy === "allow") { await this.handleToolApprove({ toolCallId, requestContext }); break; } if (policy === "deny") { await this.handleToolDecline({ toolCallId, requestContext }); break; } const approvalPromise = this.#session.approval.arm({ toolName }); this.emit({ type: "tool_approval_required", toolCallId, toolName, args: toolArgs }); const approval = await approvalPromise; this.#session.approval.clearToolName(); if (approval.decision === "approve") { await this.handleToolApprove({ toolCallId, requestContext: approval.requestContext ?? requestContext }); } else { await this.handleToolDecline({ toolCallId, requestContext: approval.requestContext ?? requestContext }); } break; } case "tool-call-suspended": { const suspToolCallId = chunk.payload.toolCallId; const suspToolName = chunk.payload.toolName; const suspArgs = getDisplayTransform(chunk.metadata, "input-available", chunk.payload.args); const suspPayload = getDisplayTransform(chunk.metadata, "suspend", chunk.payload.suspendPayload); const suspResumeSchema = chunk.payload.resumeSchema; const suspRunId = this.#session.run.getRunId(); if (suspRunId) { this.#session.suspensions.register({ toolCallId: suspToolCallId, runId: suspRunId, toolName: suspToolName }); } state.isSuspended = true; this.emit({ type: "tool_suspended", toolCallId: suspToolCallId, toolName: suspToolName, args: suspArgs, suspendPayload: suspPayload, resumeSchema: suspResumeSchema }); break; } case "error": { const streamError = chunkXSOONORA_cjs.getErrorFromUnknown(chunk.payload.error); this.emit({ type: "error", error: streamError }); break; } case "step-finish": { const usage = chunk.payload?.output?.usage; if (usage) { const usageRecord = usage; const promptTokens = getUsageNumber(usageRecord, "promptTokens") ?? getUsageNumber(usageRecord, "inputTokens") ?? 0; const completionTokens = getUsageNumber(usageRecord, "completionTokens") ?? getUsageNumber(usageRecord, "outputTokens") ?? 0; const totalTokens = getUsageNumber(usageRecord, "totalTokens") ?? promptTokens + completionTokens; const stepUsage = { promptTokens, completionTokens, totalTokens }; addOptionalUsageField2(stepUsage, "reasoningTokens", getUsageNumber(usageRecord, "reasoningTokens")); addOptionalUsageField2(stepUsage, "cachedInputTokens", getUsageNumber(usageRecord, "cachedInputTokens")); addOptionalUsageField2( stepUsage, "cacheCreationInputTokens", getUsageNumber(usageRecord, "cacheCreationInputTokens") ); if (usageRecord.raw !== void 0) { stepUsage.raw = usageRecord.raw; } this.#session.addUsage(stepUsage); this.persistTokenUsage().catch(() => { }); this.emit({ type: "usage_update", usage: stepUsage }); } break; } case "finish": { const finishReason = chunk.payload.stepResult?.reason; const finishProviderMetadata = chunk.payload?.metadata?.providerMetadata ?? chunk.payload?.providerMetadata; const fallbackNotice = describeServerSideFallback(finishProviderMetadata); if (fallbackNotice) { this.emit({ type: "info", message: fallbackNotice }); } if (finishReason === "stop" || finishReason === "end-turn") { state.currentMessage.stopReason = "complete"; } else if (finishReason === "tool-calls") { state.currentMessage.stopReason = "tool_use"; } else { const errorMessage = describeNonSuccessFinishReason(finishReason, finishProviderMetadata); if (errorMessage) { state.currentMessage.stopReason = "error"; state.currentMessage.errorMessage = errorMessage; state.terminalError = errorMessage; } else { state.currentMessage.stopReason = "complete"; } } break; } case "goal": { this.finishCurrentMessageAndRotate(state); this.emit({ type: "goal_evaluation", payload: chunk.payload }); break; } // Observational Memory data parts // NOTE: OM data parts arrive as { type, data: { ... } } — NOT { type, payload } case "data-om-status": { const d = chunk.data; if (d?.windows) { const w = d.windows; const active = w.active ?? {}; const msgs = active.messages ?? {}; const obs = active.observations ?? {}; const buffObs = w.buffered?.observations ?? {}; const buffRef = w.buffered?.reflection ?? {}; this.emit({ type: "om_status", windows: { active: { messages: { tokens: msgs.tokens ?? 0, threshold: msgs.threshold ?? 0 }, observations: { tokens: obs.tokens ?? 0, threshold: obs.threshold ?? 0 } }, buffered: { observations: { status: buffObs.status ?? "idle", chunks: buffObs.chunks ?? 0, messageTokens: buffObs.messageTokens ?? 0, projectedMessageRemoval: buffObs.projectedMessageRemoval ?? 0, observationTokens: buffObs.observationTokens ?? 0 }, reflection: { status: buffRef.status ?? "idle", inputObservationTokens: buffRef.inputObservationTokens ?? 0, observationTokens: buffRef.observationTokens ?? 0 } } }, recordId: d.recordId ?? "", threadId: d.threadId ?? "", stepNumber: d.stepNumber ?? 0, generationCount: d.generationCount ?? 0 }); } break; } case "data-om-observation-start": { const payload = chunk.data; if (payload && payload.cycleId) { if (payload.operationType === "observation") { this.emit({ type: "om_observation_start", cycleId: payload.cycleId, operationType: payload.operationType, tokensToObserve: payload.tokensToObserve ?? 0 }); } else if (payload.operationType === "reflection") { this.emit({ type: "om_reflection_start", cycleId: payload.cycleId, tokensToReflect: payload.tokensToObserve ?? 0 }); } } break; } case "data-om-observation-end": { const payload = chunk.data; if (payload && payload.cycleId) { if (payload.operationType === "reflection") { this.emit({ type: "om_reflection_end", cycleId: payload.cycleId, durationMs: payload.durationMs ?? 0, compressedTokens: payload.observationTokens ?? 0, observations: payload.observations }); } else { this.emit({ type: "om_observation_end", cycleId: payload.cycleId, durationMs: payload.durationMs ?? 0, tokensObserved: payload.tokensObserved ?? 0, observationTokens: payload.observationTokens ?? 0, observations: payload.observations, currentTask: payload.currentTask, suggestedResponse: payload.suggestedResponse }); } } break; } case "data-om-observation-failed": { const payload = chunk.data; if (payload) { const operationType = payload.operationType === "reflection" ? "reflection" : "observation"; const error = payload.error ?? "Unknown error"; if (operationType === "reflection") { this.emit({ type: "om_reflection_failed", cycleId: payload.cycleId ?? "unknown", error, durationMs: payload.durationMs ?? 0 }); } else { this.emit({ type: "om_observation_failed", cycleId: payload.cycleId ?? "unknown", error, durationMs: payload.durationMs ?? 0 }); } this.abortForOmFailure({ operationType, stage: "run", error }); return { message: state.currentMessage }; } break; } // Async buffering lifecycle case "data-om-buffering-start": { const payload = chunk.data; if (payload && payload.cycleId) { this.emit({ type: "om_buffering_start", cycleId: payload.cycleId, operationType: payload.operationType ?? "observation", tokensToBuffer: payload.tokensToBuffer ?? 0 }); } break; } case "data-om-buffering-end": { const payload = chunk.data; if (payload && payload.cycleId) { this.emit({ type: "om_buffering_end", cycleId: payload.cycleId, operationType: payload.operationType ?? "observation", tokensBuffered: payload.tokensBuffered ?? 0, bufferedTokens: payload.bufferedTokens ?? 0, observations: payload.observations }); } break; } case "data-om-buffering-failed": { const payload = chunk.data; if (payload) { const operationType = payload.operationType ?? "observation"; const error = payload.error ?? "Unknown error"; this.emit({ type: "om_buffering_failed", cycleId: payload.cycleId, operationType, error }); this.abortForOmFailure({ operationType, stage: "buffering", error }); return { message: state.currentMessage }; } break; } case "data-signal": { const payload = chunk.data; if (payload?.type === "state") { const stateSignal = toStateSignalContent(payload); if (stateSignal) { state.currentMessage.content.push(stateSignal); this.emit({ type: "message_update", message: state.currentMessage }); } } else if (payload?.type === "reactive" && payload.tagName === "system-reminder") { const reminder = toSystemReminderContent(payload); if (reminder) { state.currentMessage.content.push(reminder); this.emit({ type: "message_update", message: state.currentMessage }); } } else if (payload?.type === "notification" && payload.tagName === "notification-summary") { const notificationSummary = toNotificationSummaryContent(payload); if (notificationSummary) { state.currentMessage.content.push(notificationSummary); this.emit({ type: "message_update", message: state.currentMessage }); } } else if (payload?.type === "notification" && payload.tagName === "notification") { const notification = toNotificationContent(payload); if (notification) { state.currentMessage.content.push(notification); this.emit({ type: "message_update", message: state.currentMessage }); } } else if (payload?.type === "reactive") { const reactiveSignal = toReactiveSignalContent(payload); if (reactiveSignal) { state.currentMessage.content.push(reactiveSignal); this.emit({ type: "message_update", message: state.currentMessage }); } } break; } case "data-user-message": { const payload = chunk.data; const message = payload ? toUserSignalMessage(payload) : void 0; if (message) { if (state.currentMessage.content.length > 0) { state.currentMessage.stopReason ??= "complete"; this.emit({ type: "message_end", message: { ...state.currentMessage } }); state.currentMessage = { id: this.generateId(), role: "assistant", content: [], createdAt: /* @__PURE__ */ new Date() }; state.textContentById.clear(); state.thinkingContentById.clear(); } this.emit({ type: "message_start", message }); this.emit({ type: "message_end", message }); } break; } // Back-compat: persisted streams may still contain data-system-reminder parts case "data-system-reminder": { const payload = chunk.data; const reminder = payload ? toSystemReminderContent(payload) : void 0; if (reminder) { state.currentMessage.content.push(reminder); this.emit({ type: "message_update", message: state.currentMessage }); } break; } case "data-om-activation": { const payload = chunk.data; if (payload && payload.cycleId) { this.emit({ type: "om_activation", cycleId: payload.cycleId, operationType: payload.operationType ?? "observation", chunksActivated: payload.chunksActivated ?? 0, tokensActivated: payload.tokensActivated ?? 0, observationTokens: payload.observationTokens ?? 0, messagesActivated: payload.messagesActivated ?? 0, generationCount: payload.generationCount ?? 0, triggeredBy: payload.triggeredBy, lastActivityAt: payload.lastActivityAt, ttlExpiredMs: payload.ttlExpiredMs, activateAfterIdle: typeof payload.config?.activateAfterIdle === "number" ? payload.config.activateAfterIdle : void 0, previousModel: payload.previousModel, currentModel: payload.currentModel }); } break; } case "data-om-thread-update": { const payload = chunk.data; if (payload && payload.newTitle) { this.emit({ type: "om_thread_title_updated", cycleId: payload.cycleId ?? "unknown", threadId: payload.threadId ?? this.#session.thread.getId() ?? "unknown", oldTitle: payload.oldTitle, newTitle: payload.newTitle }); } break; } // Sandbox streaming data chunks (from workspace execute_command tool) case "data-sandbox-stdout": { const d = chunk.data; if (d?.output && d?.toolCallId) { this.emit({ type: "shell_output", toolCallId: d.toolCallId, output: d.output, stream: "stdout" }); } break; } case "data-sandbox-stderr": { const d = chunk.data; if (d?.output && d?.toolCallId) { this.emit({ type: "shell_output", toolCallId: d.toolCallId, output: d.output, stream: "stderr" }); } break; } } } finishStreamState(state) { if (this.hasCurrentMessageContent(state) || !state.lastFinishedMessage) { this.emit({ type: "message_end", message: state.currentMessage }); return { message: state.currentMessage, suspended: state.isSuspended || void 0 }; } return { message: state.lastFinishedMessage, suspended: state.isSuspended || void 0 }; } // =========================================================================== // Control // =========================================================================== /** * Abort the current operation. */ abort() { const hadPendingSuspensions = this.#session.displayState.get().pendingSuspensions.size > 0; this.#session.displayState.clearPendingSuspensions(); this.#session.abortRun(); if (hadPendingSuspensions) { this.dispatchDisplayStateChanged(); } } /** * Detach from the current thread's event stream without switching to another * thread. Used by the TUI `/new` command to stop receiving cross-process * events from the old thread while the new thread creation is deferred until * the first user message. * * The current thread ID is preserved so that {@link createThread} can still * release the thread lock (when configured) for the previous thread. */ detachFromCurrentThread() { this.abort(); this.cleanupAgentThreadSubscription(); } /** * Steer the agent mid-stream: aborts current run and sends a new message. */ async steer({ content, requestContext }) { this.abort(); this.#session.followUps.clear(); this.emit({ type: "follow_up_queued", count: 0 }); await this.sendMessage({ content, requestContext }); } /** * Queue a follow-up message to be processed after the current operation completes. */ async followUp({ content, requestContext }) { if (this.#session.run.isRunning()) { this.#session.followUps.enqueue({ content, requestContext }); this.emit({ type: "follow_up_queued", count: this.#session.followUps.count() }); } else { await this.sendMessage({ content, requestContext }); } } /** * True when one or more tools are parked awaiting a resume (e.g. ask_user / * request_access suspensions). A suspended run nulls the AbortController, so * isRunning() returns false even though the run is still pending — callers that * need to know whether the harness is awaiting user input (e.g. to allow abort) * should check this too. */ /** * Resolve once the current thread's stream is fully idle. * * After `abort()` is called the run's status can still be `'running'` for a * few microtasks while the underlying model stream finalizes. Callers that * need to send a fresh signal after an abort (e.g. plan approval → mode * switch → trigger reminder) should await this before calling `sendSignal` * to avoid the new signal being queued onto the dying run, which would then * be drained with the previous run's already-aborted abortSignal. */ async waitForCurrentThreadStreamIdle() { while (this.#session.stream.isActive() || this.#session.run.getRunId() !== null) { await new Promise((resolve) => setTimeout(resolve, 0)); } } getSubagentDisplayName(agentType) { return this.config.subagents?.find((subagent) => subagent.id === agentType)?.name; } /** * Respond to a pending tool suspension from the UI. * Provides resume data so the suspended tool can continue execution. * * `toolCallId` selects which suspended tool to resume — required when more than * one tool is suspended concurrently (e.g. parallel `ask_user` calls, see issue * #13642). When omitted it resolves to the sole pending suspension. */ async respondToToolSuspension({ resumeData, toolCallId, requestContext }) { const resolvedToolCallId = this.#session.suspensions.resolveToolCallId(toolCallId); if (!resolvedToolCallId) return; const suspension = this.#session.suspensions.get({ toolCallId: resolvedToolCallId }); try { if (suspension?.toolName === "submit_plan") { await this.handlePlanApprovalResume({ toolCallId: resolvedToolCallId, response: resumeData, requestContext }); return; } await this.handleToolResume({ resumeData, toolCallId: resolvedToolCallId, requestContext }); } catch (error) { const err = chunkXSOONORA_cjs.getErrorFromUnknown(error); this.emit({ type: "error", error: err }); this.emit({ type: "agent_end", reason: "error" }); } } // =========================================================================== // Plan Approval // =========================================================================== /** * Respond to a suspended `submit_plan` tool call. * * `submit_plan` is an agent-agnostic tool that pauses via the native tool-suspension * primitive. The Harness layers its planning UX on top of that generic pause here: * * - On **rejection**, the plan-mode run is resumed with the feedback so the agent can * revise and submit again. This is an ordinary tool resume. * - On **approval**, the parked plan-mode suspension is abandoned and the Harness * switches to its default (execution) mode. switchMode aborts the plan-mode run, so * there is no point resuming it first; the next signal/message drives the fresh * default-mode run. The model still sees the "approved" tool result on the rebuilt * message history when the default-mode run starts. * * Non-Harness consumers (a plain Agent in Studio or a customer app) instead resume the * tool directly via `agent.resumeStream({ action, feedback })` — no modes involved. */ async handlePlanApprovalResume({ toolCallId, response, requestContext }) { if (response.action === "rejected") { await this.handleToolResume({ resumeData: response, toolCallId, requestContext }); return; } this.#session.suspensions.delete({ toolCallId }); const currentMode = this.#session.mode.resolve(); const transitionModeId = currentMode.transitionsTo ?? this.config.defaultModeId ?? this.config.modes.find((mode) => mode.default || mode.metadata?.default === true)?.id ?? this.config.modes[0]?.id; const transitionMode = this.listModes().find((mode) => mode.id === transitionModeId); if (transitionMode && transitionMode.id !== this.#session.mode.get()) { await new Promise((resolveTimeout) => setTimeout(resolveTimeout, 0)); await this.switchMode({ modeId: transitionMode.id }); await this.waitForCurrentThreadStreamIdle(); } } async handleToolApprove({ toolCallId, requestContext: requestContextInput }) { const runId = this.#session.run.getRunId(); if (!runId) { throw new Error("No active run to approve tool call for"); } const agent = this.getCurrentAgent(); const requestContext = await this.buildRequestContext(requestContextInput); const isYolo = this.#session.state.get().yolo === true; const threadId = this.#session.thread.getId(); await agent.approveToolCall({ runId, toolCallId, requireToolApproval: !isYolo, memory: threadId ? { thread: threadId, resource: this.#session.identity.getResourceId() } : void 0, abortSignal: this.#session.run.ensureAbortController().signal, requestContext, toolsets: await this.buildToolsets(requestContext) }); } async handleToolDecline({ toolCallId, requestContext: requestContextInput }) { const runId = this.#session.run.getRunId(); if (!runId) { throw new Error("No active run to decline tool call for"); } const agent = this.getCurrentAgent(); const requestContext = await this.buildRequestContext(requestContextInput); const isYolo = this.#session.state.get().yolo === true; const threadId = this.#session.thread.getId(); await agent.declineToolCall({ runId, toolCallId, requireToolApproval: !isYolo, memory: threadId ? { thread: threadId, resource: this.#session.identity.getResourceId() } : void 0, abortSignal: this.#session.run.ensureAbortController().signal, requestContext, toolsets: await this.buildToolsets(requestContext) }); } async handleToolResume({ resumeData, toolCallId, requestContext: requestContextInput }) { const suspension = this.#session.suspensions.get({ toolCallId }); if (!suspension) { throw new Error("No active suspension to resume"); } const agent = this.getCurrentAgent(); this.#session.suspensions.delete({ toolCallId }); this.#session.displayState.deletePendingSuspension(toolCallId); const requestContext = await this.buildRequestContext(requestContextInput); const threadId = this.#session.thread.getId(); const output = await agent.resumeStream(resumeData, { // Re-supply the shared run budget (maxSteps, etc). Without it the resumed // run merges over the agent's small default maxSteps and stops mid-task. ...this.buildSharedRunOptions(), runId: suspension.runId, toolCallId, memory: threadId ? { thread: threadId, resource: this.#session.identity.getResourceId() } : void 0, abortSignal: this.#session.run.ensureAbortController().signal, requestContext, toolsets: await this.buildToolsets(requestContext) }); await this.processStream(output, requestContext); } // =========================================================================== // Event System // =========================================================================== /** * Subscribe to harness events. Returns an unsubscribe function. */ subscribe(listener) { this.listeners.push(listener); return () => { const index = this.listeners.indexOf(listener); if (index !== -1) { this.listeners.splice(index, 1); } }; } emit(event) { this.#session.displayState.apply(event); this.dispatchToListeners(event); if (event.type !== "display_state_changed") { this.dispatchDisplayStateChanged(); } } dispatchDisplayStateChanged() { this.dispatchToListeners({ type: "display_state_changed", displayState: this.#session.displayState.get() }); } dispatchToListeners(event) { for (const listener of this.listeners) { try { const result = listener(event); if (result && typeof result === "object" && "catch" in result) { result.catch((err) => console.error("Error in harness event listener:", err)); } } catch (err) { console.error("Error in harness event listener:", err); } } } // =========================================================================== // Runtime Context // =========================================================================== /** * Build the toolsets object that includes built-in harness tools (ask_user, submit_plan, * and optionally subagent) plus any user-configured tools. * Used by sendMessage, handleToolApprove, and handleToolDecline. */ async buildToolsets(requestContext) { const builtInTools = { ask_user: chunkD37QL5LB_cjs.askUserTool, submit_plan: chunkD37QL5LB_cjs.submitPlanTool, task_write: chunkD37QL5LB_cjs.taskWriteTool, task_update: chunkD37QL5LB_cjs.taskUpdateTool, task_complete: chunkD37QL5LB_cjs.taskCompleteTool, task_check: chunkD37QL5LB_cjs.taskCheckTool }; let resolvedHarnessTools = void 0; if (this.config.tools) { const tools = typeof this.config.tools === "function" ? await this.config.tools({ requestContext }) : this.config.tools; if (tools) { resolvedHarnessTools = { ...tools }; } } if (this.config.subagents?.length && this.config.resolveModel) { const currentMode = this.#session.mode.resolve(); const hasMemory = Boolean(this.config.memory); builtInTools.subagent = createSubagentTool({ subagents: this.config.subagents, resolveModel: this.config.resolveModel, harnessTools: resolvedHarnessTools, fallbackModelId: currentMode?.defaultModelId, getParentModelId: () => this.#session.model.get(), // Resolved lazily so forked subagents see the current mode's agent // even if the mode switches between tool-call scheduling and execution. getParentAgent: () => { try { return this.getCurrentAgent(); } catch { return void 0; } }, // Only wired up when memory is configured. Clones at the memory layer // (not via Harness.cloneThread) so the parent thread stays the active // thread while the forked subagent runs on the clone. // // The clone is tagged with `forkedSubagent: true` + `parentThreadId` so // that thread pickers / startup flows can hide transient fork threads — // see `listThreads` (filtered by default). cloneThreadForFork: hasMemory ? async ({ sourceThreadId, resourceId, title }) => { const memory = await this.resolveMemory(); const result2 = await memory.cloneThread({ sourceThreadId, resourceId: resourceId ?? this.#session.identity.getResourceId(), title, metadata: { forkedSubagent: true, parentThreadId: sourceThreadId } }); return { id: result2.thread.id, resourceId: result2.thread.resourceId }; } : void 0, // Forks inherit the parent's toolsets verbatim so harness-injected // tools (`ask_user`, `submit_plan`, user-configured harness tools, etc.) // remain available inside the fork. The `subagent` entry itself is // deliberately kept — its schema/description are part of the parent's // prompt-cache prefix, and stripping it would invalidate the cache. // Recursive forking is blocked at runtime instead: see the patched // `subagent` execute that the forked tool path installs in `tools.ts`. getParentToolsets: (forkRequestContext) => this.buildToolsets(forkRequestContext ?? requestContext) }); } if (this.config.disableBuiltinTools?.length) { for (const toolId of this.config.disableBuiltinTools) { delete builtInTools[toolId]; } } const permissionRules = this.getPermissionRules(); for (const [toolId, policy] of Object.entries(permissionRules.tools)) { if (policy === "deny") { delete builtInTools[toolId]; delete resolvedHarnessTools?.[toolId]; } } const result = { harnessBuiltIn: builtInTools }; if (resolvedHarnessTools) { result.harness = resolvedHarnessTools; } if (this.config.agent) { const currentMode = this.#session.mode.resolve(); const modeTools = currentMode.tools ?? currentMode.additionalTools; if (modeTools) { result.modeTools = modeTools; } } return result; } /** * Build request context for agent execution. * Tools can access harness state via requestContext.get('harness'). */ async buildRequestContext(requestContext) { requestContext ??= new chunkPJIAL3WK_cjs.RequestContext(); const harnessContext = { harnessId: this.id, state: this.#session.state.get(), getState: () => this.#session.state.get(), setState: (updates) => this.#session.state.set(updates), updateState: (updater) => this.#session.state.update(updater), threadId: this.#session.thread.getId(), resourceId: this.#session.identity.getResourceId(), session: { modeId: this.#session.mode.get(), modelId: this.#session.model.get(), state: { get: () => this.#session.state.get(), set: (updates) => this.#session.state.set(updates), update: (updater) => this.#session.state.update(updater) } }, abortSignal: this.#session.run.getAbortSignal(), workspace: this.workspace, emitEvent: (event) => this.emit(event), getSubagentModelId: (params) => this.getSubagentModelId(params) }; requestContext.set("harness", harnessContext); if (this.workspaceFn) { const resolved = await Promise.resolve(this.workspaceFn({ requestContext, mastra: this.#internalMastra })); harnessContext.workspace = resolved; this.workspace = resolved; } return requestContext; } /** * Resolve memory from config — handles both static instances and dynamic factory functions. */ async resolveMemory() { const mem = this.config.memory; if (!mem) { throw new Error("Memory is not configured on this Harness"); } if (typeof mem !== "function") { return mem; } const requestContext = await this.buildRequestContext(); const resolved = await Promise.resolve(mem({ requestContext })); if (!resolved) { throw new Error("Dynamic memory factory returned empty value"); } return resolved; } // =========================================================================== // Token Usage // =========================================================================== async persistTokenUsage() { const threadId = this.#session.thread.getId(); if (!threadId || !this.config.storage) return; try { const memoryStorage = await this.getMemoryStorage(); const thread = await memoryStorage.getThreadById({ threadId }); if (thread) { await memoryStorage.saveThread({ thread: { ...thread, metadata: { ...thread.metadata, tokenUsage: this.#session.getTokenUsage() }, updatedAt: /* @__PURE__ */ new Date() } }); } } catch { } } // =========================================================================== // Workspace // =========================================================================== getWorkspace() { return this.workspace; } /** * Eagerly resolve the workspace. For dynamic workspaces (factory function), * this triggers resolution and caches the result so getWorkspace() returns it. * Useful for code paths outside the request flow (e.g. slash commands). */ async resolveWorkspace({ requestContext } = {}) { if (this.workspace) return this.workspace; if (this.workspaceFn) { await this.buildRequestContext(requestContext); return this.workspace; } return void 0; } hasWorkspace() { return this.config.workspace !== void 0; } isWorkspaceReady() { if (this.workspaceFn) return true; return this.workspaceInitialized && this.workspace !== void 0; } async destroyWorkspace() { if (this.workspaceFn) return; if (this.workspace && this.workspaceInitialized) { try { this.emit({ type: "workspace_status_changed", status: "destroying" }); await this.workspace.destroy(); this.emit({ type: "workspace_status_changed", status: "destroyed" }); } catch (error) { console.warn("Workspace destroy failed:", error); } finally { this.workspaceInitialized = false; } } } // =========================================================================== // Heartbeat Handlers // =========================================================================== startHeartbeats() { const handlers = [...this.config.heartbeatHandlers ?? []]; if (!handlers.length) return; for (const hb of handlers) { if (this.heartbeatTimers.has(hb.id)) continue; const run = async () => { try { await hb.handler(); } catch (error) { console.error(`[Heartbeat:${hb.id}] failed:`, error); } }; if (hb.immediate !== false) { void run(); } const timer = setInterval(run, hb.intervalMs); timer.unref(); this.heartbeatTimers.set(hb.id, { timer, shutdown: hb.shutdown }); } } registerHeartbeat(handler) { void this.removeHeartbeat({ id: handler.id }); const run = async () => { try { await handler.handler(); } catch (error) { console.error(`[Heartbeat:${handler.id}] failed:`, error); } }; if (handler.immediate !== false) { void run(); } const timer = setInterval(run, handler.intervalMs); timer.unref(); this.heartbeatTimers.set(handler.id, { timer, shutdown: handler.shutdown }); } async removeHeartbeat({ id }) { const entry = this.heartbeatTimers.get(id); if (entry) { clearInterval(entry.timer); this.heartbeatTimers.delete(id); try { await entry.shutdown?.(); } catch (error) { console.error(`[Heartbeat:${id}] shutdown failed:`, error); } } } async stopHeartbeats() { const entries = [...this.heartbeatTimers.entries()]; this.heartbeatTimers.clear(); for (const [id, entry] of entries) { clearInterval(entry.timer); try { await entry.shutdown?.(); } catch (error) { console.error(`[Heartbeat:${id}] shutdown failed:`, error); } } } // =========================================================================== // Lifecycle // =========================================================================== async destroy() { this.cleanupAgentThreadSubscription(); await this.stopHeartbeats(); await this.destroyWorkspace(); } // =========================================================================== // Session // =========================================================================== async getSession() { return { currentThreadId: this.#session.thread.getId(), currentModeId: this.#session.mode.get(), threads: await this.#session.thread.list() }; } // =========================================================================== // Utilities // =========================================================================== generateId() { if (this.config.idGenerator) { return this.config.idGenerator(); } return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; } }; Object.defineProperty(exports, "askUserTool", { enumerable: true, get: function () { return chunkD37QL5LB_cjs.askUserTool; } }); Object.defineProperty(exports, "assignTaskIds", { enumerable: true, get: function () { return chunkD37QL5LB_cjs.assignTaskIds; } }); Object.defineProperty(exports, "submitPlanTool", { enumerable: true, get: function () { return chunkD37QL5LB_cjs.submitPlanTool; } }); Object.defineProperty(exports, "taskCheckTool", { enumerable: true, get: function () { return chunkD37QL5LB_cjs.taskCheckTool; } }); Object.defineProperty(exports, "taskCompleteTool", { enumerable: true, get: function () { return chunkD37QL5LB_cjs.taskCompleteTool; } }); Object.defineProperty(exports, "taskUpdateTool", { enumerable: true, get: function () { return chunkD37QL5LB_cjs.taskUpdateTool; } }); Object.defineProperty(exports, "taskWriteTool", { enumerable: true, get: function () { return chunkD37QL5LB_cjs.taskWriteTool; } }); exports.Harness = Harness; exports.Session = Session; exports.defaultDisplayState = defaultDisplayState; exports.defaultOMProgressState = defaultOMProgressState; exports.parseSubagentMeta = parseSubagentMeta; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map