mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
docs(durable): examples for turn limits and continue, and admission inside a commit
This commit is contained in:
parent
7e15f96019
commit
3ac64a074d
3 changed files with 178 additions and 0 deletions
|
|
@ -954,6 +954,8 @@ node --conditions=source --experimental-strip-types test/examples/13-chat.ts
|
|||
| [30-reload-and-restart](test/examples/30-reload-and-restart.ts) | Reloading an extension mid-call, and stored choices surviving a restart |
|
||||
| [31-transcript](test/examples/31-transcript.ts) | A client's transcript over the windowed view: live appends, a stall with a gap, loading it and earlier pages, and resuming with `from` |
|
||||
| [32-tests-after-edits](test/examples/32-tests-after-edits.ts) | An `afterRun` hook that runs the tests after every run that edited files, a fix run when they fail, and input waiting for them |
|
||||
| [33-turn-limit-and-continue](test/examples/33-turn-limit-and-continue.ts) | A per-conversation turn limit, `runFailure`, and continuing without a user message |
|
||||
| [34-submit-in-commit](test/examples/34-submit-in-commit.ts) | Admitting a visitor's steer inside the host's own commit with `submit(tx, draft)` and `whenIdle: "reject"` |
|
||||
| [00](test/examples/00-conversation.ts)–[12](test/examples/12-recovery.ts) | The layers underneath: sessions, documents, forks, watches, the Harness, agent configuration, reload, extension state, tasks, recovery |
|
||||
|
||||
Examples that call OpenAI need `OPENAI_API_KEY`; most use the faux provider otherwise.
|
||||
|
|
|
|||
73
packages/durable/test/examples/33-turn-limit-and-continue.ts
Normal file
73
packages/durable/test/examples/33-turn-limit-and-continue.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// A per-conversation turn limit stops a long run after a number of tool rounds; a continue resumes it without a user
|
||||
// message, with a fresh budget. A host learns that a run stopped from `pi.live.runFailure`.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/33-turn-limit-and-continue.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import {
|
||||
ConversationBusy,
|
||||
createRegistry,
|
||||
defineExtension,
|
||||
defineTool,
|
||||
Harness,
|
||||
LiveDoc,
|
||||
MemoryStorage,
|
||||
} from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
||||
const step = defineTool({
|
||||
name: "step",
|
||||
description: "Do one step of the work",
|
||||
parameters: Type.Object({ n: Type.Number() }),
|
||||
execute: async (args) => ({ output: [{ type: "text", text: `step ${args.n} done` }] }),
|
||||
});
|
||||
const call = (n: number) => fauxAssistantMessage(fauxToolCall("step", { n }), { stopReason: "toolUse" });
|
||||
|
||||
const faux = fauxProvider();
|
||||
const models = createModels();
|
||||
models.setProvider(faux.provider);
|
||||
// Four steps, then the answer. Two tool rounds per run are allowed.
|
||||
faux.setResponses([call(1), call(2), call(3), call(4), fauxAssistantMessage("All four steps are done.")]);
|
||||
|
||||
const registry = createRegistry();
|
||||
registry.install(defineExtension({ name: "steps", tools: [step] }));
|
||||
const harness = await Harness.open(new MemoryStorage(), { models, registry }, context);
|
||||
// Stored on the conversation, like its model: children and forks inherit it.
|
||||
const root = await harness.root(context, { agent: { model: { provider: "faux", modelId: "faux-1" }, maxTurns: 2 } });
|
||||
|
||||
const first = await (await root.submit({ type: "input", content: "Do the four steps." }, context)).wait(context);
|
||||
const failure = (await harness.snapshot(LiveDoc, root.id, context))?.runFailure;
|
||||
console.log("first run:", first.status, first.reason, `(${failure?.detail})`);
|
||||
|
||||
// An idle conversation runs a continue on the context as it is, the round's results last, with a fresh budget.
|
||||
const resumed = await root.submit({ type: "continue" }, context);
|
||||
// It never queues: while that run works, another continue is refused.
|
||||
try {
|
||||
await root.submit({ type: "continue" }, context);
|
||||
} catch (error) {
|
||||
if (!(error instanceof ConversationBusy)) throw error;
|
||||
console.log("second continue while busy: ConversationBusy");
|
||||
}
|
||||
const second = await resumed.wait(context);
|
||||
console.log("first continue:", second.status, second.status === "done" ? "" : second.reason);
|
||||
const third = await (await root.submit({ type: "continue" }, context)).wait(context);
|
||||
const live = await harness.snapshot(LiveDoc, root.id, context);
|
||||
console.log("second continue:", third.status, live?.runFailure === undefined ? "(runFailure cleared)" : "");
|
||||
|
||||
const page = await root.entries({ order: "ascending" }, 100, undefined, context);
|
||||
for (const entry of page.items) {
|
||||
const message = entry.model?.[0];
|
||||
if (message?.role === "user") console.log(">", message.content);
|
||||
if (message?.role === "toolResult")
|
||||
console.log(" tool:", message.content[0]?.type === "text" ? message.content[0].text : "");
|
||||
if (message?.role === "assistant" && message.stopReason === "stop") {
|
||||
console.log("agent:", message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join(""));
|
||||
}
|
||||
}
|
||||
// A continue after an answer has nothing to answer: it ends without a model request.
|
||||
const after = await (await root.submit({ type: "continue" }, context)).wait(context);
|
||||
console.log("continue after the answer:", after.status, after.reason);
|
||||
await harness.close(context);
|
||||
103
packages/durable/test/examples/34-submit-in-commit.ts
Normal file
103
packages/durable/test/examples/34-submit-in-commit.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// A host lets people message an agent while it works: a message must join the current run as a steer and never start a
|
||||
// run of its own. `submit(tx, draft)` admits it inside the host's own commit, together with the host's check and its own
|
||||
// state; `whenIdle: "reject"` refuses it when no run is working.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/34-submit-in-commit.ts
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import {
|
||||
ConversationIdle,
|
||||
createRegistry,
|
||||
defineDoc,
|
||||
defineExtension,
|
||||
defineTool,
|
||||
Harness,
|
||||
MemoryStorage,
|
||||
} from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
||||
// The host's own state: who may message this conversation, and what they sent.
|
||||
const Visitors = defineDoc<{ muted: string[]; sent: { from: string; text: string }[] }>({
|
||||
kind: "app.visitors",
|
||||
version: 1,
|
||||
scope: "conversation",
|
||||
history: "latest",
|
||||
fork: "initial",
|
||||
initial: () => ({ muted: [], sent: [] }),
|
||||
checkpointWhen: () => false,
|
||||
});
|
||||
|
||||
// A tool that waits until released, so messages arrive while the run works.
|
||||
let release!: () => void;
|
||||
const working = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const build = defineTool({
|
||||
name: "build",
|
||||
description: "Build the project",
|
||||
parameters: Type.Object({}),
|
||||
execute: async () => {
|
||||
await working;
|
||||
return { output: [{ type: "text", text: "built" }] };
|
||||
},
|
||||
});
|
||||
|
||||
const faux = fauxProvider();
|
||||
const models = createModels();
|
||||
models.setProvider(faux.provider);
|
||||
faux.setResponses([
|
||||
fauxAssistantMessage(fauxToolCall("build", {}), { stopReason: "toolUse" }),
|
||||
fauxAssistantMessage("Built, and I added the test Ana asked for."),
|
||||
]);
|
||||
const registry = createRegistry();
|
||||
registry.install(defineExtension({ name: "build", tools: [build] }));
|
||||
const harness = await Harness.open(new MemoryStorage(), { models, registry }, context);
|
||||
const root = await harness.root(context, { agent: { model: { provider: "faux", modelId: "faux-1" } } });
|
||||
await root.commit(async (tx) => {
|
||||
(await tx.doc(Visitors, root.id)).muted.push("bob");
|
||||
}, context);
|
||||
|
||||
/** A visitor's message: checked, admitted as a steer of the current run, and recorded, in one commit or not at all. */
|
||||
async function message(from: string, text: string, requestId: string): Promise<string> {
|
||||
try {
|
||||
return await harness.commit(async (tx) => {
|
||||
const visitors = await tx.doc(Visitors, root.id);
|
||||
if (visitors.muted.includes(from)) return `${from} is muted`;
|
||||
// Before the commit's own table writes; document writes may come before or after.
|
||||
const id = await root.submit(tx, {
|
||||
type: "input",
|
||||
content: `${from}: ${text}`,
|
||||
whenBusy: "steer",
|
||||
whenIdle: "reject",
|
||||
requestId,
|
||||
});
|
||||
visitors.sent.push({ from, text });
|
||||
return `${from}'s message is submission ${id}`;
|
||||
}, context);
|
||||
} catch (error) {
|
||||
if (error instanceof ConversationIdle) return `${from}: the agent isn't working now`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(await message("ana", "Add a test for the build.", "ana-1")); // idle: refused, nothing written
|
||||
const run = await root.submit({ type: "input", content: "Build the project." }, context);
|
||||
console.log(await message("ana", "Add a test for the build.", "ana-2")); // busy: joins the run as a steer
|
||||
console.log(await message("bob", "Delete everything.", "bob-1")); // the host's own check refuses it
|
||||
release();
|
||||
await run.wait(context);
|
||||
await root.waitForIdle(context);
|
||||
|
||||
const page = await root.entries({ order: "ascending" }, 100, undefined, context);
|
||||
for (const entry of page.items) {
|
||||
const message = entry.model?.[0];
|
||||
if (message?.role === "user") console.log(">", message.content);
|
||||
if (message?.role === "assistant" && message.stopReason === "stop") {
|
||||
console.log("agent:", message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join(""));
|
||||
}
|
||||
}
|
||||
console.log("recorded:", (await harness.snapshot(Visitors, root.id, context))?.sent);
|
||||
await harness.close(context);
|
||||
Loading…
Add table
Add a link
Reference in a new issue