import type { Model } from '../../provider/pi-ai-types.js'; import type { MiniAgentEvent, MiniAgentResult } from '../subagent/agent-events.js'; import type { ChatResult, MossAgentConfig, MossAgentEvent } from './moss-agent-types.js'; import type { ToolCall, ToolResult } from '../tools/tool-types.js'; type ModelBridgeConfig = Pick< MossAgentConfig, | 'contextTokens' | 'llmProvider' | 'maxTokens' | 'model' | 'reasoning' | 'roundTripAssistantThinking' | 'api' >; function asRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {}; } function normalizePublicStopReason(reason: string): string { if (reason === 'stop') return 'end_turn'; if (reason === 'length') return 'max_tokens'; if (reason === 'toolUse' || reason === 'toolCall') return 'tool_use'; return reason; } export function createModelDefFromMossConfig(config: ModelBridgeConfig): Model { const modelId = String(config.model || 'dmoss-default-model'); const roundTripsThinkingHistory = config.roundTripAssistantThinking === true || Boolean(config.reasoning); return { id: modelId, name: modelId, api: config.api ?? 'openai-completions', provider: config.llmProvider.id, baseUrl: '', reasoning: roundTripsThinkingHistory, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: config.contextTokens ?? 200_000, maxTokens: config.maxTokens ?? 4096, }; } export interface MossAgentLoopEventAdapter { onMiniEvent(event: MiniAgentEvent): MossAgentEvent[]; getResult(result: MiniAgentResult): ChatResult; getDoneEvent(result: MiniAgentResult): Extract; } export interface MossAgentLoopEventAdapterOptions { isAbortError?: (error: string) => boolean; isAborted?: () => boolean; contextTokens?: number; } export function createMossAgentLoopEventAdapter( options?: MossAgentLoopEventAdapterOptions ): MossAgentLoopEventAdapter { let response = ''; const thinking: string[] = []; const toolCalls: ToolCall[] = []; const toolResults: ToolResult[] = []; let usage: | { inputTokens: number; outputTokens: number; cacheReadTokens?: number; cacheCreationTokens?: number; } | undefined; let compactions = 0; let stopReason = 'unknown'; const getResult = (result: MiniAgentResult): ChatResult => { const toolBudgetReached = toolResults.some( (toolResult) => toolResult.outcome === 'blocked' && /tool budget reached/i.test(toolResult.content), ); return { response: response || result.finalText, toolCalls, toolResults, ...(usage ? { usage } : {}), ...(thinking.length > 0 ? { thinking } : {}), ...(compactions > 0 ? { compactions } : {}), stopReason: toolBudgetReached ? 'tool_budget_reached' : options?.isAborted?.() ? 'aborted_by_user' : stopReason === 'unknown' && (response || result.finalText) ? 'end_turn' : stopReason, }; }; return { onMiniEvent(event) { switch (event.type) { case 'message_delta': response += event.delta; return [{ type: 'text_delta', delta: event.delta }]; case 'message_end': response = event.text; return []; case 'thinking_delta': thinking.push(event.delta); return [{ type: 'thinking_delta', delta: event.delta }]; case 'tool_execution_start': { const input = asRecord(event.args); toolCalls.push({ id: event.toolCallId, name: event.toolName, input }); return [ { type: 'tool_start', toolCallId: event.toolCallId, toolName: event.toolName, input, }, ]; } case 'tool_execution_end': { const content = event.content ?? event.result; toolResults.push({ toolUseId: event.toolCallId, content, isError: event.isError, ...(event.outcome ? { outcome: event.outcome } : {}), ...(event.durationMs !== undefined ? { durationMs: event.durationMs } : {}), ...(event.aborted ? { aborted: event.aborted } : {}), ...(event.structuredContent ? { structuredContent: event.structuredContent } : {}), }); return [ { type: 'tool_end', toolCallId: event.toolCallId, toolName: event.toolName, result: event.result, isError: event.isError, ...(event.outcome ? { outcome: event.outcome } : {}), ...(event.durationMs !== undefined ? { durationMs: event.durationMs } : {}), ...(event.aborted ? { aborted: event.aborted } : {}), ...(event.structuredContent ? { structuredContent: event.structuredContent } : {}), }, ]; } case 'turn_start': return [{ type: 'turn_start', turn: event.turn }]; case 'turn_end': { const incomingStopReason = event.stopReason ? normalizePublicStopReason(event.stopReason) : undefined; const resolvedStopReason = incomingStopReason ?? (stopReason === 'unknown' && response ? 'end_turn' : stopReason); stopReason = resolvedStopReason; return [ { type: 'turn_end', turn: event.turn, stopReason: resolvedStopReason, ...(event.totalToolCalls !== undefined ? { totalToolCalls: event.totalToolCalls } : {}), }, ]; } case 'turn_transition': stopReason = normalizePublicStopReason(event.reason); return []; case 'llm_usage': { const cacheReadTokens = (usage?.cacheReadTokens ?? 0) + (event.cacheReadTokens ?? 0); const cacheCreationTokens = (usage?.cacheCreationTokens ?? 0) + (event.cacheCreationTokens ?? 0); usage = { inputTokens: (usage?.inputTokens ?? 0) + event.inputTokens, outputTokens: (usage?.outputTokens ?? 0) + event.outputTokens, ...(cacheReadTokens > 0 ? { cacheReadTokens } : {}), ...(cacheCreationTokens > 0 ? { cacheCreationTokens } : {}), }; return [ { type: 'llm_usage' as const, inputTokens: event.inputTokens, outputTokens: event.outputTokens, ...(event.cacheReadTokens !== undefined ? { cacheReadTokens: event.cacheReadTokens } : {}), ...(event.cacheCreationTokens !== undefined ? { cacheCreationTokens: event.cacheCreationTokens } : {}), ...(options?.contextTokens ? { contextTokens: options.contextTokens } : {}), }, ]; } case 'compaction': compactions += 1; return [ { type: 'compaction', summaryChars: event.summaryChars, droppedMessages: event.droppedMessages, ...(event.checkpointOutline ? { checkpointOutline: event.checkpointOutline } : {}), }, ]; case 'context_action': { const microcompact = event.actions.find((action) => action.kind === 'microcompact'); if (!microcompact) return []; return [ { type: 'microcompact', compressedCount: microcompact.count, savedChars: microcompact.savedChars, savedTokens: microcompact.savedTokens, }, ]; } case 'working_context_checkpoint': return [ { type: 'working_context_checkpoint', status: event.status, reason: event.reason, goal: event.goal, nextAction: event.nextAction, }, ]; case 'run_metrics': compactions = Math.max(compactions, event.metrics.contextCompactions); return [ { type: 'cache_metrics', promptCacheEnabled: Boolean(event.metrics.promptCacheEnabled), promptCacheDebug: Boolean(event.metrics.promptCacheDebug), stableChars: event.metrics.promptCacheStableChars ?? 0, dynamicChars: event.metrics.promptCacheDynamicChars ?? 0, eligible: Boolean(event.metrics.promptCacheEligible), eligibilityReason: event.metrics.promptCacheEligibilityReason ?? 'missing_stable_prefix', minStableChars: event.metrics.promptCacheMinStableChars ?? 0, maxDynamicCharsRatio: event.metrics.promptCacheMaxDynamicCharsRatio ?? 0, prefixChecks: event.metrics.promptPrefixChecks ?? 0, prefixChanges: event.metrics.promptPrefixChanges ?? 0, toolOrderChecks: event.metrics.promptToolOrderChecks ?? 0, toolOrderChanges: event.metrics.promptToolOrderChanges ?? 0, cacheReadTokens: usage?.cacheReadTokens ?? 0, cacheCreationTokens: usage?.cacheCreationTokens ?? 0, }, ]; case 'retry': // Emit a retry event so the host/TUI can clear partial visible output // from the failed attempt before the new attempt streams fresh deltas. // Previously this was `return []` — the host saw no signal, so // retried-attempt deltas were appended to the failed attempt's // partial text, producing duplicated/garbled output. // (Found by moss self-iteration — glm-5.2 reviewed stream helpers.) return [{ type: 'retry' as const, attempt: event.attempt, error: event.error }]; case 'context_overflow_compact': return []; case 'agent_error': stopReason = options?.isAbortError?.(event.error) ? 'aborted_by_user' : 'error'; return [ { type: 'error', error: event.error, retriable: false, ...(event.surface ? { errorSurface: event.surface } : {}), }, ]; default: return []; } }, getResult, getDoneEvent(result) { return { type: 'done', result: getResult(result) }; }, }; }