mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
The artifact tool takes inline: true to also show the produced version below its transcript card: Markdown/text rendered, HTML/SVG in the sandboxed preview frame. Drawings fill the frame's width (an SVG with a viewBox or an absolute size scales to it; an HTML page that is only an SVG or image too) and the frame takes the content height the preview reports, capped at 70vh. Every viewer sees it, guests included (owner decision). The TUI renders inline Markdown and text. SVG previews now carry the diagnostics bridge in CDATA (its && broke the XML).
221 lines
9.6 KiB
TypeScript
221 lines
9.6 KiB
TypeScript
// The fake provider of demo and test mode (P2_FAKE, `startServer({ fake: true })`): prompts that make the faux model
|
|
// call a tool (SCRIPTED and the `<tool> <json>` forms), one-shot agents' sample answers, and echoes otherwise.
|
|
import { appendFileSync } from "node:fs";
|
|
import type { Message } from "@earendil-works/pi-ai";
|
|
import { type CreateModelsOptions, createModels } from "@earendil-works/pi-ai/models";
|
|
import {
|
|
fauxAssistantMessage,
|
|
fauxProvider,
|
|
fauxText,
|
|
fauxThinking,
|
|
fauxToolCall,
|
|
} from "@earendil-works/pi-ai/providers/faux";
|
|
import { SPEAKER_PREFIX } from "@p2/app/v1";
|
|
import { CONVERSATION_PREFIX } from "../conversations/contract.ts";
|
|
|
|
// Fake-mode prompts that make the faux model call one tool; anything else is echoed.
|
|
const SCRIPTED: Record<string, () => ReturnType<typeof fauxToolCall>> = {
|
|
tool: () => fauxToolCall("read", { path: "fixture.txt" }),
|
|
write: () => fauxToolCall("write", { path: "created.txt", content: "before\n" }),
|
|
edit: () => fauxToolCall("edit", { path: "created.txt", edits: [{ oldText: "before", newText: "after" }] }),
|
|
bash: () => fauxToolCall("bash", { command: "pwd" }),
|
|
"send-file": () => fauxToolCall("send_file", { path: "created.txt" }),
|
|
artifact: () =>
|
|
fauxToolCall("artifact", {
|
|
name: "demo",
|
|
title: "Demo artifact",
|
|
type: "html",
|
|
content: "<!doctype html><title>Demo</title><h1>Artifact demo</h1>",
|
|
}),
|
|
subagent: () =>
|
|
fauxToolCall("subagent", {
|
|
action: "spawn",
|
|
name: "helper",
|
|
message: "hello helper",
|
|
thinkingLevel: "high",
|
|
tools: ["read"],
|
|
}),
|
|
"subagent-configure": () =>
|
|
fauxToolCall("subagent", {
|
|
action: "configure",
|
|
name: "helper",
|
|
model: "faux/faux-1",
|
|
thinkingLevel: "high",
|
|
tools: ["bash", "artifact"],
|
|
}),
|
|
"subagent-second": () =>
|
|
fauxToolCall("subagent", { action: "spawn", name: "second", message: "hello second", tools: ["read"] }),
|
|
"subagent-answer": () => fauxToolCall("subagent", { action: "answer", name: "helper" }),
|
|
"subagent-send": () => fauxToolCall("subagent", { action: "send", name: "helper", message: "hello again" }),
|
|
"subagent-archive": () => fauxToolCall("subagent", { action: "archive", name: "helper" }),
|
|
"subagent-status": () => fauxToolCall("subagent", { action: "status" }),
|
|
"subagent-bad": () =>
|
|
fauxToolCall("subagent", { action: "spawn", name: "bad", message: "x", model: "nope/missing" }),
|
|
// Code mode: a script whose nested calls are two tools (codemode plugin).
|
|
codemode: () =>
|
|
fauxToolCall("codemode", {
|
|
code: [
|
|
'const written = await tools.bash({ command: "echo first >> codemode.log; printf written" }).then((result) => result.output);',
|
|
'const read = await tools.read({ path: "codemode.log" });',
|
|
'text("bash: " + written);',
|
|
"return read;",
|
|
].join("\n"),
|
|
}),
|
|
// Code mode across a worker restart: the slow middle call is interrupted, the script catches it and continues.
|
|
"codemode-restart": () =>
|
|
fauxToolCall("codemode", {
|
|
code: [
|
|
'const one = (await tools.bash({ command: "echo one >> codemode.log; printf one" })).output;',
|
|
"let two;",
|
|
'try { two = (await tools.bash({ command: "sleep 5; echo two >> codemode.log; printf two" })).output; }',
|
|
'catch (error) { two = "two failed: " + error.message; }',
|
|
'const three = (await tools.bash({ command: "echo three >> codemode.log; printf three" })).output;',
|
|
'return [one, two, three].join(" | ");',
|
|
].join("\n"),
|
|
}),
|
|
"slow-tool": () => fauxToolCall("bash", { command: "printf tool-running; sleep 4; printf tool-finished" }),
|
|
};
|
|
|
|
/** The system prompt of a one-shot agent (plugins/agent) ends its instructions with its schema on this line's end. */
|
|
const ONE_SHOT_SCHEMA = "matching this JSON Schema, without prose or code fences:\n";
|
|
|
|
/** The person's text of a user message, without the speaker or peer block in front. */
|
|
function userText(message: Message | undefined): string {
|
|
if (message?.role !== "user") return "";
|
|
if (typeof message.content === "string") return message.content;
|
|
const first = message.content[0];
|
|
const parts =
|
|
first?.type === "text" && (first.text.startsWith(SPEAKER_PREFIX) || first.text.startsWith(CONVERSATION_PREFIX))
|
|
? message.content.slice(1)
|
|
: message.content;
|
|
return parts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n");
|
|
}
|
|
|
|
/** A one-shot agent's answer schema, from its conversation's system messages. */
|
|
function oneShotSchema(messages: readonly Message[]): unknown {
|
|
for (const message of messages) {
|
|
if (message.role !== "system") continue;
|
|
const texts = [
|
|
typeof message.content === "string" ? message.content : message.content.map((part) => part.text).join(""),
|
|
...Object.values(message.sections ?? {}).flatMap((section) => (section ? [section] : [])),
|
|
];
|
|
for (const text of texts) {
|
|
const at = text.indexOf(ONE_SHOT_SCHEMA);
|
|
if (at >= 0) return JSON.parse(text.slice(at + ONE_SHOT_SCHEMA.length).split("\n")[0] ?? "null");
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* A small value matching a JSON Schema: the first enum value, every property, one array item, empty strings and
|
|
* minimums. Enough for code mode workflow scripts to run end to end in fake mode (docs/workflows.md).
|
|
*/
|
|
function sampleOf(schema: unknown): unknown {
|
|
if (typeof schema !== "object" || schema === null) return null;
|
|
const spec = schema as Record<string, unknown>;
|
|
if (Array.isArray(spec.enum)) return spec.enum[0];
|
|
if ("const" in spec) return spec.const;
|
|
switch (spec.type) {
|
|
case "object":
|
|
return Object.fromEntries(
|
|
Object.entries((spec.properties ?? {}) as Record<string, unknown>).map(([key, value]) => [
|
|
key,
|
|
sampleOf(value),
|
|
]),
|
|
);
|
|
case "array":
|
|
return spec.maxItems === 0 ? [] : [sampleOf(spec.items)];
|
|
case "string":
|
|
return "x".repeat(typeof spec.minLength === "number" ? spec.minLength : 0);
|
|
case "integer":
|
|
case "number":
|
|
return typeof spec.minimum === "number" ? spec.minimum : 0;
|
|
case "boolean":
|
|
return false;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Explicit demo/test mode. Responses depend on committed input, not a volatile
|
|
// queue position, so reopening an interrupted request is deterministic.
|
|
// Tests set P2_FAKE_REQUESTS (via workerEnv) to a file that receives each request's messages as one JSON line.
|
|
export function createFakeModels(options: CreateModelsOptions = {}) {
|
|
const requests = process.env.P2_FAKE_REQUESTS;
|
|
const models = createModels(options);
|
|
const faux = fauxProvider({ api: "p2-faux", tokensPerSecond: 200, tokenSize: { min: 4, max: 4 } });
|
|
models.setProvider({
|
|
...faux.provider,
|
|
streamSimple(model, context, options) {
|
|
if (requests) {
|
|
const messages = context.messages.map((message) => ({
|
|
role: message.role,
|
|
content:
|
|
typeof message.content === "string"
|
|
? message.content
|
|
: message.content.map((part) =>
|
|
part.type === "image" ? { type: "image", mimeType: part.mimeType } : part,
|
|
),
|
|
}));
|
|
appendFileSync(requests, `${JSON.stringify(messages)}\n`);
|
|
}
|
|
faux.appendResponses([
|
|
(transcript) => {
|
|
const text = userText(transcript.messages.findLast((message) => message.role === "user"));
|
|
// A one-shot agent answers a small value matching its schema, unless its task is a `json …` or `slow…`
|
|
// test prompt (exact answers, retries with non-JSON echoes, a long stream to stop).
|
|
const schema = oneShotSchema(transcript.messages);
|
|
const task = userText(transcript.messages.find((message) => message.role === "user"));
|
|
if (schema !== undefined && !task.startsWith("json ") && !task.startsWith("slow"))
|
|
return fauxAssistantMessage([fauxText(JSON.stringify(sampleOf(schema)))]);
|
|
// A section that changed during the run (tools loaded by `tool_search`) adds a system message after the result.
|
|
const last = transcript.messages.findLast((message) => message.role !== "system");
|
|
if (last?.role !== "toolResult") {
|
|
const call = Object.hasOwn(SCRIPTED, text)
|
|
? SCRIPTED[text]?.()
|
|
: text.startsWith("conversations ")
|
|
? fauxToolCall("conversations", JSON.parse(text.slice("conversations ".length)))
|
|
: text.startsWith("rename-session ")
|
|
? fauxToolCall("rename_session", { title: text.slice("rename-session ".length) })
|
|
: text.startsWith("ports ")
|
|
? fauxToolCall("ports", JSON.parse(text.slice("ports ".length)))
|
|
: // `codemode <script>` runs that script (the bare `codemode` above is a fixed one).
|
|
text.startsWith("codemode ")
|
|
? fauxToolCall("codemode", { code: text.slice("codemode ".length) })
|
|
: text.startsWith("subagent ")
|
|
? fauxToolCall("subagent", JSON.parse(text.slice("subagent ".length)))
|
|
: text.startsWith("team ")
|
|
? fauxToolCall("team", JSON.parse(text.slice("team ".length)))
|
|
: /^(artifact|history_(search|read|handoff)|location_(envs|dirs|move)|tool_search|mcp__\w+) /.test(
|
|
text,
|
|
)
|
|
? fauxToolCall(
|
|
text.slice(0, text.indexOf(" ")),
|
|
JSON.parse(text.slice(text.indexOf(" ") + 1)),
|
|
)
|
|
: undefined;
|
|
if (call)
|
|
return fauxAssistantMessage(
|
|
[fauxText(text === "tool" ? "Reading fixture." : `Running ${call.name}.`), call],
|
|
{ stopReason: "toolUse" },
|
|
);
|
|
}
|
|
return fauxAssistantMessage([
|
|
fauxThinking("private-thinking-sentinel"),
|
|
// `json <text>` answers exactly `<text>` (code mode's one-shot agents parse it).
|
|
fauxText(
|
|
text.startsWith("json ")
|
|
? text.slice("json ".length)
|
|
: text.startsWith("slow")
|
|
? `Echo: ${text} ${"streaming ".repeat(300)}`
|
|
: `Echo: ${text}`,
|
|
),
|
|
]);
|
|
},
|
|
]);
|
|
return faux.provider.streamSimple(model, context, options);
|
|
},
|
|
});
|
|
return models;
|
|
}
|