Skip to main content

newt_core/agentic/
git_tool.rs

1//! The `git` tool seam (PR4, issue #461).
2//!
3//! `newt-git` (the embedded `GitEngine` over `grit-lib`) depends on `newt-core`
4//! for [`GitCaveats`](crate::git_caveats::GitCaveats), so `newt-core` can NOT
5//! depend on `newt-git` (circular). The agent loop reaches the engine through
6//! the same **trait-injection** seam already used for
7//! [`NoteSink`](super::note_sink::NoteSink) /
8//! [`RecallSource`](super::recall::RecallSource) /
9//! [`MemorySource`](super::memory_fetch::MemorySource): newt-core defines this
10//! trait, `newt-git` implements it (`LocalGitTool`), and the binary
11//! (`newt-tui`/`newt-cli`) injects the impl into [`ChatCtx`](super::ChatCtx)
12//! per turn. `execute_tool` dispatches the `git` tool through it; the tool is
13//! advertised only when an impl is injected (presence gate).
14
15use crate::git_caveats::GitCaveats;
16
17/// The injected git capability. Object-safe and shareable (the loop holds
18/// `&dyn GitTool`; `Send + Sync` because the borrow lives across `.await`
19/// points, exactly like [`RecallSource`](super::recall::RecallSource)).
20///
21/// `op` is one of `status` | `log` | `diff` | `add` | `commit` | `branch`;
22/// `args` is the tool-call argument object. The implementation enforces
23/// `caveats` (fail-closed) and returns either a rendered, model-readable result
24/// string (`Ok`) or an error string the tool layer surfaces verbatim (`Err`) —
25/// including capability denials, so the model can see *why* a write was refused.
26pub trait GitTool: Send + Sync {
27    fn dispatch(
28        &self,
29        op: &str,
30        args: &serde_json::Value,
31        caveats: &GitCaveats,
32    ) -> Result<String, String>;
33}
34
35/// The advertised `git` tool definition — pushed into the tool list only when a
36/// [`GitTool`] is injected (`merged_tool_definitions(.., with_git = true)`), so
37/// eval / headless / non-repo sessions never see it.
38pub fn git_tool_definition() -> serde_json::Value {
39    serde_json::json!({
40        "type": "function",
41        "function": {
42            "name": "git",
43            "description": "Run a git operation through the embedded engine \
44                            (NOT run_command; this is always available — do not \
45                            look for a separate git tool). Local-only: initialize \
46                            a repo here with init (when the workspace is not yet a \
47                            git repo), read status/log/diff, stage with add, \
48                            commit, amend the last commit, create a branch \
49                            (branch), switch to / create-and-switch a branch \
50                            (checkout), delete a branch (branch-delete), and \
51                            rebase (structured plan: reword/squash/drop). \
52                            Writes (init/add/commit/amend/branch/checkout/ \
53                            branch-delete/rebase) require the session to permit \
54                            them; there are no network ops (pull/fetch/push are \
55                            unavailable).",
56            "parameters": {
57                "type": "object",
58                "properties": {
59                    "op": {
60                        "type": "string",
61                        "enum": ["init", "status", "log", "diff", "add", "commit", "amend", "branch", "rebase", "checkout", "branch-delete", "stash", "stash-list", "stash-pop", "stash-apply", "stash-drop"],
62                        "description": "The git operation to run."
63                    },
64                    "index": {
65                        "type": "integer",
66                        "description": "For op=stash-pop/stash-apply/stash-drop: the stash index k (stash@{k}); default 0 (newest)."
67                    },
68                    "onto": {
69                        "type": "string",
70                        "description": "For op=rebase: the base commit/ref to replay the plan onto."
71                    },
72                    "plan": {
73                        "type": "array",
74                        "description": "For op=rebase: ordered steps, oldest first. Each: \
75                                        {commit, action: pick|reword|squash|fixup|drop, message?}. \
76                                        reword/squash use message; squash folds into the previous \
77                                        commit keeping both messages; fixup folds discarding it; \
78                                        drop removes the commit. Aborts (no change) on any conflict.",
79                        "items": {
80                            "type": "object",
81                            "properties": {
82                                "commit": { "type": "string" },
83                                "action": {
84                                    "type": "string",
85                                    "enum": ["pick", "reword", "squash", "fixup", "drop"]
86                                },
87                                "message": { "type": "string" }
88                            },
89                            "required": ["commit", "action"]
90                        }
91                    },
92                    "paths": {
93                        "type": "array",
94                        "items": { "type": "string" },
95                        "description": "For op=add: the paths to stage."
96                    },
97                    "message": {
98                        "type": "string",
99                        "description": "For op=commit: the commit message. For \
100                                        op=amend: the reworded message (omit to \
101                                        keep the existing one)."
102                    },
103                    "name": {
104                        "type": "string",
105                        "description": "The branch name — for op=branch (create), \
106                                        op=checkout (switch to / create), and \
107                                        op=branch-delete."
108                    },
109                    "create": {
110                        "type": "boolean",
111                        "description": "For op=checkout: create the branch at HEAD \
112                                        when it does not exist (default true, i.e. \
113                                        `checkout -b`). Set false to switch to an \
114                                        existing branch only."
115                    },
116                    "spec": {
117                        "type": "string",
118                        "enum": ["worktree", "staged"],
119                        "description": "For op=diff: worktree (unstaged, default) or staged."
120                    },
121                    "limit": {
122                        "type": "integer",
123                        "description": "For op=log: max commits (default 20)."
124                    }
125                },
126                "required": ["op"]
127            }
128        }
129    })
130}