agents
Version:
A home for your AI agents
2,721 lines • 100 kB
JavaScript
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../../classPrivateFieldGet2-DZBYAB34.js";
import { t as _classPrivateMethodInitSpec } from "../../classPrivateMethodInitSpec-qMjJ6sHQ.js";
import { n as bindLifecycleCapability, t as LifecycleCapability } from "../../capability-DBAKc6UZ.js";
import { n as STREAM_RESUME_NONE_REASONS, r as StreamAccumulator, t as CHAT_MESSAGE_TYPES } from "../../protocol-eXeJxhgu.js";
import { n as Streams } from "../../streams-lsHMJhRW.js";
import { i as createToolsFromClientSchemas, n as iterateWithStallWatchdog, r as parseProtocolMessage, t as ChatStreamStalledError } from "../../stall-watchdog-BpRF2uwG.js";
import { t as Sessions } from "../../sessions-_vN5BEZX.js";
import { a as toTranscriptMessage, r as toResponseChunk, t as answerToolCall } from "../../turns-DCV3ycDl.js";
import { n as openHarnessStore } from "../../store-CslVzfN7.js";
import { convertToModelMessages, stepCountIs, streamText, toUIMessageStream } from "ai";
//#region src/harness/think/events.ts
var _listeners = /* @__PURE__ */ new WeakMap();
var _observers = /* @__PURE__ */ new WeakMap();
/** Fan-out of session events to listeners, per session. */
var SessionEvents = class {
constructor() {
_classPrivateFieldInitSpec(this, _listeners, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _observers, /* @__PURE__ */ new Set());
}
/** Listen to every session. Returns the unsubscribe. */
observe(listener) {
_classPrivateFieldGet2(_observers, this).add(listener);
return () => {
_classPrivateFieldGet2(_observers, this).delete(listener);
};
}
/** Listen to one session. Returns the unsubscribe. */
subscribe(session, listener) {
let listeners = _classPrivateFieldGet2(_listeners, this).get(session);
if (!listeners) {
listeners = /* @__PURE__ */ new Set();
_classPrivateFieldGet2(_listeners, this).set(session, listeners);
}
listeners.add(listener);
return () => {
listeners.delete(listener);
if (listeners.size === 0) _classPrivateFieldGet2(_listeners, this).delete(session);
};
}
/** Deliver an event to a session's listeners. A throwing listener is logged. */
emit(session, event) {
for (const observer of _classPrivateFieldGet2(_observers, this)) try {
observer(session, event);
} catch (error) {
console.error("A ThinkHarness observer failed", error);
}
for (const listener of _classPrivateFieldGet2(_listeners, this).get(session) ?? []) try {
listener(event);
} catch (error) {
console.error("A ThinkHarness session listener failed", error);
}
}
};
var _close = /* @__PURE__ */ new WeakMap();
var _listener = /* @__PURE__ */ new WeakMap();
var _queue = /* @__PURE__ */ new WeakMap();
var _onStop = /* @__PURE__ */ new WeakMap();
var _draining = /* @__PURE__ */ new WeakMap();
var _stopped = /* @__PURE__ */ new WeakMap();
var _Watcher_brand = /* @__PURE__ */ new WeakSet();
/** One watch: events queue until `start`, then reach the listener in order. */
var Watcher = class {
constructor(state, onStop) {
_classPrivateMethodInitSpec(this, _Watcher_brand);
_classPrivateFieldInitSpec(this, _close, () => {});
_classPrivateFieldInitSpec(this, _listener, void 0);
_classPrivateFieldInitSpec(this, _queue, []);
_classPrivateFieldInitSpec(this, _onStop, void 0);
_classPrivateFieldInitSpec(this, _draining, false);
_classPrivateFieldInitSpec(this, _stopped, false);
this.state = state;
_classPrivateFieldSet2(_onStop, this, onStop);
this.closed = new Promise((resolve) => {
_classPrivateFieldSet2(_close, this, resolve);
});
}
/** Start delivering queued and later events. */
start(listener) {
_classPrivateFieldSet2(_listener, this, listener);
_assertClassBrand(_Watcher_brand, this, _drain).call(this);
}
/** Stop the watch. */
async stop() {
if (_classPrivateFieldGet2(_stopped, this)) return;
_classPrivateFieldSet2(_stopped, this, true);
_classPrivateFieldGet2(_onStop, this).call(this);
_classPrivateFieldGet2(_close, this).call(this);
}
/** Queue events for the listener. */
push(events) {
if (_classPrivateFieldGet2(_stopped, this) || events.length === 0) return;
_classPrivateFieldGet2(_queue, this).push(events);
_assertClassBrand(_Watcher_brand, this, _drain).call(this);
}
};
async function _drain() {
if (!_classPrivateFieldGet2(_listener, this) || _classPrivateFieldGet2(_draining, this)) return;
_classPrivateFieldSet2(_draining, this, true);
try {
for (;;) {
const events = _classPrivateFieldGet2(_queue, this).shift();
if (!events || _classPrivateFieldGet2(_stopped, this)) return;
try {
await _classPrivateFieldGet2(_listener, this).call(this, events);
} catch (error) {
console.error("A ThinkHarness session watcher failed", error);
}
}
} finally {
_classPrivateFieldSet2(_draining, this, false);
}
}
//#endregion
//#region src/harness/think/records.ts
/** The shared store's table prefix for ThinkHarness's records. */
const STORE_PREFIX = "cf_think_harness_store_";
/** The tables ThinkHarness kept its records in before the shared store. */
const LEGACY_SESSIONS = "cf_think_harness_sessions";
const LEGACY_OPERATIONS = "cf_think_harness_operations";
const LEGACY_TOOL_CALLS = "cf_think_harness_tool_calls";
const OPEN$1 = ["queued", "running"];
const EMPTY_STATE = {
clientTools: [],
toolCalls: {}
};
function isRecord$1(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function stringOrNull(value) {
return typeof value === "string" ? value : null;
}
function count(value) {
return typeof value === "number" ? value : 0;
}
function parseMeta(value) {
const fields = isRecord$1(value) ? value : {};
return {
source: fields.source === "client" ? "client" : "server",
parentId: stringOrNull(fields.parentId),
messageId: stringOrNull(fields.messageId),
streamId: stringOrNull(fields.streamId),
pendingModel: fields.pendingModel === true,
steps: count(fields.steps),
interruptions: count(fields.interruptions),
overflowRetries: count(fields.overflowRetries),
abandonReason: stringOrNull(fields.abandonReason)
};
}
function newMeta(source) {
return {
source,
parentId: null,
messageId: null,
streamId: null,
pendingModel: false,
steps: 0,
interruptions: 0,
overflowRetries: 0,
abandonReason: null
};
}
function metaJson(meta) {
return { ...meta };
}
function withProgress(meta, change) {
return {
source: meta.source,
parentId: change.parentId === void 0 ? meta.parentId : change.parentId,
messageId: change.messageId === void 0 ? meta.messageId : change.messageId,
streamId: change.streamId === void 0 ? meta.streamId : change.streamId,
pendingModel: change.pendingModel ?? meta.pendingModel,
steps: change.steps ?? meta.steps,
interruptions: change.interruptions ?? meta.interruptions,
overflowRetries: change.overflowRetries ?? meta.overflowRetries,
abandonReason: change.abandonReason === void 0 ? meta.abandonReason : change.abandonReason
};
}
function inputJson(input) {
return input;
}
function parseInput(value) {
return value;
}
function parseText(result) {
return isRecord$1(result) && typeof result.text === "string" ? result.text : void 0;
}
function parseSessionState(value) {
if (!isRecord$1(value)) return EMPTY_STATE;
const toolCalls = Object.create(null);
const calls = value.toolCalls;
if (isRecord$1(calls)) {
for (const [toolCallId, call] of Object.entries(calls)) if (isRecord$1(call) && typeof call.operationId === "string" && typeof call.attempts === "number") toolCalls[toolCallId] = {
operationId: call.operationId,
attempts: call.attempts
};
}
return {
clientTools: Array.isArray(value.clientTools) ? value.clientTools : [],
toolCalls
};
}
function sessionStateJson(state) {
return {
clientTools: state.clientTools,
toolCalls: { ...state.toolCalls }
};
}
function parseOperation(stored) {
const meta = parseMeta(stored.meta);
return {
session: stored.session,
operationId: stored.id,
seq: stored.seq,
input: parseInput(stored.input),
status: stored.status,
source: meta.source,
parentId: meta.parentId ?? void 0,
messageId: meta.messageId ?? void 0,
streamId: meta.streamId ?? void 0,
pendingModel: meta.pendingModel,
steps: meta.steps,
interruptions: meta.interruptions,
overflowRetries: meta.overflowRetries,
reason: stored.reason,
text: stored.status === "done" ? parseText(stored.result) : void 0,
abandonReason: meta.abandonReason ?? void 0,
createdAt: stored.createdAt
};
}
var _store = /* @__PURE__ */ new WeakMap();
var _OperationRecords_brand = /* @__PURE__ */ new WeakSet();
/**
* The harness's records, over the shared harness store: sessions, the
* operation queue, and started tool calls. Opening it moves records an
* earlier version left in its own tables into the store.
*/
var OperationRecords = class {
/**
* Open the records, creating the store's tables and importing any legacy
* tables. Cheap after the first time: keep one per isolate.
*
* @param storage - The Durable Object's storage.
*/
constructor(storage) {
_classPrivateMethodInitSpec(this, _OperationRecords_brand);
_classPrivateFieldInitSpec(this, _store, void 0);
_classPrivateFieldSet2(_store, this, openHarnessStore(storage, { prefix: STORE_PREFIX }));
importLegacyTables(storage, _classPrivateFieldGet2(_store, this));
}
/** Record a session, unless it exists. */
ensureSession(id, parent) {
_classPrivateFieldGet2(_store, this).createSession({
id,
...parent !== void 0 && { parent },
state: sessionStateJson(EMPTY_STATE)
});
}
/** Every session, oldest first. */
sessions() {
return _classPrivateFieldGet2(_store, this).sessions().map((session) => ({
id: session.id,
parent: session.parent,
createdAt: session.createdAt
}));
}
/** The client tools a session's client last declared. */
clientTools(session) {
return [..._assertClassBrand(_OperationRecords_brand, this, _state).call(this, session).clientTools];
}
/** Replace the client tools a session's client declared. */
setClientTools(session, tools) {
_assertClassBrand(_OperationRecords_brand, this, _updateState).call(this, session, (state) => ({
...state,
clientTools: tools
}));
}
/** Insert a queued operation. Returns false when the id already exists. */
insert(session, operationId, input, source) {
return _classPrivateFieldGet2(_store, this).enqueue({
session,
id: operationId,
input: inputJson(input),
meta: metaJson(newMeta(source))
}).accepted;
}
/** One operation. */
get(session, operationId) {
const stored = _classPrivateFieldGet2(_store, this).operation(session, operationId);
return stored ? parseOperation(stored) : void 0;
}
/** Unsettled operations, oldest first; every session when none is given. */
open(session) {
return _classPrivateFieldGet2(_store, this).operations({
...session !== void 0 && { session },
status: OPEN$1
}).map(parseOperation);
}
/** Change an operation's progress. */
update(session, operationId, change) {
_classPrivateFieldGet2(_store, this).transaction(() => {
const stored = _classPrivateFieldGet2(_store, this).operation(session, operationId);
if (!stored) return;
_classPrivateFieldGet2(_store, this).setOperationMeta(session, operationId, metaJson(withProgress(parseMeta(stored.meta), change)));
});
}
/**
* Move a queued operation to running, with its progress so far.
* Returns false when it was not queued.
*/
start(session, operationId, change) {
return _classPrivateFieldGet2(_store, this).transaction(() => {
if (!_classPrivateFieldGet2(_store, this).start(session, operationId)) return false;
this.update(session, operationId, change);
return true;
});
}
/**
* Settle an open operation, with a last change to its progress. Returns
* false when it had already settled.
*/
settle(session, operationId, outcome, change = {}) {
return _classPrivateFieldGet2(_store, this).transaction(() => {
const settled = _classPrivateFieldGet2(_store, this).settle(session, operationId, outcome.status === "done" ? {
status: "done",
result: { text: outcome.text }
} : outcome);
if (settled) this.update(session, operationId, change);
return settled;
});
}
/** Forget every settled operation in a session, for a reset. */
deleteSettled(session) {
_classPrivateFieldGet2(_store, this).transaction(() => {
_classPrivateFieldGet2(_store, this).deleteSettled(session);
_assertClassBrand(_OperationRecords_brand, this, _updateState).call(this, session, (state) => ({
...state,
toolCalls: {}
}));
});
}
/** The record of a started tool call, if it was started. */
toolCall(session, toolCallId) {
const { toolCalls } = _assertClassBrand(_OperationRecords_brand, this, _state).call(this, session);
if (!Object.hasOwn(toolCalls, toolCallId)) return void 0;
const call = toolCalls[toolCallId];
return call && {
session,
toolCallId,
...call
};
}
/** Record that a tool call is starting. Returns the attempt number. */
startToolCall(session, toolCallId, operationId) {
const attempts = (this.toolCall(session, toolCallId)?.attempts ?? 0) + 1;
_assertClassBrand(_OperationRecords_brand, this, _updateState).call(this, session, (state) => ({
...state,
toolCalls: {
...state.toolCalls,
[toolCallId]: {
operationId,
attempts
}
}
}));
return attempts;
}
/** Forget a tool call once its result is in the transcript. */
finishToolCall(session, toolCallId) {
_assertClassBrand(_OperationRecords_brand, this, _updateState).call(this, session, (state) => {
const { [toolCallId]: _finished, ...toolCalls } = state.toolCalls;
return {
...state,
toolCalls
};
});
}
};
function _state(session) {
return parseSessionState(_classPrivateFieldGet2(_store, this).session(session)?.state);
}
function _updateState(session, change) {
_classPrivateFieldGet2(_store, this).transaction(() => {
const stored = _classPrivateFieldGet2(_store, this).session(session);
if (!stored) return;
_classPrivateFieldGet2(_store, this).setSessionState(session, sessionStateJson(change(parseSessionState(stored.state))));
});
}
function parseJsonColumn(text) {
return JSON.parse(text);
}
/**
* Move the records an earlier version kept in its own tables into the
* store, then drop those tables, in one transaction: either every record
* moves or none does and the next start tries again. Operations keep their
* order, status and progress; sessions keep their creation time, client
* tools and started tool calls, so an operation that was in flight resumes
* and recovers as it would have.
*/
function importLegacyTables(storage, store) {
const sql = storage.sql;
const present = new Set(sql.exec(`SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (?, ?, ?)`, LEGACY_SESSIONS, LEGACY_OPERATIONS, LEGACY_TOOL_CALLS).toArray().map((row) => row.name));
if (present.size === 0) return;
storage.transactionSync(() => {
const states = /* @__PURE__ */ new Map();
if (present.has(LEGACY_SESSIONS)) {
const rows = sql.exec(`SELECT id, parent, client_tools, created_at FROM ${LEGACY_SESSIONS}
ORDER BY created_at, id`).toArray();
for (const row of rows) {
store.createSession({
id: row.id,
...row.parent !== null && { parent: row.parent },
createdAt: row.created_at
});
states.set(row.id, parseSessionState({
clientTools: row.client_tools === null ? [] : parseJsonColumn(row.client_tools),
toolCalls: {}
}));
}
}
if (present.has(LEGACY_TOOL_CALLS)) {
const rows = sql.exec(`SELECT session_id, tool_call_id, operation_id, attempts
FROM ${LEGACY_TOOL_CALLS}`).toArray();
for (const row of rows) {
const state = states.get(row.session_id) ?? EMPTY_STATE;
states.set(row.session_id, {
...state,
toolCalls: {
...state.toolCalls,
[row.tool_call_id]: {
operationId: row.operation_id,
attempts: row.attempts
}
}
});
}
}
for (const [id, state] of states) {
store.createSession({ id });
store.setSessionState(id, sessionStateJson(state));
}
if (present.has(LEGACY_OPERATIONS)) {
const rows = sql.exec(`SELECT * FROM ${LEGACY_OPERATIONS} ORDER BY seq`).toArray();
for (const row of rows) importLegacyOperation(store, row);
}
for (const table of [
LEGACY_SESSIONS,
LEGACY_OPERATIONS,
LEGACY_TOOL_CALLS
]) sql.exec(`DROP TABLE IF EXISTS ${table}`);
});
}
function importLegacyOperation(store, row) {
const session = row.session_id;
const id = row.operation_id;
if (!store.session(session)) store.createSession({
id: session,
state: sessionStateJson(EMPTY_STATE)
});
const meta = {
source: row.source === "client" ? "client" : "server",
parentId: row.parent_id,
messageId: row.message_id,
streamId: row.stream_id,
pendingModel: row.pending_model === 1,
steps: row.steps,
interruptions: row.interruptions,
overflowRetries: row.overflow_retries,
abandonReason: row.abandon_reason ?? null
};
store.deleteOperation(session, id);
store.enqueue({
session,
id,
input: parseJsonColumn(row.input),
meta: metaJson(meta)
});
switch (row.status) {
case "queued": return;
case "running":
store.start(session, id);
return;
case "done":
store.settle(session, id, {
status: "done",
result: { text: row.text ?? "" }
});
return;
case "unanswered":
store.settle(session, id, {
status: "unanswered",
reason: row.reason ?? "unanswered"
});
return;
default: throw new Error(`Unknown ThinkHarness operation status ${row.status} in ${LEGACY_OPERATIONS}`);
}
}
//#endregion
//#region src/harness/think/tools.ts
/** The model-facing copy of a tool set, restricted to `activeTools` if given. */
function modelTools(tools, activeTools) {
const active = activeTools ? new Set(activeTools) : void 0;
const offered = {};
for (const [name, tool] of Object.entries(tools)) {
if (active && !active.has(name)) continue;
if (tool.type === "provider") {
offered[name] = tool;
continue;
}
const { execute: _execute, needsApproval: _needsApproval, ...definition } = tool;
if ("recovery" in definition) delete definition.recovery;
offered[name] = definition;
}
return offered;
}
/** Whether the harness runs this tool, as opposed to a client or provider. */
function isServerTool(tools, name) {
const tool = Object.hasOwn(tools, name) ? tools[name] : void 0;
return tool !== void 0 && tool.type !== "provider" && typeof tool.execute === "function";
}
/** Whether a tool's own `needsApproval` asks for approval of this call. */
async function toolNeedsApproval(tool, input, options) {
const needsApproval = tool.needsApproval;
if (typeof needsApproval === "function") return Boolean(await needsApproval(input, {
...options,
context: void 0
}));
return needsApproval === true;
}
function isAsyncIterable(value) {
return typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
}
/**
* Run one tool's `execute`. A streaming tool's outputs before its last are
* reported to `onPreliminary`; its last output is the result.
*/
async function executeTool(tool, input, options) {
const execute = tool.execute;
if (typeof execute !== "function") return {
ok: false,
errorText: "The tool has no execute function"
};
try {
const result = await execute(input, {
toolCallId: options.toolCallId,
messages: options.messages,
abortSignal: options.abortSignal,
context: void 0
});
if (!isAsyncIterable(result)) return {
ok: true,
output: result
};
let last;
let seen = false;
for await (const output of result) {
if (seen) options.onPreliminary(last);
last = output;
seen = true;
}
return {
ok: true,
output: last
};
} catch (error) {
return {
ok: false,
errorText: error instanceof Error ? error.message : String(error)
};
}
}
//#endregion
//#region src/harness/think/turn.ts
/** Whether a part is a tool call. */
function isToolCallPart(part) {
return (part.type.startsWith("tool-") || part.type === "dynamic-tool") && "toolCallId" in part;
}
/** The tool name a tool call part carries. */
function toolNameOf(part) {
return part.type === "dynamic-tool" ? part.toolName : part.type.slice(5);
}
/** The parts of a message's last model step. */
function lastStep(message) {
let start = 0;
message.parts.forEach((part, index) => {
if (part.type === "step-start") start = index + 1;
});
return message.parts.slice(start);
}
/** Whether a tool call has its result, error or denial. */
function isSettled(part) {
return part.state === "output-available" || part.state === "output-error" || part.state === "output-denied";
}
/**
* Decide the turn's next action.
*
* @param progress - The operation's durable progress.
* @param message - The turn's assistant message, once it has one.
* @param isServerTool - Whether the harness runs a tool of this name.
* @param maxSteps - Most model calls one operation makes.
* @returns What to do next.
*/
function nextAction(progress, message, isServerTool, maxSteps) {
if (progress.streamId !== void 0) return { _tag: "recover-model" };
if (message === void 0) return progress.pendingModel ? { _tag: "call-model" } : { _tag: "end" };
const runnable = [];
let awaiting = false;
for (const part of lastStep(message)) {
if (!isToolCallPart(part) || isSettled(part) || part.providerExecuted) continue;
const server = isServerTool(toolNameOf(part));
switch (part.state) {
case "input-available":
if (server) runnable.push(part);
else awaiting = true;
break;
case "approval-responded":
if (server && part.approval.approved) runnable.push(part);
else awaiting = true;
break;
case "approval-requested":
awaiting = true;
break;
case "input-streaming": break;
}
}
if (runnable.length > 0) return {
_tag: "run-tools",
calls: runnable
};
if (awaiting) return { _tag: "await-input" };
if (progress.pendingModel && progress.steps < maxSteps) return { _tag: "call-model" };
return { _tag: "end" };
}
/** The text of a message's text parts. */
function textOf(message) {
if (!message) return "";
return message.parts.map((part) => part.type === "text" ? part.text : "").join("");
}
/**
* The partial message an interrupted model call leaves, made safe to keep:
* tool calls whose input never finished streaming are dropped, since the
* model never committed to them.
*/
function settledPartial(message) {
return {
...message,
parts: message.parts.filter((part) => !(isToolCallPart(part) && part.state === "input-streaming"))
};
}
/** Whether a message has anything besides step markers. */
function hasContent(message) {
return message.parts.some((part) => part.type !== "step-start");
}
//#endregion
//#region src/harness/think/harness.ts
/**
* Thrown by `submit()` when asked to steer a running turn, which
* ThinkHarness does not support. Submit without `whenBusy` to queue the
* input behind the running turn instead.
*
* @experimental
*/
var SteerNotSupportedError = class extends Error {
/** @param session - The session the submission was for. */
constructor(session) {
super("ThinkHarness does not support whenBusy: \"steer\". Submit without it to queue the input behind the running turn.");
this.session = session;
this._tag = "SteerNotSupportedError";
this.name = "SteerNotSupportedError";
}
};
/** The root session's id: the Sessions capability's default session. */
const ROOT_SESSION = "";
const WAKE_FN = "wake";
const HEARTBEAT_MS = 3e4;
const DEFAULT_MAX_STEPS = 10;
const DEFAULT_MAX_ATTEMPTS = 10;
const DEFAULT_BACKOFF_MS = 1e3;
const MAX_BACKOFF_MS = 6e4;
const DEFAULT_STALL_TIMEOUT_MS = 12e4;
const MAX_OVERFLOW_RETRIES = 1;
/** Ceiling for one stored segment, under the 2 MB SQLite row limit. */
const STREAM_MAX_CHUNK_BYTES = 19e5;
/** Chunks packed into one stored stream segment. */
const SEGMENT_CHUNKS = 10;
/** Raw bytes packed into one stored segment before it is written. */
const SEGMENT_MAX_BYTES = 256e3;
/** Longest a streamed chunk waits in memory before it is written. */
const FLUSH_AFTER_MS = 100;
/** Segments read at a time when rebuilding an interrupted model call. */
const READ_PAGE = 50;
const INTERRUPTED_TOOL_ERROR = "The tool call was interrupted before it finished, because the agent restarted. It may or may not have taken effect.";
function wakeJobId(session) {
return `think-wake:${session}`;
}
function sessionOfJob(payload) {
return typeof payload === "object" && payload !== null && "session" in payload && typeof payload.session === "string" ? payload.session : void 0;
}
async function resolve(value, session) {
if (typeof value === "function") return value({ session });
return value;
}
function errorText(error) {
return error instanceof Error ? error.message : String(error);
}
function isToolAnswer(input) {
return typeof input === "object" && !Array.isArray(input) && "type" in input && (input.type === "approval" || input.type === "tool-result");
}
function isUIMessage(input) {
return typeof input === "object" && !Array.isArray(input) && "role" in input && "parts" in input;
}
function harnessInputMessage(input, id) {
return {
id: input.messageId ?? id,
role: "user",
parts: input.parts.map((part) => part.type === "text" ? {
type: "text",
text: part.text
} : {
type: "file",
mediaType: part.mediaType,
url: part.url,
...part.filename !== void 0 && { filename: part.filename }
})
};
}
/** The stored form of an input. */
function operationInput(input, operationId, autoContinue) {
if (typeof input === "string") return {
kind: "messages",
messages: [{
id: operationId,
role: "user",
parts: [{
type: "text",
text: input
}]
}]
};
if (Array.isArray(input)) return {
kind: "messages",
messages: input
};
if (isToolAnswer(input)) return {
kind: "answer",
answer: input,
autoContinue
};
if (isUIMessage(input)) return {
kind: "messages",
messages: [input]
};
return {
kind: "messages",
messages: [harnessInputMessage(input, operationId)]
};
}
function toolCallAnswer(answer) {
return answer.type === "approval" ? {
type: "approval-response",
approvalId: answer.approvalId,
approved: answer.approved,
...answer.reason !== void 0 && { reason: answer.reason }
} : {
type: "tool-result",
toolCallId: answer.toolCallId,
result: answer.result
};
}
/** Replace one tool call part of a message. */
function withToolPart(message, toolCallId, update) {
return {
...message,
parts: message.parts.map((part) => isToolCallPart(part) && part.toolCallId === toolCallId ? update(part) : part)
};
}
function withOutcome(part, outcome) {
return outcome.ok ? {
...part,
state: "output-available",
output: outcome.output,
preliminary: void 0
} : {
...part,
state: "output-error",
errorText: outcome.errorText
};
}
function asUIMessages(messages) {
return messages;
}
function asUIMessage(message) {
return message;
}
function asSessionMessage(message) {
return message;
}
var _options = /* @__PURE__ */ new WeakMap();
var _events = /* @__PURE__ */ new WeakMap();
var _drives = /* @__PURE__ */ new WeakMap();
var _live = /* @__PURE__ */ new WeakMap();
var _waiters = /* @__PURE__ */ new WeakMap();
var _configured = /* @__PURE__ */ new WeakMap();
var _streams = /* @__PURE__ */ new WeakMap();
var _sessions = /* @__PURE__ */ new WeakMap();
var _bound = /* @__PURE__ */ new WeakMap();
var _ownMessages = /* @__PURE__ */ new WeakMap();
var _ownClears = /* @__PURE__ */ new WeakMap();
var _heartbeatMs = /* @__PURE__ */ new WeakMap();
var _records = /* @__PURE__ */ new WeakMap();
var _ThinkHarness_brand = /* @__PURE__ */ new WeakSet();
/**
* Think's agent loop hosted in a Durable Object, behind the same small
* harness interface as `PiHarness`: `harness.prompt()`, `harness.sessions`,
* `harness.session(id)`. It implements the shared `AgentHarness`
* interface, so a Channels host can serve its sessions.
*
* Transcripts live in a Sessions capability and in-flight model output in
* a Streams capability, both owned by the harness, and the harness's queue
* of operations in its own table. The harness runs every server tool call itself, so after an
* eviction it knows exactly which call was cut short and applies that
* tool's recovery policy.
*
* Durability is the Lifecycle's job queue, used directly: one wake job per
* session. A submission is written, then the session's wake is scheduled.
* While a session has work, the wake job reschedules itself as a heartbeat,
* so an eviction leaves it due and its alarm restarts the object. Each step
* of a turn reads only durable state, so the restarted object continues
* where the last write left off.
*
* @experimental The API may change between releases.
*/
var ThinkHarness = class extends LifecycleCapability {
/**
* @param options - The model, tools and hooks.
*/
constructor(options) {
super("think-harness");
_classPrivateMethodInitSpec(this, _ThinkHarness_brand);
_classPrivateFieldInitSpec(this, _options, void 0);
_classPrivateFieldInitSpec(this, _events, new SessionEvents());
_classPrivateFieldInitSpec(this, _drives, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _live, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _waiters, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _configured, /* @__PURE__ */ new Set());
_classPrivateFieldInitSpec(this, _streams, new Streams({ maxChunkBytes: STREAM_MAX_CHUNK_BYTES }));
_classPrivateFieldInitSpec(this, _sessions, void 0);
_classPrivateFieldInitSpec(this, _bound, false);
_classPrivateFieldInitSpec(this, _ownMessages, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _ownClears, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _heartbeatMs, HEARTBEAT_MS);
_classPrivateFieldInitSpec(this, _records, void 0);
_classPrivateFieldSet2(_options, this, options);
_classPrivateFieldSet2(_sessions, this, new Sessions({ ...options.reservedMetadataKeys && { reservedMetadataKeys: options.reservedMetadataKeys } }));
this.sessions = new ThinkSessions(this);
}
/** Create the harness's tables and wake every session with open work. */
async onStart() {
_assertClassBrand(_ThinkHarness_brand, this, _bindOwned).call(this);
await _classPrivateFieldGet2(_sessions, this).onStart();
await _classPrivateFieldGet2(_streams, this).onStart();
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
store.ensureSession("");
const sessions = new Set(store.open().map((op) => op.session));
for (const session of sessions) {
const wake = this.lifecycle.jobs.get(wakeJobId(session));
if (wake && wake.time > Date.now()) continue;
await _assertClassBrand(_ThinkHarness_brand, this, _wake).call(this, session);
}
}
/**
* The alarm memory-limit breaker sealed: the work it stopped would only
* run out of memory again. Mark the running operations to be settled
* `unanswered` with reason `"out_of_memory"` rather than run again.
* Queued operations behind them still run. Runs after the isolate's
* work unwound, so it keeps to small synchronous writes.
*/
onMemoryLimit(context) {
if (!context.sealed) return;
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
for (const op of store.open()) {
if (op.status !== "running") continue;
store.update(op.session, op.operationId, { abandonReason: "out_of_memory" });
}
}
/** Run a session's wake job. */
async onJob(context) {
if (context.job.fn !== WAKE_FN) return;
const session = sessionOfJob(context.job.payload);
if (session !== void 0) return _assertClassBrand(_ThinkHarness_brand, this, _wakeStep).call(this, session);
}
/**
* A handle on one session. No I/O until you call it.
*
* @param id - The session. Default: the root session.
*/
session(id = "") {
return new ThinkSession(this, id);
}
/** Submit to the root session and wait for the answer. */
prompt(input, options = {}) {
return this.session().prompt(input, options);
}
/** Durably submit to the root session. Resolves before the model runs. */
submit(input, options = {}) {
return this.session().submit(input, options);
}
/** Operations not settled yet, oldest first, in one session or all. */
async pending(session) {
await this.lifecycle.ready();
return _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session).map((op) => ({
operationId: op.operationId,
session: op.session,
status: op.status === "queued" ? "queued" : "running"
}));
}
/** @internal */
async enqueue(session, input, options) {
if (options.whenBusy === "steer") throw new SteerNotSupportedError(session);
await this.lifecycle.ready();
const operationId = options.operationId ?? crypto.randomUUID();
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
let accepted = false;
this.lifecycle.storage.transactionSync(() => {
store.ensureSession(session);
if (options.clientTools !== void 0) store.setClientTools(session, options.clientTools);
accepted = store.insert(session, operationId, input, options.source ?? "server");
});
if (accepted) _classPrivateFieldGet2(_events, this).emit(session, {
type: "operation",
status: {
operationId,
status: "queued"
}
});
await _assertClassBrand(_ThinkHarness_brand, this, _wake).call(this, session);
_assertClassBrand(_ThinkHarness_brand, this, _ensureDrive).call(this, session);
return {
operationId,
session,
accepted
};
}
/** @internal Stop one operation, or everything open in a session. */
async abort(session, operationId) {
await this.lifecycle.ready();
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
const targets = operationId === void 0 ? store.open(session) : [store.get(session, operationId)].filter((op) => op !== void 0 && (op.status === "queued" || op.status === "running"));
for (const op of targets) {
const live = _classPrivateFieldGet2(_live, this).get(session);
if (live?.operationId === op.operationId) live.abort.abort();
else if (op.status === "queued") {
store.settle(session, op.operationId, {
status: "unanswered",
reason: "withdrawn"
});
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, op.operationId);
} else await _assertClassBrand(_ThinkHarness_brand, this, _abandon).call(this, op, "aborted");
}
return targets.length > 0;
}
/** @internal Wait for an operation to settle. */
async wait(session, operationId, signal) {
await this.lifecycle.ready();
for (;;) {
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
if (!op) return {
operationId,
session,
status: "unanswered",
reason: "not_found"
};
if (op.status === "done" || op.status === "unanswered") return _assertClassBrand(_ThinkHarness_brand, this, _result).call(this, op);
await new Promise((resolveWait, reject) => {
if (signal?.aborted) {
reject(signal.reason);
return;
}
const key = waiterKey(session, operationId);
const waiters = _classPrivateFieldGet2(_waiters, this).get(key) ?? /* @__PURE__ */ new Set();
_classPrivateFieldGet2(_waiters, this).set(key, waiters);
const done = () => {
signal?.removeEventListener("abort", aborted);
resolveWait();
};
const aborted = () => {
waiters.delete(done);
reject(signal?.reason);
};
waiters.add(done);
signal?.addEventListener("abort", aborted, { once: true });
});
}
}
/** @internal */
async inspect(session, operationId) {
await this.lifecycle.ready();
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
if (!op) return void 0;
switch (op.status) {
case "queued":
case "running": return {
operationId,
session,
status: op.status
};
case "done":
case "unanswered": return _assertClassBrand(_ThinkHarness_brand, this, _result).call(this, op);
}
}
/** @internal The active transcript of a session. */
async messages(session) {
return asUIMessages(await _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).getHistory());
}
/** @internal Abort everything in a session and clear its transcript. */
async reset(session, handoff) {
await this.abort(session);
await _classPrivateFieldGet2(_drives, this).get(session);
const handle = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session);
_classPrivateFieldGet2(_ownClears, this).set(session, (_classPrivateFieldGet2(_ownClears, this).get(session) ?? 0) + 1);
try {
await handle.clearMessages();
} finally {
const left = (_classPrivateFieldGet2(_ownClears, this).get(session) ?? 1) - 1;
if (left === 0) _classPrivateFieldGet2(_ownClears, this).delete(session);
else _classPrivateFieldGet2(_ownClears, this).set(session, left);
}
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).deleteSettled(session);
_classPrivateFieldGet2(_events, this).emit(session, { type: "reset" });
if (handoff) {
const note = {
id: crypto.randomUUID(),
role: "system",
parts: [{
type: "text",
text: handoff
}]
};
const release = _assertClassBrand(_ThinkHarness_brand, this, _markOwn).call(this, session, [note.id]);
try {
await handle.appendMessage(asSessionMessage(note));
} finally {
release();
}
_classPrivateFieldGet2(_events, this).emit(session, {
type: "message",
message: note
});
}
}
/** @internal */
subscribe(session, listener) {
return _classPrivateFieldGet2(_events, this).subscribe(session, listener);
}
/** @internal The shared harness interface's watch. */
async watch(session) {
await this.lifecycle.ready();
let watcher;
const early = [];
const unsubscribe = _classPrivateFieldGet2(_events, this).subscribe(session, (event) => {
const events = toSessionEvents(event);
if (watcher) watcher.push(events);
else early.push(events);
});
const live = _classPrivateFieldGet2(_live, this).get(session);
const partial = live ? [...live.partial] : _assertClassBrand(_ThinkHarness_brand, this, _recoverableChunks).call(this, session);
const messages = await this.messages(session);
const open = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session);
const running = open.find((op) => op.status === "running");
watcher = new Watcher({
messages: messages.map((message) => toTranscriptMessage(message)),
pending: open.map((op) => ({
operationId: op.operationId,
status: op.status === "queued" ? "queued" : "placed"
})),
...running && { run: {
operations: [running.operationId],
...partial.length > 0 && { partial: partial.flatMap((chunk) => {
const converted = toResponseChunk(chunk, {});
return converted ? [converted] : [];
}) }
} }
}, unsubscribe);
for (const events of early) watcher.push(events);
return watcher;
}
/** @internal The running operation and its model call's chunks so far. */
async inFlight(session) {
await this.lifecycle.ready();
const running = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session).find((op) => op.status === "running");
if (!running) return void 0;
const live = _classPrivateFieldGet2(_live, this).get(session);
if (live?.operationId === running.operationId) return {
operationId: running.operationId,
continuation: live.continues,
chunks: [...live.partial]
};
return {
operationId: running.operationId,
continuation: await _assertClassBrand(_ThinkHarness_brand, this, _message).call(this, running) !== void 0,
chunks: running.streamId ? _assertClassBrand(_ThinkHarness_brand, this, _streamChunks).call(this, running.streamId) : []
};
}
/**
* Listen to every session's events, for a host that serves several
* sessions over one set of connections. Returns the unsubscribe.
*/
observe(listener) {
return _classPrivateFieldGet2(_events, this).observe(listener);
}
/** @internal */
async createSession(parent) {
await this.lifecycle.ready();
const id = crypto.randomUUID();
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).ensureSession(id, parent);
return this.session(id);
}
/** @internal Copy a session's active path into a new session. */
async fork(from) {
const source = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, from);
const rows = await source.getHistoryRowStats();
const fork = await this.createSession(from);
const target = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, fork.id);
let parentId = null;
for (const row of rows) {
const message = await source.getMessage(row.id);
if (!message) continue;
await target.appendMessage(message, { parentId });
parentId = message.id;
}
return fork;
}
/** @internal */
async listSessions() {
await this.lifecycle.ready();
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
const busy = new Set(store.open().map((op) => op.session));
return store.sessions().map((record) => ({
id: record.id,
...record.parent !== void 0 && { parent: record.parent },
busy: busy.has(record.id)
}));
}
/** @internal The answers to a message: its children in the tree. */
async branches(session, messageId) {
return asUIMessages(await _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).getBranches(messageId));
}
/** @internal Full-text search over a session's messages. */
search(session, query, options) {
return _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).search(query, options?.limit === void 0 ? void 0 : { limit: options.limit });
}
/** @internal Compact a session with its compaction function. */
async compact(session) {
return _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).compact();
}
};
/** Schedule the session's wake job, now or at `time`. */
function _wake(session, time = Date.now()) {
return this.lifecycle.jobs.push({
id: wakeJobId(session),
fn: WAKE_FN,
time,
payload: { session },
singleflight: true,
recoveryLoop: true
});
}
/**
* One run of a session's wake job. With work open, it makes sure a drive
* is running and keeps a heartbeat; with none, it completes.
*/
function _wakeStep(session) {
const heartbeat = { rescheduleAt: Date.now() + _classPrivateFieldGet2(_heartbeatMs, this) };
if (_classPrivateFieldGet2(_drives, this).has(session)) return heartbeat;
if (_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session).length === 0) return void 0;
const drive = _assertClassBrand(_ThinkHarness_brand, this, _ensureDrive).call(this, session);
this.lifecycle.trackAlarmWork(drive);
return heartbeat;
}
/** Run the session's open operations in order, unless already running. */
function _ensureDrive(session) {
const running = _classPrivateFieldGet2(_drives, this).get(session);
if (running) return running;
let wakeAt;
const drive = (async () => {
try {
await this.lifecycle.runInHostContext(async () => {
for (;;) {
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session)[0];
if (!op) return;
const outcome = await _assertClassBrand(_ThinkHarness_brand, this, _run).call(this, op);
if (outcome.retryAt !== void 0) {
wakeAt = outcome.retryAt;
return;
}
}
});
} catch (error) {
console.error("ThinkHarness drive failed", error);
wakeAt = Date.now() + _classPrivateFieldGet2(_heartbeatMs, this);
} finally {
_classPrivateFieldGet2(_drives, this).delete(session);
}
await _assertClassBrand(_ThinkHarness_brand, this, _wake).call(this, session, wakeAt);
})();
_classPrivateFieldGet2(_drives, this).set(session, drive);
return drive;
}
async function _run(first) {
const { session, operationId } = first;
const live = {
operationId,
abort: new AbortController(),
partial: [],
continues: false
};
_classPrivateFieldGet2(_live, this).set(session, live);
try {
let op = first;
if (op.status === "queued") {
op = await _assertClassBrand(_ThinkHarness_brand, this, _place).call(this, op);
if (!op) return {};
}
const continuation = await _assertClassBrand(_ThinkHarness_brand, this, _message).call(this, op) !== void 0;
_classPrivateFieldGet2(_events, this).emit(session, {
type: "run-start",
operationId,
continuation
});
try {
return await _assertClassBrand(_ThinkHarness_brand, this, _steps).call(this, session, operationId, live);
} finally {
_classPrivateFieldGet2(_events, this).emit(session, {
type: "run-end",
operationId
});
}
} catch (error) {
console.error(`ThinkHarness operation ${operationId} failed`, error);
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
if (op && (op.status === "running" || op.status === "queued")) await _assertClassBrand(_ThinkHarness_brand, this, _abandon).call(this, op, errorText(error));
return {};
} finally {
_classPrivateFieldGet2(_live, this).delete(session);
}
}
async function _steps(session, operationId, live) {
const maxSteps = _classPrivateFieldGet2(_options, this).maxSteps ?? DEFAULT_MAX_STEPS;
for (;;) {
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
if (!op || op.status !== "running") return {};
if (live.abort.signal.aborted) {
await _assertClassBrand(_ThinkHarness_brand, this, _abandon).call(this, op, "aborted");
return {};
}
if (op.abandonReason !== void 0) {
await _assertClassBrand(_ThinkHarness_brand, this, _abandon).call(this, op, op.abandonReason);
return {};
}
const message = await _assertClassBrand(_ThinkHarness_brand, this, _message).call(this, op);
const tools = await _assertClassBrand(_ThinkHarness_brand, this, _tools).call(this, session);
const action = nextAction(op, message, (name) => isServerTool(tools, name), maxSteps);
switch (action._tag) {
case "recover-model": {
const outcome = await _assertClassBrand(_ThinkHarness_brand, this, _recoverModel).call(this, op, message);
if (outcome) return outcome;
break;
}
case "call-model": {
const outcome = await _assertClassBrand(_ThinkHarness_brand, this, _callModel).call(this, op, message, tools, live);
if (outcome) return outcome;
break;
}
case "run-tools":
if (!message) return {};
await _assertClassBrand(_ThinkHarness_brand, this, _runTools).call(this, op, message, action.calls, tools, live);
break;
case "await-input":
case "end":
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, () => {
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
status: "done",
text: textOf(message)
});
});
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, message);
return {};
}
}
}
/**
* Place a queued operation: write what it brings to the transcript and
* mark it running, in one transaction. Returns the running operation, or
* undefined when placing settled it.
*/
async function _place(op) {
const { session, operationId } = op;
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
const handle = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session);
const input = op.input;
const placed = [];
let settledText;
let unanswered;
const running = (change) => store.start(session, operationId, {
parentId: change.parentId ?? null,
messageId: change.messageId,
pendingModel: true
});
switch (input.kind) {
case "messages": {
let messages = input.messages;
if (op.source === "client") {
if (messages.some((message) => message.role !== "user")) {
unanswered = "client_role";
break;
}
const fresh = [];
for (const message of messages) if (!await handle.getMessage(message.id)) fresh.push(message);
messages = fresh;
}
const last = messages.at(-1);
if (!last) {
unanswered = "empty";
break;
}
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, (put) => {
let parentId;
for (const message of messages) {
put(message, {
parentId,
source: op.source
});
parentId = message.id;
placed.push(message);
}
running({
parentId: last.id,
messageId: crypto.randomUUID()
});
});
break;
}
case "answer": {
const history = asUIMessages(await handle.getHistory());
const tools = await _assertClassBrand(_ThinkHarness_brand, this, _tools).call(this, session);
const answer = input.answer;
if (answer.type === "tool-result" && op.source === "client") {
const call = history.flatMap((message) => message.parts).find((part) => isToolCallPart(part) && part.toolCallId === answer.toolCallId);
if (call && isServerTool(tools, toolNameOf(call))) {
unanswered = "not_client_tool";
break;
}
}
const answered = answerToolCall(history, toolCallAnswer(answer));
if (!answered) {
unanswered = "not_waiting";
break;
}
const latest = history.at(-1);
const continues = input.autoContinue && latest?.id === answered.id && nextAction({
streamId: void 0,
pendingModel: true,
steps: 0
}, answered, (name) => isServerTool(tools, name), Number.POSITIVE_INFINITY)._tag !== "await-input";
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, (put) => {
put(answered);
placed.push(answered);
if (continues) running({ messageId: answered.id });
else store.settle(session, operationId, {
status: "done",
text: textOf(answered)
});
});
if (!continues) settledText = textOf(answered);
break;
}
case "regenerate": {
const history = asUIMessages(await handle.getHistory());
const target = input.messageId ?? [...history].reverse().find((message) => message.role === "user")?.id;
if (!target || !await handle.getMessage(target)) {
unanswered = "not_found";
break;
}
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, () => running({
parentId: target,
messageId: crypto.randomUUID()
}));
break;
}
case "continue": {
const latest = await handle.getLatestLeaf();
if (!latest) {
unanswered = "not_found";
break;
}
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, () => latest.role === "assistant" ? running({ messageId: latest.id }) : running({
parentId: latest.id,
messageId: crypto.randomUUID()
}));
break;
}
}
for (const message of placed) _classPrivateFieldGet2(_events, this).emit(session, {
type: "message",
message,
operationId
});
if (unanswered !== void 0) {
store.settle(session, operationId, {
status: "unanswered",
reason: unanswered
});
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId);
return;
}
if (settledText !== void 0) {
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId);
return;
}
_classPrivateFieldGet2(_events, this).emit(session, {
type: "operation",
status: {
operationId,
status: "placed"
}
});
return store.get(session, operationId);
}
/**
* One model call. Its chunks go to a stream as they arrive; when it ends,
* the message is persisted and the stream discarded in one transaction.
* Returns an outcome only when the operation must wait.
*/
async function _callModel(op, existing, tools, live) {
const { session, operationId } = op;
const messageId = op.messageId ?? crypto.randomUUID();
const hooks = _classPrivateFieldGet2(_options, this).hooks;
const handle = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session);
const history = asUIMessages(await handle.getHistory({ leafId: existing ? existing.id : op.parentId }));
const system = await resolve(_classPrivateFieldGet2(_options, this).system, session);
const config = await hooks?.beforeTurn?.({
session,
operationId,
step: op.steps,
continuation: existing !== void 0,
messages: history,
system,
tools: Object.keys(tools)
}) ?? {};
const model = config.model ?? await resolve(_classPrivateFieldGet2(_options, this).model, session);
if (model === void 0) throw new Error("ThinkHarness has no model");
const offered = modelTools(tools, config.activeTools);
const effectiveSystem = config.system ?? system;
const messages = config.messages ?? await convertToModelMessages(history, {
tools,
ignoreIncompleteToolCalls: true
});
const streamId = `think:${crypto.randomUUID()}`;
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).update(session, operationId, {
streamId,
messageId
});
const writer = await _classPrivateFieldGet2(_streams, this).open(streamId, {
tag: streamTag(session, operationId),
metadata: {
session,
operationId,
messageId
}
});
const stall = new AbortController();
const signal = AbortSignal.any([live.abort.signal, stall.signal]);
let modelError;
let final;
let finishReason;
let caught;
live.partial = [];
live.continues = existing !== void 0;
let segment = [];
let segmentBytes = 0;
let flushTimer;
const flush = () => {
if (flushTimer !== void 0) clearTimeout(flushTimer);
flushTimer = void 0;
if (segment.length === 0) return;
writer.append(segment);
segment = [];
segmentBytes = 0;
};
try {
const stream = toUIMessageStream({
stream: streamText({
model,
...effectiveSystem !== void 0 && { system: effectiveSystem },
messages,
tools: offered,
...config.toolChoice !== void 0 && { toolChoice: config.toolChoice },
...config.providerOptions !== void 0 && { providerOptions: config.providerOptions },
...config.maxOutputTokens !== void 0 && { maxOutputTokens: config.maxOutputTokens },
...config.temperature !== void 0 && { temperature: config.temperature },
stopWhen: stepCountIs(1),
abortSignal: signal,
onError: ({ error }) => {
modelError ??= error;
}
}).stream,
tools: offered,
originalMessages: existing ? history : [],
generateMessageId: () => messageId,
onEnd: ({ responseMessage }) => {
final = responseMessage;
}
});
const stallTimeoutMs = _classPrivateFieldGet2(_options, this).recovery?.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
for await (const chunk of untilAborted(iterateWithStallWatchdog(stream, stallTimeoutMs, () => stall.abort(new ChatStreamStalledError("Model stream stalled"))), live.abort.signal)) {
if (chunk.type === "finish") finishReason = chunk.finishReason;
live.partial.push(chunk);
segment.push(chunk);
segmentBytes += JSON.stringify(chunk).length;
if (segment.length >= SEGMENT_CHUNKS || segmentBytes >= SEGMENT_MAX_BYTES) flush();
else flushTimer ??= setTimeout(flush, FLUSH_AFTER_MS);
_classPrivateFieldGet2(_events, this).emit(session, {
type: "chunk",
operationId,
chunk
});
if (hooks?.onChunk) try {
hooks.onChunk({
session,
operationId,
chunk
});
} catch (error) {
console.error("ThinkHarness onChunk hook failed", error);
}
}
} catch (error) {
caught = error;
}
flush();
const message = final ?? accumulate(messageId, existing, live.partial);
const partial = message ? settledPartial(message) : void 0;
const keep = partial && hasContent(partial) ? partial : void 0;
const stalled = caught instanceof ChatStreamStalledError || stall.signal.aborted;
const failure = stalled ? void 0 : modelError ?? caught;
if (live.abort.signal.aborted) {
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
if (keep) put(keep, { parentId: op.parentId });
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
status: "unanswered",
reason: "aborted"
}, { streamId: null });
});
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, keep);
return {};
}
if (stalled || failure !== void 0) {
if (failure !== void 0) try {
await hooks?.onError?.(failure, {
session,
operationId
});
} catch (error) {
console.error("ThinkHarness onError hook failed", error);
}
const kind = stalled ? "retry" : hooks?.classifyError?.(failure) ?? "fail";
if (kind === "context-overflow" && op.overflowRetries < MAX_OVERFLOW_RETRIES) {
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
if (keep) put(keep, { parentId: op.parentId });
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).update(session, operationId, {
streamId: null,
overflowRetries: op.overflowRetries + 1
});
});
try {
await handle.compact();
} catch (error) {
console.warn("ThinkHarness could not compact the session", error);
}
return;
}
if (kind === "retry") return _assertClassBrand(_ThinkHarness_brand, this, _interrupted).call(this, op, streamId, keep);
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
if (keep) put(keep, { parentId: op.parentId });
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
status: "unanswered",
reason: errorText(failure)
}, { streamId: null });
});
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, keep);
return {};
}
if (!message) throw new Error("The model call produced no message");
const toolCalls = lastStep(message).some((part) => isToolCallPart(part) && !part.providerExecuted);
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
put(message, { parentId: op.parentId });
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).update(session, operationId, {
streamId: null,
steps: op.steps + 1,
interruptions: 0,
pendingModel: toolCalls
});
});
live.partial = [];
live.continues = true;
_classPrivateFieldGet2(_events, this).emit(session, {
type: "message",
message,
operationId
});
await hooks?.onStepFinish?.({
session,
operationId,
step: op.steps,
message,
finishReason
});
}
/**
* A model call an eviction cut short: keep what it produced, then call
* the model again to continue the same message, within the budget.
*/
async function _recoverModel(op, existing) {
const streamId = op.streamId;
if (streamId === void 0) return void 0;
const chunks = _assertClassBrand(_ThinkHarness_brand, this, _streamChunks).call(this, streamId);
const rebuilt = chunks.length > 0 ? accumulate(op.messageId ?? crypto.randomUUID(), existing, chunks) : void 0;
const partial = rebuilt ? settledPartial(rebuilt) : void 0;
const keep = partial && hasContent(partial) ? partial : void 0;
return _assertClassBrand(_ThinkHarness_brand, this, _interrupted).call(this, op, streamId, keep);
}
/** Count an interruption, keep the partial, and decide whether to go on. */
async function _interrupted(op, streamId, partial) {
const { session, operationId } = op;
const recovery = _classPrivateFieldGet2(_options, this).recovery;
const attempt = op.interruptions + 1;
let decision = attempt > (recovery?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS) ? "abandon" : "continue";
if (decision === "continue" && _classPrivateFieldGet2(_options, this).hooks?.onRecovery) decision = await _classPrivateFieldGet2(_options, this).hooks.onRecovery({
session,
operationId,
attempt,
partial
});
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
if (partial) put(partial, { parentId: op.parentId });
const progress = {
streamId: null,
interruptions: attempt
};
if (decision === "abandon") _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
status: "unanswered",
reason: "interrupted"
}, progress);
else _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).update(session, operationId, progress);
});
if (partial) _classPrivateFieldGet2(_events, this).emit(session, {
type: "message",
message: partial,
operationId
});
if (decision === "abandon") {
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, partial);
return {};
}
const delay = backoff(attempt, recovery?.backoffMs ?? DEFAULT_BACKOFF_MS);
return delay > 0 ? { retryAt: Date.now() + delay } : void 0;
}
async function _runTools(op, message, calls, tools, live) {
const { session, operationId } = op;
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
const hooks = _classPrivateFieldGet2(_options, this).hooks;
const modelMessages = await convertToModelMessages(asUIMessages(await _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).getHistory({ leafId: message.id })).slice(0, -1), {
tools,
ignoreIncompleteToolCalls: true
});
let current = message;
const toRun = [];
const approvals = [];
for (const call of calls) {
const toolName = toolNameOf(call);
const tool = tools[toolName];
if (!tool) continue;
if (call.state === "input-available") {
if (await _classPrivateFieldGet2(_options, this).toolApproval?.({
session,
toolName,
toolCallId: call.toolCallId,
input: call.input
}) || await toolNeedsApproval(tool, call.input, {
toolCallId: call.toolCallId,
messages: modelMessages
})) {
approvals.push(call.toolCallId);
continue;
}
}
toRun.push(call);
}
if (approvals.length > 0) {
for (const toolCallId of approvals) current = withToolPart(current, toolCallId, (part) => ({
...part,
state: "approval-requested",
approval: { id: `approval:${toolCallId}` }
}));
const snapshot = current;
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, (put) => put(snapshot));
_classPrivateFieldGet2(_events, this).emit(session, {
type: "message",
message: snapshot,
operationId
});
}
await Promise.all(toRun.map(async (call) => {
const toolName = toolNameOf(call);
const tool = tools[toolName];
if (!tool) return;
const { toolCallId } = call;
const started = store.toolCall(session, toolCallId);
let outcome;
if (started) {
const policy = toolRecovery(tool);
const maxAttempts = _classPrivateFieldGet2(_options, this).recovery?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
if (policy === "report" || started.attempts >= maxAttempts) outcome = {
ok: false,
errorText: INTERRUPTED_TOOL_ERROR
};
}
let attempt = started?.attempts ?? 0;
let input = call.input;
if (!outcome) {
attempt = store.startToolCall(session, toolCallId, operationId);
const decision = await hooks?.beforeToolCall?.({
session,
operationId,
toolCallId,
toolName,
input,
attempt
});
if (decision?.action === "block") outcome = {
ok: false,
errorText: decision.reason
};
else if (decision?.action === "substitute") outcome = {
ok: true,
output: decision.output
};
else {
if (decision?.action === "run" && decision.input !== void 0) input = decision.input;
outcome = await executeTool(tool, input, {
toolCallId,
messages: modelMessages,
abortSignal: live.abort.signal,
onPreliminary: (output) => _classPrivateFieldGet2(_events, this).emit(session, {
type: "chunk",
operationId,
chunk: {
type: "tool-output-available",
toolCallId,
output,
preliminary: true
}
})
});
}
}
const settled = outcome;
await hooks?.afterToolCall?.({
session,
operationId,
toolCallId,
toolName,
input,
attempt,
...settled
});
current = withToolPart(current, toolCallId, (part) => withOutcome(part, settled));
const snapshot = current;
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, (put) => {
put(snapshot);
store.finishToolCall(session, toolCallId);
store.update(session, operationId, { interruptions: 0 });
});
_classPrivateFieldGet2(_events, this).emit(session, {
type: "chunk",
operationId,
chunk: settled.ok ? {
type: "tool-output-available",
toolCallId,
output: settled.output
} : {
type: "tool-output-error",
toolCallId,
errorText: settled.errorText
}
});
_classPrivateFieldGet2(_events, this).emit(session, {
type: "message",
message: snapshot,
operationId
});
}));
}
/**
* End an operation that cannot run on: keep what its interrupted model
* call produced, mark any tool call it left open as failed, and settle
* it `unanswered`.
*/
async function _abandon(op, reason) {
const { session, operationId } = op;
const existing = await _assertClassBrand(_ThinkHarness_brand, this, _message).call(this, op);
let message = existing;
const chunks = op.streamId === void 0 ? [] : _assertClassBrand(_ThinkHarness_brand, this, _streamChunks).call(this, op.streamId);
if (chunks.length > 0) {
const rebuilt = accumulate(op.messageId ?? crypto.randomUUID(), existing, chunks);
if (rebuilt) message = settledPartial(rebuilt);
}
if (message) {
for (const part of lastStep(message)) if (isToolCallPart(part) && !part.providerExecuted && (part.state === "input-available" || part.state === "approval-responded")) message = withToolPart(message, part.toolCallId, (call) => withOutcome(call, {
ok: false,
errorText: reason
}));
}
const keep = message && hasContent(message) && message !== existing ? message : void 0;
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, op.streamId, (put) => {
if (keep) put(keep, { parentId: op.parentId });
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
status: "unanswered",
reason
}, { streamId: null });
});
if (keep) _classPrivateFieldGet2(_events, this).emit(session, {
type: "message",
message: keep,
operationId
});
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, keep ?? existing);
}
/** Tell everyone an operation settled. Its row is already written. */
async function _settled(session, operationId, message) {
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
if (!op) return;
const result = _assertClassBrand(_ThinkHarness_brand, this, _result).call(this, op);
_classPrivateFieldGet2(_events, this).emit(session, {
type: "operation",
status: result.status === "done" ? {
operationId,
status: "done",
...result.text !== void 0 && { text: result.text }
} : {
operationId,
status: "unanswered",
...result.reason !== void 0 && { reason: result.reason }
}
});
const key = waiterKey(session, operationId);
for (const waiter of _classPrivateFieldGet2(_waiters, this).get(key) ?? []) waiter();
_classPrivateFieldGet2(_waiters, this).delete(key);
try {
await _classPrivateFieldGet2(_options, this).hooks?.onTurnEnd?.({
...result,
...message && { message }
});
} catch (error) {
console.error("ThinkHarness onTurnEnd hook failed", error);
}
}
function _result(op) {
const { operationId, session } = op;
return op.status === "done" ? {
operationId,
session,
status: "done",
text: op.text ?? "",
...op.messageId !== void 0 && { messageId: op.messageId }
} : {
operationId,
session,
status: "unanswered",
...op.reason !== void 0 && { reason: op.reason }
};
}
function _tables() {
_classPrivateFieldGet2(_records, this) ?? _classPrivateFieldSet2(_records, this, new OperationRecords(this.lifecycle.storage));
return _classPrivateFieldGet2(_records, this);
}
/** The Sessions handle for a session, configured on first use. */
/**
* Give the Sessions and Streams the harness owns this capability's
* Lifecycle services. Done on first use, not only in `onStart`, because an
* RPC can read a transcript before startup (the read itself then waits
* for startup).
*/
function _bindOwned() {
if (_classPrivateFieldGet2(_bound, this)) return;
bindLifecycleCapability(_classPrivateFieldGet2(_sessions, this), this.lifecycle);
bindLifecycleCapability(_classPrivateFieldGet2(_streams, this), this.lifecycle);
_classPrivateFieldSet2(_bound, this, true);
_classPrivateFieldGet2(_sessions, this).subscribe((change) => {
const session = change.sessionId;
switch (change.type) {
case "append":
case "update":
case "import":
if (_assertClassBrand(_ThinkHarness_brand, this, _takeOwn).call(this, session, change.message.id)) return;
_classPrivateFieldGet2(_events, this).emit(session, {
type: "message",
message: asUIMessage(change.message)
});
return;
case "clear":
if ((_classPrivateFieldGet2(_ownClears, this).get(session) ?? 0) > 0) return;
_classPrivateFieldGet2(_events, this).emit(session, { type: "reset" });
return;
case "delete":
case "compact":
case "compaction":
_classPrivateFieldGet2(_events, this).emit(session, { type: "transcript" });
return;
}
});
}
/** Note messages the harness is about to write; returns the release. */
function _markOwn(session, ids) {
let own = _classPrivateFieldGet2(_ownMessages, this).get(session);
if (!own) {
own = /* @__PURE__ */ new Map();
_classPrivateFieldGet2(_ownMessages, this).set(session, own);
}
for (const id of ids) own.set(id, (own.get(id) ?? 0) + 1);
return () => {
const current = _classPrivateFieldGet2(_ownMessages, this).get(session);
if (!current) return;
for (const id of ids) {
const left = (current.get(id) ?? 1) - 1;
if (left <= 0) current.delete(id);
else current.set(id, left);
}
if (current.size === 0) _classPrivateFieldGet2(_ownMessages, this).delete(session);
};
}
/** Whether a change is one of the harness's own writes; consumes the mark. */
function _takeOwn(session, id) {
const own = _classPrivateFieldGet2(_ownMessages, this).get(session);
const count = own?.get(id);
if (!own || count === void 0) return false;
if (count <= 1) own.delete(id);
else own.set(id, count - 1);
if (own.size === 0) _classPrivateFieldGet2(_ownMessages, this).delete(session);
return true;
}
function _handle$1(session) {
_assertClassBrand(_ThinkHarness_brand, this, _bindOwned).call(this);
const handle = _classPrivateFieldGet2(_sessions, this).session(session);
if (!_classPrivateFieldGet2(_configured, this).has(session)) {
_classPrivateFieldGet2(_configured, this).add(session);
_classPrivateFieldGet2(_options, this).configureSession?.(handle, session);
}
return handle;
}
async function _message(op) {
if (op.messageId === void 0) return void 0;
const message = await _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, op.session).getMessage(op.messageId);
return message ? asUIMessages([message])[0] : void 0;
}
async function _tools(session) {
const tools = await resolve(_classPrivateFieldGet2(_options, this).tools, session) ?? {};
const client = createToolsFromClientSchemas(_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).clientTools(session));
for (const name of Object.keys(client)) if (Object.hasOwn(tools, name)) console.warn(`ThinkHarness ignored a client tool named "${name}": a server tool has that name`);
return {
...client,
...tools
};
}
/**
* Persist messages and operation changes in one transaction; with a
* stream id, the transaction also settles and discards that stream (the
* cutover), so an eviction leaves either the stream or the message.
*/
async function _commit(session, streamId, writes) {
const sync = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).__DO_NOT_USE_WILL_BREAK__sync();
const afters = [];
const ids = [];
const commit = () => {
afters.length = 0;
ids.length = 0;
writes((message, options = {}) => {
ids.push(message.id);
afters.push(sync.upsert(asSessionMessage(message), {
...options.parentId !== void 0 && { parentId: options.parentId },
source: options.source ?? "server"
}).after);
});
};
try {
if (!(streamId !== void 0 && _classPrivateFieldGet2(_streams, this).__DO_NOT_USE_WILL_BREAK__sync().settle(streamId, "completed", null, {
commit,
discard: true
}))) this.lifecycle.storage.transactionSync(commit);
} catch (error) {
sync.abandon();
throw error;
}
const release = _assertClassBrand(_ThinkHarness_brand, this, _markOwn).call(this, session, ids);
try {
for (const after of afters) await after();
} finally {
release();
}
}
/** Every chunk a stream holds, without waiting for more. */
function _streamChunks(streamId) {
const streams = _classPrivateFieldGet2(_streams, this).__DO_NOT_USE_WILL_BREAK__sync();
if (!streams.getStream(streamId)) return [];
const chunks = [];
let from = 0;
for (;;) {
const rows = streams.readChunks(streamId, from, READ_PAGE);
for (const row of rows) {
const segment = JSON.parse(row.chunk);
chunks.push(...segment);
}
if (rows.length < READ_PAGE) return chunks;
from = (rows.at(-1)?.seq ?? from) + 1;
}
}
/** Chunks an evicted model call left in its stream, for a late watcher. */
function _recoverableChunks(session) {
const running = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session).find((op) => op.status === "running");
return running?.streamId ? _assertClassBrand(_ThinkHarness_brand, this, _streamChunks).call(this, running.streamId) : [];
}
/**
* Rebuild a message from the chunks of a model call, applied on top of the
* message as it was before the call.
*/
function accumulate(messageId, existing, chunks) {
if (chunks.length === 0) return existing;
const accumulator = new StreamAccumulator({
messageId: existing?.id ?? messageId,
continuation: existing !== void 0,
...existing && {
existingParts: existing.parts,
...isRecord(existing.metadata) && { existingMetadata: existing.metadata }
}
});
for (const chunk of chunks) accumulator.applyChunk(chunk);
return {
...accumulator.toMessage(),
role: "assistant"
};
}
/**
* Stop iterating as soon as `signal` aborts, even if the source never
* notices: a model stream that ignores its abort signal must not hold an
* aborted turn open.
*/
async function* untilAborted(source, signal) {
const iterator = source[Symbol.asyncIterator]();
let onAbort = () => {};
const aborted = new Promise((resolveAbort) => {
onAbort = () => resolveAbort("aborted");
if (signal.aborted) onAbort();
else signal.addEventListener("abort", onAbort, { once: true });
});
try {
for (;;) {
const next = await Promise.race([iterator.next(), aborted]);
if (next === "aborted") {
iterator.return?.().catch(() => {});
return;
}
if (next.done) return;
yield next.value;
}
} finally {
signal.removeEventListener("abort", onAbort);
}
}
/**
* Classify the context-window overflow errors of common providers
* (Anthropic, OpenAI, Google, Bedrock, Mistral and others) as
* `"context-overflow"`, and everything else as `"fail"`. Pass it as
* `hooks.classifyError`, or call it from your own classifier.
*
* @param error - The model call's error.
* @returns `"context-overflow"` or `"fail"`.
* @experimental
*/
function classifyContextOverflow(error) {
let text;
if (error instanceof Error) text = error.message;
else if (typeof error === "string") text = error;
else try {
text = JSON.stringify(error);
} catch {
text = String(error);
}
return CONTEXT_OVERFLOW_PATTERN.test(text) ? "context-overflow" : "fail";
}
const CONTEXT_OVERFLOW_PATTERN = /prompt is too long|context[_ ]length[_ ]exceeded|maximum context length|exceeds the maximum number of tokens|input token count|reduce the length of|input is too long|too many (?:input )?tokens|context window/i;
/** A tool's own recovery field, or the default. */
function toolRecovery(tool) {
return "recovery" in tool && tool.recovery === "rerun" ? "rerun" : "report";
}
function isRecord(value) {
return typeof value === "object" && value !== null;
}
function backoff(attempt, baseMs) {
if (attempt <= 1) return 0;
return Math.min(baseMs * 2 ** (attempt - 2), MAX_BACKOFF_MS);
}
function streamTag(session, operationId) {
return `think:${JSON.stringify([session, operationId])}`;
}
function waiterKey(session, operationId) {
return JSON.stringify([session, operationId]);
}
/** Project a session event onto the shared harness interface's events. */
function toSessionEvents(event) {
switch (event.type) {
case "operation": return [{
type: "operation",
status: event.status
}];
case "run-start": return [{
type: "run-start",
operations: [event.operationId]
}];
case "run-end": return [{
type: "run-end",
operations: [event.operationId]
}];
case "chunk": {
const chunk = toResponseChunk(event.chunk, {});
return chunk ? [{
type: "chunk",
chunk
}] : [];
}
case "message": return [{
type: "message",
message: toTranscriptMessage(event.message)
}];
case "reset": return [{ type: "reset" }];
case "transcript": return [];
}
}
var _harness$1 = /* @__PURE__ */ new WeakMap();
/**
* One session, addressed through the harness.
*
* @experimental
*/
var ThinkSession = class {
/**
* @param harness - The harness that owns the session.
* @param id - The session's id.
*/
constructor(harness, id) {
_classPrivateFieldInitSpec(this, _harness$1, void 0);
_classPrivateFieldSet2(_harness$1, this, harness);
this.id = id;
}
/** Durably submit input. Resolves before the model runs. */
submit(input, options = {}) {
const operationId = options.operationId ?? crypto.randomUUID();
return _classPrivateFieldGet2(_harness$1, this).enqueue(this.id, operationInput(input, operationId, options.autoContinue ?? true), {
...options,
operationId
});
}
/** Submit and wait for the answer and the updated transcript. */
async prompt(input, options = {}) {
const receipt = await this.submit(input, options);
return {
...await this.wait(receipt.operationId),
messages: await this.messages()
};
}
/**
* Answer a user message again. The new answer is a branch beside the
* earlier ones, which `Session.getBranches()` still reads.
*
* @param messageId - The user message. Default: the latest one.
*/
regenerate(messageId, options = {}) {
return _classPrivateFieldGet2(_harness$1, this).enqueue(this.id, {
kind: "regenerate",
...messageId !== void 0 && { messageId }
}, options);
}
/** Call the model again to continue the latest message. */
continue(options = {}) {
return _classPrivateFieldGet2(_harness$1, this).enqueue(this.id, { kind: "continue" }, options);
}
/** Wait for an operation to settle. Aborting `signal` stops only the wait. */
wait(operationId, signal) {
return _classPrivateFieldGet2(_harness$1, this).wait(this.id, operationId, signal);
}
/**
* Withdraw one queued operation or abort it while it runs; with no id,
* everything open in the session.
*/
abort(operationId) {
return _classPrivateFieldGet2(_harness$1, this).abort(this.id, operationId);
}
/** Abort everything and clear the transcript, optionally leaving a note. */
reset(handoff) {
return _classPrivateFieldGet2(_harness$1, this).reset(this.id, handoff);
}
/** The active transcript. */
messages() {
return _classPrivateFieldGet2(_harness$1, this).messages(this.id);
}
/** Operations not settled yet, oldest first. */
pending() {
return _classPrivateFieldGet2(_harness$1, this).pending(this.id);
}
/**
* The running operation, with the chunks its model call has streamed so
* far, including those an eviction left behind. Send them, then
* `subscribe()`'s chunk events, to a client that joins mid-turn.
*/
inFlight() {
return _classPrivateFieldGet2(_harness$1, this).inFlight(this.id);
}
/**
* The answers to a message: its children in the transcript tree. A
* regenerated answer is a branch beside the earlier ones.
*
* @param messageId - The message, usually a user message.
*/
branches(messageId) {
return _classPrivateFieldGet2(_harness$1, this).branches(this.id, messageId);
}
/** Full-text search over this session's messages. */
search(query, options) {
return _classPrivateFieldGet2(_harness$1, this).search(this.id, query, options);
}
/**
* Summarize older messages with the compaction function set in
* `configureSession`. Resolves to null when there was nothing to compact.
*/
compact() {
return _classPrivateFieldGet2(_harness$1, this).compact(this.id);
}
/** Listen to the session's events in the AI SDK's vocabulary. */
subscribe(listener) {
return _classPrivateFieldGet2(_harness$1, this).subscribe(this.id, listener);
}
/** The shared harness interface's watch: the state, then every change. */
watch() {
return _classPrivateFieldGet2(_harness$1, this).watch(this.id);
}
/**
* Submit input and stream its answer to `callback`, as Think's `chat()`
* does: `onStart`, then `onEvent` with each UI chunk as JSON, then
* `onDone` or `onError`. Works over RPC with an `RpcTarget` callback.
*
* The callback lives in memory. If the object is evicted mid-turn the
* turn still finishes, but the callback is gone; a caller that must not
* lose the answer should `wait()` on the receipt as well.
*/
async chat(input, callback, options = {}) {
const operationId = options.operationId ?? crypto.randomUUID();
let delivered = Promise.resolve();
const deliver = (fn) => {
delivered = delivered.then(fn).catch((error) => {
console.error("ThinkHarness chat callback failed", error);
});
};
const unsubscribe = this.subscribe((event) => {
if (event.type === "chunk" && event.operationId === operationId) deliver(() => callback.onEvent(JSON.stringify(event.chunk)));
});
try {
const receipt = await this.submit(input, {
...options,
operationId
});
deliver(() => callback.onStart?.({ operationId: receipt.operationId }));
const result = await this.wait(receipt.operationId);
unsubscribe();
await delivered;
if (result.status === "done") await callback.onDone();
else await callback.onError(result.reason ?? "unanswered");
return result;
} finally {
unsubscribe();
}
}
/** One operation's status, or undefined when the id is unknown. */
inspect(operationId) {
return _classPrivateFieldGet2(_harness$1, this).inspect(this.id, operationId);
}
/** Whether the session has open work. */
async busy() {
return (await this.pending()).length > 0;
}
};
var _harness2 = /* @__PURE__ */ new WeakMap();
/**
* Every session in this object.
*
* @experimental
*/
var ThinkSessions = class {
/** @param harness - The harness that owns the sessions. */
constructor(harness) {
_classPrivateFieldInitSpec(this, _harness2, void 0);
_classPrivateFieldSet2(_harness2, this, harness);
}
/** A handle on one session. */
get(id) {
return _classPrivateFieldGet2(_harness2, this).session(id);
}
/** A new, empty session. */
create() {
return _classPrivateFieldGet2(_harness2, this).createSession();
}
/** A new session that starts with `from`'s active transcript. */
fork(from) {
return _classPrivateFieldGet2(_harness2, this).fork(from);
}
/** Every session, with whether it has open work. */
list() {
return _classPrivateFieldGet2(_harness2, this).listSessions();
}
};
//#endregion
//#region src/harness/think/chat.ts
/** `WebSocket.OPEN`; the constant is not defined on every runtime's global. */
const OPEN = 1;
function send(connection, frame) {
if (connection.readyState !== OPEN) return;
try {
connection.send(JSON.stringify(frame));
} catch {}
}
function parseBody(body) {
if (typeof body !== "string" || body === "") return {};
try {
const parsed = JSON.parse(body);
return typeof parsed === "object" && parsed !== null ? parsed : {};
} catch {
return {};
}
}
function clientToolsOf(value) {
return value.map((tool) => ({
name: tool.name,
...tool.description !== void 0 && { description: tool.description },
...tool.parameters !== void 0 && { parameters: tool.parameters }
}));
}
var _harness = /* @__PURE__ */ new WeakMap();
var _webSockets = /* @__PURE__ */ new WeakMap();
var _session = /* @__PURE__ */ new WeakMap();
var _framing = /* @__PURE__ */ new WeakMap();
var _awaitingAck = /* @__PURE__ */ new WeakMap();
var _replayed = /* @__PURE__ */ new WeakMap();
var _requesters = /* @__PURE__ */ new WeakMap();
var _pendingProbes = /* @__PURE__ */ new WeakMap();
var _unobserve = /* @__PURE__ */ new WeakMap();
var _ThinkChat_brand = /* @__PURE__ */ new WeakSet();
/**
* Serves one ThinkHarness session over Think's chat WebSocket protocol and
* its `GET …/get-messages` route. Install it on the Lifecycle after the
* harness and the WebSockets capability:
*
* ```ts
* readonly webSockets = new WebSockets();
* readonly chat = new ThinkChat({ harness: this.harness, webSockets: this.webSockets });
* readonly lifecycle = Lifecycle.install(this)
* .use(this.harness)
* .use(this.webSockets).use(this.chat);
* ```
*
* Supported: chat requests (`submit-message` and `regenerate-message`),
* cancel, clear, client tool results, approvals, and stream resume after
* a reconnect or an eviction. Not supported: `messageConcurrency`
* strategies other than queueing, client transcript sync, and the
* recovery progress frame.
*
* @experimental The API may change between releases.
*/
var ThinkChat = class extends LifecycleCapability {
/** @param options - The harness, its WebSockets, and the session to serve. */
constructor(options) {
super("think-chat");
_classPrivateMethodInitSpec(this, _ThinkChat_brand);
_classPrivateFieldInitSpec(this, _harness, void 0);
_classPrivateFieldInitSpec(this, _webSockets, void 0);
_classPrivateFieldInitSpec(this, _session, void 0);
_classPrivateFieldInitSpec(this, _framing, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _awaitingAck, /* @__PURE__ */ new Set());
_classPrivateFieldInitSpec(this, _replayed, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _requesters, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _pendingProbes, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _unobserve, void 0);
_classPrivateFieldSet2(_harness, this, options.harness);
_classPrivateFieldSet2(_webSockets, this, options.webSockets);
_classPrivateFieldSet2(_session, this, options.session ?? "");
_classPrivateFieldGet2(_webSockets, this).use({
onConnect: (connection) => _assertClassBrand(_ThinkChat_brand, this, _onConnect).call(this, connection),
onMessage: (connection, message) => _assertClassBrand(_ThinkChat_brand, this, _onMessage).call(this, connection, message),
onClose: (connection) => _assertClassBrand(_ThinkChat_brand, this, _forget).call(this, connection),
onError: (connection) => _assertClassBrand(_ThinkChat_brand, this, _forget).call(this, connection)
});
_classPrivateFieldSet2(_unobserve, this, _classPrivateFieldGet2(_harness, this).observe((session, event) => {
if (session === _classPrivateFieldGet2(_session, this)) _assertClassBrand(_ThinkChat_brand, this, _onEvent).call(this, event);
}));
}
/** Answer `GET …/get-messages` with the session's transcript. */
async onRequest(context) {
const { request } = context;
if (request.method !== "GET") return void 0;
if (!new URL(request.url).pathname.endsWith("/get-messages")) return;
return Response.json(await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).messages());
}
/** Stop listening to the harness. */
dispose() {
_classPrivateFieldGet2(_unobserve, this).call(this);
}
};
async function _onConnect(connection) {
const session = _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this));
send(connection, {
type: CHAT_MESSAGE_TYPES.CHAT_MESSAGES,
messages: await session.messages(),
connect: true
});
const inFlight = await session.inFlight();
if (inFlight) _assertClassBrand(_ThinkChat_brand, this, _offerResume).call(this, connection, inFlight.operationId);
}
function _forget(connection) {
_classPrivateFieldGet2(_awaitingAck, this).delete(connection.id);
_classPrivateFieldGet2(_pendingProbes, this).delete(connection.id);
_classPrivateFieldGet2(_replayed, this).delete(connection.id);
}
function _offerResume(connection, operationId, probeId) {
_classPrivateFieldGet2(_awaitingAck, this).add(connection.id);
send(connection, {
type: CHAT_MESSAGE_TYPES.STREAM_RESUMING,
id: operationId,
...probeId !== void 0 && { probeId }
});
}
async function _onMessage(connection, raw) {
if (typeof raw !== "string") return false;
const event = parseProtocolMessage(raw);
if (!event) return false;
await _assertClassBrand(_ThinkChat_brand, this, _handle).call(this, connection, event);
return true;
}
async function _handle(connection, event) {
const session = _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this));
switch (event.type) {
case "chat-request": {
const body = parseBody(event.init.body);
const clientTools = body.clientTools ? { clientTools: body.clientTools } : {};
_classPrivateFieldGet2(_requesters, this).set(event.id, connection.id);
if (body.trigger === "regenerate-message") {
await session.regenerate(void 0, { operationId: event.id });
return;
}
const known = new Set((await session.messages()).map((message) => message.id));
const fresh = (body.messages ?? []).filter((message) => !known.has(message.id));
if (!(await session.submit(fresh, {
operationId: event.id,
source: "client",
...clientTools
})).accepted) {
const status = await session.inspect(event.id);
if (status?.status === "done" || status?.status === "unanswered") {
_classPrivateFieldGet2(_requesters, this).delete(event.id);
await _assertClassBrand(_ThinkChat_brand, this, _finish).call(this, {
operationId: status.operationId,
status: status.status,
..."reason" in status && status.reason !== void 0 && { reason: status.reason }
}, void 0, connection);
}
}
return;
}
case "cancel":
await session.abort(event.id);
return;
case "clear":
await session.reset();
for (const other of _classPrivateFieldGet2(_webSockets, this).getConnections()) if (other.id !== connection.id) send(other, { type: CHAT_MESSAGE_TYPES.CHAT_CLEAR });
return;
case "tool-result": {
const operationId = crypto.randomUUID();
_classPrivateFieldGet2(_requesters, this).set(operationId, connection.id);
await session.submit({
type: "tool-result",
toolCallId: event.toolCallId,
result: event.state === "output-error" ? {
ok: false,
...event.errorText !== void 0 && { errorText: event.errorText }
} : {
ok: true,
output: event.output ?? null
}
}, {
operationId,
source: "client",
autoContinue: event.autoContinue ?? true,
...event.clientTools && { clientTools: clientToolsOf(event.clientTools) }
});
return;
}
case "tool-approval": {
const approvalId = await _assertClassBrand(_ThinkChat_brand, this, _approvalId).call(this, event.toolCallId);
if (approvalId === void 0) return;
const operationId = crypto.randomUUID();
_classPrivateFieldGet2(_requesters, this).set(operationId, connection.id);
await session.submit({
type: "approval",
approvalId,
approved: event.approved
}, {
operationId,
source: "client",
autoContinue: event.autoContinue ?? true
});
return;
}
case "stream-resume-request": {
const inFlight = await session.inFlight();
if (inFlight) _assertClassBrand(_ThinkChat_brand, this, _offerResume).call(this, connection, inFlight.operationId, event.probeId);
else if ((await session.pending()).length > 0) {
_classPrivateFieldGet2(_pendingProbes, this).set(connection.id, event.probeId);
send(connection, {
type: CHAT_MESSAGE_TYPES.STREAM_PENDING,
...event.probeId !== void 0 && { probeId: event.probeId }
});
} else send(connection, {
type: CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE,
reason: STREAM_RESUME_NONE_REASONS.IDLE,
...event.probeId !== void 0 && { probeId: event.probeId }
});
return;
}
case "stream-resume-ack":
await _assertClassBrand(_ThinkChat_brand, this, _replay).call(this, connection, event.id);
return;
case "messages": return;
}
}
/** Replay a running model call's chunks to one connection, then go live. */
async function _replay(connection, operationId) {
const inFlight = await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).inFlight();
if (!inFlight || inFlight.operationId !== operationId) {
_classPrivateFieldGet2(_awaitingAck, this).delete(connection.id);
send(connection, {
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
id: operationId,
body: "",
done: true,
replay: true
});
return;
}
const { continuation, chunks } = inFlight;
let seq = 0;
for (const chunk of chunks) send(connection, {
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
id: operationId,
body: JSON.stringify(chunk),
done: false,
replay: true,
seq: seq++,
...continuation && { continuation: true }
});
send(connection, {
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
id: operationId,
body: "",
done: false,
replay: true,
replayComplete: true,
...continuation && { continuation: true }
});
_classPrivateFieldGet2(_replayed, this).set(connection.id, {
operationId,
continuation
});
_classPrivateFieldGet2(_awaitingAck, this).delete(connection.id);
}
async function _approvalId(toolCallId) {
const messages = await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).messages();
for (const message of [...messages].reverse()) for (const part of message.parts) if ("toolCallId" in part && part.toolCallId === toolCallId && "approval" in part && part.approval) return part.approval.id;
}
function _onEvent(event) {
switch (event.type) {
case "run-start": {
const requester = _classPrivateFieldGet2(_requesters, this).get(event.operationId);
_classPrivateFieldGet2(_framing, this).set(event.operationId, {
continuation: event.continuation,
requester,
finish: void 0,
started: false
});
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) {
if (!_classPrivateFieldGet2(_pendingProbes, this).has(connection.id)) continue;
const probeId = _classPrivateFieldGet2(_pendingProbes, this).get(connection.id);
_classPrivateFieldGet2(_pendingProbes, this).delete(connection.id);
_assertClassBrand(_ThinkChat_brand, this, _offerResume).call(this, connection, event.operationId, probeId);
}
if (event.continuation && requester !== void 0) {
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) if (connection.id === requester) _assertClassBrand(_ThinkChat_brand, this, _offerResume).call(this, connection, event.operationId);
}
return;
}
case "chunk": {
const framing = _classPrivateFieldGet2(_framing, this).get(event.operationId);
if (event.chunk.type === "finish") {
if (framing) framing.finish = event.chunk;
return;
}
if (event.chunk.type === "start" && framing) {
if (framing.started) return;
framing.started = true;
}
_assertClassBrand(_ThinkChat_brand, this, _broadcastChunk).call(this, event.operationId, event.chunk, framing);
return;
}
case "message": {
if (event.operationId !== void 0 && _classPrivateFieldGet2(_framing, this).has(event.operationId)) return;
const requester = event.operationId === void 0 ? void 0 : _classPrivateFieldGet2(_requesters, this).get(event.operationId);
_assertClassBrand(_ThinkChat_brand, this, _broadcastTranscript).call(this, requester);
return;
}
case "operation": {
const { status } = event;
if (status.status !== "done" && status.status !== "unanswered") return;
const framing = _classPrivateFieldGet2(_framing, this).get(status.operationId);
_classPrivateFieldGet2(_framing, this).delete(status.operationId);
_classPrivateFieldGet2(_requesters, this).delete(status.operationId);
_assertClassBrand(_ThinkChat_brand, this, _finish).call(this, status, framing).then(() => _assertClassBrand(_ThinkChat_brand, this, _resolveProbes).call(this));
return;
}
case "transcript":
_assertClassBrand(_ThinkChat_brand, this, _broadcastTranscript).call(this);
return;
case "reset":
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) send(connection, { type: CHAT_MESSAGE_TYPES.CHAT_CLEAR });
return;
case "run-end": return;
}
}
function _broadcastChunk(operationId, chunk, framing) {
const body = JSON.stringify(chunk);
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) {
if (_classPrivateFieldGet2(_awaitingAck, this).has(connection.id)) continue;
const replayed = _classPrivateFieldGet2(_replayed, this).get(connection.id);
const continuation = replayed?.operationId === operationId ? replayed.continuation : framing?.continuation ?? false;
send(connection, {
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
id: operationId,
body,
done: false,
...continuation && { continuation: true }
});
}
}
async function _broadcastTranscript(except) {
const messages = await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).messages();
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) {
if (connection.id === except) continue;
send(connection, {
type: CHAT_MESSAGE_TYPES.CHAT_MESSAGES,
messages
});
}
}
/** Tell probes still waiting that nothing will stream, once the session is idle. */
async function _resolveProbes() {
if (_classPrivateFieldGet2(_pendingProbes, this).size === 0) return;
if ((await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).pending()).length > 0) return;
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) {
if (!_classPrivateFieldGet2(_pendingProbes, this).has(connection.id)) continue;
const probeId = _classPrivateFieldGet2(_pendingProbes, this).get(connection.id);
_classPrivateFieldGet2(_pendingProbes, this).delete(connection.id);
send(connection, {
type: CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE,
reason: STREAM_RESUME_NONE_REASONS.IDLE,
...probeId !== void 0 && { probeId }
});
}
}
/** The transcript first, then the terminal frame (#2119). */
async function _finish(status, framing, only) {
const { operationId } = status;
if (framing?.finish) _assertClassBrand(_ThinkChat_brand, this, _broadcastChunk).call(this, operationId, framing.finish, framing);
const messages = await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).messages();
const reason = status.status === "unanswered" ? status.reason : void 0;
const outcome = status.status === "done" ? "completed" : reason === "aborted" ? "aborted" : reason === "withdrawn" || reason === "not_waiting" || reason === "not_found" || reason === "empty" ? "skipped" : "error";
const connections = only ? [only] : _classPrivateFieldGet2(_webSockets, this).getConnections();
for (const connection of connections) {
send(connection, {
type: CHAT_MESSAGE_TYPES.CHAT_MESSAGES,
messages
});
send(connection, {
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
id: operationId,
body: outcome === "error" ? reason ?? "" : "",
done: true,
outcome,
...outcome === "error" && { error: true },
...framing?.continuation && { continuation: true }
});
_classPrivateFieldGet2(_awaitingAck, this).delete(connection.id);
if (_classPrivateFieldGet2(_replayed, this).get(connection.id)?.operationId === operationId) _classPrivateFieldGet2(_replayed, this).delete(connection.id);
}
}
//#endregion
export { ROOT_SESSION, SteerNotSupportedError, ThinkChat, ThinkHarness, ThinkSession, ThinkSessions, classifyContextOverflow };
//# sourceMappingURL=index.js.map