Files
famapp/src/modules/agent/tool-executor.ts
T
ginnoir d3e79edb9e
CI / checks (push) Has been cancelled
fix(agent): raise tool rounds and ease calendar creates
Local models were burning the 8-round budget on simple event adds.
Raise the limit, resolve calendars by name/default, and inject the
household clock so relative dates work.
2026-07-08 21:40:44 -05:00

541 lines
21 KiB
TypeScript

type ApiCallResult = {
status: number;
body: unknown;
};
export type ToolExecutor = (name: string, argsJson: string) => Promise<string>;
export function createApiToolExecutor(request: Request): ToolExecutor {
const origin = new URL(request.url).origin;
return async (name: string, argsJson: string) => {
const args = parseArgs(argsJson);
const result = await dispatchTool(name, args, request, origin);
return JSON.stringify(result);
};
}
function parseArgs(argsJson: string): Record<string, unknown> {
if (!argsJson.trim()) return {};
const parsed: unknown = JSON.parse(argsJson);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Tool arguments must be a JSON object");
}
return parsed as Record<string, unknown>;
}
async function dispatchTool(
name: string,
args: Record<string, unknown>,
request: Request,
origin: string,
): Promise<ApiCallResult> {
switch (name) {
case "list_lists": {
const type = typeof args.type === "string" ? args.type : undefined;
const qs = type ? `?type=${encodeURIComponent(type)}` : "";
return callApi(request, origin, "GET", `/api/v1/lists${qs}`);
}
case "list_list_items": {
const listId = requireString(args, "listId");
return callApi(request, origin, "GET", `/api/v1/lists/${listId}/items`);
}
case "add_list_item": {
const text = requireString(args, "text");
let listId = typeof args.listId === "string" ? args.listId : undefined;
if (!listId) {
const listType = typeof args.listType === "string" ? args.listType : undefined;
const listsResult = await callApi(
request,
origin,
"GET",
listType ? `/api/v1/lists?type=${encodeURIComponent(listType)}` : "/api/v1/lists",
);
if (listsResult.status !== 200 || !Array.isArray(listsResult.body)) {
return listsResult;
}
const first = listsResult.body[0] as { id?: string } | undefined;
listId = first?.id;
if (!listId) {
return { status: 404, body: { error: "No matching list found" } };
}
}
const body: Record<string, unknown> = { text };
if (typeof args.qty === "string") body.qty = args.qty;
return callApi(request, origin, "POST", `/api/v1/lists/${listId}/items`, body);
}
case "update_list_item": {
const listId = requireString(args, "listId");
const itemId = requireString(args, "itemId");
const body: Record<string, unknown> = {};
if (typeof args.done === "boolean") body.done = args.done;
if (typeof args.text === "string") body.text = args.text;
if (typeof args.qty === "string") body.qty = args.qty;
return callApi(request, origin, "PATCH", `/api/v1/lists/${listId}/items/${itemId}`, body);
}
case "delete_list_item": {
const listId = requireString(args, "listId");
const itemId = requireString(args, "itemId");
return callApi(request, origin, "DELETE", `/api/v1/lists/${listId}/items/${itemId}`);
}
case "create_list": {
const body = {
type: requireString(args, "type"),
name: requireString(args, "name"),
};
return callApi(request, origin, "POST", "/api/v1/lists", body);
}
case "update_list": {
const listId = requireString(args, "listId");
const body: Record<string, unknown> = {};
if (typeof args.name === "string") body.name = args.name;
if (typeof args.archived === "boolean") body.archived = args.archived;
return callApi(request, origin, "PATCH", `/api/v1/lists/${listId}`, body);
}
case "delete_list": {
const listId = requireString(args, "listId");
return callApi(request, origin, "DELETE", `/api/v1/lists/${listId}`);
}
case "list_calendars":
return callApi(request, origin, "GET", "/api/v1/calendars");
case "list_events": {
const from = requireString(args, "from");
const to = requireString(args, "to");
const calendarIds =
typeof args.calendarIds === "string" && args.calendarIds.length > 0
? args.calendarIds
: "all";
const qs = new URLSearchParams({ from, to, calendarIds });
return callApi(request, origin, "GET", `/api/v1/events?${qs.toString()}`);
}
case "create_event": {
const resolved = await resolveCalendarId(args, request, origin);
if ("error" in resolved) return resolved.error;
const body: Record<string, unknown> = {
calendarId: resolved.calendarId,
title: requireString(args, "title"),
startAt: requireString(args, "startAt"),
endAt: requireString(args, "endAt"),
allDay: typeof args.allDay === "boolean" ? args.allDay : false,
};
if (typeof args.location === "string") body.location = args.location;
if (typeof args.notes === "string") body.notes = args.notes;
if (typeof args.remindMinutesBefore === "number") {
body.remindMinutesBefore = args.remindMinutesBefore;
}
return callApi(request, origin, "POST", "/api/v1/events", body);
}
case "update_event": {
const eventId = requireString(args, "eventId");
const body: Record<string, unknown> = {};
if (typeof args.title === "string") body.title = args.title;
if (typeof args.startAt === "string") body.startAt = args.startAt;
if (typeof args.endAt === "string") body.endAt = args.endAt;
if (typeof args.allDay === "boolean") body.allDay = args.allDay;
if (typeof args.location === "string") body.location = args.location;
if (typeof args.notes === "string") body.notes = args.notes;
if (typeof args.remindMinutesBefore === "number") {
body.remindMinutesBefore = args.remindMinutesBefore;
}
return callApi(request, origin, "PATCH", `/api/v1/events/${eventId}`, body);
}
case "delete_event": {
const eventId = requireString(args, "eventId");
return callApi(request, origin, "DELETE", `/api/v1/events/${eventId}`);
}
case "create_calendar": {
const body: Record<string, unknown> = {
name: requireString(args, "name"),
};
if (typeof args.color === "string") body.color = args.color;
if (args.visibility === "private" || args.visibility === "household") {
body.visibility = args.visibility;
}
return callApi(request, origin, "POST", "/api/v1/calendars", body);
}
case "list_notes":
return callApi(request, origin, "GET", "/api/v1/notes");
case "create_note": {
const body: Record<string, unknown> = {
title: requireString(args, "title"),
body: typeof args.body === "string" ? args.body : "",
pinned: typeof args.pinned === "boolean" ? args.pinned : false,
};
if (typeof args.remindAt === "string") body.remindAt = args.remindAt;
return callApi(request, origin, "POST", "/api/v1/notes", body);
}
case "update_note": {
const noteId = requireString(args, "noteId");
const body: Record<string, unknown> = {};
if (typeof args.title === "string") body.title = args.title;
if (typeof args.body === "string") body.body = args.body;
if (typeof args.pinned === "boolean") body.pinned = args.pinned;
if (typeof args.remindAt === "string") body.remindAt = args.remindAt;
if (args.remindAt === null) body.remindAt = null;
return callApi(request, origin, "PATCH", `/api/v1/notes/${noteId}`, body);
}
case "delete_note": {
const noteId = requireString(args, "noteId");
return callApi(request, origin, "DELETE", `/api/v1/notes/${noteId}`);
}
case "list_journal_entries": {
const limit = typeof args.limit === "number" ? String(args.limit) : undefined;
const qs = limit ? `?limit=${encodeURIComponent(limit)}` : "";
return callApi(request, origin, "GET", `/api/v1/journal/entries${qs}`);
}
case "create_journal_entry": {
const body: Record<string, unknown> = {
recordedAt: requireString(args, "recordedAt"),
};
if (typeof args.title === "string") body.title = args.title;
if (typeof args.body === "string") body.body = args.body;
if (Array.isArray(args.moods)) body.moods = args.moods;
if (typeof args.stress === "number") body.stress = args.stress;
if (typeof args.pillsTaken === "boolean") body.pillsTaken = args.pillsTaken;
return callApi(request, origin, "POST", "/api/v1/journal/entries", body);
}
case "update_journal_entry": {
const entryId = requireString(args, "entryId");
const body: Record<string, unknown> = {};
if (typeof args.recordedAt === "string") body.recordedAt = args.recordedAt;
if (typeof args.title === "string") body.title = args.title;
if (typeof args.body === "string") body.body = args.body;
if (Array.isArray(args.moods)) body.moods = args.moods;
if (typeof args.stress === "number") body.stress = args.stress;
if (typeof args.pillsTaken === "boolean") body.pillsTaken = args.pillsTaken;
return callApi(request, origin, "PATCH", `/api/v1/journal/entries/${entryId}`, body);
}
case "delete_journal_entry": {
const entryId = requireString(args, "entryId");
return callApi(request, origin, "DELETE", `/api/v1/journal/entries/${entryId}`);
}
case "list_bangs": {
const limit = typeof args.limit === "number" ? String(args.limit) : undefined;
const qs = limit ? `?limit=${encodeURIComponent(limit)}` : "";
return callApi(request, origin, "GET", `/api/v1/bangs${qs}`);
}
case "add_bang": {
const body: Record<string, unknown> = {};
if (typeof args.occurredOn === "string") body.occurredOn = args.occurredOn;
return callApi(request, origin, "POST", "/api/v1/bangs", body);
}
case "update_bang": {
const bangId = requireString(args, "bangId");
const body = { occurredOn: requireString(args, "occurredOn") };
return callApi(request, origin, "PATCH", `/api/v1/bangs/${bangId}`, body);
}
case "delete_bang": {
const bangId = requireString(args, "bangId");
return callApi(request, origin, "DELETE", `/api/v1/bangs/${bangId}`);
}
case "list_garden_containers":
return callApi(request, origin, "GET", "/api/v1/garden/containers");
case "list_garden_plants": {
const containerId = typeof args.containerId === "string" ? args.containerId : undefined;
const qs = containerId ? `?containerId=${encodeURIComponent(containerId)}` : "";
return callApi(request, origin, "GET", `/api/v1/garden/plants${qs}`);
}
case "get_garden_plant": {
const plantId = requireString(args, "plantId");
return callApi(request, origin, "GET", `/api/v1/garden/plants/${plantId}`);
}
case "create_garden_container": {
const body: Record<string, unknown> = { name: requireString(args, "name") };
if (typeof args.type === "string") body.type = args.type;
if (typeof args.locationNotes === "string") body.locationNotes = args.locationNotes;
return callApi(request, origin, "POST", "/api/v1/garden/containers", body);
}
case "update_garden_container": {
const containerId = requireString(args, "containerId");
const body: Record<string, unknown> = {};
if (typeof args.name === "string") body.name = args.name;
if (typeof args.type === "string") body.type = args.type;
if (typeof args.locationNotes === "string") body.locationNotes = args.locationNotes;
return callApi(request, origin, "PATCH", `/api/v1/garden/containers/${containerId}`, body);
}
case "delete_garden_container": {
const containerId = requireString(args, "containerId");
return callApi(request, origin, "DELETE", `/api/v1/garden/containers/${containerId}`);
}
case "create_garden_plant": {
const body: Record<string, unknown> = { name: requireString(args, "name") };
if (typeof args.category === "string") body.category = args.category;
if (typeof args.containerId === "string") body.containerId = args.containerId;
if (typeof args.healthStatus === "string") body.healthStatus = args.healthStatus;
if (typeof args.notes === "string") body.notes = args.notes;
return callApi(request, origin, "POST", "/api/v1/garden/plants", body);
}
case "update_garden_plant": {
const plantId = requireString(args, "plantId");
const body: Record<string, unknown> = {};
if (typeof args.name === "string") body.name = args.name;
if (typeof args.category === "string") body.category = args.category;
if (typeof args.containerId === "string") body.containerId = args.containerId;
if (typeof args.healthStatus === "string") body.healthStatus = args.healthStatus;
if (typeof args.notes === "string") body.notes = args.notes;
return callApi(request, origin, "PATCH", `/api/v1/garden/plants/${plantId}`, body);
}
case "delete_garden_plant": {
const plantId = requireString(args, "plantId");
return callApi(request, origin, "DELETE", `/api/v1/garden/plants/${plantId}`);
}
case "log_garden_care": {
const plantId = requireString(args, "plantId");
const body: Record<string, unknown> = { careType: requireString(args, "careType") };
if (typeof args.notes === "string") body.notes = args.notes;
if (typeof args.performedAt === "string") body.performedAt = args.performedAt;
return callApi(request, origin, "POST", `/api/v1/garden/plants/${plantId}/care-logs`, body);
}
case "list_garden_care_logs": {
const plantId = requireString(args, "plantId");
const limit = typeof args.limit === "number" ? `?limit=${args.limit}` : "";
return callApi(request, origin, "GET", `/api/v1/garden/plants/${plantId}/care-logs${limit}`);
}
case "list_garden_care_schedules": {
const plantId = requireString(args, "plantId");
return callApi(request, origin, "GET", `/api/v1/garden/plants/${plantId}/care-schedules`);
}
case "upsert_garden_care_schedule": {
const plantId = requireString(args, "plantId");
const body: Record<string, unknown> = {
careType: requireString(args, "careType"),
intervalDays: requireNumber(args, "intervalDays"),
};
if (typeof args.enabled === "boolean") body.enabled = args.enabled;
return callApi(
request,
origin,
"POST",
`/api/v1/garden/plants/${plantId}/care-schedules`,
body,
);
}
case "delete_garden_care_schedule": {
const scheduleId = requireString(args, "scheduleId");
return callApi(request, origin, "DELETE", `/api/v1/garden/care-schedules/${scheduleId}`);
}
case "toggle_garden_care_schedule": {
const scheduleId = requireString(args, "scheduleId");
const body = { enabled: requireBoolean(args, "enabled") };
return callApi(request, origin, "PATCH", `/api/v1/garden/care-schedules/${scheduleId}`, body);
}
case "push_overdue_garden_care":
return callApi(request, origin, "POST", "/api/v1/garden/overdue-care/push");
case "schedule_garden_care_on_calendar": {
const scheduleId = requireString(args, "scheduleId");
const body: Record<string, unknown> = {
calendarId: requireString(args, "calendarId"),
};
if (typeof args.reminderMinutesBefore === "number") {
body.reminderMinutesBefore = args.reminderMinutesBefore;
}
return callApi(
request,
origin,
"POST",
`/api/v1/garden/care-schedules/${scheduleId}/calendar`,
body,
);
}
case "list_shareable_entity_types":
return callApi(request, origin, "GET", "/api/v1/share-links?entityTypes=true");
case "list_share_links": {
const qs = new URLSearchParams();
if (typeof args.entityType === "string") qs.set("entityType", args.entityType);
if (typeof args.entityId === "string") qs.set("entityId", args.entityId);
const query = qs.toString();
return callApi(
request,
origin,
"GET",
query ? `/api/v1/share-links?${query}` : "/api/v1/share-links",
);
}
case "create_share_link": {
const body: Record<string, unknown> = {
entityType: requireString(args, "entityType"),
entityId: requireString(args, "entityId"),
};
if (typeof args.expiresAt === "string") body.expiresAt = args.expiresAt;
if (typeof args.write === "boolean") {
body.capabilities = { write: args.write };
}
return callApi(request, origin, "POST", "/api/v1/share-links", body);
}
case "revoke_share_link": {
const linkId = requireString(args, "linkId");
return callApi(request, origin, "DELETE", `/api/v1/share-links/${linkId}`);
}
case "get_api_docs":
return getApiDocs(args);
case "call_api":
return callApiFallback(args, request, origin);
default:
return { status: 400, body: { error: `Unknown tool: ${name}` } };
}
}
async function resolveCalendarId(
args: Record<string, unknown>,
request: Request,
origin: string,
): Promise<{ calendarId: string } | { error: ApiCallResult }> {
const calendarId = typeof args.calendarId === "string" ? args.calendarId.trim() : "";
if (calendarId) return { calendarId };
const calendarsResult = await callApi(request, origin, "GET", "/api/v1/calendars");
if (calendarsResult.status !== 200 || !Array.isArray(calendarsResult.body)) {
return { error: calendarsResult };
}
const calendars = calendarsResult.body as Array<{ id?: string; name?: string }>;
const calendarName =
typeof args.calendarName === "string" ? args.calendarName.trim().toLowerCase() : "";
if (calendarName) {
const match = calendars.find(
(calendar) =>
typeof calendar.name === "string" && calendar.name.toLowerCase() === calendarName,
);
if (!match?.id) {
return {
error: {
status: 404,
body: {
error: `No calendar named "${args.calendarName}"`,
calendars: calendars.map((calendar) => ({ id: calendar.id, name: calendar.name })),
},
},
};
}
return { calendarId: match.id };
}
const first = calendars[0];
if (!first?.id) {
return { error: { status: 404, body: { error: "No calendars found" } } };
}
return { calendarId: first.id };
}
function requireString(args: Record<string, unknown>, key: string): string {
const value = args[key];
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`Missing required argument: ${key}`);
}
return value;
}
function requireNumber(args: Record<string, unknown>, key: string): number {
const value = args[key];
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`Missing required argument: ${key}`);
}
return value;
}
function requireBoolean(args: Record<string, unknown>, key: string): boolean {
const value = args[key];
if (typeof value !== "boolean") {
throw new Error(`Missing required argument: ${key}`);
}
return value;
}
async function getApiDocs(args: Record<string, unknown>): Promise<ApiCallResult> {
const { readFile } = await import("fs/promises");
const path = await import("path");
const specPath = path.join(process.cwd(), "docs", "api", "openapi.yaml");
const spec = await readFile(specPath, "utf8");
const search = typeof args.search === "string" ? args.search.trim().toLowerCase() : "";
if (!search) {
return {
status: 200,
body: {
spec,
hint: "Pass search to filter paths, or use call_api with a /api/v1/* path.",
},
};
}
const lines = spec.split("\n");
const matches = lines.filter((line) => line.toLowerCase().includes(search));
return {
status: 200,
body: {
search,
matchCount: matches.length,
matches: matches.slice(0, 100),
hint: "Use call_api with method and path from the matches above.",
},
};
}
async function callApiFallback(
args: Record<string, unknown>,
request: Request,
origin: string,
): Promise<ApiCallResult> {
const method = requireString(args, "method").toUpperCase();
if (!["GET", "POST", "PATCH", "DELETE"].includes(method)) {
return { status: 400, body: { error: "method must be GET, POST, PATCH, or DELETE" } };
}
let apiPath = requireString(args, "path");
if (!apiPath.startsWith("/api/v1/")) {
return { status: 400, body: { error: "path must start with /api/v1/" } };
}
if (apiPath.includes("..")) {
return { status: 400, body: { error: "invalid path" } };
}
if (typeof args.query === "object" && args.query !== null && !Array.isArray(args.query)) {
const qs = new URLSearchParams();
for (const [key, value] of Object.entries(args.query as Record<string, unknown>)) {
if (value !== undefined && value !== null) qs.set(key, String(value));
}
const query = qs.toString();
if (query) apiPath += `?${query}`;
}
const body =
typeof args.body === "object" && args.body !== null && !Array.isArray(args.body)
? (args.body as Record<string, unknown>)
: undefined;
return callApi(request, origin, method, apiPath, body);
}
async function callApi(
request: Request,
origin: string,
method: string,
path: string,
body?: Record<string, unknown>,
): Promise<ApiCallResult> {
const response = await fetch(`${origin}${path}`, {
method,
headers: {
"Content-Type": "application/json",
cookie: request.headers.get("cookie") ?? "",
},
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const text = await response.text();
let parsed: unknown = null;
if (text) {
try {
parsed = JSON.parse(text);
} catch {
parsed = text;
}
}
return { status: response.status, body: parsed };
}