feat(agents): add file:// URI support in prompt_append configuration
Port devxoul's PR #821 feature to current codebase structure. Supports absolute, relative, ~/home paths with percent-encoding. Gracefully handles malformed URIs and missing files with warnings. Co-authored-by: devxoul <devxoul@gmail.com>
This commit is contained in:
@@ -171,6 +171,7 @@ export async function createBuiltinAgents(
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
mergedCategories,
|
||||
directory,
|
||||
userCategories: categories,
|
||||
})
|
||||
if (atlasConfig) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentOverrideConfig } from "../types"
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import { deepMerge, migrateAgentConfig } from "../../shared"
|
||||
import { resolvePromptAppend } from "./resolve-file-uri"
|
||||
|
||||
/**
|
||||
* Expands a category reference from an agent override into concrete config properties.
|
||||
@@ -28,19 +29,23 @@ export function applyCategoryOverride(
|
||||
if (categoryConfig.maxTokens !== undefined) result.maxTokens = categoryConfig.maxTokens
|
||||
|
||||
if (categoryConfig.prompt_append && typeof result.prompt === "string") {
|
||||
result.prompt = result.prompt + "\n" + categoryConfig.prompt_append
|
||||
result.prompt = result.prompt + "\n" + resolvePromptAppend(categoryConfig.prompt_append)
|
||||
}
|
||||
|
||||
return result as AgentConfig
|
||||
}
|
||||
|
||||
export function mergeAgentConfig(base: AgentConfig, override: AgentOverrideConfig): AgentConfig {
|
||||
export function mergeAgentConfig(
|
||||
base: AgentConfig,
|
||||
override: AgentOverrideConfig,
|
||||
directory?: string
|
||||
): AgentConfig {
|
||||
const migratedOverride = migrateAgentConfig(override as Record<string, unknown>) as AgentOverrideConfig
|
||||
const { prompt_append, ...rest } = migratedOverride
|
||||
const merged = deepMerge(base, rest as Partial<AgentConfig>)
|
||||
|
||||
if (prompt_append && merged.prompt) {
|
||||
merged.prompt = merged.prompt + "\n" + prompt_append
|
||||
merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append, directory)
|
||||
}
|
||||
|
||||
return merged
|
||||
@@ -49,7 +54,8 @@ export function mergeAgentConfig(base: AgentConfig, override: AgentOverrideConfi
|
||||
export function applyOverrides(
|
||||
config: AgentConfig,
|
||||
override: AgentOverrideConfig | undefined,
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
mergedCategories: Record<string, CategoryConfig>,
|
||||
directory?: string
|
||||
): AgentConfig {
|
||||
let result = config
|
||||
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
@@ -58,7 +64,7 @@ export function applyOverrides(
|
||||
}
|
||||
|
||||
if (override) {
|
||||
result = mergeAgentConfig(result, override)
|
||||
result = mergeAgentConfig(result, override, directory)
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -16,6 +16,7 @@ export function maybeCreateAtlasConfig(input: {
|
||||
availableAgents: AvailableAgent[]
|
||||
availableSkills: AvailableSkill[]
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
directory?: string
|
||||
userCategories?: CategoriesConfig
|
||||
useTaskSystem?: boolean
|
||||
}): AgentConfig | undefined {
|
||||
@@ -28,6 +29,7 @@ export function maybeCreateAtlasConfig(input: {
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
mergedCategories,
|
||||
directory,
|
||||
userCategories,
|
||||
} = input
|
||||
|
||||
@@ -58,7 +60,7 @@ export function maybeCreateAtlasConfig(input: {
|
||||
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
||||
}
|
||||
|
||||
orchestratorConfig = applyOverrides(orchestratorConfig, orchestratorOverride, mergedCategories)
|
||||
orchestratorConfig = applyOverrides(orchestratorConfig, orchestratorOverride, mergedCategories, directory)
|
||||
|
||||
return orchestratorConfig
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function collectPendingBuiltinAgents(input: {
|
||||
config = applyEnvironmentContext(config, directory)
|
||||
}
|
||||
|
||||
config = applyOverrides(config, override, mergedCategories)
|
||||
config = applyOverrides(config, override, mergedCategories, directory)
|
||||
|
||||
// Store for later - will be added after sisyphus and hephaestus
|
||||
pendingAgentConfigs.set(name, config)
|
||||
|
||||
@@ -85,7 +85,7 @@ export function maybeCreateHephaestusConfig(input: {
|
||||
}
|
||||
|
||||
if (hephaestusOverride) {
|
||||
hephaestusConfig = mergeAgentConfig(hephaestusConfig, hephaestusOverride)
|
||||
hephaestusConfig = mergeAgentConfig(hephaestusConfig, hephaestusOverride, directory)
|
||||
}
|
||||
return hephaestusConfig
|
||||
}
|
||||
|
||||
109
src/agents/builtin-agents/resolve-file-uri.test.ts
Normal file
109
src/agents/builtin-agents/resolve-file-uri.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { homedir, tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { resolvePromptAppend } from "./resolve-file-uri"
|
||||
|
||||
describe("resolvePromptAppend", () => {
|
||||
const fixtureRoot = join(tmpdir(), `resolve-file-uri-${Date.now()}`)
|
||||
const configDir = join(fixtureRoot, "config")
|
||||
const homeFixtureDir = join(homedir(), `.resolve-file-uri-home-${Date.now()}`)
|
||||
|
||||
const absoluteFilePath = join(fixtureRoot, "absolute.txt")
|
||||
const relativeFilePath = join(configDir, "relative.txt")
|
||||
const spacedFilePath = join(fixtureRoot, "with space.txt")
|
||||
const homeFilePath = join(homeFixtureDir, "home.txt")
|
||||
|
||||
beforeAll(() => {
|
||||
mkdirSync(fixtureRoot, { recursive: true })
|
||||
mkdirSync(configDir, { recursive: true })
|
||||
mkdirSync(homeFixtureDir, { recursive: true })
|
||||
|
||||
writeFileSync(absoluteFilePath, "absolute-content", "utf8")
|
||||
writeFileSync(relativeFilePath, "relative-content", "utf8")
|
||||
writeFileSync(spacedFilePath, "encoded-content", "utf8")
|
||||
writeFileSync(homeFilePath, "home-content", "utf8")
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(fixtureRoot, { recursive: true, force: true })
|
||||
rmSync(homeFixtureDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test("returns non-file URI strings unchanged", () => {
|
||||
//#given
|
||||
const input = "append this text"
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe(input)
|
||||
})
|
||||
|
||||
test("resolves absolute file URI to file contents", () => {
|
||||
//#given
|
||||
const input = `file://${absoluteFilePath}`
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe("absolute-content")
|
||||
})
|
||||
|
||||
test("resolves relative file URI using configDir", () => {
|
||||
//#given
|
||||
const input = "file://./relative.txt"
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input, configDir)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe("relative-content")
|
||||
})
|
||||
|
||||
test("resolves home directory URI path", () => {
|
||||
//#given
|
||||
const input = `file://~/${homeFixtureDir.split("/").pop()}/home.txt`
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe("home-content")
|
||||
})
|
||||
|
||||
test("resolves percent-encoded URI path", () => {
|
||||
//#given
|
||||
const input = `file://${encodeURIComponent(spacedFilePath)}`
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe("encoded-content")
|
||||
})
|
||||
|
||||
test("returns warning for malformed percent-encoding", () => {
|
||||
//#given
|
||||
const input = "file://%E0%A4%A"
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toContain("[WARNING: Malformed file URI")
|
||||
})
|
||||
|
||||
test("returns warning when file does not exist", () => {
|
||||
//#given
|
||||
const input = "file:///path/does/not/exist.txt"
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toContain("[WARNING: Could not resolve file URI")
|
||||
})
|
||||
})
|
||||
30
src/agents/builtin-agents/resolve-file-uri.ts
Normal file
30
src/agents/builtin-agents/resolve-file-uri.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { isAbsolute, resolve } from "node:path"
|
||||
|
||||
export function resolvePromptAppend(promptAppend: string, configDir?: string): string {
|
||||
if (!promptAppend.startsWith("file://")) return promptAppend
|
||||
|
||||
const encoded = promptAppend.slice(7)
|
||||
|
||||
let filePath: string
|
||||
try {
|
||||
const decoded = decodeURIComponent(encoded)
|
||||
const expanded = decoded.startsWith("~/") ? decoded.replace(/^~\//, `${homedir()}/`) : decoded
|
||||
filePath = isAbsolute(expanded)
|
||||
? expanded
|
||||
: resolve(configDir ?? process.cwd(), expanded)
|
||||
} catch {
|
||||
return `[WARNING: Malformed file URI (invalid percent-encoding): ${promptAppend}]`
|
||||
}
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
return `[WARNING: Could not resolve file URI: ${promptAppend}]`
|
||||
}
|
||||
|
||||
try {
|
||||
return readFileSync(filePath, "utf8")
|
||||
} catch {
|
||||
return `[WARNING: Could not read file: ${promptAppend}]`
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export function maybeCreateSisyphusConfig(input: {
|
||||
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
||||
}
|
||||
|
||||
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories)
|
||||
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories, directory)
|
||||
sisyphusConfig = applyEnvironmentContext(sisyphusConfig, directory)
|
||||
|
||||
return sisyphusConfig
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* - Extended reasoning context for complex tasks
|
||||
*/
|
||||
|
||||
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
|
||||
|
||||
export function buildDefaultSisyphusJuniorPrompt(
|
||||
useTaskSystem: boolean,
|
||||
promptAppend?: string
|
||||
@@ -40,7 +42,7 @@ Task NOT complete without:
|
||||
</Style>`
|
||||
|
||||
if (!promptAppend) return prompt
|
||||
return prompt + "\n\n" + promptAppend
|
||||
return prompt + "\n\n" + resolvePromptAppend(promptAppend)
|
||||
}
|
||||
|
||||
function buildConstraintsSection(useTaskSystem: boolean): string {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
* - Explicit decision criteria needed (model won't infer)
|
||||
*/
|
||||
|
||||
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
|
||||
|
||||
export function buildGptSisyphusJuniorPrompt(
|
||||
useTaskSystem: boolean,
|
||||
promptAppend?: string
|
||||
@@ -85,7 +87,7 @@ Task NOT complete without evidence:
|
||||
</style_spec>`
|
||||
|
||||
if (!promptAppend) return prompt
|
||||
return prompt + "\n\n" + promptAppend
|
||||
return prompt + "\n\n" + resolvePromptAppend(promptAppend)
|
||||
}
|
||||
|
||||
function buildGptBlockedActionsSection(useTaskSystem: boolean): string {
|
||||
|
||||
@@ -12,6 +12,7 @@ export const AgentOverrideConfigSchema = z.object({
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
top_p: z.number().min(0).max(1).optional(),
|
||||
prompt: z.string().optional(),
|
||||
/** Text to append to agent prompt. Supports file:// URIs (file:///abs, file://./rel, file://~/home) */
|
||||
prompt_append: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
disable: z.boolean().optional(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CategoryConfig } from "../config/schema";
|
||||
import { PROMETHEUS_PERMISSION, PROMETHEUS_SYSTEM_PROMPT } from "../agents/prometheus";
|
||||
import { resolvePromptAppend } from "../agents/builtin-agents/resolve-file-uri";
|
||||
import { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements";
|
||||
import {
|
||||
fetchAvailableModels,
|
||||
@@ -92,7 +93,7 @@ export async function buildPrometheusAgentConfig(params: {
|
||||
const { prompt_append, ...restOverride } = override;
|
||||
const merged = { ...base, ...restOverride };
|
||||
if (prompt_append && typeof merged.prompt === "string") {
|
||||
merged.prompt = merged.prompt + "\n" + prompt_append;
|
||||
merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user