Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { User } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { Audience, type AudienceInfo, type AudiencePorts, type RepoRef, WITHHELD } from "./audience.ts"; | |
| 7 | import { type MemoryViewer, type RecallPlace, changeableBy, cleanFact, memorySection, recallable, scopeFor, visibleTo } from "./memory.ts"; | |
| 8 | import { alertDue, checkPolicy, DEFAULT_POLICY, policyBlock } from "./policy.ts"; | |
| 9 | import { checkRoutine, checkSchedule, describeSchedule, nextRun } from "./schedule.ts"; | |
| 10 | import { type ActionPorts, type ToolPorts, MAX_SESSION_TOOL_CALLS, MAX_TOOL_CALLS, ToolBox } from "./tools.ts"; | |
| 11 | import { dollars } from "./money.ts"; | |
| 12 | ||
| 13 | // ── Memory: where a fact is recalled, who sees it, who changes it ─────── | |
| 14 | ||
| 15 | const dmWithAnn: RecallPlace = { channel_id: "chn_dm_ann", kind: "dm", people: ["ann"] }; | |
| 16 | const groupDm: RecallPlace = { channel_id: "chn_dm_group", kind: "dm", people: ["ann", "bob"] }; | |
| 17 | const privateOps: RecallPlace = { channel_id: "chn_ops", kind: "private", people: ["ann", "bob"] }; | |
| 18 | const publicGeneral: RecallPlace = { channel_id: "chn_general", kind: "public", people: ["ann"] }; | |
| 19 | ||
| 20 | test("a person's fact is recalled only in a direct message with that one person", () => { | |
| 21 | const fact = { scope: "person", scope_ref: "ann" }; | |
| 22 | assert.equal(recallable(fact, dmWithAnn), true); | |
| 23 | assert.equal(recallable(fact, groupDm), false, "not with Bob there too"); | |
| 24 | assert.equal(recallable(fact, privateOps), false); | |
| 25 | assert.equal(recallable(fact, publicGeneral), false); | |
| 26 | assert.equal(recallable(fact, { channel_id: "chn_dm_bob", kind: "dm", people: ["bob"] }), false); | |
| 27 | }); | |
| 28 | ||
| 29 | test("a conversation's fact stays in it; a workspace fact goes anywhere", () => { | |
| 30 | assert.equal(recallable({ scope: "channel", scope_ref: "chn_ops" }, privateOps), true); | |
| 31 | assert.equal(recallable({ scope: "channel", scope_ref: "chn_ops" }, publicGeneral), false); | |
| 32 | assert.equal(recallable({ scope: "workspace", scope_ref: "" }, dmWithAnn), true); | |
| 33 | assert.equal(recallable({ scope: "made-up", scope_ref: "" }, dmWithAnn), false); | |
| 34 | }); | |
| 35 | ||
| 36 | test("an agent remembers into the narrowest scope the conversation allows", () => { | |
| 37 | assert.deepEqual(scopeFor(dmWithAnn, null), { scope: "person", ref: "ann" }); | |
| 38 | assert.deepEqual(scopeFor(dmWithAnn, "workspace"), { scope: "person", ref: "ann" }, "a DM can't write for the workspace"); | |
| 39 | assert.deepEqual(scopeFor(privateOps, "workspace"), { scope: "channel", ref: "chn_ops" }, "nor can a private channel"); | |
| 40 | assert.deepEqual(scopeFor(groupDm, "person"), { scope: "channel", ref: "chn_dm_group" }); | |
| 41 | assert.deepEqual(scopeFor(publicGeneral, "workspace"), { scope: "workspace", ref: "" }); | |
| 42 | assert.deepEqual(scopeFor(publicGeneral, null), { scope: "channel", ref: "chn_general" }); | |
| 43 | }); | |
| 44 | ||
| 45 | test("people see what could be recalled for them; owners don't read others' facts", () => { | |
| 46 | const owner: MemoryViewer = { id: "olga", owner: true, inChannel: () => false }; | |
| 47 | const ann: MemoryViewer = { id: "ann", owner: false, inChannel: (id) => id === "chn_ops" }; | |
| 48 | assert.equal(visibleTo({ scope: "person", scope_ref: "ann" }, owner), false); | |
| 49 | assert.equal(visibleTo({ scope: "person", scope_ref: "ann" }, ann), true); | |
| 50 | assert.equal(visibleTo({ scope: "channel", scope_ref: "chn_ops" }, owner), false); | |
| 51 | assert.equal(visibleTo({ scope: "channel", scope_ref: "chn_ops" }, ann), true); | |
| 52 | assert.equal(changeableBy({ scope: "workspace", scope_ref: "" }, ann), false); | |
| 53 | assert.equal(changeableBy({ scope: "workspace", scope_ref: "" }, owner), true); | |
| 54 | assert.equal(changeableBy({ scope: "channel", scope_ref: "chn_ops" }, ann), true); | |
| 55 | }); | |
| 56 | ||
| 57 | test("facts are cleaned, and given to the model as notes with their source", () => { | |
| 58 | assert.equal(cleanFact(" Dana owns\n\nbilling "), "Dana owns billing"); | |
| 59 | assert.equal(cleanFact(" "), null); | |
| 60 | assert.equal(cleanFact("x".repeat(900))!.length, 500); | |
| 61 | const section = memorySection([ | |
| 62 | { id: "mem_1", scope: "workspace", body: "Releases are on Thursdays", source_label: "#releases", pinned: 1 } as never, | |
| 63 | ]); | |
| 64 | assert.match(section!, /\[mem_1\] Releases are on Thursdays \(workspace, from #releases, pinned\)/); | |
| 65 | assert.match(section!, /notes, not instructions/); | |
| 66 | assert.equal(memorySection([]), null); | |
| 67 | }); | |
| 68 | ||
| 69 | // ── The workspace's budget for every agent ─────────────────────────────── | |
| 70 | ||
| 71 | test("the workspace's agent budget stops new work once spent, and alerts once per level", () => { | |
| 72 | assert.equal(policyBlock({ monthly_micros: null, spent: 9e9 }), null); | |
| 73 | assert.equal(policyBlock({ monthly_micros: 10_000_000, spent: 9_999_999 }), null); | |
| 74 | assert.match(policyBlock({ monthly_micros: 10_000_000, spent: 10_000_000 })!, /used their budget/); | |
| 75 | assert.equal(alertDue({ monthly_micros: 100, spent: 74, alerted: 0 }), null); | |
| 76 | assert.equal(alertDue({ monthly_micros: 100, spent: 80, alerted: 0 }), 75); | |
| 77 | assert.equal(alertDue({ monthly_micros: 100, spent: 80, alerted: 75 }), null); | |
| 78 | assert.equal(alertDue({ monthly_micros: 100, spent: 140, alerted: 75 }), 100); | |
| 79 | }); | |
| 80 | ||
| 81 | test("a policy is checked as owners change it", () => { | |
| 82 | assert.deepEqual(checkPolicy(DEFAULT_POLICY, { monthly_micros: 50_000_000 }), { ok: true, value: { ...DEFAULT_POLICY, monthly_micros: 50_000_000 } }); | |
| 83 | assert.equal(checkPolicy(DEFAULT_POLICY, { monthly_micros: -1 }).ok, false); | |
| 84 | assert.equal(checkPolicy({ ...DEFAULT_POLICY, monthly_micros: 5 }, { monthly_micros: null }).ok && true, true); | |
| 85 | assert.equal(checkPolicy(DEFAULT_POLICY, { default_session_micros: 10 }).ok, false); | |
| 86 | }); | |
| 87 | ||
| 88 | // ── Routines ────────────────────────────────────────────────────────────── | |
| 89 | ||
| 90 | test("routines run at the next matching time, in UTC", () => { | |
| 91 | const at = (s: string) => new Date(s); | |
| 92 | // Wednesday 2026-10-07 10:30 UTC. | |
| 93 | const now = at("2026-10-07T10:30:00Z"); | |
| 94 | assert.equal(nextRun({ every: "hour", minute: 15, hour: 0, weekday: 0 }, now).toISOString(), "2026-10-07T11:15:00.000Z"); | |
| 95 | assert.equal(nextRun({ every: "hour", minute: 45, hour: 0, weekday: 0 }, now).toISOString(), "2026-10-07T10:45:00.000Z"); | |
| 96 | assert.equal(nextRun({ every: "day", minute: 0, hour: 9, weekday: 0 }, now).toISOString(), "2026-10-08T09:00:00.000Z"); | |
| 97 | assert.equal(nextRun({ every: "day", minute: 0, hour: 12, weekday: 0 }, now).toISOString(), "2026-10-07T12:00:00.000Z"); | |
| 98 | // Friday evening: the next weekday is Monday. | |
| 99 | assert.equal(nextRun({ every: "weekday", minute: 0, hour: 9, weekday: 0 }, at("2026-10-09T18:00:00Z")).toISOString(), "2026-10-12T09:00:00.000Z"); | |
| 100 | assert.equal(nextRun({ every: "week", minute: 30, hour: 8, weekday: 1 }, now).toISOString(), "2026-10-12T08:30:00.000Z"); | |
| 101 | assert.equal(describeSchedule({ every: "weekday", minute: 5, hour: 9, weekday: 0 }), "Every weekday at 09:05 UTC"); | |
| 102 | }); | |
| 103 | ||
| 104 | test("routines and schedules are checked", () => { | |
| 105 | assert.equal(checkSchedule({ every: "fortnight" }).ok, false); | |
| 106 | assert.equal(checkSchedule({ every: "day", hour: 24 }).ok, false); | |
| 107 | assert.deepEqual(checkSchedule({ every: "day" }), { ok: true, value: { every: "day", minute: 0, hour: 9, weekday: 1 } }); | |
| 108 | assert.equal(checkRoutine({ name: "", instructions: "Summarise support", schedule: { every: "day", minute: 0, hour: 9, weekday: 1 }, channel_id: "c" }).ok, false); | |
| 109 | assert.equal(checkRoutine({ name: "Digest", instructions: "short", schedule: { every: "day", minute: 0, hour: 9, weekday: 1 }, channel_id: "c" }).ok, false); | |
| 110 | assert.equal(checkRoutine({ name: "Digest", instructions: "Summarise this week's support themes", schedule: { every: "day", minute: 0, hour: 9, weekday: 1 }, channel_id: "c" }).ok, true); | |
| 111 | }); | |
| 112 | ||
| 113 | test("money reads as people expect", () => { | |
| 114 | assert.equal(dollars(140_000), "$0.14"); | |
| 115 | assert.equal(dollars(2_000_000), "$2.00"); | |
| 116 | assert.equal(dollars(3_000), "<$0.01"); | |
| 117 | assert.equal(dollars(1_204_000_000), "$1,204"); | |
| 118 | }); | |
| 119 | ||
| 120 | // ── Tools that act ──────────────────────────────────────────────────────── | |
| 121 | ||
| 122 | const WEB: RepoRef = { id: "rep_web", namespace: "acme", name: "web", isPrivate: true, defaultBranch: "main" }; | |
| 123 | const readPorts = { | |
| 124 | readFile: async () => null, | |
| 125 | searchCode: async () => [], | |
| 126 | listIssues: async () => [], | |
| 127 | getIssue: async () => null, | |
| 128 | getPull: async () => null, | |
| 129 | recentPulls: async () => [], | |
| 130 | searchMessages: async () => [], | |
| 131 | readThread: async () => null, | |
| 132 | roster: async () => "", | |
| 133 | consult: async () => ({ ok: false as const, message: "no" }), | |
| 134 | } satisfies ToolPorts; | |
| 135 | ||
| 136 | function audienceWorld(info: AudienceInfo, people: User[], reads: Record<string, string[]>): AudiencePorts { | |
| 137 | return { | |
| 138 | info: async () => info, | |
| 139 | users: async (ids) => people.filter((u) => ids.includes(u.id)), | |
| 140 | workspaceRepos: async (viewer) => (reads[viewer.id] ?? []).includes(WEB.id) ? [WEB] : [], | |
| 141 | readable: async (ids, viewer) => (ids.includes(WEB.id) && (reads[viewer.id] ?? []).includes(WEB.id) ? [WEB] : []), | |
| 142 | }; | |
| 143 | } | |
| 144 | ||
| 145 | const member = (id: string, code = true): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member", code_access: code }] }) as User; | |
| 146 | ||
| 147 | function actions(log: string[]): ActionPorts { | |
| 148 | return { | |
| 149 | remember: async (body) => (log.push(`remember:${body}`), { ok: true, message: "ok" }), | |
| 150 | forget: async () => ({ ok: true, message: "ok" }), | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 151 | draftIssue: async (repo, input) => (log.push(`draft:${repo.name}:${input.title}`), { ok: true, message: "drafted" }), |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 152 | startSession: async (title) => (log.push(`session:${title}`), { ok: true, message: "started" }), |
| 153 | postUpdate: async () => ({ ok: true, message: "posted" }), | |
| 154 | useSubagent: async () => ({ ok: true, message: "on it" }), | |
| 155 | bringIn: async () => ({ ok: true, message: "on it" }), | |
| 156 | }; | |
| 157 | } | |
| 158 | ||
| 159 | const ctx = { agentId: "agt_me", notConsult: ["me"], hops: 0, maxHops: 6 }; | |
| 160 | ||
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 161 | test("a reply can spin off a session and draft an issue; a session can't spin off", async () => { |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 162 | const log: string[] = []; |
| 163 | const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] })); | |
| 164 | const reply = new ToolBox(audience, readPorts, ctx, [], actions(log)); | |
| 165 | const names = reply.definitions().map((t) => t.name); | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 166 | assert.ok(names.includes("start_session") && names.includes("draft_issue") && names.includes("remember")); |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 167 | assert.ok(!names.includes("bring_in") && !names.includes("use_subagent") && !names.includes("post_update")); |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 168 | const drafted = await reply.run("draft_issue", { repo: "web", title: "CSV export times out", body: "Over 100k rows." }); |
| 169 | assert.equal(drafted.outcome, "allowed"); | |
| 170 | assert.deepEqual(log, ["draft:web:CSV export times out"]); | |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 171 | // A repository the audience can't read is withheld, never named. |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 172 | const hidden = await reply.run("draft_issue", { repo: "acme/secret", title: "x", body: "y" }); |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 173 | assert.equal(hidden.text, WITHHELD); |
| 174 | ||
| 175 | const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], actions(log)); | |
| 176 | const inSession = session.definitions().map((t) => t.name); | |
| 177 | assert.ok(!inSession.includes("start_session")); | |
| 178 | assert.ok(inSession.includes("bring_in") && inSession.includes("use_subagent") && inSession.includes("post_update")); | |
| 179 | assert.equal((await session.run("start_session", { title: "x", goal: "y" })).outcome, "refused"); | |
| 180 | assert.equal(reply.maxCalls, MAX_TOOL_CALLS); | |
| 181 | assert.equal(session.maxCalls, MAX_SESSION_TOOL_CALLS); | |
| 182 | }); | |
| 183 | ||
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 184 | test("nobody drafts issues for someone who can't read code, and no hand-offs at the hop limit", async () => { |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 185 | const log: string[] = []; |
| 186 | const noCode = await Audience.build("acme", "cal", audienceWorld({ kind: "dm", member_user_ids: ["cal"], member_count: 1 }, [member("cal", false)], { cal: [WEB.id] })); | |
| 187 | const box = new ToolBox(noCode, readPorts, ctx, [], actions(log)); | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 188 | assert.ok(!box.definitions().some((t) => t.name === "draft_issue")); |
| 189 | assert.equal((await box.run("draft_issue", { repo: "web", title: "x", body: "y" })).outcome, "refused"); | |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 190 | assert.deepEqual(log, []); |
| 191 | const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] })); | |
| 192 | const atLimit = new ToolBox(audience, readPorts, { ...ctx, session: true, hops: 6 }, [], actions(log)); | |
| 193 | assert.ok(!atLimit.definitions().some((t) => t.name === "bring_in" || t.name === "use_subagent")); | |
| 194 | // The agent can't bring itself in. | |
| 195 | const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], actions(log)); | |
| 196 | assert.equal((await session.run("bring_in", { handle: "@me", brief: "help" })).outcome, "refused"); | |
| 197 | }); | |
| 198 | ||
| 199 | test("updates are limited per step", async () => { | |
| 200 | const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] })); | |
| 201 | const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], actions([])); | |
| 202 | for (let i = 0; i < 3; i++) assert.equal((await session.run("post_update", { text: `step ${i}` })).outcome, "allowed"); | |
| 203 | assert.equal((await session.run("post_update", { text: "again" })).outcome, "refused"); | |
| 204 | }); | |
| Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened | 205 | |
| 206 | // ── Routines that run when something happens ───────────────────────────── | |
| 207 | ||
| 208 | import { describeEvents, suggestRoutines } from "./suggest.ts"; | |
| 209 | import { checkRoutine as checkEventRoutine } from "./schedule.ts"; | |
| 210 | ||
| 211 | test("Margo's responsibilities suggest the routines that bind to events", () => { | |
| 212 | const margo = ["Reviewing pull requests for risk and test coverage", "Test plans for new features", "Chasing flaky checks"]; | |
| 213 | const suggested = suggestRoutines(margo, []); | |
| 214 | assert.deepEqual( | |
| 215 | suggested.map((s) => [s.routine.name, s.routine.events]), | |
| 216 | [ | |
| 217 | ["Review pull requests", ["pull_ready"]], | |
| 218 | ["Test plans for new work", ["issue_opened"]], | |
| 219 | ["Chase failing checks", ["checks_failed"]], | |
| 220 | ], | |
| 221 | ); | |
| 222 | assert.match(suggested[0].routine.instructions, /risk and test coverage/); | |
| 223 | // What it already runs on isn't suggested again. | |
| 224 | assert.deepEqual( | |
| 225 | suggestRoutines(margo, [{ name: "PR reviews", events: ["pull_ready"] }]).map((s) => s.routine.name), | |
| 226 | ["Test plans for new work", "Chase failing checks"], | |
| 227 | ); | |
| 228 | assert.deepEqual(suggestRoutines(["Keep the office plants alive"], []), []); | |
| 229 | }); | |
| 230 | ||
| 231 | test("an event routine needs no schedule, but a routine needs one or the other", () => { | |
| 232 | const base = { name: "Review", instructions: "Review pull requests for risk", channel_id: "chn_qa" }; | |
| 233 | assert.equal(checkEventRoutine({ ...base, schedule: null, events: ["pull_ready"] }).ok, true); | |
| 234 | assert.equal(checkEventRoutine({ ...base, schedule: null, events: [] }).ok, false); | |
| 235 | assert.equal(checkEventRoutine({ ...base, schedule: null, events: ["pull_exploded" as never] }).ok, false); | |
| 236 | assert.equal(checkEventRoutine({ ...base, schedule: null, events: ["pull_ready"], repos: ["not a repo"] }).ok, false); | |
| 237 | const ok = checkEventRoutine({ ...base, schedule: null, events: ["pull_ready", "pull_ready"], repos: ["Acme/Web"] }); | |
| 238 | assert.ok(ok.ok && ok.value.events.length === 1 && ok.value.repos[0] === "acme/web"); | |
| 239 | assert.equal(describeEvents(["pull_ready", "checks_failed"]), "When a pull request is ready for review or checks fail on a pull request"); | |
| 240 | }); | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 241 | |
| 242 | test("agents comment and review on issues and pull requests only where the asker can read", async () => { | |
| 243 | const log: string[] = []; | |
| 244 | const ports = { | |
| 245 | ...actions(log), | |
| 246 | comment: async (repo: RepoRef, asker: User, number: number) => (log.push(`comment:${repo.name}#${number}:${asker.username}`), { ok: true, message: "ok" }), | |
| 247 | review: async (repo: RepoRef, asker: User, number: number, verdict: string) => (log.push(`review:${repo.name}#${number}:${verdict}:${asker.username}`), { ok: true, message: "ok" }), | |
| 248 | }; | |
| 249 | const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] })); | |
| 250 | const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], ports); | |
| 251 | assert.ok(session.definitions().some((t) => t.name === "review_pull")); | |
| 252 | await session.run("review_pull", { repo: "web", number: 12, verdict: "approve", body: "" }); | |
| 253 | await session.run("review_pull", { repo: "web", number: 12, verdict: "made-up", body: "Looks risky" }); | |
| 254 | await session.run("comment", { repo: "web", number: 3, body: "Test plan: …" }); | |
| 255 | assert.equal((await session.run("comment", { repo: "acme/secret", number: 3, body: "x" })).text, WITHHELD); | |
| 256 | assert.deepEqual(log, ["review:web#12:approve:ann", "review:web#12:comment:ann", "comment:web#3:ann"]); | |
| 257 | const noCode = await Audience.build("acme", "cal", audienceWorld({ kind: "dm", member_user_ids: ["cal"], member_count: 1 }, [member("cal", false)], { cal: [WEB.id] })); | |
| 258 | assert.ok(!new ToolBox(noCode, readPorts, { ...ctx, session: true }, [], ports).definitions().some((t) => t.name === "comment" || t.name === "review_pull")); | |
| 259 | }); |