Skip to content

feat(core): agent-runtime types β€” Phase 1 of architecture unification - #253

Merged
dylanneve1 merged 1 commit into
mainfrom
feat/agent-runtime-types-phase1
May 24, 2026
Merged

feat(core): agent-runtime types β€” Phase 1 of architecture unification#253
dylanneve1 merged 1 commit into
mainfrom
feat/agent-runtime-types-phase1

Conversation

@claudiusthebot

Copy link
Copy Markdown
Collaborator

What

First step of the architecture unification plan
(docs/talon-architecture-unification-plan.md). Adds the canonical
shapes β€” AgentEvent, RunPolicy, ModelRef, ToolDescriptor,
and the split capability interfaces β€” under
src/core/agent-runtime/, plus an adapter that wraps the existing
QueryBackend as a Backend composed object so Phase 3+ work can
begin without churning every concrete backend in this same PR.

No production caller imports any of this yet. Phase 1 contract:
types-first, no behaviour change. The full test suite (2900 cases)
stays green and the only code added under src/ is the new
agent-runtime/ directory.

Why

Talon has accreted five backends under time pressure. Behaviour
drifts per backend β€” event parsing, usage extraction, MCP setup,
abort handling, and logging are all re-implemented in slightly
different shapes. The plan replaces that with single sources of
truth (events, policies, model identity, tool registry, store
abstraction). This PR is the smallest reviewable opener:

  1. Define the new shapes.
  2. Provide an adapter from the legacy interface to the new one.
  3. Add tests for every type-level helper and adapter translation rule.
  4. Touch zero backend implementations.

The second PR will plumb resolveActiveModel() β†’ ModelRef through
/status, /model, chat query, heartbeat, and dream (Phase 2 in
the plan).

What lands

src/core/agent-runtime/
  events.ts           AgentEvent, AgentError, AgentResult, UsageSnapshot
                      + emptyUsage / addUsage / isAgentEventOf /
                        isAgentRunTerminator
  model-ref.ts        BackendId, ModelRef, CacheSupport, ModelSource
                      + BACKEND_IDS literal, isBackendId, sameModelRef,
                        makeBareModelRef
  run-policy.ts       RunKind, RunPolicy + sub-policies (ToolPolicy,
                      DeliveryPolicy, TimeoutPolicy, LoggingPolicy,
                      SessionPolicy, PermissionPolicy)
                      + defaultRunPolicyFor / allowsDelivery /
                        requiresAmbientChat
  tool-descriptor.ts  ToolDescriptor, ToolFilter, applyToolFilter
  capabilities.ts     ChatBackend, BackgroundRunner, ModelCatalog,
                      SessionBackend, ToolRuntime, UsageTelemetry
                      + composed Backend object with explicit
                        capability flags (deriveCapabilities)
  adapter.ts          adaptQueryBackend(legacy, id, label, opts)
                      β€” synthesises minimal AgentEvent streams around
                        query() and runOneShotAgent()
                      β€” translates UnifiedModelInfo β†’ ModelRef
                      β€” proxies session / tools / usage methods
                      β€” optional logSink receives appendLog so callers
                        can keep the legacy markdown stream alive
                        during migration
  index.ts            Barrel

Design notes (lifted from the plan)

  • AgentEvent is structurally typed β€” switch on event.type;
    no instanceof, no class hierarchy.
  • ModelRef.{backend, id} is the identity pair; everything else
    is metadata for UIs and telemetry.
  • RunPolicy lifts the loose contextLabel string into a real
    policy object. defaultRunPolicyFor(kind) returns the right shape
    for chat / heartbeat / dream / trigger / test.
  • BACKEND_IDS is the single literal source for the BackendId
    union. The config.ts zod enums stay duplicated for now and get
    reconciled in a later phase (noted in model-ref.ts).
  • The adapter is deliberately less rich than native Phase 3
    backends will be
    β€” no per-token streaming, no tool-call events.
    It can't lie about events it cannot observe; Phase 3 backends emit
    the full event surface natively.

Testing

  • 50 new cases across two files:

    • agent-runtime-types.test.ts (32) β€” pure helpers across events,
      model-ref, run-policy, tool-descriptor, capabilities.
    • agent-runtime-adapter.test.ts (18) β€” every adapter translation
      rule pinned: chat happy path / empty text / thrown query / abort
      classification; background appendLog β†’ logSink routing and error
      classification; ModelCatalog kinds (exact / ambiguous / missing);
      selectableOnly + query filter; getDefaultModel with and
      without getModelInfo; cacheSupport propagation;
      sessions / tools / usage proxies; capability-flag
      derivation.
  • Tests pass (npm test) β€” 2900 / 2912 (12 pre-existing skips on
    the live-integration tier).

  • Type check passes (npm run typecheck)

  • No new lint warnings (npm run lint β€” 18 pre-existing warnings,
    none in new code).

  • Prettier clean on the new files.

Out of scope

  • No backend rewrites.
  • No dispatcher / heartbeat / dream / /status / /model changes.
  • The config.ts backend zod enums stay duplicated until Phase 2 or
    later. Note in model-ref.ts flags the link.
  • The richer core/errors.ts classifier is not yet wired through the
    adapter β€” Phase 3 native backends will use it directly. The adapter
    ships a small inline classifier that maps the common cases.

Refs docs/talon-architecture-unification-plan.md

Introduces the canonical shapes from the architecture unification plan
without changing runtime behaviour. No production caller imports any of
this yet β€” the adapter exists so Phase 3+ work can begin while the
existing QueryBackend implementations stay untouched.

What lands

src/core/agent-runtime/
  events.ts          AgentEvent / AgentError / AgentResult / UsageSnapshot
                     plus emptyUsage / addUsage / isAgentEventOf /
                     isAgentRunTerminator helpers.
  model-ref.ts       BackendId / ModelRef / CacheSupport / ModelSource
                     plus isBackendId / sameModelRef / makeBareModelRef
                     and the BACKEND_IDS constant (single literal source
                     for the five current backends).
  run-policy.ts      RunKind / RunPolicy + sub-policies (ToolPolicy,
                     DeliveryPolicy, TimeoutPolicy, LoggingPolicy,
                     SessionPolicy, PermissionPolicy) plus
                     defaultRunPolicyFor / allowsDelivery /
                     requiresAmbientChat.
  tool-descriptor.ts ToolDescriptor / ToolFilter / applyToolFilter.
  capabilities.ts    ChatBackend / BackgroundRunner / ModelCatalog /
                     SessionBackend / ToolRuntime / UsageTelemetry plus
                     a composed Backend object with explicit capability
                     flags (deriveCapabilities).
  adapter.ts         adaptQueryBackend(legacy, id, label, opts) β€” wraps
                     the existing QueryBackend as a Backend by
                     synthesising minimal AgentEvent streams around
                     query() and runOneShotAgent(), translating
                     UnifiedModelInfo β†’ ModelRef, and proxying
                     session / tools / usage methods. Optional logSink
                     receives appendLog calls so callers can keep the
                     legacy markdown stream alive during migration.
  index.ts           Barrel.

Tests

  agent-runtime-types.test.ts (32 cases) β€” pure helpers across events,
  model-ref, run-policy, tool-descriptor, capabilities.

  agent-runtime-adapter.test.ts (18 cases) β€” every adapter translation
  rule pinned: chat happy path / empty text / thrown query / abort
  classification; background appendLog β†’ logSink routing and error
  classification; ModelCatalog kinds (exact / ambiguous / missing);
  selectableOnly + query filter; getDefaultModel with and without
  getModelInfo; cacheSupport propagation; sessions / tools / usage
  proxies; capability flag derivation.

50 new cases, 2900 / 2912 in the full suite (12 pre-existing skips on
the live-integration tier). typecheck clean, prettier clean, no new
lint warnings.

Design notes lifted from the plan

  - AgentEvent is structurally typed (switch on event.type); no
    instanceof, no class hierarchy.
  - ModelRef.{backend,id} is the identity pair; everything else is
    metadata for UIs and telemetry.
  - RunPolicy lifts the loose "contextLabel" string into a real policy
    object: defaultRunPolicyFor(kind) returns the right shape for
    chat / heartbeat / dream / trigger / test.
  - BACKEND_IDS is the single literal source for the BackendId union;
    config.ts zod enums remain duplicated for now and get reconciled
    in a later phase (noted in model-ref.ts).
  - Adapter behaviour is deliberately less rich than native Phase 3
    backends will be (no per-token streaming, no tool-call events) so
    the bridge can't lie about events it cannot observe.

Refs docs/talon-architecture-unification-plan.md
@dylanneve1
dylanneve1 enabled auto-merge (squash) May 24, 2026 11:53
@dylanneve1
dylanneve1 merged commit b7b7649 into main May 24, 2026
63 of 65 checks passed
dylanneve1 pushed a commit that referenced this pull request May 24, 2026
Introduces the ModelRef-shaped counterpart to
core/active-model.ts's string-side resolveActiveModelForChat. Phase
2.1 contract: add the function + tests, no caller migration. The
existing 5-step string-shaped chain stays the single chain-of-truth β€”
this module wraps it and enriches the returned id into a ModelRef
carrying the metadata downstream consumers (/status, /model menu,
telemetry) currently re-derive on the spot.

What lands

src/core/agent-runtime/
  resolver.ts        resolveActiveModelRefForChat(chatId, backend,
                     backendId, config) β†’ { ref: ModelRef | null,
                     source: ActiveModelSource } plus the
                     getActiveModelRefForChat() convenience wrapper.
  index.ts           Re-exports the new symbols.

Enrichment strategy (deliberate fall-through)

  1. backend.getModelInfo(id)         β€” preferred, full metadata
  2. backend.resolveModel(id) β€” exact β€” fallback, also full metadata
  3. makeBareModelRef(backend, id)    β€” last resort, identity-only
                                       (with cacheSupport stamped)

cacheSupport propagation
  Each ref carries the backend's cacheMetrics field β€” backends
  report caching uniformly across their catalog today, so this is
  the right place to stamp it.

ActiveModelSource β†’ ModelSource mapping
  override-valid             β†’ "chat"
  override-invalid-fallback  β†’ "fallback"
  backend-canonical          β†’ "backend-default"
  config-backend-defaults    β†’ "config"
  config-legacy-global       β†’ "config"
  none                       β†’ (ref is null)

Invariants
  - ref === null iff the 5-step chain returned model === null OR the
    supplied backendId is not a known BackendId.
  - source is returned verbatim from the underlying chain so callers
    keep using identical toast wording.

Tests

agent-runtime-resolver.test.ts (17 cases):
  - Enrichment via getModelInfo (preferred)
  - Enrichment via resolveModel (fallback)
  - Bare ref fallback when neither catalog method matches
  - cacheSupport propagation (read / readwrite / undefined β†’ "none")
  - ref returned for trusted overrides when no resolveModel exists
  - Source mapping β€” every ActiveModelSource β†’ ModelSource branch
  - Null edge cases (chain exhausts, unknown backendId, null backend)
  - Survives getModelInfo throwing (falls through to resolveModel)
  - Survives resolveModel throwing (falls through to bare ref)
  - getActiveModelRefForChat convenience wrapper

Full suite: 2917 / 2929 (12 pre-existing skips). 17 net new tests
on top of Phase 1's 50. Typecheck clean. Prettier clean. No new
lint warnings.

Out of scope

  - No caller migration. /status, /model, chat query, heartbeat,
    dream all keep calling resolveActiveModelForChat. Phase 2.2+
    converts them one at a time, each in its own PR.
  - No changes to core/active-model.ts itself. The new module wraps,
    doesn't fork.
  - The lossy "config-backend-defaults" + "config-legacy-global" β†’
    "config" collapse is intentional β€” ModelRef.source captures
    provenance (chat / config / backend), the ActiveModelSource tag
    returned alongside keeps the fine-grained reason for toast
    wording.

Stacked on top of feat/agent-runtime-types-phase1 (#253).
Refs docs/talon-architecture-unification-plan.md
dylanneve1 pushed a commit that referenced this pull request May 24, 2026
#254)

* feat(core): agent-runtime types β€” Phase 1 of architecture unification

Introduces the canonical shapes from the architecture unification plan
without changing runtime behaviour. No production caller imports any of
this yet β€” the adapter exists so Phase 3+ work can begin while the
existing QueryBackend implementations stay untouched.

What lands

src/core/agent-runtime/
  events.ts          AgentEvent / AgentError / AgentResult / UsageSnapshot
                     plus emptyUsage / addUsage / isAgentEventOf /
                     isAgentRunTerminator helpers.
  model-ref.ts       BackendId / ModelRef / CacheSupport / ModelSource
                     plus isBackendId / sameModelRef / makeBareModelRef
                     and the BACKEND_IDS constant (single literal source
                     for the five current backends).
  run-policy.ts      RunKind / RunPolicy + sub-policies (ToolPolicy,
                     DeliveryPolicy, TimeoutPolicy, LoggingPolicy,
                     SessionPolicy, PermissionPolicy) plus
                     defaultRunPolicyFor / allowsDelivery /
                     requiresAmbientChat.
  tool-descriptor.ts ToolDescriptor / ToolFilter / applyToolFilter.
  capabilities.ts    ChatBackend / BackgroundRunner / ModelCatalog /
                     SessionBackend / ToolRuntime / UsageTelemetry plus
                     a composed Backend object with explicit capability
                     flags (deriveCapabilities).
  adapter.ts         adaptQueryBackend(legacy, id, label, opts) β€” wraps
                     the existing QueryBackend as a Backend by
                     synthesising minimal AgentEvent streams around
                     query() and runOneShotAgent(), translating
                     UnifiedModelInfo β†’ ModelRef, and proxying
                     session / tools / usage methods. Optional logSink
                     receives appendLog calls so callers can keep the
                     legacy markdown stream alive during migration.
  index.ts           Barrel.

Tests

  agent-runtime-types.test.ts (32 cases) β€” pure helpers across events,
  model-ref, run-policy, tool-descriptor, capabilities.

  agent-runtime-adapter.test.ts (18 cases) β€” every adapter translation
  rule pinned: chat happy path / empty text / thrown query / abort
  classification; background appendLog β†’ logSink routing and error
  classification; ModelCatalog kinds (exact / ambiguous / missing);
  selectableOnly + query filter; getDefaultModel with and without
  getModelInfo; cacheSupport propagation; sessions / tools / usage
  proxies; capability flag derivation.

50 new cases, 2900 / 2912 in the full suite (12 pre-existing skips on
the live-integration tier). typecheck clean, prettier clean, no new
lint warnings.

Design notes lifted from the plan

  - AgentEvent is structurally typed (switch on event.type); no
    instanceof, no class hierarchy.
  - ModelRef.{backend,id} is the identity pair; everything else is
    metadata for UIs and telemetry.
  - RunPolicy lifts the loose "contextLabel" string into a real policy
    object: defaultRunPolicyFor(kind) returns the right shape for
    chat / heartbeat / dream / trigger / test.
  - BACKEND_IDS is the single literal source for the BackendId union;
    config.ts zod enums remain duplicated for now and get reconciled
    in a later phase (noted in model-ref.ts).
  - Adapter behaviour is deliberately less rich than native Phase 3
    backends will be (no per-token streaming, no tool-call events) so
    the bridge can't lie about events it cannot observe.

Refs docs/talon-architecture-unification-plan.md

* feat(core): resolveActiveModelRefForChat β€” Phase 2.1

Introduces the ModelRef-shaped counterpart to
core/active-model.ts's string-side resolveActiveModelForChat. Phase
2.1 contract: add the function + tests, no caller migration. The
existing 5-step string-shaped chain stays the single chain-of-truth β€”
this module wraps it and enriches the returned id into a ModelRef
carrying the metadata downstream consumers (/status, /model menu,
telemetry) currently re-derive on the spot.

What lands

src/core/agent-runtime/
  resolver.ts        resolveActiveModelRefForChat(chatId, backend,
                     backendId, config) β†’ { ref: ModelRef | null,
                     source: ActiveModelSource } plus the
                     getActiveModelRefForChat() convenience wrapper.
  index.ts           Re-exports the new symbols.

Enrichment strategy (deliberate fall-through)

  1. backend.getModelInfo(id)         β€” preferred, full metadata
  2. backend.resolveModel(id) β€” exact β€” fallback, also full metadata
  3. makeBareModelRef(backend, id)    β€” last resort, identity-only
                                       (with cacheSupport stamped)

cacheSupport propagation
  Each ref carries the backend's cacheMetrics field β€” backends
  report caching uniformly across their catalog today, so this is
  the right place to stamp it.

ActiveModelSource β†’ ModelSource mapping
  override-valid             β†’ "chat"
  override-invalid-fallback  β†’ "fallback"
  backend-canonical          β†’ "backend-default"
  config-backend-defaults    β†’ "config"
  config-legacy-global       β†’ "config"
  none                       β†’ (ref is null)

Invariants
  - ref === null iff the 5-step chain returned model === null OR the
    supplied backendId is not a known BackendId.
  - source is returned verbatim from the underlying chain so callers
    keep using identical toast wording.

Tests

agent-runtime-resolver.test.ts (17 cases):
  - Enrichment via getModelInfo (preferred)
  - Enrichment via resolveModel (fallback)
  - Bare ref fallback when neither catalog method matches
  - cacheSupport propagation (read / readwrite / undefined β†’ "none")
  - ref returned for trusted overrides when no resolveModel exists
  - Source mapping β€” every ActiveModelSource β†’ ModelSource branch
  - Null edge cases (chain exhausts, unknown backendId, null backend)
  - Survives getModelInfo throwing (falls through to resolveModel)
  - Survives resolveModel throwing (falls through to bare ref)
  - getActiveModelRefForChat convenience wrapper

Full suite: 2917 / 2929 (12 pre-existing skips). 17 net new tests
on top of Phase 1's 50. Typecheck clean. Prettier clean. No new
lint warnings.

Out of scope

  - No caller migration. /status, /model, chat query, heartbeat,
    dream all keep calling resolveActiveModelForChat. Phase 2.2+
    converts them one at a time, each in its own PR.
  - No changes to core/active-model.ts itself. The new module wraps,
    doesn't fork.
  - The lossy "config-backend-defaults" + "config-legacy-global" β†’
    "config" collapse is intentional β€” ModelRef.source captures
    provenance (chat / config / backend), the ActiveModelSource tag
    returned alongside keeps the fine-grained reason for toast
    wording.

Stacked on top of feat/agent-runtime-types-phase1 (#253).
Refs docs/talon-architecture-unification-plan.md

---------

Co-authored-by: claudiusthebot <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants