fix(athena): resolve 4 compatibility and correctness issues
- Use case-insensitive casing in duplicate name test to verify actual logic - Align permission type with SDK AgentConfig pattern (as AgentConfig["permission"]) - Move duplicate-name validation from schema to runtime for graceful fallback - Place skipped members details before 'end your turn' in council guard prompt
This commit is contained in:
parent
2eb8f5741a
commit
f6cdba07ec
@ -1,6 +1,6 @@
|
|||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||||
import type { AgentMode, AgentPromptMetadata } from "../types"
|
import type { AgentMode, AgentPromptMetadata } from "../types"
|
||||||
import { createAgentToolRestrictions, type PermissionValue } from "../../shared/permission-compat"
|
import { createAgentToolRestrictions } from "../../shared/permission-compat"
|
||||||
import { applyModelThinkingConfig } from "./model-thinking-config"
|
import { applyModelThinkingConfig } from "./model-thinking-config"
|
||||||
|
|
||||||
const MODE: AgentMode = "primary"
|
const MODE: AgentMode = "primary"
|
||||||
@ -211,10 +211,10 @@ The switch_agent tool switches the active agent. After you call it, end your res
|
|||||||
export function createAthenaAgent(model: string): AgentConfig {
|
export function createAthenaAgent(model: string): AgentConfig {
|
||||||
const restrictions = createAgentToolRestrictions(["write", "edit", "call_omo_agent"])
|
const restrictions = createAgentToolRestrictions(["write", "edit", "call_omo_agent"])
|
||||||
|
|
||||||
const permission: Record<string, PermissionValue> = {
|
const permission = {
|
||||||
...restrictions.permission,
|
...restrictions.permission,
|
||||||
question: "allow",
|
question: "allow",
|
||||||
}
|
} as AgentConfig["permission"]
|
||||||
|
|
||||||
const base = {
|
const base = {
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||||
|
|
||||||
const MISSING_COUNCIL_PROMPT = `
|
const MISSING_COUNCIL_PROMPT_HEADER = `
|
||||||
|
|
||||||
## CRITICAL: No Council Members Configured
|
## CRITICAL: No Council Members Configured
|
||||||
|
|
||||||
@ -32,7 +32,9 @@ You have no council members registered. This means the Athena council config is
|
|||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Each member requires \`model\` (\`"provider/model-id"\` format) and \`name\` (display name). Minimum 2 members required. Optional fields: \`variant\`, \`temperature\`.
|
Each member requires \`model\` (\`"provider/model-id"\` format) and \`name\` (display name). Minimum 2 members required. Optional fields: \`variant\`, \`temperature\`.`
|
||||||
|
|
||||||
|
const MISSING_COUNCIL_PROMPT_FOOTER = `
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -47,12 +49,14 @@ export function appendMissingCouncilPrompt(
|
|||||||
athenaConfig: AgentConfig,
|
athenaConfig: AgentConfig,
|
||||||
skippedMembers?: Array<{ name: string; reason: string }>,
|
skippedMembers?: Array<{ name: string; reason: string }>,
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
let prompt = MISSING_COUNCIL_PROMPT
|
let prompt = MISSING_COUNCIL_PROMPT_HEADER
|
||||||
|
|
||||||
if (skippedMembers && skippedMembers.length > 0) {
|
if (skippedMembers && skippedMembers.length > 0) {
|
||||||
const skipDetails = skippedMembers.map((m) => `- **${m.name}**: ${m.reason}`).join("\n")
|
const skipDetails = skippedMembers.map((m) => `- **${m.name}**: ${m.reason}`).join("\n")
|
||||||
prompt += `\n\n### Why Council Failed\n\nThe following members were skipped:\n${skipDetails}`
|
prompt += `\n\n### Why Council Failed\n\nThe following members were skipped:\n${skipDetails}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prompt += MISSING_COUNCIL_PROMPT_FOOTER
|
||||||
|
|
||||||
return { ...athenaConfig, prompt }
|
return { ...athenaConfig, prompt }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { registerCouncilMemberAgents } from "./council-member-agents"
|
import { registerCouncilMemberAgents } from "./council-member-agents"
|
||||||
|
|
||||||
describe("council-member-agents", () => {
|
describe("council-member-agents", () => {
|
||||||
test("skips duplicate names and disables council when below minimum", () => {
|
test("skips case-insensitive duplicate names and disables council when below minimum", () => {
|
||||||
//#given
|
//#given
|
||||||
const config = {
|
const config = {
|
||||||
members: [
|
members: [
|
||||||
{ model: "openai/gpt-5.3-codex", name: "GPT" },
|
{ model: "openai/gpt-5.3-codex", name: "GPT" },
|
||||||
{ model: "anthropic/claude-opus-4-6", name: "GPT" },
|
{ model: "anthropic/claude-opus-4-6", name: "gpt" },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
//#when
|
//#when
|
||||||
|
|||||||
@ -330,8 +330,9 @@ describe("CouncilConfigSchema", () => {
|
|||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("rejects council with duplicate member names", () => {
|
test("accepts council with duplicate member names for graceful runtime handling", () => {
|
||||||
//#given
|
//#given - duplicate detection is handled at runtime by registerCouncilMemberAgents,
|
||||||
|
// not at schema level, to allow graceful fallback instead of hard parse failure
|
||||||
const config = {
|
const config = {
|
||||||
members: [
|
members: [
|
||||||
{ model: "anthropic/claude-opus-4-6", name: "analyst" },
|
{ model: "anthropic/claude-opus-4-6", name: "analyst" },
|
||||||
@ -343,11 +344,11 @@ describe("CouncilConfigSchema", () => {
|
|||||||
const result = CouncilConfigSchema.safeParse(config)
|
const result = CouncilConfigSchema.safeParse(config)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("rejects council with case-insensitive duplicate names", () => {
|
test("accepts council with case-insensitive duplicate names for graceful runtime handling", () => {
|
||||||
//#given
|
//#given - case-insensitive dedup is handled at runtime by registerCouncilMemberAgents
|
||||||
const config = {
|
const config = {
|
||||||
members: [
|
members: [
|
||||||
{ model: "anthropic/claude-opus-4-6", name: "Claude" },
|
{ model: "anthropic/claude-opus-4-6", name: "Claude" },
|
||||||
@ -359,7 +360,7 @@ describe("CouncilConfigSchema", () => {
|
|||||||
const result = CouncilConfigSchema.safeParse(config)
|
const result = CouncilConfigSchema.safeParse(config)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("accepts council with unique member names", () => {
|
test("accepts council with unique member names", () => {
|
||||||
|
|||||||
@ -20,13 +20,7 @@ export const CouncilMemberSchema = z.object({
|
|||||||
}).strict()
|
}).strict()
|
||||||
|
|
||||||
export const CouncilConfigSchema = z.object({
|
export const CouncilConfigSchema = z.object({
|
||||||
members: z.array(CouncilMemberSchema).min(2).refine(
|
members: z.array(CouncilMemberSchema).min(2),
|
||||||
(members) => {
|
|
||||||
const names = members.map(m => m.name.toLowerCase())
|
|
||||||
return new Set(names).size === names.length
|
|
||||||
},
|
|
||||||
{ message: "Council member names must be unique (case-insensitive)" },
|
|
||||||
),
|
|
||||||
}).strict()
|
}).strict()
|
||||||
|
|
||||||
export type CouncilMemberConfig = z.infer<typeof CouncilMemberSchema>
|
export type CouncilMemberConfig = z.infer<typeof CouncilMemberSchema>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user