akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
341 lines (226 loc) • 8.9 kB
Markdown
# 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:
- `type` as data
- small contracts as extension seams
- fixed-stage pipelines as orchestrators
Do not use:
- one mega interface with many optional methods
- open-ended plugin graphs
- process logic hidden behind global type switches
## 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:
- search
- improve
- indexing
Shape:
```ts
for (const stage of stages) {
for (const contributor of contributorsFor(stage)) {
if (!contributor.appliesTo(ctx)) continue;
contributor.run(ctx);
}
}
```
Why:
- preserves readability
- makes order explicit
- prevents plugin soup
## 3.2 Ordered Contributor Registry
Use when one stage needs several isolated policies.
Examples:
- ranking signals
- proposal validators
Shape:
```ts
interface Contributor<TContext> {
name: string;
order?: number;
appliesTo?(ctx: TContext): boolean;
}
```
Why:
- testable in isolation
- easy to add behavior without editing the orchestrator
## 3.3 Structural Contract
Use for stable physical concerns such as refs and paths.
Example:
- `PathResolver`
Why:
- storage layout is real and durable
- should not be mixed with ranking, rendering, or validation
Rule:
- keep structural contracts small and boring
## 3.4 Classification As Facts
Classification should produce facts, not choose downstream behavior too early.
Good output:
- `type`
- `specificity`
- annotations or reasons
Avoid:
- coupling classification directly to renderer names or search policy
## 3.5 Process-Local Validation
Validation belongs to the process that needs it.
Examples:
- proposal validation
- lint validation
- improve preflight validation
Avoid one shared validator that tries to know every process.
## 3.6 One Pipeline, Many Signals
Search must remain one pipeline.
Use contributors for:
- exact-match boosts
- type preference boosts
- belief-state boosts
- graph boosts
- utility boosts
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:
- lower risk
- easier regression testing
- architectural progress without feature churn
## 3.8 Refactor-Only Safety Rule
When the work is explicitly architectural cleanup:
- no functionality changes
- all tests must pass with the same validation expectations
- the only allowed test edits are import-path or symbol-import updates caused by file moves
- do not rewrite assertions, fixtures, or expected outputs to accommodate the refactor
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](../specs/0.9.0-docs-code-drift-register.md#q-16--functional-contract-patternsmd-4-aspiration-or-contract)
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
- source/provider model
- CLI command contracts
- DB schema and persistence boundaries
- write-target resolution
- search stage order
- final score normalization and clamping
- proposal storage
If these become pluggable, the system becomes harder to reason about.
## 6. What Gets Delegated
- heuristics
- enrichment
- validation rules
- process-specific side-effects
- applicability logic
- search explanations
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.