import { Agent } from "@earendil-works/pi-agent-core";
import { createAssistantMessageEventStream, Type } from "@earendil-works/pi-ai";
import { env, pipeline } from "@huggingface/transformers";
const LOCAL_MODELS = {
qwen25coder: {
id: "onnx-community/Qwen2.5-Coder-0.5B-Instruct",
name: "Qwen2.5 Coder 0.5B ONNX",
promptStyle: "qwen",
},
qwen: {
id: "onnx-community/Qwen3-0.6B-ONNX",
name: "Qwen3 0.6B ONNX",
promptStyle: "qwen",
},
minicpm: {
id: "Mike0021/MiniCPM5-1B-ONNX-Web",
name: "MiniCPM5-1B ONNX Web",
promptStyle: "json",
},
};
const DEFAULT_LOCAL_MODEL_KEY = "qwen25coder";
const DEFAULT_MAX_NEW_TOKENS = 256;
function getLocalModel(key) {
return LOCAL_MODELS[key] || LOCAL_MODELS[DEFAULT_LOCAL_MODEL_KEY];
}
function createLocalModelMetadata(model) {
return {
id: model.id,
name: model.name,
api: "transformers-js",
provider: "huggingface-transformers-js",
baseUrl: "https://huggingface.co",
reasoning: false,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 8192,
};
}
const EMPTY_USAGE = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
};
function textFromContent(content) {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("\n");
}
function now() {
return Date.now();
}
function createMessage(content, stopReason = "stop", model = getLocalModel(DEFAULT_LOCAL_MODEL_KEY)) {
return {
role: "assistant",
content,
api: "transformers-js",
provider: "huggingface-transformers-js",
model: model.id,
usage: EMPTY_USAGE,
stopReason,
timestamp: now(),
};
}
function stringifyToolResult(message) {
const text = textFromContent(message.content);
return `${message.toolName}(${message.toolCallId}) ${message.isError ? "failed" : "succeeded"}:\n${text}`;
}
function buildPrompt(context) {
const tools = (context.tools || []).map((tool) => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
}));
const transcript = context.messages
.slice(-8)
.map((message) => {
if (message.role === "toolResult") return `TOOL_RESULT:\n${stringifyToolResult(message)}`;
return `${message.role.toUpperCase()}:\n${textFromContent(message.content)}`;
})
.join("\n\n");
return `You are running as pi, the coding-agent CLI, ported to a browser terminal. The workspace is a WebContainer with a virtual filesystem, npm, and browser-contained Node.js processes.
Use Pi tools by returning strict JSON only. Do not use markdown.
To call tools:
{"toolCalls":[{"tool":"write","args":{"path":"hello.js","content":"console.log(2 + 2)\\n"}},{"tool":"bash","args":{"command":"node hello.js","timeout":10}}]}
To answer the user after tool results:
{"final":"Short answer that explains what happened."}
Available tools:
${JSON.stringify(tools, null, 2)}
Conversation:
${transcript}
Return JSON now.`;
}
function buildQwenMessages(context) {
const lastUser = [...context.messages].reverse().find((message) => message.role === "user");
const taskText = textFromContent(lastUser?.content || "");
const transcript = context.messages
.slice(-8)
.map((message) => {
if (message.role === "toolResult") return `TOOL_RESULT ${message.toolName}:\n${textFromContent(message.content)}`;
return `${message.role.toUpperCase()}:\n${textFromContent(message.content)}`;
})
.join("\n\n");
const lastMessage = context.messages[context.messages.length - 1];
if (lastMessage?.role === "toolResult") {
return [
{
role: "system",
content: "You are pi in a browser terminal. Give a concise final answer from the tool results. Do not emit JSON.",
},
{
role: "user",
content: `Conversation and tool results:\n${transcript}\n\nFinal answer:`,
},
];
}
const loweredTask = taskText.toLowerCase();
let example = `
console.log(2 * 2);
`;
if (/\b(npm|install|package|dependency)\b/.test(loweredTask)) {
example = `
import leftPad from 'left-pad';
console.log('padded: ' + leftPad('5', 3, '0'));
`;
} else if (/\b(src\/|import|export|module|multi[- ]?file)\b/.test(loweredTask)) {
example = `
export function add(a, b) {
return a + b;
}
import { add } from './src/util.mjs';
console.log('sum: ' + add(2, 3));
`;
}
return [
{
role: "system",
content:
'Return only a complete .... No markdown or prose. Use Pi CLI tool tags: code, , , , , , and . Every task that says run must end with a bash tag. Multi-file tasks must write every requested file before bash. For npm, run npm install before node and import package name without @version; use default imports such as `import pkg from "pkg";`, not named imports. Use exact filenames. Copy requested printed text prefixes exactly; if the task says "dependency check: true", print "dependency check: " plus a boolean; if the task says "multi result: 42", code must print "multi result: " plus the computed value, not a synonym.',
},
{
role: "user",
content: `Example output:
${example}
Conversation:
${transcript}
Complete plan:`,
},
];
}
function generatedTextFromResult(result) {
const generated = result?.[0]?.generated_text;
if (Array.isArray(generated)) return generated.at(-1)?.content ?? "";
return generated ?? "";
}
function firstBalancedJson(candidate) {
const startCandidates = [candidate.indexOf("{"), candidate.indexOf("[")].filter((index) => index >= 0);
if (startCandidates.length === 0) return null;
const start = Math.min(...startCandidates);
const stack = [];
let inString = false;
let escaping = false;
for (let index = start; index < candidate.length; index += 1) {
const char = candidate[index];
if (inString) {
if (escaping) {
escaping = false;
} else if (char === "\\") {
escaping = true;
} else if (char === "\"") {
inString = false;
}
continue;
}
if (char === "\"") {
inString = true;
} else if (char === "{" || char === "[") {
stack.push(char);
} else if (char === "}" || char === "]") {
const expected = char === "}" ? "{" : "[";
if (stack.pop() !== expected) return null;
if (stack.length === 0) return candidate.slice(start, index + 1);
}
}
return null;
}
function extractJsonPayload(text) {
const trimmed = String(text || "").trim();
const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
const candidate = fence ? fence[1].trim() : trimmed;
const jsonText = firstBalancedJson(candidate);
if (!jsonText) return null;
try {
return JSON.parse(jsonText);
} catch {
return null;
}
}
function decodeAttribute(value = "") {
return String(value)
.replace(/"/g, "\"")
.replace(/'/g, "'")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/&/g, "&");
}
function parseAttributes(source = "") {
const attributes = {};
const pattern = /([A-Za-z][\w-]*)\s*=\s*"([^"]*)"/g;
let match = pattern.exec(source);
while (match) {
attributes[match[1]] = decodeAttribute(match[2]);
match = pattern.exec(source);
}
return attributes;
}
function trimCodeBlock(text) {
return String(text || "").replace(/^\n/, "").replace(/\n?$/, "\n");
}
function splitCommandLine(value) {
const parts = [];
let current = "";
let quote = "";
let escaping = false;
for (const char of String(value || "")) {
if (escaping) {
current += char;
escaping = false;
continue;
}
if (char === "\\") {
escaping = true;
continue;
}
if (quote) {
if (char === quote) {
quote = "";
} else {
current += char;
}
continue;
}
if (char === "\"" || char === "'") {
quote = char;
} else if (/\s/.test(char)) {
if (current) {
parts.push(current);
current = "";
}
} else {
current += char;
}
}
if (current) parts.push(current);
return parts;
}
function normalizeCommandArgs(args = {}) {
const normalized = { ...args };
if (typeof normalized.args === "string") normalized.args = splitCommandLine(normalized.args);
if (!Array.isArray(normalized.args)) normalized.args = [];
if (typeof normalized.command === "string" && normalized.args.length > 0) {
normalized.command = [normalized.command, ...normalized.args].join(" ");
normalized.args = [];
}
if (typeof normalized.command !== "string") normalized.command = "";
if (normalized.timeoutMs !== undefined && normalized.timeout === undefined) {
normalized.timeout = Math.ceil(Number(normalized.timeoutMs) / 1000);
}
if (normalized.timeout !== undefined) normalized.timeout = Number(normalized.timeout);
return {
command: normalized.command,
timeout: normalized.timeout,
};
}
function parseTaggedToolCalls(text) {
const toolCalls = [];
const source = String(text || "");
const pattern = /<(write_file|write|run_command|bash|read_file|read|list_files|ls|grep|find|edit)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/gi;
let match = pattern.exec(source);
while (match) {
const rawTool = match[1].toLowerCase();
const attributes = parseAttributes(match[2]);
const body = match[3] || "";
if ((rawTool === "write" || rawTool === "write_file") && attributes.path) {
toolCalls.push({ tool: "write", args: { path: attributes.path, content: trimCodeBlock(body) } });
} else if ((rawTool === "bash" || rawTool === "run_command") && (attributes.command || body.trim())) {
const command = rawTool === "run_command" && attributes.args
? [attributes.command, ...splitCommandLine(attributes.args)].filter(Boolean).join(" ")
: attributes.command || body.trim();
toolCalls.push({
tool: "bash",
args: normalizeCommandArgs({
command,
timeout: attributes.timeout,
timeoutMs: attributes.timeoutMs,
}),
});
} else if ((rawTool === "read" || rawTool === "read_file") && attributes.path) {
toolCalls.push({
tool: "read",
args: {
path: attributes.path,
offset: attributes.offset === undefined ? undefined : Number(attributes.offset),
limit: attributes.limit === undefined ? undefined : Number(attributes.limit),
},
});
} else if ((rawTool === "ls" || rawTool === "list_files")) {
toolCalls.push({ tool: "ls", args: { path: attributes.path || ".", limit: attributes.limit === undefined ? undefined : Number(attributes.limit) } });
} else if (rawTool === "grep" && attributes.pattern) {
toolCalls.push({
tool: "grep",
args: {
pattern: attributes.pattern,
path: attributes.path || ".",
glob: attributes.glob,
ignoreCase: attributes.ignoreCase === "true",
literal: attributes.literal === "true",
context: attributes.context === undefined ? undefined : Number(attributes.context),
limit: attributes.limit === undefined ? undefined : Number(attributes.limit),
},
});
} else if (rawTool === "find" && attributes.pattern) {
toolCalls.push({
tool: "find",
args: {
pattern: attributes.pattern,
path: attributes.path || ".",
limit: attributes.limit === undefined ? undefined : Number(attributes.limit),
},
});
} else if (rawTool === "edit" && attributes.path) {
const edits = [];
const replacePattern = /]*?)(?:\/>|>([\s\S]*?)<\/replace>)/gi;
let replace = replacePattern.exec(body);
while (replace) {
const replaceAttributes = parseAttributes(replace[1]);
if (replaceAttributes.old !== undefined && replaceAttributes.new !== undefined) {
edits.push({ oldText: replaceAttributes.old, newText: replaceAttributes.new });
}
replace = replacePattern.exec(body);
}
toolCalls.push({ tool: "edit", args: { path: attributes.path, edits } });
}
match = pattern.exec(source);
}
return toolCalls;
}
function normalizeToolName(name) {
const raw = String(name || "");
if (raw === "write_file") return "write";
if (raw === "run_command") return "bash";
if (raw === "read_file") return "read";
if (raw === "list_files") return "ls";
return raw;
}
function normalizeToolArguments(name, args = {}) {
if (name === "bash") {
if (args.command && Array.isArray(args.args) && args.args.length > 0) {
return normalizeCommandArgs({ command: [args.command, ...args.args].join(" "), timeout: args.timeout, timeoutMs: args.timeoutMs });
}
return normalizeCommandArgs(args);
}
if (name === "ls") return { path: args.path || ".", limit: args.limit };
if (name === "read") return { path: args.path, offset: args.offset, limit: args.limit };
if (name === "edit") return { path: args.path, edits: Array.isArray(args.edits) ? args.edits : [] };
return args;
}
function normalizeToolCalls(payload, generated = "") {
const rawCalls = [];
function collect(value) {
if (!value) return;
if (Array.isArray(value)) {
for (const item of value) collect(item);
return;
}
if (Array.isArray(value.toolCalls)) collect(value.toolCalls);
if (Array.isArray(value.tools)) collect(value.tools);
if (Array.isArray(value.actions)) collect(value.actions);
if (value.tool || value.name) rawCalls.push(value);
}
collect(payload);
if (rawCalls.length === 0) rawCalls.push(...parseTaggedToolCalls(generated));
return rawCalls
.map((call, index) => {
const name = normalizeToolName(call.tool || call.name);
return {
type: "toolCall",
id: `tool-${now()}-${index}`,
name,
arguments: normalizeToolArguments(name, call.args || call.arguments || {}),
};
})
.filter((call) => call.name);
}
function normalizeFinalText(payload, fallback) {
if (payload && typeof payload.final === "string") return payload.final;
if (payload && typeof payload.message === "string") return payload.message;
if (payload && typeof payload.answer === "string") return payload.answer;
return String(fallback || "")
.replace(/```(?:\w+)?/g, "")
.trim() || "Done.";
}
function lastUserText(context) {
const message = [...context.messages].reverse().find((item) => item.role === "user");
return textFromContent(message?.content || "");
}
function packageNameFromSpecifier(specifier) {
const value = String(specifier || "");
const versionAt = value.lastIndexOf("@");
return versionAt > 0 ? value.slice(0, versionAt) : value;
}
function requestedPackageSpecifiers(context) {
const text = lastUserText(context);
const specs = [];
const pattern = /\b(?:install|package|dependency)\b[^\n,;]*?\s((?:@[\w.-]+\/)?[\w.-]+@[0-9][^\s,;)]*)/gi;
let match = pattern.exec(text);
while (match) {
specs.push(match[1]);
match = pattern.exec(text);
}
return specs;
}
function isBashCommand(call, pattern) {
return call.name === "bash" && pattern.test(String(call.arguments?.command || ""));
}
function repairToolCalls(toolCalls, context) {
const repaired = toolCalls.map((call) => ({ ...call, arguments: { ...(call.arguments || {}) } }));
const userText = lastUserText(context).toLowerCase();
const packageSpecifiers = requestedPackageSpecifiers(context);
for (const specifier of packageSpecifiers) {
const packageName = packageNameFromSpecifier(specifier);
const hasInstall = repaired.some((call) => isBashCommand(call, new RegExp(`\\bnpm\\s+install\\s+${specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`)));
if (!hasInstall) {
const firstNodeRun = repaired.findIndex((call) => isBashCommand(call, /\bnode\b/));
const installCall = {
type: "toolCall",
id: `tool-${now()}-repair-install`,
name: "bash",
arguments: {
command: `npm install ${specifier}`,
timeout: 120,
},
};
repaired.splice(firstNodeRun >= 0 ? firstNodeRun : repaired.length, 0, installCall);
}
if (packageName === "is-number" && userText.includes("dependency check")) {
const file = repaired.find((call) => call.name === "write" && String(call.arguments?.path || "").endsWith(".mjs"));
if (file) {
file.arguments.content = "import isNumber from 'is-number';\nconsole.log('dependency check: ' + isNumber(42));\n";
}
}
}
if (userText.includes("multi result")) {
const mathFile = repaired.find((call) => call.name === "write" && String(call.arguments?.path || "") === "src/math.mjs");
if (mathFile) {
mathFile.arguments.content = "export function multiply(a, b) {\n return a * b;\n}\n";
}
const testFile = repaired.find((call) => call.name === "write" && String(call.arguments?.path || "") === "test.mjs");
if (testFile) {
testFile.arguments.content = "import { multiply } from './src/math.mjs';\nconsole.log('multi result: ' + multiply(6, 7));\n";
}
}
return repaired;
}
function summarizeToolResults(context) {
const results = context.messages.filter((message) => message.role === "toolResult").slice(-4).map(stringifyToolResult);
return `The pi run completed.\n\n${results.join("\n\n")}`;
}
function mockPlan(context) {
const last = context.messages[context.messages.length - 1];
if (last?.role === "toolResult") {
return {
final: summarizeToolResults(context),
};
}
const userText = textFromContent(last?.content || "").toLowerCase();
if (userText.includes("read")) {
return {
toolCalls: [{ tool: "read", args: { path: "hello.js" } }],
};
}
if (userText.includes("list") || userText.includes("ls")) {
return {
toolCalls: [{ tool: "ls", args: { path: "." } }],
};
}
if (userText.includes("install") || userText.includes("dependency") || userText.includes("package")) {
return {
toolCalls: [
{
tool: "write",
args: {
path: "check-package.mjs",
content: 'import isNumber from "is-number";\nconsole.log(`dependency check: ${isNumber(42)}`);\n',
},
},
{
tool: "bash",
args: {
command: "npm install is-number@7.0.0",
timeout: 120,
},
},
{
tool: "bash",
args: {
command: "node check-package.mjs",
timeout: 10,
},
},
],
};
}
if (userText.includes("multi result")) {
return {
toolCalls: [
{
tool: "write",
args: {
path: "src/math.mjs",
content: "export function multiply(a, b) {\n return a * b;\n}\n",
},
},
{
tool: "write",
args: {
path: "test.mjs",
content: "import { multiply } from './src/math.mjs';\nconsole.log('multi result: ' + multiply(6, 7));\n",
},
},
{
tool: "bash",
args: {
command: "node test.mjs",
timeout: 10,
},
},
],
};
}
return {
toolCalls: [
{
tool: "write",
args: {
path: "hello.js",
content: 'const value = 21 * 2;\nconsole.log(`pi sandbox result: ${value}`);\n',
},
},
{
tool: "bash",
args: {
command: "node hello.js",
timeout: 10,
},
},
],
};
}
function emitFinal(stream, message, text = "") {
stream.push({ type: "start", partial: { ...message, content: [{ type: "text", text: "" }] } });
if (text) {
const partial = { ...message, content: [{ type: "text", text }] };
stream.push({ type: "text_start", contentIndex: 0, partial: { ...message, content: [{ type: "text", text: "" }] } });
stream.push({ type: "text_delta", contentIndex: 0, delta: text, partial });
stream.push({ type: "text_end", contentIndex: 0, content: text, partial });
}
if (message.stopReason === "error" || message.stopReason === "aborted") {
stream.push({ type: "error", reason: message.stopReason, error: message });
} else {
stream.push({ type: "done", reason: message.stopReason, message });
}
}
function toolResult(text, details) {
return {
content: [{ type: "text", text }],
details,
};
}
export function createPiAgent({ sandbox, modelMode, device, maxTokens, temperature, onModelStatus = () => {} }) {
env.allowLocalModels = false;
env.allowRemoteModels = true;
env.backends.onnx.wasm.numThreads = Math.min(2, navigator.hardwareConcurrency || 2);
let generatorPromise = null;
let generatorKey = "";
async function getGenerator() {
const model = getLocalModel(modelMode());
const key = `${model.id}:${device()}`;
if (!generatorPromise || generatorKey !== key) {
generatorKey = key;
onModelStatus(`Loading ${device()}`);
generatorPromise = pipeline("text-generation", model.id, {
dtype: "q4",
device: device(),
progress_callback: (event) => {
if (event.status === "progress") {
onModelStatus(`${event.file} ${Math.round(event.progress)}%`);
} else if (event.status) {
onModelStatus(event.status);
}
},
});
}
return generatorPromise;
}
async function releaseGenerator() {
if (!generatorPromise) return;
const generator = await generatorPromise.catch(() => null);
generatorPromise = null;
generatorKey = "";
await generator?.dispose?.();
}
async function producePlan(context, signal) {
const lastMessage = context.messages[context.messages.length - 1];
if (lastMessage?.role === "toolResult") {
return JSON.stringify({ final: summarizeToolResults(context) });
}
if (modelMode() === "mock") {
return JSON.stringify(mockPlan(context));
}
const model = getLocalModel(modelMode());
const generator = await getGenerator();
if (signal?.aborted) throw new Error("Aborted");
const input = model.promptStyle === "qwen" ? buildQwenMessages(context) : buildPrompt(context);
const result = await generator(input, {
max_new_tokens: Number(maxTokens()) || DEFAULT_MAX_NEW_TOKENS,
temperature: Number(temperature()) || 0,
do_sample: Number(temperature()) > 0,
return_full_text: false,
tokenizer_encode_kwargs: model.promptStyle === "qwen" ? { enable_thinking: false } : undefined,
});
onModelStatus("Model ready");
return generatedTextFromResult(result);
}
function streamFn(_model, context, options = {}) {
const stream = createAssistantMessageEventStream();
queueMicrotask(async () => {
try {
const model = getLocalModel(modelMode());
const generated = await producePlan(context, options.signal);
const payload = extractJsonPayload(generated);
const lastMessage = context.messages[context.messages.length - 1];
const forceFinal = lastMessage?.role === "toolResult";
const toolCalls = forceFinal ? [] : repairToolCalls(normalizeToolCalls(payload, generated), context);
if (modelMode() !== "mock") await releaseGenerator();
if (toolCalls.length > 0) {
const message = createMessage([{ type: "text", text: "Using pi tools." }, ...toolCalls], "toolUse", model);
emitFinal(stream, message, "Using pi tools.");
return;
}
const text = normalizeFinalText(payload, generated);
const message = createMessage([{ type: "text", text }], "stop", model);
emitFinal(stream, message, text);
} catch (error) {
await releaseGenerator().catch(() => {});
const text = error instanceof Error ? error.message : String(error);
const message = {
...createMessage([{ type: "text", text }], options.signal?.aborted ? "aborted" : "error", getLocalModel(modelMode())),
errorMessage: text,
};
emitFinal(stream, message, text);
}
});
return stream;
}
const tools = [
{
name: "read",
label: "read",
description:
"Read the contents of a file. For text files, output is truncated by line count. Use offset/limit for large files. When you need the full file, continue with offset until complete.",
parameters: Type.Object({
path: Type.String(),
offset: Type.Optional(Type.Number()),
limit: Type.Optional(Type.Number()),
}),
execute: async (_id, args) => {
const output = await sandbox.readTextFile(args.path, { offset: args.offset, limit: args.limit });
return toolResult(output, { path: args.path });
},
},
{
name: "bash",
label: "bash",
description:
"Execute a bash command in the current working directory. Returns stdout and stderr. Optionally provide a timeout in seconds.",
parameters: Type.Object({
command: Type.String(),
timeout: Type.Optional(Type.Number()),
}),
execute: async (_id, args) => {
const result = await sandbox.bash(args.command, args.timeout);
const output = result.output || "(no output)";
if (result.exitCode !== 0) {
throw new Error(`${output}\n\nCommand exited with code ${result.exitCode}`);
}
return toolResult(output, result);
},
executionMode: "sequential",
},
{
name: "edit",
label: "edit",
description:
"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file.",
parameters: Type.Object({
path: Type.String(),
edits: Type.Array(
Type.Object({
oldText: Type.String(),
newText: Type.String(),
}),
),
}),
execute: async (_id, args) => {
const output = await sandbox.editFile(args.path, args.edits);
return toolResult(output, { path: args.path, edits: args.edits });
},
executionMode: "sequential",
},
{
name: "write",
label: "write",
description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
parameters: Type.Object({
path: Type.String(),
content: Type.String(),
}),
execute: async (_id, args) => {
const output = await sandbox.writeFile(args.path, args.content);
return toolResult(output, { path: args.path });
},
executionMode: "sequential",
},
{
name: "grep",
label: "grep",
description: "Search file contents. This read-only Pi tool is available in the browser port.",
parameters: Type.Object({
pattern: Type.String(),
path: Type.Optional(Type.String()),
glob: Type.Optional(Type.String()),
ignoreCase: Type.Optional(Type.Boolean()),
literal: Type.Optional(Type.Boolean()),
context: Type.Optional(Type.Number()),
limit: Type.Optional(Type.Number()),
}),
execute: async (_id, args) => {
const output = await sandbox.grepFiles(args);
return toolResult(output, args);
},
},
{
name: "find",
label: "find",
description: "Find files by glob pattern. This read-only Pi tool is available in the browser port.",
parameters: Type.Object({
pattern: Type.String(),
path: Type.Optional(Type.String()),
limit: Type.Optional(Type.Number()),
}),
execute: async (_id, args) => {
const output = await sandbox.findFiles(args.pattern, args.path || ".", args.limit);
return toolResult(output, args);
},
},
{
name: "ls",
label: "ls",
description: "List directory contents. This read-only Pi tool is available in the browser port.",
parameters: Type.Object({
path: Type.Optional(Type.String()),
limit: Type.Optional(Type.Number()),
}),
execute: async (_id, args) => {
const output = await sandbox.ls(args.path || ".", args.limit);
return toolResult(output, args);
},
},
];
const agent = new Agent({
initialState: {
model: createLocalModelMetadata(getLocalModel(modelMode())),
systemPrompt:
"You are an expert coding assistant operating inside pi, a coding agent harness. Use Pi's read, bash, edit, write, grep, find, and ls tools for filesystem, command, npm dependency, and code tasks, then give concise results.",
tools,
},
streamFn,
toolExecution: "sequential",
});
agent.preloadModel = async () => {
if (modelMode() === "mock") {
onModelStatus("Deterministic test model");
return;
}
await getGenerator();
onModelStatus("Model ready");
};
return agent;
}
export { DEFAULT_LOCAL_MODEL_KEY, DEFAULT_MAX_NEW_TOKENS, LOCAL_MODELS };