UNPKG

agents

Version:

A home for your AI agents

605 lines (604 loc) • 23.6 kB
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 { t as LifecycleCapability } from "../../capability-DBAKc6UZ.js"; import { a as toTranscriptMessage, i as toResponseChunks, n as awaitsInput, o as toUIMessage, r as toResponseChunk, s as toUIMessageChunk, t as answerToolCall } from "../../turns-DCV3ycDl.js"; import { convertToModelMessages, jsonSchema, stepCountIs, streamText, toUIMessageStream, tool } from "ai"; //#region src/harness/ai-sdk/harness.ts const PREFIX = "ai-sdk-harness:"; const WAKE_FN = "wake"; const DEFAULT_SESSION = "main"; const HEARTBEAT_MS = 3e4; var _options = /* @__PURE__ */ new WeakMap(); var _heartbeatMs = /* @__PURE__ */ new WeakMap(); var _runs = /* @__PURE__ */ new WeakMap(); var _driving = /* @__PURE__ */ new WeakMap(); var _watchers = /* @__PURE__ */ new WeakMap(); var _waiters = /* @__PURE__ */ new WeakMap(); var _AiSdkHarness_brand = /* @__PURE__ */ new WeakSet(); /** * An agent harness around the AI SDK's `streamText`, hosted in a Durable * Object: sessions, their transcripts, and a queue of operations, all in the * object's storage. One operation runs at a time per session; a run is one * `streamText` call over the session's transcript. * * The AI SDK keeps no state of its own, so a run that an eviction cuts short * starts again from the saved transcript, and its unsaved output is lost. * Steering is not supported: `whenBusy: "steer"` queues like a follow-up. * * @experimental The API may change between releases. */ var AiSdkHarness = class extends LifecycleCapability { constructor(options) { super("ai-sdk-harness"); _classPrivateMethodInitSpec(this, _AiSdkHarness_brand); _classPrivateFieldInitSpec(this, _options, void 0); _classPrivateFieldInitSpec(this, _heartbeatMs, void 0); _classPrivateFieldInitSpec(this, _runs, /* @__PURE__ */ new Map()); _classPrivateFieldInitSpec(this, _driving, /* @__PURE__ */ new Map()); _classPrivateFieldInitSpec(this, _watchers, /* @__PURE__ */ new Map()); _classPrivateFieldInitSpec(this, _waiters, /* @__PURE__ */ new Map()); _classPrivateFieldSet2(_options, this, options); _classPrivateFieldSet2(_heartbeatMs, this, options.heartbeatMs ?? HEARTBEAT_MS); this.sessions = { create: async () => _assertClassBrand(_AiSdkHarness_brand, this, _create).call(this, { id: crypto.randomUUID() }), fork: async (from) => { _assertClassBrand(_AiSdkHarness_brand, this, _ensure).call(this, from); const session = _assertClassBrand(_AiSdkHarness_brand, this, _create).call(this, { id: crypto.randomUUID(), parent: from }); for (const message of _assertClassBrand(_AiSdkHarness_brand, this, _messages).call(this, from)) _assertClassBrand(_AiSdkHarness_brand, this, _putMessage).call(this, session.id, message); return session; }, list: async () => { _assertClassBrand(_AiSdkHarness_brand, this, _ensure).call(this, DEFAULT_SESSION); return [..._get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).list({ prefix: `${PREFIX}s:` })].map(([, { id, parent }]) => ({ id, ...parent !== void 0 && { parent }, busy: _assertClassBrand(_AiSdkHarness_brand, this, _pending).call(this, id).length > 0 })); } }; } async onStart() { const sessions = [..._get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).list({ prefix: `${PREFIX}s:` })]; for (const [, session] of sessions) if (_assertClassBrand(_AiSdkHarness_brand, this, _pending).call(this, session.id).length > 0) await _assertClassBrand(_AiSdkHarness_brand, this, _wake).call(this, session.id); } async onJob(context) { if (context.job.fn !== WAKE_FN) return; const session = context.job.payload?.session; if (typeof session !== "string") return; if (!_classPrivateFieldGet2(_driving, this).has(session) && _assertClassBrand(_AiSdkHarness_brand, this, _pending).call(this, session).length === 0) return; this.lifecycle.trackAlarmWork(_assertClassBrand(_AiSdkHarness_brand, this, _drive).call(this, session)); return { rescheduleAt: Date.now() + _classPrivateFieldGet2(_heartbeatMs, this) }; } session(id = DEFAULT_SESSION) { return { id, submit: (input, options) => _assertClassBrand(_AiSdkHarness_brand, this, _submit).call(this, id, input, options), abort: (operationId) => _assertClassBrand(_AiSdkHarness_brand, this, _abort).call(this, id, operationId), wait: (operationId, signal) => _assertClassBrand(_AiSdkHarness_brand, this, _wait).call(this, id, operationId, signal), reset: (handoff) => _assertClassBrand(_AiSdkHarness_brand, this, _reset).call(this, id, handoff), watch: async () => _assertClassBrand(_AiSdkHarness_brand, this, _watch).call(this, id) }; } }; async function _submit(session, input, options = {}) { _assertClassBrand(_AiSdkHarness_brand, this, _ensure).call(this, session); const operationId = options.operationId ?? crypto.randomUUID(); if (_assertClassBrand(_AiSdkHarness_brand, this, _operation).call(this, session, operationId)) return { operationId, session, accepted: false }; const op = { operationId, seq: _assertClassBrand(_AiSdkHarness_brand, this, _nextSeq).call(this, session), input, status: "queued" }; _assertClassBrand(_AiSdkHarness_brand, this, _saveOperation).call(this, session, op); _assertClassBrand(_AiSdkHarness_brand, this, _emit).call(this, session, [{ type: "operation", status: { operationId, status: "queued" } }]); await _assertClassBrand(_AiSdkHarness_brand, this, _wake).call(this, session); _assertClassBrand(_AiSdkHarness_brand, this, _drive).call(this, session); return { operationId, session, accepted: true }; } async function _abort(session, operationId) { const run = _classPrivateFieldGet2(_runs, this).get(session); const targets = operationId ? [_assertClassBrand(_AiSdkHarness_brand, this, _operation).call(this, session, operationId)].filter((op) => op !== void 0 && isPending(op)) : _assertClassBrand(_AiSdkHarness_brand, this, _pending).call(this, session); for (const op of targets) if (run?.operations.includes(op.operationId)) run.abort.abort(); else _assertClassBrand(_AiSdkHarness_brand, this, _settle).call(this, session, op, { status: "unanswered", reason: "withdrawn" }); return targets.length > 0; } async function _wait(session, operationId, signal) { const op = _assertClassBrand(_AiSdkHarness_brand, this, _operation).call(this, session, operationId); if (!op) return { operationId, session, status: "unanswered", reason: "not_found" }; const settled = isPending(op) ? await new Promise((resolve, 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 = (result) => { signal?.removeEventListener("abort", aborted); resolve(result); }; const aborted = () => { waiters.delete(done); reject(signal?.reason); }; waiters.add(done); signal?.addEventListener("abort", aborted, { once: true }); }) : op; return settled.status === "done" ? { operationId, session, status: "done", ...settled.text !== void 0 && { text: settled.text } } : { operationId, session, status: "unanswered", ...settled.reason !== void 0 && { reason: settled.reason } }; } async function _reset(session, handoff) { _assertClassBrand(_AiSdkHarness_brand, this, _ensure).call(this, session); await _assertClassBrand(_AiSdkHarness_brand, this, _abort).call(this, session); await _classPrivateFieldGet2(_driving, this).get(session); for (const [key] of _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).list({ prefix: messagePrefix(session) })) _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).delete(key); const events = [{ type: "reset" }]; if (handoff) { const note = { id: crypto.randomUUID(), role: "system", parts: [{ type: "text", text: handoff }] }; _assertClassBrand(_AiSdkHarness_brand, this, _putMessage).call(this, session, note); events.push({ type: "message", message: _assertClassBrand(_AiSdkHarness_brand, this, _transcript).call(this, note) }); } _assertClassBrand(_AiSdkHarness_brand, this, _emit).call(this, session, events); } function _watch(session) { _assertClassBrand(_AiSdkHarness_brand, this, _ensure).call(this, session); const run = _classPrivateFieldGet2(_runs, this).get(session); const state = { messages: _assertClassBrand(_AiSdkHarness_brand, this, _messages).call(this, session).map((m) => _assertClassBrand(_AiSdkHarness_brand, this, _transcript).call(this, m)), pending: _assertClassBrand(_AiSdkHarness_brand, this, _pending).call(this, session).map((op) => ({ operationId: op.operationId, status: op.status === "placed" ? "placed" : "queued" })), ...run && { run: { operations: [...run.operations], ...run.partial.length > 0 && { partial: [...run.partial] } } } }; const watchers = _classPrivateFieldGet2(_watchers, this).get(session) ?? /* @__PURE__ */ new Set(); _classPrivateFieldGet2(_watchers, this).set(session, watchers); const watcher = new Watcher(state, () => watchers.delete(watcher)); watchers.add(watcher); return watcher; } /** Run the session's operations in order until none are left. */ function _drive(session) { let driving = _classPrivateFieldGet2(_driving, this).get(session); if (!driving) { driving = (async () => { try { for (;;) { const next = _assertClassBrand(_AiSdkHarness_brand, this, _pending).call(this, session)[0]; if (!next) return; await _assertClassBrand(_AiSdkHarness_brand, this, _run).call(this, session, next); } } finally { _classPrivateFieldGet2(_driving, this).delete(session); } })(); _classPrivateFieldGet2(_driving, this).set(session, driving); } return driving; } async function _run(session, op) { const { operationId } = op; let owner = "parts" in op.input ? op.input.from?.participantId : void 0; if (op.status === "queued") { let placed; if ("parts" in op.input) placed = { id: op.input.messageId ?? operationId, role: "user", parts: op.input.parts.map(toUIPart) }; else { const answered = _assertClassBrand(_AiSdkHarness_brand, this, _answer).call(this, session, op.input); if (typeof answered === "string") { _assertClassBrand(_AiSdkHarness_brand, this, _settle).call(this, session, op, { status: "unanswered", reason: answered }); return; } placed = answered; } _assertClassBrand(_AiSdkHarness_brand, this, _putMessage).call(this, session, placed); op = { ...op, status: "placed" }; _assertClassBrand(_AiSdkHarness_brand, this, _saveOperation).call(this, session, op); _assertClassBrand(_AiSdkHarness_brand, this, _emit).call(this, session, [{ type: "message", message: _assertClassBrand(_AiSdkHarness_brand, this, _transcript).call(this, placed) }, { type: "operation", status: { operationId, status: "placed" } }]); } const messages = _assertClassBrand(_AiSdkHarness_brand, this, _messages).call(this, session); const last = messages.at(-1); if (last?.role === "assistant") { owner = last.metadata?.owner; if (awaitsInput$1(last)) { _assertClassBrand(_AiSdkHarness_brand, this, _settle).call(this, session, op, { status: "done" }); return; } } const run = { operations: [operationId], partial: [], abort: new AbortController() }; _classPrivateFieldGet2(_runs, this).set(session, run); _assertClassBrand(_AiSdkHarness_brand, this, _emit).call(this, session, [{ type: "run-start", operations: [operationId] }]); const { model, tools, system } = _classPrivateFieldGet2(_options, this); let message; let error; try { const stream = toUIMessageStream({ stream: streamText({ model, ...system !== void 0 && { system }, ...tools && { tools }, stopWhen: _classPrivateFieldGet2(_options, this).stopWhen ?? stepCountIs(5), messages: await convertToModelMessages(messages, tools && { tools }), abortSignal: run.abort.signal }).stream, ...tools && { tools }, originalMessages: messages, generateMessageId: () => crypto.randomUUID(), onEnd: ({ responseMessage }) => { message = { ...responseMessage, metadata: { ...responseMessage.metadata, ...owner !== void 0 && { owner } } }; } }); for await (const chunk of toResponseChunks(stream, { ...tools && { tools }, ...owner !== void 0 && { owner } })) { run.partial.push(chunk); _assertClassBrand(_AiSdkHarness_brand, this, _emit).call(this, session, [{ type: "chunk", chunk }]); } } catch (caught) { error = caught; } if (message) { _assertClassBrand(_AiSdkHarness_brand, this, _putMessage).call(this, session, message); _assertClassBrand(_AiSdkHarness_brand, this, _emit).call(this, session, [{ type: "message", message: _assertClassBrand(_AiSdkHarness_brand, this, _transcript).call(this, message) }]); } _classPrivateFieldGet2(_runs, this).delete(session); if (run.abort.signal.aborted) _assertClassBrand(_AiSdkHarness_brand, this, _settle).call(this, session, op, { status: "unanswered", reason: "aborted" }); else if (error !== void 0) { console.error(`AI SDK run for "${operationId}" failed`, error); _assertClassBrand(_AiSdkHarness_brand, this, _settle).call(this, session, op, { status: "unanswered", reason: error instanceof Error ? error.message : "failed" }); } else _assertClassBrand(_AiSdkHarness_brand, this, _settle).call(this, session, op, { status: "done", text: message ? textOf(message) : "" }); _assertClassBrand(_AiSdkHarness_brand, this, _emit).call(this, session, [{ type: "run-end", operations: [operationId] }]); } /** The message an answer updates, or why it is not wanted. */ function _answer(session, answer) { const messages = _assertClassBrand(_AiSdkHarness_brand, this, _messages).call(this, session); const event = 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 }; if (answer.type === "tool-result") { const owner = messages.find((m) => m.parts.some((p) => "toolCallId" in p && p.toolCallId === answer.toolCallId))?.metadata?.owner; const from = answer.from?.participantId; if (owner !== void 0 && from !== void 0 && owner !== from) return "not_owner"; } return answerToolCall(messages, event) ?? "not_waiting"; } function _settle(session, op, result) { const settled = { ...op, ...result }; _assertClassBrand(_AiSdkHarness_brand, this, _saveOperation).call(this, session, settled); const { operationId } = op; _assertClassBrand(_AiSdkHarness_brand, this, _emit).call(this, session, [{ type: "operation", status: result.status === "done" ? { operationId, status: "done", ...result.text !== void 0 && { text: result.text } } : { operationId, status: "unanswered", reason: result.reason } }]); const key = waiterKey(session, operationId); for (const waiter of _classPrivateFieldGet2(_waiters, this).get(key) ?? []) waiter(settled); _classPrivateFieldGet2(_waiters, this).delete(key); } function _wake(session) { return this.lifecycle.jobs.push({ id: `${PREFIX}wake:${session}`, fn: WAKE_FN, time: Date.now(), payload: { session }, singleflight: true }); } function _emit(session, events) { for (const watcher of _classPrivateFieldGet2(_watchers, this).get(session) ?? []) watcher.push(events); } function _get_kv() { return this.lifecycle.storage.kv; } function _create(session) { _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).put(`${PREFIX}s:${session.id}`, session); return this.session(session.id); } /** * Sessions are created on first use, so any id names one. That is not an * authorization gap: whoever reaches the agent may create sessions anyway. */ function _ensure(session) { if (_get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).get(`${PREFIX}s:${session}`) === void 0) _assertClassBrand(_AiSdkHarness_brand, this, _create).call(this, { id: session }); } function _nextSeq(session) { const key = `${PREFIX}seq:${session}`; const seq = (_get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).get(key) ?? 0) + 1; _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).put(key, seq); return seq; } function _messages(session) { return [..._get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).list({ prefix: messagePrefix(session) })].map(([, message]) => message); } /** Save a message, in place of any with its id. */ function _putMessage(session, message) { const indexKey = `${PREFIX}mi:${JSON.stringify(session)}:${message.id}`; let key = _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).get(indexKey); if (key === void 0 || _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).get(key) === void 0) { key = messagePrefix(session) + pad(_assertClassBrand(_AiSdkHarness_brand, this, _nextSeq).call(this, session)); _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).put(indexKey, key); } _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).put(key, message); } function _operation(session, operationId) { return _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).get(operationKey(session, operationId)); } function _saveOperation(session, op) { _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).put(operationKey(session, op.operationId), op); const pendingKey = `${pendingPrefix(session)}${pad(op.seq)}`; if (isPending(op)) _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).put(pendingKey, op.operationId); else _get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).delete(pendingKey); } /** Unsettled operations, oldest first. */ function _pending(session) { return [..._get_kv.call(_assertClassBrand(_AiSdkHarness_brand, this)).list({ prefix: pendingPrefix(session) })].map(([, id]) => _assertClassBrand(_AiSdkHarness_brand, this, _operation).call(this, session, id)).filter((op) => op !== void 0); } function _transcript(message) { const owner = message.metadata?.owner; return toTranscriptMessage(message, { ..._classPrivateFieldGet2(_options, this).tools && { tools: _classPrivateFieldGet2(_options, this).tools }, ...owner !== void 0 && { owner } }); } var _close = /* @__PURE__ */ new WeakMap(); var _listener = /* @__PURE__ */ new WeakMap(); var _queue = /* @__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) { this.state = state; this.onStop = onStop; _classPrivateMethodInitSpec(this, _Watcher_brand); _classPrivateFieldInitSpec(this, _close, void 0); _classPrivateFieldInitSpec(this, _listener, void 0); _classPrivateFieldInitSpec(this, _queue, []); _classPrivateFieldInitSpec(this, _draining, false); _classPrivateFieldInitSpec(this, _stopped, false); this.closed = new Promise((resolve) => { _classPrivateFieldSet2(_close, this, resolve); }); } start(listener) { _classPrivateFieldSet2(_listener, this, listener); _assertClassBrand(_Watcher_brand, this, _drain).call(this); } async stop() { _classPrivateFieldSet2(_stopped, this, true); this.onStop(); _classPrivateFieldGet2(_close, this).call(this); } push(events) { if (_classPrivateFieldGet2(_stopped, this)) 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 { while (_classPrivateFieldGet2(_queue, this).length > 0 && !_classPrivateFieldGet2(_stopped, this)) { const events = _classPrivateFieldGet2(_queue, this).shift(); try { await _classPrivateFieldGet2(_listener, this).call(this, events); } catch (error) { console.error("A session watcher failed", error); } } } finally { _classPrivateFieldSet2(_draining, this, false); } } function isPending(op) { return op.status === "queued" || op.status === "placed"; } /** Whether the message has a tool call waiting for a result or approval. */ function awaitsInput$1(message) { return message.parts.some((part) => "toolCallId" in part && (part.state === "input-available" || part.state === "approval-requested")); } function textOf(message) { return message.parts.map((part) => part.type === "text" ? part.text : "").join(""); } function toUIPart(part) { return part.type === "text" ? { type: "text", text: part.text } : { type: "file", mediaType: part.mediaType, url: part.url, ...part.filename !== void 0 && { filename: part.filename } }; } function pad(seq) { return String(seq).padStart(12, "0"); } function messagePrefix(session) { return `${PREFIX}m:${JSON.stringify(session)}:`; } function pendingPrefix(session) { return `${PREFIX}p:${JSON.stringify(session)}:`; } function operationKey(session, operationId) { return `${PREFIX}o:${JSON.stringify(session)}:${operationId}`; } function waiterKey(session, operationId) { return JSON.stringify([session, operationId]); } //#endregion //#region src/experimental/channels/tool-schema.ts const channelMessageJsonSchema = { type: "object", properties: { title: { type: "string", description: "Optional title or topic for the message" }, markdown: { type: "string", minLength: 1, description: "Message content formatted as Markdown" } }, required: ["markdown"], additionalProperties: false }; function parseChannelMessage(value) { if (typeof value === "object" && value !== null) { const candidate = value; if (typeof candidate.markdown === "string" && candidate.markdown.length > 0 && (candidate.title === void 0 || typeof candidate.title === "string")) return { ...typeof candidate.title === "string" ? { title: candidate.title } : {}, markdown: candidate.markdown }; } throw new Error("Expected an optional string title and a non-empty Markdown string"); } //#endregion //#region src/harness/ai-sdk/send-message-tool.ts const channelMessageSchema = jsonSchema(channelMessageJsonSchema, { validate(value) { try { return { success: true, value: parseChannelMessage(value) }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error(String(error)) }; } } }); /** * Adapt one Host-resolved surface to an AI SDK tool. * * The caller chooses the key used in its ToolSet and owns model-facing policy * such as the description, examples, metadata, and approval requirement. */ function createSendMessageTool(gateway, surface, options = {}) { return tool({ ...options, inputSchema: channelMessageSchema, execute: (message) => gateway.deliver(surface, message) }); } //#endregion export { AiSdkHarness, answerToolCall, awaitsInput, createSendMessageTool, toResponseChunk, toResponseChunks, toTranscriptMessage, toUIMessage, toUIMessageChunk }; //# sourceMappingURL=index.js.map