/**
* A2A Task Manager — Routes incoming A2A messages to existing tools
* and manages the task lifecycle (creation, status, artifacts).
* Uses getDeps() for all shared dependencies.
*
* Supported skills:
* - scout_inventory → product search / discovery
* - manage_cart → cart CRUD operations
* - negotiate_terms → UCP capability negotiation
* - execute_checkout → AP2 mandate chain checkout
* - track_order → order tracking / fulfillment status
*/
import { randomUUID } from 'node:crypto';
import type {
Task,
TaskState,
Message,
Part,
DataPart,
Artifact,
} from './types.js';
import { getDeps } from '../dynamo/factory.js';
import { scoutInventory } from '../tools/scout-inventory.js';
import { manageCart } from '../tools/manage-cart.js';
import { negotiateTerms } from '../tools/negotiate-terms.js';
import { executeCheckout } from '../tools/execute-checkout.js';
import type { ExecuteCheckoutDeps } from '../tools/execute-checkout.js';
import { trackOrder } from '../tools/track-order.js';
// ─── Constants ───
const AVAILABLE_SKILLS = [
'scout_inventory',
'manage_cart',
'negotiate_terms',
'execute_checkout',
'track_order',
] as const;
type SkillName = (typeof AVAILABLE_SKILLS)[number];
// ─── Helpers ───
/**
* Build a completed or failed Task from a result or error.
*/
function buildTask(
state: TaskState,
resultText: string,
data: Record<string, unknown> | null,
): Task {
const now = new Date().toISOString();
const statusMessage: Message = {
role: 'agent',
parts: [{ type: 'text', text: resultText }],
};
const task: Task = {
id: randomUUID(),
status: {
state,
timestamp: now,
message: statusMessage,
},
};
if (data) {
const artifact: Artifact = {
name: 'result',
parts: [{ type: 'data', data } as DataPart],
};
task.artifacts = [artifact];
}
return task;
}
/**
* Extract the structured input from a message's parts.
*
* Strategy:
* 1. Look for the first DataPart and use its `data` directly.
* 2. Failing that, look for a TextPart and try to JSON.parse it.
* 3. If neither works, return null.
*/
function extractInput(parts: Part[]): { skill: string; params: Record<string, unknown> } | null {
// 1. Try DataPart first
for (const part of parts) {
if (part.type === 'data') {
const data = part.data as Record<string, unknown>;
if (typeof data.skill === 'string') {
return {
skill: data.skill,
params: (data.params as Record<string, unknown>) ?? {},
};
}
}
}
// 2. Fallback: try TextPart → JSON.parse
for (const part of parts) {
if (part.type === 'text') {
try {
const parsed = JSON.parse(part.text) as Record<string, unknown>;
if (typeof parsed.skill === 'string') {
return {
skill: parsed.skill,
params: (parsed.params as Record<string, unknown>) ?? {},
};
}
} catch {
// Not valid JSON — skip
}
}
}
return null;
}
// ─── Task Manager ───
export class TaskManager {
/**
* Process an incoming A2A message, route it to the appropriate tool,
* and return a Task representing the outcome.
*/
async processMessage(message: Message): Promise<Task> {
// 1. Extract structured input from message parts
const input = extractInput(message.parts);
if (!input) {
return buildTask(
'failed',
'Could not extract structured input from message. Expected a data part or JSON text part with { "skill": "...", "params": { ... } }.',
null,
);
}
const { skill, params } = input;
// 2. Validate skill name
if (!AVAILABLE_SKILLS.includes(skill as SkillName)) {
return buildTask(
'failed',
`Unknown skill: "${skill}". Available skills: ${AVAILABLE_SKILLS.join(', ')}.`,
null,
);
}
// 3. Route to the appropriate tool
try {
const result = await this.dispatch(skill as SkillName, params);
return buildTask(
'completed',
`Skill "${skill}" completed successfully.`,
result as Record<string, unknown>,
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return buildTask(
'failed',
`Skill "${skill}" failed: ${errorMessage}`,
null,
);
}
}
/**
* Dispatch to the correct tool function based on skill name.
*/
private async dispatch(skill: SkillName, params: Record<string, unknown>): Promise<unknown> {
const deps = getDeps();
switch (skill) {
case 'scout_inventory':
return scoutInventory(params as unknown as Parameters<typeof scoutInventory>[0]);
case 'manage_cart':
return manageCart(params as unknown as Parameters<typeof manageCart>[0]);
case 'negotiate_terms':
return negotiateTerms(
params as unknown as Parameters<typeof negotiateTerms>[0],
deps.config,
);
case 'execute_checkout': {
const checkoutDeps: ExecuteCheckoutDeps = {
sessionManager: deps.sessionManager,
verifier: deps.verifier,
mandateStore: deps.mandateStore,
guardrail: deps.guardrail,
feeCollector: deps.feeCollector,
storefrontAPI: deps.storefrontAPI,
};
return executeCheckout(
params as unknown as Parameters<typeof executeCheckout>[0],
checkoutDeps,
);
}
case 'track_order':
return trackOrder(params as unknown as Parameters<typeof trackOrder>[0]);
default: {
// Exhaustive check — should never be reached
const _exhaustive: never = skill;
throw new Error(`Unhandled skill: ${String(_exhaustive)}`);
}
}
}
}