evOo-local/packages/adapters/src/coach-tools.test.ts
Claude 98b0798f60
feat(coach): main-bot coach tools and goals in its prompt
The main agent had no way to record what the user wants to achieve or how
their day went, and its prompt never showed their goals or habits.

- update_goal, update_habit, log_check_in and write_daily_brief, main-bot
  only and, like update_user_profile, withheld from shared channels and
  from runs that answer another owner's agent. Goals and habits can be
  named by id or exact title.
- The main bot's own runs list active goals and habits (with ids, done
  state and streak) in the <user> block; the basics-only block and every
  other bot never see them. The main agent role adds a short coaching
  stance and names each coach tool only when the run has it, so the
  check-in routine prompts stay tool-agnostic.
- The scripted runtime understands a few coach commands and the morning
  check-in, so tests can drive the tools end to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:20:45 +00:00

80 lines
3.2 KiB
TypeScript

import type { PrismaClient } from "@rakazo/db";
import { describe, expect, it } from "vitest";
import { COACH_TOOL_NAMES, executeCoachTool } from "./coach-tools.js";
import { selectBuiltinToolsForRun } from "./executor-tools.js";
function toolNames(options: {
mainBot?: boolean;
messagingChannelRun?: boolean;
foreignAgentRun?: boolean;
trigger?: string;
}) {
return selectBuiltinToolsForRun({
graphicalToolsAllowed: false,
groupId: null,
trigger: options.trigger ?? "user",
semanticMemoryEnabled: false,
messagingChannelRun: options.messagingChannelRun ?? false,
foreignAgentRun: options.foreignAgentRun,
bot: {
kind: options.mainBot ? "main" : "specialist",
restrictBuiltinTools: false,
allowedBuiltinTools: [],
},
}).map((tool) => tool.name);
}
/** Any database access fails the test: refusals and invalid input must not reach it. */
const untouchedPrisma = new Proxy(
{},
{
get(_target, property) {
throw new Error(`unexpected database access: ${String(property)}`);
},
},
) as PrismaClient;
const context = (kind: string) => ({ userId: "user-1", bot: { id: "bot-1", kind } });
describe("coach tools", () => {
it("are the main bot's alone, also in its check-in routines, and never for other people", () => {
for (const name of COACH_TOOL_NAMES) {
expect(toolNames({ mainBot: true })).toContain(name);
expect(toolNames({ mainBot: true, trigger: "routine" })).toContain(name);
expect(toolNames({ mainBot: false })).not.toContain(name);
expect(toolNames({ mainBot: true, messagingChannelRun: true })).not.toContain(name);
expect(toolNames({ mainBot: true, foreignAgentRun: true })).not.toContain(name);
}
});
it("refuse specialists even when they call a tool anyway", async () => {
for (const name of COACH_TOOL_NAMES) {
await expect(
executeCoachTool(untouchedPrisma, context("specialist"), name, { title: "Run" }),
).resolves.toEqual({ error: "Only the user's main bot can coach the user." });
}
});
it("reject invalid or incomplete arguments before writing", async () => {
const main = context("main");
await expect(executeCoachTool(untouchedPrisma, main, "update_goal", {})).resolves.toEqual({
error: "A new goal needs a title.",
});
await expect(
executeCoachTool(untouchedPrisma, main, "update_goal", { title: "x", target_date: "soon" }),
).resolves.toEqual({ error: expect.stringContaining("target_date") });
await expect(
executeCoachTool(untouchedPrisma, main, "update_habit", { title: "Walk", cadence: "hourly" }),
).resolves.toEqual({ error: expect.stringContaining("cadence") });
await expect(
executeCoachTool(untouchedPrisma, main, "log_check_in", { habit: "Walk", goal: "10k" }),
).resolves.toEqual({ error: "Log a habit and a goal in separate calls." });
await expect(
executeCoachTool(untouchedPrisma, main, "log_check_in", { mood: 9 }),
).resolves.toEqual({ error: expect.stringContaining("mood") });
await expect(
executeCoachTool(untouchedPrisma, main, "write_daily_brief", { text: " " }),
).resolves.toEqual({ error: expect.stringContaining("text") });
});
});