diff --git a/apps/p2/docs/p2-notes.md b/apps/p2/docs/p2-notes.md index 72c896f96..82a58a926 100644 --- a/apps/p2/docs/p2-notes.md +++ b/apps/p2/docs/p2-notes.md @@ -1073,7 +1073,7 @@ translated phone drawer and intercept chat taps. by its author in the entry data, an agent-written one by its `origin`. Agent-written inputs record the writing turn's origin: subagent tasks and one-shot agents' prompts (`p2.subagent-task`), `team` messages and reports (`p2.agent-message`; a report has the origin of the subagent's run that answered, `answerOrigin`: its inputs after - the previous run's last answer), and peer messages (`p2.peer-message`). A guest's input steered into a run takes the + the previous run's final answer, `stop` or `length`, past mid-run error answers and compaction markers), and peer messages (`p2.peer-message`). A guest's input steered into a run takes the owner's tools away for the rest of it; a guest-started chain never reaches them. Known gap: inputs of earlier runs still in the context are not considered. `test/p2/inputs.test.ts`, `dev-tools.test.ts`, `tool-access.test.ts`. - **Code mode** (`plugins/codemode`): the worker facet registers Durable's `codemode` extension diff --git a/apps/p2/src/runtime/origin.ts b/apps/p2/src/runtime/origin.ts index b4f6302b6..2bfc27889 100644 --- a/apps/p2/src/runtime/origin.ts +++ b/apps/p2/src/runtime/origin.ts @@ -48,7 +48,11 @@ export function entryOrigin(entry: EntryRecord | undefined): ConversationOrigin /** * The origin of the run that ended with the answer `answer` of `conversationId`, read after it ended (a subagent's - * report): its inputs are the input entries after the previous run's last answer. The owner's only if all are. + * 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, @@ -61,9 +65,9 @@ export async function answerOrigin( const page = await tx.scanEntries({ conversationId, maxEntryId: (answer - 1) as EntryId }, 100, cursor); for (const entry of page.items) { const message = entry.model?.[0]; - // The previous run ended at its last answer; a reset or compaction marker starts the context afresh. - if (message?.role === "assistant" && message.stopReason !== "toolUse") return found ? "owner" : undefined; - if (entry.head !== undefined) return found ? "owner" : undefined; + 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; diff --git a/apps/p2/test/p2/inputs.test.ts b/apps/p2/test/p2/inputs.test.ts index d969d370f..5c396f3ce 100644 --- a/apps/p2/test/p2/inputs.test.ts +++ b/apps/p2/test/p2/inputs.test.ts @@ -163,5 +163,14 @@ describe("structured inputs", () => { // The guest's input before the previous answer belongs to the previous run. assert.equal(await answerOrigin(tx([]), 1 as ConversationId, 6 as EntryId), "owner"); assert.equal(await answerOrigin(tx([]), 1 as ConversationId, 3 as EntryId), undefined, "no input of its own"); + // A retried model error and an overflow compaction's marker are inside the run: the guest's input before them + // counts. + const midRun = [ + entry({ id: 6, kind: "p2.input", data: { author: ALICE, text: "x", attachments: [] }, model: user("x") }), + entry({ id: 7, kind: "pi.assistant", model: [{ role: "assistant", stopReason: "error", content: [] }] }), + entry({ id: 8, kind: "pi.compaction", head: 8, model: user("summary") }), + entry({ id: 9, kind: "pi.assistant", model: [{ role: "assistant", stopReason: "aborted", content: [] }] }), + ]; + assert.equal(await answerOrigin(tx(midRun), 1 as ConversationId, 10 as EntryId), undefined); }); });