feat(agents): add Hephaestus - autonomous deep worker agent (#1287)
* refactor(keyword-detector): split constants into domain-specific modules * feat(shared): add requiresAnyModel and isAnyFallbackModelAvailable * feat(config): add hephaestus to agent schemas * feat(agents): add Hephaestus autonomous deep worker * feat(cli): update model-fallback for hephaestus support * feat(plugin): add hephaestus to config handler with ordering * test(delegate-task): update tests for hephaestus agent * docs: update AGENTS.md files for hephaestus * docs: add hephaestus to READMEs * chore: regenerate config schema * fix(delegate-task): bypass requiresModel check when user provides explicit config * docs(hephaestus): add 4-part context structure for explore/librarian prompts * docs: fix review comments from cubic (non-breaking changes) - Move Hephaestus from Primary Agents to Subagents (uses own fallback chain) - Fix Hephaestus fallback chain documentation (claude-opus-4-5 → gemini-3-pro) - Add settings.local.json to claude-code-hooks config sources - Fix delegate_task parameters in ultrawork prompt (agent→subagent_type, background→run_in_background, add load_skills) - Update line counts in AGENTS.md (index.ts: 788, manager.ts: 1440) * docs: fix additional documentation inconsistencies from oracle review - Fix delegate_task parameters in Background Agents example (docs/features.md) - Fix Hephaestus fallback chain in root AGENTS.md to match model-requirements.ts * docs: clarify Hephaestus has no fallback (requires gpt-5.2-codex only) Hephaestus uses requiresModel constraint - it only activates when gpt-5.2-codex is available. The fallback chain in code is unreachable, so documentation should not mention fallbacks. * fix(hephaestus): remove unreachable fallback chain entries Hephaestus has requiresModel: gpt-5.2-codex which means the agent only activates when that specific model is available. The fallback entries (claude-opus-4-5, gemini-3-pro) were unreachable and misleading. --------- Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
# SHARED UTILITIES KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
55 cross-cutting utilities: path resolution, token truncation, config parsing, model resolution.
|
||||
|
||||
55 cross-cutting utilities. Import via barrel pattern: `import { log, deepMerge } from "../../shared"`
|
||||
|
||||
**Categories**: Path resolution, Token truncation, Config parsing, Model resolution, System directives, Tool restrictions
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
@@ -10,7 +13,7 @@ shared/
|
||||
├── logger.ts # File-based logging (/tmp/oh-my-opencode.log)
|
||||
├── dynamic-truncator.ts # Token-aware context window management (194 lines)
|
||||
├── model-resolver.ts # 3-step resolution (Override → Fallback → Default)
|
||||
├── model-requirements.ts # Agent/category model fallback chains (132 lines)
|
||||
├── model-requirements.ts # Agent/category model fallback chains (162 lines)
|
||||
├── model-availability.ts # Provider model fetching & fuzzy matching (154 lines)
|
||||
├── jsonc-parser.ts # JSONC parsing with comment support
|
||||
├── frontmatter.ts # YAML frontmatter extraction (JSON_SCHEMA only)
|
||||
|
||||
@@ -95,22 +95,22 @@ describe("resolveVariantForModel", () => {
|
||||
expect(variant).toBe("max")
|
||||
})
|
||||
|
||||
test("returns correct variant for openai provider", () => {
|
||||
// given
|
||||
test("returns correct variant for openai provider (hephaestus agent)", () => {
|
||||
// #given hephaestus has openai/gpt-5.2-codex with variant "medium" in its chain
|
||||
const config = {} as OhMyOpenCodeConfig
|
||||
const model = { providerID: "openai", modelID: "gpt-5.2" }
|
||||
const model = { providerID: "openai", modelID: "gpt-5.2-codex" }
|
||||
|
||||
// when
|
||||
const variant = resolveVariantForModel(config, "sisyphus", model)
|
||||
// #when
|
||||
const variant = resolveVariantForModel(config, "hephaestus", model)
|
||||
|
||||
// then
|
||||
expect(variant).toBe("medium")
|
||||
})
|
||||
|
||||
test("returns undefined for provider with no variant in chain", () => {
|
||||
// given
|
||||
test("returns undefined for provider not in sisyphus chain", () => {
|
||||
// #given openai is not in sisyphus fallback chain anymore
|
||||
const config = {} as OhMyOpenCodeConfig
|
||||
const model = { providerID: "google", modelID: "gemini-3-pro" }
|
||||
const model = { providerID: "openai", modelID: "gpt-5.2" }
|
||||
|
||||
// when
|
||||
const variant = resolveVariantForModel(config, "sisyphus", model)
|
||||
|
||||
@@ -259,6 +259,26 @@ export async function fetchAvailableModels(
|
||||
return modelSet
|
||||
}
|
||||
|
||||
export function isAnyFallbackModelAvailable(
|
||||
fallbackChain: Array<{ providers: string[]; model: string }>,
|
||||
availableModels: Set<string>,
|
||||
): boolean {
|
||||
if (availableModels.size === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const entry of fallbackChain) {
|
||||
const hasAvailableProvider = entry.providers.some((provider) => {
|
||||
return fuzzyMatchModel(entry.model, availableModels, [provider]) !== null
|
||||
})
|
||||
if (hasAvailableProvider) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
log("[isAnyFallbackModelAvailable] no model available in chain", { chainLength: fallbackChain.length })
|
||||
return false
|
||||
}
|
||||
|
||||
export function __resetModelCache(): void {}
|
||||
|
||||
export function isModelCacheAvailable(): boolean {
|
||||
|
||||
@@ -23,20 +23,25 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.variant).toBe("high")
|
||||
})
|
||||
|
||||
test("sisyphus has valid fallbackChain with claude-opus-4-5 as primary", () => {
|
||||
// given - sisyphus agent requirement
|
||||
test("sisyphus has valid fallbackChain with claude-opus-4-5 as primary and requiresAnyModel", () => {
|
||||
// #given - sisyphus agent requirement
|
||||
const sisyphus = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
||||
|
||||
// when - accessing Sisyphus requirement
|
||||
// then - fallbackChain exists with claude-opus-4-5 as first entry
|
||||
// #when - accessing Sisyphus requirement
|
||||
// #then - fallbackChain exists with claude-opus-4-5 as first entry, glm-4.7-free as last
|
||||
expect(sisyphus).toBeDefined()
|
||||
expect(sisyphus.fallbackChain).toBeArray()
|
||||
expect(sisyphus.fallbackChain.length).toBeGreaterThan(0)
|
||||
expect(sisyphus.fallbackChain).toHaveLength(5)
|
||||
expect(sisyphus.requiresAnyModel).toBe(true)
|
||||
|
||||
const primary = sisyphus.fallbackChain[0]
|
||||
expect(primary.providers[0]).toBe("anthropic")
|
||||
expect(primary.model).toBe("claude-opus-4-5")
|
||||
expect(primary.variant).toBe("max")
|
||||
|
||||
const last = sisyphus.fallbackChain[4]
|
||||
expect(last.providers[0]).toBe("opencode")
|
||||
expect(last.model).toBe("glm-4.7-free")
|
||||
})
|
||||
|
||||
test("librarian has valid fallbackChain with glm-4.7 as primary", () => {
|
||||
@@ -156,10 +161,21 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.providers[0]).toBe("kimi-for-coding")
|
||||
})
|
||||
|
||||
test("all 9 builtin agents have valid fallbackChain arrays", () => {
|
||||
// given - list of 9 agent names
|
||||
test("hephaestus requires gpt-5.2-codex", () => {
|
||||
// #given - hephaestus agent requirement
|
||||
const hephaestus = AGENT_MODEL_REQUIREMENTS["hephaestus"]
|
||||
|
||||
// #when - accessing hephaestus requirement
|
||||
// #then - requiresModel is set to gpt-5.2-codex
|
||||
expect(hephaestus).toBeDefined()
|
||||
expect(hephaestus.requiresModel).toBe("gpt-5.2-codex")
|
||||
})
|
||||
|
||||
test("all 10 builtin agents have valid fallbackChain arrays", () => {
|
||||
// #given - list of 10 agent names
|
||||
const expectedAgents = [
|
||||
"sisyphus",
|
||||
"hephaestus",
|
||||
"oracle",
|
||||
"librarian",
|
||||
"explore",
|
||||
@@ -173,8 +189,8 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
// when - checking AGENT_MODEL_REQUIREMENTS
|
||||
const definedAgents = Object.keys(AGENT_MODEL_REQUIREMENTS)
|
||||
|
||||
// then - all agents present with valid fallbackChain
|
||||
expect(definedAgents).toHaveLength(9)
|
||||
// #then - all agents present with valid fallbackChain
|
||||
expect(definedAgents).toHaveLength(10)
|
||||
for (const agent of expectedAgents) {
|
||||
const requirement = AGENT_MODEL_REQUIREMENTS[agent]
|
||||
expect(requirement).toBeDefined()
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ModelRequirement = {
|
||||
fallbackChain: FallbackEntry[]
|
||||
variant?: string // Default variant (used when entry doesn't specify one)
|
||||
requiresModel?: string // If set, only activates when this model is available (fuzzy match)
|
||||
requiresAnyModel?: boolean // If true, requires at least ONE model in fallbackChain to be available (or empty availability treated as unavailable)
|
||||
}
|
||||
|
||||
export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
@@ -17,9 +18,15 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "medium" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
{ providers: ["opencode"], model: "glm-4.7-free" },
|
||||
],
|
||||
requiresAnyModel: true,
|
||||
},
|
||||
hephaestus: {
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "medium" },
|
||||
],
|
||||
requiresModel: "gpt-5.2-codex",
|
||||
},
|
||||
oracle: {
|
||||
fallbackChain: [
|
||||
|
||||
Reference in New Issue
Block a user