Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions packages/opencode/src/session/dynamic-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import path from "path"
import os from "os"
import { Instance } from "../project/instance"

export namespace DynamicPrompt {
export interface Context {
/** Provider ID (e.g., "anthropic", "openai") */
providerID: string
/** Model ID (e.g., "claude-3-5-sonnet-20241022") */
modelID: string
/** Current working directory */
directory: string
/** Project root directory */
worktree: string
/** Whether the directory is a git repository */
isGitRepo: boolean
/** Current platform */
platform: NodeJS.Platform
/** Current username */
username: string
}

/**
* Type for the system prompt function that can be exported from TypeScript/JavaScript files
*/
export type SystemPromptFunction = (context: Context) => string | Promise<string>

/**
* Resolves a prompt string, handling dynamic TypeScript/JavaScript imports if needed
*/
export async function resolve(prompt: string, context: Context): Promise<string> {
// Check if prompt starts with file:// and points to a TS/JS file
if (!prompt.startsWith("file://")) {
return prompt
}

const filePath = prompt.slice(7) // Remove "file://" prefix
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(context.directory, filePath)

// Check if the file is a TypeScript or JavaScript file
const ext = path.extname(resolvedPath).toLowerCase()
if (![".ts", ".js", ".mts", ".mjs"].includes(ext)) {
// Not a TS/JS file, read as text (existing behavior)
return await Bun.file(resolvedPath).text()
}

try {
// Import the TypeScript/JavaScript file
const module = await import(resolvedPath)

// Try to get the system function (named export) or default export
const systemFunction = module.system || module.default

if (typeof systemFunction !== "function") {
throw new Error(
`Dynamic prompt file "${resolvedPath}" must export a "system" function or default function that returns a string`,
)
}

// Call the function with context and await if it returns a promise
const result = await systemFunction(context)

if (typeof result !== "string") {
throw new Error(`Dynamic prompt function in "${resolvedPath}" must return a string, got ${typeof result}`)
}

return result
} catch (error) {
throw new Error(
`Failed to load dynamic prompt from "${resolvedPath}": ${error instanceof Error ? error.message : String(error)}`,
)
}
}

/**
* Creates the context object for dynamic prompts
*/
export function createContext(input: {
providerID: string
modelID: string
username?: string
directory?: string
worktree?: string
isGitRepo?: boolean
}): Context {
let directory: string
let worktree: string
let isGitRepo: boolean

if (input.directory !== undefined && input.worktree !== undefined && input.isGitRepo !== undefined) {
// Use provided values (for testing)
directory = input.directory
worktree = input.worktree
isGitRepo = input.isGitRepo
} else {
// Use Instance values (production)
const project = Instance.project
directory = Instance.directory
worktree = Instance.worktree
isGitRepo = project.vcs === "git"
}

return {
providerID: input.providerID,
modelID: input.modelID,
directory,
worktree,
isGitRepo,
platform: process.platform,
username: input.username || os.userInfo().username,
}
}
}
38 changes: 31 additions & 7 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import { Instance } from "../project/instance"
import { Bus } from "../bus"
import { ProviderTransform } from "../provider/transform"
import { SystemPrompt } from "./system"
import { DynamicPrompt } from "./dynamic-prompt"
import { Plugin } from "../plugin"
import { Config } from "../config/config"

import PROMPT_PLAN from "../session/prompt/plan.txt"
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
Expand Down Expand Up @@ -401,13 +403,35 @@ export namespace SessionPrompt {
modelID: string
}) {
let system = SystemPrompt.header(input.providerID)
system.push(
...(() => {
if (input.system) return [input.system]
if (input.agent.prompt) return [input.agent.prompt]
return SystemPrompt.provider(input.modelID)
})(),
)

// Determine which prompt to use and potentially resolve dynamically
let promptToResolve: string | undefined
if (input.system) {
promptToResolve = input.system
} else if (input.agent.prompt) {
promptToResolve = input.agent.prompt
}

if (promptToResolve) {
const config = await Config.get()
const context = DynamicPrompt.createContext({
providerID: input.providerID,
modelID: input.modelID,
username: config.username,
})

try {
const resolvedPrompt = await DynamicPrompt.resolve(promptToResolve, context)
system.push(resolvedPrompt)
} catch (error) {
log.error("failed to resolve dynamic prompt", { error: error instanceof Error ? error.message : String(error) })
// Keep the original prompt if dynamic resolution fails
system.push(promptToResolve)
}
} else {
system.push(...SystemPrompt.provider(input.modelID))
}

system.push(...(await SystemPrompt.environment()))
system.push(...(await SystemPrompt.custom()))
// max 2 system prompt messages for caching purposes
Expand Down
Loading