Skip to content

Extension 接口 ​

Extension 的每个钩子,TaskView、Tool、ToolContext、ToolOutput、ToolFlags 的每个成员,以及它们用到的几个小类型,都照着 noeta/core/extension.py、noeta/core/tool.py、noeta/core/records.py、noeta/core/questions.py 整理。每个钩子什么时候调用、为什么这样设计,见插件体系;完整的例子见写一个插件。

插件常用的名字都能从包的顶层导入:

python
from noeta import (Extension, TaskView, Tool, ToolContext, ToolOutput, ToolChanges, ToolUse,
                   ContextPart, Reminder, Command, CommandContext, Allow, Deny, Ask, Rewrite,
                   Recorded, TaskState, ask, read_answer, text_output, function_tool)

Extension ​

class Extension(noeta/core/extension.py)。所有方法都可选,默认什么都不做。

类属性 ​

属性类型含义
namestr插件的注册名,也是它的记录所用的名字。必填;同一个代理里不能重复。
add_toClassVar[tuple[str, ...]]只对插件文件有效:它自己加入哪些代理("*" 表示所有代理),排在代理自己的插件后面。已安装的包写了也不生效。宿主设置里写 enabled = false 仍然能关掉它。
foldsClassVar[tuple[str, ...]]除了自己的记录,reduce 还要折叠哪些名字下的记录,折进本插件的状态。

状态 ​

方法返回什么时候调用
initial_state(self)Any每次折叠一开始:还没有任何记录时的状态。默认 None。
reduce(self, state, kind: str, data)Any每次折叠时,按日志顺序,对本插件的每条记录(以及 folds 里那些名字的记录)调用一次。用户的消息被删掉时,也会带着 records.ERASED_KIND("core.erased")和被删的 seq 调用。必须是纯函数。
continue_state(self, state)Any另一个任务接着这个任务的对话跑时(分叉、恢复的子代理):新任务从什么状态开始。默认全部带过去。

工具和上下文 ​

方法返回什么时候调用
tools(self, view: TaskView)Sequence[Tool]每一步。同名时第一个提供的插件说了算;每个进程只对一次冲突记一次日志。
tool_changes(self, view: TaskView, offered: Sequence[Tool])ToolChanges | None每一步,所有插件的 tools 之后。offered 是套上代理允许、禁止清单之后的工具,不含别的插件的改动;每个插件拿到的 offered 都一样。加进来的工具如果被代理的清单拒绝,整份答复作废。
context(self, view: TaskView)Sequence[ContextPart]每次调用模型:系统提示里的文字。
async reminders(self, view: TaskView)Sequence[Reminder]每次调用模型之前,排在所有插件的 before_model 前面(和 Claude Code 一样,先加备注、再决定要不要压缩)。运行器把它们记下,放进这次请求、接在已记录的消息后面,所以 before_model 看得到它们。
inherit(self, state: TaskState)Mapping[str, Any]这个任务派出子任务时:在派出它的工具给的配置之上,再加上这些配置项。

调用模型 ​

方法返回什么时候调用
async on_start(self, view: TaskView)None运行器每次接手任务、第一步之前。
async before_model(self, view: TaskView, request: ModelRequest)ModelRequest每次调用模型之前、提醒之后,按插件顺序,每个拿到上一个返回的请求。可以抛 TurnDone(不调用,这一轮结束)、RequestStale(按新状态重新拼请求)或 TurnFailed(reason, detail)。
async on_context_overflow(self, view: TaskView, request: ModelRequest)bool模型以太长为由拒了 request。腾出地方(记下一次压缩)后返回 True,然后重新调用。谁都没腾,这一轮以 context_overflow 失败。写成 (self, view) 的老版本也照样会被调用。
async on_empty_reply(self, view: TaskView, response: ModelResponse)Message | None回复以 end_turn 结束,却没有文字也没有工具调用。返回一条让它接着干的消息,第一个返回的算数。内核把它记进一条 continue 记录(原因 empty_reply),再调用一次模型。谁都没给,这一轮就不带回答地结束(turn_done,原因 empty_reply)。

工具调用 ​

方法返回什么时候调用
async before_tool(self, view: TaskView, call: ToolUse)Gate | None每个调用,问所有插件。有 Deny 就拒;其次是第一个 Rewrite(改写后的调用再过一遍,最多改写三次);再其次是第一个 Ask。None 表示没意见。
async resolve_ask(self, view: TaskView, call: ToolUse, asks: Sequence[Ask])Gate | None调用马上要因为 asks 挂起,且没人拒绝。返回 Allow 或 Deny 就是替用户回答,Rewrite 用别的输入跑(再过一遍检查),Ask 换掉原来的询问;第一个答复算数。重启后同一个调用可能再过一遍检查,所以花了代价得到的答复应该记下来复用。
async after_tool(self, view: TaskView, call: ToolUse, output: ToolOutput)ToolOutput调用跑完、结果记下之前,按插件顺序。

事件 ​

方法返回什么时候调用
async on_event(self, view: TaskView, recorded: Recorded)None每追加一个事件之后,带着追加那一刻的状态。这里记下的东西紧接着追加;对结束任务的那个 TaskEnded,这里记的会被丢掉。每个事件只送一次,崩溃会丢,除非 delivered_through 另有说明。
delivered_through(self, view: TaskView)int | None本插件已经处理到日志的哪个 seq,从它自己的状态里读。任务每次被认领时,这个 seq 之后的事件会重新交给 on_event:做到至少送达一次。None(默认):不重送。

压缩 ​

方法返回什么时候调用
async before_compact(self, view: TaskView, trigger: Literal["manual", "auto"], instructions: str)str | Deny | None压缩插件在写摘要之前来问。返回文字就加进摘要的要求里;返回 Deny 就跳过这次压缩。
async resident(self, view: TaskView)Sequence[Message]压缩时:放在保留消息后面、重新挂上的内容(某个清单、环境信息),记进 Compacted.after。
async lead_resident(self, view: TaskView)Sequence[Message]压缩时:放在摘要前面、重新生成的内容(项目说明、记忆索引),记进 Compacted.lead。

命令 ​

方法返回什么时候调用
async commands(self, ctx: CommandContext)Sequence[Command]前端列出斜杠命令时。
async run_command(self, ctx: CommandContext, name: str, args: str)str执行本插件某个 template 为空的命令 /name args:返回要发出的消息文字。

ToolSet(tools, name="tools") 把一组普通的工具包成一个 Extension。

放行结论(Gate) ​

before_tool 和 resolve_ask 的答复;Gate = Allow | Deny | Ask | Rewrite。

类型字段含义
Allowapprove: bool = False、outright: bool = False没意见。approve:插件替这个调用作保(比如允许它的 PreToolUse 钩子),能满足其他插件不是 firm 的询问。outright:权限层按自己的设置直接放行;工具会看到 ToolContext.allowed_outright。
Denyreason: str = ""拒绝;模型看到这个原因。
Askprompt: str = ""、detail: dict = {}、firm: bool = False挂起调用,等人回答。prompt 是前端显示的文字。firm 的询问(询问规则、钩子发起的询问)不会被别的插件的 Allow(approve=True) 满足。
Rewriteinput: dict、name: str | None = None换个输入跑,或者换成另一个工具。

钩子返回的几种类型 ​

类型字段含义
ContextParttext: str、order: int = 100、before: str = ""系统提示里的文字,按 order 换行接在后面,放在提示词那个带缓存的块里;带 before 时,插在提示词里第一行与它相同的那一行前面。必须是稳定的文字,会变的内容改成记一条消息。
Remindertext: str、kind: str = "reminder"、data = None、origin: "system" | "memory" = "system"、role: "user" | "system" = "user"、order: int = 100作为 (extension, kind, data) 连同消息一起记下。role="user" 的放进用户那一轮;role="system" 的是后面的一个系统轮。先放 user 的,再按 order 放 system 的。
Commandname、description、template = ""、argument_hint = ""、local = False一个斜杠命令。template 里的 {args} 换掉后就是发出的消息;为空时由 run_command 给出。local 命令的文字只显示给用户,不会到模型那里。
CommandContextcwd: str、task_id: str | None = None命令在哪里被列出或执行。
ToolChangesadd: Sequence[Tool] = ()、withhold: Mapping[str, str] = {}、entries: Sequence[ToolEntry] = ()插件自己的工具之外再提供的工具;从请求里扣下的工具,各带一个调用时返回的错误(在任何检查之前就答复);不是工具的请求条目。
ToolEntryspec: ToolSpec、before: str = ""工具清单里一个永远不会被调用的条目(厂商的模式开关),放在名为 before 的工具前面,否则放在不是 listed_last 的工具后面。

TaskView ​

钩子看到的东西(@dataclass class TaskView):只读的折叠状态,加上记录的办法。钩子里记的东西,在钩子返回后由运行器追加。

字段 ​

字段类型含义
stateTaskState折叠出来的任务状态(只读)。
completeasync (ModelRequest) -> ModelResponse,或 None通过任务的 provider 调用模型,带重试,不算对话里的一步。
envExecEnv | None任务的执行环境。
postasync (body, dedupe_key=None),或 None往这个任务自己的收件箱投递。
count_tokensasync (text) -> int | None,或 Noneprovider 对"一条只含 text 的用户消息"的精确 token 数。
read_logasync (task_id, after=0) -> Sequence[Recorded],或 None另一个任务的日志,只读;不存在的任务读出来是空的。
child_tasksasync (task_id) -> list[str],或 None某个任务的子任务编号。
extensionsSequence[Extension]任务的插件,按代理里的顺序。
modelstr任务用的模型。
agentsMapping[str, Agent]这台工人能跑的所有代理。
allowed_toolsSequence[str] | None代理的工具白名单(None:所有工具)。
denied_toolsSequence[str]代理从不提供的工具名。
keeps_tailbool | None压缩向各插件要常驻内容时:摘要后面是否还保留消息。
keptSequence[Message]压缩向各插件要常驻内容时:保留下来的那些消息。
spill_dirstr | None超长输出的存放目录(None:不存)。
identitystr任务请求开头的身份行。
effortEffort | None任务的推理强度。
next_request() -> ModelRequest,或 None下一次模型调用会以什么请求开始(不跑提醒和 before_model),给两轮之间要共用提示词缓存的工作用。
pending、creates、enqueued列表这个钩子暂存的东西;由运行器追加、投递。

属性和方法 ​

成员返回含义
task_idstr任务编号。
cwdstr配置里的 cwd,否则执行环境的,否则 ""。
ext_state(name)Any某个插件折叠出的状态。
folded(reducers=None)TaskState把这个 view 上暂存的记录都折叠进去之后的状态,在副本上算。
prompt(name, default)str这个任务里某个有名字的提示词的文字。
record(extension, kind, data=None, *, message=None)None记一条;带 message 时,这条消息也进对话。写一个内核不认识的 core 记录会抛 ValueError。
enqueue(body, *, dedupe_key=None, task_id=None)None在和钩子记录同一次追加里,投递到本任务(或另一个存在的任务)的收件箱,崩溃时两者要么都在要么都不在。
spawn(agent, prompt, *, config=None, queue=None, key="")str(子任务编号)在工具调用之外暂存一个子任务;和其他子任务一样被跟踪(ChildEnded 会进收件箱)。key 相同,编号就相同。
find_tool(name)Tool | None这个任务以 name 提供的工具:一个插件用另一个插件工具的办法。直接调它的 run 不会再过检查。
deferred_tool_names()list[str]这个任务提供的延迟加载工具,排好序。
compacted(event: Compacted)None记下一次压缩(压缩插件唯一的内核事件)。
async residents(*, lead=False, keeps_tail=None, kept=())list[Message]所有插件的 resident(或 lead_resident)消息;某个插件出错就写日志并跳过。
notify(text, *, kind=None, data=None)None告诉用户一件事;前端显示,模型看不到。
set_timer(name, at, text="")None在 at(epoch 秒)之后、任务闲着时叫醒它,text 作为下一条消息;同名的会替换。持久:哪台工人都能触发。
clear_timer(name)None撤回一个计时器。
erase_messages(seqs)None把这些 seq 上用户的消息(以及它们的 inline 展开和 memory 召回)从对话里拿掉。

notify、set_timer、clear_timer、erase_messages 来自 records.CoreNotices,在 ToolContext 上也有。

Tool ​

class Tool(Protocol)(noeta/core/tool.py)。

成员类型含义
namestr模型看到的名字。
descriptionstr给模型看的说明;提示词 tool/<name> 可以覆盖它。
input_schemadict输入的 JSON Schema;除非 unchecked_input,在任何检查之前先按它校验。
read_onlybool只读,不持有共享状态:同一次回复里可以和别的只读调用一起跑。
clearablebool结果可以通过再调一次拿回来,所以旧结果可以从上下文里清掉。
async run(input, ctx: ToolContext)ToolOutput执行调用。抛出的异常会变成一个写着异常的错误结果。
flags(可选)ToolFlags见下;没有时是 NO_FLAGS(用 tool_flags(tool) 读)。
async resume(input, ctx, outcome)(可选)ToolOutput会挂起的工具必须有:问题的 outcome 是 Answer,子任务的是 {child_id: ChildEnded},等待被撤回时是 None。
async recover(input, ctx)(可选)ToolOutput | None崩溃留下的没跑完的调用:按 ctx.idempotency_key 查到的结果;查不出来返回 None。比 retry_safe 先问。
async rule_inputs(input, view)(可选)Sequence[(input, allow_too)]权限规则还要把这个调用当作哪些别的输入来判断。

function_tool(fn) 或 @function_tool(name=None, description=None, read_only=False, clearable=False, deferred=False) 把一个带类型标注的函数变成工具:参数说明来自标注(str、int、float、bool、list[...]、dict、Literal、X | None、Annotated[T, "说明"]),工具说明来自 docstring;类型是 ToolContext(或名叫 ctx)的参数会拿到上下文(noeta/toolkit/function_tool.py)。

ToolFlags ​

@dataclass(frozen=True) class ToolFlags:

标志默认含义
deferredFalse只把名字告诉模型,发出时带 defer_loading;经 ToolSearch 加载。
search_hint""ToolSearch 按关键词搜时也能匹配到的几个词;不发给模型。
loaded_descriptionNone工具带着 defer_loading 发出时改用的说明。
listed_lastFalse排在其他工具后面(把一组会变的工具放在缓存前缀的末尾)。
run_lastFalse在同一次回复的其他调用之后再跑。
past_agent_listsFalse不管代理的允许、禁止清单怎么写都提供。
retry_safeFalse再跑一次不会改变外面的任何东西:崩溃后没跑完的调用会重跑(最多开始 MAX_ATTEMPTS = 3 次)。
silent_reply_afterNone"call" 或 "success":自用户上一条消息以来调用过它(或成功调用过)之后,空回复就结束这一轮。
unchecked_inputFalse输入不校验,原样交给工具。
safe_while_planningFalse不是只读,但不改文件、不碰任务之外的东西;计划模式下不用询问就能跑。
max_result_chars50_000文字结果超过这个长度(最多 RESULT_CEILING_CHARS = 50,000)就存成文件、给预览;None 表示工具自己控制结果长度、不受此限。

ToolOutput ​

class ToolOutput(Struct);text_output(text, *, is_error=False, **meta) 生成一个纯文字的。

字段类型含义
contentlist[Text | Image | ToolReference]模型拿到的内容。
is_errorbool是不是错误结果。
metadict和结果一起记下,给前端和插件用;从不发给模型。
displayDisplay | None前端可以怎么显示:kind(markdown、code、diff、table、progress、kv)、data、fallback。
parkPark | None调用需要等待时,用它代替结果。
end_turnstr | None这次回复的结果都落下后结束这一轮,用这段文字作为回答(子代理交回结果)。
block_listbool即使是纯文字,也按块列表发出。

Park(on: "question" | "children", keys: list[str] = [], detail: dict = {})。问问题用 ask({"question": ..., "header"?, "options"?: [{"label", "description"?}], "multiSelect"?}, ...) 生成,resume 里用 read_answer(outcome, asked) 读答案(问题被撤回时返回 None;noeta/core/questions.py)。

ToolContext ​

@dataclass class ToolContext:正在跑的工具看到的东西。

字段类型含义
task_id、call_idstr任务和这次调用。
idempotency_key(属性)str<task id>:<call id>:这次调用不管在哪台工人上跑多少次都一样。可以交给能识别重复请求的外部系统,或者在 recover 里按它查结果。
envExecEnv任务的执行环境。
stateMapping[str, Any]每个插件折叠出的状态,按名字(只读)。
record(extension, kind, data) -> None记一条;在这次调用的 ToolFinished 之前追加。写一个内核不认识的 core 记录会抛 ValueError。
spill_dirstr | None太长、无法直接返回的完整输出存到哪。
spawn(agent, prompt, *, config=None, background=False, queue=None, history_from=None, history_through=None) -> str,或 None暂存一个子任务,和这次调用的记录一起原子地创建,所以重试的调用不会建出第二个子任务。history_from 表示接着那个任务的对话。
postasync (body, task_id=None, dedupe_key=None),或 None在一步之外往这个任务的收件箱投递,或者投给另一个任务(不存在的任务会抛 UnknownTask)。
root_idstr根任务的编号。
keep() -> KeptClaim,或 None让这台工人一直占着这个任务的认领,等待期间也不放,直到释放:给只存在于本进程里的工作用(后台任务)。
tasks、task异步函数,或 None存储里任务表的行;某一个任务的行。
read_logasync (task_id, after=0),或 None另一个任务的记录。
by_modelbool是不是模型发起的调用(插件自己跑工具时为 False)。
approvedbool用户在审批提示里允许了这次调用。
allowed_outrightbool权限层按设置直接放行,没有问任何人。
model、identity、effort和 TaskView 上的一样。
prompt(name, default) -> str这个任务里某个有名字的提示词的文字。
count_tokensasync (text) -> int | None,或 Noneprovider 给的精确 token 数。
completeasync (ModelRequest) -> ModelResponse,或 None调用模型,带重试,不算一步。
progress(text) -> None,或 None让实时观看的前端看到调用正在做什么;从不记录,也不发给模型。

KeptClaim(keep() 的返回值):lost: bool、lease_seconds: float、on_lost(callback)(认领丢了时调用一次;返回一个能撤回这个回调的函数)、release()。

钩子可以抛的异常 ​

异常从哪里抛效果
TurnDonebefore_model、reminders这一轮不调用模型就算完成(某个本地命令已经做完了事)。
RequestStalebefore_model对话变了(压缩):重新拼请求,每个钩子都看到新状态。
TurnFailed(reason, detail="")before_model、reminders、on_context_overflow这一轮以 reason 失败:交互式任务等下一条消息,一次性任务以失败结束。

其他异常怎么处理,见插件体系。