From d44aae37de6e431f2992d0500c3f94d3aaf77eb8 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 25 Aug 2026 16:31:06 +0700 Subject: [PATCH] refactor(coderd/x/chatd): return PersistedStep directly from ExecuteLocalTools (#28359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Stack**: **#28359 (this PR)** → #28360 → #28361 → #28362 `chatloop.ToolExecutionOutcome` only wrapped a `PersistedStep`, so callers had to reach through `outcome.Step` for every field. `ExecuteLocalTools` now returns the `PersistedStep` directly and the wrapper type is deleted. No behavior change. First of a four-PR stack that makes local tool execution count toward Coder Agent runtime (splitting #28211). This refactor keeps the rename noise out of the feature PRs. Refs CODAGT-928 (https://linear.app/codercom/issue/CODAGT-928/track-local-tool-execution-for-agent-runtime). --- coderd/x/chatd/chatloop/chatloop.go | 23 +++++++++-------------- coderd/x/chatd/generation.go | 13 +++++++------ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/coderd/x/chatd/chatloop/chatloop.go b/coderd/x/chatd/chatloop/chatloop.go index 3e14b8943e3..ba6c1a7d0f3 100644 --- a/coderd/x/chatd/chatloop/chatloop.go +++ b/coderd/x/chatd/chatloop/chatloop.go @@ -275,11 +275,6 @@ type ExecuteLocalToolsOptions struct { Clock quartz.Clock } -// ToolExecutionOutcome is the durable tool-result content from one batch. -type ToolExecutionOutcome struct { - Step PersistedStep -} - // GenerateCompactionOptions configures one context compaction call. type GenerateCompactionOptions struct { Model fantasy.LanguageModel @@ -562,7 +557,7 @@ func contentFilterError(provider string, metadata fantasy.ProviderMetadata) erro // ExecuteLocalTools runs local tool calls and returns durable tool results. It // does not retry or persist. -func ExecuteLocalTools(ctx context.Context, opts ExecuteLocalToolsOptions) (ToolExecutionOutcome, error) { +func ExecuteLocalTools(ctx context.Context, opts ExecuteLocalToolsOptions) (PersistedStep, error) { if opts.Metrics == nil { opts.Metrics = NopMetrics() } @@ -584,7 +579,7 @@ func ExecuteLocalTools(ctx context.Context, opts ExecuteLocalToolsOptions) (Tool // without capturing the publisher at construction time. ctx = WithMessagePartPublisher(ctx, opts.PublishMessagePart) if ctx.Err() != nil { - return ToolExecutionOutcome{}, ctx.Err() + return PersistedStep{}, ctx.Err() } localCalls := make([]fantasy.ToolCallContent, 0, len(opts.ToolCalls)) @@ -594,7 +589,7 @@ func ExecuteLocalTools(ctx context.Context, opts ExecuteLocalToolsOptions) (Tool } } if len(localCalls) == 0 { - return ToolExecutionOutcome{}, nil + return PersistedStep{}, nil } var result stepResult @@ -616,12 +611,12 @@ func ExecuteLocalTools(ctx context.Context, opts ExecuteLocalToolsOptions) (Tool result.content = append(result.content, tr) } if ctx.Err() != nil { - return ToolExecutionOutcome{}, ctx.Err() + return PersistedStep{}, ctx.Err() } - return ToolExecutionOutcome{Step: PersistedStep{ + return PersistedStep{ Content: result.content, ToolResultCreatedAt: result.toolResultCreatedAt, - }}, nil + }, nil } maxResultBytes := toolResultByteBudget(opts.ContextLimit) @@ -650,15 +645,15 @@ func ExecuteLocalTools(ctx context.Context, opts ExecuteLocalToolsOptions) (Tool }, ) if ctx.Err() != nil { - return ToolExecutionOutcome{}, ctx.Err() + return PersistedStep{}, ctx.Err() } for _, tr := range toolResults { result.content = append(result.content, tr) } - return ToolExecutionOutcome{Step: PersistedStep{ + return PersistedStep{ Content: result.content, ToolResultCreatedAt: result.toolResultCreatedAt, - }}, nil + }, nil } // prepareMessagesForRequest applies the prompt preparation pipeline used diff --git a/coderd/x/chatd/generation.go b/coderd/x/chatd/generation.go index cb1440c7029..a3a2aeb2e96 100644 --- a/coderd/x/chatd/generation.go +++ b/coderd/x/chatd/generation.go @@ -858,7 +858,7 @@ func (s *taskStarter) executeLocalTools( provider = prepared.Model.Provider() modelName = prepared.Model.ModelID() } - var outcome chatloop.ToolExecutionOutcome + var outcome chatloop.PersistedStep var spawnDispatchErr error if len(allowed) > 0 { outcome, err = chatloop.ExecuteLocalTools(ctx, chatloop.ExecuteLocalToolsOptions{ @@ -886,18 +886,19 @@ func (s *taskStarter) executeLocalTools( // the tool run; its failure surfaces as a tool result error. The // step still commits so a sibling tool that already ran keeps its // result and is not re-executed, and the turn fails afterwards. - if hookErr := chathooks.DispatchFailureFromResults(outcome.Step.Content); hookErr != nil { + if hookErr := chathooks.DispatchFailureFromResults(outcome.Content); hookErr != nil { spawnDispatchErr = chathooks.GenerationDispatchError(agenthooks.EventUserPromptSubmit, hookErr) } } - postResults, postDispatchErr := s.server.hooks.PostToolUseResults(ctx, chathooks.ChatFor(prepared.Chat, input.hookTurnID()), outcome.Step.Content) + postResults, postDispatchErr := s.server.hooks.PostToolUseResults(ctx, chathooks.ChatFor(prepared.Chat, input.hookTurnID()), outcome.Content) for _, result := range denied { - outcome.Step.Content = append(outcome.Step.Content, result) + outcome.Content = append(outcome.Content, result) } - chathooks.RestoreToolCallOrder(outcome.Step.Content, decision.localToolCalls) + chathooks.RestoreToolCallOrder(outcome.Content, decision.localToolCalls) + step := stepDataFromPersisted(outcome) messages, err := buildCommitStepMessages(buildCommitStepMessagesInput{ modelConfigID: prepared.ModelConfigID, - step: stepDataFromPersisted(outcome.Step), + step: step, toolNameToConfigID: prepared.ToolNameToConfigID, logger: s.opts.Logger, contentVersion: chatprompt.CurrentContentVersion,