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:
- Parse scope / username (application)
- Call NyxID
/users/me (integration, HTTP)
- Call NyxID
/api-keys (integration, side-effecting external resource creation)
- Call NyxID
/proxy/.../user preflight (integration, possibly revoking the key just made)
- Get / create actor (runtime)
- Prime projection scope (projection)
- Dispatch
InitializeSkillRunnerCommand
- Dispatch
TriggerSkillRunnerExecutionCommand
- Synchronously poll projection up to 20 times (
WaitForCreatedAgentAsync, AgentBuilderTool.cs:310-317)
- Save preference (actor command)
- 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.
Symptom
AgentBuilderTool.CreateDailyReportAgentAsync(AgentBuilderTool.cs:178-340, 162 lines) is a god function spanning four layers in one synchronous body:/users/me(integration, HTTP)/api-keys(integration, side-effecting external resource creation)/proxy/.../userpreflight (integration, possibly revoking the key just made)InitializeSkillRunnerCommandTriggerSkillRunnerExecutionCommandWaitForCreatedAgentAsync, AgentBuilderTool.cs:310-317)Architectural violations
WaitForCreatedAgentAsyncis the soft form of this.Proposed direction
Split along the command-skeleton lines:
AgentBuilderTool): produces a singleCreateDailyReportSubscriptionCommandenvelope and dispatches it. Returnsaccepted + commandIdimmediately. Per-template helpers (daily / social-media) stay split per-template; no shared god function.AgentExecutionCredentialGAgent(see #refactor-credential-actor) → wait forCredentialPreflightSucceededEvent(continuation) → persistSubscriptionInitializedEvent→ schedule first runUserAgentCatalogProjectorconsumes committed events; noWaitForCreatedAgentAsync. 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:
EnsureUserAgentCatalogProjectionAsyncpriming andWaitForCreatedAgentAsyncpolling both deleted.Acceptance
AgentBuilderTool.CreateDailyReportAgentAsyncno longer makes NyxID HTTP calls directly.{status:\"accepted\", command_id}synchronously; user-facing confirmation arrives via outbound channel after committed event reaches relay.Dependencies
Related