/** * 企画・Plan Agent * * accessiblePools: [] (pool-less のみ) * 利用可能ツール: query_crm, query_erp, query_hr (全て pool-less) * * タスク: 情報収集結果を受け取り、具体的な施策を立案する。 * Passkey (host-only) で店長に承認を求める。 */ import type { A2ATaskRequest, A2ATaskResponse } from '../platform-types.js'; import { AgentBase } from '../agent-base.js'; interface AnalysisResult { targetDate: string; weather: { isRainy: boolean; precipitationProbability: number; summary: string }; inventory: { hasDessertStock: boolean; dessertsAvailable: Array<{ name: string; stock: number }> }; customers: { highCancelTendencyCount: number }; reservations: { tomorrowCount: number }; actionRecommended: boolean; summary: string; } export class PlanningAgent extends AgentBase { readonly agentId = 'planning-agent'; readonly accessiblePools: string[] = []; async handle(task: A2ATaskRequest): Promise { const { analysis, rawData } = task.payload.parameters as { analysis?: AnalysisResult; rawData?: unknown; }; if (!analysis) { return this.error(task, 'No analysis data provided in task parameters'); } if (!analysis.actionRecommended) { return this.cancelled(task, `施策実施条件未達: ${analysis.summary}`); } try { // 1. Get today's reservations from CRM for targeting const reservationData = await this.callTool('query_crm', { entity: 'reservations', filter: `date=${analysis.targetDate}`, }) as { reservations?: Array<{ customerName: string; customerId: string }> }; // 2. Get dessert inventory details from ERP const inventoryData = await this.callTool('query_erp', { entity: 'inventory', item: 'dessert', }) as { items?: Array<{ name: string; stock: number; costPerUnit: number }> }; // 3. Build the plan const desserts = (inventoryData?.items ?? []).filter((i) => (i.stock ?? 0) > 0); const targetDessert = desserts[0]; const targetCustomers = (reservationData?.reservations ?? []).slice(0, analysis.customers.highCancelTendencyCount); const plan = { title: '無料デザートキャンペーン', targetDate: analysis.targetDate, rationale: analysis.summary, campaign: { dessertItem: targetDessert?.name ?? 'デザート', freeItemPerCustomer: 1, totalCost: (targetDessert?.costPerUnit ?? 0) * targetCustomers.length, }, targetCustomers: targetCustomers.map((c) => ({ customerId: c.customerId, name: c.customerName, })), emailTemplate: buildEmailTemplate(analysis.targetDate, targetDessert?.name ?? 'デザート'), createdAt: new Date().toISOString(), status: 'pending_approval', }; console.log( `[planning] Plan created: ${plan.title} for ${targetCustomers.length} customers`, ); // 4. Approval is handled by the Host (Passkey ceremony triggered by DispatchAgent) // PlanningAgent returns the plan with status: pending_approval. // The orchestration layer (Director → Dispatch) is responsible for auth flow. return this.success(task, { plan, rawData, nextAgent: 'dispatch-agent', }); } catch (err) { return this.error(task, (err as Error).message); } } } function buildEmailTemplate(date: string, dessertName: string): string { return `件名: 【特別ご招待】明日のご来店に無料${dessertName}をプレゼント お客様へ 明日 ${date} のご予約、誠にありがとうございます。 明日は雨模様のお天気が予想されますが、 特別に無料の${dessertName}をご用意しております。 ぜひお越しください。スタッフ一同、お待ちしております。 ※ 本メールは予約システムより自動送信されています。`; }