mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
78 lines
3.8 KiB
TypeScript
78 lines
3.8 KiB
TypeScript
// Who started a tool's turn (worker): tools with `origin: "owner"` (`tool-access.ts`) and agents writing into another
|
|
// conversation (subagent tasks and reports, `team`, peer messages, one-shot agents) read it.
|
|
import type { Context } from "@earendil-works/chord/context";
|
|
import {
|
|
type ConversationId,
|
|
type EntryId,
|
|
type EntryRecord,
|
|
LiveDoc,
|
|
type ToolExecutionApi,
|
|
type Tx,
|
|
} from "@earendil-works/pi-durable";
|
|
import type { ConversationOrigin } from "./entries.ts";
|
|
import { messageData } from "./input.ts";
|
|
|
|
/**
|
|
* Who started the calling tool's turn: the owner, or nobody allowed (undefined). The turn is the conversation's current
|
|
* run, a code mode script's nested calls included: it is the owner's only if every input the run placed so far is
|
|
* (`entryOrigin`). A guest's message steered into the run takes it away for the rest of the run.
|
|
*/
|
|
export async function originOf(api: ToolExecutionApi, context: Context): Promise<ConversationOrigin | undefined> {
|
|
const inputs = (await api.snapshot(LiveDoc, api.conversationId, context))?.run?.inputs ?? [];
|
|
if (inputs.length === 0) return undefined;
|
|
return api.commit(async (tx) => {
|
|
let placed = 0;
|
|
for (const id of inputs) {
|
|
const submission = await tx.submission(id);
|
|
// A continue places no entry: it adds no input, and only inputs grant.
|
|
if (submission?.type === "continue") continue;
|
|
const entry = submission?.entry === undefined ? undefined : await tx.entry(submission.entry);
|
|
if (entryOrigin(entry) !== "owner") return undefined;
|
|
placed++;
|
|
}
|
|
return placed > 0 ? "owner" : undefined;
|
|
}, context);
|
|
}
|
|
|
|
/**
|
|
* The origin an input entry records in its data: a person's input is the owner's when its author is the owner; what an
|
|
* agent wrote (a subagent's task or report, a `team` or peer message, a one-shot agent's task) says `origin: "owner"`
|
|
* when the turn that wrote it was the owner's. Anything else, a tool's plain input included, grants nothing.
|
|
*/
|
|
export function entryOrigin(entry: EntryRecord | undefined): ConversationOrigin | undefined {
|
|
const person = messageData(entry?.data);
|
|
if (person) return person.author.kind === "owner" && person.author.id === "owner" ? "owner" : undefined;
|
|
const data = entry?.data;
|
|
return data && typeof data === "object" && !Array.isArray(data) && data.origin === "owner" ? "owner" : undefined;
|
|
}
|
|
|
|
/**
|
|
* The origin of the run that ended with the answer `answer` of `conversationId`, read after it ended (a subagent's
|
|
* report, when `pi.live.run` is gone): its inputs are the input entries after the previous run's final answer. The
|
|
* owner's only if all are. Only a final answer (`stop`, `length`) bounds the scan: a retried model error appends an
|
|
* `error` answer and an overflow compaction a head marker inside a run, and scanning past a failed or aborted run only
|
|
* adds inputs, so the result can only be stricter. (An `onYield` continuation after a final answer would hide the
|
|
* inputs before it; p2 installs no such hook.)
|
|
*/
|
|
export async function answerOrigin(
|
|
tx: Tx,
|
|
conversationId: ConversationId,
|
|
answer: EntryId,
|
|
): Promise<ConversationOrigin | undefined> {
|
|
let found = false;
|
|
let cursor: Parameters<Tx["scanEntries"]>[2];
|
|
for (;;) {
|
|
const page = await tx.scanEntries({ conversationId, maxEntryId: (answer - 1) as EntryId }, 100, cursor);
|
|
for (const entry of page.items) {
|
|
const message = entry.model?.[0];
|
|
if (message?.role === "assistant" && (message.stopReason === "stop" || message.stopReason === "length"))
|
|
return found ? "owner" : undefined;
|
|
// Durable's own entries with user messages (compaction summaries, handoffs) are not inputs.
|
|
if (message?.role !== "user" || (entry.kind.startsWith("pi.") && entry.kind !== "pi.user")) continue;
|
|
if (entryOrigin(entry) !== "owner") return undefined;
|
|
found = true;
|
|
}
|
|
if (!page.next) return found ? "owner" : undefined;
|
|
cursor = page.next;
|
|
}
|
|
}
|