Skip to content

Compared with other agents ​

How noeta's plugin surface lines up with three other agents: Claude Code (whose behaviour noeta follows), pi and DeepSeek Harness (dsh). Use it to port a plugin, or to see what noeta left out on purpose and why. How noeta checks that the model sees what Claude Code would send is a separate page, Matching Claude Code.

The short version ​

Claude CodepiDeepSeek Harness (dsh)noeta
Runs asone local process per sessionone processone processany number of workers on one store, or one process
A task's state lives inthe local sessionthe process, plus session entriesthe process, plus session eventsthe task's log in the store; everything is folded from it
Another machine can continue a tasknononoyes, from its log
Plugins written inmarkdown, JSON, shell / HTTP hooks, MCPTypeScriptTypeScript (Cordis)Python, plus Claude Code's markdown, JSON, hooks and MCP formats
A task's plugin setwhatever is loadedwhatever is loadedwhatever is loadedrecorded when it starts; a worker without one never runs it
Sends data home by defaulttelemetry where analytics are oninstall telemetry, provider attribution headerssession log, plugin inventory, an anonymous id (official API)nothing

What follows is the detail. Only noeta's points hold across machines: state is folded from the task log, the plugin set is pinned per task, and any worker resumes any task; pi and dsh run in one process, Claude Code in one local session.

Steps to write a plugin, next to pi ​

One tool plus an event handler, from nothing:

Stepnoetapi
1. installuv tool install --with 'noeta-tui @ git+https://github.com/initxy/noeta#subdirectory=tui' 'noeta @ git+https://github.com/initxy/noeta'npm install -g @earendil-works/pi-coding-agent
2. model keyNOETA_API_KEY / ANTHROPIC_API_KEY, or api_key in config.toml/login, or a provider key variable
3. write one file~/.noeta/plugins/x.py: an Extension with add_to = ("main",), one from noeta import … line~/.pi/agent/extensions/x.ts
4. run itnoeta, or /reload-plugins in a running onepi, or /reload
a file kept elsewhere, one runnoeta --plugin-dir ./x.pypi -e ./x.ts

Both take 4 steps, and 1 command to try a file in place. A skill is 1 step in noeta (a SKILL.md folder in ~/.noeta/skills). The noeta side is walked through in Write a plugin.

Compared item by item ​

Sources: pi-mono e7a0df2 (coding-agent 1.4.0), DeepSeek Harness f3803c96 (0.2.2-alpha.2), Claude Code 2.1.312–2.1.314. Verdicts: have (noeta covers it, maybe in another shape), added (built for this comparison), won't (left out on purpose). The noeta column names the code; Extension interface has each method.

Extension pointpidshClaude CodenoetaVerdictReason
Lifecycle
Task startsession_startsession/created, agent/createdSessionStarton_start; hook SessionStarthaveWhat is captured there is recorded, so a resumed task sends it again.
Task endsession_shutdownsession/disposedSessionEndon_event (TaskEnded); hook SessionEndhaveAn end is an event in the log.
Every appended event—session/event—on_eventhaveMetrics, audit and bridges read the log as it grows.
Turn, model call, idle / busyturn_*, agent_*, agent_settledagent/status—on_event: InputTaken, ModelCalled, ModelReplied, WaitinghaveEach is a logged event; no second bus.
Keep a turn going at its endagent_before_settle, turn_endagent/turn-stoppingStop, SubagentStopon_event on ModelReplied posting Say(when="now"); hook StophaveGoing on is an inbox item, so it survives a crash.
Transform or block the person's inputinputagent/pre-stepUserPromptSubmitreminders + view.erase_messages + raise TurnFailed; hook UserPromptSubmithaveA blocked prompt is erased by a core record (ADR-026).
Before / after compaction, own summarysession_before_compact, session_compacta CompactionEnginePreCompact, PostCompactbefore_compact (text or Deny); on_event (Compacted); a replacement records view.compactedhaveCompaction is a plugin; others take part through before_compact and resident.
Context overflow—agent/request-errorbuilt inon_context_overflow(view, request)addedThe hook is handed the request that overflowed, so no plugin has to keep a copy per task in memory.
Instruction files loaded——InstructionsLoadedhook InstructionsLoadedaddedClaude Code has it: once per instruction file shown.
Model or effort switchedmodel_select, thinking_level_selectagent-preset/selectedPostModelSwitchon_event (InputTaken); hook PostModelSwitchhaveA switch is input in the log.
Veto a fork, switch or tree movesession_before_fork, _switch, _treesessions.fork/rewind, no hooka fork is a new task (history_from); continue_state picks what carries overwon'tA fork never changes its source task, so there is nothing to veto.
A plugin grants project trustproject_trust—trust dialognoeta trustwon'tTrust is the person's call; code that grants it widens what code may do.
Claude Code's other hook events——PreModelSwitch, Setup, TeammateIdle, Elicitation, ConfigChange, FileChanged, Worktree…warned and skipped (noeta/plugins/hooks/config.py)won'tEach belongs to a Claude Code feature noeta does not have.
Loader, HMR, market, web host events—Cordis loader/*, hmr/*, plugin-market/*, webserver/*—entry points, the HTTP APIwon'tThey are dsh's loader and web host, not agent behaviour.
Model requests
Change one request (messages, system)context, context_with_systemagent/pre-step, system-prompt/assemble—before_model on the ModelRequest; TurnDone, RequestStalehaveThe request stays a function of the log and these hooks.
Request headersbefore_provider_headersdeepseekLlmApiExtensionscustom-headers variableModelRequest.headers in before_model; settings extra_headershavePer request, from the hooks.
Rewrite the raw provider payloadbefore_provider_requestllm/stream——won'tA request must be reproducible from the log; wire changes go in a provider.
Raw response, stream deltasafter_provider_response, provider_stream_eventagent/assistant-stream—ModelReplied via on_event; deltas reach front ends onlywon'tDeltas are never stored: a plugin acting on them acts on what the log does not hold.
Replace the final replymessage_end———won'tThe recorded reply is the truth; change the next request instead.
Cache warmingcache_warming_decision———won'tA call per idle task breaks "waiting costs nothing"; cache breakpoints do the work.
Retry policy as a hook—agent/request-errorbuilt inrecorded kernel retries (noeta/core/retry.py)won'tRetries are the same on every worker and visible in the log.
Tools
Register a tool, change the set at runtimeregisterTooltools.registerMCP server, SDK tool()tools(view) every step; function_toolhaveThe set may change from one step to the next.
Override or wrap a built-insame name wins; create*Tooltools.setExecutorPreToolUse updatedInputRewrite, after_tool, view.find_tool; switch the built-in's plugin offhaveThe first name wins, so a replacement is explicit, never a silent shadow.
Gate a calltool_calltools/pre-executePreToolUse, rulesbefore_tool → Allow / Deny / Rewrite / AskhaveDeny > Rewrite > Ask > Allow; a rewrite is gated again.
Answer an ask in code—approval/requestPermissionRequest, canUseToolresolve_askhaveAnswered before the call parks.
Change a resulttool_resulttools/post-executePostToolUseafter_toolhaveBefore the result is recorded.
Wrap the whole dispatch—tools/execute—before_tool + after_toolwon'tRetries belong to the tool (recover, retry_safe), timing to ToolStarted / ToolFinished.
Deferred toolsexposurewhendefer_loading + ToolSearchToolFlags.deferred; tool_changeshaveSent only once ToolSearch loads them.
Parallel or lastexecutionModeisConcurrencySaferead-only tools togetherTool.read_only, ToolFlags.run_lasthaveRead-only calls of one reply run together.
Progress while a tool runsonUpdate—live Bash output, tool_progressToolContext.progressaddedClaude Code has it; it streams with the live output and is never recorded.
A tool calls the modelmodelRegistry.completectx.llmWebFetch's small modelToolContext.completeaddedWebFetch's digest goes through it, so a call that moves to another worker still reaches the task's model.
A tool calls another toolctx.executeTool——view.find_tool(name).run(…)haveNot gated again (by_model is false).
Wait for a person or childrenblocking ui.confirmapproval, questionsAskUserQuestionask(), ToolOutput.parkhaveThe task holds no worker while it waits.
Crash recovery of one call———Tool.recover, ToolFlags.retry_safe, ctx.idempotency_keyhavenoeta only: a call never runs twice unknowingly.
Background work—jobs.startbackground Bashctx.keep()haveThe task keeps its claim on the worker the job lives on.
Structured output for the modeloutputSchemaoutput.schemaMCP structuredContentToolOutput.meta, display for front endswon'tThe model reads content; structure rides along for front ends.
Rendering code for calls and resultsrenderCall, renderResultUI slots—ToolOutput.display as datawon'tFront ends import no plugin and may be remote or in another language.
Shell environmentspawnHookshellEnv.registersettings envsettings env; a Rewrite per callhaveClaude Code's settings key.
Commands and flags
Slash commandsregisterCommandcommands.registercommands/*.md, skillscommands / run_command; commands/*.mdhaveFront ends list and expand them (noeta/core/commands.py).
Argument completiongetArgumentCompletions—argument-hintCommand.argument_hinthaveA hint is data; a callback cannot reach a remote front end.
A command drives the sessioncommand contextctx.sessionsbuilt-ins onlythe client APIwon'tNew, fork and reload belong to front ends over the client, not to code inside a task.
KeybindingsregisterShortcutshortcuts.registerthe person's keybindings.json—won'tPlugins never reach into front ends; Claude Code lets people, not plugins, bind keys.
Plugin command-line flagsregisterFlagcmdlineArgs—[plugins.<name>], agent-file argumentswon'tA flag lives in one process; arguments are pinned per task and the same on every worker.
Load a plugin for one runpi -e--patch (a config overlay that may add plugins)--plugin-dir--plugin-diraddedClaude Code has it.
Context
System prompt textsystemPromptOptionssystemPrompt.sectionoutput styles, --append-system-promptContextParthaveInside the one cached block.
Replace the system promptSYSTEM.mdsystem-prompt/assemble--system-promptprompt override system/<agent>; agent file bodyhaveOverrides are recorded when a task starts, so a resumed task sends the same bytes.
Notes each turnsendMessage(nextTurn)agent.inject<system-reminder>remindershaveRecorded, so a resumed task resends them.
Steer mid-turnsendUserMessageagent.steerstreaming inputview.post, Say(when="now")haveAn inbox item, taken at the next step.
Skills, prompt templates, instruction filesSKILL.md, prompts/, AGENTS.mdskills.register, agent instructionsskills, commands, CLAUDE.mdskills, markdown_commands, instructions (NOETA.md, AGENTS.md)haveClaude Code's formats in noeta's own places (ADR-004).
Code plugin adds skills, commands, agentsresources_discoverskills.registerProvidera plugin's directoriesa Python plugin directory's skills/, commands/, agents/addedA Python plugin directory may ship them beside its code.
Text kept across a compaction——built inresident, lead_residenthaveAn interface, so any plugin's text survives.
UI
Dialogsui.select, confirm, inputapproval, questionsAskUserQuestion, permission promptsask(), AskhaveEvery front end shows them with no plugin code.
Noticesui.notifytoast slotNotification hookview.notify(text, kind, data)haveNever reaches the model as the person.
Panels, widgets, editor, themesui.*106 UI slotsmods, themes—won'tFront ends import no plugin; a plugin publishes data a front end renders.
Status linesetStatus—statusLine setting—won'tLeft to plugins: records and notify carry what a front end may show.
Plugin HTTP routes—webServer.register——won'tOne API; traffic from outside comes in through a Channel.
State
Plugin entries, rebuilt from historyappendEntrysession events, projections—view.record + reduce, snapshotshavePlugin state is a fold of the log.
Labels on entriessetLabel———won'tNo Claude Code counterpart; a plugin can record its own.
Timers—ctx.schedulescheduled wake-upsview.set_timerhaveA timer is a record; the store fires it on any worker.
Read other tasksSessionManager.listsessionQuery—read_log, child_tasks, ctx.taskshaveRead only.
Storage backend—SessionPersistence—noeta.stores, store_contracthaveChecked by the shared contract tests.
Models
A provider (wire)registerProviderllm.registerAdapter—a noeta.providers packagehaveFound by name through entry points, like the built-ins.
Several providers, a model switched across themmodels.json providers; /model across themnot checkedone provider at a time; /model among its models[providers.<name>] tables; /model, stream-json set_model and ACP switch across them; the chosen provider and the model's limits are recorded in the task's configaddedThe owner asked for any model, switched mid-task; recording the route keeps a resumed task on the same provider on every worker.
A provider from a plugin fileyesyes——won'tThe plugin pin does not cover providers; one on a single worker breaks "any worker resumes any task".
Models and their familymodels.jsonregisterModelDiscovery—register_models, a models file; any familyaddedA third-party provider declares its own family; only anthropic and openai carry conventions.
Login providersoauthDeepSeek account/loginkeys, {env:} / {keyring:}won'tA server holds operator keys; subscription login is a desktop concern.
Route per requestregisterVirtualModelllm/stream—provider_for; before_model sets modelhaveFrom an SDK host.
Sub-agents
Sub-agent definitionsan examplesubagents.registerProvideragents/*.mdagent files, subtaskshaveClaude Code's agent files.
Per-agent plugins, config to children, queues—agent presetsper-agent tools, MCP, hooksagent plugins, inherit, spawn(queue=)haveA child may run on another machine's queue.
Other harnesses as sub-agents—codex, claude-code, ACP——won'tA sub-agent is a task in this store; another harness is a tool or an MCP server.
Packaging
One file found by itself~/.pi/agent/extensions—a plugin directory~/.noeta/plugins, a trusted .noeta/pluginshaveA project's code loads only once trusted.
Packages and installnpm, git, pi installdsh plugin add/plugin installentry points; noeta plugin add <git>@<commit>haveInstalling runs nothing from the plugin.
Hosted gallery, auto-updategallery, pi updateplugin marketmarketplaces, auto-updatereads a Claude Code marketplace.jsonwon'tAn operations product and a supply-chain risk; pinned installs instead.
Hot reload/reloadHMR/reload-plugins/reload-plugins between turns; rolling restarthaveNever inside a turn: a request must stay a function of the log.
Plugin set pinned per task———records.Pluginshavenoeta only: a worker without a plugin never runs the task without it.
Bus or services between pluginspi.eventsCordis services—through the kernel onlywon'tState stays in the log, and plugins stay replaceable one by one.
The agent loads plugins it wrote—dynamic Cordis—writes to .noeta/ askwon'tModel output would install code into the harness.
Sandbox for in-process plugin code——mods behind an APIexec env, MCP servers, command hookswon'tNot achievable in CPython; isolation is offered where it is real.
Names a plugin importsExtensionAPItyped ctx—from noeta import Extension, Tool, Allow, Deny, Ask, Rewrite, ToolUse, Recorded, ToolChanges, TaskState, …addedOne import line for a plugin file.
Config
Per-plugin config, secretssettings keys, $ENVconfig schemasuserConfig, keychain[plugins.<name>]; {env:}, {keyring:}haveA secret is resolved only when the plugin is made.
A Claude Code plugin's userConfig, settings.json——asked on enable; agentasked on noeta plugin add; agentaddedClaude Code has them.
Read others' settings, live configgetSettingsctx.settings, Volatile—a plugin's own argumentswon'tOne plugin should not depend on another's settings; a change is a reload, pinned per task.
Monitors, bin/——plugin parts—won'tA per-session process does not fit a task that holds no worker while waiting (use a channel or a timer); bin/ silently widens what Bash runs.
LSP servers——plugin part—won'tA language server holds one checkout's state on one machine; run the checker as a tool in the exec env (examples/run-tests) or behind an MCP server.
Output styles——plugin partprompt text recorded at start (examples/house-style)won'tnoeta has no style switch; a style is prompt text a plugin adds.
Themes——plugin part—won'tColors belong to each front end.
Data sent home by defaultinstall telemetry and provider attribution headers (PI_TELEMETRY)on official-API requests: the session log (dsh_session_log), the plugin inventory (dsh_plugin_packages) and an anonymous user idtelemetry where analytics are onnothingwon'tNever. An opt-in exporter can be an on_event plugin the operator installs.

What the "won't" rows have in common ​

Most of what noeta leaves out falls under four rules, each a consequence of a task being a log that any worker may continue:

  • The request is a function of the log. Nothing may change the bytes sent to the model unless the log can reproduce it on another worker: no raw payload rewriting, no acting on stream deltas, no replacing the recorded reply, no hot reload inside a turn.
  • Plugins never reach into front ends. Front ends may be remote, in another language, or several at once, so a plugin publishes data (records, notify, ToolOutput.display) and each front end decides how to show it: no panels, keybindings, themes or render callbacks.
  • Nothing lives in one process unless a claim keeps it there. Per-session monitors, LSP servers and plugin-only providers would tie a task to one machine; a channel, a timer, an exec env or ctx.keep() is used instead.
  • What widens what code may do is the person's call. Plugins cannot grant trust, the model's writes to .noeta/ are asked about (so it cannot install a plugin it wrote), and installs are pinned and run nothing.