KORIOINC / PI EXTENSIONSCURRENT IMPLEMENTATION

@korioinc/pi-subagents

Foreground work, explicitly owned.

One process. Three execution slots by default, user-configurable. Runner coordinates invocation lifetime; Session owns the SDK child. Parent adapters retain authentication and selected tool dispatch. This page maps the current code, not a proposed design.

BASE bf1e4a9 + local refactorsSOURCE DATE 2026-10-23MODE FOREGROUND / NO QUEUE

01 — Execution & ownership

DEPENDENCIES, NOT NETWORK HOPS

RunnerPool reserves capacity and deadlines, then synchronously invokes AgentRegistry to commit the parent fence and intent. Only successful commitment enables progress and execution. A failed uncommitted admission clears its own references before releasing the logical lane, so delayed finalization cannot revoke a newer retry. Runtime prepares configuration; Runner coordinates ChildSession and safe cleanup.

Execution ownership and parent dependencies Controller enters AgentRegistry, which reserves RunnerPool capacity and commits admission before Runner prepares through Runtime and starts ChildSession. The registry supplies in-memory history to each disposable child runtime. Single process · one parent generation Parent services · same process SUBMIT ADMIT PREPARE CONFIG LIFECYCLE HISTORY CONTROLLER + REGISTRY Logical admission validate + fence + intent authorize every disclosure BEFORE CAPTURE / DISCOVERY RunnerPool 3 slots default · configurable startup + run deadlines ADMITTED INVOCATION Runner host + mode-aware prompt budget / pending ledger IN-PROCESS COLLABORATOR Runtime preparation agents / skills / tools bounded, frozen config OWNED MCP SEPARATE USER CONFIG Selected MCP clients direct / search + call REGISTRY-OWNED, SDK-NATIVE Native child history generation-local memory SRC/SESSION ChildSession owns SDK child + events prompt / stream / usage abort / shutdown / dispose MODEL PATH Parent model dispatch modelRegistry.streamSimple parent auth / OAuth ORDINARY TOOLS ONLY Parent tool dispatch ctx.executeTool() parent permission hooks Solid arrows: work / dependency · dashed: configuration return
Results settle through ChildSession → Runner → AgentRegistry history validation → RunnerPool → Controller. Delivery requires the same generation, parent session, workspace and project-trust state, with the latest accepted input fence on the branch. Continuation requires a validated parent fence; a null fence grants no authority. All agents require a manually created definition in the user agents directory. Definitions can explicitly select local read_text / list_files when absent from parent tools. Controller · Runner · Runtime

Capabilities follow tools, not inherited text. Append inherits the parent policy without resource catalogs, followed by the fixed subagent role. Replace starts with the optional prompt_instruction_path file, then the fixed role, without the parent prompt. These inputs form the native preamble. The SDK then renders the agent body as <addendum>, eligible skills, cwd and the indirect MCP catalog. Replace requires a nonempty body. Disabled definitions and unknown semantic fields fail closed.

Shared workspace and stable authority. Named agents may mutate the parent workspace through selected tools. Same-path directory replacement revokes retained authority. Parent-dispatched tools retain permission hooks and combined cancellation. The SDK's native write/edit queue serializes same-file operations within its process/module; it does not cover bash, MCP or the whole workspace. Native child conversation retention is separate from volatile run answers.

Session passes native systemPrompt, appendSystemPrompt and selected-only skillsOverride inputs to the SDK resource loader. No custom composer or forced final prompt remains. The task is a separate initial user message inside <subagent_task>, with &<> escaped as XML text. Optional reference warning and REFERENCE JSON come before that task block. Each spawn starts a new conversation without parent entries; follow-up retains only that child’s native history.

One resource owner, one completion coordinator. Session implements SDK construction, binding, events and teardown. The native provider bridge uses public Provider.streamSimple and lazyStream, not an agent stream override. It owns producer settlement, late fetch acquisition and reader cleanup; terminal delivery alone cannot release a request. Before each dispatch, ParentProvider checks the actual system text and effective tool declarations, including dynamic SDK changes. Runner orders abort() → Budget.drain() → close() before Pool settlement. Abort starts MCP closure but waits only for SDK abort. After operation drainage, close joins MCP and provider closure alongside SDK shutdown/disposal; an idle SDK no longer waits for MCP closure. Final settlement still requires every owner. Session reports transport promises into Runner's existing ledger; it does not create a second ledger or custom conversation engine. ChildSession implementation.

02 — Lifecycle & completion

spawn_agent creates a new agentId and first runId. followup_task({ agentId, requestId, message }) gives an idle agent a fresh foreground runtime with current policy/tools and its native conversation. A running agent instead receives steering and a receipt with no usage or independent answer; the original run retains its remaining budget, deadline and completion owner. Identical authorized retries return receipts without repeated effects or usage. send_message stays supplemental data, not a task or new turn.

close_agent({ agentId }) idempotently closes idle or history-unusable agents and releases in-memory history and receipts. Active/blocked work must first be interrupted and safely settled. Spawn, follow-up and close are direct model-only tools, denied to children. subagent_inspect accepts one of runId, agentId or scope: "runs" | "agents"; no selector preserves run inspection. Agent inspection returns metadata/receipts, never transcripts or credentials. Controls, results, handoffs and activity require the same parent/workspace and a branch containing the latest accepted input fence. Existing activity presentation is unchanged.

AgentRegistry owns native SessionManager.inMemory() history until explicit close or the parent extension generation ends. Idle history has no time-based expiry. Reload, parent-session replacement and exit drain owned work, then discard history. A later generation cannot reopen the conversation. Terminal results and closed-agent tombstones retain their separate one-hour limits.

Memory capacity failures mark history history_unusable and stop continuation rather than truncate it. Disk storage and backups are outside this package; earlier disk files remain untouched, without migration or deletion. No automatic replay or background execution is added. See the memory-only history lifetime and agent continuation guide.

POOL PHASES ARE AUTHORITATIVE

Execution ending starts settlement, not release. Stop requests cancel further work; cleanup and accounting still own the slot and foreground completion.

Run phases and the safe completion barrier Preparing and running lead to settling; stop intent can enter cancelling, grace expiry enters blocked, and only a safe settlement resolves terminal, including a direct blocked-to-terminal transition if the executor is still pending. START ENDS SAFE PREP ENDS STOP STOP WORK ENDS 5S GRACE STOP + GRACE LATE SAFE ADMITTED preparing policy + bounded reads child creation EXECUTING running child model + tools owned delegated calls COMPLETION BARRIER settling calls / callbacks / SDK cleanup + final usage SAFE OWNERSHIP RESOLVED terminal success / failure / cancel release + resolve once STOP INTENT cancelling no further work await owned operations PROGRESS, NOT COMPLETION blocked slot + foreground held unknown safety: stay here Late-safe edge requires a still-pending executor; blocked never transitions back to settling.
Updates report admission, running, settling, and blocked states; inspect/cancel reads the current phase. Progress never releases ownership. Pre-dispatch abort or expiry can go straight to terminal. Terminal releases the slot, then retains a bounded volatile answer. RunnerPool.submit / stop

Executor rejection is different from late cleanup. Rejection immediately leaves the run blocked, with no future settlement continuation or autonomous recovery. A blocked run marks pool health degraded but retains only its own slot. Other free slots remain usable. That run's foreground completion and the shutdown barrier remain pending while cleanup is unconfirmed. Stop during settlement preserves the outcome; the 5s grace starts at the first stop request, not at entry to settling.

One usage owner per path. Outer result carries child-model usage. Parent tool dispatch records nested tool usage; forwarded results omit duplicates. Parent SDK combines both exactly once.

Cleanup precedes completion. Runner waits for started delegated calls, callbacks, provider/transport cleanup, and safe SDK shutdown. A grace timeout reports blocked; it does not prove safety.

03 — Catalog & generation drain

SEPARATE LIFETIME · NO EXECUTION SLOT

AgentCatalog owns one catalog API list task per generation; admitted named runs discover independently. Concurrent lists return busy; unsafe or draining catalogs return unavailable. Discovery starts no child, provider call, or tool execution.

Catalog ownership and concurrent generation drain Catalog listing runs one bounded discovery task without an execution slot, while session shutdown concurrently cancels and awaits pool and catalog drains; unsafe catalog cleanup disables later listings and keeps its own promise pending without draining the pool. Catalog request LIST SCAN RETURN AWAIT AWAIT CONTROLLER TOOL list_agents bounded tool response no execution admission ONE OWNED TASK AgentCatalog.list() active → busy draining → unavailable BOUNDED PREPARATION Agent discovery roots / reads / parse 60s startup deadline BOUNDED CATALOG Names & diagnostics omission counts included partial ≠ absent CONTROLLER EVENT session_shutdown start both drains concurrently CANCEL RUNS pool.drain() await every run.done CANCEL SCAN catalog.drain() await owned catalog task GENERATION BARRIER Await both drains then discard generation unsafe → remain pending Unsafe catalog: own promise held; pool continues.
Shutdown rejects new work, cancels owned work, and awaits both lifetimes. Unsafe catalog cleanup disables later listings and retains its own unresolved promise; existing runs and independent per-run discovery continue. Shutdown handler · AgentCatalog

Child-owned selected MCP

NO PARENT MCP FALLBACK

mcp: [figma-bridge] selects only entries in mcp-sub-agents.json. ChildSession connects every selected server before prompting. Parent MCP, codemode, search/resource routes and declared aggregators are not inherited.

Small model context
Default codemode exposure uses native child ALL_TOOLS, searchTools(), describeTool() and tools.mcp__…(). Native child tool_search activates deferred tools; direct tools are declared and hidden tools remain unreachable. The SDK server catalog advertises indirect codemode/deferred access; direct tools use declarations. No parent registry is inherited.
Owned from acquisition
Abort starts MCP closure and SDK cancellation concurrently, then Runner drains Budget. First-close failures remain unsafe. Native registrations and nested calls retain time and output budgets. Each actual tool call consumes one start; codemode itself consumes neither the tool allowance nor a leaf concurrency slot.
Explicit permissions
Selected user-configured servers authorize child-local calls; parent permission extensions are not inherited. Stdio receives minimal operational environment plus explicit values. HTTP redirects and oversized bodies are rejected.

Supported: stdio and HTTPS/loopback HTTP with explicit env/header credentials. OAuth/provider-token auth and command-based secrets remain unsupported. Native resources, dynamic tool-list refresh and schema validation follow main Pi within the selected server set. See configuration and native SDK boundaries. No live user MCP server was used for local validation.

Explicit child skills

PARENT ACTIVATION UNCHANGED

The extension catalogs bounded local default/settings/package candidates, including parent-disabled skills. Only explicitly selected names, descriptions and locations go to the native resource loader. The SDK renders <skills> when native read or bash is selected; a hidden direct declaration uses indirect-access wording. Empty tools, read_text alone and arbitrary custom/MCP readers do not qualify. Candidate and selected frontmatter are read and revalidated. Runtime execution preparation validates selected bodies and computes the continuation fingerprint without inserting bodies into the initial prompt. Explicit selection opts in disable-model-invocation: true skills under the same reader gate. No package installation, network lookup or parent activation changes occur. Package manifest exclusions still apply; duplicate names keep the first valid discovered skill.

Unknown dynamic roots require explicit local settings paths. skill_add_folders and skills: all are removed. Append replaces parent skill, tool and MCP catalogs and removes this extension's exact native delegation rules. Generated parent cwd is removed only when no custom cwd section was supplied; explicit custom cwd policy is preserved. Other policy sections are preserved; opaque forced prompts fail. Neither mode inherits parent package/extension skill catalogs. Omitted or empty skill selection advertises none and performs no skill discovery. @name task requests parent-model delegation through spawn_agent({ agent, task }); it does not guarantee execution. See source coverage and migration.

Diagnose without unlocking

OBSERVATIONS, NOT SAFETY EVIDENCE

subagent_inspect({ runId }) separates pool phase from Runner stage. The no-ID form also includes catalog health and origin-authorized scan diagnostics. These snapshots never change admission, cancellation or completion.

Where and how long
Current stage, phase/stage age, admission and first-stop time. Monotonic durations freeze at terminal completion; repeated cancellation does not reset them.
What still owns work
pendingTools and pendingTransports count tracked top-level calls and provider requests through cleanup, not every nested operation or socket. Zero is not proof of safety.
Recovery distinction
Blocked recovery: await_owned_work may still settle safely. recovery: containment_required means no automatic recovery continuation. stage: mcp_close means SDK cleanup attempts finished while MCP closure remains pending. None of these observations authorizes force release or automatic replay.

Bounded, owner-only activity. ID-specific inspect adds budget-admission totals and up to eight recent SDK tool names/states. Names are capped at 128 UTF-8 bytes; arguments, results, raw errors, prompts and credentials are excluded. Activity is absent from progress, final/cancel responses and no-ID lists. Tool names can identify servers; the same session and submission-anchor ancestry is required.

Containment is not recovery. A blocked run retains its own slot; other free slots remain usable. Unsafe catalog cleanup disables only catalog listings. Unsafe cleanup retains its owner's foreground completion and shutdown barrier. Remaining capacity does not isolate shared-workspace or external effects. Process termination may lose volatile results and usage; never automatically replay accepted work.

SDK adapter is in Session. Runner retains orchestration and budgets. Stable SDK >=1.4.0 <2.0.0 is supported. The development workspace uses SDK 1.4.0.

Runtime bounds

COOPERATIVE LIMITS, NOT ISOLATION
Execution
3 runs by default · startup 60s · invocation 30m · stop grace 5s. Full → busy; draining → unavailable; degraded → remaining capacity usable. No queue. Set maxConcurrentRuns from 1 through 32 in user sub-agents.json; reload to apply. Each provider owner serializes its requests through transport cleanup. Each run permits 256 actual tool starts and 4 concurrent leaf tools. Codemode itself is excluded; each nested tool call consumes one start.
Preparation
Per file 64 KiB · candidate reads 8 MiB · roots 32 · entries 4096 · depth 16 · prompt/configuration 4 MiB. Preparation checks raw inputs; preflight leaves rendered_prompt unchecked, along with skill_body_validation when skills are selected. Startup checks the SDK prompt, and each dispatch checks final system text plus effective tool declarations.
Retention
32 terminal answers · 1h · 256 KiB per answer · 32 open agents · 256 MiB generation history. Volatile and origin-scoped. Active or blocked ownership never expires. Each serialized tool result or update has a 64 MiB ceiling, checked after JSON serialization. SDK text formatting/spill and structured results remain native. No aggregate tool-result memory is reserved. Cumulative model tokens are uncapped unless the definition sets max_model_tokens.

Turn, invocation, and tool deadlines enter safe guard: no new work, up to 30s to finish existing operations, then one no-tools handoff (up to 60s / 2,048 output tokens, within a 90s window). The result remains incomplete. Hard cancellation and token caps still win; unresolved cleanup retains ownership and prevents summarization.

These bounds do not universally cap provider retries, billing, process memory, or non-cooperative operations. No timeout proves cleanup safety. Preparation limits · Run budgets · Pool and retention

Source index

0e74535 BASE + CHILD MCP · 2026-10-17