Effect Abstraction Audit — Reactive Agents
SkillMediaUse when analyzing the Reactive Agents codebase for architectural improvements, abstraction opportunities, composability gaps, or Effect-TS engineering quality — before proposing refactors, during design reviews, or when codebase complexity is growing.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Effect Abstraction Audit — Reactive Agents skill
What this skill tells your AI
The instructions your AI receives, as published by tylerjrbuell/reactive-agents-ts in .agents/skills/effect-abstraction-audit/SKILL.md and read by ahel’s review.
Targeted architectural analysis for a TypeScript + Effect + Bun agentic framework. Goal: identify high-value abstraction opportunities that reduce accidental complexity, improve composability, and strengthen type guarantees — without hiding the Effect model.
Guiding Principle: Prefer making Effects more explicit and composable over hiding them behind abstractions. If an abstraction reduces visibility into
Effect<A, E, R>, it is likely a regression.
When to Use
- Before proposing a refactor — validate the problem is real, not hypothetical
- When a module is growing past ~300 LOC
- When similar
pipe(...)chains appear 3+ times across different files - When
throw,as any, or untypedunknownappears in domain logic - When a new agent workflow is being designed
Do NOT use for: one-off fixes, simple feature additions, or performance-sensitive hot paths in Bun-optimized code.
Wiki Integration
Before launching an audit, query the wiki for prior abstraction work to avoid duplicating effort. See [[wiki/Development/Wiki-Workflow|Wiki-Workflow.md]] for the canonical pattern.
claude-obsidian:wiki-query "<subsystem> abstraction effect-ts"
claude-obsidian:wiki-query "service layer composition <subsystem>"
This surfaces:
- Prior decisions in
wiki/Decisions/that constrain abstractions - Past audits in
wiki/Research/Audit-Reports-*/covering the same area - Architectural debt items in
wiki/Issues/Running Issues Log.md - Mechanism validations affecting abstraction choices in
wiki/Experiments/
After the audit, persist findings:
- Significant abstraction opportunity identified →
claude-obsidian:savetowiki/Research/Audit-Reports-YYYY-MM-DD/effect-abstraction-<scope>.md - New architectural debt item → Edit
wiki/Issues/Running Issues Log.md - Decision to defer/reject →
claude-obsidian:savetowiki/Decisions/
Analysis Lens — 7 Signals
Scan for these patterns in order of ROI:
A. Repeated Effect Pipelines
Similar pipe(Effect.flatMap, Effect.map, ...) chains across files. Repeated retry/timeout/logging patterns.
→ Candidate: Composable domain-specific combinators
B. Ad Hoc Service Access
Direct imports instead of Context.Tag usage. Hidden dependencies inside functions.
→ Candidate: Explicit service interfaces + Layer-based injection
C. Inconsistent Error Modeling
Mix of throw, Effect.fail, untyped unknown. Loss of domain error semantics.
→ Candidate: Unified domain error algebra (tagged unions via Data.TaggedError)
D. Agent Workflow Duplication
Repeated patterns: tool selection, validation, retry loops, state transitions across strategies.
→ Candidate: Composable Phase[] or Guard[] additions to the kernel pipeline
E. Conditional Explosion
Large if/else or switch blocks for tool handling, decision logic, provider routing.
→ Candidate: Strategy pattern via tagged services or MetaToolHandler registry entries
F. Layer Fragmentation
Layers defined inconsistently or too locally. No clear composition root per package.
→ Candidate: Centralized createXxxLayer() factory per package
G. Side-Effect Leakage
Logging, IO, or network calls mixed into business logic outside Effect.tryPromise / Effect.sync.
→ Candidate: Effect encapsulation boundary at module edge
Evaluation Filter (Strict)
For each candidate, answer all three:
- Concrete issue today? (duplication / type unsafety / hidden deps / workflow brittleness)
- Does it reduce Effect complexity, cognitive load in pipelines, or risk of runtime failure?
- Does it align with Effect principles? Explicit
R, typedE, referential transparency?
Reject if:
- Hides the Effect model behind opaque helpers
- Reduces type visibility (narrows
Etoneverwithout justification) - Introduces "magic" initialization or implicit wiring
Preferred Abstraction Forms
1. Domain Effect Combinator
// Before: repeated across 4 files
pipe(effect, Effect.retry(Schedule.exponential("100 millis")), Effect.withSpan("tool-exec"))
// After
const withToolExecution = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(Effect.retry(Schedule.exponential("100 millis")), Effect.withSpan("tool-exec"))
2. Tagged Service Interface
class ToolRouter extends Context.Tag("ToolRouter")<
ToolRouter,
{ route: (call: ToolCall) => Effect.Effect<ToolOutput, ToolError> }
>() {}
3. New Kernel Phase
// phases/validate.ts — answers: "is this tool call safe AND well-formed?"
export const validate: Phase = (state, ctx) =>
Effect.gen(function* () {
// ... validation logic
return state
})
// Compose: makeKernel({ phases: [...defaultPhases, validate] })
4. MetaToolHandler Registry Entry (act.ts)
// For inline meta-tools — one-line addition to metaToolRegistry
metaToolRegistry.set("checkpoint", handleCheckpoint)
5. Typed Error Channel Consolidation
type KernelError =
| { _tag: "ThinkFailed"; cause: LLMError }
| { _tag: "GuardRejected"; tool: string; reason: string }
| { _tag: "ActFailed"; cause: ToolError }
Project-Specific Context
Known Architecture Debt (audit these first)
Re-verify counts and wiring before each audit (wc -l, rg); the bullets below were last aligned with the tree on 2026-08-06.
-
KernelState.meta— typed but residual casts remain —KernelMetainterface now exists and most sites access typed fields directly. Residualas Record<string, unknown>casts inarbitrator.tsanditerate-pass.tswere removed in the 2026-08-06 sweep (HS-206). Remaining: someas unknown ascasts persist for import-cycle avoidance (e.g.budgetLimits). Medium ROI to resolve via re-export or shared types package. -
— RESOLVED.buildDynamicContextis dead in the live kernel pathbuildDynamicContextwas already removed prior to this audit refresh.buildStaticContextremains the sole active path. -
context-engine.tssize — On the order of ~500 LOC (not ~690). It holds scoring, environment/rules/tool-reference builders, static context builder, and helpers. Maintenance concern reduced after dynamic path removal. -
Provider adapter hooks — 4-hook system, all wired —
ProviderAdapterinpackages/llm-provider/src/adapter.tsnow has 4 guidance hooks +parseToolCalls(taskFraming/toolGuidance/systemPromptPatchwere removed in v0.14), consumed in the kernel as follows (confirm withrgif paths move):continuationHint,qualityCheck→packages/reasoning/src/kernel/capabilities/reason/think-guards.tserrorRecovery→packages/reasoning/src/kernel/capabilities/act/act.tssynthesisPrompt→packages/reasoning/src/kernel/capabilities/act/conversation-assembly.tsDo not file issues for “unwired hooks” without checking these files first.
-
Adaptive meta-strategy defaults off — Routing exists (
packages/reasoning/src/strategies/adaptive.ts, selected whenconfig.adaptive.enabledinpackages/reasoning/src/services/reasoning-service.ts).defaultReasoningConfigsetsadaptive.enabled: falseinpackages/reasoning/src/types/config.ts. That is a product/default choice, not absent multi-step routing code. -
Duplicated output-quality gate— RESOLVED.enforceOutputQualityGateunified infinalize.ts. Bothplan-execute.tsandreflexion.tsnow call the shared version. -
— RESOLVED.ContextProfilevs runtimemaxTokensContextProfile.maxTokensis now declared (zeroas anycasts for this field). -
Layer<any, any>on public builder API (HS-208) — PARTIALLY RESOLVED 2026-09-03. 2 of 5 sites tightened:packages/reasoning/src/services/reasoning-service.ts:181—any→Layer.Layer<LLMService, never>. Note:Layer'sROutis contravariant, so the loosest type bothllmLayeralone and the ToolService-merged layer satisfy is the narrowerLLMService, not theLLMService | ToolServiceunion (that union was tried first and rejected by the compiler — TS2322).packages/runtime/src/builder/withers/_state.ts:151—any,any,any→Layer.Layer<never, unknown, unknown>, now matching the two other declarations of the same conceptual field (builder.ts:384,runtime-construction.ts:152).- Both packages' full test suites green after the change (reasoning 2806/2806, runtime 4792/4792 combined with llm-provider).
Still open, deliberately left alone:
packages/runtime/src/runtime-types.ts:326(RuntimeOptions.extraLayers) and the widening cast atbuilder/build-effect/runtime-construction.ts:404-411— the inline comment there explicitly scopes that cast to bridging_state.ts's (now-fixed) narrow type into this still-anypublic option surface, and calls fixingruntime-types.tsitself "out of scope" at that call site.agent-instantiation.ts:120'sLayer.Layer<any, never, never>cast is heavily and correctly documented (collapses a 15+-conditional-optional-service union deliberately) — leave it, it already replaced 6 worse casts.
-
— RESOLVED 2026-09-03.FallbackChaindead codepackages/llm-provider/src/fallback-chain.ts(plain OOP class, rawthrow, no Ref/Tag/Layer) had zero live callers; its exact feature set (error-threshold provider switching, per-model chain) was already superseded by the Effect-nativecascadeWithTransitionsinpackages/runtime/src/llm-fallback-cascade.ts(see that file's own comment: "P0-3: those knobs were removed because they were never wired"). Deleted source + test +index.tsexport; llm-provider typecheck/build/tests (446/446) green after removal. -
No— INVESTIGATED AND RETRACTED 2026-09-03. Original framing ("zero tracing infra, 3 competing systems") was wrong on both counts.Effect.withSpan/Effect.fntracing anywherepackages/observability/src/tracing/tracer.ts'smakeTracer/obs.withSpanIS live and wired by default: called frompackages/runtime/src/engine/pipeline.ts:262,267(phase spans) andpackages/runtime/src/execution-engine.ts:1709(task execution span), auto-provided viacreateObservabilityLayerinruntime.ts:824-842wheneverenableObservability: true(the default).packages/observe'sOpenInferenceTracerLayeris a deliberately separate, opt-in layer (flows through theextraLayersseam, see item 8) emitting OpenInference semantic-convention attributes for LLM-observability platforms (Arize Phoenix etc.) — different consumer, different export target, not a competitor; it's also mid-flight WS-4 Phase 3 work as of today (git log shows a same-day TDD red-phase commit), not legacy debt. One real, much smaller item survives:tracer.ts'smakeTracerhand-rolls parent/child span context via a manualRef<{traceId,spanId,parentSpanId}>instead of Effect's built-inTracerservice +FiberRef-based automatic propagation. Swapping the internal plumbing (keep the sameobs.withSpanpublic API) is a legitimate cleanup, but it's actively used and covered by 6 test files — Medium risk for Low-Medium reward, lower priority than anything else in this list. Not attempted this session. -
console.log/console.warninside domain services — PARTIALLY RESOLVED 2026-09-03. Fixed (mechanical, already insideEffect.gen,console.log→yield* Effect.logDebug/Effect.logInfo):packages/gateway/src/services/scheduler-service.ts:148,157;apps/cortex/server/services/mcp-discovery.ts:39(same class — aconsole.logafter ayield*inside a hydratedEffect.gen; the two OTHERconsole.*calls in that same file, line 12 beforeEffect.genstarts and line 43 inside a post-Effect.runPromise.catch(), are correctly left alone — they're outside any Effect context). Verified each: typecheck clean, build green, package test suites pass (gateway 123/123, cortex-server 347/347). IMPORTANT CORRECTION (2026-09-03):skill-registry.ts/skill-resolver.ts'sconsole.warnsites, andpackages/memory/src/database.ts:312, are NOT stray debt — they are explicitly governed, deliberate "Category-A" sync-fallback sites, pinned by an anti-regression AST-walker test atpackages/observability/tests/console-ceiling.test.ts(ceiling: 9 activeconsole.warnsites across runtime/reasoning/reactive-intelligence/memorysrc, 0console.error, each site individually justified in that test's own doc comment, which cross-references the Category-A/B doc-block atpackages/core/src/errors/index.ts). Category-A = legitimate: sync code with no hydrated Effect runtime to thread through (builder/setup/skill-file-load paths). Category-B = the real smell:console.*inside an already-hydrated Effect context, which should beEffect.log*. Before flagging ANYconsole.warn/console.errorsite inruntime/reasoning/reactive-intelligence/memorysrc as debt, checkconsole-ceiling.test.ts's doc comment first — it already classifies every currently-known site. Onlyconsole.log(not covered by the ceiling test) and genuinely new/unclassifiedconsole.warn/console.errorsites are worth auditing fresh. -
— RESOLVED 2026-09-03.completeStructured()retry loop duplicated across all 5 LLM providersanthropic.ts,gemini.ts,litellm.ts,openai.ts,local.tseach independently hand-rolled the identical skeleton (attempt loop, repair-prompt injection using the previous attempt's error,JSON.parse+Schema.decodeUnknownEither,parseAttemptsaccumulation, finalLLMParseError) — only the actual API call + prompt wording differed. Extracted topackages/llm-provider/src/structured-parse-retry.ts'srunStructuredParseWithRetry(); each provider now supplies only arunAttempt({attempt, lastError}) => Effect<string, LLMErrors>closure with its provider-specific request-building. Note: this is NOT aSchedulecandidate —Effect.retry/Schedulere-run the same effect on failure, but this loop feeds the previous error into the next request's messages, so the sharedforloop insideEffect.genis the correct idiom;Schedulewas considered and rejected. New unit tests intests/structured-parse-retry.test.ts(5 cases: first-attempt success, retry-with-error-threading, schema-decode-failure retry, exhaustion →LLMParseErrorwith all attempts, non-parse-error passthrough) — no prior test coverage existed for this loop's internals at the provider level. Verified: typecheck clean on first pass all 5 files, full monorepo build green (37/37), llm-provider suite 451/451 (446 baseline + 5 new). -
Layer.scopedcleanup gap — INVESTIGATED 2026-09-03, NO GENUINE LEAK FOUND. Checkedpackages/tools/src/mcp/mcp-client.ts(Docker container lifecycle) andpackages/runtime/src/agent/gateway-runner.ts(setInterval). Both manage their resource entirely outside theLayersystem (plain imperative modules, zeroLayer/acquireRelease/addFinalizerusage) but both have real, working cleanup:mcp-client.tsviacleanupConnectionEntry()+process.on("exit"/"SIGINT"/"SIGTERM")handlers (deliberately hardened per HS-12 — "library code must not unilaterally call process.exit");gateway-runner.ts's timer viagetTimer()cleared ingateway-driver.ts:230'sbuildGatewayHandle().stop()chain.packages/gateway/src/services/has zero timer usage at all (cron is computed on-demand, not self-scheduled) — nothing there. Recommendation: leave both alone. Converting either toLayer.scopedwould be a real idiom-consistency win but touches process-signal semantics specifically hardened against a past bug, for no leak-fixing benefit (nothing currently leaks) — Medium-High risk for Medium reward, lower priority than every other item in this list. Do not re-flag this as "missing Layer.scoped = bug" in future passes without checking the actual lifecycle first (naive grep forsetInterval/docker-spawn withoutLayer.scopednearby produces false positives here). -
— RESOLVED 2026-09-03.apps/cortex/server— stale(result as any).debriefworkaround, duplicated 2xrunner-service.ts:410,426andgateway-process-manager.ts:380,385both castAgentResulttoanyto reach.debrief, with a comment claiming "the framework's DebriefCompleted event is not yet wired." The comment describes real (and still-true) behavior — the execution engine itself doesn't publish this event, Cortex synthesizes it — but theas anycasts were stale:AgentResult.debrief?: AgentDebrief(packages/runtime/src/builder/types.ts:1076) has been properly typed for a while, andAgentDebrief's shape is structurally compatible with theDebriefPayloadtheDebriefCompletedevent expects (packages/core/src/types/cortex-events.ts:73-21) — no real type mismatch existed. Removed bothas anysites at each call site (4 total); typecheck clean on first try (properly scoped toapps/cortex— a naive root-tsconfigtscrun pulls in unrelatedscripts//wiki/noise, scope with-p apps/cortexorcdfirst). Verified: build green, cortex-server suite 347/347. Scan for the same pattern in other Cortex call sites if any exist (grep -rn "as any" apps/cortex/serverfor anything reaching into a framework result type) — this pass only checked the 2 sites the original fork flagged, did not do an exhaustiveapps/cortexas anysweep. -
apps/cortex/server/api/agents.ts:65,106,api/runs.ts:100—body as anyon Elysia route handlers — OUT OF SCOPE for this skill. Real external-input-boundary type gap (const b = body as anyon POST/PATCH bodies), but these are Elysia HTTP routes, not Effect code — the idiomatic fix is Elysia's ownt.Object()route-level schema, notSchema.decodeUnknown. Also a live API input-validation behavior change (could start rejecting previously-accepted malformed bodies), not a pure refactor. Not attempted — flag for a general TS/API-hygiene pass, not an Effect-TS audit. Same reasoning applies toapps/cortex/server/db/queries.ts:509(singleas anyon abetter-sqlite3dynamic UPDATE, Low ROI, not Effect-related) andtool-playground-invoke.ts:114(isolated, low-value).
Keeping this skill accurate
After large kernel or adapter changes, refresh the Known Architecture Debt section and the Quick ROI table so audits do not chase fixed problems.
Kernel Extension Points (prefer these over new files)
- New phase →
packages/reasoning/src/kernel/capabilities/<cap>/<name>.ts, insert viamakeKernel({ phases: [...] }) - New guard → add
Guardfn tokernel/capabilities/act/guard.ts, add todefaultGuards[] - New meta-tool → one entry in
metaToolRegistryinkernel/capabilities/act/act.ts
Bun Constraints
- Fast startup → avoid over-layering at runtime initialization boundaries
bun:sqliteis synchronous → alwaysEffect.sync(() => db.query(...)), neverEffect.tryPromise- Native
fetch/ file I/O → wrap inEffect.tryPromisewith typedcatch - ESM + bundling → avoid abstractions that break tree-shaking (no barrel re-exports of large modules)
Anti-Abstraction Signals
Call out where abstraction should NOT be added:
| Pattern | Reason to Leave Alone |
|---|---|
Single-use Effect.gen blocks | Inline is clearer than a named combinator |
think.ts streaming loop | Hot path; abstraction adds call stack overhead |
Provider-specific formatting in *-adapter.ts | Each adapter is intentionally isolated |
kernel-state.ts core shape | Avoid opaque runtime wrappers around KernelState; extending declared types (e.g. structured meta) is good when it improves safety |
Test helpers that call Effect.runPromise | Localized; not worth a shared util |
Output Format
Structure findings as:
1. Executive Summary
One paragraph: what the most significant architectural gap is and why it matters now.
2. High-ROI Abstractions (Detailed)
For each:
- Signal (which of A–G)
- Current Pattern (Before) — exact file path + line range
- Proposed Abstraction (After) — typed code snippet
- Why It Works — Effect composability, type safety, testability
- Impact — duplication reduction, coverage improvement, refactor risk (low/medium/high)
3. Medium / Low ROI
Name + one-sentence rationale. No full treatment needed.
4. Anti-Abstraction Findings
Patterns that look like candidates but should stay inline.
5. Incremental Refactoring Plan
Ordered steps, each independently shippable. Each step must:
- Leave the build green (
bun run buildpasses) - Leave tests green (
bun testpasses) - Not require coordinated changes across >3 packages simultaneously
Quick ROI Reference
| Signal | Typical ROI | Refactor Risk |
|---|---|---|
| Repeated Effect pipelines (3+ sites) | High | Low |
Missing Context.Tag for injected deps | High | Medium |
| Dead code / unused exports (e.g. unused context builders) | High | Low |
Untyped KernelState.meta forcing as any | High | Medium |
| Stale skill/docs claims vs actual wiring | Low | Low |
| Error channel consolidation | Medium | Low |
| Layer fragmentation | Medium | Medium |
| New phase extraction | Medium | Low |
| Conditional explosion in strategies | Low–Medium | High |
Signals
- GitHub stars
- 27
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
effect-abstraction-audit- Source
- github.com/tylerjrbuell/reactive-agents-ts