跳转到正文
返回门户

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 不会执行。

方法 ​

ts
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 结束后清空为 "")

ts
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 时该会话在返回前已被销毁)
ts
instance.stop(): Promise<{ stopped }>

终止该实例的在途 run(会话保留)。只有确实停掉了一轮运行才返回 stopped: true;实例空闲(尚未 send、上一轮已结束或已 destroy)时返回 { stopped: false }

ts
instance.destroy(): Promise<{ destroyed, sessionId }>

销毁绑定的会话并关闭 tab;幂等——会话已不存在直接成功。返回的 sessionId 是被销毁的会话 id(从未建会话时为 ""),之后 instance.sessionId 为 ""

ts
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 对话记录数与时间线条目数

ts
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

ts
instance.onProgress(callback): () => void

订阅粗粒度进度事件,返回退订函数;callback 收到 { event, ...payload }:

  • run-started:{ sessionId, model, reasoningEffort }(实际下发的模型与档位)
  • progress:{ sessionId, entryCount, lastEntryKind, lastEntryLabel },会话时间线每追加一条推一次
  • run-finished:{ sessionId },无论正常结束、出错还是被停止都会推送

callback 抛出的异常会被吞掉,不影响事件分发

ts
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 处理
systemPromptstringcustom 必填整份替换的系统提示词,框架不再注入任何内容;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 触发。
builtinToolsstring[]否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
modelstring否模型 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。不参与上架静态审查
destroyOnEndboolean否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 取消时,该问题被自动撤下。

字段类型必填说明
questionstring是问题正文(为空时以 LFAGENT_INVALID_OPTIONS 拒绝)
titlestring否弹窗标题
optionsArray<string / { id?, label }>是选项列表(1–24 项,label 不可为空);字符串项等价于 { id: '序号', label },id 缺省用 '1'、'2'…;id 不可重复;id '__custom__' 为保留值(内置「其它」项占用)。违反任一条以 LFAGENT_INVALID_OPTIONS 拒绝
allowMultipleboolean否true = 多选(至少选一项),默认 false = 单选
allowCustomboolean否默认 true:自动追加「其它」选项,勾选后展开输入框,用户输入通过 customText 返回;传 false 关闭(仅接受固定选项时使用)
signalAbortSignal否额外的取消源;所属 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_FOUNDIDE 侧找不到该实例(如 IDE 页面重载后实例表丢失)
LFAGENT_SESSION_GONE实例尚未 send 过或会话已被销毁(getTranscript 时)
LFAGENT_SESSION_UNAVAILABLEsend 时会话正文无法从工作区读回,为避免丢失历史拒绝运行
LFAGENT_ABORTEDsend / 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 应答,会以不带错误码的超时错误拒绝。

示例

js
// ==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()(浏览器打印对话框)。