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