import { ZERO_COST } from '../constants/limits.js' import { assembleSystemPrompt } from '../persona/assembler.js' import { resolveModelPricing } from '../pricing/index.js' import { collectChatCompletion } from '../provider/collect-chat-completion.js' import { SCOPE_ATTRIBUTE } from '../utils/log/types.js' import { GENAI } from '../constants/telemetry/index.js' import type { AdvisorDefinition, AdvisoryBudget } from '../types/advisory/config.js' import type { AdvisoryRequest, AdvisoryResult } from '../types/advisory/result.js' import type { CostInfo, TokenUsage } from '../types/common/index.js' import { type Message, createSystemMessage, createUserMessage } from '../types/message/index.js' import type { LLMToolSchema } from '../types/tool/index.js' import { accumulateUnpricedCost, calculateCost } from '../utils/cost.js' import { type Logger, resolveLogger } from '../utils/logger.js' import { renderAdvisoryHistory } from './history.js' import { ADVISORY_RESPONSE_CONTRACT, parseAdvisoryResponse } from './parse.js' /** * What the advisor is told about how urgent the caller said this is. * * `'normal'` says nothing on purpose. A sentence asserting the ordinary case * is prompt weight that changes no answer, and stating it on every call * would make the two that matter harder to notice. */ const URGENCY_DIRECTION: Record<'low' | 'normal' | 'high', string | undefined> = { high: 'This request is marked URGENT. Lead with the single most important action and keep the reasoning to what is needed to justify it.', normal: undefined, low: 'This request is marked low urgency. There is room to note secondary considerations and alternatives worth weighing.', } /** One successfully dispatched SDK request and the records appended after it. */ export interface AdvisoryTurnContext { readonly iteration: number /** Isolated at dispatch, including request-only evidence; not provider wire bytes. */ readonly requestMessages: readonly Message[] /** Starts with that request's assistant response; may include tools and newer input. */ readonly subsequentMessages: readonly Message[] } export interface AdvisoryCallContext { readonly messages: Message[] /** Optional exact-turn trajectory, preferred over the canonical history projection. */ readonly turn?: AdvisoryTurnContext readonly workingStateSummary?: string readonly toolCatalog?: LLMToolSchema[] readonly iteration: number } export interface AdvisoryExecutionResult { readonly result: AdvisoryResult readonly usage: TokenUsage readonly cost: CostInfo readonly durationMs: number } export class AdvisoryExecutor { private readonly logger: Logger private readonly budget: AdvisoryBudget | undefined private readonly signal: AbortSignal | undefined constructor(logger?: Logger, budget?: AdvisoryBudget, signal?: AbortSignal) { this.logger = resolveLogger(logger).child({ [SCOPE_ATTRIBUTE]: 'advisory/executor' }) this.budget = budget this.signal = signal } /** * The response ceiling for one call: the tighter of what the advisor * asks for and what the budget allows. An advisor that names no ceiling * still gets the budget's, which is the whole point of a per-call cap. */ private responseTokenCeiling(advisor: AdvisorDefinition): number | undefined { const cap = this.budget?.maxTokensPerCall if (cap === undefined) return advisor.maxResponseTokens if (advisor.maxResponseTokens === undefined) return cap return Math.min(advisor.maxResponseTokens, cap) } async consult( advisor: AdvisorDefinition, request: AdvisoryRequest, callCtx: AdvisoryCallContext, ): Promise { const startMs = Date.now() const systemPrompt = this.buildSystemPrompt(advisor, request.urgency) const contextMessages = this.buildContext(advisor, request, callCtx) const messages: Message[] = [ createSystemMessage(systemPrompt), ...contextMessages, createUserMessage(request.question), ] this.logger.debug('advisory call starting', { 'namzu.advisory.id': advisor.id, [GENAI.REQUEST_MODEL]: advisor.model, 'namzu.advisory.message_count': messages.length, 'namzu.advisory.urgency': request.urgency, }) const response = await collectChatCompletion( advisor.provider.chatStream({ model: advisor.model, messages, temperature: advisor.temperature, maxTokens: this.responseTokenCeiling(advisor), toolChoice: 'none', ...(this.signal ? { signal: this.signal } : {}), }), ) const durationMs = Date.now() - startMs const result = parseAdvisoryResponse(response.message.content ?? '') const cost = this.computeCost(advisor, response.usage) this.logger.info('advisory call completed', { 'namzu.advisory.id': advisor.id, [GENAI.REQUEST_MODEL]: advisor.model, 'namzu.duration_ms': durationMs, 'namzu.usage.total_tokens': response.usage.totalTokens, }) return { result, usage: response.usage, cost, durationMs, } } private buildSystemPrompt( advisor: AdvisorDefinition, urgency?: AdvisoryRequest['urgency'], ): string { // The contract is appended to every branch, not folded into the // default: an advisor with its own prompt or a persona is still read // back by the same parser, and used to be the one never told so. const parts = [this.describeAdvisor(advisor), ADVISORY_RESPONSE_CONTRACT] // The caller is invited to say how urgent this is, and the value used // to reach exactly one debug log line — `urgency: 'high'` and // `urgency: 'low'` produced byte-identical requests. Telling the // ADVISOR is the honest minimum: it is the party that can act on the // answer, and it costs one sentence rather than a routing policy this // kernel has no business inventing. const direction = URGENCY_DIRECTION[urgency ?? 'normal'] if (direction) parts.push(direction) return parts.join('\n\n') } private describeAdvisor(advisor: AdvisorDefinition): string { if (advisor.systemPrompt) { return advisor.systemPrompt } if (advisor.persona) { return assembleSystemPrompt(advisor.persona) } return [ `You are ${advisor.name}, an advisory agent.`, advisor.domains && advisor.domains.length > 0 ? `Your domains of expertise: ${advisor.domains.join(', ')}.` : undefined, 'Provide concise, actionable advice. Focus on what the agent should do next.', ] .filter(Boolean) .join('\n\n') } private buildContext( advisor: AdvisorDefinition, request: AdvisoryRequest, callCtx: AdvisoryCallContext, ): Message[] { if (request.includeContext === false) { return [] } const contextParts: string[] = [] if (callCtx.workingStateSummary) { contextParts.push(`## Working State\n${callCtx.workingStateSummary}`) } if (callCtx.toolCatalog && callCtx.toolCatalog.length > 0) { const toolLines = callCtx.toolCatalog.map((tool) => { const description = tool.function.description?.trim() return description ? `- ${tool.function.name}: ${description}` : `- ${tool.function.name}` }) contextParts.push( [ '## Runtime Tool Summary', 'These tools are available to the executor. Their executable schemas remain owned by the runtime tool catalogue; use this as advisory context only.', toolLines.join('\n'), ].join('\n'), ) } const history = renderAdvisoryHistory(callCtx.messages, advisor.maxContextTokens, callCtx.turn) if (history) contextParts.push(history) if (contextParts.length === 0) { return [] } return [createUserMessage(contextParts.join('\n\n'))] } /** * Cost for one call, from the advisor's own pricing, then the catalogue. * * When neither has a rate this reports the tokens as UNPRICED rather than * as a cost of zero. The previous version returned a zero rate card and a * zero total, defended on the grounds that a cost CAP over unpriced * advisors is refused at construction so nothing enforces against it — true * of the cap, and beside the point for the reader. `AdvisoryResult.cost` is * reported to the host, and `$0.00` for a call that cost real money is the * exact defect this change exists to remove; it does not become acceptable * because the number happens to be unenforced. */ private computeCost(advisor: AdvisorDefinition, usage: TokenUsage): CostInfo { const pricing = advisor.pricing ?? resolveModelPricing(advisor.provider.id, advisor.model) if (!pricing) { return accumulateUnpricedCost(ZERO_COST, usage) } return calculateCost(usage, pricing) } }