Skip to content

Extension interface ​

Every hook of Extension, every member of TaskView, Tool, ToolContext, ToolOutput and ToolFlags, and the small types they use, read from noeta/core/extension.py, noeta/core/tool.py, noeta/core/records.py and noeta/core/questions.py. For when each hook runs and why, see the plugin system; for a worked example, see Write a plugin.

The names a plugin usually needs import from the package root:

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). Every method is optional; the defaults do nothing.

Class attributes ​

AttributeTypeMeaning
namestrThe plugin's registry name and the name its records are written under. Required; unique within an agent.
add_toClassVar[tuple[str, ...]]For a plugin file: the agents it joins by itself ("*" for every agent), after the agent's own plugins. Ignored for an installed package. enabled = false in the host's setting still turns it off.
foldsClassVar[tuple[str, ...]]Other record names whose records reduce also folds into this plugin's state.

State ​

MethodReturnsCalled
initial_state(self)AnyOnce per fold: the state before any record. Default None.
reduce(self, state, kind: str, data)AnyFor each of this plugin's records (and those of the names in folds), in log order, on every fold. Also with records.ERASED_KIND ("core.erased") and the erased seqs when a person's messages are erased. Must be pure.
continue_state(self, state)AnyWhen a task continues this one's conversation (a fork, a resumed sub-agent): the state it starts from. Default: all of it.

Tools and context ​

MethodReturnsCalled
tools(self, view: TaskView)Sequence[Tool]Every step. The first plugin offering a name wins; a clash is logged once per process.
tool_changes(self, view: TaskView, offered: Sequence[Tool])ToolChanges | NoneEvery step, after every plugin's tools. offered is their tools after the agent's allow and deny lists, never another plugin's changes; every plugin gets the same offered. An added tool the agent's lists refuse drops the whole answer.
context(self, view: TaskView)Sequence[ContextPart]Every model call: text for the system prompt.
async reminders(self, view: TaskView)Sequence[Reminder]Before every model call, ahead of every plugin's before_model (as Claude Code adds its notes before it decides to compact). The runner records them and puts them in this request after the recorded messages, so before_model sees them.
inherit(self, state: TaskState)Mapping[str, Any]When this task spawns a child: config entries laid over the config the spawning tool gave it.

The model call ​

MethodReturnsCalled
async on_start(self, view: TaskView)NoneEach time a runner picks the task up, before the first step.
async before_model(self, view: TaskView, request: ModelRequest)ModelRequestBefore every model call, after the reminders, in plugin order; each gets the previous one's request. May raise TurnDone (no call; the turn ends), RequestStale (the request is built again from the new state) or TurnFailed(reason, detail).
async on_context_overflow(self, view: TaskView, request: ModelRequest)boolThe model refused request as too long. Return True after making room (recording a compaction); the call is made again. When none does, the turn fails with context_overflow. A hook written as (self, view) is still called.
async on_empty_reply(self, view: TaskView, response: ModelResponse)Message | NoneThe reply ended (end_turn) with no text and no tool call. A message to go on with; the first one wins. The core records it in a continue entry (reason empty_reply) and calls the model again. When none comes, the turn ends without an answer (turn_done, reason empty_reply).

Tool calls ​

MethodReturnsCalled
async before_tool(self, view: TaskView, call: ToolUse)Gate | NoneFor each call, every plugin. Any Deny wins, then the first Rewrite (the rewritten call is gated again, at most three rewrites), then the first Ask. None is no opinion.
async resolve_ask(self, view: TaskView, call: ToolUse, asks: Sequence[Ask])Gate | NoneThe call is about to park on asks and nobody denied it. Allow or Deny answers in the person's place, Rewrite runs it with other input (gated again), Ask replaces the asks; the first answer wins. A call can be gated again after a restart, so an answer that cost something should be recorded and reused.
async after_tool(self, view: TaskView, call: ToolUse, output: ToolOutput)ToolOutputAfter the call ran, before its result is recorded, in plugin order.

Events ​

MethodReturnsCalled
async on_event(self, view: TaskView, recorded: Recorded)NoneAfter each appended event, with the state as of the append. Records made here are appended right after; for the TaskEnded that ends the task they are dropped. Each event once, lost on a crash, unless delivered_through says otherwise.
delivered_through(self, view: TaskView)int | NoneThe log seq through which this plugin has handled its events, read from its own state. Whenever the task is claimed, the events after it are passed to on_event again: at least once. None (default): no redelivery.

Compaction ​

MethodReturnsCalled
async before_compact(self, view: TaskView, trigger: Literal["manual", "auto"], instructions: str)str | Deny | NoneBy the compaction plugin, before it summarizes. Text is added to the summary's instructions; Deny skips this compaction.
async resident(self, view: TaskView)Sequence[Message]While compacting: content to re-attach after the kept messages (a listing, the environment). Recorded in Compacted.after.
async lead_resident(self, view: TaskView)Sequence[Message]While compacting: content to rebuild ahead of the summary (instructions, the memory index). Recorded in Compacted.lead.

Commands ​

MethodReturnsCalled
async commands(self, ctx: CommandContext)Sequence[Command]When a front end lists slash commands.
async run_command(self, ctx: CommandContext, name: str, args: str)str/name args for one of this plugin's commands whose template is empty: the message text.

ToolSet(tools, name="tools") wraps a plain list of tools as an Extension.

Gates ​

The answers of before_tool and resolve_ask; Gate = Allow | Deny | Ask | Rewrite.

TypeFieldsMeaning
Allowapprove: bool = False, outright: bool = FalseNo objection. approve: the plugin vouches for the call (a PreToolUse hook that allows it), satisfying other plugins' asks that are not firm. outright: a permission layer allows it by its own settings; the tool sees ToolContext.allowed_outright.
Denyreason: str = ""Refuse; the model sees the reason.
Askprompt: str = "", detail: dict = {}, firm: bool = FalsePark the call until someone answers. prompt is what a front end shows. A firm ask (an ask rule, a hook asking) is not satisfied by another plugin's approving Allow.
Rewriteinput: dict, name: str | None = NoneRun the call with other input, or as another tool.

Parts the hooks return ​

TypeFieldsMeaning
ContextParttext: str, order: int = 100, before: str = ""System prompt text, appended after a newline in order, inside the prompt's cached block; with before, placed ahead of the prompt's first line equal to it. Must be stable: text that changes is recorded as a message instead.
Remindertext: str, kind: str = "reminder", data = None, origin: "system" | "memory" = "system", role: "user" | "system" = "user", order: int = 100Recorded as (extension, kind, data) with its message. role="user" rides in the person's turn; role="system" is a system turn after it. User ones first, then system ones by order.
Commandname, description, template = "", argument_hint = "", local = FalseA slash command. template with {args} is the message sent; empty means run_command gives it. A local command's text is shown to the person and never reaches the model.
CommandContextcwd: str, task_id: str | None = NoneWhere a command is listed or run.
ToolChangesadd: Sequence[Tool] = (), withhold: Mapping[str, str] = {}, entries: Sequence[ToolEntry] = ()Tools offered besides the plugins' own; offered tools left out of the request, each with the error a call to it gets (answered before any gate); request entries that are no tool.
ToolEntryspec: ToolSpec, before: str = ""A tool-list entry that is never called (a vendor's mode switch), placed right before the tool named before, else after the tools that are not listed_last.

TaskView ​

What a hook sees (@dataclass class TaskView): the folded state, read only, and ways to record. Records made during a hook are appended by the runner right after the hook returns.

Fields ​

FieldTypeMeaning
stateTaskStateThe folded task state (read only).
completeasync (ModelRequest) -> ModelResponse, or NoneCall the model through the task's providers, retried, not recorded as a step.
envExecEnv | NoneThe task's exec env.
postasync (body, dedupe_key=None), or NonePost to this task's own inbox.
count_tokensasync (text) -> int | None, or NoneThe provider's exact count of one user message holding text.
read_logasync (task_id, after=0) -> Sequence[Recorded], or NoneAnother task's log, read only; an unknown task reads as empty.
child_tasksasync (task_id) -> list[str], or NoneThe ids of a task's children.
extensionsSequence[Extension]The task's plugins, in the agent's order.
modelstrThe model the task runs on.
agentsMapping[str, Agent]Every agent this worker can run.
allowed_toolsSequence[str] | NoneThe agent's tool allowlist (None: every tool).
denied_toolsSequence[str]Tool names the agent never offers.
keeps_tailbool | NoneWhile a compaction asks for residents: whether messages stay after its summary.
keptSequence[Message]While a compaction asks for residents: the messages that stay.
spill_dirstr | NoneThe spill root for long outputs (None: nothing is saved).
identitystrThe identity line the task's requests open with.
effortEffort | NoneThe task's effort.
next_request() -> ModelRequest, or NoneThe request the next model call would start from (no reminder or before_model run), for work between turns that shares the prompt cache.
pending, creates, enqueuedlistsWhat this hook staged; the runner appends and posts them.

Properties and methods ​

MemberReturnsMeaning
task_idstrThe task's id.
cwdstrThe config's cwd, else the exec env's, else "".
ext_state(name)AnyA plugin's folded state.
folded(reducers=None)TaskStateThe state with every record pending on this view folded in, on a copy.
prompt(name, default)strThe task's text for a named prompt.
record(extension, kind, data=None, *, message=None)NoneRecord an entry; with message, it also joins the conversation. A core record of an unknown kind raises ValueError.
enqueue(body, *, dedupe_key=None, task_id=None)NonePost to this task's inbox (or another existing task's) in the same append as the hook's records, so a crash keeps both or neither.
spawn(agent, prompt, *, config=None, queue=None, key="")str (child id)Stage a child task outside a tool call; tracked like any child (ChildEnded arrives in the inbox). The same key gives the same id.
find_tool(name)Tool | NoneThe tool this task offers under name, how one plugin uses another's tool. Calling its run directly is not gated again.
deferred_tool_names()list[str]The deferred tools this task offers, sorted.
compacted(event: Compacted)NoneRecord a compaction (the compaction plugin's one core event).
async residents(*, lead=False, keeps_tail=None, kept=())list[Message]Every plugin's resident (or lead_resident) messages; a failing plugin is logged and skipped.
notify(text, *, kind=None, data=None)NoneTell the person; front ends show it, the model never sees it.
set_timer(name, at, text="")NoneWake the task at at (epoch seconds) once idle, with text as the next message; same name replaces. Durable: any worker fires it.
clear_timer(name)NoneTake a timer back.
erase_messages(seqs)NoneTake the person's messages at those log seqs (and their inline expansions and memory recalls) out of the conversation.

notify, set_timer, clear_timer and erase_messages come from records.CoreNotices and exist on ToolContext too.

Tool ​

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

MemberTypeMeaning
namestrThe model-visible name.
descriptionstrThe model-facing description; the prompt tool/<name> overrides it.
input_schemadictJSON Schema of the input; checked before any gate unless unchecked_input.
read_onlyboolOnly reads and holds no shared state: may run alongside other read-only calls of the same reply.
clearableboolIts result can be fetched again, so an old one may be cleared from the context.
async run(input, ctx: ToolContext)ToolOutputRun the call. An exception becomes an error result naming it.
flags (optional)ToolFlagsSee below; NO_FLAGS when absent (read with tool_flags(tool)).
async resume(input, ctx, outcome) (optional)ToolOutputRequired of a tool that parks: outcome is the Answer for a question, {child_id: ChildEnded} for children, None when the wait was withdrawn.
async recover(input, ctx) (optional)ToolOutput | NoneFor a call a crash left unfinished: its result, looked up by ctx.idempotency_key; None when it cannot be told. Asked before retry_safe.
async rule_inputs(input, view) (optional)Sequence[(input, allow_too)]Other inputs the permission rules also judge the call as.

function_tool(fn) or @function_tool(name=None, description=None, read_only=False, clearable=False, deferred=False) makes a tool from a typed function: its schema from the annotations (str, int, float, bool, list[...], dict, Literal, X | None, Annotated[T, "text"]), its description from the docstring; a parameter typed ToolContext (or named ctx) gets the context (noeta/toolkit/function_tool.py).

ToolFlags ​

@dataclass(frozen=True) class ToolFlags:

FlagDefaultMeaning
deferredFalseNamed to the model by name only and sent with defer_loading; loaded through ToolSearch.
search_hint""Words a ToolSearch keyword query also matches; never sent.
loaded_descriptionNoneThe description sent instead while the tool goes with defer_loading.
listed_lastFalseSent after the other tools (a changing set at the end of the cached prefix).
run_lastFalseRuns after the reply's other calls.
past_agent_listsFalseOffered whatever the agent's allow and deny lists say.
retry_safeFalseRunning it again changes nothing outside: an unfinished call runs again after a crash (up to MAX_ATTEMPTS = 3 starts).
silent_reply_afterNone"call" or "success": an empty reply after such a call since the person's last message ends the turn.
unchecked_inputFalseInput goes to the tool unchecked.
safe_while_planningFalseNot read-only, yet changes no file and nothing outside the task; plan mode lets it run without asking.
max_result_chars50_000A text result longer than this (at most RESULT_CEILING_CHARS = 50,000) is saved to a file with a preview; None exempts a tool that bounds its own result.

ToolOutput ​

class ToolOutput(Struct); text_output(text, *, is_error=False, **meta) makes a text one.

FieldTypeMeaning
contentlist[Text | Image | ToolReference]What the model gets.
is_errorboolAn error result.
metadictRecorded with the result for front ends and plugins; never sent to the model.
displayDisplay | NoneHow a front end may show it: kind (markdown, code, diff, table, progress, kv), data, fallback.
parkPark | NoneSet instead of a result when the call must wait.
end_turnstr | NoneEnd the turn once this reply's results land, with this text as the answer (a sub-agent's hand-back).
block_listboolSend the result as a list of blocks even when it is plain text.

Park(on: "question" | "children", keys: list[str] = [], detail: dict = {}). A question is made with ask({"question": ..., "header"?, "options"?: [{"label", "description"?}], "multiSelect"?}, ...), and its resume reads the answer with read_answer(outcome, asked) (None when the question was withdrawn; noeta/core/questions.py).

ToolContext ​

@dataclass class ToolContext: what a running tool sees.

FieldTypeMeaning
task_id, call_idstrThe task and this call.
idempotency_key (property)str<task id>:<call id>: the same for every run of this call on any worker. Pass it to an outside system that can drop a repeat, or look the outcome up by it in recover.
envExecEnvThe task's exec env.
stateMapping[str, Any]Every plugin's folded state, by name (read only).
record(extension, kind, data) -> NoneRecord an entry; appended just before the call's ToolFinished. A core record of an unknown kind raises ValueError.
spill_dirstr | NoneWhere full outputs too large to return go.
spawn(agent, prompt, *, config=None, background=False, queue=None, history_from=None, history_through=None) -> str, or NoneStage a child task, created atomically with this call's record, so a retried call never makes a second child. history_from continues that task's conversation.
postasync (body, task_id=None, dedupe_key=None), or NonePost to this task's inbox from outside a step, or to another task (an unknown one raises UnknownTask).
root_idstrThe root task's id.
keep() -> KeptClaim, or NoneKeep this task's claim with this worker, over its waits too, until released: for work that lives only in this process (a background job).
tasks, taskasync, or NoneRows of the store's task table; one task's row.
read_logasync (task_id, after=0), or NoneAnother task's records.
by_modelboolThe model made this call (False when a plugin runs a tool itself).
approvedboolA person allowed this call at its prompt.
allowed_outrightboolA permission layer allowed it by its settings, with nobody asked.
model, identity, effortAs on TaskView.
prompt(name, default) -> strThe task's text for a named prompt.
count_tokensasync (text) -> int | None, or NoneThe provider's exact count.
completeasync (ModelRequest) -> ModelResponse, or NoneCall the model, retried, not a step.
progress(text) -> None, or NoneShow front ends watching live what the call is doing; never recorded nor sent to the model.

KeptClaim (what keep() returns): lost: bool, lease_seconds: float, on_lost(callback) (called once the claim is lost; returns a function that takes it back), release().

Exceptions a hook may raise ​

ExceptionFromEffect
TurnDonebefore_model, remindersThe turn is complete without a model call (a local command did its work).
RequestStalebefore_modelThe conversation changed (a compaction): the request is built again, every hook seeing the new state.
TurnFailed(reason, detail="")before_model, reminders, on_context_overflowThe turn fails with reason: an interactive task waits for the next message, a one-shot task ends failed.

Any other exception is handled as the plugin system page describes.