import { BaseMessage } from "@langchain/core/messages"; import type { ToolContext } from "../shared/agent/tool.factory.js"; import { type ChatPersonaConfig } from "./chat.persona.js"; import type { DebugMetaToolCall, DebugMetaLlm, DebugMetaOrchestratorNegotiations, DebugMetaDiscoveryQuestions } from "./chat-streaming.types.js"; import type { Question, QuestionStrategy } from "../questions/domain/question.schema.js"; export { ITERATION_NUDGE } from "./chat.prompt.js"; /** * Writer callback for streaming custom data out of the graph node. * Matches the `config.writer` signature from LangGraphRunnableConfig. */ export type StreamWriter = (data: unknown) => void; /** * Events emitted by `streamRun()` via the writer callback. * * - `iteration_start` — new agent loop iteration begins * - `llm_start` — LLM begins generating response * - `text_chunk` — a token (or group of tokens) of model text * - `llm_end` — LLM finished generating (may have tool calls) * - `tool_activity` — tool starts or finishes execution * - `graph_start` — a LangGraph sub-graph begins inside a tool * - `graph_end` — a LangGraph sub-graph completes * - `agent_start` — an LLM agent begins inside a graph node * - `agent_end` — an LLM agent completes */ export type AgentStreamEvent = { type: "iteration_start"; iteration: number; } | { type: "llm_start"; iteration: number; } | { type: "text_chunk"; content: string; } | { type: "llm_end"; iteration: number; hasToolCalls: boolean; toolNames?: string[]; } | { type: "tool_activity"; phase: "start"; name: string; } | { type: "tool_activity"; phase: "end"; name: string; success: boolean; summary?: string; steps?: Array<{ step: string; detail?: string; data?: Record; }>; } | { type: "response_reset"; reason: string; } | { type: "hallucination_detected"; blockType: string; tool: string; } | { type: "graph_start"; name: string; } | { type: "graph_end"; name: string; durationMs: number; } | { type: "phase_start"; name: string; } | { type: "phase_end"; name: string; durationMs: number; } | { type: "agent_start"; name: string; } | { type: "agent_end"; name: string; durationMs: number; summary: string; } | { type: "negotiation_session_start"; opportunityId: string; negotiationConversationId: string; sourceUserId: string; candidateUserId: string; candidateName?: string; startedAt: number; } | { type: "negotiation_session_end"; opportunityId: string; negotiationConversationId: string; durationMs: number; } | { type: "negotiation_turn"; opportunityId: string; negotiationConversationId: string; turnIndex: number; actor: "source" | "candidate"; action: "propose" | "accept" | "reject" | "counter" | "question" | "outreach" | "withdraw" | "decline" | "ask_user"; reasoning?: string; message?: string; suggestedRoles?: { ownUser?: string; otherUser?: string; }; durationMs: number; } | { type: "negotiation_outcome"; opportunityId: string; outcome: "accepted" | "rejected_stalled" | "waiting_for_agent" | "timed_out" | "turn_cap" | "screened_out"; turnCount: number; reasoning?: string; agreedRoles?: { ownUser?: string; otherUser?: string; }; } | { type: "user_question"; questions: Array<{ id: string; }>; } | { type: "status"; message: string; } | { type: "decision_questions"; questions: Question[]; } | { type: "chat_summarizer_start"; payload: { sessionId: string; }; } | { type: "chat_summarizer_end"; payload: { durationMs: number; }; } | { type: "question_generator_start"; payload: { inputMode: "transcripts" | "insights"; negotiationCount: number; hasChatContext: boolean; truncated?: { originalCount: number; keptCount: number; }; }; } | { type: "question_generator_end"; payload: { finalCount: number; strategies: QuestionStrategy[]; durationMs: number; inputMode: "transcripts" | "insights"; }; }; /** * Soft limit: After this many iterations, inject a nudge message. */ export declare const SOFT_ITERATION_LIMIT = 8; /** * Hard limit: Force exit after this many iterations to prevent infinite loops. */ export declare const HARD_ITERATION_LIMIT = 12; /** * Result of a single agent iteration. */ export interface AgentIterationResult { /** Whether the agent wants to continue (made tool calls) or stop (produced final response) */ shouldContinue: boolean; /** Tool calls made in this iteration (if any) */ toolCalls?: Array<{ id: string; name: string; args: Record; }>; /** Tool results from executing the tool calls */ toolResults?: Array<{ toolCallId: string; name: string; result: string; }>; /** Final response text (if agent is done) */ responseText?: string; /** Updated messages array */ messages: BaseMessage[]; } /** * ChatAgent: ReAct-style agent that uses tools to help users. * * The agent operates in a loop: * 1. Receive messages (conversation history + tool results) * 2. Decide: call tools OR respond to user * 3. If tools called: execute them, add results, loop back * 4. If response: return final text * * Use `ChatAgent.create(context)` to construct (async factory). */ export declare class ChatAgent { private resolvedContext; private persona; private model; private tools; private toolsByName; /** * Private constructor — use `ChatAgent.create()` instead. */ private constructor(); /** * Extracts the text content of the most recent HumanMessage. */ private static getCurrentUserMessage; /** * Detects a prior visible action proposal without considering current-turn * tool calls. The reporter uses this only to make typed confirmation * language contextual rather than executable on its own. */ static hasPriorAgentActionProposal(messages: BaseMessage[]): boolean; /** * Async factory: creates a ChatAgent with resolved user/index context. * Resolves user/network identity from DB during tool initialization. * * @param context - Tool context (database, userId, scope, deps) * @param persona - Persona config (prompt builder, toolset, loop behaviors). * Defaults to the orchestrator persona. */ static create(context: ToolContext, persona?: ChatPersonaConfig): Promise; /** * Run a single iteration of the agent loop. * * @param messages - Current conversation including any tool results * @param iterationCount - Current iteration number (for soft limit) * @returns Result indicating whether to continue and any tool calls/response */ runIteration(messages: BaseMessage[], iterationCount: number): Promise; /** * Execute tool calls, potentially in parallel. */ private executeToolCalls; /** * Check whether `list_opportunities` returned valid opportunity blocks. * Persisted cards from that tool are the only valid source. */ private static hasOpportunitySource; /** * Detect hallucinated ```intent_proposal or ```opportunity blocks in model text * that were NOT generated by the corresponding tool call. * * A tool call that returned 0 cards (e.g. "Found 0 match(es)") counts as * NOT having produced valid blocks — the LLM must not fabricate them. * * @returns Block info if hallucination detected, null otherwise */ private detectHallucinatedBlock; /** * Strip ```opportunity and ```intent_proposal code blocks from text * when no corresponding successful tool call was made. * Defense-in-depth: catches hallucinated blocks that slip past detectHallucinatedBlock * (e.g. after a correction iteration that still hallucinates). * * @param text - The response text to sanitize * @param toolsUsed - Tool call records from the agent loop * @returns Sanitized text with unbacked blocks removed */ private stripUnbackedBlocks; private static readonly PHANTOM_WRITE_PATTERNS; /** * Detect when the model claims to have performed a write action * without having called any tools in the turn. */ private static detectPhantomWrite; private static readonly STEP_DETAIL_MAX; private static readonly STEP_NAME_MAX; /** * Post-process a tool result: strip _graphTimings, extract summary/debugSteps, * and optionally normalize a tool result before a create_intent callback. * * Returns the normalized result string and extracted debug metadata so both * the normal streaming tool loop and the hallucination-recovery branch * produce identical LLM-facing payloads. */ private normalizeToolResult; /** * Run the full agent loop until completion or hard limit. * * @param initialMessages - Starting conversation messages * @returns Final response text and full message history */ run(initialMessages: BaseMessage[]): Promise<{ responseText: string; messages: BaseMessage[]; iterationCount: number; }>; /** * Run the full agent loop with streaming narration. * * Instead of returning a single blob at the end, this method calls * `writer()` for every text token and tool-activity event so the * consumer (graph node) can push them out via `config.writer`. * * @param initialMessages - Starting conversation messages * @param writer - Callback to emit streaming events (from `config.writer`) * @param signal - Optional AbortSignal to cancel the streaming LLM call and tool execution * @returns Final response metadata (same shape as `run()`) */ streamRun(initialMessages: BaseMessage[], writer?: StreamWriter, signal?: AbortSignal): Promise<{ responseText: string; messages: BaseMessage[]; iterationCount: number; debugMeta: { graph: string; iterations: number; tools: DebugMetaToolCall[]; llm: DebugMetaLlm; orchestratorNegotiations?: DebugMetaOrchestratorNegotiations; discoveryQuestions?: DebugMetaDiscoveryQuestions; }; }>; }