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 = { AddCalendarEvent: ADD_CALENDAR_EVENT_DEF, ListCalendarEvents: LIST_CALENDAR_EVENTS_DEF, }; export async function executeTool( name: string, input: Record, ctx: ToolContext, ): Promise { if (name === 'AddCalendarEvent') return executeAddCalendarEvent(input, ctx); if (name === 'ListCalendarEvents') return executeListCalendarEvents(input, ctx); return null; } async function executeAddCalendarEvent( input: Record, ctx: ToolContext, ): Promise { 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, ctx: ToolContext, ): Promise { 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 }; }