executeToolsForStep runs the tool-execution phase of a single chatloop step. It enforces the exclusive-tool policy, partitions built-in versus dynamic tool calls, dispatches built-in tools, and when dynamic tool calls are present persists the step and returns ErrDynamicToolCall so the caller can exe
( ctx context.Context, opts RunOptions, result *stepResult, provider, modelName string, step int, stepStart time.Time, publishMessagePart func(codersdk.ChatMessageRole, codersdk.ChatMessagePart), )
| 1264 | // caller must propagate (ErrInterrupted, ErrDynamicToolCall, ctx.Err(), |
| 1265 | // or a persistence failure). |
| 1266 | func executeToolsForStep( |
| 1267 | ctx context.Context, |
| 1268 | opts RunOptions, |
| 1269 | result *stepResult, |
| 1270 | provider, modelName string, |
| 1271 | step int, |
| 1272 | stepStart time.Time, |
| 1273 | publishMessagePart func(codersdk.ChatMessageRole, codersdk.ChatMessagePart), |
| 1274 | ) ([]fantasy.ToolResultContent, error) { |
| 1275 | // Check for context cancellation before starting tool |
| 1276 | // execution. If the chat was interrupted between stream |
| 1277 | // completion and here, persist what we have and bail out. |
| 1278 | if ctx.Err() != nil { |
| 1279 | if errors.Is(context.Cause(ctx), ErrInterrupted) { |
| 1280 | persistInterruptedStep(ctx, opts, result) |
| 1281 | return nil, ErrInterrupted |
| 1282 | } |
| 1283 | return nil, ctx.Err() |
| 1284 | } |
| 1285 | |
| 1286 | // Enforce exclusivity across ALL locally-executable tool |
| 1287 | // calls (both built-in and dynamic) before partitioning. |
| 1288 | // Checking only the built-in partition would let the model |
| 1289 | // bypass the policy by mixing an exclusive tool with a |
| 1290 | // dynamic tool: the exclusive tool would still run and the |
| 1291 | // dynamic call would still be handed to the caller for |
| 1292 | // external execution, breaking the planning-only contract. |
| 1293 | localCandidates := make([]fantasy.ToolCallContent, 0, len(result.toolCalls)) |
| 1294 | for _, tc := range result.toolCalls { |
| 1295 | if !tc.ProviderExecuted { |
| 1296 | localCandidates = append(localCandidates, tc) |
| 1297 | } |
| 1298 | } |
| 1299 | policyResults, exclusiveViolation := applyExclusiveToolPolicy( |
| 1300 | localCandidates, |
| 1301 | opts.ExclusiveToolNames, |
| 1302 | opts.Metrics, |
| 1303 | provider, |
| 1304 | modelName, |
| 1305 | ) |
| 1306 | if exclusiveViolation { |
| 1307 | now := dbtime.Now() |
| 1308 | for _, tr := range policyResults { |
| 1309 | recordToolResultTimestamp(result, tr.ToolCallID, now) |
| 1310 | publishToolAttachments(ctx, opts.Logger, tr, now, publishMessagePart) |
| 1311 | ssePart := chatprompt.PartFromContentWithLogger(ctx, opts.Logger, tr) |
| 1312 | ssePart.CreatedAt = &now |
| 1313 | publishMessagePart(codersdk.ChatMessageRoleTool, ssePart) |
| 1314 | } |
| 1315 | for _, tr := range policyResults { |
| 1316 | result.content = append(result.content, tr) |
| 1317 | } |
| 1318 | // Mirror the post-execution interruption check used by the |
| 1319 | // non-policy path: if the chat was interrupted while we |
| 1320 | // synthesized policy errors, route through |
| 1321 | // persistInterruptedStep so the synthesized results are not |
| 1322 | // dropped when the regular PersistStep path fails on a |
| 1323 | // canceled context. |
no test coverage detected