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,
|
availableAgents,
|
||||||
availableSkills,
|
availableSkills,
|
||||||
mergedCategories,
|
mergedCategories,
|
||||||
|
directory,
|
||||||
userCategories: categories,
|
userCategories: categories,
|
||||||
})
|
})
|
||||||
if (atlasConfig) {
|
if (atlasConfig) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { AgentConfig } from "@opencode-ai/sdk"
|
|||||||
import type { AgentOverrideConfig } from "../types"
|
import type { AgentOverrideConfig } from "../types"
|
||||||
import type { CategoryConfig } from "../../config/schema"
|
import type { CategoryConfig } from "../../config/schema"
|
||||||
import { deepMerge, migrateAgentConfig } from "../../shared"
|
import { deepMerge, migrateAgentConfig } from "../../shared"
|
||||||
|
import { resolvePromptAppend } from "./resolve-file-uri"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expands a category reference from an agent override into concrete config properties.
|
* 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.maxTokens !== undefined) result.maxTokens = categoryConfig.maxTokens
|
||||||
|
|
||||||
if (categoryConfig.prompt_append && typeof result.prompt === "string") {
|
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
|
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 migratedOverride = migrateAgentConfig(override as Record<string, unknown>) as AgentOverrideConfig
|
||||||
const { prompt_append, ...rest } = migratedOverride
|
const { prompt_append, ...rest } = migratedOverride
|
||||||
const merged = deepMerge(base, rest as Partial<AgentConfig>)
|
const merged = deepMerge(base, rest as Partial<AgentConfig>)
|
||||||
|
|
||||||
if (prompt_append && merged.prompt) {
|
if (prompt_append && merged.prompt) {
|
||||||
merged.prompt = merged.prompt + "\n" + prompt_append
|
merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append, directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
return merged
|
return merged
|
||||||
@@ -49,7 +54,8 @@ export function mergeAgentConfig(base: AgentConfig, override: AgentOverrideConfi
|
|||||||
export function applyOverrides(
|
export function applyOverrides(
|
||||||
config: AgentConfig,
|
config: AgentConfig,
|
||||||
override: AgentOverrideConfig | undefined,
|
override: AgentOverrideConfig | undefined,
|
||||||
mergedCategories: Record<string, CategoryConfig>
|
mergedCategories: Record<string, CategoryConfig>,
|
||||||
|
directory?: string
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
let result = config
|
let result = config
|
||||||
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
|
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
|
||||||
@@ -58,7 +64,7 @@ export function applyOverrides(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (override) {
|
if (override) {
|
||||||
result = mergeAgentConfig(result, override)
|
result = mergeAgentConfig(result, override, directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function maybeCreateAtlasConfig(input: {
|
|||||||
availableAgents: AvailableAgent[]
|
availableAgents: AvailableAgent[]
|
||||||
availableSkills: AvailableSkill[]
|
availableSkills: AvailableSkill[]
|
||||||
mergedCategories: Record<string, CategoryConfig>
|
mergedCategories: Record<string, CategoryConfig>
|
||||||
|
directory?: string
|
||||||
userCategories?: CategoriesConfig
|
userCategories?: CategoriesConfig
|
||||||
useTaskSystem?: boolean
|
useTaskSystem?: boolean
|
||||||
}): AgentConfig | undefined {
|
}): AgentConfig | undefined {
|
||||||
@@ -28,6 +29,7 @@ export function maybeCreateAtlasConfig(input: {
|
|||||||
availableAgents,
|
availableAgents,
|
||||||
availableSkills,
|
availableSkills,
|
||||||
mergedCategories,
|
mergedCategories,
|
||||||
|
directory,
|
||||||
userCategories,
|
userCategories,
|
||||||
} = input
|
} = input
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ export function maybeCreateAtlasConfig(input: {
|
|||||||
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
||||||
}
|
}
|
||||||
|
|
||||||
orchestratorConfig = applyOverrides(orchestratorConfig, orchestratorOverride, mergedCategories)
|
orchestratorConfig = applyOverrides(orchestratorConfig, orchestratorOverride, mergedCategories, directory)
|
||||||
|
|
||||||
return orchestratorConfig
|
return orchestratorConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function collectPendingBuiltinAgents(input: {
|
|||||||
config = applyEnvironmentContext(config, directory)
|
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
|
// Store for later - will be added after sisyphus and hephaestus
|
||||||
pendingAgentConfigs.set(name, config)
|
pendingAgentConfigs.set(name, config)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function maybeCreateHephaestusConfig(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hephaestusOverride) {
|
if (hephaestusOverride) {
|
||||||
hephaestusConfig = mergeAgentConfig(hephaestusConfig, hephaestusOverride)
|
hephaestusConfig = mergeAgentConfig(hephaestusConfig, hephaestusOverride, directory)
|
||||||
}
|
}
|
||||||
return hephaestusConfig
|
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 = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
||||||
}
|
}
|
||||||
|
|
||||||
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories)
|
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories, directory)
|
||||||
sisyphusConfig = applyEnvironmentContext(sisyphusConfig, directory)
|
sisyphusConfig = applyEnvironmentContext(sisyphusConfig, directory)
|
||||||
|
|
||||||
return sisyphusConfig
|
return sisyphusConfig
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
* - Extended reasoning context for complex tasks
|
* - Extended reasoning context for complex tasks
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
|
||||||
|
|
||||||
export function buildDefaultSisyphusJuniorPrompt(
|
export function buildDefaultSisyphusJuniorPrompt(
|
||||||
useTaskSystem: boolean,
|
useTaskSystem: boolean,
|
||||||
promptAppend?: string
|
promptAppend?: string
|
||||||
@@ -40,7 +42,7 @@ Task NOT complete without:
|
|||||||
</Style>`
|
</Style>`
|
||||||
|
|
||||||
if (!promptAppend) return prompt
|
if (!promptAppend) return prompt
|
||||||
return prompt + "\n\n" + promptAppend
|
return prompt + "\n\n" + resolvePromptAppend(promptAppend)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildConstraintsSection(useTaskSystem: boolean): string {
|
function buildConstraintsSection(useTaskSystem: boolean): string {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
* - Explicit decision criteria needed (model won't infer)
|
* - Explicit decision criteria needed (model won't infer)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
|
||||||
|
|
||||||
export function buildGptSisyphusJuniorPrompt(
|
export function buildGptSisyphusJuniorPrompt(
|
||||||
useTaskSystem: boolean,
|
useTaskSystem: boolean,
|
||||||
promptAppend?: string
|
promptAppend?: string
|
||||||
@@ -85,7 +87,7 @@ Task NOT complete without evidence:
|
|||||||
</style_spec>`
|
</style_spec>`
|
||||||
|
|
||||||
if (!promptAppend) return prompt
|
if (!promptAppend) return prompt
|
||||||
return prompt + "\n\n" + promptAppend
|
return prompt + "\n\n" + resolvePromptAppend(promptAppend)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGptBlockedActionsSection(useTaskSystem: boolean): string {
|
function buildGptBlockedActionsSection(useTaskSystem: boolean): string {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const AgentOverrideConfigSchema = z.object({
|
|||||||
temperature: z.number().min(0).max(2).optional(),
|
temperature: z.number().min(0).max(2).optional(),
|
||||||
top_p: z.number().min(0).max(1).optional(),
|
top_p: z.number().min(0).max(1).optional(),
|
||||||
prompt: z.string().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(),
|
prompt_append: z.string().optional(),
|
||||||
tools: z.record(z.string(), z.boolean()).optional(),
|
tools: z.record(z.string(), z.boolean()).optional(),
|
||||||
disable: z.boolean().optional(),
|
disable: z.boolean().optional(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { CategoryConfig } from "../config/schema";
|
import type { CategoryConfig } from "../config/schema";
|
||||||
import { PROMETHEUS_PERMISSION, PROMETHEUS_SYSTEM_PROMPT } from "../agents/prometheus";
|
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 { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements";
|
||||||
import {
|
import {
|
||||||
fetchAvailableModels,
|
fetchAvailableModels,
|
||||||
@@ -92,7 +93,7 @@ export async function buildPrometheusAgentConfig(params: {
|
|||||||
const { prompt_append, ...restOverride } = override;
|
const { prompt_append, ...restOverride } = override;
|
||||||
const merged = { ...base, ...restOverride };
|
const merged = { ...base, ...restOverride };
|
||||||
if (prompt_append && typeof merged.prompt === "string") {
|
if (prompt_append && typeof merged.prompt === "string") {
|
||||||
merged.prompt = merged.prompt + "\n" + prompt_append;
|
merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append);
|
||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user