Skip to content

Core interface reference

The supported boundaries between mikan's platform, runtime, harness, session, execution, vault, extension, and host layers.

This page documents mikan’s architectural interfaces: the contracts that cross a subsystem, process, persistence, or package boundary. It is not an inventory of every exported helper. A helper used only inside one subsystem is an implementation detail even when TypeScript currently marks it export.

LevelMeaningCompatibility expectation
PublicImported by npm consumers or extension authorsPreserve or version deliberately
Host integrationUsed to embed the runtime or add a platform/runtime backendPreserve while the integration exists
Wire / persistedStored on disk or sent across a process/network boundaryMigrate explicitly; readers should tolerate supported older forms
InternalConnects modules inside the mikan CLIMay change with coordinated call sites and tests

The package currently exposes more symbols than this policy intends because src/index.ts re-exports the entire harness. Treat the table below as the intended contract; see Core simplification for the migration.

flowchart TD
P["Platform adapter"] --> I["Normalized intake"]
I --> R["Conversation runtime"]
R --> H["Agent harness"]
H --> T["Agent tools"]
T --> E["Executor"]
R --> S["Session and chat stores"]
R --> V["Vault and execution resolver"]
X["Host extensions"] --> H
W["Web and events"] --> R

The runtime is the coordinator. Platforms must not know harness persistence, the harness must not know platform SDK objects, and tools must not know whether execution is local, containerized, or remote.

The npm entry point is @geminixiang/mikan, implemented by src/index.ts.

GroupMain symbolsConsumer
Runtime embeddingcreateConversationRuntime, ConversationRuntime, ConversationRuntimeOptionsA host that supplies platform and execution dependencies
Platform contractMessagingBot, ConversationEvent, ConversationContext, ConversationResponder, MessagingInfoPlatform adapters and embedders
CommandsCommandHandler, CommandContext, CommandServices, dispatchCommandHosts adding deterministic commands
ExecutionExecutor, SandboxConfig, SandboxAdapter, createExecutor, parseSandboxArg, validateSandboxRuntime backends and hosts
ExtensionsMikanExtensionApi, hook/event types, subagent typesTrusted extension authors
Harness embeddingMikanAgentSession, SessionStore, MikanModels, settings and event-format functionsAdvanced embedders; not ordinary bot integrations

Importing a source path under src/ or a generated path under dist/ is unsupported. Only paths declared by package exports should be considered public. At present the package has no explicit exports map, so this is a documentation constraint rather than an enforced one.

Source: src/types.ts (re-exported by src/adapter.ts).

The platform-neutral trigger delivered to MessagingEventHandler.

FieldContract
conversationIdRaw platform conversation/channel identifier; must not contain : because session keys reserve it
conversationKinddirect or shared; affects session and credential policy
tsTriggering platform message identifier
thread_tsOptional parent/root platform message identifier
userPlatform user identifier
textText after platform mention removal
attachmentsFiles already downloaded to host paths
sessionKeyOptional platform-selected override; otherwise derived by session policy
vaultConversationIdOptional alternate identity used only for credential routing

Carries the richer normalized message, response port, and platform metadata for the same run:

interface ConversationContext {
message: ConversationMessage;
responder: ConversationResponder;
platform: MessagingInfo;
}

ConversationMessage and ConversationEvent currently duplicate message id, session, conversation kind, user, text, attachments, and thread identity. Adapters must keep both representations consistent. This is an internal compatibility seam, not a desirable model for new integrations.

The runtime/harness output port. Required operations cover final text, replacement, diagnostics, tool status, typing/working state, file upload, and response deletion. Streaming (appendResponseDelta, finishResponse) and reaction (react) are optional capabilities.

Capability rule: callers must feature-detect optional methods. An adapter may buffer output or implement streaming natively, but it must preserve call order for a single run.

The host-facing platform port. It combines:

  • lifecycle: start();
  • outbound messages: postMessage, updateMessage, optional upload/reaction/private replies;
  • intake scheduling: enqueueEvent;
  • discovery/policy: getMessagingInfo().

MessagingInfo.trustModel is security-sensitive. membership permits the ambient shared-vault policy when the sandbox topology is isolated. An open trigger surface such as public GitHub activity must return open-trigger.

ChatAdapter is a smaller lifecycle-only interface (start, stop, getMessagingInfo) but the CLI uses MessagingBot. Do not implement both unless a caller explicitly requires ChatAdapter.

Source: src/runtime/types.ts and src/runtime/conversation-runtime.ts.

createConversationRuntime(options) returns ConversationRuntime, the single owner of per-session serialization, command dispatch, runner caching, stop/reset behavior, idle eviction, and graceful shutdown.

MethodSemantics
handleEvent(event, bot, context)Serialize by derived session key, then dispatch a command or agent run
runSession(options)Execute one already-serialized unit; intended for controlled host use
handleStop(...) / forceStop(...)Cooperative/user-visible stop versus immediate internal stop
handleNewCommand(...)Abort, reset persisted session state, and discard the cached runner
MethodSemantics
isRunning(sessionKey)Current in-process run state
getRunningSessions()Snapshot for admin/observability surfaces
switchConversationModel(...)Update a cached runner; returns whether one existed
refreshConversationEnvironment(...)Re-resolve a cached runner environment
shutdown(timeoutMs?)Reject new work, stop runners, and wait for in-flight work up to the deadline

ConversationRuntimeOptions supplies workspace/configuration fields, sandbox/resource services, optional portal token stores, optional commands, model registry, proactive platform operations, and platform tool-pack factories. Missing vault and portal stores degrade to disabled implementations; setting a portal URL without its store is an error when used.

Concurrency invariant: calls for one session key are serial; different session keys may run concurrently. A platform tool pack is therefore created per runner, never shared globally, because bindRun mutates its run binding.

Sources: src/commands/manifest.ts, src/commands/types.ts, and src/commands/registry.ts.

COMMAND_MANIFEST is the platform-facing inventory used to derive slash forms and native registration. CommandHandler.tryHandle(context) is the execution contract. Handlers run in order and the first true result consumes the message.

Built-in commands run before extension commands. An unmatched slash-prefixed message is still a normal agent prompt. stop is a platform intake magic word, not a CommandHandler; session is the only accepted bare command.

CommandContext contains normalized actor/conversation identity, response and bot ports, command text, privacy state, and CommandServices. Command handlers must not reach into a platform SDK event.

Sources: src/harness/index.ts, runner.ts, session-store.ts, models.ts, and types.ts.

Owns the model turn loop, message persistence, retries, compaction, budget circuit breakers, extension hooks, and abort behavior. Its externally meaningful operations are prompt/run, subscribe, abort, session reload, model/thinking selection, and disposal.

Owns version 3 append-only session JSONL and tree reconstruction. SessionHeader.version is currently 3 (CURRENT_SESSION_VERSION). Session entries may branch; consumers must use store/context helpers rather than assuming the file is a flat chat transcript.

MikanModels resolves built-in and custom pi-ai models. FileCredentialStore persists provider credentials in the state directory. Vault credentials are a different boundary: they are injected into tool execution, not used to authenticate the host-side model client.

Harness listeners observe model/tool lifecycle events. Budget settings cap tokens, cost, duration, and LLM calls. A budget trip aborts the run and emits budget_exceeded; it is a terminal outcome, not a retry signal.

Source: src/harness/extensions/types.ts. Full authoring examples are in Extension development.

An extension is trusted host code exporting activate(api). It is activated per conversation harness instance and may return a disposer.

SurfaceContract
onRegister ordered hooks for prompt, tool, message, compaction, error, and budget events
registerToolAdd a pi-agent-core tool to this runner
registerCommandAdd deterministic /name handling after built-ins
onDisposeRelease resources on reset, eviction, or shutdown; LIFO order
contextRead-only conversation/workspace/model identity
pathsConversation-private and explicitly shared host-only data directories
secretsRead-only extension secrets by name
schedules / triggerRunPersist or fire autonomous event-file runs
subagent.runFresh isolated run with explicit tools, schema, and budget
notify / react / uploadFileOptional host platform capabilities

Hook errors are logged and skipped. before_agent_start and tool_result rewrites chain; tool_call uses the first non-undefined result. A blocked pre-start hook prevents the model call and session persistence.

Extension schedules and immediate runs do not inherit conversation history. Their task text must be self-contained and must not contain secrets.

Source: src/tools/types.ts.

Core tools use the executor and host services supplied by the runner. EventStore is the canonical CRUD port for event files. Invalid event JSON remains listable with payload: null so an admin can diagnose or delete it.

PlatformToolPackFactory creates a private PlatformToolPack per runner. bindRun selects whether its tools apply to the current platform/conversation. The factory boundary keeps GitHub-specific tools out of the core tool list while preventing cross-conversation mutable binding.

Sources: src/sandbox/types.ts and src/sandbox/index.ts.

The discriminated union currently supports host, container, image, gondolin, firecracker, and cloudflare. Parsing syntax and maturity are documented under Sandbox.

This is the stable execution boundary and the most important sandbox contract:

OperationRequirement
execReturn stdout, stderr, and exit code; honor timeout/abort where supported
readFile / readFileBase64Transport content without adding shell parsing layers
writeFileStage and replace so an aborted write cannot truncate the target
getWorkspacePathMap a host workspace root to the runtime-visible root
getPathContextDeclare host/runtime path semantics and optional reverse mapping
getSandboxConfigReturn the concrete configuration in use

Remote/exec-only implementations share base64-chunked file transport. Tool implementations must call executor file methods instead of building cat, printf, or quoting protocols.

An adapter recognizes one CLI value, optionally validates it, and optionally creates an executor. image deliberately has no direct executor: actor/vault resolution provisions a concrete container first.

Although the type is exported, adapter registration is currently a closed list inside src/sandbox/index.ts; external consumers cannot add an adapter to parseSandboxArg or createExecutor.

9. Vault and execution-resolution interface

Section titled “9. Vault and execution-resolution interface”

Source: src/vault/types.ts; policy is in src/vault/policy.ts.

VaultManager resolves credential env and mount files by canonical actor key, lists and mutates private/shared vaults, and reports whether storage is enabled. Secrets live under the host-only state directory.

Credential flow is:

  1. Runtime supplies platform trust, conversation, user, and sandbox topology.
  2. The execution resolver selects the actor/vault identity.
  3. VaultManager resolves env and mounts.
  4. The concrete executor receives only the credentials intended for that runtime.

Host sandbox execution never receives vault injection. Open-trigger platforms never receive an ambient shared vault. These are security invariants, not convenience defaults.

10. Session, chat, and identity interfaces

Section titled “10. Session, chat, and identity interfaces”

Sources: src/sessions/*, src/store.ts, and src/sandbox/identity.ts.

There are two intentional records:

RecordPurpose
<conversation>/log.jsonlPlatform truth: user/bot messages and attachments
<conversation>/sessions/*.jsonlAgent truth: prompts, model/tool messages, branches, and compaction

Do not merge them. Chat history can rebuild a missing top-level agent session, while agent sessions contain data that never appeared on the platform.

Session keys use conversationId[:suffix]; only src/sessions/session-key.ts owns this grammar. Callers must use deriveSessionKey, conversationIdOf, and threadSuffixOf, never split strings directly.

ChatHistorySync resolves top-level/thread scope, bootstraps new sessions, synchronizes new platform log entries, resets sessions, and coordinates a thread waiting for an active parent run to seal.

Source: src/harness/event-format.ts.

events/*.json is a workspace-level scheduling bus. The canonical union is:

type EventFilePayload =
| { type: "immediate"; conversationId: string; text: string /* common optional fields */ }
| { type: "one-shot"; conversationId: string; text: string; at: string }
| { type: "periodic"; conversationId: string; text: string; schedule: string; timezone: string };

Common optional fields are platform, conversationKind, and userId. channelId is accepted only as a legacy read alias. All writers use buildEventPayload; all readers use parseEventPayload.

The bus is shared and agent-writable by design. Ownership prefixes are cooperative, not an authorization boundary.

PathOwnerVisibility / compatibility
<state-dir>/settings.jsonconfigHost-only global settings
<state-dir>/conversations/<id>/settings.jsonconfig/adminHost-only conversation overrides
<state-dir>/auth.jsonharness credential storeHost-only model-provider credentials
<state-dir>/models.jsonharness model catalogHost-only custom providers/models
<state-dir>/vaults/**vault/loginHost-only secrets and mount files
<state-dir>/{global,conversations}/**/extensionsextension loaderTrusted host code
<workspace>/MEMORY.mdagent/userSandbox-visible durable memory
<workspace>/skills/**/SKILL.mdskillsSandbox-visible prompt instructions
<workspace>/events/*.jsonevent store/watcherShared scheduling wire format
<workspace>/<id>/log.jsonlplatform storeAppend-only platform history
<workspace>/<id>/sessions/*.jsonlsession storeVersioned append-only agent history

The state directory must not be inside the sandbox-visible workspace. Important state files use atomic private writes; append-only logs use append semantics.

Source: src/web/server.ts and portal modules. These routes support mikan’s bundled UI; they are not a general public REST API.

SurfaceRoutesAuthentication model
HealthGET /healthNone; returns { "ok": true }
LoginGET /link, POST /api/link/complete, POST /api/oauth/start, GET /oauth/callbackShort-lived link token plus OAuth state
Session viewerGET /session, GET /session/stream, optional POST /session/messageSession-view token
AdminGET /admin, /admin/api/*Admin token
Agent eventsGET /api/agent-events/streamDeployment-controlled event stream

Admin endpoints cover conversation inventory/state/usage, global and conversation settings, models, workspace files, skills, events, platform configuration, and login/session links. Route payloads are internal UI contracts and may evolve together with the bundled frontend.

gondolin:default uses a detached Node worker on the same host. The worker process and its persisted runtime inventory form an internal lifecycle boundary used for restart recovery; there is no network worker protocol or remote placement contract.

When changing a boundary, verify:

  1. Is it public, host integration, persisted/wire, or internal?
  2. Does the change alter identity, trust, path, concurrency, or lifecycle semantics?
  3. Do producers and consumers share one canonical parser/builder/type?
  4. Does old persisted data still load, or is there an explicit migration?
  5. Are optional capabilities feature-detected?
  6. Can the implementation be moved behind an existing port instead of expanding the core contract?