diff options
Diffstat (limited to 'packages/system-prompt/src/service.ts')
| -rw-r--r-- | packages/system-prompt/src/service.ts | 177 |
1 files changed, 106 insertions, 71 deletions
diff --git a/packages/system-prompt/src/service.ts b/packages/system-prompt/src/service.ts index 8d6ede5..2741c98 100644 --- a/packages/system-prompt/src/service.ts +++ b/packages/system-prompt/src/service.ts @@ -29,20 +29,62 @@ const TEMPLATE_KEY = "template"; const resolvedKey = (conversationId: string): string => `resolved:${conversationId}`; const resolvedCwdKey = (conversationId: string): string => `resolved-cwd:${conversationId}`; const resolvedComputerIdKey = (conversationId: string): string => - `resolved-computer:${conversationId}`; + `resolved-computer:${conversationId}`; export interface SystemPromptServiceDeps { - /** Namespaced KV (`host.storage("system-prompt")`). */ - readonly storage: StorageNamespace; - /** Injected effects for variable resolution (local). */ - readonly adapters: ResolverAdapters; - /** - * Optional: build remote-backed adapters for a given computerId. When - * `construct` is called with a `computerId`, this is invoked to obtain - * adapters that read/run commands on the REMOTE machine (via the - * ExecBackend/SSH). Absent → falls back to the local `adapters`. - */ - readonly resolveRemoteAdapters?: (computerId: string, cwd: string) => Promise<ResolverAdapters>; + /** Namespaced KV (`host.storage("system-prompt")`). */ + readonly storage: StorageNamespace; + /** Injected effects for variable resolution (local). */ + readonly adapters: ResolverAdapters; + /** + * Optional: build remote-backed adapters for a given computerId. When + * `construct` is called with a `computerId`, this is invoked to obtain + * adapters that read/run commands on the REMOTE machine (via the + * ExecBackend/SSH). Absent → falls back to the local `adapters`. + */ + readonly resolveRemoteAdapters?: (computerId: string, cwd: string) => Promise<ResolverAdapters>; +} + +/** + * Resolve a template against the current environment (the shared resolution + * path used by both `construct` — which persists the result — and + * `resolveText`, which does not). Always resolves the fixed catalog + * (`system:*`, `prompt:*`, `git:*`) plus any `file:<path>` keys referenced by + * the template. Selects remote-backed adapters when `context.computerId` is + * set, mirroring `construct`. + */ +async function resolveTemplate( + deps: SystemPromptServiceDeps, + template: string, + cwd: string, + context?: { + readonly model?: string; + readonly conversationId?: string; + readonly workspaceId?: string; + readonly computerId?: string; + }, +): Promise<string> { + const referencedKeys = extractVariables(template); + const resolverContext: ResolverContext = { + ...(context?.conversationId !== undefined ? { conversationId: context.conversationId } : {}), + ...(context?.model !== undefined ? { model: context.model } : {}), + ...(context?.workspaceId !== undefined ? { workspaceId: context.workspaceId } : {}), + }; + + // Select adapters: when computerId is set, use remote-backed adapters + // (read files / run commands on the REMOTE machine via SSH). Otherwise + // use the local adapters. + const computerId = context?.computerId; + const adapters = + computerId !== undefined && deps.resolveRemoteAdapters !== undefined + ? await deps.resolveRemoteAdapters(computerId, cwd) + : deps.adapters; + + const vars = await resolveVariables(cwd, adapters, { + context: resolverContext, + referencedKeys, + }); + return parseTemplate(template, vars); } /** @@ -50,63 +92,56 @@ export interface SystemPromptServiceDeps { * State is owned (not ambient): the storage reference lives in this closure. */ export function createSystemPromptService(deps: SystemPromptServiceDeps): SystemPromptService { - return { - async construct(conversationId, cwd, context) { - let template = await deps.storage.get(TEMPLATE_KEY); - if (template === null) template = DEFAULT_TEMPLATE; - - const referencedKeys = extractVariables(template); - const resolverContext: ResolverContext = { - conversationId, - ...(context?.model !== undefined ? { model: context.model } : {}), - ...(context?.workspaceId !== undefined ? { workspaceId: context.workspaceId } : {}), - }; - - // Select adapters: when computerId is set, use remote-backed adapters - // (read files / run commands on the REMOTE machine via SSH). Otherwise - // use the local adapters. - const computerId = context?.computerId; - const adapters = - computerId !== undefined && deps.resolveRemoteAdapters !== undefined - ? await deps.resolveRemoteAdapters(computerId, cwd) - : deps.adapters; - - const vars = await resolveVariables(cwd, adapters, { - context: resolverContext, - referencedKeys, - }); - const result = parseTemplate(template, vars); - - await deps.storage.set(resolvedKey(conversationId), result); - await deps.storage.set(resolvedCwdKey(conversationId), cwd); - // Store the computerId (or empty string for local) so the cache can be - // invalidated when the computer changes. - await deps.storage.set(resolvedComputerIdKey(conversationId), computerId ?? ""); - return result; - }, - - async get(conversationId) { - return deps.storage.get(resolvedKey(conversationId)); - }, - - async getWithMeta(conversationId) { - const [prompt, cwd, computerIdStored] = await Promise.all([ - deps.storage.get(resolvedKey(conversationId)), - deps.storage.get(resolvedCwdKey(conversationId)), - deps.storage.get(resolvedComputerIdKey(conversationId)), - ]); - // Empty string → null (local, no computerId). Non-empty → the alias. - const computerId = computerIdStored === null ? null : computerIdStored || null; - return { prompt, cwd, computerId }; - }, - - async getTemplate() { - const stored = await deps.storage.get(TEMPLATE_KEY); - return stored ?? DEFAULT_TEMPLATE; - }, - - async setTemplate(template) { - await deps.storage.set(TEMPLATE_KEY, template); - }, - }; + return { + async construct(conversationId, cwd, context) { + let template = await deps.storage.get(TEMPLATE_KEY); + if (template === null) template = DEFAULT_TEMPLATE; + + const result = await resolveTemplate(deps, template, cwd, { + conversationId, + ...(context?.model !== undefined ? { model: context.model } : {}), + ...(context?.workspaceId !== undefined ? { workspaceId: context.workspaceId } : {}), + ...(context?.computerId !== undefined ? { computerId: context.computerId } : {}), + }); + + await deps.storage.set(resolvedKey(conversationId), result); + await deps.storage.set(resolvedCwdKey(conversationId), cwd); + // Store the computerId (or empty string for local) so the cache can be + // invalidated when the computer changes. + await deps.storage.set(resolvedComputerIdKey(conversationId), context?.computerId ?? ""); + return result; + }, + + async resolveText(template, cwd, context) { + // An empty template has no variables to resolve; short-circuit to + // avoid spawning git / reading files for nothing (the heartbeat's + // default-empty prompts hit this every run). + if (template === "") return ""; + return resolveTemplate(deps, template, cwd, context); + }, + + async get(conversationId) { + return deps.storage.get(resolvedKey(conversationId)); + }, + + async getWithMeta(conversationId) { + const [prompt, cwd, computerIdStored] = await Promise.all([ + deps.storage.get(resolvedKey(conversationId)), + deps.storage.get(resolvedCwdKey(conversationId)), + deps.storage.get(resolvedComputerIdKey(conversationId)), + ]); + // Empty string → null (local, no computerId). Non-empty → the alias. + const computerId = computerIdStored === null ? null : computerIdStored || null; + return { prompt, cwd, computerId }; + }, + + async getTemplate() { + const stored = await deps.storage.get(TEMPLATE_KEY); + return stored ?? DEFAULT_TEMPLATE; + }, + + async setTemplate(template) { + await deps.storage.set(TEMPLATE_KEY, template); + }, + }; } |
