UNPKG

agents

Version:

A home for your AI agents

736 lines (735 loc) • 30.1 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 { isDurableObjectMemoryLimitReset } from "../../retries.js"; import { i as renderSkillResource, n as formatSkillDiagnostic, r as loadSkillCatalog } from "../../catalog-BpOejOsE.js"; import { obj, opt, str } from "@earendil-works/pi-schema"; import { LiveDoc, ROOT_CONVERSATION_ID } from "@earendil-works/pi-durable"; import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context"; import { openDurableObjectSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/cloudflare"; import { StringEnum } from "@earendil-works/pi-ai"; //#region src/harness/pi/session-store.ts const DEFAULT_PREFIX = "pi_"; /** Open pi-durable's storage over this object's SQLite database. */ function openPiSessionStore(storage, options = {}) { const prefix = options.prefix ?? DEFAULT_PREFIX; if (prefix.toLowerCase().startsWith("_cf_")) return Promise.reject(/* @__PURE__ */ new Error("The pi session store prefix must not start with _cf_")); return openDurableObjectSqliteStorage(storage, { prefix }); } //#endregion //#region src/harness/pi/harness.ts const BG = BACKGROUND_CONTEXT; /** The root session's id. */ const ROOT_SESSION = String(ROOT_CONVERSATION_ID); /** * The wake timings. They suit a real deployment; only tests change them, * with `setWakeTimingForTests`, so a suite does not sit on the real * heartbeat. * * Longest one wake waits on pi. It waits inside an alarm invocation, which * has a 15 minute wall-time limit, so a longer run is waited on across * several alarms. */ const WAIT_BUDGET_MS = 10 * 6e4; /** * The wake job's heartbeat while it waits, and how often it re-checks work * it cannot wait on, such as background tasks. If the object is evicted, the * job is still due and its alarm restarts the object. */ const HEARTBEAT_MS = 3e4; /** * How often a wait re-reads `pi.wake()` when the factory did not pass * `onWake`, so nothing tells the harness when it changes. */ const POLL_MS = 1e3; /** * The longest a wake job whose dispatch keeps failing waits before it tries * again. Its delay starts at the heartbeat and doubles up to this. */ const MAX_WAKE_BACKOFF_MS = 10 * 6e4; /** * Wake dispatches that failed in a row. Stored, so the backoff holds across * restarts, and outside the job: a push that saved it in the job's payload * could overwrite a submit's due-now push. */ const WAKE_ERRORS_KEY = "pi-harness:wake-errors"; const WAKE_FN = "wake"; /** * The one wake job. Before Pi 1.4 there was one per session, with ids * `pi-wake:<session>`; a left-over one hands over to this one. */ const WAKE_JOB_ID = "pi-wake"; /** pi-durable's stored reference to a pi-ai model. */ function modelRef(model) { return { provider: model.provider, modelId: model.id }; } function conversationId(session) { const id = Number(session); if (!Number.isSafeInteger(id) || id < 1) throw new Error(`Invalid pi session ${JSON.stringify(session)}`); return id; } /** The text of an assistant entry, for an operation's result. */ function assistantText(entry) { const message = entry?.model?.[0]; if (message?.role !== "assistant") return ""; return message.content.map((part) => part.type === "text" ? part.text : "").join(""); } function signalContext(signal) { return signal ? withAbortSignal(signal, BG) : BG; } var _options = /* @__PURE__ */ new WeakMap(); var _waiting = /* @__PURE__ */ new WeakMap(); var _changed = /* @__PURE__ */ new WeakMap(); var _admitting = /* @__PURE__ */ new WeakMap(); var _onWakeWired = /* @__PURE__ */ new WeakMap(); var _waitBudgetMs = /* @__PURE__ */ new WeakMap(); var _heartbeatMs = /* @__PURE__ */ new WeakMap(); var _pollMs = /* @__PURE__ */ new WeakMap(); var _opening = /* @__PURE__ */ new WeakMap(); var _lastClose = /* @__PURE__ */ new WeakMap(); var _closing = /* @__PURE__ */ new WeakMap(); var _PiHarness_brand = /* @__PURE__ */ new WeakSet(); var _onWake = /* @__PURE__ */ new WeakMap(); /** * pi-durable hosted in a Durable Object, behind a small harness interface: * `harness.prompt()`, `harness.sessions`, `harness.session(id)`. How a session reaches a * client (sockets, SSE, RPC) is the host's glue, built on `session.events()`. * * pi owns everything about a run: the transcript, the inbox of steers and * follow-ups, generation and tool tasks, retries, crash recovery, and the * live view. It keeps all of it in its own tables in this object's SQLite * database (see `session-store.ts`). * * What pi cannot do on a Durable Object is wake itself: its scheduler runs * in memory, and an evicted object has no memory. The harness is that wake, * with one Lifecycle job driven by `pi.wake()`. Input goes to pi once, in * `submit()`, after the job is scheduled. While pi has work running, the * job waits on it, rescheduling itself as a heartbeat. While pi's tasks * sleep, the job is due when the first one wakes; pi tells the harness * through the `onWake` the factory passes to `Harness.open`. When pi has * nothing to do, the job completes. An eviction mid-run leaves the job due, * so its alarm restarts the object, pi reopens and resumes its own tasks, * and the job waits again. */ var PiHarness = class extends LifecycleCapability { constructor(options) { super("pi-harness"); _classPrivateMethodInitSpec(this, _PiHarness_brand); _classPrivateFieldInitSpec(this, _options, void 0); _classPrivateFieldInitSpec(this, _waiting, void 0); _classPrivateFieldInitSpec(this, _changed, void 0); _classPrivateFieldInitSpec(this, _admitting, 0); _classPrivateFieldInitSpec(this, _onWakeWired, false); _classPrivateFieldInitSpec(this, _waitBudgetMs, WAIT_BUDGET_MS); _classPrivateFieldInitSpec(this, _heartbeatMs, HEARTBEAT_MS); _classPrivateFieldInitSpec(this, _pollMs, POLL_MS); _classPrivateFieldInitSpec(this, _opening, void 0); _classPrivateFieldInitSpec(this, _lastClose, Promise.resolve()); _classPrivateFieldInitSpec(this, _closing, /* @__PURE__ */ new WeakSet()); _classPrivateFieldInitSpec(this, _onWake, (wake) => { _classPrivateFieldSet2(_onWakeWired, this, true); _classPrivateFieldGet2(_changed, this)?.call(this); if (_classPrivateFieldGet2(_waiting, this)) return; _assertClassBrand(_PiHarness_brand, this, _wakeFor).call(this, wake).catch((error) => _assertClassBrand(_PiHarness_brand, this, _wakeError).call(this, error)); }); _classPrivateFieldSet2(_options, this, options); this.sessions = new PiSessions(this); } async onStart(_context) { await _assertClassBrand(_PiHarness_brand, this, _open).call(this); } async onJob(context) { if (context.job.fn !== WAKE_FN) return; let outcome; if (context.job.id !== WAKE_JOB_ID) { await _assertClassBrand(_PiHarness_brand, this, _wake).call(this); outcome = void 0; } else outcome = await _assertClassBrand(_PiHarness_brand, this, _wakeStep).call(this); const kv = this.lifecycle.storage.kv; if (kv.get(WAKE_ERRORS_KEY) !== void 0) kv.delete(WAKE_ERRORS_KEY); return outcome; } /** * A wake dispatch that failed through Lifecycle's retries, as when pi * cannot be opened. Lifecycle would complete the job, and pi's work would * never be woken again, so try again later: at the heartbeat, doubling * while the failures continue, up to 10 minutes. */ onJobError(context, error) { if (context.job.fn !== WAKE_FN) return; console.error(`pi wake job "${context.job.id}" failed after ${context.attempt} attempts; retrying with backoff`, error); _assertClassBrand(_PiHarness_brand, this, _wakeError).call(this, error); const kv = this.lifecycle.storage.kv; const errors = (kv.get(WAKE_ERRORS_KEY) ?? 0) + 1; kv.put(WAKE_ERRORS_KEY, errors); const delay = Math.min(_classPrivateFieldGet2(_heartbeatMs, this) * 2 ** (errors - 1), MAX_WAKE_BACKOFF_MS); return { rescheduleAt: Date.now() + delay }; } /** Close pi's in-memory resources. Durable state is untouched. */ async dispose() { const opening = _classPrivateFieldGet2(_opening, this); _classPrivateFieldSet2(_opening, this, void 0); await (await opening?.catch(() => void 0))?.pi.close(BG); } /** A handle on one session. No I/O until you call it. */ session(id = ROOT_SESSION) { return new PiSession(this, id); } /** Submit a prompt and wait for its answer. */ prompt(input, options = {}) { return this.session(options.session).prompt(input, options); } /** Durably submit a prompt. Resolves before the model runs. */ submit(input, options = {}) { return this.session(options.session).submit(input, options); } /** * Run an idle session again on its transcript as it is, without new input. * See `PiSession.continue`. */ continue(options = {}) { return this.session(options.session).continue(options); } /** Stop one operation, or everything running in a session. */ abort(options = {}) { return this.session(options.session).abort(options.operationId); } wait(operationId, options = {}) { return this.session(options.session).wait(operationId, options.signal); } /** The session's active transcript, as pi's entries. */ messages(options = {}) { return this.session(options.session).messages(); } /** Submissions pi has not settled yet, oldest first. */ async pending(options = {}) { const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this); const id = options.session === void 0 ? void 0 : conversationId(options.session); return (await pi.inspect(BG)).submissions.flatMap((record) => { if (record.requestId === void 0) return []; if (id !== void 0 && record.conversationId !== id) return []; let status; switch (record.status) { case "queued": status = "queued"; break; case "placed": status = "running"; break; case "done": case "unanswered": return []; default: return record; } return [{ operationId: record.requestId, session: String(record.conversationId), status }]; }); } /** The opened pi Harness, for anything the interface does not cover. */ async pi() { return (await _assertClassBrand(_PiHarness_brand, this, _open).call(this)).pi; } /** @internal */ async conversation(session) { const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this); const conversation = await pi.conversation(conversationId(session), BG); if (!conversation) throw new Error(`Unknown pi session ${session}`); return conversation; } /** @internal */ async storage() { return (await _assertClassBrand(_PiHarness_brand, this, _open).call(this)).storage; } /** @internal */ async enqueue(session, input, options) { return _assertClassBrand(_PiHarness_brand, this, _admitDraft).call(this, session, options.operationId, { type: "input", content: input, whenBusy: options.whenBusy ?? "followUp", ...options.whenIdle ? { whenIdle: options.whenIdle } : {}, ...options.entry ? { entry: options.entry } : {} }); } /** @internal Start a run on the context as it is, without new input. */ enqueueContinue(session, options) { return _assertClassBrand(_PiHarness_brand, this, _admitDraft).call(this, session, options.operationId, { type: "continue" }); } /** @internal Withdraw a queued input, or abort the run it joined. */ async withdraw(session, operationId) { const { pi, storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this); const id = conversationId(session); const record = await storage.submissionByRequest(id, operationId, BG); if (!record || record.status === "done" || record.status === "unanswered") return false; const outcome = await pi.abortSubmission(record.id, BG, id); switch (outcome) { case "aborted": return true; case "already_placed": await (await pi.conversation(id, BG))?.abort(BG); return true; case "settled": case "not_found": return false; default: return outcome; } } /** @internal Wait for pi to settle an operation, by its request id. */ async settled(session, operationId, signal) { const conversation = await this.conversation(session); const context = signalContext(signal); const submission = await _assertClassBrand(_PiHarness_brand, this, _findSubmission).call(this, conversation.id, operationId, context); if (!submission) return { operationId, session, status: "unanswered", reason: "not_found" }; return _assertClassBrand(_PiHarness_brand, this, _result).call(this, session, operationId, await submission.wait(context), context); } /** @internal The per-conversation defaults supported by pi-durable. */ agentDefaults() { const defaults = _classPrivateFieldGet2(_options, this).defaults; return { ...defaults?.model === void 0 ? {} : { model: modelRef(defaults.model) }, ...defaults?.thinkingLevel === void 0 ? {} : { thinkingLevel: defaults.thinkingLevel }, ...defaults?.maxTurns === void 0 ? {} : { maxTurns: defaults.maxTurns } }; } }; async function _admitDraft(session, requested, draft) { const operationId = requested ?? crypto.randomUUID(); const { storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this); const conversation = await this.conversation(session); return { operationId, session, accepted: await _assertClassBrand(_PiHarness_brand, this, _admit).call(this, async () => { const accepted = await storage.submissionByRequest(conversation.id, operationId, BG) === void 0; await conversation.submit({ ...draft, requestId: operationId }, BG); return accepted; }) }; } /** * Hand pi work inside the wake. The wake is scheduled first, so if the * object dies after pi has the work, that job restarts it; while `hand` * is in flight the step will not complete. The wake steps again after, so * it sees the work even if it just completed. */ async function _admit(hand) { var _this$admitting; _classPrivateFieldSet2(_admitting, this, (_this$admitting = _classPrivateFieldGet2(_admitting, this), _this$admitting++, _this$admitting)); let result; try { await _assertClassBrand(_PiHarness_brand, this, _wake).call(this); result = await hand(); } finally { var _this$admitting3; _classPrivateFieldSet2(_admitting, this, (_this$admitting3 = _classPrivateFieldGet2(_admitting, this), _this$admitting3--, _this$admitting3)); } await _assertClassBrand(_PiHarness_brand, this, _wake).call(this); return result; } /** Schedule the wake job now, or at `time`. */ function _wake(time = Date.now()) { return this.lifecycle.jobs.push({ id: WAKE_JOB_ID, fn: WAKE_FN, time, singleflight: true, recoveryLoop: true }); } /** Schedule the wake job for what `pi.wake()` says, if pi needs one. */ async function _wakeFor(wake) { if (wake.active) await _assertClassBrand(_PiHarness_brand, this, _wake).call(this); else if (wake.at !== void 0) await _assertClassBrand(_PiHarness_brand, this, _wake).call(this, wake.at); } /** * One run of the wake job. It never admits or replays anything: it reads * `pi.wake()`, which is all of pi's sessions at once. Running work is * waited on; a sleep reschedules the job for when it ends; no work * completes the job. pi does all the work in between. */ async function _wakeStep() { const heartbeat = { rescheduleAt: Date.now() + _classPrivateFieldGet2(_heartbeatMs, this) }; if (_classPrivateFieldGet2(_waiting, this)) return heartbeat; const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this); pi.resume(); const wake = pi.wake(); if (wake.active) { const wait = _assertClassBrand(_PiHarness_brand, this, _waitWhileActive).call(this, pi).then((ended) => { _classPrivateFieldSet2(_waiting, this, void 0); if (ended) _assertClassBrand(_PiHarness_brand, this, _wake).call(this); }, (error) => { _classPrivateFieldSet2(_waiting, this, void 0); throw error; }); _classPrivateFieldSet2(_waiting, this, wait); if (!this.lifecycle.trackAlarmWork(wait)) wait.catch(() => {}); return heartbeat; } if (_classPrivateFieldGet2(_admitting, this) > 0) return heartbeat; if (wake.at !== void 0) return { rescheduleAt: wake.at }; } /** * Wait while pi has work running, up to the wait budget. With `onWake`, a * change ends each sleep early, and so does pi closing; without it, the * sleep is `pollMs`. A task sleeping beside the running work, such as a * retry backoff, also ends the sleep when it is due, and the wait calls * `pi.resume()` to run it: with `onWake`, pi arms no timer for it, and * the wake job's alarm is not set for it while the job waits. Whether the * wait ended, with pi idle or at the budget, rather than failing. Rejects * with a memory-limit reset. */ async function _waitWhileActive(pi) { const deadline = Date.now() + _classPrivateFieldGet2(_waitBudgetMs, this); try { let wake = pi.wake(); while (wake.active) { const now = Date.now(); const left = deadline - now; if (left <= 0) return true; let sleep = _classPrivateFieldGet2(_onWakeWired, this) ? left : Math.min(left, _classPrivateFieldGet2(_pollMs, this)); if (wake.at !== void 0) sleep = Math.min(sleep, wake.at - now); await new Promise((resolve) => { const timer = setTimeout(resolve, sleep); _classPrivateFieldSet2(_changed, this, () => { clearTimeout(timer); resolve(); }); }); _classPrivateFieldSet2(_changed, this, void 0); if (wake.at !== void 0 && Date.now() >= wake.at) pi.resume(); wake = pi.wake(); } return true; } catch (error) { if (!_classPrivateFieldGet2(_closing, this).has(pi)) _assertClassBrand(_PiHarness_brand, this, _wakeError).call(this, error); if (isDurableObjectMemoryLimitReset(error)) throw error; return false; } } function _wakeError(error) { this.lifecycle.events.emit("pi:wake_error", { error: error instanceof Error ? error.message : String(error) }); } /** * pi, opened. Waits for Lifecycle startup first, so pi is only ever * opened inside it: startup holds the input gate (`blockConcurrencyWhile`) * and awaits the open in `onStart`, so an open begun before startup would * be awaited behind a closed gate, with its timers and I/O held back, and * never finish. * * pi stays open until `dispose()`, or until it closes on its own: any * storage error fails pi's Session for good. Then the next call, or the * wake job's next step, opens pi again once the failed pi has closed, and * pi recovers from what it had committed. Closing waits for running task * code to return, so code that ignores its abort signal delays the reopen. * An open that fails is not kept either, so the next call tries again. */ async function _open() { await this.lifecycle.ready(); if (_classPrivateFieldGet2(_opening, this) === void 0) { const opening = _assertClassBrand(_PiHarness_brand, this, _doOpen).call(this, (pi, closed) => _assertClassBrand(_PiHarness_brand, this, _piClosing).call(this, opening, pi, closed)).catch((error) => { if (_classPrivateFieldGet2(_opening, this) === opening) _classPrivateFieldSet2(_opening, this, void 0); throw error; }); _classPrivateFieldSet2(_opening, this, opening); } return _classPrivateFieldGet2(_opening, this); } async function _doOpen(onClose) { await _classPrivateFieldGet2(_lastClose, this); const storage = await openPiSessionStore(this.lifecycle.storage); const pi = await _classPrivateFieldGet2(_options, this).harness({ storage, context: BG, onWake: _classPrivateFieldGet2(_onWake, this) }); try { const closed = pi.closed; pi.subscribeClose(() => onClose(pi, closed)); await pi.root(BG, { agent: this.agentDefaults() }); pi.resume(); await new Promise((resolve) => setTimeout(resolve, 0)); if (!_classPrivateFieldGet2(_onWakeWired, this)) console.warn("PiHarness: the harness factory did not pass onWake to Harness.open, so pi's timers keep this Durable Object in memory while a task sleeps. Pass it: harness: ({ storage, context, onWake }) => Harness.open(storage, { ...options, onWake }, context)"); await _assertClassBrand(_PiHarness_brand, this, _wakeFor).call(this, pi.wake()); } catch (error) { pi.close(BG).catch(() => {}); throw error; } return { pi, storage }; } /** * pi began closing: on its own, as a storage error makes it, or by * `dispose()`. */ function _piClosing(opening, pi, closed) { _classPrivateFieldGet2(_closing, this).add(pi); _classPrivateFieldGet2(_changed, this)?.call(this); if (_classPrivateFieldGet2(_opening, this) !== opening) return; _classPrivateFieldSet2(_opening, this, void 0); _classPrivateFieldSet2(_lastClose, this, closed); } async function _findSubmission(id, operationId, context) { const { pi, storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this); const record = await storage.submissionByRequest(id, operationId, context); return record ? pi.submission(record.id, context) : void 0; } async function _result(session, operationId, settled, context) { if (settled.status === "unanswered") return { operationId, session, status: "unanswered", reason: settled.reason }; if (settled.type === "write") return { operationId, session, status: "done" }; return { operationId, session, status: "done", text: assistantText((await (await this.conversation(session)).entries({ minEntryId: settled.answer, maxEntryId: settled.answer }, 1, void 0, context)).items[0]) }; } var _harness = /* @__PURE__ */ new WeakMap(); /** One pi conversation, addressed through the harness. */ var PiSession = class { constructor(harness, id) { _classPrivateFieldInitSpec(this, _harness, void 0); _classPrivateFieldSet2(_harness, this, harness); this.id = id; } /** Durably submit a prompt. Resolves before the model runs. */ submit(input, options = {}) { return _classPrivateFieldGet2(_harness, this).enqueue(this.id, input, options); } /** 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() }; } /** * Start a run on the transcript as it is, without a new message: for * example, after a run ended unanswered because the model failed or the * object kept crashing. Only an idle session takes one; a busy one rejects * with pi-durable's `ConversationBusy`. A transcript that ends in an answer * settles `unanswered` with reason `nothing_to_answer`. */ continue(options = {}) { return _classPrivateFieldGet2(_harness, this).enqueueContinue(this.id, options); } /** Join the running work after its current tool round. */ steer(input, options = {}) { return this.submit(input, { ...options, whenBusy: "steer" }); } /** Wait for an operation to settle. Aborting `signal` stops only the wait. */ wait(operationId, signal) { return _classPrivateFieldGet2(_harness, this).settled(this.id, operationId, signal); } /** * Withdraw one queued operation (or abort the run it joined), or, with no * id, abort the session: pi withdraws queued inputs and aborts the * running work. */ async abort(operationId) { if (operationId !== void 0) return _classPrivateFieldGet2(_harness, this).withdraw(this.id, operationId); await (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).abort(BG); return true; } /** Start a new context, optionally from a handoff note. */ async reset(handoff) { await (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).reset(handoff, BG); } /** * Change this session's model, by its `provider` and `id`; it must be on * the `Models` the factory opened pi with (see `PiModel`). */ async setModel(model) { await (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).configure({ model: modelRef(model) }, BG); } /** The active transcript, as pi's entries since the newest reset. */ async messages() { return [...(await (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).context(BG)).entries]; } /** * Watch this session: pi's conversation view, a window over the transcript * with the session's documents, such as `pi.live` (the answer being * streamed, running tools, and why the last run ended without an answer) * and `pi.inbox`. The first value holds the newest entries; new entries * are appended. Each change carries pi's operations, for a client that * forwards them. To render more than the window, merge each value into a * `ConversationTranscript` that pages with `entries()`. */ async watch(window, context = BG) { return (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).watch(context, window); } /** * A page of this session's transcript, newest first unless `query.order` * is `ascending`, including entries before its resets. The loader a * `ConversationTranscript` takes. */ async entries(query, limit, context = BG) { return (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).entries(query, limit, void 0, context); } async busy() { return (await (await _classPrivateFieldGet2(_harness, this).pi()).snapshot(LiveDoc, conversationId(this.id), BG))?.run !== void 0; } }; var _harness2 = /* @__PURE__ */ new WeakMap(); /** Every pi conversation in this object. */ var PiSessions = class { constructor(harness) { _classPrivateFieldInitSpec(this, _harness2, void 0); _classPrivateFieldSet2(_harness2, this, harness); } get(id) { return _classPrivateFieldGet2(_harness2, this).session(id); } /** A new top-level session, configured like the root. */ async create() { const conversation = await (await _classPrivateFieldGet2(_harness2, this).pi()).createConversation({ ownership: { kind: "ownerless" }, agent: _classPrivateFieldGet2(_harness2, this).agentDefaults() }, BG); return _classPrivateFieldGet2(_harness2, this).session(String(conversation.id)); } /** A new session that sees `from`'s history up to its newest entry. */ async fork(from) { const conversation = await _classPrivateFieldGet2(_harness2, this).conversation(from); const at = (await conversation.entries({}, 1, void 0, BG)).items[0]; if (!at) throw new Error(`Session ${from} has no entries to fork from`); const fork = await conversation.fork(at.id, { ownership: { kind: "ownerless" } }, BG); return _classPrivateFieldGet2(_harness2, this).session(String(fork.id)); } /** * Every conversation, including ones subagent tools own. Read from pi's * storage directly: the Harness has no conversation listing. */ async list() { const storage = await _classPrivateFieldGet2(_harness2, this).storage(); const pi = await _classPrivateFieldGet2(_harness2, this).pi(); const sessions = []; let cursor; for (;;) { const page = await storage.scanConversations({}, 100, cursor, BG); for (const record of page.items) { const live = await pi.snapshot(LiveDoc, record.id, BG); sessions.push({ id: String(record.id), ...record.parent ? { parent: String(record.parent.conversationId) } : {}, busy: live?.run !== void 0 }); } if (page.next === void 0) return sessions; cursor = page.next; } } }; //#endregion //#region src/harness/pi/skills.ts function textResult(text) { return { output: [{ type: "text", text }] }; } /** A failed tool call: pi sends it to the model as an error result. */ function errorResult(error) { return { output: [{ type: "text", text: error.message }], isError: true }; } /** * Model-facing tools for a catalog: `activate_skill` and * `read_skill_resource`. These match the tools `@cloudflare/think` offers, so * a skill behaves the same in both frameworks. */ function skillTools(catalog) { const names = catalog.skills.map((entry) => entry.skill.name); if (names.length === 0) return []; const nameSchema = StringEnum(names); return [{ name: "activate_skill", description: "Activate a skill by name. Use this when the user's task matches one of the available skills; the response contains the skill's full instructions.", parameters: obj({ name: nameSchema }, { open: true }), replay: "safe", async execute(input) { const activated = catalog.activate(input.name); return activated.ok ? textResult(activated.value) : errorResult(activated.error); } }, { name: "read_skill_resource", description: "Read a file bundled with a skill, such as a reference document or template. Provide the skill name and the file's path from the skill's resource list, or a qualified path like skill-name/references/file.md.", parameters: obj({ name: opt(nameSchema), path: str({ minLength: 1 }) }, { open: true }), replay: "safe", async execute(input) { const read = await catalog.readResource(input.name === void 0 ? { path: input.path } : { name: input.name, path: input.path }); return read.ok ? textResult(renderSkillResource(read.value)) : errorResult(read.error); } }]; } /** * `agents/skills` sources as a pi extension, named `agents.skills`: the * activation tools and a `skills` system prompt section. Install it on * pi's registry in the harness factory. Sources are read once, when the * extension is built. * * ```ts * import * as agentSkills from "agents/skills"; * * registry.install( * await skills([agentSkills.workspace(workspace), agentSkills.r2(env.SKILLS)]) * ); * ``` * * @param sources - Skill sources in priority order. * @returns The pi extension. */ async function skills(sources) { const catalog = await loadSkillCatalog(sources); for (const diagnostic of catalog.diagnostics) console.warn(`pi skills: ${formatSkillDiagnostic(diagnostic)}`); const prompt = catalog.prompt(); return { name: "agents.skills", tools: skillTools(catalog), sections: prompt === null ? [] : [{ key: "skills", render: () => prompt }] }; } //#endregion export { PiHarness, PiSession, PiSessions, ROOT_SESSION, openPiSessionStore, skills }; //# sourceMappingURL=index.js.map