返回 DeepSeek-Reasonix
execute_batch_audit.go
根目录 / internal / agent / execute_batch_audit.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "strings"
7 "time"
8
9 "reasonix/internal/event"
10 "reasonix/internal/evidence"
11 "reasonix/internal/provider"
12 "reasonix/internal/tool"
13 )
14
15 func (a *Agent) emitBatchToolResult(ctx context.Context, c provider.ToolCall, o toolOutcome, committedMessage provider.Message, duration, started int64, parallel bool, batchStart time.Time) error {
16 t, _, ambiguous := a.svc.tools.ResolveCall(c.Name)
17 ok := t != nil && len(ambiguous) == 0
18 readOnly := ok && t.ReadOnly()
19 if c.ResolvedReadOnly != nil {
20 readOnly = *c.ResolvedReadOnly
21 }
22 tr := event.Tool{
23 RunState: outcomeRunState(o),
24 ID: c.ID,
25 Name: c.Name,
26 Args: c.Arguments,
27 ResolvedName: c.ResolvedName,
28 CapabilityID: c.CapabilityID,
29 Output: o.output,
30 Err: o.errMsg,
31 ReadOnly: readOnly,
32 Truncated: o.truncated,
33 DurationMs: duration,
34 Execution: toEventShellExecution(o.execution, duration),
35 PresentedFiles: append([]provider.PresentedFile(nil), o.presentedFiles...),
36 }
37 if o.diagnostic != nil {
38 tr.Diagnostic, _ = json.Marshal(o.diagnostic)
39 }
40 if o.subagentOutcome != nil {
41 tr.SubagentRef = o.subagentOutcome.Ref
42 tr.SubagentStatus = string(o.subagentOutcome.Status)
43 tr.SubagentErrorCode = o.subagentOutcome.ErrorCode
44 tr.SubagentRetryable = o.subagentOutcome.Retryable
45 } else if isSubagentToolCall(c) {
46 if outcome, ok := ParseSubagentOutcome(o.output); ok {
47 tr.SubagentRef = outcome.Ref
48 tr.SubagentStatus = string(outcome.Status)
49 tr.SubagentErrorCode = outcome.ErrorCode
50 tr.SubagentRetryable = outcome.Retryable
51 }
52 }
53 var committedTodos []evidence.TodoItem
54 if c.Name == "todo_write" && o.errMsg == "" && !o.blocked {
55 receipt := evidence.ReceiptFromToolCall("todo_write", json.RawMessage(c.Arguments), true, true)
56 // Successful execution means the strict todo_write validator already
57 // accepted these arguments. Commit the normalized call data itself; tool
58 // output is presentation and may be compacted independently.
59 committedTodos = append([]evidence.TodoItem(nil), receipt.Todos...)
60 for i := range committedTodos {
61 committedTodos[i].Content = strings.TrimSpace(committedTodos[i].Content)
62 }
63 tr.TodoWritten = true
64 tr.Todos = make([]event.Todo, len(committedTodos))
65 for i, todo := range committedTodos {
66 tr.Todos[i] = event.Todo{Content: todo.Content, Status: todo.Status}
67 }
68 }
69 if started > 0 {
70 tr.StartedAt = started
71 tr.EndedAt = started + duration
72 if mutation := o.workspaceMutation; mutation != nil {
73 tr.WorkspaceMutation = true
74 tr.WorkspacePaths = append([]string(nil), mutation.Paths...)
75 tr.WorkspaceAllPaths = mutation.AllPaths
76 }
77 }
78 if err := event.EmitChecked(a.svc.sink, event.Event{Kind: event.ToolResult, MessageID: messageIdentity(ctx), Tool: tr, CommittedMessage: &committedMessage}); err != nil {
79 return err
80 }
81 if tr.TodoWritten {
82 a.setTodoState(committedTodos)
83 }
84 if o.truncated && o.truncMsg != "" {
85 a.svc.sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: o.truncMsg})
86 }
87 a.recordToolExecutionAudit(readOnly, parallel, started, duration, batchStart, o)
88 return nil
89 }
90
91 func isSubagentToolName(name string) bool {
92 switch name {
93 case "task", "read_only_task", "run_skill", "read_only_skill", "explore", "research", "review", "security_review", "security-review", "parallel_tasks", "fleet":
94 return true
95 default:
96 return false
97 }
98 }
99
100 func isSubagentToolCall(call provider.ToolCall) bool {
101 return isSubagentToolName(call.Name) || strings.HasPrefix(strings.TrimSpace(call.CapabilityID), "skill:")
102 }
103
104 func (a *Agent) recordToolExecutionAudit(readOnly, parallel bool, startedAt, durationMs int64, batchStart time.Time, o toolOutcome) {
105 if a == nil || a.capabilityAudit == nil || startedAt <= 0 {
106 return
107 }
108 queueMs := max(startedAt-batchStart.UnixMilli(), 0)
109 rawBytes := len(o.output)
110 if o.rawOutput != "" {
111 rawBytes = len(o.rawOutput)
112 }
113 a.capabilityAudit.RecordToolExecution(readOnly, parallel, queueMs, durationMs, rawBytes, len(o.output))
114 }
115
116 func (a *Agent) buildBatchToolResult(ctx context.Context, call provider.ToolCall, o toolOutcome) provider.Message {
117 state := outcomeRunState(o)
118 msg := provider.Message{Role: provider.RoleTool, Content: o.output, Images: o.images, VisionSummary: o.visionSummary, ToolCallID: call.ID, Name: call.Name, ToolRunState: state, ToolExecution: toProviderToolExecution(o.execution), PresentedFiles: provider.NewPresentedFilesMetadata(o.presentedFiles)}
119 if len(o.images) > 0 && a.imageResolver != nil {
120 if inputs, err := a.imageResolver.PersistToolImages(ctx, o.images); err == nil && len(inputs) > 0 {
121 msg.ImageInputs = inputs
122 msg.Images = nil
123 } else if err != nil {
124 msg.Images = nil
125 msg.Content += "\n[Image persistence failed. The tool already executed; its text result remains valid. Do not repeat the original action to retry image processing.]"
126 msg.ToolDiagnostic, _ = json.Marshal(map[string]string{"code": "image_persistence_failed", "message": "Image persistence failed; completed tool text was preserved."})
127 }
128 }
129 if o.diagnostic != nil && len(msg.ToolDiagnostic) == 0 {
130 msg.ToolDiagnostic, _ = json.Marshal(o.diagnostic)
131 }
132 if o.rawOutput != "" && o.rawOutput != o.output {
133 msg.RawContent = o.rawOutput
134 }
135 if env, ok := a.finalizedReadEnvelope(ctx, call, o); ok {
136 if env.HasMore && len(msg.ToolDiagnostic) == 0 {
137 msg.ToolDiagnostic, _ = json.Marshal(tool.OperationDiagnostic{Code: tool.ReadPartial, Path: env.Source.CanonicalPath, OperationID: call.ID, ActualSnapshot: env.Source.Snapshot, RequiredRanges: env.DeliveredRanges, Recovery: "continue with the next window only if the task requires more coverage"})
138 }
139 if raw, err := json.Marshal(env); err == nil {
140 msg.ReadResult = raw
141 }
142 }
143 if msg.ID == "" {
144 msg.ID = NewMessageID()
145 }
146 return msg
147 }
148
148 lines GO