akm docs

Functional Contract Patterns

Quick reference for the repeated design patterns used in akm's refactor away from type-centric behavior.


1. Core Rule

Model behavior around core processes, not around one large asset type object.

Use:

Do not use:


2. Repeated Pattern

For each process:

  1. define a small context object
  2. define a narrow contributor interface
  3. register ordered contributors
  4. let each contributor decide appliesTo(...)
  5. keep one central orchestrator for stage order

This is the standard pattern to repeat across the codebase.

Contributor invariants:

  1. registration is static and in-process only
  2. ordering is deterministic
  3. composition semantics are declared once per seam
  4. execution is sequential by default
  5. contributors do not dispatch into other registries directly
  6. if a seam needs more machinery than this, it is probably too abstract

3. Pattern Catalog

3.1 Fixed-Stage Pipeline

Use when a process already has a clear top-down flow.

Examples:

Shape:

for (const stage of stages) {
  for (const contributor of contributorsFor(stage)) {
    if (!contributor.appliesTo(ctx)) continue;
    contributor.run(ctx);
  }
}

Why:


3.2 Ordered Contributor Registry

Use when one stage needs several isolated policies.

Examples:

Shape:

interface Contributor<TContext> {
  name: string;
  order?: number;
  appliesTo?(ctx: TContext): boolean;
}

Why:


3.3 Structural Contract

Use for stable physical concerns such as refs and paths.

Example:

Why:

Rule:


3.4 Classification As Facts

Classification should produce facts, not choose downstream behavior too early.

Good output:

Avoid:


3.5 Process-Local Validation

Validation belongs to the process that needs it.

Examples:

Avoid one shared validator that tries to know every process.


3.6 One Pipeline, Many Signals

Search must remain one pipeline.

Use contributors for:

Do not create separate per-type scoring pipelines.


3.7 Adapters Before Rewrites

First move existing behavior behind the new seam.

Only after parity is proven should behavior be reorganized.

Why:


3.8 Refactor-Only Safety Rule

When the work is explicitly architectural cleanup:

This keeps architectural cleanup separate from feature or bug-fix work.


3.8a Non-goals

This pattern guide is not permission to:

  1. build a framework
  2. add complexity for its own sake
  3. create registries everywhere by default
  4. replace simple code with abstract dispatch when no real hotspot exists
  5. introduce dynamic plugin systems or runtime discovery
  6. change runtime behavior under the banner of refactoring

The purpose of these patterns is to remove concrete duplication and switchboard logic while keeping the system simpler to reason about than it is today.


3.9 Execution Pipeline Contract

Every execution crosses the same plain functions:

  1. resolveExecution() selects the engine (nearest layer, defaults.engine, then the opencode-sdk fallback), merges the engine's defaults with the caller's layers (nearest wins), expands a models.json alias once, authorizes tools against execution.allowedTools, and returns the ResolvedExecutionRequestV1, the engine's RunnerSpec with the request applied, and per-field provenance. Credentials stay symbolic.
  2. buildExecution() hands the request to the harness's own builder (derived from HARNESS_REGISTRY), or builds chat messages for a direct LLM. A field the transport cannot carry becomes a secret-free notice; a tool policy it cannot enforce, or a denied tool selection, is a pre-dispatch error.
  3. buildExecutionFromWire() does the same from a journaled { request, runner } without reading config, aliases, environment variables, or credentials — workflow resume.
  4. runExecution() reads credentials for the call, runs the agent CLI, the OpenCode SDK, or the chat transport (an exhaustive switch over llm | agent | sdk), and redacts every secret the child could have seen.

A prompt-free interactive native-agent launch (akm agent) resolves its engine directly; it carries no request to build.

Diagnostic provenance is field metadata (field, layer, kind, via), not resolved values or content. Lowering notices are secret-free records with a fixed public vocabulary; arbitrary authored inference keys are represented by wildcard field names rather than copied into diagnostics.


3.10 Session Log Harness Contract

Use one narrow raw-event ingestion seam for harness logs and session histories.

Rule:

  1. harness adapters discover files and parse raw events
  2. shared AKM logic performs normalization, fingerprinting, aggregation, and de-duplication
  3. new harnesses should not require edits to shared unions or duplicated aggregation logic

4. Implemented Contracts

[0.9.0 change, ruled Q-06/Q-16] This section previously listed twelve "recommended" contracts; seven had no implementation anywhere in src/ (PathResolver, MatchContributor, MetadataContributor, LintContributor, ImproveContributor, IndexPostProcessor, AgentRunner) and were aspirational, not a contract any code followed. Trimmed to the contributor contracts below. SearchHitEnricher and ActionContributor were later folded into direct calls (src/indexer/search/search-hit-enrichers.ts, buildLocalAction in src/indexer/search/db-search.ts) — each had one implementation. The engine-owned AgentRequestLowerer is the separate structural boundary documented in §3.9, not a revival of the removed generic AgentRunner. See the drift register if a removed entry needs reviving; add it back only alongside a real implementation.

4.1 RankingContributor

For search score adjustments and explanations.

4.2 ProposalValidator

For proposal acceptance checks.

4.3 SessionLogHarness

For raw session-log or history ingestion from external harnesses.


5. What Stays Centralized

If these become pluggable, the system becomes harder to reason about.


6. What Gets Delegated

If these stay centralized, switchboards keep growing.


7. Smells To Avoid

  1. One large interface with many optional methods
  2. Registries keyed only by type
  3. Matching logic that also chooses presentation names
  4. Renderers that also own indexing and search policy
  5. Ad hoc per-command path lookup logic
  6. Open-ended capability graphs with unclear precedence
  7. Hidden special cases spread across callers instead of isolated behind one seam
  8. Architectural cleanup that changes behavior or rewrites tests to fit the refactor

8. Review Checklist

When adding a new seam, ask:

  1. Is this a stable process boundary?
  2. Can the contributor be tested in isolation?
  3. Does orchestration remain readable top-down?
  4. Is ordering explicit?
  5. Is type only an input, not the behavior object?
  6. Can existing behavior be adapted before rewriting it?
  7. Is this a refactor-only change with behavior preserved?
  8. Does onboarding a new external harness require only one narrow adapter?

If the answer is no to several of these, the abstraction is probably too large.