Files
maestro/src/engine/tools/calendar.ts
T
oss-sync b857c33ef6
CI / build-and-test (push) Has been cancelled
sync: update from private repo (f6d625db)
2026-06-26 03:35:45 +00:00

194 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ToolDef } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult } from './core.js';
import type { Repository } from '../../db/repository.js';
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;
const MAX_TITLE_LEN = 200;
const MAX_DESC_LEN = 4000;
let _repo: Repository | null = null;
export function setCalendarRepo(repo: Repository | null): void {
_repo = repo;
}
const ADD_CALENDAR_EVENT_DEF: ToolDef = {
type: 'function',
function: {
name: 'AddCalendarEvent',
description:
'実行中タスクのスペースのカレンダーに予定を1件追加する(created_by=agent)。詳細は ReadToolDoc({ name: "AddCalendarEvent" }) で取得可能。',
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: '予定のタイトル(必須)' },
date: { type: 'string', description: '開始日 YYYY-MM-DD(ローカル日付、必須)' },
end_date: { type: 'string', description: '終了日 YYYY-MM-DD。複数日にまたがる予定のとき指定。省略すると単日' },
time: { type: 'string', description: '開始 HH:MM。省略すると終日扱い' },
end_time: { type: 'string', description: '終了 HH:MM。time(開始)があるときのみ有効。単日は開始以降のみ' },
description: { type: 'string', description: '予定の補足説明(任意)' },
},
required: ['title', 'date'],
},
},
};
const LIST_CALENDAR_EVENTS_DEF: ToolDef = {
type: 'function',
function: {
name: 'ListCalendarEvents',
description:
'実行中タスクのスペースのカレンダー予定を日付範囲で一覧する。詳細は ReadToolDoc({ name: "AddCalendarEvent" }) で取得可能。',
parameters: {
type: 'object',
properties: {
from: { type: 'string', description: '範囲開始 YYYY-MM-DD(任意)' },
to: { type: 'string', description: '範囲終了 YYYY-MM-DD(任意)' },
},
required: [],
},
},
};
export const TOOL_DEFS: Record<string, ToolDef> = {
AddCalendarEvent: ADD_CALENDAR_EVENT_DEF,
ListCalendarEvents: LIST_CALENDAR_EVENTS_DEF,
};
export async function executeTool(
name: string,
input: Record<string, unknown>,
ctx: ToolContext,
): Promise<ToolResult | null> {
if (name === 'AddCalendarEvent') return executeAddCalendarEvent(input, ctx);
if (name === 'ListCalendarEvents') return executeListCalendarEvents(input, ctx);
return null;
}
async function executeAddCalendarEvent(
input: Record<string, unknown>,
ctx: ToolContext,
): Promise<ToolResult> {
if (!_repo) {
return { output: 'Calendar repo is not initialized', isError: true };
}
const spaceId = ctx.spaceId;
if (!spaceId) {
return {
output:
'このタスクはスペースに属していないため予定を追加できません(旧 local タスクには spaceId がありません)',
isError: true,
};
}
const title = input['title'];
const date = input['date'];
const endDateInput = input['end_date'];
const time = input['time'];
const endTimeInput = input['end_time'];
const description = input['description'];
if (typeof title !== 'string' || !title.trim()) {
return { output: 'title は必須です', isError: true };
}
if (title.length > MAX_TITLE_LEN) {
return { output: `title が長すぎます(最大 ${MAX_TITLE_LEN} 文字)`, isError: true };
}
if (typeof date !== 'string' || !DATE_PATTERN.test(date)) {
return { output: 'date は YYYY-MM-DD 形式で指定してください', isError: true };
}
let endDate: string | null = null;
if (endDateInput !== undefined && endDateInput !== null && endDateInput !== '') {
if (typeof endDateInput !== 'string' || !DATE_PATTERN.test(endDateInput)) {
return { output: 'end_date は YYYY-MM-DD 形式で指定してください', isError: true };
}
if (endDateInput < date) {
return { output: 'end_date は date 以降の日付にしてください', isError: true };
}
endDate = endDateInput;
}
const startTime = typeof time === 'string' && time !== '' ? time : null;
if (time !== undefined && time !== null && time !== '') {
if (typeof time !== 'string' || !TIME_PATTERN.test(time)) {
return { output: 'time は HH:MM 形式で指定してください', isError: true };
}
}
let endTime: string | null = null;
if (endTimeInput !== undefined && endTimeInput !== null && endTimeInput !== '') {
if (typeof endTimeInput !== 'string' || !TIME_PATTERN.test(endTimeInput)) {
return { output: 'end_time は HH:MM 形式で指定してください', isError: true };
}
if (startTime == null) {
return { output: 'end_time は time(開始時刻)とあわせて指定してください', isError: true };
}
const multiDay = endDate != null && endDate > date;
if (!multiDay && endTimeInput < startTime) {
return { output: 'end_time は開始時刻以降にしてください(単日の予定)', isError: true };
}
endTime = endTimeInput;
}
if (description !== undefined && description !== null) {
if (typeof description !== 'string') {
return { output: 'description は文字列で指定してください', isError: true };
}
if (description.length > MAX_DESC_LEN) {
return { output: `description が長すぎます(最大 ${MAX_DESC_LEN} 文字)`, isError: true };
}
}
const sourceTaskId = ctx.taskId ? Number(ctx.taskId) : null;
const ev = await _repo.createCalendarEvent({
spaceId,
ownerId: ctx.ownerId ?? null,
date,
endDate,
time: startTime,
endTime,
title: title.trim(),
description: typeof description === 'string' ? description : null,
createdBy: 'agent',
sourceTaskId: Number.isFinite(sourceTaskId) ? sourceTaskId : null,
});
const timeLabel = ev.time ? (ev.endTime ? `${ev.time}–${ev.endTime}` : ev.time) : '(終日)';
return {
output: `予定を追加しました: ${ev.date}${ev.endDate ? `–${ev.endDate}` : ''} ${timeLabel} 「${ev.title}」(id=${ev.id})`,
isError: false,
};
}
async function executeListCalendarEvents(
input: Record<string, unknown>,
ctx: ToolContext,
): Promise<ToolResult> {
if (!_repo) {
return { output: 'Calendar repo is not initialized', isError: true };
}
const spaceId = ctx.spaceId;
if (!spaceId) {
return {
output: 'このタスクはスペースに属していないため予定を一覧できません',
isError: true,
};
}
const from = input['from'];
const to = input['to'];
if (from !== undefined && (typeof from !== 'string' || !DATE_PATTERN.test(from))) {
return { output: 'from は YYYY-MM-DD 形式で指定してください', isError: true };
}
if (to !== undefined && (typeof to !== 'string' || !DATE_PATTERN.test(to))) {
return { output: 'to は YYYY-MM-DD 形式で指定してください', isError: true };
}
const events = await _repo.listCalendarEvents(spaceId, {
from: typeof from === 'string' ? from : undefined,
to: typeof to === 'string' ? to : undefined,
});
if (events.length === 0) {
return { output: '指定範囲に予定はありません', isError: false };
}
const lines = events.map((e) => {
const timeLabel = e.time ? (e.endTime ? `${e.time}–${e.endTime}` : e.time) : '(終日)';
return `- ${e.date}${e.endDate ? `–${e.endDate}` : ''} ${timeLabel} 「${e.title}」(id=${e.id}, ${e.createdBy})`;
});
return { output: lines.join('\n'), isError: false };
}