Skip to content

refactor(daily-builder): decompose AgentBuilderTool god function into command + saga; remove query-time projection polling #446

@eanzhao

Description

@eanzhao

Architectural follow-up surfaced in docs/audit-scorecard/2026-04-27-daily-pipeline-architecture-review.md §B3 + §B7.

Symptom

AgentBuilderTool.CreateDailyReportAgentAsync (AgentBuilderTool.cs:178-340, 162 lines) is a god function spanning four layers in one synchronous body:

  1. Parse scope / username (application)
  2. Call NyxID /users/me (integration, HTTP)
  3. Call NyxID /api-keys (integration, side-effecting external resource creation)
  4. Call NyxID /proxy/.../user preflight (integration, possibly revoking the key just made)
  5. Get / create actor (runtime)
  6. Prime projection scope (projection)
  7. Dispatch InitializeSkillRunnerCommand
  8. Dispatch TriggerSkillRunnerExecutionCommand
  9. Synchronously poll projection up to 20 times (WaitForCreatedAgentAsync, AgentBuilderTool.cs:310-317)
  10. Save preference (actor command)
  11. Format reply JSON

Architectural violations

  • CLAUDE.md "命令骨架内聚: Normalize → Resolve Target → Build Context → Build Envelope → Dispatch → Receipt → Observe" — currently bundles all phases plus side effects.
  • CLAUDE.md "ACK 诚实: 同步返回只承诺已达到阶段 (默认 accepted + stable command id)" — current return value mixes "created" and "accepted (projection not confirmed)" inside the synchronous webhook window.
  • CLAUDE.md "禁止 query-time replay/priming: ApplicationService 不得在 query 方法内同步补投影" — WaitForCreatedAgentAsync is the soft form of this.
  • CLAUDE.md "中间层维护事实状态" — orchestration state lives in this tool.

Proposed direction

Split along the command-skeleton lines:

  • Tool layer (AgentBuilderTool): produces a single CreateDailyReportSubscriptionCommand envelope and dispatches it. Returns accepted + commandId immediately. Per-template helpers (daily / social-media) stay split per-template; no shared god function.
  • Saga layer (inside the business actor — see #refactor-split-skill-runner for the actor identity): the actor itself sequences:
    • resolve owner identity → request credential issuance from AgentExecutionCredentialGAgent (see #refactor-credential-actor) → wait for CredentialPreflightSucceededEvent (continuation) → persist SubscriptionInitializedEvent → schedule first run
  • Projection / readmodel: UserAgentCatalogProjector consumes committed events; no WaitForCreatedAgentAsync. Caller receives the user-facing "agent registered" line via outbound channel (Lark) when subscription's committed event reaches the relay, not via tool's synchronous return.

Net effect:

  • 162-line method becomes ~30 lines (command construction + dispatch).
  • All the side-effecting integration steps move into actor sagas where they're individually persisted and retryable.
  • EnsureUserAgentCatalogProjectionAsync priming and WaitForCreatedAgentAsync polling both deleted.

Acceptance

Dependencies

  • Best landed alongside or after #refactor-credential-actor and #refactor-split-skill-runner — those define the actors the saga lives in.

Related

  • This is the structural fix behind "why does AgentBuilderTool have so many failure modes" — most of them are this function's failure modes.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions