feat(team-mode): add member guidance and parser utilities

This commit is contained in:
YeonGyu-Kim
2026-04-28 10:45:34 +09:00
parent 783d70558b
commit 8a42d7fd69
2 changed files with 107 additions and 0 deletions
+25
View File
@@ -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: "<name>"\` 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
`
}
+82
View File
@@ -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<string, unknown>,
agentEligibilityRegistry: Readonly<Record<string, { verdict: "eligible" | "conditional" | "hard-reject"; rejectionMessage?: string }>>,
): MemberValidationError {
const name = typeof input.name === "string" ? input.name : "<unnamed>"
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 : "<unknown>"
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<TMember>(
memberSchema: { safeParse(input: unknown): { success: true; data: TMember } | { success: false } },
agentEligibilityRegistry: Readonly<Record<string, { verdict: "eligible" | "conditional" | "hard-reject"; rejectionMessage?: string }>>,
): (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<string, unknown>
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
}
}