返回 DeepSeek-Reasonix
workspace_mutation.go
根目录 / internal / agent / workspace_mutation.go
1 package agent
2
3 import (
4 "encoding/json"
5 "strings"
6 "sync"
7
8 "reasonix/internal/event"
9 "reasonix/internal/evidence"
10 "reasonix/internal/tool"
11 )
12
13 type workspaceEffectiveCall struct {
14 name string
15 args json.RawMessage
16 readOnly bool
17 }
18
19 var workspaceMutationSignalMu sync.Mutex
20
21 func finalizeWorkspaceMutationOutcome(out *toolOutcome, plan *toolCallPlan) {
22 out.executed = plan.executed
23 if plan.evidenceName != "" {
24 out.effective = workspaceEffectiveCall{
25 name: plan.evidenceName, args: append([]byte(nil), plan.evidenceArgs...), readOnly: plan.readOnly,
26 }
27 }
28 if !plan.executed || isMCPLifecycleConnectTarget(plan.runTool) {
29 return
30 }
31 if mutation, ok := workspaceMutationForCall(plan.call.ID, plan.evidenceName, plan.evidenceArgs, plan.readOnly); ok {
32 out.workspaceMutation = &mutation
33 }
34 }
35
36 // tool.before can turn nominally read-only parallel calls into writers. Keep
37 // the optional sink callback serial while publishing from each worker as soon
38 // as that concrete replacement completes.
39 func recordWorkspaceMutation(sink event.Sink, mutation *event.WorkspaceMutation) {
40 if mutation == nil {
41 return
42 }
43 workspaceMutationSignalMu.Lock()
44 defer workspaceMutationSignalMu.Unlock()
45 event.RecordWorkspaceMutation(sink, *mutation)
46 }
47
48 // workspaceMutationForCall classifies host resource invalidation independently
49 // from the delivery evidence ledger. Delivery asks whether a call invalidates a
50 // completed-review receipt; the desktop asks which workspace resources may have
51 // changed. Those contracts intentionally differ for operations such as a bare
52 // git commit, which changes HEAD/index/history without changing file contents.
53 func workspaceMutationForCall(toolID, toolName string, args json.RawMessage, readOnly bool) (event.WorkspaceMutation, bool) {
54 toolName = strings.TrimSpace(toolName)
55 if toolName == "" || evidence.IsNonMutationMetaTool(toolName) || workspaceHostStateOnlyTool(toolName) {
56 return event.WorkspaceMutation{}, false
57 }
58 effects := evidence.ClassifyToolCall(toolName, args, readOnly)
59 if !effects.WorkspaceMutation {
60 return event.WorkspaceMutation{}, false
61 }
62
63 paths := evidence.ToolCallPaths(args)
64 mutation := event.WorkspaceMutation{
65 ToolID: toolID,
66 ToolName: toolName,
67 Paths: paths,
68 AllPaths: tool.IsShellToolName(toolName) || len(paths) == 0,
69 Content: effects.ContentMutation,
70 Tree: effects.ContentMutation,
71 WorkingTree: effects.ContentMutation,
72 GitMeta: effects.RepositoryMutation,
73 }
74 return mutation, true
75 }
76
77 func workspaceHostStateOnlyTool(toolName string) bool {
78 if strings.HasPrefix(toolName, "mcp_connect__") {
79 return true
80 }
81 switch toolName {
82 case "job_kill", "kill_shell", "remember", "forget":
83 return true
84 default:
85 return false
86 }
87 }
88
88 lines GO