Codemode
本页面是 Pi 官方文档 的中文翻译。仅供学习参考。
codemode 工具让模型编写 JavaScript 脚本,调用 pi 的其他工具,并运行非 LLM 模型(例如分类器和图片模型)。只有脚本的输出会到达模型,因此脚本可以并行运行调用,并在模型看到之前过滤大量结果。如何打开它,见启用 codemode。
脚本
工具输入是原始 JavaScript 源码,不是 JSON,也不是 markdown 代码围栏。它作为 async 函数体在 QuickJS 沙箱中运行,因此顶层 await 和 return 可用。沙箱没有 Node API、文件系统、网络或定时器;脚本只能通过工具和 models 接触外部世界。
脚本可以以选项行开头:
// @options: {"max_output_tokens": 2000, "timeout_ms": 60000}
max_output_tokens(默认 10000)限制输出。更长的输出保留首尾,完整文本写入临时文件,路径包含在结果中。
timeout_ms 是整个脚本的硬截止时间。默认未设置。生成图片可能需要几分钟,因此生成图片的脚本不要设太短的截止时间。
结果以 Script completed 或 Script failed 开头,然后是墙钟时间和输出。失败的脚本保留部分输出,后面是 Script error: 和错误。工具调用是真实的:失败前已发出的调用不会撤销。脚本结束时仍在运行的调用会被取消,未 await 的 promise 会被丢弃。
全局变量
调用工具
会话能调用的每个工具都是 tools 的一个方法,按标识符命名:不是合法 JavaScript 标识符的字符会变成 _,因此 MCP 工具 mcp__dev-radius__search 是 tools.mcp__dev_radius__search。每个方法接受一个包含该工具参数的对象。
调用解析成什么取决于工具:
- 带 output schema 的工具解析为结构化值。
bash 解析为 { output, truncated, full_output_path?, exit_code, wall_time_seconds },非零退出码也如此。它的 output 不受模型看到的 2000 行或 50KB 限制:最多保留 1 MiB,更长的输出在省略标记两侧保留首尾各 512 KiB,并设置 truncated,完整输出在 full_output_path。
- MCP 工具解析为它们的
CallToolResult,包括 isError 和 structuredContent。
- 其他工具(例如
read、edit 和 write)解析为文本输出。
调用失败、被拦截或参数无效时,会以携带工具错误文本的 Error reject。用 Promise.allSettled() 可以保留成功调用的结果。
codemode 描述用 TypeScript 声明列出工具,按 namespace 分组(例如一个 MCP 服务器)。deferred exposure 的工具(包括默认 codemode exposure 的 MCP 工具)不列出,因此 MCP 服务器连接时描述保持不变。列出的声明共享 3000 估计 Token 的预算(设置中的 codemode.inlineBudget)。脚本用 searchTools()、describeTool()、describeNamespace() 查找其余工具,或过滤 ALL_TOOLS。
codemode 激活时,设置中的 codemode.mode 决定其他工具如何呈现。on(默认)时,已声明的工具继续声明,描述中说明如何从脚本调用它们。only 时,它们对模型隐藏,改列在 codemode 描述中,因此模型通过脚本调用它们。
存储值
store(key, value) 把 JSON 值保存在字符串 key 下,供之后的 codemode 调用使用;存 undefined 会删除该 key。load(key) 返回该值,或 undefined。只有脚本成功时才会保留写入:每个成功存储值的脚本会向会话追加一条 codemode-store 自定义条目,因此恢复的会话保留这些值,每个分支只看到自己路径上写入的值。
store 用于 ID、游标或摘要这类小状态。单个值的 JSON 最多 262144 个字符,全部值合计最多 1048576。不要存图片数据;用 image() 展示图片,或用工具写入文件。
模型
models 访问模型目录,并用会话凭证运行非 LLM 模型:分类器(针对 JSON 状态回答带类型的问题)和图片模型(生成图片)。聊天模型会被列出,但不能从脚本运行。有哪些分类器和图片模型,见使用分类器模型 和 使用图片模型。
type ModelType = 'chat' | 'image' | 'classifier';
/** A catalog entry. `provider` and `id` identify it; other fields depend on the type. */
interface ModelInfo {
type?: ModelType;
provider: string;
id: string;
name: string;
api: string;
input: ('text' | 'image')[];
contextWindow?: number;
[key: string]: unknown;
}
declare const models: {
/** Every known model of a type, optionally for one provider. */
getModelsOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;
/** Models of a type whose provider has working credentials. */
getAvailableOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;
/** One catalog entry, or undefined. */
getModelOfType(type: ModelType, provider: string, id: string): Promise<ModelInfo | undefined>;
/** Answer `context.questions` about `context.state`; answers are in `result.answers` by question ID. */
classify(model: ModelInfo, context: ClassifierContext): Promise<ClassifierResult>;
/** Generate images from `context.input` text and image blocks; show `result.output` blocks with image(). Can take minutes. */
generateImages(model: ModelInfo, context: ImagesContext): Promise<ImagesResult>;
};
classify() 和 generateImages() 只用 model 的 provider 和 id,因此 { provider, id } 也可以。它们在 Provider 出错时不会抛出:检查 stopReason 和 errorMessage。每个脚本最多同时运行四个这样的调用;更多调用会等待空闲槽位,因此对许多项使用 Promise.all() 没问题。它们的用量会加到 codemode 工具结果上,并计入会话费用。
不同 Provider 的模型 ID 不同,例如 typesafe/jev-latest 和 openrouter/typesafe/jev-1.13。用 models.getAvailableOfType(type) 查找当前凭证可用的 ID。
分类
interface ClassifierContext {
/** The data to classify. */
state: Record<string, unknown>;
/** Questions by ID. One call answers all of them. */
questions: Record<string, ClassifierQuestion>;
}
type ClassifierQuestion =
/** Pick one label. `criteria` maps each label to what it means. */
| { type: 'choice'; instructions: string; criteria: Record<string, string> }
/** Score on an ordered scale. `criteria` describes each level, lowest first. */
| { type: 'score'; instructions: string; criteria: string[] }
/** Yes or no. */
| { type: 'bool'; instructions: string; criteria: { true: string; false: string } };
interface ClassifierResult {
provider: string;
model: string;
/** Answers by question ID. */
answers: Record<string, ClassifierAnswer>;
usage?: ModelUsage;
stopReason: 'stop' | 'error' | 'aborted';
errorMessage?: string;
}
type ClassifierAnswer =
| { type: 'choice'; choice: string; probabilities: Record<string, number>; confidence: number }
/** `score` is the expected level index, from 0 to `criteria.length - 1`. */
| { type: 'score'; score: number; confidence: number }
/** Probability of `true`. */
| { type: 'bool'; probability: number };
/** Token counts and cost in USD, when the service reports them. */
type ModelUsage = { input: number; output: number; totalTokens: number; cost: { total: number } };
对多个条目分类时,每个条目调用一次 classify()。下面的脚本给反馈消息排序,例如脚本前面某个工具返回的那些:
const jev = await models.getModelOfType('classifier', 'typesafe', 'jev-latest');
const results = await Promise.all(
messages.map((message) =>
models.classify(jev, {
state: { message },
questions: {
sentiment: {
type: 'choice',
instructions: 'How does the user feel about the product?',
criteria: { positive: 'Satisfied or happy', negative: 'Unhappy or frustrated', neutral: 'Neither' },
},
urgency: {
type: 'score',
instructions: 'How urgently does this need a reply?',
criteria: ['no reply needed', 'reply this week', 'reply today'],
},
},
}),
),
);
return results.map((result, i) =>
result.stopReason === 'stop'
? { message: messages[i], sentiment: result.answers.sentiment.choice, urgency: result.answers.urgency.score }
: { message: messages[i], error: result.errorMessage },
);
生成图片
interface ImagesContext {
/** The prompt as text blocks, plus image blocks to edit or use as references. */
input: (TextBlock | ImageBlock)[];
}
interface ImagesResult {
provider: string;
model: string;
/** Generated images, and text blocks for models that also return text. */
output: (TextBlock | ImageBlock)[];
usage?: ModelUsage;
stopReason: 'stop' | 'error' | 'aborted';
errorMessage?: string;
}
type TextBlock = { type: 'text'; text: string };
/** `data` is base64. */
type ImageBlock = { type: 'image'; data: string; mimeType: string };
用 image(block) 展示生成的图片。不要用 text()、console 或 return 打印 data:它很大,模型也无法当文本读。生成的图片不会保存到磁盘;要保留,用工具写入文件。
// @options: {"timeout_ms": 300000}
const painter = await models.getModelOfType('image', 'openrouter', 'google/gemini-2.5-flash-image');
const result = await models.generateImages(painter, {
input: [{ type: 'text', text: 'A red fox in the snow, watercolor' }],
});
if (result.stopReason !== 'stop') return result.errorMessage;
for (const block of result.output) {
if (block.type === 'image') image(block);
else text(block.text);
}
限制
- 脚本的 VM 有 256 MB 内存。用尽会抛出
InternalError: out of memory;过滤或聚合大数据,不要一直累积。
- 脚本等待一个永远无法 settle 的 promise(没有待处理的工具调用)会立即失败,因为没有定时器。
- 脚本不能启动其他
codemode 脚本。
法律声明:本页面是 pi.dev 官方文档的中文翻译版本,仅供学习参考。本网站与 pi.dev 及 Earendil Inc. 无任何法律关系。