Merge remote-tracking branch 'origin/dev' into feat/custom-agents
# Conflicts: # src/agents/utils.test.ts # src/plugin-handlers/agent-config-handler.ts
This commit is contained in:
@@ -17,7 +17,6 @@ import type { AvailableAgent, AvailableSkill, AvailableCategory } from "../dynam
|
||||
import { buildCategorySkillsDelegationGuide } from "../dynamic-agent-prompt-builder"
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import { mergeCategories } from "../../shared/merge-categories"
|
||||
import { createAgentToolRestrictions } from "../../shared/permission-compat"
|
||||
|
||||
import { getDefaultAtlasPrompt } from "./default"
|
||||
import { getGptAtlasPrompt } from "./gpt"
|
||||
@@ -30,7 +29,7 @@ import {
|
||||
buildDecisionMatrix,
|
||||
} from "./prompt-section-builder"
|
||||
|
||||
const MODE: AgentMode = "primary"
|
||||
const MODE: AgentMode = "all"
|
||||
|
||||
export type AtlasPromptSource = "default" | "gpt" | "gemini"
|
||||
|
||||
@@ -100,11 +99,6 @@ function buildDynamicOrchestratorPrompt(ctx?: OrchestratorContext): string {
|
||||
}
|
||||
|
||||
export function createAtlasAgent(ctx: OrchestratorContext): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"task",
|
||||
"call_omo_agent",
|
||||
])
|
||||
|
||||
const baseConfig = {
|
||||
description:
|
||||
"Orchestrates work via task() to complete ALL tasks in a todo list until fully done. (Atlas - OhMyOpenCode)",
|
||||
@@ -113,7 +107,6 @@ export function createAtlasAgent(ctx: OrchestratorContext): AgentConfig {
|
||||
temperature: 0.1,
|
||||
prompt: buildDynamicOrchestratorPrompt(ctx),
|
||||
color: "#10B981",
|
||||
...restrictions,
|
||||
}
|
||||
|
||||
return baseConfig as AgentConfig
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { createEnvContext } from "./env-context"
|
||||
|
||||
describe("createEnvContext", () => {
|
||||
test("returns omo-env block with timezone and locale", () => {
|
||||
// #given - no setup needed
|
||||
|
||||
// #when
|
||||
const result = createEnvContext()
|
||||
|
||||
// #then
|
||||
expect(result).toContain("<omo-env>")
|
||||
expect(result).toContain("</omo-env>")
|
||||
expect(result).toContain("Timezone:")
|
||||
expect(result).toContain("Locale:")
|
||||
expect(result).not.toContain("Current date:")
|
||||
})
|
||||
|
||||
test("does not include time with seconds precision to preserve token cache", () => {
|
||||
// #given - seconds-precision time changes every second, breaking cache on every request
|
||||
|
||||
// #when
|
||||
const result = createEnvContext()
|
||||
|
||||
// #then - no HH:MM:SS pattern anywhere in the output
|
||||
expect(result).not.toMatch(/\d{1,2}:\d{2}:\d{2}/)
|
||||
})
|
||||
|
||||
test("does not include date or time fields since OpenCode already provides them", () => {
|
||||
// #given - OpenCode's system.ts already injects date, platform, working directory
|
||||
|
||||
// #when
|
||||
const result = createEnvContext()
|
||||
|
||||
// #then - only timezone and locale remain; both are stable across requests
|
||||
expect(result).not.toContain("Current date:")
|
||||
expect(result).not.toContain("Current time:")
|
||||
})
|
||||
})
|
||||
@@ -1,32 +1,15 @@
|
||||
/**
|
||||
* Creates OmO-specific environment context (time, timezone, locale).
|
||||
* Creates OmO-specific environment context (timezone, locale).
|
||||
* Note: Working directory, platform, and date are already provided by OpenCode's system.ts,
|
||||
* so we only include fields that OpenCode doesn't provide to avoid duplication.
|
||||
* See: https://github.com/code-yeongyu/oh-my-opencode/issues/379
|
||||
*/
|
||||
export function createEnvContext(): string {
|
||||
const now = new Date()
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale
|
||||
|
||||
const dateStr = now.toLocaleDateString(locale, {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
|
||||
const timeStr = now.toLocaleTimeString(locale, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: true,
|
||||
})
|
||||
|
||||
return `
|
||||
<omo-env>
|
||||
Current date: ${dateStr}
|
||||
Current time: ${timeStr}
|
||||
Timezone: ${timezone}
|
||||
Locale: ${locale}
|
||||
</omo-env>`
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
categorizeTools,
|
||||
} from "./dynamic-agent-prompt-builder";
|
||||
|
||||
const MODE: AgentMode = "primary";
|
||||
const MODE: AgentMode = "all";
|
||||
|
||||
function buildTodoDisciplineSection(useTaskSystem: boolean): string {
|
||||
if (useTaskSystem) {
|
||||
@@ -448,6 +448,21 @@ ${oracleSection}
|
||||
4. **Run build** if applicable — exit code 0 required
|
||||
5. **Tell user** what you verified and the results — keep it clear and helpful
|
||||
|
||||
### Auto-Commit Policy (MANDATORY for implementation/fix work)
|
||||
|
||||
1. **Auto-commit after implementation is complete** when the task includes feature/fix code changes
|
||||
2. **Commit ONLY after verification gates pass**:
|
||||
- \`lsp_diagnostics\` clean on all modified files
|
||||
- Related tests pass
|
||||
- Typecheck/build pass when applicable
|
||||
3. **If any gate fails, DO NOT commit** — fix issues first, re-run verification, then commit
|
||||
4. **Use Conventional Commits format** with meaningful intent-focused messages:
|
||||
- \`feat(scope): add ...\` for new functionality
|
||||
- \`fix(scope): resolve ...\` for bug fixes
|
||||
- \`refactor(scope): simplify ...\` for internal restructuring
|
||||
5. **Do not make placeholder commits** (\`wip\`, \`temp\`, \`update\`) or commit unverified code
|
||||
6. **If user explicitly says not to commit**, skip commit and report that changes are left uncommitted
|
||||
|
||||
- **File edit** — \`lsp_diagnostics\` clean
|
||||
- **Build** — Exit code 0
|
||||
- **Tests** — Pass (or pre-existing failures noted)
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
buildGeminiIntentGateEnforcement,
|
||||
} from "./sisyphus-gemini-overlays";
|
||||
|
||||
const MODE: AgentMode = "primary";
|
||||
const MODE: AgentMode = "all";
|
||||
export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "utility",
|
||||
cost: "EXPENSIVE",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createLibrarianAgent } from "./librarian"
|
||||
import { createExploreAgent } from "./explore"
|
||||
import { createMomusAgent } from "./momus"
|
||||
import { createMetisAgent } from "./metis"
|
||||
import { createAtlasAgent } from "./atlas"
|
||||
|
||||
const TEST_MODEL = "anthropic/claude-sonnet-4-5"
|
||||
|
||||
@@ -96,4 +97,18 @@ describe("read-only agent tool restrictions", () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("Atlas", () => {
|
||||
test("allows delegation tools for orchestration", () => {
|
||||
// given
|
||||
const agent = createAtlasAgent({ model: TEST_MODEL })
|
||||
|
||||
// when
|
||||
const permission = (agent.permission ?? {}) as Record<string, string>
|
||||
|
||||
// then
|
||||
expect(permission["task"]).toBeUndefined()
|
||||
expect(permission["call_omo_agent"]).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+21
-13
@@ -2,11 +2,17 @@ import { describe, test, expect } from "bun:test";
|
||||
import { isGptModel, isGeminiModel } from "./types";
|
||||
|
||||
describe("isGptModel", () => {
|
||||
test("standard openai provider models", () => {
|
||||
test("standard openai provider gpt models", () => {
|
||||
expect(isGptModel("openai/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("openai/gpt-4o")).toBe(true);
|
||||
expect(isGptModel("openai/o1")).toBe(true);
|
||||
expect(isGptModel("openai/o3-mini")).toBe(true);
|
||||
});
|
||||
|
||||
test("o-series models are not gpt by name", () => {
|
||||
expect(isGptModel("openai/o1")).toBe(false);
|
||||
expect(isGptModel("openai/o3-mini")).toBe(false);
|
||||
expect(isGptModel("litellm/o1")).toBe(false);
|
||||
expect(isGptModel("litellm/o3-mini")).toBe(false);
|
||||
expect(isGptModel("litellm/o4-mini")).toBe(false);
|
||||
});
|
||||
|
||||
test("github copilot gpt models", () => {
|
||||
@@ -17,9 +23,6 @@ describe("isGptModel", () => {
|
||||
test("litellm proxied gpt models", () => {
|
||||
expect(isGptModel("litellm/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("litellm/gpt-4o")).toBe(true);
|
||||
expect(isGptModel("litellm/o1")).toBe(true);
|
||||
expect(isGptModel("litellm/o3-mini")).toBe(true);
|
||||
expect(isGptModel("litellm/o4-mini")).toBe(true);
|
||||
});
|
||||
|
||||
test("other proxied gpt models", () => {
|
||||
@@ -27,6 +30,11 @@ describe("isGptModel", () => {
|
||||
expect(isGptModel("custom-provider/gpt-5.2")).toBe(true);
|
||||
});
|
||||
|
||||
test("venice provider gpt models", () => {
|
||||
expect(isGptModel("venice/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("venice/gpt-4o")).toBe(true);
|
||||
});
|
||||
|
||||
test("gpt4 prefix without hyphen (legacy naming)", () => {
|
||||
expect(isGptModel("litellm/gpt4o")).toBe(true);
|
||||
expect(isGptModel("ollama/gpt4")).toBe(true);
|
||||
@@ -39,8 +47,8 @@ describe("isGptModel", () => {
|
||||
});
|
||||
|
||||
test("gemini models are not gpt", () => {
|
||||
expect(isGptModel("google/gemini-3-pro")).toBe(false);
|
||||
expect(isGptModel("litellm/gemini-3-pro")).toBe(false);
|
||||
expect(isGptModel("google/gemini-3.1-pro")).toBe(false);
|
||||
expect(isGptModel("litellm/gemini-3.1-pro")).toBe(false);
|
||||
});
|
||||
|
||||
test("opencode provider is not gpt", () => {
|
||||
@@ -50,29 +58,29 @@ describe("isGptModel", () => {
|
||||
|
||||
describe("isGeminiModel", () => {
|
||||
test("#given google provider models #then returns true", () => {
|
||||
expect(isGeminiModel("google/gemini-3-pro")).toBe(true);
|
||||
expect(isGeminiModel("google/gemini-3.1-pro")).toBe(true);
|
||||
expect(isGeminiModel("google/gemini-3-flash")).toBe(true);
|
||||
expect(isGeminiModel("google/gemini-2.5-pro")).toBe(true);
|
||||
});
|
||||
|
||||
test("#given google-vertex provider models #then returns true", () => {
|
||||
expect(isGeminiModel("google-vertex/gemini-3-pro")).toBe(true);
|
||||
expect(isGeminiModel("google-vertex/gemini-3.1-pro")).toBe(true);
|
||||
expect(isGeminiModel("google-vertex/gemini-3-flash")).toBe(true);
|
||||
});
|
||||
|
||||
test("#given github copilot gemini models #then returns true", () => {
|
||||
expect(isGeminiModel("github-copilot/gemini-3-pro")).toBe(true);
|
||||
expect(isGeminiModel("github-copilot/gemini-3.1-pro")).toBe(true);
|
||||
expect(isGeminiModel("github-copilot/gemini-3-flash")).toBe(true);
|
||||
});
|
||||
|
||||
test("#given litellm proxied gemini models #then returns true", () => {
|
||||
expect(isGeminiModel("litellm/gemini-3-pro")).toBe(true);
|
||||
expect(isGeminiModel("litellm/gemini-3.1-pro")).toBe(true);
|
||||
expect(isGeminiModel("litellm/gemini-3-flash")).toBe(true);
|
||||
expect(isGeminiModel("litellm/gemini-2.5-pro")).toBe(true);
|
||||
});
|
||||
|
||||
test("#given other proxied gemini models #then returns true", () => {
|
||||
expect(isGeminiModel("custom-provider/gemini-3-pro")).toBe(true);
|
||||
expect(isGeminiModel("custom-provider/gemini-3.1-pro")).toBe(true);
|
||||
expect(isGeminiModel("ollama/gemini-3-flash")).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
+1
-6
@@ -70,14 +70,9 @@ function extractModelName(model: string): string {
|
||||
return model.includes("/") ? model.split("/").pop() ?? model : model
|
||||
}
|
||||
|
||||
const GPT_MODEL_PREFIXES = ["gpt-", "gpt4", "o1", "o3", "o4"]
|
||||
|
||||
export function isGptModel(model: string): boolean {
|
||||
if (model.startsWith("openai/") || model.startsWith("github-copilot/gpt-"))
|
||||
return true
|
||||
|
||||
const modelName = extractModelName(model).toLowerCase()
|
||||
return GPT_MODEL_PREFIXES.some((prefix) => modelName.startsWith(prefix))
|
||||
return modelName.includes("gpt")
|
||||
}
|
||||
|
||||
const GEMINI_PROVIDERS = ["google/", "google-vertex/"]
|
||||
|
||||
@@ -603,8 +603,8 @@ describe("createBuiltinAgents with requiresProvider gating (hephaestus)", () =>
|
||||
}
|
||||
})
|
||||
|
||||
test("hephaestus is not created when only github-copilot provider is connected", async () => {
|
||||
// #given - github-copilot provider has models available
|
||||
test("hephaestus IS created when github-copilot is connected with a GPT model", async () => {
|
||||
// #given - github-copilot provider has gpt-5.3-codex available
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["github-copilot/gpt-5.3-codex"])
|
||||
)
|
||||
@@ -614,8 +614,8 @@ describe("createBuiltinAgents with requiresProvider gating (hephaestus)", () =>
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
|
||||
|
||||
// #then
|
||||
expect(agents.hephaestus).toBeUndefined()
|
||||
// #then - github-copilot is now a valid provider for hephaestus
|
||||
expect(agents.hephaestus).toBeDefined()
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
cacheSpy.mockRestore()
|
||||
@@ -1002,7 +1002,7 @@ describe("buildAgent with category and skills", () => {
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then - category's built-in model is applied
|
||||
expect(agent.model).toBe("google/gemini-3-pro")
|
||||
expect(agent.model).toBe("google/gemini-3.1-pro")
|
||||
})
|
||||
|
||||
test("agent with category and existing model keeps existing model", () => {
|
||||
|
||||
Reference in New Issue
Block a user