LFAgent
全局变量:LFAgent · 启用方式:// @require agent · 前置条件:workspace: ['agent']
入口文件必须声明 // @require agent:未声明时 LFAgent 全局根本不会注入,即使命令声明了 workspace: ['agent'],handler 内也只会得到 ReferenceError: LFAgent is not defined(上架静态审查同样只在声明了 agent 时执行)。
Agent 编排库:创建原生或自定义的 AI agent 实例,在 IDE 的独立「LFAgent 会话」(只读)中与主 Agent 并发运行。这些会话经由发起会话输入框上方的「子智能体」折叠面板管理(树状展示父子关系,打开条目即弹出橙色呼吸 tab),不进历史列表。授权规则:终端临时脚本(run <文件> / run -c / run --tab / run --target background)可在顶层直接调用——run 本身就是一次来自 IDE/agent 的授权,IDE 会自动为该进程签发 agent 租约(进程结束即吊销,并强停其名下 agent run),与直调 readFile/writeFile/execTerminalCommand 同一口径;插件源码(liveDebug 重注入、启用/上架的 bundle)则仅能在声明了 workspace: ['agent'] 的 LFRuntime.cli.register 命令 handler 内、且该 plugincmd 调用进行期间使用——脱离调用上下文时 LFAgent.create / LFAgent.askUser 抛 WORKSPACE_LEASE_REQUIRED(调用恰好结算的竞态下也可能由后台返回 WORKSPACE_LEASE_EXPIRED);实例方法(send / status / stop / destroy / getTranscript)在创建它的那次调用(或终端 run 进程)结束后调用抛 LFAGENT_CALL_MISMATCH(若此时也没有任何活动租约则为 WORKSPACE_LEASE_REQUIRED);onProgress 与只读属性(agentId / kind / sessionId)不受租约限制。已上架应用还必须具备 LFRuntime.workspace 授权,否则 plugincmd call 直接以退出码 126(WORKSPACE_UNAUTHORIZED)拒绝,handler 不会执行。
方法
LFAgent.create(options): Promise<LFAgentInstance>创建 agent 实例;kind 为 'native'(复用完整插件 agent,全部内置工具)或 'custom'(自定义 system prompt + 内置工具白名单)。两种 kind 都可以通过 tools 追加页面侧自定义工具(只增不改,见下表)。必须传入一个直接的对象字面量(可为 {}):启用 agent 时 Compile / liveDebug / run <文件> / run -c 都会执行静态审查,无参调用 LFAgent.create() 或传入变量都会编译失败。返回的实例句柄带只读属性 agentId / kind / sessionId(会话首次 send 才懒建,之前 sessionId 为空字符串;destroy 后、以及 destroyOnEnd 实例每次 send 结束后清空为 "")
instance.send(text, options?): Promise<{ text, status, usage: { inputTokens, outputTokens, totalTokens, points, costUnit, pointsSource }, estimatedCostCny, sessionId }>发送指令并等待整轮 run 完成;首次 send 才懒建会话(不会自动打开 tab,需从「子智能体」面板打开)。所属 plugincmd 调用的 ctx.signal 自动级联,无需显式传入;options.signal 是额外的取消源(如 AbortSignal.timeout(ms))。任一中止都会通知 IDE 停掉 run,并以 LFAGENT_ABORTED 拒绝,原始原因在 error.cause;调用时 signal 已处于 abort 状态则立即以 LFAGENT_ABORTED 拒绝(cause 为 signal.reason),不会启动 run。上一轮 send 未结束时再次 send 以 LFAGENT_BUSY 拒绝。text 为空白时以 LFAGENT_INVALID_OPTIONS 拒绝。结果字段:
text:模型最终答复;为空时为"Agent 已结束。"status:本轮 run 的结束状态(缺省'completed')usage:inputTokens/outputTokens/totalTokens;points为本轮点数,无计费数据时为null;costUnit恒为'points';pointsSource为计费来源(多轮实际计费与本地回退混合时为'mixed',也可能是'unknown'/'unavailable'/'deprecated-estimated-cost-cny')estimatedCostCny:usage.points / 200,仅作 deprecated 兼容sessionId:本轮使用的会话 id(destroyOnEnd时该会话在返回前已被销毁)
instance.stop(): Promise<{ stopped }>终止该实例的在途 run(会话保留)。只有确实停掉了一轮运行才返回 stopped: true;实例空闲(尚未 send、上一轮已结束或已 destroy)时返回 { stopped: false }
instance.destroy(): Promise<{ destroyed, sessionId }>销毁绑定的会话并关闭 tab;幂等——会话已不存在直接成功。返回的 sessionId 是被销毁的会话 id(从未建会话时为 ""),之后 instance.sessionId 为 ""
instance.status(): Promise<{ agentId, kind, model, resolvedModel, reasoningEffort, resolvedReasoningEffort, destroyOnEnd, sessionId, sessionExists, running, messageCount, timelineCount }>查询实例与会话状态快照:model / reasoningEffort 为请求值(reasoningEffort 已归一化),resolvedModel / resolvedReasoningEffort 为此刻解析出的实际值;sessionExists 表示会话记录仍存在;running 表示当前有 run 在途;messageCount / timelineCount 为会话的 API 对话记录数与时间线条目数
instance.getTranscript(options?): Promise<{ sessionId, include, totalEntries, entries } | { sessionId, include, meta }>导出会话上下文;include 取 'messages'(默认,参与 prompt 的 API 对话记录)/ 'timeline'(会话时间线条目)/ 'meta'。messages / timeline 返回 { sessionId, include, totalEntries, entries: [{ index, entry }] };meta 返回 { sessionId, include, meta: { id, title, createdAt, updatedAt, origin, lfagentMeta } }。超过 4000 字符的字符串会被截断并标注 …[truncated N chars]。include 取其它值以 LFAGENT_INVALID_OPTIONS 拒绝;尚未 send 或会话已销毁时抛 LFAGENT_SESSION_GONE
instance.onProgress(callback): () => void订阅粗粒度进度事件,返回退订函数;callback 收到 { event, ...payload }:
run-started:{ sessionId, model, reasoningEffort }(实际下发的模型与档位)progress:{ sessionId, entryCount, lastEntryKind, lastEntryLabel },会话时间线每追加一条推一次run-finished:{ sessionId },无论正常结束、出错还是被停止都会推送
callback 抛出的异常会被吞掉,不影响事件分发
LFAgent.askUser(options): Promise<{ askId, allowMultiple, allowCustom, selectedIds, selectedLabels, customText }>向用户弹出悬浮提问卡(盖在 agent 面板上,不嵌入会话时间线)并等待作答;与 agent 实例无关,可在自定义工具 handler 中直接调用(见下方「askUser(options) 参数」)
send() 的规范计费结果在 usage.points、usage.costUnit === 'points'、usage.pointsSource;多轮实际计费与本地回退来源混合时 pointsSource 为 'mixed'。顶层 estimatedCostCny = usage.points / 200 仅 deprecated compatibility。
create(options) 参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
kind / mode | 'native' / 'custom' | 否 | mode 是旧别名,运行时同样生效(取 kind ?? mode);二者并存且不同时以 LFAGENT_INVALID_OPTIONS 拒绝。默认 'native':复用完整插件 agent(含终端,允许套娃再调 plugincmd)。'custom':完全自定义。只接受 'native' / 'custom',其它值(如 'smart')以 LFAGENT_INVALID_OPTIONS 拒绝,不会静默按 native 处理 |
systemPrompt | string | custom 必填 | 整份替换的系统提示词,框架不再注入任何内容;native 不得声明 |
tools | { name, description?, parameters?, handler }[] | 否 | 运行时 description/parameters 可省略(parameters 缺省为 { type: 'object', properties: {} });上架时一旦声明工具,name/description/parameters/handler 四项全部必填。两种 kind 均可:页面侧自定义工具(native 在全工具集之上追加)。只允许新增——与内建工具重名会以 LFAGENT_INVALID_OPTIONS 拒绝(内建工具不可修改/删除);缺 name、缺 handler 或工具名重复同样以 LFAGENT_INVALID_OPTIONS 拒绝。parameters 是 OpenAI function-calling JSON schema;handler(params, { signal, toolCallId }) 在当前网页内执行,返回 string 或 { output, isError }——返回对象时内容必须放在 output 字段,其它形状的对象(如直接返回数据对象)会得到空输出,返回数据请先 JSON.stringify;handler 抛错时以 error.message 作为输出并标记 isError;run 被终止时 signal 触发。 |
builtinTools | string[] | 否 | custom 专用(native 忽略):内置工具白名单(如 ['read_file']),未知工具名会被静默忽略。控制类工具 think / respond / attempt_completion / summarize_task 始终保留;ask_followup_question 始终排除(LFAgent 会话只读,没人能回答追问,需要问用户请在自定义工具里调 LFAgent.askUser)。白名单含 execute_command / execute_run_script 时自动并入对应的只读帮助工具 get_command_help / get_builtin_library_help |
model | string | 否 | 模型 id,或 'inherit'(默认):主 Agent 有在途 run 时取其本轮模型,否则取 IDE 当前对话模型;每次 send 重新解析(status().resolvedModel 是当下的解析结果)。嵌套孵化的 LFAgent 不会继承上级 LFAgent 的模型。create 时不校验模型 id |
reasoningEffort | 'inherit' / 'low' / 'medium' / 'high' / 'xhigh' | 否 | 思考强度。'inherit'(默认)= 跟随用户在模型下拉里设置的统一档位(每次 send 重新读取);固定档位对所有模型统一生效——厂商参数差异由网关映射,不支持推理的模型自动忽略,无需按模型分支。取值大小写不敏感、首尾空白忽略,并接受别名:none / minimal → low,max / x-high / extra_high → xhigh。其它值以 LFAGENT_INVALID_OPTIONS 拒绝。status() 同时返回归一化后的请求值 reasoningEffort 与实际下发的 resolvedReasoningEffort。不参与上架静态审查 |
destroyOnEnd | boolean | 否 | send 结束(自然/被迫)后自动销毁会话并关闭 tab,instance.sessionId 随之变为 "";默认 false |
上架时的严格静态审查规则
- 上架脚本中的调用必须是直接的
LFAgent.create({ ... }),且恰好传入一个对象字面量配置(可为{});无参调用、别名、间接调用或动态配置无法提审 kind或旧别名mode必须静态解析为native/custom,并存时必须相同;custom 必须提供非空静态systemPrompt,native 不得声明- 每个已声明工具必须同时包含
name/description/parameters/handler;前三项必须是直接字面量,或引用递归静态可求值且从未被重新赋值的const(稳定编译产物 var 仅兼容),工具名须匹配[A-Za-z0-9_.-]{1,64}且唯一 - 允许静态字符串拼接、仅含静态插值的模板字符串,以及由 JSON 字面量组成的对象 / 数组 / primitive;这些值会在服务端严格提取后签封
- 源码应使用稳定
const;稳定var仅用于兼容 esbuild 对顶层 const 的降级产物,不建议手写。禁止函数调用或await、网络 / 存储 / 环境读取、computed 动态属性、spread 和循环引用 - 禁止间接、可选链、计算属性或别名方式访问
LFAgent.create;禁止变异或别名传递工具数组/工具对象,也不得向未知函数传递;参与 systemPrompt/description/parameters 的常量不能在 LFAgent 声明之外复用 - 禁止伪造
__lfReviewed、authorPack、lf-author-pack-v1;上限为 32 个 Agent、每 Agent 64 个工具、512 个 author entries、65536 字符 systemPrompt、4096 字符 description、32768 字节规范 JSON parameters handler不属于模型 schema,可继续使用正常的动态页面逻辑、闭包和异步调用- 无法严格静态提取的作者提示词或 schema 会拒绝上架,绝不降级成明文运行。Compile、liveDebug、run <项目文件> 与 run -c 在启用 agent 时都会执行同一静态规则,并按源码位置报错;local / liveDebug 仍走 v2 开发传输,但不能绕过静态可提取性
askUser(options) 参数
单选/多选提问弹窗。默认在选项末尾自动追加一个「其它」选项(保留 id __custom__):用户勾选后出现输入框自定义作答。并发提问(哪怕来自不同的 plugincmd 调用)不会叠出新弹窗,而是排进同一个弹窗队列,右上角计数从 1/1 变 1/2;回答完一题自动进入下一题。弹窗不可关闭,用户必须作答——唯一的例外是提问所属的 plugincmd 调用结算或 signal 取消时,该问题被自动撤下。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
question | string | 是 | 问题正文(为空时以 LFAGENT_INVALID_OPTIONS 拒绝) |
title | string | 否 | 弹窗标题 |
options | Array<string / { id?, label }> | 是 | 选项列表(1–24 项,label 不可为空);字符串项等价于 { id: '序号', label },id 缺省用 '1'、'2'…;id 不可重复;id '__custom__' 为保留值(内置「其它」项占用)。违反任一条以 LFAGENT_INVALID_OPTIONS 拒绝 |
allowMultiple | boolean | 否 | true = 多选(至少选一项),默认 false = 单选 |
allowCustom | boolean | 否 | 默认 true:自动追加「其它」选项,勾选后展开输入框,用户输入通过 customText 返回;传 false 关闭(仅接受固定选项时使用) |
signal | AbortSignal | 否 | 额外的取消源;所属 plugincmd 调用的 ctx.signal 已自动级联,无需传入。任一 abort 都会把该问题撤出弹窗队列并以 LFAGENT_ABORTED 拒绝(原始原因在 error.cause);调用时 signal 已 abort 则立即拒绝,不会弹出问题 |
返回 { askId, allowMultiple, allowCustom, selectedIds, selectedLabels, customText };单选时 selectedIds 恰有一个元素。用户选了「其它」时 selectedIds 含 '__custom__',selectedLabels 对应位置与 customText 都是用户输入的文本;未选「其它」时 customText 为空串。
生命周期与并发
- 实例与创建它的 plugincmd 调用同生共死:调用结算(结果返回 / 超时 / Ctrl+C / 断连)时,该调用创建的全部 run 被强制终止,destroyOnEnd 的实例连会话一起销毁。
plugincmd call不传--timeout即不限时(默认),长任务保持默认即可;handler 可读ctx.timeoutMs(0 = 不限时)在入口发现限时调用并主动拒绝 - 实例方法(send / status / stop / destroy / getTranscript)绑定创建该实例的那次 plugincmd 调用(或终端 run 进程)的租约,只在它存活期间可用;同一页面实例里有其它调用并发进行时照常可用。创建它的调用结束后再调用实例方法以
LFAGENT_CALL_MISMATCH拒绝(若此时也没有任何活动租约则为WORKSPACE_LEASE_REQUIRED)。LFAgent.create/LFAgent.askUser则绑定"当前"调用——同一页面实例内多个 plugincmd 调用重叠时,它们会绑定到最近开始的那次调用(随那次调用结算而被清场)。请避免让会创建 agent 或调用 askUser 的命令并发重叠 - 并发是同事件循环的异步并发(与 subagent 同机制),不是真正的线程;CPU 密集型任务仍会互相阻塞
- 每个 LFAgent 会话拥有独立的后台终端:
execute_command/execute_run_script等在会话专属终端中执行,cwd、环境变量与前台队列均与主 Agent 终端及其它 LFAgent 会话隔离(工作区文件仍实时同步),不会与发起 plugincmd 的主 Agent 终端相互等待;该终端随会话删除或调用结算自动回收 - LFAgent 会话在 IDE 中只读:无输入框、
agent send与回滚都会被拒绝;但上下文可正常查看与导出。会话不进历史列表、也不自动弹 tab——唯一入口是孵化它的会话输入框上方的「子智能体」折叠面板(按父子关系组树,嵌套孵化的挂在上级条目下),面板里可终止运行、打开橙色 tab 围观、或在整棵子树停止后二次确认删除(级联删除全部子孙);shell 的 agent session 命令同样可删 - 回退语义:LFAgent 产生的工作区文件改动与发起 plugincmd 的会话绑定记账——在发起会话里回退到 plugincmd 节点(或更早)时,LFAgent 产生的全部文件改动连带撤销
- 同一实例同时只能有一个 send 在途:上一轮未结束时再次 send 必定以
LFAGENT_BUSY拒绝(页面侧与 IDE 侧都会拦截)
错误码
| 错误码 | 说明 |
|---|---|
WORKSPACE_LEASE_REQUIRED | 插件源码中不在 plugincmd 调用上下文内,或命令未声明 workspace: ['agent'](终端 run 临时脚本自动持有 run 租约,不会触发) |
WORKSPACE_LEASE_EXPIRED | 请求到达后台时租约已吊销或过期(调用刚好结算、超时或被中止时的竞态) |
WORKSPACE_SCOPE_DENIED | 后台校验租约时发现 scope 不含 agent,或请求的终端会话与租约绑定的不一致 |
WORKSPACE_UNAUTHORIZED | 已上架应用缺少 LFRuntime.workspace 授权:plugincmd call 以退出码 126 拒绝,错误输出以 WORKSPACE_UNAUTHORIZED: 开头,invoke 不下发、handler 不执行(不会以 error.code 出现在页面里) |
LFAGENT_BUSY | 该实例上一轮 send 尚未结束 |
LFAGENT_INVALID_OPTIONS | 参数非法:create 的 kind 不是 native / custom、kind/mode 冲突、custom 缺 systemPrompt、工具缺 name / 缺 handler / 重名 / 与内建重名、reasoningEffort 非法;send 空文本;getTranscript 的 include 非法;askUser 问题为空、选项为空 / 超过 24 项 / label 为空 / id 重复 / 占用保留 id __custom__ |
LFAGENT_CALL_MISMATCH | 创建该实例的那次 plugincmd 调用(或终端 run 进程)已结束后又调用实例方法 |
LFAGENT_NOT_FOUND | IDE 侧找不到该实例(如 IDE 页面重载后实例表丢失) |
LFAGENT_SESSION_GONE | 实例尚未 send 过或会话已被销毁(getTranscript 时) |
LFAGENT_SESSION_UNAVAILABLE | send 时会话正文无法从工作区读回,为避免丢失历史拒绝运行 |
LFAGENT_ABORTED | send / askUser 被中止(ctx.signal、options.signal、AbortSignal.timeout、Ctrl+C 等),或调用时 signal 已 abort(不会启动 run / 弹出问题);原始原因在 error.cause |
LFAGENT_INVOKE_SETTLED | 所属 plugincmd 调用已结算(askUser 未答完时被清场) |
LFAGENT_TRANSPORT_UNAVAILABLE | 无法建立到 IDE 的通道:当前世界没有扩展消息能力(如 run --tab 的 MAIN world 且无终端中继),或该脚本实例所在 USER_SCRIPT world 的消息配置已失效(停用/重新启用应用、同项目开启 liveDebug 等之后,旧实例仍能被 plugincmd 调到)——刷新宿主页面或重启 liveDebug / 重新 run |
LFAGENT_TRANSPORT_LOST | 请求完成前与扩展后台的通道断开:扩展重载或后台 Service Worker 重启、终端 run 的宿主中继断开或未应答、消息投递失败等。plugincmd 调用结算本身不会触发它(结算表现为 LFAGENT_ABORTED 或 LFAGENT_INVOKE_SETTLED) |
请以 error.code 判断错误,不要解析 error.message。send / askUser 之外的请求(create / stop / destroy / status / getTranscript)若 60 秒内未得到 IDE 应答,会以不带错误码的超时错误拒绝。
示例
// ==UserScript==
// @name Data Analyst Command
// @match https://example.com/*
// @require agent
// ==/UserScript==
LFRuntime.cli.register({
name: "analyze",
brief: "Spawn a sub-agent to analyze the current page data",
workspace: ["agent"], // 声明 agent scope,handler 内才能使用 LFAgent
async handler(argv) {
// native:复用完整插件 agent(全部内置工具);模型继承调用方
const agent = await LFAgent.create({ kind: "native", destroyOnEnd: false });
// 应用源码(liveDebug / 已安装)里 LFRuntime.print 写入宿主网页的 console(liveDebug 可用 diag 查看),
// 不在终端;plugincmd 在终端显示的是 handler 的返回值。务必写全 LFRuntime.print,裸 print() 是 window.print()
const stop = agent.onProgress((event) => LFRuntime.print("progress", JSON.stringify(event)));
try {
// 参数为空时给默认指令,否则空文本会被 LFAGENT_INVALID_OPTIONS 拒绝;
// ctx.signal 已自动级联:调用被中断时 run 一并终止
const task = argv.join(" ") || "Summarize the main data on this page";
const result = await agent.send(task);
return `${result.text}\n(tokens: ${result.usage.totalTokens}, points: ${result.usage.points})`;
} finally {
stop();
}
},
});
// 示例桩:替换成你自己的页面读取逻辑
const readSkuFromPage = (sku) => ({ sku, title: document.title, url: location.href });
// custom 同样必须在声明了 agent workspace 的 plugincmd handler 内创建
LFRuntime.cli.register({
name: "audit-sku",
workspace: ["agent"],
async handler(argv) {
const custom = await LFAgent.create({
kind: "custom",
systemPrompt: "You are a terse SKU auditor. Use the lookup_sku tool.",
model: "inherit",
destroyOnEnd: true,
tools: [{
name: "lookup_sku",
description: "Look up a SKU record on this page",
parameters: { type: "object", properties: { sku: { type: "string" } }, required: ["sku"] },
// 返回字符串;返回对象时必须写成 { output, isError }
handler: async ({ sku }) => JSON.stringify(readSkuFromPage(sku)),
}],
});
// options.signal 是额外的取消源,这里给本轮限时 2 分钟
const answer = await custom.send(`Audit SKU ${argv[0] || "ABC-123"}`, { signal: AbortSignal.timeout(120000) });
return answer.text;
},
});在终端 run 临时脚本里直接调用 LFAgent 时,要输出到终端请写全 LFRuntime.print(...)——裸写 print() 调用的是 window.print()(浏览器打印对话框)。