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}