# p0148: route skill rounds through ISkillRoundToolPolicy

- [Architecture] Tool-set acquisition for the four non-Verify round paths moved behind `ISkillRoundToolPolicy` — a per-call decision surface that returns `IReadOnlyList<AITool>` from (`RoleSkillDefinition`, `PipelineContext`). Three concrete implementations land: `DiscussionRoundToolPolicy`, `StructuredRoundToolPolicy`, `FilterRoundToolPolicy`. The Verify path keeps its existing direct `IToolKit.GetToolsFor` call — it already builds the right surface and migrating it would expand scope.
- [Architecture] The policy interface lives in `AgentSmith.Contracts.Services` (per the YAML's file list) and signals "tool decision" at the contract layer. It depends only on `RoleSkillDefinition` and `PipelineContext` from Contracts so the contract layer doesn't pull in `SkillExecutionPhase` (which lives in `Application.Models`). Implementations in `AgentSmith.Application.Services.SkillRounds` map per-skill metadata + per-run pipeline state into the concrete `IToolHost` selection via the existing `IToolKit`.
- [Architecture] The policy is threaded through the executor chain (handler → executor → dispatcher) rather than injected straight into the dispatcher, because `SkillRoundDispatcher` is a single shared transient service across three handlers — each with a different policy. Adding a per-call `ISkillRoundToolPolicy` parameter on `ISkillRoundDispatcher.DispatchAsync`, `IDiscussionRoundExecutor.ExecuteAsync`, and `IStructuredRoundExecutor.ExecuteAsync` keeps each handler's policy choice local while preserving the shared executor + dispatcher singletons.
- [Architecture] `SkillRoundDispatcher` is now an `IConceptWriter` that publishes the `tool_set_size` int concept at dispatch time. The dispatcher is registered both as a concrete `SkillRoundDispatcher` (transient) and as `ISkillRoundDispatcher` (resolves to the same concrete) so `IConceptWriter` registration can pick up the concrete type without a downcast. p0125c/d's three-step IConceptWriter pattern repeats here.
- [Implementation] `StructuredRoundToolPolicy` returns Read/Grep for `api-security-scan` pipelines (both Active and Passive mode) and empty for `security-scan`. Active vs Passive doesn't change the IToolHost-derived tool set today — probing flows through the prompt strategy's `{"probe": ...}` JSON contract, not via an `HttpProbeToolHost`. If a probe IToolHost lands later, the policy is the right place to add it.
- [Implementation] `DiscussionRoundToolPolicy` gates the tool set on `RoleSkillDefinition.InvestigatorMode`: `verify_hint` and `survey` get Read/Grep/ListFiles via `FilesystemToolHost`; other discussion skills (mad-discussion opinions, legal-analysis commentary, ticket-driven prose) get empty. The pipeline preset (mad-discussion vs fix-bug) doesn't enter the decision — the skill's role metadata is authoritative.
- [Implementation] `FilterRoundToolPolicy` is the empty-by-design policy. Today the FilterRoundHandler uses it directly (not via the dispatcher) because filter dispatch happens in two places inside the handler (list-mode batch + artifact-mode single-shot), each of which constructs a SkillCallRequest directly against ISkillCallRuntime — they pre-date the SkillRoundDispatcher. The policy lookup happens inline at each request-construction site to keep the architectural shape (policy decides tools) consistent.
- [Implementation] The `tool_set_size` concept emission in the dispatcher uses a try/catch around `KeyNotFoundException` so the dispatch path doesn't blow up when the deployed `concept-vocabulary.yaml` doesn't yet declare the concept (cross-repo lag during the rollout). A debug log surfaces the skip. Once the agent-smith-skills PR ships, every call surfaces the value.
- [TradeOff] The dispatcher gained a logger + concepts factory dependency on top of `ISkillCallRuntime`. The decision-emission was kept inline (not extracted to a `IConceptEmitter` service) because the YAML's "emit at dispatch time" requirement is a tight semantic coupling — the value emitted is the very tool list the dispatcher just built, not a derivable downstream observation. A service would obscure that.
- [TradeOff] Could have unified Filter dispatch through `ISkillRoundDispatcher` for full consistency, but FilterRoundHandler's batched list-mode owns the per-batch `BuildPrompt → ExecuteAsync → BufferRuntimeObservations → ObservationParser` loop, and folding it into the dispatcher would require either the dispatcher to know about the filter-specific parsing path or a callback shape that defeats the point. Left as inline policy lookup; the architectural intent (no `Array.Empty<AITool>()` literal) is satisfied.
- [TradeOff] The `StructuredRoundToolPolicy` reads `ContextKeys.ActiveMode` in tests for visibility, but the current implementation doesn't branch on it. Documented so the next change here (when a probe IToolHost lands) knows to consult both ActiveMode and the role's category before adding tools.
