refactor(skills): move fork-skill validation into SkillsToolset#2524
Merged
dgageot merged 1 commit intodocker:mainfrom Apr 27, 2026
Merged
Conversation
Extract skill-specific business rules (lookup, fork-mode validation, content expansion) from the runtime's handleRunSkill into a new SkillsToolset.PrepareForkSubSession method. The runtime handler now keeps only the parts that genuinely need its private internals: sub-session creation, OpenTelemetry tracing, and event forwarding. - Add PreparedSkillFork struct + PrepareForkSubSession method to pkg/tools/builtin/skills.go. It returns either a populated bundle or a *tools.ToolCallResult describing the failure (skill missing, not configured for fork, content read error). - Shrink handleRunSkill from ~80 to ~50 lines; it now delegates validation/expansion to the toolset and focuses on runtime-private orchestration. - Add unit tests for PrepareForkSubSession covering the happy path, not-found, not-fork, and read-failure cases. These previously had no equivalent at the toolset layer. Assisted-By: docker-agent
gtardif
approved these changes
Apr 27, 2026
dgageot
added a commit
that referenced
this pull request
Apr 27, 2026
The merge of #2524 (move fork-skill validation into SkillsToolset) on top of #2525 (skill model-override) left handleRunSkill referencing identifiers that no longer exist in its scope: skill.Model and params.Name. The refactor renamed params -> args and dropped the local skill variable in favour of a PreparedSkillFork bundle, but the model-override block introduced by #2525 was not updated, so package github.com/docker/docker-agent/pkg/runtime failed to build. Restore the build by surfacing the model override through the toolset, in line with the refactor's intent of keeping skill-specific business rules out of the runtime. - Add Model to PreparedSkillFork and populate it from skill.Model inside (*SkillsToolset).PrepareForkSubSession, so the runtime no longer needs a direct skills.Skill reference. - Switch handleRunSkill's override block to prepared.Model and prepared.SkillName; the WithAgentModel call, defer restore(), and warning log are otherwise unchanged. - Extend the PrepareForkSubSession happy-path test to assert the Model field is propagated, and add a _NoModelOverride case that pins down the empty-Model contract the runtime relies on to skip the override path. Validated with go build, go vet, golangci-lint (0 issues), and go test on pkg/runtime, pkg/tools/builtin, pkg/skills, and pkg/agent. Assisted-By: docker-agent
dgageot
added a commit
to dgageot/cagent
that referenced
this pull request
Apr 27, 2026
Session compaction is now first-class in the hooks mechanism. Two new events let users observe, veto, or replace a compaction without touching the runtime: - before_compaction fires immediately before a compaction. The hook receives input_tokens, output_tokens, context_limit and a compaction_reason of "threshold" (proactive 90% trigger), "overflow" (post-overflow recovery) or "manual" (user-invoked /compact). It can veto the compaction (Decision: "block") or supply a custom summary string via HookSpecificOutput.summary, in which case the runtime applies that summary verbatim and skips the LLM-based summarization. - after_compaction fires once a summary has been applied to the session. Receives the produced summary text in Input.summary alongside the same token / reason fields. Purely observational. The runtime also now re-emits session_start with Source="compact" right after a successful compaction so hooks like add_environment_info can re-inject ambient context that may have been lost in the summary. This honors the previously dead "compact" Source value documented on hooks.Input.Source. When no compaction-related hooks are configured, control flow through doCompact is bit-for-bit identical to the previous implementation — guarded by a new TestDoCompactNoHooksMatchesPriorBehavior regression test. dispatchHook short-circuits when no executor is configured, so the hook dispatch sites are essentially free in the common case. Implementation notes: - pkg/hooks/types.go gets the two new EventType constants, two new Config fields, six new Input fields (input_tokens, output_tokens, context_limit, compaction_reason, summary; the existing source field is documented to include "compact"), and a Summary field on HookSpecificOutput plus Result. aggregate() surfaces the first non-empty Summary into Result.Summary; concurrent execution makes "first wins" deterministic at the value level (no concatenation, no clobbering). - pkg/runtime/hooks.go adds executeBeforeCompactionHooks, executeAfterCompactionHooks, and a parameterised executeSessionStartHooksWithSource (the existing executeSessionStartHooks is now a thin wrapper). - pkg/runtime/session_compaction.go threads a reason parameter into doCompact and adds a computeFirstKeptEntry helper so the hook-supplied-summary path keeps the same maxKeepTokens tail-keep policy as the LLM path. A new internal compactWithReason method on LocalRuntime forwards the reason; the public Summarize stays unchanged and reports "manual". loop.go callsites tag "threshold" and "overflow". Schema and example: - agent-schema.json adds the two new properties on HooksConfig with the full contract documented (TestSchemaMatchesGoTypes covers drift between schema and Go types). - examples/hooks.yaml adds two well-commented blocks demonstrating observational logging and veto/replace semantics, with a warning about denying on compaction_reason="overflow". Tests: - pkg/hooks: 5 new tests covering allow-by-default, exit-code-2 veto, HookSpecificOutput.summary surfacing into Result.Summary, first-summary-wins under concurrent dispatch, after_compaction's observational contract, and the JSON wire format. - pkg/runtime: 5 new tests covering deny-skips-everything, hook-summary-skips-LLM, after_compaction fires with the produced summary, session_start re-emit with Source="compact", and the no-hooks regression guard. Drive-by fix: a merge between docker#2524 (PrepareForkSubSession extraction) and the earlier model-override series left pkg/runtime/skill_runner.go referencing two no-longer-defined symbols (skill, params). PreparedSkillFork now carries the Model override and the runner reads from prepared.Model / prepared.SkillName. Assisted-By: docker-agent
dgageot
added a commit
to dgageot/cagent
that referenced
this pull request
Apr 28, 2026
Session compaction is now first-class in the hooks mechanism. Two new events let users observe, veto, or replace a compaction without touching the runtime: - before_compaction fires immediately before a compaction. The hook receives input_tokens, output_tokens, context_limit and a compaction_reason of "threshold" (proactive 90% trigger), "overflow" (post-overflow recovery) or "manual" (user-invoked /compact). It can veto the compaction (Decision: "block") or supply a custom summary string via HookSpecificOutput.summary, in which case the runtime applies that summary verbatim and skips the LLM-based summarization. - after_compaction fires once a summary has been applied to the session. Receives the produced summary text in Input.summary alongside the same token / reason fields. Purely observational. The runtime also now re-emits session_start with Source="compact" right after a successful compaction so hooks like add_environment_info can re-inject ambient context that may have been lost in the summary. This honors the previously dead "compact" Source value documented on hooks.Input.Source. When no compaction-related hooks are configured, control flow through doCompact is bit-for-bit identical to the previous implementation — guarded by a new TestDoCompactNoHooksMatchesPriorBehavior regression test. dispatchHook short-circuits when no executor is configured, so the hook dispatch sites are essentially free in the common case. Implementation notes: - pkg/hooks/types.go gets the two new EventType constants, two new Config fields, six new Input fields (input_tokens, output_tokens, context_limit, compaction_reason, summary; the existing source field is documented to include "compact"), and a Summary field on HookSpecificOutput plus Result. aggregate() surfaces the first non-empty Summary into Result.Summary; concurrent execution makes "first wins" deterministic at the value level (no concatenation, no clobbering). - pkg/runtime/hooks.go adds executeBeforeCompactionHooks, executeAfterCompactionHooks, and a parameterised executeSessionStartHooksWithSource (the existing executeSessionStartHooks is now a thin wrapper). - pkg/runtime/session_compaction.go threads a reason parameter into doCompact and adds a computeFirstKeptEntry helper so the hook-supplied-summary path keeps the same maxKeepTokens tail-keep policy as the LLM path. A new internal compactWithReason method on LocalRuntime forwards the reason; the public Summarize stays unchanged and reports "manual". loop.go callsites tag "threshold" and "overflow". Schema and example: - agent-schema.json adds the two new properties on HooksConfig with the full contract documented (TestSchemaMatchesGoTypes covers drift between schema and Go types). - examples/hooks.yaml adds two well-commented blocks demonstrating observational logging and veto/replace semantics, with a warning about denying on compaction_reason="overflow". Tests: - pkg/hooks: 5 new tests covering allow-by-default, exit-code-2 veto, HookSpecificOutput.summary surfacing into Result.Summary, first-summary-wins under concurrent dispatch, after_compaction's observational contract, and the JSON wire format. - pkg/runtime: 5 new tests covering deny-skips-everything, hook-summary-skips-LLM, after_compaction fires with the produced summary, session_start re-emit with Source="compact", and the no-hooks regression guard. Drive-by fix: a merge between docker#2524 (PrepareForkSubSession extraction) and the earlier model-override series left pkg/runtime/skill_runner.go referencing two no-longer-defined symbols (skill, params). PreparedSkillFork now carries the Model override and the runner reads from prepared.Model / prepared.SkillName. Assisted-By: docker-agent
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extract skill-specific business rules (lookup, fork-mode validation, content
expansion) from the runtime's
handleRunSkillinto a newSkillsToolset.PrepareForkSubSessionmethod. The runtime handler now keepsonly the parts that genuinely need its private internals: sub-session
creation, OpenTelemetry tracing, and event forwarding.
What changed
PreparedSkillForkstruct +PrepareForkSubSessionmethod topkg/tools/builtin/skills.go. It returns either a populated bundle or a*tools.ToolCallResultdescribing the failure (skill missing, notconfigured for fork, content read error).
handleRunSkillfrom ~80 to ~50 lines; it now delegatesvalidation/expansion to the toolset and focuses on runtime-private
orchestration.
PrepareForkSubSessioncovering the happy path,not-found, not-fork, and read-failure cases. These previously had no
equivalent at the toolset layer.
Why stop here
Going further — moving
handleRunSkillitself out ofpkg/runtime(likethe
bgAgentspattern inpkg/tools/builtin/agent) — runs into a realobstacle:
run_skillneeds live event forwarding to the user's streamso the skill execution streams in the TUI, and the events channel is typed
as
chan runtime.Event. The bgAgent pattern sidesteps this by collectingoutput via a plain
OnContent func(string)callback, whichrun_skillcan't use without changing UX. Fully extracting would require either
pushing
Eventout into a shared package (a much larger refactor) orloose-typing the channel as
chan any. Neither felt worth it for the ~50lines that remain — those genuinely are runtime concerns (sub-session
creation, OTEL span, event channel).
Validation
+ "mise lint" +— 0 issues+ "mise test" +— all packages passAssisted-By: docker-agent