Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Ask no longer takes back answers it read for, keeps to the workspace, and says what it remembers. A provider was given only the words of earlier answers, never the tool reads behind them, so asked a follow-up, the model saw an answer with no tool calls and "corrected" it as unchecked, though the panel showed "Read g1t in 4 steps". Each earlier answer is now read back starting with a bracketed note of its reads (each step's label and what it found, failed or not run; at most 800 characters, first so cutting a long answer short keeps it), and the prompt says those reads happened, to read again if one may be stale, and never to write such a note itself. The prompt keeps answers to the workspace: other repositories on g1t they can see (public ones, other workspaces') come in only when named or asked about, and an answer is no longer padded with a list of what it could see instead. It also says what it remembers: the direct message with @g1t that the Ask panel shares, its latest 40 messages since the last new conversation, and no other memory. Checked: chat's 71 tests (new: an earlier answer read back with its reads) and typecheck, cargo test for g1t-integrations (65) and its wasm build.

syntaqxcommitted Parentc28ae41Browse files
3 files+70−40/3 viewed
+24−0
1515 metaColumn,
1616 notSetUpText,
1717 pageFrom,
18+ readsNote,
1819 shownAnswer,
1920 startsAnswer,
2021 type SaidRow,
7980 ]);
8081 });
8182
83+test("an earlier answer is read back with what it read in g1t, so it is not taken for made up", () => {
84+ const steps = [
85+ { type: "tool", id: "t1", name: "repository", action: "list", label: "Listed repositories", status: "done", summary: "9 repositories" },
86+ { type: "tool", id: "t2", name: "repository", action: "get", label: "Read acme/rocket", status: "failed", summary: "Repository not found" },
87+ { type: "sources", sources: [] },
88+ ];
89+ const rows = [
90+ row("m1", ana, "Open issues in acme/rocket?"),
91+ row("m2", G1T_KEY, "There is no acme/rocket.", { answer: { state: "done", steps } }),
92+ row("m3", ana, "Did you check?"),
93+ ];
94+ assert.deepEqual(conversation(rows)[1], {
95+ role: "assistant",
96+ content:
97+ "[g1t's tool reads for this answer: Listed repositories: 9 repositories; Read acme/rocket (failed: Repository not found)]\n\nThere is no acme/rocket.",
98+ });
99+ assert.equal(readsNote([]), null);
100+ assert.equal(readsNote(undefined), null);
101+ const many = Array.from({ length: 50 }, (_, i) => ({ type: "tool", id: `t${i}`, label: `Read file number ${i}`, status: "done" }));
102+ const long = readsNote(many)!;
103+ assert.ok(long.length <= 800 && long.endsWith("…]"));
104+});
105+
82106 test("the conversation leaves out failed answers, the set-up note, the answer being written, cards and deleted messages", () => {
83107 const rows = [
84108 row("m1", ana, "First"),
+30−3
8282 /** A message as the conversation reads it: oldest first. */
8383 export type SaidRow = { id: string; author: string; kind: "text" | "card"; body: string; meta: string | null; deleted_at: string | null };
8484
85+/** The longest note of an earlier answer's reads, in characters. */
86+const READS_NOTE_CHARS = 800;
87+
8588 /**
89+ * What an earlier answer read in g1t, as a line put before its text when
90+ * it is read back: a provider is given only the words of past answers, so
91+ * without it the model takes an answer made from tool reads for one made
92+ * up, and takes it back. Null when the answer read nothing.
93+ */
94+export function readsNote(steps: ChatAnswer["steps"]): string | null {
95+ const tools = (steps ?? []).filter((step) => step.type === "tool" && typeof step.label === "string");
96+ if (tools.length === 0) return null;
97+ const parts = tools.map((step) => {
98+ const label = String(step.label).trim();
99+ const summary = typeof step.summary === "string" ? step.summary.trim() : "";
100+ if (step.status === "failed") return `${label} (failed${summary ? `: ${summary}` : ""})`;
101+ if (step.status === "refused") return `${label} (not run)`;
102+ return summary ? `${label}: ${summary}` : label;
103+ });
104+ let note = `[g1t's tool reads for this answer: ${parts.join("; ")}]`;
105+ if (note.length > READS_NOTE_CHARS) note = `${note.slice(0, READS_NOTE_CHARS - 2)}…]`;
106+ return note;
107+}
108+
109+/**
86110 * What an answer reads: the direct message's messages after the latest
87111 * "New conversation" mark, the person's as `user` and g1t's answers as
88112 * `assistant`, oldest first, alternating, starting and ending with the
89113 * person, within Ask's limits (the newest kept). Answers that failed, the
90114 * set-up note and the answer being written are left out; several messages
91− * in a row from one side are read as one. Empty when there is nothing to
92− * answer.
115+ * in a row from one side are read as one, and each answer starts with what
116+ * it read in g1t ([`readsNote`]). Empty when there is nothing to answer.
93117 */
94118 export function conversation(rows: readonly SaidRow[], answering: string | null = null): AskMessage[] {
95119 let start = 0;
99123 const turns: AskMessage[] = [];
100124 for (const row of rows.slice(start)) {
101125 if (row.deleted_at || row.kind !== "text" || row.id === answering) continue;
102− const text = row.body.trim();
126+ let text = row.body.trim();
103127 if (!text) continue;
104128 let role: AskMessage["role"];
105129 if (row.author === G1T_KEY) {
106130 const answer = parseMeta(row.meta).answer;
107131 if (!answer || answer.setup || answer.state === "error" || answer.state === "thinking" || answer.state === "streaming") continue;
108132 role = "assistant";
133+ // First, so cutting a long answer short keeps it.
134+ const reads = readsNote(answer.steps);
135+ if (reads) text = `${reads}\n\n${text}`;
109136 } else if (row.author.startsWith("user:")) {
110137 role = "user";
111138 } else continue;
+16−1
315315 than guessing.\n\
316316 - To find a repository, use the repository tool's `list` or the search tool. Repositories are \
317317 \"owner/name\", here usually \"{slug}/…\".\n\
318+ - Keep to {slug}. Other repositories on g1t they can see (public ones, other workspaces') come into an \
319+ answer only when they name one or ask beyond the workspace; never list or mention them otherwise, and \
320+ never point out that they can see a public repository.\n\
318321 - For one pull request, read it with pull_request `get` (status, checks, reviews) and `changes` for \
319322 the diff; for one issue, issue `get`.\n\
320323 - Why a run or a deploy failed: workflow `list_runs` with `status` \"failure\" (and `workflow`, such as \
375378 what a tool returned in this answer or from the conversation. Never invent one, never write example or \
376379 placeholder rows, and never link to something a tool did not show you. When asked for a table, fill it \
377380 only with rows a tool returned. When something they name does not exist (a tool finds nothing, or says \
378− not found), say so plainly, and do not link to it."
381+ not found), say so plainly, and do not link to it. Say only what answers the question: no lists of \
382+ what you could see instead, unless they ask.\n\n\
383+ ## This conversation\n\
384+ This is their direct message with @g1t, which the Ask panel shares. g1t keeps it, and you are given its \
385+ latest {ASK_MAX_MESSAGES} messages since they last started a new conversation; you remember nothing \
386+ beyond that, and have no other memory. Each of your earlier answers begins with a bracketed note, which \
387+ g1t adds, of the tool reads it was made from: those reads happened, so never take an earlier answer back \
388+ for lacking tool calls; if one may be stale, read again. Never write such a note yourself."
379389 ));
380390 let mut place = Vec::new();
381391 if let Some(path) = page.path.as_deref().filter(|path| !path.is_empty()) {
952962 // Work is handed off, never done here.
953963 assert!(prompt.contains("never change anything in g1t yourself"));
954964 assert!(prompt.contains("Claude Code"));
965+ // It keeps to the workspace, knows what it remembers, and trusts its earlier reads.
966+ assert!(prompt.contains("Keep to acme."));
967+ assert!(prompt.contains("latest 40 messages"));
968+ assert!(prompt.contains("have no other memory"));
969+ assert!(prompt.contains("never take an earlier answer back"));
955970 let bare = system_prompt(&Context { name: "acme", ..context(&AskPage::default(), &[], false) });
956971 assert!(bare.contains("workspace acme,"));
957972 assert!(!bare.contains("## Where they are"));

Comments

Sign in to comment.