diff --git a/src/features/team-mode/member-guidance.ts b/src/features/team-mode/member-guidance.ts new file mode 100644 index 000000000..e8849dfb6 --- /dev/null +++ b/src/features/team-mode/member-guidance.ts @@ -0,0 +1,25 @@ +import type { TeamModeConfig } from "../../config/schema/team-mode" + +export function buildTeammateCommunicationAddendum(_config: TeamModeConfig): string { + return ` +# Team Communication + +You are running as a team member. Your text responses are NOT visible to other team members or the lead. + +IMPORTANT: For ALL team_* tool calls, use the TeamRunId shown above as the \`teamRunId\` parameter. Do NOT use the team name. + +Do not call lead-only lifecycle tools such as \`team_shutdown_request\`, \`team_delete\`, \`team_approve_shutdown\`, or \`team_reject_shutdown\`. + +Use these tools instead: +- team_send_message: Send results, blockers, or completion updates to the lead. Use \`to: "lead"\` for the lead, \`to: ""\` for a specific member. Include \`summary\` and \`references\` when they help the lead triage quickly. +- team_task_update: Update your task status. Move to \`status: "in_progress"\` when you start working, and \`status: "completed"\` when done. \`status: "claimed"\` is optional if you want to explicitly claim before you begin. +- team_task_list: See all team tasks and their status. +- team_task_get: Get details of a specific task. +- delegate-task: Use this for bounded side work when needed. + +When you finish your assigned work, ALWAYS: +1. Send your results to lead via team_send_message +2. Mark your task as completed via team_task_update +3. Send a completion message to lead so the lead can decide whether to request shutdown +` +} diff --git a/src/features/team-mode/member-parser.ts b/src/features/team-mode/member-parser.ts new file mode 100644 index 000000000..3e4914a9a --- /dev/null +++ b/src/features/team-mode/member-parser.ts @@ -0,0 +1,82 @@ +export class MemberValidationError extends Error { + constructor( + message: string, + public readonly memberName?: string, + public readonly issue?: string, + ) { + super(message) + this.name = "MemberValidationError" + } +} + +function translateMemberError( + input: Record, + agentEligibilityRegistry: Readonly>, +): MemberValidationError { + const name = typeof input.name === "string" ? input.name : "" + const hasCategory = input.category != null + const hasSubagentType = input.subagent_type != null + const hasKind = input.kind === "category" || input.kind === "subagent_type" + + if (hasCategory && hasSubagentType) { + return new MemberValidationError( + `Member '${name}' specifies both 'category' and 'subagent_type'. Must specify exactly one via 'kind' discriminator.`, + name, + "both-kinds", + ) + } + + if (!hasKind && !hasCategory && !hasSubagentType) { + return new MemberValidationError( + `Member '${name}' missing 'kind' discriminator. Specify either {kind:'category', category, prompt} or {kind:'subagent_type', subagent_type}.`, + name, + "missing-kind", + ) + } + + if (input.kind === "category" || (!hasKind && hasCategory)) { + const category = typeof input.category === "string" ? input.category : "" + return new MemberValidationError( + `Member '${name}' uses category '${category}' but is missing required 'prompt' field. Category members must supply a task prompt.`, + name, + "category-missing-prompt", + ) + } + + if (input.kind === "subagent_type" || (!hasKind && hasSubagentType)) { + const subagentType = typeof input.subagent_type === "string" ? input.subagent_type : String(input.subagent_type) + if (typeof input.subagent_type !== "string" || !agentEligibilityRegistry[input.subagent_type]) { + return new MemberValidationError( + `Unknown subagent_type '${subagentType}'. Available ELIGIBLE agents: sisyphus, atlas, sisyphus-junior, hephaestus (if D-36 applied). Use delegate-task for read-only agents like oracle, librarian, explore, metis, momus, multimodal-looker.`, + name, + "unknown-subagent", + ) + } + } + + return new MemberValidationError(`Member '${name}' validation failed.`, name, "zod-residual") +} + +export function createParseMember( + memberSchema: { safeParse(input: unknown): { success: true; data: TMember } | { success: false } }, + agentEligibilityRegistry: Readonly>, +): (input: unknown) => TMember { + return function parseMember(input: unknown) { + if (input == null || typeof input !== "object") { + throw new MemberValidationError("Member must be an object") + } + + const raw = input as Record + const result = memberSchema.safeParse( + raw.kind === undefined && (raw.category !== undefined || raw.subagent_type !== undefined) + ? { ...raw, kind: raw.category !== undefined ? "category" : "subagent_type" } + : raw, + ) + + if (!result.success) { + throw translateMemberError(raw, agentEligibilityRegistry) + } + + return result.data + } +}