Skip to content

Core simplification

Evidence-based plan to reduce mikan's public surface and internal coupling without changing behavior.

This audit uses the boundaries in Core interface reference. The goal is not to minimize file count; it is to make the core explainable as a small set of stable contracts and move optional policy behind them.

The smallest coherent mikan core is:

  1. one normalized conversation input and one response port;
  2. one runtime that serializes a session and owns runner lifecycle;
  3. one harness interface for a model turn;
  4. one session store and one platform log store, intentionally separate;
  5. one executor interface for all tool I/O;
  6. one credential-resolution seam;
  7. registries for optional platform, command, tool, extension, and sandbox capabilities.

Web portals, particular chat SDKs, Docker provisioning, Gondolin runtime management, Firecracker, Cloudflare, and GitHub-specific tools are products or plugins around this core.

PriorityFindingEvidenceSimplification
P0The npm boundary is wider than the supported product boundarysrc/index.ts uses export * from "./harness/index.js"; package metadata has no exports mapDeclare explicit root exports and subpaths; stop publishing internal helpers accidentally
P0Platform input has two overlapping canonical shapesConversationEvent and ConversationMessage repeat id/session/kind/user/text/attachments/thread fields and are passed togetherIntroduce one ConversationInput envelope; derive compatibility views during migration
P0MessagingBot has too many reasons to changelifecycle, output, upload/reaction, private diagnostics, event queue, and metadata are one interfaceSplit normalized platform port from optional capabilities; keep one adapter object that composes them
P1Sandbox extensibility is only nominalSandboxAdapter is exported, but a closed array in sandbox/index.ts owns parsing/creationAdd a registry or make adapters internal; move experimental backends out of the default core assembly
P1Runtime construction leaks command implementation detailsConversationRuntimeOptions is an Omit<CommandServices, ...> and reconstructs command services internallyDefine explicit runtime dependencies, then build CommandServices in a command adapter
P1Commands still have two inventoriesThe manifest owns platform registration while the registry owns handlers; adding a command requires bothA CommandDefinition should contain manifest metadata and a handler factory/reference
P1The harness is both a public SDK and an internal engineRoot exports session parsing, credentials, settings, loader, event schema, hooks, and runner details togetherPublish ./extension, ./runtime, and optionally ./harness subpaths with separate stability policies
P2Optional runtime products are compiled into the central CLIPlatform SDKs, portals, six sandbox modes, and Gondolin runtime code are assembled in main.tsMake main.ts a composition root over registries; lazy/optional product modules become replaceable
P2Several capability interfaces use optional methodsMessagingBot and ConversationResponder grow by adding ? methodsUse named capability objects (reactions, uploads, streaming) so absence and requirements are explicit
P2HTTP route contracts are manually dispatched and mostly untypedweb/server.ts and portal modules branch on method/path; payload types are localKeep the UI API internal, but centralize route descriptors and request/response schemas where payloads cross modules

Some apparent duplication protects important semantics:

  • Keep log.jsonl separate from agent session JSONL. They record different truths and support recovery.
  • Keep host paths separate from runtime paths. Collapsing them breaks container/remote execution and can expose host-only data.
  • Keep model-provider credentials separate from sandbox vault credentials.
  • Keep per-session queues. Global serialization would waste concurrency; no serialization would corrupt conversation order.
  • Keep platform trust policy explicit. Inferring credential safety from platform names is unsafe.
  • Keep executor-owned file transport. Replacing it with shell snippets reintroduces quoting, size, and partial-write failures.
  • Keep extension code host-only and sandbox workspace data separate.
interface ConversationInput {
platform: MessagingInfo;
conversation: {
id: string;
kind: ConversationKind;
vaultId?: string;
};
message: {
id: string;
parentId?: string;
sessionKey?: string;
actor: { id: string; name?: string };
text: string;
attachments: readonly Attachment[];
};
respond: ConversationResponder;
}

The runtime derives ConversationEvent and ConversationMessage only for old call sites. Once adapters and tests use the envelope, delete both compatibility shapes.

interface PlatformPort {
readonly info: MessagingInfo;
lifecycle: { start(): Promise<void>; stop(): Promise<void> };
messages: PlatformMessages;
capabilities?: {
reactions?: PlatformReactions;
uploads?: PlatformUploads;
privateMessages?: PlatformPrivateMessages;
};
}

This removes feature detection by arbitrary method name and lets commands/extensions request the exact capability they need.

interface ConversationRuntimeOptions {
workingDir: string;
sandbox: SandboxConfig;
createRunner: RunnerFactory;
commands?: readonly CommandDefinition[];
vault?: VaultResolver;
resources?: SandboxResourceController;
portals?: PortalServices;
platformCapabilities?: PlatformCapabilities;
platformToolPacks?: readonly PlatformToolPackFactory[];
}

The runtime should not inherit its constructor shape from another subsystem’s service bag.

interface SandboxRegistry {
register<T extends SandboxConfig>(adapter: SandboxAdapter<T>): void;
parse(value: string): SandboxConfig;
validate(config: SandboxConfig): Promise<void>;
createExecutor(config: SandboxConfig, context: ExecutorContext): Promise<Executor>;
}

The default CLI registers stable backends. Gondolin, Firecracker, and Cloudflare can be registered by optional product modules without changing tool or runtime code.

  1. Add a package exports map for ., ./extension, and ./runtime.
  2. Replace the root export * with an explicit list.
  3. Add API-extractor-style snapshot tests using emitted declarations or a checked symbol list.
  4. Mark currently exported-but-unsupported symbols deprecated for one release before removal.

This is first because every internal refactor is harder after consumers depend on accidental exports.

  1. Add ConversationInput and an adapter from it to the current runtime call.
  2. Convert one platform adapter and its tests at a time.
  3. Change handleEvent(event, bot, context) to handle(input).
  4. Delete duplicated fields and compatibility adapters after all platforms migrate.

This should remove consistency bugs around ts versus message.id, thread_ts versus threadTs, and event user versus message userId.

  1. Extract message, reaction, upload, and private-message ports from MessagingBot.
  2. Adapt existing bots without changing their SDK code.
  3. Pass narrow capabilities to commands, extensions, events, and admin instead of the whole bot.
  4. Retire ChatAdapter if no independent caller remains.

Phase 4 — simplify runtime construction and commands

Section titled “Phase 4 — simplify runtime construction and commands”
  1. Replace the Omit<CommandServices, ...> inheritance with explicit runtime options.
  2. Make portal services one optional object rather than three token-store fields plus a URL.
  3. Join command metadata and handler registration into CommandDefinition.
  4. Generate platform registration, parsing inventory, and handler order from that definition list.

Phase 5 — separate optional execution products

Section titled “Phase 5 — separate optional execution products”
  1. Introduce SandboxRegistry while preserving the existing default functions as wrappers.
  2. Register host/container/image as the default distribution.
  3. Register Gondolin, Firecracker, and Cloudflare from optional composition modules.

main.ts should parse CLI configuration, instantiate registries/services, start selected products, and coordinate shutdown. Platform SDK initialization and backend-specific policy should remain in their modules. A useful completion criterion is that the composition root reads like configuration, not business logic.

The simplification is complete when:

  • a new platform implements one normalized input adapter plus declared output capabilities;
  • a new sandbox registers one adapter without editing a core switch or array;
  • runtime construction does not mention individual portal token-store types;
  • adding a command changes one definition and one handler implementation, not several platform inventories;
  • the root npm declaration surface contains only documented symbols;
  • persisted session/event/config formats and trust/path invariants remain compatible;
  • existing unit tests, build, lint, and knip pass after each phase.

Keep the first implementation PR behavior-neutral:

  1. add the package exports map with compatibility subpaths;
  2. replace duplicate root type re-exports with explicit exports;
  3. add a public API snapshot test;
  4. introduce ConversationInput plus conversion helpers, but migrate only one adapter;
  5. record deprecations without deleting functionality.

Do not combine this with removing a sandbox backend or changing persisted formats. Those changes have different rollback and operational risks.