agents
Version:
A home for your AI agents
736 lines (735 loc) • 30.1 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 { 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