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
283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
import type { AgentRunRequest } from "@rakazo/adapter-kit";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import type * as ComputerLifecycleModule from "./computer-lifecycle.js";
|
|
import { createRunExecutor, MAIN_ONLY_BUILTIN_TOOLS } from "./executor.js";
|
|
|
|
vi.mock("./computer-lifecycle.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof ComputerLifecycleModule>()),
|
|
acquireComputerExecutionLease: async () => null,
|
|
provisionComputer: async () => ({ id: "computer-1", kind: "desktop" }),
|
|
}));
|
|
|
|
type BotPolicy = { kind?: string; restrictBuiltinTools?: boolean; allowedBuiltinTools?: string[] };
|
|
type Call = { name: string; args?: Record<string, unknown> };
|
|
type Effect = { id: string; status: string; idempotencyKey: string } & Record<string, unknown>;
|
|
|
|
/** In-memory external effects, enough for the idempotency record of a mutating builtin tool. */
|
|
function effectStore() {
|
|
const effects: Effect[] = [];
|
|
const find = (where: { id?: string; idempotencyKey?: string }) =>
|
|
effects.find((effect) =>
|
|
where.id ? effect.id === where.id : effect.idempotencyKey === where.idempotencyKey,
|
|
);
|
|
return {
|
|
findMany: vi.fn(async () => []),
|
|
findUnique: vi.fn(async ({ where }: { where: { id?: string; idempotencyKey?: string } }) => {
|
|
const effect = find(where);
|
|
return effect ? { ...effect } : null;
|
|
}),
|
|
create: vi.fn(async ({ data }: { data: Omit<Effect, "id"> }) => {
|
|
const effect = { ...data, id: `effect-${effects.length + 1}` } as Effect;
|
|
effects.push(effect);
|
|
return { ...effect };
|
|
}),
|
|
update: vi.fn(async ({ where, data }: { where: { id: string }; data: Partial<Effect> }) => {
|
|
Object.assign(find(where) ?? {}, data);
|
|
}),
|
|
updateMany: vi.fn(
|
|
async ({ where, data }: { where: { id: string; status: string }; data: Partial<Effect> }) => {
|
|
const effect = find(where);
|
|
if (!effect || effect.status !== where.status) return { count: 0 };
|
|
Object.assign(effect, data);
|
|
return { count: 1 };
|
|
},
|
|
),
|
|
};
|
|
}
|
|
|
|
async function runBot(policy: BotPolicy, calls: Call[] = [], models: Record<string, unknown> = {}) {
|
|
const run = {
|
|
id: "run-1",
|
|
botId: "bot-1",
|
|
threadId: "thread-1",
|
|
taskId: "task-1",
|
|
spaceId: "space-1",
|
|
userId: "user-1",
|
|
status: "queued",
|
|
trigger: "user",
|
|
leaseFence: 0,
|
|
};
|
|
const routineFindMany = vi.fn(async () => []);
|
|
const prisma = {
|
|
run: {
|
|
findUnique: vi.fn(async () => run),
|
|
findUniqueOrThrow: vi.fn(async () => run),
|
|
updateMany: vi.fn(async ({ data }: { data: Record<string, unknown> }) => {
|
|
Object.assign(run, data);
|
|
return { count: 1 };
|
|
}),
|
|
},
|
|
bot: {
|
|
findUniqueOrThrow: vi.fn(async () => ({
|
|
id: run.botId,
|
|
name: "Assistant",
|
|
title: "Assistant",
|
|
description: "Test assistant",
|
|
computerId: "computer-1",
|
|
computer: { id: "computer-1", scope: "dedicated" },
|
|
kind: "specialist",
|
|
restrictBuiltinTools: false,
|
|
allowedBuiltinTools: [],
|
|
...policy,
|
|
})),
|
|
findMany: vi.fn(async () => []),
|
|
},
|
|
attempt: {
|
|
create: vi.fn(async () => ({ id: "attempt-1" })),
|
|
update: vi.fn(),
|
|
updateMany: vi.fn(async () => ({ count: 1 })),
|
|
},
|
|
thread: { findUniqueOrThrow: vi.fn(async () => ({ id: run.threadId, groupId: null })) },
|
|
message: { findMany: vi.fn(async () => []) },
|
|
task: { findUniqueOrThrow: vi.fn(async () => ({ id: run.taskId, prompt: "Work" })) },
|
|
connection: { findMany: vi.fn(async () => []) },
|
|
spaceModelPreference: { findFirst: vi.fn(async () => null) },
|
|
userModelCredential: { findFirst: vi.fn(async () => null) },
|
|
deploymentSettings: {
|
|
findUnique: vi.fn(async () => ({
|
|
defaultModelProvider: "scripted",
|
|
defaultModelId: "scripted",
|
|
})),
|
|
},
|
|
taughtSkill: { findMany: vi.fn(async () => []) },
|
|
agentSecret: { findMany: vi.fn(async () => []) },
|
|
agentSkill: { findMany: vi.fn(async () => []) },
|
|
scratchpadItem: { findMany: vi.fn(async () => []) },
|
|
actionApprovalRule: { findMany: vi.fn(async () => []) },
|
|
actionAutoReviewPreference: { findUnique: vi.fn(async () => ({ enabled: false })) },
|
|
externalEffect: effectStore(),
|
|
routine: { findMany: routineFindMany },
|
|
// The user block every run states.
|
|
user: { findUnique: vi.fn(async () => ({ name: "Ada" })) },
|
|
userProfile: { findUnique: vi.fn(async () => null) },
|
|
userFact: { findMany: vi.fn(async () => []) },
|
|
// The main bot's goals and habits.
|
|
userGoal: { findMany: vi.fn(async () => []) },
|
|
habit: { findMany: vi.fn(async () => []) },
|
|
...models,
|
|
};
|
|
let request: AgentRunRequest | undefined;
|
|
const results: unknown[] = [];
|
|
const jobs = { enqueue: vi.fn(async () => undefined) };
|
|
const executor = createRunExecutor({
|
|
prisma,
|
|
runtime: {
|
|
describe: () => ({ capabilities: { scripted: false } }),
|
|
run: async function* (next: AgentRunRequest) {
|
|
request = next;
|
|
for (const [index, call] of calls.entries()) {
|
|
results.push(await next.executeTool!(call.name, call.args ?? {}, `call-${index}`));
|
|
}
|
|
yield { type: "done" as const, text: "Done" };
|
|
},
|
|
},
|
|
sandbox: { describe: () => ({ capabilities: { graphical: false } }) },
|
|
memory: { read: async () => ({ documents: [] }) },
|
|
memoryProviders: { resolve: async () => null },
|
|
events: {
|
|
append: vi.fn(async () => undefined),
|
|
pauseRunForInput: vi.fn(async () => true),
|
|
finalizeRun: vi.fn(async () => ({ continuationRunId: null })),
|
|
},
|
|
jobs,
|
|
secrets: [],
|
|
} as unknown as Parameters<typeof createRunExecutor>[0]);
|
|
await executor.continueRun(run.id, "worker-1");
|
|
if (!request) throw new Error("the runtime was never started");
|
|
const tools = request.tools;
|
|
return { names: tools.map((tool) => tool.name), tools, results, routineFindMany };
|
|
}
|
|
|
|
describe("bot capabilities in runs", () => {
|
|
it("exposes every builtin to an unrestricted bot", async () => {
|
|
const { names } = await runBot({});
|
|
expect(names).toEqual(expect.arrayContaining(["write_file", "shell", "schedule_list"]));
|
|
});
|
|
|
|
it("exposes only the allowlisted builtins and refuses calls to any other", async () => {
|
|
const { names, results, routineFindMany } = await runBot(
|
|
{ restrictBuiltinTools: true, allowedBuiltinTools: ["schedule_list", "web_search"] },
|
|
[
|
|
{ name: "write_file", args: { path: "notes/x.txt", content: "nope" } },
|
|
{ name: "schedule_list" },
|
|
],
|
|
);
|
|
expect(names).toEqual(["web_search", "schedule_list"]);
|
|
expect(results[0]).toEqual({ error: "Tool write_file is not available in this run." });
|
|
expect(results[1]).toEqual({ routines: [] });
|
|
expect(routineFindMany).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("exposes no builtins when the allowlist is empty", async () => {
|
|
const { names, results } = await runBot({ restrictBuiltinTools: true }, [
|
|
{ name: "schedule_list" },
|
|
]);
|
|
expect(names).toEqual([]);
|
|
expect(results[0]).toEqual({ error: "Tool schedule_list is not available in this run." });
|
|
});
|
|
|
|
it("refuses a tool no run ever exposes", async () => {
|
|
const { results } = await runBot({}, [{ name: "made_up_tool" }]);
|
|
expect(results[0]).toEqual({ error: "Tool made_up_tool is not available in this run." });
|
|
});
|
|
|
|
it("offers schedule_create's bot option to the main bot only", async () => {
|
|
const properties = async (policy: BotPolicy) =>
|
|
Object.keys(
|
|
((await runBot(policy)).tools.find((tool) => tool.name === "schedule_create")?.inputSchema
|
|
.properties ?? {}) as object,
|
|
);
|
|
expect(await properties({ kind: "main" })).toContain("bot");
|
|
expect(await properties({})).not.toContain("bot");
|
|
});
|
|
|
|
it("withholds main-only tools from every other bot", async () => {
|
|
const mainOnly = MAIN_ONLY_BUILTIN_TOOLS as Set<string>;
|
|
// No main-only tool ships yet in this package; schedule_list stands in for one.
|
|
mainOnly.add("schedule_list");
|
|
try {
|
|
const specialist = await runBot({}, [{ name: "schedule_list" }]);
|
|
expect(specialist.names).not.toContain("schedule_list");
|
|
expect(specialist.results[0]).toEqual({
|
|
error: "Tool schedule_list is not available in this run.",
|
|
});
|
|
const main = await runBot({ kind: "main" }, [{ name: "schedule_list" }]);
|
|
expect(main.names).toContain("schedule_list");
|
|
expect(main.results[0]).toEqual({ routines: [] });
|
|
} finally {
|
|
mainOnly.delete("schedule_list");
|
|
}
|
|
});
|
|
|
|
it("schedules for a named specialist when the main bot asks, in that bot's space and thread", async () => {
|
|
const routineCreate = vi.fn(async ({ data }: { data: Record<string, unknown> }) => ({
|
|
id: "routine-1",
|
|
...data,
|
|
}));
|
|
const { results } = await runBot(
|
|
{ kind: "main" },
|
|
[
|
|
{
|
|
name: "schedule_create",
|
|
args: { name: "Digest", prompt: "Summarize", delayMinutes: 5, bot: "Atlas" },
|
|
},
|
|
],
|
|
{
|
|
bot: {
|
|
findUniqueOrThrow: vi.fn(async () => ({
|
|
id: "bot-1",
|
|
name: "Chief",
|
|
title: "",
|
|
description: "",
|
|
computerId: "computer-1",
|
|
computer: { id: "computer-1", scope: "dedicated" },
|
|
kind: "main",
|
|
restrictBuiltinTools: false,
|
|
allowedBuiltinTools: [],
|
|
})),
|
|
findMany: vi.fn(async () => [
|
|
{
|
|
id: "atlas-1",
|
|
name: "Atlas",
|
|
title: "",
|
|
description: "",
|
|
spaceId: "space-2",
|
|
space: { name: "Studio" },
|
|
thread: { id: "atlas-dm" },
|
|
},
|
|
]),
|
|
},
|
|
chatGroup: { findFirst: vi.fn(async () => null) },
|
|
routine: { findMany: vi.fn(async () => []), create: routineCreate },
|
|
},
|
|
);
|
|
expect(results[0]).toMatchObject({ ok: true, routineId: "routine-1", bot: "Atlas" });
|
|
expect(routineCreate).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
spaceId: "space-2",
|
|
botId: "atlas-1",
|
|
threadId: "atlas-dm",
|
|
createdByBotId: "bot-1",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("refuses the bot option from any other bot", async () => {
|
|
const routineCreate = vi.fn();
|
|
const { results } = await runBot(
|
|
{},
|
|
[
|
|
{
|
|
name: "schedule_create",
|
|
args: { name: "x", prompt: "y", delayMinutes: 5, bot: "Atlas" },
|
|
},
|
|
],
|
|
{ routine: { findMany: vi.fn(async () => []), create: routineCreate } },
|
|
);
|
|
expect(results[0]).toEqual({ error: "Only the main agent can schedule for another bot." });
|
|
expect(routineCreate).not.toHaveBeenCalled();
|
|
});
|
|
});
|