feat(team-mode): add member guidance and parser utilities
This commit is contained in:
@@ -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
|
||||
`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user