@illuma-ai/agents 1.1.4 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/agents/AgentContext.cjs +6 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +28 -5
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +3 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/fileManifest.cjs +49 -0
- package/dist/cjs/utils/fileManifest.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +6 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +28 -5
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/fileManifest.mjs +46 -0
- package/dist/esm/utils/fileManifest.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +4 -1
- package/dist/types/types/graph.d.ts +35 -0
- package/dist/types/utils/fileManifest.d.ts +17 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +7 -0
- package/src/graphs/Graph.ts +31 -6
- package/src/graphs/gapFeatures.test.ts +153 -2
- package/src/types/graph.ts +36 -0
- package/src/utils/fileManifest.ts +49 -0
- package/src/utils/index.ts +1 -0
package/dist/esm/main.mjs
CHANGED
|
@@ -40,6 +40,7 @@ export { extractFinishReason, isMaxTokensFinish } from './utils/toolCallContinua
|
|
|
40
40
|
export { buildMultiDocHintContent, buildPostPruneNote, detectDocuments, hasTaskTool, shouldInjectMultiDocHint } from './utils/contextPressure.mjs';
|
|
41
41
|
export { ToolDiscoveryCache } from './utils/toolDiscoveryCache.mjs';
|
|
42
42
|
export { applyCalibration, createPruneCalibration, updatePruneCalibration } from './utils/pruneCalibration.mjs';
|
|
43
|
+
export { FILE_MANIFEST_PREFIX, buildFileManifestBlock } from './utils/fileManifest.mjs';
|
|
43
44
|
export { CustomOpenAIClient } from './llm/openai/index.mjs';
|
|
44
45
|
export { ChatOpenRouter } from './llm/openrouter/index.mjs';
|
|
45
46
|
export { getChatModelClass, llmProviders } from './llm/providers.mjs';
|
package/dist/esm/main.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"main.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.mjs","sources":["../../../src/types/graph.ts"],"sourcesContent":["// src/types/graph.ts\nimport type {\n START,\n StateType,\n UpdateType,\n StateGraph,\n StateGraphArgs,\n StateDefinition,\n CompiledStateGraph,\n BinaryOperatorAggregate,\n} from '@langchain/langgraph';\nimport type { BindToolsInput } from '@langchain/core/language_models/chat_models';\nimport type {\n BaseMessage,\n AIMessageChunk,\n SystemMessage,\n} from '@langchain/core/messages';\nimport type { RunnableConfig, Runnable } from '@langchain/core/runnables';\nimport type { ChatGenerationChunk } from '@langchain/core/outputs';\nimport type { GoogleAIToolType } from '@langchain/google-common';\nimport type {\n ToolMap,\n ToolEndEvent,\n GenericTool,\n LCTool,\n ToolApprovalConfig,\n} from '@/types/tools';\nimport type { Providers, Callback, GraphNodeKeys } from '@/common';\nimport type { StandardGraph, MultiAgentGraph } from '@/graphs';\nimport type { ClientOptions } from '@/types/llm';\nimport type {\n RunStep,\n RunStepDeltaEvent,\n MessageDeltaEvent,\n ReasoningDeltaEvent,\n} from '@/types/stream';\nimport type { TokenCounter } from '@/types/run';\n\n/** Interface for bound model with stream and invoke methods */\nexport interface ChatModel {\n stream?: (\n messages: BaseMessage[],\n config?: RunnableConfig\n ) => Promise<AsyncIterable<AIMessageChunk>>;\n invoke: (\n messages: BaseMessage[],\n config?: RunnableConfig\n ) => Promise<AIMessageChunk>;\n}\n\nexport type GraphNode = GraphNodeKeys | typeof START;\nexport type ClientCallback<T extends unknown[]> = (\n graph: StandardGraph,\n ...args: T\n) => void;\n\nexport type ClientCallbacks = {\n [Callback.TOOL_ERROR]?: ClientCallback<[Error, string]>;\n [Callback.TOOL_START]?: ClientCallback<unknown[]>;\n [Callback.TOOL_END]?: ClientCallback<unknown[]>;\n};\n\nexport type SystemCallbacks = {\n [K in keyof ClientCallbacks]: ClientCallbacks[K] extends ClientCallback<\n infer Args\n >\n ? (...args: Args) => void\n : never;\n};\n\nexport type BaseGraphState = {\n messages: BaseMessage[];\n /**\n * Structured response when using structured output mode.\n * Contains the validated JSON response conforming to the configured schema.\n */\n structuredResponse?: Record<string, unknown>;\n};\n\nexport type MultiAgentGraphState = BaseGraphState & {\n agentMessages?: BaseMessage[];\n};\n\nexport type IState = BaseGraphState;\n\nexport interface EventHandler {\n handle(\n event: string,\n data:\n | StreamEventData\n | ModelEndData\n | RunStep\n | RunStepDeltaEvent\n | MessageDeltaEvent\n | ReasoningDeltaEvent\n | { result: ToolEndEvent },\n metadata?: Record<string, unknown>,\n graph?: StandardGraph | MultiAgentGraph\n ): void | Promise<void>;\n}\n\nexport type GraphStateChannels<T extends BaseGraphState> =\n StateGraphArgs<T>['channels'];\n\nexport type Workflow<\n T extends BaseGraphState = BaseGraphState,\n U extends Partial<T> = Partial<T>,\n N extends string = string,\n> = StateGraph<T, U, N>;\n\nexport type CompiledWorkflow<\n T extends BaseGraphState = BaseGraphState,\n U extends Partial<T> = Partial<T>,\n N extends string = string,\n> = CompiledStateGraph<T, U, N>;\n\nexport type CompiledStateWorkflow = CompiledStateGraph<\n StateType<{\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n }>,\n UpdateType<{\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n }>,\n string,\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n StateDefinition\n>;\n\nexport type CompiledMultiAgentWorkflow = CompiledStateGraph<\n StateType<{\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n agentMessages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n }>,\n UpdateType<{\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n agentMessages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n }>,\n string,\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n agentMessages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n agentMessages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n StateDefinition\n>;\n\nexport type CompiledAgentWorfklow = CompiledStateGraph<\n {\n messages: BaseMessage[];\n },\n {\n messages?: BaseMessage[] | undefined;\n },\n '__start__' | `agent=${string}` | `tools=${string}`,\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n StateDefinition,\n {\n [x: `agent=${string}`]: Partial<BaseGraphState>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [x: `tools=${string}`]: any;\n }\n>;\n\nexport type SystemRunnable =\n | Runnable<\n BaseMessage[],\n (BaseMessage | SystemMessage)[],\n RunnableConfig<Record<string, unknown>>\n >\n | undefined;\n\n/**\n * Optional compile options passed to workflow.compile().\n * These are intentionally untyped to avoid coupling to library internals.\n */\nexport type CompileOptions = {\n // A checkpointer instance (e.g., MemorySaver, SQL saver)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n checkpointer?: any;\n interruptBefore?: string[];\n interruptAfter?: string[];\n /**\n * Human-in-the-loop tool approval configuration.\n * When set, tools matching the policy will trigger an interrupt()\n * before execution, pausing the graph for human approval.\n * Requires a checkpointer to be set for interrupt/resume to work.\n */\n toolApprovalConfig?: ToolApprovalConfig;\n};\n\nexport type EventStreamCallbackHandlerInput =\n Parameters<CompiledWorkflow['streamEvents']>[2] extends Omit<\n infer T,\n 'autoClose'\n >\n ? T\n : never;\n\nexport type StreamChunk =\n | (ChatGenerationChunk & {\n message: AIMessageChunk;\n })\n | AIMessageChunk;\n\n/**\n * Data associated with a StreamEvent.\n */\nexport type StreamEventData = {\n /**\n * The input passed to the runnable that generated the event.\n * Inputs will sometimes be available at the *START* of the runnable, and\n * sometimes at the *END* of the runnable.\n * If a runnable is able to stream its inputs, then its input by definition\n * won't be known until the *END* of the runnable when it has finished streaming\n * its inputs.\n */\n input?: unknown;\n /**\n * The output of the runnable that generated the event.\n * Outputs will only be available at the *END* of the runnable.\n * For most runnables, this field can be inferred from the `chunk` field,\n * though there might be some exceptions for special cased runnables (e.g., like\n * chat models), which may return more information.\n */\n output?: unknown;\n /**\n * A streaming chunk from the output that generated the event.\n * chunks support addition in general, and adding them up should result\n * in the output of the runnable that generated the event.\n */\n chunk?: StreamChunk;\n /**\n * Runnable config for invoking other runnables within handlers.\n */\n config?: RunnableConfig;\n /**\n * Custom result from the runnable that generated the event.\n */\n result?: unknown;\n /**\n * Custom field to indicate the event was manually emitted, and may have been handled already\n */\n emitted?: boolean;\n};\n\n/**\n * A streaming event.\n *\n * Schema of a streaming event which is produced from the streamEvents method.\n */\nexport type StreamEvent = {\n /**\n * Event names are of the format: on_[runnable_type]_(start|stream|end).\n *\n * Runnable types are one of:\n * - llm - used by non chat models\n * - chat_model - used by chat models\n * - prompt -- e.g., ChatPromptTemplate\n * - tool -- LangChain tools\n * - chain - most Runnables are of this type\n *\n * Further, the events are categorized as one of:\n * - start - when the runnable starts\n * - stream - when the runnable is streaming\n * - end - when the runnable ends\n *\n * start, stream and end are associated with slightly different `data` payload.\n *\n * Please see the documentation for `EventData` for more details.\n */\n event: string;\n /** The name of the runnable that generated the event. */\n name: string;\n /**\n * An randomly generated ID to keep track of the execution of the given runnable.\n *\n * Each child runnable that gets invoked as part of the execution of a parent runnable\n * is assigned its own unique ID.\n */\n run_id: string;\n /**\n * Tags associated with the runnable that generated this event.\n * Tags are always inherited from parent runnables.\n */\n tags?: string[];\n /** Metadata associated with the runnable that generated this event. */\n metadata: Record<string, unknown>;\n /**\n * Event data.\n *\n * The contents of the event data depend on the event type.\n */\n data: StreamEventData;\n};\n\nexport type GraphConfig = {\n provider: string;\n thread_id?: string;\n run_id?: string;\n};\n\nexport type PartMetadata = {\n progress?: number;\n asset_pointer?: string;\n status?: string;\n action?: boolean;\n output?: string;\n};\n\nexport type ModelEndData =\n | (StreamEventData & { output: AIMessageChunk | undefined })\n | undefined;\nexport type GraphTools = GenericTool[] | BindToolsInput[] | GoogleAIToolType[];\nexport type StandardGraphInput = {\n runId?: string;\n signal?: AbortSignal;\n agents: AgentInputs[];\n tokenCounter?: TokenCounter;\n indexTokenCountMap?: Record<string, number>;\n};\n\nexport type GraphEdge = {\n /** Agent ID, use a list for multiple sources */\n from: string | string[];\n /** Agent ID, use a list for multiple destinations */\n to: string | string[];\n description?: string;\n /** Can return boolean or specific destination(s) */\n condition?: (state: BaseGraphState) => boolean | string | string[];\n /** EdgeType.HANDOFF creates tools for dynamic routing, EdgeType.DIRECT creates direct edges with parallel execution */\n edgeType?: import('@/common').EdgeType;\n /**\n * For direct edges: Optional prompt to add when transitioning through this edge.\n * String prompts can include variables like {results} which will be replaced with\n * messages from startIndex onwards. When {results} is used, excludeResults defaults to true.\n *\n * For handoff edges: Description for the input parameter that the handoff tool accepts,\n * allowing the supervisor to pass specific instructions/context to the transferred agent.\n */\n prompt?:\n | string\n | ((\n messages: BaseMessage[],\n runStartIndex: number\n ) => string | Promise<string> | undefined);\n /**\n * When true, excludes messages from startIndex when adding prompt.\n * Automatically set to true when {results} variable is used in prompt.\n */\n excludeResults?: boolean;\n /**\n * For handoff edges: Customizes the parameter name for the handoff input.\n * Defaults to \"instructions\" if not specified.\n * Only applies when prompt is provided for handoff edges.\n */\n promptKey?: string;\n};\n\nexport type MultiAgentGraphInput = StandardGraphInput & {\n edges: GraphEdge[];\n};\n\n/**\n * Structured output mode determines how the agent returns structured data.\n * - 'tool': Uses tool calling to return structured output (works with all tool-calling models)\n * - 'provider': Uses provider-native structured output via LangChain's jsonMode (OpenAI, Anthropic, etc.)\n * - 'native': Uses provider's constrained decoding API directly for guaranteed schema compliance\n * (Anthropic output_config.format, OpenAI response_format.json_schema). Falls back to 'tool' for unsupported providers.\n * - 'auto': Automatically selects the best strategy — 'native' for supported providers, 'tool' for others\n */\nexport type StructuredOutputMode = 'tool' | 'provider' | 'native' | 'auto';\n\n/**\n * Resolved method used internally after mode resolution.\n * Maps to LangChain's withStructuredOutput method parameter plus our native path.\n */\nexport type ResolvedStructuredOutputMethod =\n | 'functionCalling'\n | 'jsonMode'\n | 'jsonSchema'\n | 'native'\n | undefined;\n\n/**\n * Error thrown when the model refuses to produce structured output due to safety policies.\n */\nexport class StructuredOutputRefusalError extends Error {\n constructor(public refusalText: string) {\n super(`Model refused to produce structured output: ${refusalText}`);\n this.name = 'StructuredOutputRefusalError';\n }\n}\n\n/**\n * Error thrown when the structured output response was truncated due to max_tokens.\n */\nexport class StructuredOutputTruncatedError extends Error {\n constructor(public stopReason: string) {\n super(\n `Structured output was truncated (stop_reason: ${stopReason}). ` +\n 'Increase max_tokens to allow the full JSON response to be generated.'\n );\n this.name = 'StructuredOutputTruncatedError';\n }\n}\n\n/**\n * Configuration for structured JSON output from agents.\n * When configured, the agent will return a validated JSON response\n * instead of streaming text.\n */\nexport interface StructuredOutputConfig {\n /**\n * JSON Schema defining the output structure.\n * The model will be forced to return data conforming to this schema.\n */\n schema: Record<string, unknown>;\n /**\n * Name for the structured output format (used in tool mode).\n * @default 'StructuredResponse'\n */\n name?: string;\n /**\n * Description of what the structured output represents.\n * Helps the model understand the expected format.\n */\n description?: string;\n /**\n * Output mode strategy.\n * @default 'auto'\n */\n mode?: StructuredOutputMode;\n /**\n * Enable strict schema validation.\n * When true, the response must exactly match the schema.\n * @default true\n */\n strict?: boolean;\n /**\n * Error handling configuration.\n * - true: Auto-retry on validation errors (default)\n * - false: Throw error on validation failure\n * - string: Custom error message for retry\n */\n handleErrors?: boolean | string;\n /**\n * Maximum number of retry attempts on validation failure.\n * @default 2\n */\n maxRetries?: number;\n /**\n * Include the raw AI message along with structured response.\n * Useful for debugging.\n * @default false\n */\n includeRaw?: boolean;\n}\n\n/**\n * Database/API structured output format (snake_case with enabled flag).\n * This matches the format stored in MongoDB and sent from frontends.\n */\nexport interface StructuredOutputInput {\n /** Whether structured output is enabled */\n enabled?: boolean;\n /** JSON Schema defining the expected response structure */\n schema?: Record<string, unknown>;\n /** Name identifier for the structured output */\n name?: string;\n /** Description of what the structured output represents */\n description?: string;\n /** Mode for structured output: 'tool' | 'provider' | 'native' | 'auto' */\n mode?: StructuredOutputMode;\n /** Whether to enforce strict schema validation */\n strict?: boolean;\n}\n\n/**\n * Trigger strategy for when summarization should activate.\n * - 'contextPercentage': Trigger when context utilization exceeds a threshold percentage\n * - 'messageCount': Trigger when pruned message count exceeds a threshold\n * - 'tokenThreshold': Trigger when total token count exceeds a raw threshold\n */\nexport type SummarizationTriggerType =\n | 'contextPercentage'\n | 'messageCount'\n | 'tokenThreshold';\n\n/**\n * Configuration for summarization behavior within the agent pipeline.\n * All fields are optional — sensible defaults are provided via constants.\n *\n * @see SUMMARIZATION_CONTEXT_THRESHOLD, SUMMARIZATION_RESERVE_RATIO, PRUNING_EMA_ALPHA\n */\nexport interface SummarizationConfig {\n /**\n * Strategy for when summarization triggers.\n * @default 'contextPercentage'\n */\n triggerType?: SummarizationTriggerType;\n\n /**\n * Threshold value interpreted based on triggerType:\n * - contextPercentage: 0-100 (percentage of context window)\n * - messageCount: absolute count of messages pruned\n * - tokenThreshold: absolute token count\n * @default 80 (for contextPercentage)\n */\n triggerThreshold?: number;\n\n /**\n * Fraction of context window (0-1) reserved for recent messages.\n * Prevents over-pruning by ensuring at least this fraction of the\n * context budget is preserved as recent conversation history.\n * @default 0.3\n */\n reserveRatio?: number;\n\n /**\n * Whether context pruning is enabled (can be disabled for debugging).\n * @default true\n */\n contextPruning?: boolean;\n\n /**\n * Initial summary text to seed across runs.\n * Different from persistedSummary: this is provided by the caller as a\n * cross-conversation seed (e.g., agent personality or recurring context),\n * while persistedSummary is loaded from the conversation's own history.\n */\n initialSummary?: string;\n}\n\n/**\n * Runtime state for EMA-based pruning calibration.\n * Maintained across iterations within a single run to smooth pruning decisions.\n */\nexport interface PruneCalibrationState {\n /** Current EMA calibration ratio */\n ratio: number;\n /** Number of calibration updates applied */\n iterations: number;\n}\n\nexport interface AgentInputs {\n agentId: string;\n /** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */\n name?: string;\n /** Description of what this agent does (used to enrich handoff tool descriptions). */\n description?: string;\n toolEnd?: boolean;\n toolMap?: ToolMap;\n tools?: GraphTools;\n provider: Providers;\n instructions?: string;\n streamBuffer?: number;\n maxContextTokens?: number;\n clientOptions?: ClientOptions;\n additional_instructions?: string;\n reasoningKey?: 'reasoning_content' | 'reasoning';\n /** Format content blocks as strings (for legacy compatibility i.e. Ollama/Azure Serverless) */\n useLegacyContent?: boolean;\n /**\n * Tool definitions for all tools, including deferred and programmatic.\n * Used for tool search and programmatic tool calling.\n * Maps tool name to LCTool definition.\n */\n toolRegistry?: Map<string, LCTool>;\n /**\n * Dynamic context that changes per-request (e.g., current time, user info).\n * This is injected as a user message rather than system prompt to preserve cache.\n * Keeping this separate from instructions ensures the system message stays static\n * and can be cached by Bedrock/Anthropic prompt caching.\n */\n dynamicContext?: string;\n /**\n * Structured output configuration (camelCase).\n * When set, disables streaming and returns a validated JSON response\n * conforming to the specified schema.\n */\n structuredOutput?: StructuredOutputConfig;\n /**\n * Structured output configuration (snake_case - database/API format).\n * Alternative to structuredOutput for compatibility with MongoDB/frontend.\n * Uses an `enabled` flag to control activation.\n * @deprecated Use structuredOutput instead when possible\n */\n structured_output?: StructuredOutputInput;\n /**\n * Serializable tool definitions for event-driven execution.\n * When provided, ToolNode operates in event-driven mode, dispatching\n * ON_TOOL_EXECUTE events instead of invoking tools directly.\n */\n toolDefinitions?: LCTool[];\n /**\n * Tool names discovered from previous conversation history.\n * These tools will be pre-marked as discovered so they're included\n * in tool binding without requiring tool_search.\n */\n discoveredTools?: string[];\n /**\n * Optional callback for summarizing messages that were pruned from context.\n * When provided, discarded messages are summarized by the caller (e.g., Ranger)\n * using a cheap LLM call, and the summary is prepended to the context.\n */\n summarizeCallback?: (\n messagesToRefine: import('@langchain/core/messages').BaseMessage[]\n ) => Promise<string | undefined>;\n /**\n * Pre-existing summary text loaded from persistent storage (MongoDB/Redis).\n * When provided, this summary is injected into the initial message context\n * so the agent has prior conversation history even on new turns.\n * Set by Ranger's SummaryStore when resuming a conversation.\n */\n persistedSummary?: string;\n /**\n * Summarization configuration controlling trigger strategy, reserve ratio,\n * and EMA calibration for pruning. When omitted, sensible defaults apply.\n * @see SummarizationConfig\n */\n summarizationConfig?: SummarizationConfig;\n}\n"],"names":[],"mappings":"AA4YA;;AAEG;AACG,MAAO,4BAA6B,SAAQ,KAAK,CAAA;AAClC,IAAA,WAAA;AAAnB,IAAA,WAAA,CAAmB,WAAmB,EAAA;AACpC,QAAA,KAAK,CAAC,CAAA,4CAAA,EAA+C,WAAW,CAAA,CAAE,CAAC;QADlD,IAAA,CAAA,WAAW,GAAX,WAAW;AAE5B,QAAA,IAAI,CAAC,IAAI,GAAG,8BAA8B;IAC5C;AACD;AAED;;AAEG;AACG,MAAO,8BAA+B,SAAQ,KAAK,CAAA;AACpC,IAAA,UAAA;AAAnB,IAAA,WAAA,CAAmB,UAAkB,EAAA;QACnC,KAAK,CACH,CAAA,8CAAA,EAAiD,UAAU,CAAA,GAAA,CAAK;AAC9D,YAAA,sEAAsE,CACzE;QAJgB,IAAA,CAAA,UAAU,GAAV,UAAU;AAK3B,QAAA,IAAI,CAAC,IAAI,GAAG,gCAAgC;IAC9C;AACD;;;;"}
|
|
1
|
+
{"version":3,"file":"graph.mjs","sources":["../../../src/types/graph.ts"],"sourcesContent":["// src/types/graph.ts\nimport type {\n START,\n StateType,\n UpdateType,\n StateGraph,\n StateGraphArgs,\n StateDefinition,\n CompiledStateGraph,\n BinaryOperatorAggregate,\n} from '@langchain/langgraph';\nimport type { BindToolsInput } from '@langchain/core/language_models/chat_models';\nimport type {\n BaseMessage,\n AIMessageChunk,\n SystemMessage,\n} from '@langchain/core/messages';\nimport type { RunnableConfig, Runnable } from '@langchain/core/runnables';\nimport type { ChatGenerationChunk } from '@langchain/core/outputs';\nimport type { GoogleAIToolType } from '@langchain/google-common';\nimport type {\n ToolMap,\n ToolEndEvent,\n GenericTool,\n LCTool,\n ToolApprovalConfig,\n} from '@/types/tools';\nimport type { Providers, Callback, GraphNodeKeys } from '@/common';\nimport type { StandardGraph, MultiAgentGraph } from '@/graphs';\nimport type { ClientOptions } from '@/types/llm';\nimport type {\n RunStep,\n RunStepDeltaEvent,\n MessageDeltaEvent,\n ReasoningDeltaEvent,\n} from '@/types/stream';\nimport type { TokenCounter } from '@/types/run';\n\n/** Interface for bound model with stream and invoke methods */\nexport interface ChatModel {\n stream?: (\n messages: BaseMessage[],\n config?: RunnableConfig\n ) => Promise<AsyncIterable<AIMessageChunk>>;\n invoke: (\n messages: BaseMessage[],\n config?: RunnableConfig\n ) => Promise<AIMessageChunk>;\n}\n\nexport type GraphNode = GraphNodeKeys | typeof START;\nexport type ClientCallback<T extends unknown[]> = (\n graph: StandardGraph,\n ...args: T\n) => void;\n\nexport type ClientCallbacks = {\n [Callback.TOOL_ERROR]?: ClientCallback<[Error, string]>;\n [Callback.TOOL_START]?: ClientCallback<unknown[]>;\n [Callback.TOOL_END]?: ClientCallback<unknown[]>;\n};\n\nexport type SystemCallbacks = {\n [K in keyof ClientCallbacks]: ClientCallbacks[K] extends ClientCallback<\n infer Args\n >\n ? (...args: Args) => void\n : never;\n};\n\nexport type BaseGraphState = {\n messages: BaseMessage[];\n /**\n * Structured response when using structured output mode.\n * Contains the validated JSON response conforming to the configured schema.\n */\n structuredResponse?: Record<string, unknown>;\n};\n\nexport type MultiAgentGraphState = BaseGraphState & {\n agentMessages?: BaseMessage[];\n};\n\nexport type IState = BaseGraphState;\n\nexport interface EventHandler {\n handle(\n event: string,\n data:\n | StreamEventData\n | ModelEndData\n | RunStep\n | RunStepDeltaEvent\n | MessageDeltaEvent\n | ReasoningDeltaEvent\n | { result: ToolEndEvent },\n metadata?: Record<string, unknown>,\n graph?: StandardGraph | MultiAgentGraph\n ): void | Promise<void>;\n}\n\nexport type GraphStateChannels<T extends BaseGraphState> =\n StateGraphArgs<T>['channels'];\n\nexport type Workflow<\n T extends BaseGraphState = BaseGraphState,\n U extends Partial<T> = Partial<T>,\n N extends string = string,\n> = StateGraph<T, U, N>;\n\nexport type CompiledWorkflow<\n T extends BaseGraphState = BaseGraphState,\n U extends Partial<T> = Partial<T>,\n N extends string = string,\n> = CompiledStateGraph<T, U, N>;\n\nexport type CompiledStateWorkflow = CompiledStateGraph<\n StateType<{\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n }>,\n UpdateType<{\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n }>,\n string,\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n StateDefinition\n>;\n\nexport type CompiledMultiAgentWorkflow = CompiledStateGraph<\n StateType<{\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n agentMessages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n }>,\n UpdateType<{\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n agentMessages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n }>,\n string,\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n agentMessages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n agentMessages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n StateDefinition\n>;\n\nexport type CompiledAgentWorfklow = CompiledStateGraph<\n {\n messages: BaseMessage[];\n },\n {\n messages?: BaseMessage[] | undefined;\n },\n '__start__' | `agent=${string}` | `tools=${string}`,\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n {\n messages: BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;\n },\n StateDefinition,\n {\n [x: `agent=${string}`]: Partial<BaseGraphState>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [x: `tools=${string}`]: any;\n }\n>;\n\nexport type SystemRunnable =\n | Runnable<\n BaseMessage[],\n (BaseMessage | SystemMessage)[],\n RunnableConfig<Record<string, unknown>>\n >\n | undefined;\n\n/**\n * Optional compile options passed to workflow.compile().\n * These are intentionally untyped to avoid coupling to library internals.\n */\nexport type CompileOptions = {\n // A checkpointer instance (e.g., MemorySaver, SQL saver)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n checkpointer?: any;\n interruptBefore?: string[];\n interruptAfter?: string[];\n /**\n * Human-in-the-loop tool approval configuration.\n * When set, tools matching the policy will trigger an interrupt()\n * before execution, pausing the graph for human approval.\n * Requires a checkpointer to be set for interrupt/resume to work.\n */\n toolApprovalConfig?: ToolApprovalConfig;\n};\n\nexport type EventStreamCallbackHandlerInput =\n Parameters<CompiledWorkflow['streamEvents']>[2] extends Omit<\n infer T,\n 'autoClose'\n >\n ? T\n : never;\n\nexport type StreamChunk =\n | (ChatGenerationChunk & {\n message: AIMessageChunk;\n })\n | AIMessageChunk;\n\n/**\n * Data associated with a StreamEvent.\n */\nexport type StreamEventData = {\n /**\n * The input passed to the runnable that generated the event.\n * Inputs will sometimes be available at the *START* of the runnable, and\n * sometimes at the *END* of the runnable.\n * If a runnable is able to stream its inputs, then its input by definition\n * won't be known until the *END* of the runnable when it has finished streaming\n * its inputs.\n */\n input?: unknown;\n /**\n * The output of the runnable that generated the event.\n * Outputs will only be available at the *END* of the runnable.\n * For most runnables, this field can be inferred from the `chunk` field,\n * though there might be some exceptions for special cased runnables (e.g., like\n * chat models), which may return more information.\n */\n output?: unknown;\n /**\n * A streaming chunk from the output that generated the event.\n * chunks support addition in general, and adding them up should result\n * in the output of the runnable that generated the event.\n */\n chunk?: StreamChunk;\n /**\n * Runnable config for invoking other runnables within handlers.\n */\n config?: RunnableConfig;\n /**\n * Custom result from the runnable that generated the event.\n */\n result?: unknown;\n /**\n * Custom field to indicate the event was manually emitted, and may have been handled already\n */\n emitted?: boolean;\n};\n\n/**\n * A streaming event.\n *\n * Schema of a streaming event which is produced from the streamEvents method.\n */\nexport type StreamEvent = {\n /**\n * Event names are of the format: on_[runnable_type]_(start|stream|end).\n *\n * Runnable types are one of:\n * - llm - used by non chat models\n * - chat_model - used by chat models\n * - prompt -- e.g., ChatPromptTemplate\n * - tool -- LangChain tools\n * - chain - most Runnables are of this type\n *\n * Further, the events are categorized as one of:\n * - start - when the runnable starts\n * - stream - when the runnable is streaming\n * - end - when the runnable ends\n *\n * start, stream and end are associated with slightly different `data` payload.\n *\n * Please see the documentation for `EventData` for more details.\n */\n event: string;\n /** The name of the runnable that generated the event. */\n name: string;\n /**\n * An randomly generated ID to keep track of the execution of the given runnable.\n *\n * Each child runnable that gets invoked as part of the execution of a parent runnable\n * is assigned its own unique ID.\n */\n run_id: string;\n /**\n * Tags associated with the runnable that generated this event.\n * Tags are always inherited from parent runnables.\n */\n tags?: string[];\n /** Metadata associated with the runnable that generated this event. */\n metadata: Record<string, unknown>;\n /**\n * Event data.\n *\n * The contents of the event data depend on the event type.\n */\n data: StreamEventData;\n};\n\nexport type GraphConfig = {\n provider: string;\n thread_id?: string;\n run_id?: string;\n};\n\nexport type PartMetadata = {\n progress?: number;\n asset_pointer?: string;\n status?: string;\n action?: boolean;\n output?: string;\n};\n\nexport type ModelEndData =\n | (StreamEventData & { output: AIMessageChunk | undefined })\n | undefined;\nexport type GraphTools = GenericTool[] | BindToolsInput[] | GoogleAIToolType[];\nexport type StandardGraphInput = {\n runId?: string;\n signal?: AbortSignal;\n agents: AgentInputs[];\n tokenCounter?: TokenCounter;\n indexTokenCountMap?: Record<string, number>;\n};\n\nexport type GraphEdge = {\n /** Agent ID, use a list for multiple sources */\n from: string | string[];\n /** Agent ID, use a list for multiple destinations */\n to: string | string[];\n description?: string;\n /** Can return boolean or specific destination(s) */\n condition?: (state: BaseGraphState) => boolean | string | string[];\n /** EdgeType.HANDOFF creates tools for dynamic routing, EdgeType.DIRECT creates direct edges with parallel execution */\n edgeType?: import('@/common').EdgeType;\n /**\n * For direct edges: Optional prompt to add when transitioning through this edge.\n * String prompts can include variables like {results} which will be replaced with\n * messages from startIndex onwards. When {results} is used, excludeResults defaults to true.\n *\n * For handoff edges: Description for the input parameter that the handoff tool accepts,\n * allowing the supervisor to pass specific instructions/context to the transferred agent.\n */\n prompt?:\n | string\n | ((\n messages: BaseMessage[],\n runStartIndex: number\n ) => string | Promise<string> | undefined);\n /**\n * When true, excludes messages from startIndex when adding prompt.\n * Automatically set to true when {results} variable is used in prompt.\n */\n excludeResults?: boolean;\n /**\n * For handoff edges: Customizes the parameter name for the handoff input.\n * Defaults to \"instructions\" if not specified.\n * Only applies when prompt is provided for handoff edges.\n */\n promptKey?: string;\n};\n\nexport type MultiAgentGraphInput = StandardGraphInput & {\n edges: GraphEdge[];\n};\n\n/**\n * Structured output mode determines how the agent returns structured data.\n * - 'tool': Uses tool calling to return structured output (works with all tool-calling models)\n * - 'provider': Uses provider-native structured output via LangChain's jsonMode (OpenAI, Anthropic, etc.)\n * - 'native': Uses provider's constrained decoding API directly for guaranteed schema compliance\n * (Anthropic output_config.format, OpenAI response_format.json_schema). Falls back to 'tool' for unsupported providers.\n * - 'auto': Automatically selects the best strategy — 'native' for supported providers, 'tool' for others\n */\nexport type StructuredOutputMode = 'tool' | 'provider' | 'native' | 'auto';\n\n/**\n * Resolved method used internally after mode resolution.\n * Maps to LangChain's withStructuredOutput method parameter plus our native path.\n */\nexport type ResolvedStructuredOutputMethod =\n | 'functionCalling'\n | 'jsonMode'\n | 'jsonSchema'\n | 'native'\n | undefined;\n\n/**\n * Error thrown when the model refuses to produce structured output due to safety policies.\n */\nexport class StructuredOutputRefusalError extends Error {\n constructor(public refusalText: string) {\n super(`Model refused to produce structured output: ${refusalText}`);\n this.name = 'StructuredOutputRefusalError';\n }\n}\n\n/**\n * Error thrown when the structured output response was truncated due to max_tokens.\n */\nexport class StructuredOutputTruncatedError extends Error {\n constructor(public stopReason: string) {\n super(\n `Structured output was truncated (stop_reason: ${stopReason}). ` +\n 'Increase max_tokens to allow the full JSON response to be generated.'\n );\n this.name = 'StructuredOutputTruncatedError';\n }\n}\n\n/**\n * Configuration for structured JSON output from agents.\n * When configured, the agent will return a validated JSON response\n * instead of streaming text.\n */\nexport interface StructuredOutputConfig {\n /**\n * JSON Schema defining the output structure.\n * The model will be forced to return data conforming to this schema.\n */\n schema: Record<string, unknown>;\n /**\n * Name for the structured output format (used in tool mode).\n * @default 'StructuredResponse'\n */\n name?: string;\n /**\n * Description of what the structured output represents.\n * Helps the model understand the expected format.\n */\n description?: string;\n /**\n * Output mode strategy.\n * @default 'auto'\n */\n mode?: StructuredOutputMode;\n /**\n * Enable strict schema validation.\n * When true, the response must exactly match the schema.\n * @default true\n */\n strict?: boolean;\n /**\n * Error handling configuration.\n * - true: Auto-retry on validation errors (default)\n * - false: Throw error on validation failure\n * - string: Custom error message for retry\n */\n handleErrors?: boolean | string;\n /**\n * Maximum number of retry attempts on validation failure.\n * @default 2\n */\n maxRetries?: number;\n /**\n * Include the raw AI message along with structured response.\n * Useful for debugging.\n * @default false\n */\n includeRaw?: boolean;\n}\n\n/**\n * Database/API structured output format (snake_case with enabled flag).\n * This matches the format stored in MongoDB and sent from frontends.\n */\nexport interface StructuredOutputInput {\n /** Whether structured output is enabled */\n enabled?: boolean;\n /** JSON Schema defining the expected response structure */\n schema?: Record<string, unknown>;\n /** Name identifier for the structured output */\n name?: string;\n /** Description of what the structured output represents */\n description?: string;\n /** Mode for structured output: 'tool' | 'provider' | 'native' | 'auto' */\n mode?: StructuredOutputMode;\n /** Whether to enforce strict schema validation */\n strict?: boolean;\n}\n\n/**\n * Trigger strategy for when summarization should activate.\n * - 'contextPercentage': Trigger when context utilization exceeds a threshold percentage\n * - 'messageCount': Trigger when pruned message count exceeds a threshold\n * - 'tokenThreshold': Trigger when total token count exceeds a raw threshold\n */\nexport type SummarizationTriggerType =\n | 'contextPercentage'\n | 'messageCount'\n | 'tokenThreshold';\n\n/**\n * Configuration for summarization behavior within the agent pipeline.\n * All fields are optional — sensible defaults are provided via constants.\n *\n * @see SUMMARIZATION_CONTEXT_THRESHOLD, SUMMARIZATION_RESERVE_RATIO, PRUNING_EMA_ALPHA\n */\nexport interface SummarizationConfig {\n /**\n * Strategy for when summarization triggers.\n * @default 'contextPercentage'\n */\n triggerType?: SummarizationTriggerType;\n\n /**\n * Threshold value interpreted based on triggerType:\n * - contextPercentage: 0-100 (percentage of context window)\n * - messageCount: absolute count of messages pruned\n * - tokenThreshold: absolute token count\n * @default 80 (for contextPercentage)\n */\n triggerThreshold?: number;\n\n /**\n * Fraction of context window (0-1) reserved for recent messages.\n * Prevents over-pruning by ensuring at least this fraction of the\n * context budget is preserved as recent conversation history.\n * @default 0.3\n */\n reserveRatio?: number;\n\n /**\n * Whether context pruning is enabled (can be disabled for debugging).\n * @default true\n */\n contextPruning?: boolean;\n\n /**\n * Initial summary text to seed across runs.\n * Different from persistedSummary: this is provided by the caller as a\n * cross-conversation seed (e.g., agent personality or recurring context),\n * while persistedSummary is loaded from the conversation's own history.\n */\n initialSummary?: string;\n}\n\n/**\n * Runtime state for EMA-based pruning calibration.\n * Maintained across iterations within a single run to smooth pruning decisions.\n */\nexport interface PruneCalibrationState {\n /** Current EMA calibration ratio */\n ratio: number;\n /** Number of calibration updates applied */\n iterations: number;\n}\n\n/**\n * Lightweight file metadata entry for conversation-level file awareness.\n * Contains only IDs and names — NOT full content — so the agent always knows\n * what files exist in the conversation even after compaction pushes old messages\n * behind the summary window. The agent can retrieve full content on-demand\n * via file_search (RAG) or content_tool read (by contentId).\n */\nexport interface FileManifestEntry {\n /** Unique file identifier (e.g., MongoDB ObjectId or UUID) */\n fileId: string;\n /** Original filename (e.g., \"quarterly-report.pdf\") */\n filename: string;\n /** Content identifier for on-demand retrieval via content_tool read */\n contentId?: string;\n /** File source (e.g., \"local\", \"sharepoint\", \"onedrive\") */\n source?: string;\n /** Index of the message that introduced this file (0-based in the original message array) */\n messageIndex?: number;\n}\n\nexport interface AgentInputs {\n agentId: string;\n /** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */\n name?: string;\n /** Description of what this agent does (used to enrich handoff tool descriptions). */\n description?: string;\n toolEnd?: boolean;\n toolMap?: ToolMap;\n tools?: GraphTools;\n provider: Providers;\n instructions?: string;\n streamBuffer?: number;\n maxContextTokens?: number;\n clientOptions?: ClientOptions;\n additional_instructions?: string;\n reasoningKey?: 'reasoning_content' | 'reasoning';\n /** Format content blocks as strings (for legacy compatibility i.e. Ollama/Azure Serverless) */\n useLegacyContent?: boolean;\n /**\n * Tool definitions for all tools, including deferred and programmatic.\n * Used for tool search and programmatic tool calling.\n * Maps tool name to LCTool definition.\n */\n toolRegistry?: Map<string, LCTool>;\n /**\n * Dynamic context that changes per-request (e.g., current time, user info).\n * This is injected as a user message rather than system prompt to preserve cache.\n * Keeping this separate from instructions ensures the system message stays static\n * and can be cached by Bedrock/Anthropic prompt caching.\n */\n dynamicContext?: string;\n /**\n * Structured output configuration (camelCase).\n * When set, disables streaming and returns a validated JSON response\n * conforming to the specified schema.\n */\n structuredOutput?: StructuredOutputConfig;\n /**\n * Structured output configuration (snake_case - database/API format).\n * Alternative to structuredOutput for compatibility with MongoDB/frontend.\n * Uses an `enabled` flag to control activation.\n * @deprecated Use structuredOutput instead when possible\n */\n structured_output?: StructuredOutputInput;\n /**\n * Serializable tool definitions for event-driven execution.\n * When provided, ToolNode operates in event-driven mode, dispatching\n * ON_TOOL_EXECUTE events instead of invoking tools directly.\n */\n toolDefinitions?: LCTool[];\n /**\n * Tool names discovered from previous conversation history.\n * These tools will be pre-marked as discovered so they're included\n * in tool binding without requiring tool_search.\n */\n discoveredTools?: string[];\n /**\n * Optional callback for summarizing messages that were pruned from context.\n * When provided, discarded messages are summarized by the caller (e.g., Ranger)\n * using a cheap LLM call, and the summary is prepended to the context.\n */\n summarizeCallback?: (\n messagesToRefine: import('@langchain/core/messages').BaseMessage[]\n ) => Promise<string | undefined>;\n /**\n * Pre-existing summary text loaded from persistent storage (MongoDB/Redis).\n * When provided, this summary is injected into the initial message context\n * so the agent has prior conversation history even on new turns.\n * Set by Ranger's SummaryStore when resuming a conversation.\n */\n persistedSummary?: string;\n /**\n * Summarization configuration controlling trigger strategy, reserve ratio,\n * and EMA calibration for pruning. When omitted, sensible defaults apply.\n * @see SummarizationConfig\n */\n summarizationConfig?: SummarizationConfig;\n /**\n * Lightweight file manifest for the conversation.\n * Contains file IDs, names, and metadata — NOT full content.\n *\n * Used by the compaction engine to inject a [Conversation Files] block\n * into the windowed view, ensuring the LLM always knows what files exist\n * even when old messages (with full file content) are behind the summary.\n *\n * The agent can retrieve full content on-demand via:\n * - file_search (RAG semantic search over embedded files)\n * - content_tool read (by contentId for exact file retrieval)\n *\n * Built by the orchestrator (e.g., Ranger) from message_file_map\n * and metadata.context_files across all conversation messages.\n */\n fileManifest?: FileManifestEntry[];\n}\n"],"names":[],"mappings":"AA4YA;;AAEG;AACG,MAAO,4BAA6B,SAAQ,KAAK,CAAA;AAClC,IAAA,WAAA;AAAnB,IAAA,WAAA,CAAmB,WAAmB,EAAA;AACpC,QAAA,KAAK,CAAC,CAAA,4CAAA,EAA+C,WAAW,CAAA,CAAE,CAAC;QADlD,IAAA,CAAA,WAAW,GAAX,WAAW;AAE5B,QAAA,IAAI,CAAC,IAAI,GAAG,8BAA8B;IAC5C;AACD;AAED;;AAEG;AACG,MAAO,8BAA+B,SAAQ,KAAK,CAAA;AACpC,IAAA,UAAA;AAAnB,IAAA,WAAA,CAAmB,UAAkB,EAAA;QACnC,KAAK,CACH,CAAA,8CAAA,EAAiD,UAAU,CAAA,GAAA,CAAK;AAC9D,YAAA,sEAAsE,CACzE;QAJgB,IAAA,CAAA,UAAU,GAAV,UAAU;AAK3B,QAAA,IAAI,CAAC,IAAI,GAAG,gCAAgC;IAC9C;AACD;;;;"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/utils/fileManifest.ts
|
|
2
|
+
//
|
|
3
|
+
// Utility for building a lightweight [Conversation Files] context block
|
|
4
|
+
// from a file manifest. Injected into the compaction windowed view so the
|
|
5
|
+
// LLM retains awareness of ALL conversation files, even when old messages
|
|
6
|
+
// (with full file content) are behind the summary.
|
|
7
|
+
/**
|
|
8
|
+
* Prefix marker for the file manifest block.
|
|
9
|
+
* Used to detect and deduplicate manifest messages across turns.
|
|
10
|
+
*/
|
|
11
|
+
const FILE_MANIFEST_PREFIX = '[Conversation Files]';
|
|
12
|
+
/**
|
|
13
|
+
* Builds a compact text block listing all files in the conversation.
|
|
14
|
+
* Each entry costs ~10-15 tokens, so 10 files ≈ 100-150 tokens total.
|
|
15
|
+
*
|
|
16
|
+
* The block includes retrieval hints so the LLM knows how to fetch
|
|
17
|
+
* full content on demand (via file_search or content_tool).
|
|
18
|
+
*
|
|
19
|
+
* @param manifest - Array of file metadata entries
|
|
20
|
+
* @returns Formatted text block, or empty string if manifest is empty/undefined
|
|
21
|
+
*/
|
|
22
|
+
function buildFileManifestBlock(manifest) {
|
|
23
|
+
if (!manifest || manifest.length === 0) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
const lines = manifest.map((entry) => {
|
|
27
|
+
const parts = [`- ${entry.filename}`];
|
|
28
|
+
if (entry.contentId) {
|
|
29
|
+
parts.push(`(content_id: ${entry.contentId})`);
|
|
30
|
+
}
|
|
31
|
+
if (entry.source) {
|
|
32
|
+
parts.push(`[${entry.source}]`);
|
|
33
|
+
}
|
|
34
|
+
return parts.join(' ');
|
|
35
|
+
});
|
|
36
|
+
return [
|
|
37
|
+
FILE_MANIFEST_PREFIX,
|
|
38
|
+
'The following files have been shared in this conversation.',
|
|
39
|
+
'Use file_search or content_tool read (with content_id) to retrieve full content when needed.',
|
|
40
|
+
'',
|
|
41
|
+
...lines,
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { FILE_MANIFEST_PREFIX, buildFileManifestBlock };
|
|
46
|
+
//# sourceMappingURL=fileManifest.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileManifest.mjs","sources":["../../../src/utils/fileManifest.ts"],"sourcesContent":["// src/utils/fileManifest.ts\n//\n// Utility for building a lightweight [Conversation Files] context block\n// from a file manifest. Injected into the compaction windowed view so the\n// LLM retains awareness of ALL conversation files, even when old messages\n// (with full file content) are behind the summary.\n\nimport type { FileManifestEntry } from '@/types/graph';\n\n/**\n * Prefix marker for the file manifest block.\n * Used to detect and deduplicate manifest messages across turns.\n */\nexport const FILE_MANIFEST_PREFIX = '[Conversation Files]';\n\n/**\n * Builds a compact text block listing all files in the conversation.\n * Each entry costs ~10-15 tokens, so 10 files ≈ 100-150 tokens total.\n *\n * The block includes retrieval hints so the LLM knows how to fetch\n * full content on demand (via file_search or content_tool).\n *\n * @param manifest - Array of file metadata entries\n * @returns Formatted text block, or empty string if manifest is empty/undefined\n */\nexport function buildFileManifestBlock(manifest: FileManifestEntry[] | undefined): string {\n if (!manifest || manifest.length === 0) {\n return '';\n }\n\n const lines = manifest.map((entry) => {\n const parts: string[] = [`- ${entry.filename}`];\n if (entry.contentId) {\n parts.push(`(content_id: ${entry.contentId})`);\n }\n if (entry.source) {\n parts.push(`[${entry.source}]`);\n }\n return parts.join(' ');\n });\n\n return [\n FILE_MANIFEST_PREFIX,\n 'The following files have been shared in this conversation.',\n 'Use file_search or content_tool read (with content_id) to retrieve full content when needed.',\n '',\n ...lines,\n ].join('\\n');\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AAIA;;;AAGG;AACI,MAAM,oBAAoB,GAAG;AAEpC;;;;;;;;;AASG;AACG,SAAU,sBAAsB,CAAC,QAAyC,EAAA;IAC9E,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE;AACtC,QAAA,OAAO,EAAE;IACX;IAEA,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,KAAI;QACnC,MAAM,KAAK,GAAa,CAAC,CAAA,EAAA,EAAK,KAAK,CAAC,QAAQ,CAAA,CAAE,CAAC;AAC/C,QAAA,IAAI,KAAK,CAAC,SAAS,EAAE;YACnB,KAAK,CAAC,IAAI,CAAC,CAAA,aAAA,EAAgB,KAAK,CAAC,SAAS,CAAA,CAAA,CAAG,CAAC;QAChD;AACA,QAAA,IAAI,KAAK,CAAC,MAAM,EAAE;YAChB,KAAK,CAAC,IAAI,CAAC,CAAA,CAAA,EAAI,KAAK,CAAC,MAAM,CAAA,CAAA,CAAG,CAAC;QACjC;AACA,QAAA,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;AACxB,IAAA,CAAC,CAAC;IAEF,OAAO;QACL,oBAAoB;QACpB,4DAA4D;QAC5D,8FAA8F;QAC9F,EAAE;AACF,QAAA,GAAG,KAAK;AACT,KAAA,CAAC,IAAI,CAAC,IAAI,CAAC;AACd;;;;"}
|
|
@@ -118,7 +118,9 @@ export declare class AgentContext {
|
|
|
118
118
|
persistedSummary?: string;
|
|
119
119
|
/** Summarization configuration controlling trigger strategy, reserve ratio, and EMA calibration */
|
|
120
120
|
summarizationConfig?: t.SummarizationConfig;
|
|
121
|
-
|
|
121
|
+
/** Lightweight file manifest for file-aware compaction (IDs and names only, no content) */
|
|
122
|
+
fileManifest?: t.FileManifestEntry[];
|
|
123
|
+
constructor({ agentId, name, description, provider, clientOptions, maxContextTokens, streamBuffer, tokenCounter, tools, toolMap, toolRegistry, toolDefinitions, instructions, additionalInstructions, dynamicContext, reasoningKey, toolEnd, instructionTokens, useLegacyContent, structuredOutput, discoveredTools, summarizeCallback, persistedSummary, summarizationConfig, fileManifest, }: {
|
|
122
124
|
agentId: string;
|
|
123
125
|
name?: string;
|
|
124
126
|
description?: string;
|
|
@@ -143,6 +145,7 @@ export declare class AgentContext {
|
|
|
143
145
|
summarizeCallback?: (messages: BaseMessage[]) => Promise<string | undefined>;
|
|
144
146
|
persistedSummary?: string;
|
|
145
147
|
summarizationConfig?: t.SummarizationConfig;
|
|
148
|
+
fileManifest?: t.FileManifestEntry[];
|
|
146
149
|
});
|
|
147
150
|
/**
|
|
148
151
|
* Checks if structured output mode is enabled for this agent.
|
|
@@ -400,6 +400,25 @@ export interface PruneCalibrationState {
|
|
|
400
400
|
/** Number of calibration updates applied */
|
|
401
401
|
iterations: number;
|
|
402
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Lightweight file metadata entry for conversation-level file awareness.
|
|
405
|
+
* Contains only IDs and names — NOT full content — so the agent always knows
|
|
406
|
+
* what files exist in the conversation even after compaction pushes old messages
|
|
407
|
+
* behind the summary window. The agent can retrieve full content on-demand
|
|
408
|
+
* via file_search (RAG) or content_tool read (by contentId).
|
|
409
|
+
*/
|
|
410
|
+
export interface FileManifestEntry {
|
|
411
|
+
/** Unique file identifier (e.g., MongoDB ObjectId or UUID) */
|
|
412
|
+
fileId: string;
|
|
413
|
+
/** Original filename (e.g., "quarterly-report.pdf") */
|
|
414
|
+
filename: string;
|
|
415
|
+
/** Content identifier for on-demand retrieval via content_tool read */
|
|
416
|
+
contentId?: string;
|
|
417
|
+
/** File source (e.g., "local", "sharepoint", "onedrive") */
|
|
418
|
+
source?: string;
|
|
419
|
+
/** Index of the message that introduced this file (0-based in the original message array) */
|
|
420
|
+
messageIndex?: number;
|
|
421
|
+
}
|
|
403
422
|
export interface AgentInputs {
|
|
404
423
|
agentId: string;
|
|
405
424
|
/** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */
|
|
@@ -475,4 +494,20 @@ export interface AgentInputs {
|
|
|
475
494
|
* @see SummarizationConfig
|
|
476
495
|
*/
|
|
477
496
|
summarizationConfig?: SummarizationConfig;
|
|
497
|
+
/**
|
|
498
|
+
* Lightweight file manifest for the conversation.
|
|
499
|
+
* Contains file IDs, names, and metadata — NOT full content.
|
|
500
|
+
*
|
|
501
|
+
* Used by the compaction engine to inject a [Conversation Files] block
|
|
502
|
+
* into the windowed view, ensuring the LLM always knows what files exist
|
|
503
|
+
* even when old messages (with full file content) are behind the summary.
|
|
504
|
+
*
|
|
505
|
+
* The agent can retrieve full content on-demand via:
|
|
506
|
+
* - file_search (RAG semantic search over embedded files)
|
|
507
|
+
* - content_tool read (by contentId for exact file retrieval)
|
|
508
|
+
*
|
|
509
|
+
* Built by the orchestrator (e.g., Ranger) from message_file_map
|
|
510
|
+
* and metadata.context_files across all conversation messages.
|
|
511
|
+
*/
|
|
512
|
+
fileManifest?: FileManifestEntry[];
|
|
478
513
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FileManifestEntry } from '@/types/graph';
|
|
2
|
+
/**
|
|
3
|
+
* Prefix marker for the file manifest block.
|
|
4
|
+
* Used to detect and deduplicate manifest messages across turns.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FILE_MANIFEST_PREFIX = "[Conversation Files]";
|
|
7
|
+
/**
|
|
8
|
+
* Builds a compact text block listing all files in the conversation.
|
|
9
|
+
* Each entry costs ~10-15 tokens, so 10 files ≈ 100-150 tokens total.
|
|
10
|
+
*
|
|
11
|
+
* The block includes retrieval hints so the LLM knows how to fetch
|
|
12
|
+
* full content on demand (via file_search or content_tool).
|
|
13
|
+
*
|
|
14
|
+
* @param manifest - Array of file metadata entries
|
|
15
|
+
* @returns Formatted text block, or empty string if manifest is empty/undefined
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildFileManifestBlock(manifest: FileManifestEntry[] | undefined): string;
|
package/package.json
CHANGED
|
@@ -51,6 +51,7 @@ export class AgentContext {
|
|
|
51
51
|
summarizeCallback,
|
|
52
52
|
persistedSummary,
|
|
53
53
|
summarizationConfig,
|
|
54
|
+
fileManifest,
|
|
54
55
|
} = agentConfig;
|
|
55
56
|
|
|
56
57
|
// Normalize structured output: support both camelCase and snake_case inputs
|
|
@@ -97,6 +98,7 @@ export class AgentContext {
|
|
|
97
98
|
summarizeCallback,
|
|
98
99
|
persistedSummary,
|
|
99
100
|
summarizationConfig,
|
|
101
|
+
fileManifest,
|
|
100
102
|
});
|
|
101
103
|
|
|
102
104
|
if (tokenCounter) {
|
|
@@ -250,6 +252,8 @@ export class AgentContext {
|
|
|
250
252
|
persistedSummary?: string;
|
|
251
253
|
/** Summarization configuration controlling trigger strategy, reserve ratio, and EMA calibration */
|
|
252
254
|
summarizationConfig?: t.SummarizationConfig;
|
|
255
|
+
/** Lightweight file manifest for file-aware compaction (IDs and names only, no content) */
|
|
256
|
+
fileManifest?: t.FileManifestEntry[];
|
|
253
257
|
|
|
254
258
|
constructor({
|
|
255
259
|
agentId,
|
|
@@ -276,6 +280,7 @@ export class AgentContext {
|
|
|
276
280
|
summarizeCallback,
|
|
277
281
|
persistedSummary,
|
|
278
282
|
summarizationConfig,
|
|
283
|
+
fileManifest,
|
|
279
284
|
}: {
|
|
280
285
|
agentId: string;
|
|
281
286
|
name?: string;
|
|
@@ -303,6 +308,7 @@ export class AgentContext {
|
|
|
303
308
|
) => Promise<string | undefined>;
|
|
304
309
|
persistedSummary?: string;
|
|
305
310
|
summarizationConfig?: t.SummarizationConfig;
|
|
311
|
+
fileManifest?: t.FileManifestEntry[];
|
|
306
312
|
}) {
|
|
307
313
|
this.agentId = agentId;
|
|
308
314
|
this.name = name;
|
|
@@ -323,6 +329,7 @@ export class AgentContext {
|
|
|
323
329
|
this.summarizeCallback = summarizeCallback;
|
|
324
330
|
this.persistedSummary = persistedSummary;
|
|
325
331
|
this.summarizationConfig = summarizationConfig;
|
|
332
|
+
this.fileManifest = fileManifest;
|
|
326
333
|
if (reasoningKey) {
|
|
327
334
|
this.reasoningKey = reasoningKey;
|
|
328
335
|
}
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -69,7 +69,8 @@ import {
|
|
|
69
69
|
updatePruneCalibration,
|
|
70
70
|
applyCalibration,
|
|
71
71
|
} from '@/utils';
|
|
72
|
-
import type { PruneCalibrationState } from '@/types/graph';
|
|
72
|
+
import type { PruneCalibrationState, FileManifestEntry } from '@/types/graph';
|
|
73
|
+
import { buildFileManifestBlock, FILE_MANIFEST_PREFIX } from '@/utils/fileManifest';
|
|
73
74
|
import {
|
|
74
75
|
buildContextAnalytics,
|
|
75
76
|
type ContextAnalytics,
|
|
@@ -1683,6 +1684,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1683
1684
|
const contentStart = systemMsg != null ? 1 : 0;
|
|
1684
1685
|
let usedTokens = 0;
|
|
1685
1686
|
let windowStart = messages.length; // index where the recent window begins
|
|
1687
|
+
let fileManifestTokens = 0; // populated in Step 4 if file manifest is injected
|
|
1686
1688
|
|
|
1687
1689
|
if (summary == null || summary === '') {
|
|
1688
1690
|
// Mode A: No summary — include as many recent messages as fit in budget
|
|
@@ -1735,7 +1737,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1735
1737
|
const hasSummary = summaryMsg != null;
|
|
1736
1738
|
|
|
1737
1739
|
// Step 4: Assemble the windowed view
|
|
1738
|
-
// [system] + [summary
|
|
1740
|
+
// [system] + [summary] + [file manifest] + [recent window]
|
|
1741
|
+
//
|
|
1742
|
+
// File manifest is injected ONLY when compaction is active (messages behind summary).
|
|
1743
|
+
// It provides the LLM with awareness of all conversation files so it can
|
|
1744
|
+
// retrieve content on demand via file_search or content_tool read.
|
|
1739
1745
|
const viewParts: BaseMessage[] = [];
|
|
1740
1746
|
if (systemMsg != null) {
|
|
1741
1747
|
viewParts.push(systemMsg);
|
|
@@ -1743,6 +1749,21 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1743
1749
|
if (summaryMsg != null) {
|
|
1744
1750
|
viewParts.push(summaryMsg);
|
|
1745
1751
|
}
|
|
1752
|
+
|
|
1753
|
+
// Inject file manifest when files exist and messages are being compacted
|
|
1754
|
+
const fileManifest = agentContext.fileManifest;
|
|
1755
|
+
if (fileManifest && fileManifest.length > 0 && compactedMessages.length > 0) {
|
|
1756
|
+
const manifestBlock = buildFileManifestBlock(fileManifest);
|
|
1757
|
+
if (manifestBlock) {
|
|
1758
|
+
const manifestMsg = new SystemMessage(manifestBlock);
|
|
1759
|
+
viewParts.push(manifestMsg);
|
|
1760
|
+
// Account for manifest tokens in the view token map
|
|
1761
|
+
const manifestTokens = tokenCounter != null ? tokenCounter(manifestMsg) : 0;
|
|
1762
|
+
// Will be inserted at the correct index when rebuilding viewTokenMap below
|
|
1763
|
+
fileManifestTokens = manifestTokens;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1746
1767
|
viewParts.push(...recentMessages);
|
|
1747
1768
|
messagesToUse = viewParts;
|
|
1748
1769
|
|
|
@@ -1758,6 +1779,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1758
1779
|
viewTokenMap[viewIdx] = summaryTokens;
|
|
1759
1780
|
viewIdx++;
|
|
1760
1781
|
}
|
|
1782
|
+
if (fileManifestTokens > 0) {
|
|
1783
|
+
viewTokenMap[viewIdx] = fileManifestTokens;
|
|
1784
|
+
viewIdx++;
|
|
1785
|
+
}
|
|
1761
1786
|
for (let i = windowStart; i < messages.length; i++) {
|
|
1762
1787
|
viewTokenMap[viewIdx] = agentContext.indexTokenCountMap[i];
|
|
1763
1788
|
viewIdx++;
|
|
@@ -1765,10 +1790,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1765
1790
|
agentContext.indexTokenCountMap = viewTokenMap;
|
|
1766
1791
|
|
|
1767
1792
|
console.debug(
|
|
1768
|
-
`[Graph:Compaction]
|
|
1769
|
-
`
|
|
1770
|
-
`summary=${summarySource}
|
|
1771
|
-
`
|
|
1793
|
+
`[Graph:Compaction] ${messages.length}→${viewParts.length} msgs | ` +
|
|
1794
|
+
`compacted=${compactedMessages.length} window=${recentMessages.length} | ` +
|
|
1795
|
+
`summary=${summarySource} | budget=${usedTokens}/${recentBudget}` +
|
|
1796
|
+
(fileManifestTokens > 0 ? ` | manifest=${fileManifest?.length ?? 0} files (${fileManifestTokens}tok)` : '')
|
|
1772
1797
|
);
|
|
1773
1798
|
|
|
1774
1799
|
// Step 5: Fire background summary update (non-blocking)
|
|
@@ -637,6 +637,8 @@ describe('Proactive Summarization — Context Pressure', () => {
|
|
|
637
637
|
|
|
638
638
|
import { applyCalibration as _applyCalibration } from '@/utils/pruneCalibration';
|
|
639
639
|
import { COMPACTION_RECENT_ROUNDS } from '@/common/constants';
|
|
640
|
+
import { buildFileManifestBlock, FILE_MANIFEST_PREFIX } from '@/utils/fileManifest';
|
|
641
|
+
import type { FileManifestEntry } from '@/types/graph';
|
|
640
642
|
|
|
641
643
|
describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
642
644
|
/**
|
|
@@ -651,8 +653,9 @@ describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
|
651
653
|
maxTokens: number;
|
|
652
654
|
summary?: string;
|
|
653
655
|
tokenCounter: TokenCounter;
|
|
656
|
+
fileManifest?: FileManifestEntry[];
|
|
654
657
|
}) {
|
|
655
|
-
const { messages, indexTokenCountMap, maxTokens, summary, tokenCounter } = opts;
|
|
658
|
+
const { messages, indexTokenCountMap, maxTokens, summary, tokenCounter, fileManifest } = opts;
|
|
656
659
|
|
|
657
660
|
const systemMsg = messages[0]?.getType() === 'system' ? messages[0] : null;
|
|
658
661
|
const systemTokens = systemMsg != null ? (indexTokenCountMap[0] ?? 0) : 0;
|
|
@@ -705,9 +708,20 @@ describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
|
705
708
|
const view: BaseMessage[] = [];
|
|
706
709
|
if (systemMsg) view.push(systemMsg);
|
|
707
710
|
if (summaryMsg) view.push(summaryMsg);
|
|
711
|
+
|
|
712
|
+
// Inject file manifest when files exist and messages are being compacted
|
|
713
|
+
let fileManifestMsg: SystemMessage | null = null;
|
|
714
|
+
if (fileManifest && fileManifest.length > 0 && compactedMessages.length > 0) {
|
|
715
|
+
const manifestBlock = buildFileManifestBlock(fileManifest);
|
|
716
|
+
if (manifestBlock) {
|
|
717
|
+
fileManifestMsg = new SystemMessage(manifestBlock);
|
|
718
|
+
view.push(fileManifestMsg);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
708
722
|
view.push(...recentMessages);
|
|
709
723
|
|
|
710
|
-
return { view, compactedMessages, recentMessages, usedTokens };
|
|
724
|
+
return { view, compactedMessages, recentMessages, usedTokens, fileManifestMsg };
|
|
711
725
|
}
|
|
712
726
|
|
|
713
727
|
it('builds a windowed view without deleting any messages', () => {
|
|
@@ -949,4 +963,141 @@ describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
|
949
963
|
expect(messages[0].content).toBe(originalFirstContent);
|
|
950
964
|
expect(messages[messages.length - 1].content).toBe(originalLastContent);
|
|
951
965
|
});
|
|
966
|
+
|
|
967
|
+
// ── File Manifest in Windowed View ─────────────────────────────────────
|
|
968
|
+
|
|
969
|
+
it('injects file manifest block when files exist and messages are compacted', () => {
|
|
970
|
+
const messages = buildConversation(20, 400);
|
|
971
|
+
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
972
|
+
for (let i = 0; i < messages.length; i++) {
|
|
973
|
+
indexTokenCountMap[i] = simpleTokenCounter(messages[i]);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const manifest: FileManifestEntry[] = [
|
|
977
|
+
{ fileId: 'f1', filename: 'report.pdf', contentId: 'abc123' },
|
|
978
|
+
{ fileId: 'f2', filename: 'data.csv', contentId: 'def456', source: 'local' },
|
|
979
|
+
];
|
|
980
|
+
|
|
981
|
+
const { view, compactedMessages, fileManifestMsg } = buildWindowedView({
|
|
982
|
+
messages,
|
|
983
|
+
indexTokenCountMap,
|
|
984
|
+
maxTokens: 600,
|
|
985
|
+
summary: 'Summary of earlier turns',
|
|
986
|
+
tokenCounter: simpleTokenCounter,
|
|
987
|
+
fileManifest: manifest,
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// File manifest is injected
|
|
991
|
+
expect(fileManifestMsg).not.toBeNull();
|
|
992
|
+
expect(compactedMessages.length).toBeGreaterThan(0);
|
|
993
|
+
|
|
994
|
+
// Manifest message contains file names and content IDs
|
|
995
|
+
const manifestContent = fileManifestMsg!.content as string;
|
|
996
|
+
expect(manifestContent).toContain(FILE_MANIFEST_PREFIX);
|
|
997
|
+
expect(manifestContent).toContain('report.pdf');
|
|
998
|
+
expect(manifestContent).toContain('abc123');
|
|
999
|
+
expect(manifestContent).toContain('data.csv');
|
|
1000
|
+
expect(manifestContent).toContain('def456');
|
|
1001
|
+
|
|
1002
|
+
// View order: [system] + [summary] + [file manifest] + [recent messages]
|
|
1003
|
+
expect(view[0].getType()).toBe('system');
|
|
1004
|
+
expect((view[1].content as string)).toContain('[Conversation Summary]');
|
|
1005
|
+
expect((view[2].content as string)).toContain(FILE_MANIFEST_PREFIX);
|
|
1006
|
+
// Recent messages follow
|
|
1007
|
+
expect(view.length).toBeGreaterThan(3);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('does NOT inject file manifest when no messages are compacted', () => {
|
|
1011
|
+
const messages = buildConversation(4, 100); // small conversation
|
|
1012
|
+
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
1013
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1014
|
+
indexTokenCountMap[i] = simpleTokenCounter(messages[i]);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const manifest: FileManifestEntry[] = [
|
|
1018
|
+
{ fileId: 'f1', filename: 'file.txt', contentId: 'aaa' },
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
const { compactedMessages, fileManifestMsg } = buildWindowedView({
|
|
1022
|
+
messages,
|
|
1023
|
+
indexTokenCountMap,
|
|
1024
|
+
maxTokens: 100_000, // everything fits
|
|
1025
|
+
tokenCounter: simpleTokenCounter,
|
|
1026
|
+
fileManifest: manifest,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// No compaction happened, so no manifest injected
|
|
1030
|
+
expect(compactedMessages.length).toBe(0);
|
|
1031
|
+
expect(fileManifestMsg).toBeNull();
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it('does NOT inject file manifest when manifest is empty', () => {
|
|
1035
|
+
const messages = buildConversation(20, 400);
|
|
1036
|
+
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
1037
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1038
|
+
indexTokenCountMap[i] = simpleTokenCounter(messages[i]);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const { fileManifestMsg } = buildWindowedView({
|
|
1042
|
+
messages,
|
|
1043
|
+
indexTokenCountMap,
|
|
1044
|
+
maxTokens: 600,
|
|
1045
|
+
summary: 'Summary',
|
|
1046
|
+
tokenCounter: simpleTokenCounter,
|
|
1047
|
+
fileManifest: [],
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
expect(fileManifestMsg).toBeNull();
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
// ===========================================================================
|
|
1055
|
+
// File Manifest Utility — Unit Tests
|
|
1056
|
+
// ===========================================================================
|
|
1057
|
+
|
|
1058
|
+
describe('buildFileManifestBlock', () => {
|
|
1059
|
+
it('returns empty string for undefined manifest', () => {
|
|
1060
|
+
expect(buildFileManifestBlock(undefined)).toBe('');
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it('returns empty string for empty manifest', () => {
|
|
1064
|
+
expect(buildFileManifestBlock([])).toBe('');
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it('builds block with file names and content IDs', () => {
|
|
1068
|
+
const manifest: FileManifestEntry[] = [
|
|
1069
|
+
{ fileId: 'f1', filename: 'report.pdf', contentId: 'abc123' },
|
|
1070
|
+
{ fileId: 'f2', filename: 'notes.md', contentId: 'def456' },
|
|
1071
|
+
];
|
|
1072
|
+
|
|
1073
|
+
const block = buildFileManifestBlock(manifest);
|
|
1074
|
+
|
|
1075
|
+
expect(block).toContain(FILE_MANIFEST_PREFIX);
|
|
1076
|
+
expect(block).toContain('report.pdf');
|
|
1077
|
+
expect(block).toContain('(content_id: abc123)');
|
|
1078
|
+
expect(block).toContain('notes.md');
|
|
1079
|
+
expect(block).toContain('(content_id: def456)');
|
|
1080
|
+
expect(block).toContain('file_search or content_tool read');
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
it('includes source when provided', () => {
|
|
1084
|
+
const manifest: FileManifestEntry[] = [
|
|
1085
|
+
{ fileId: 'f1', filename: 'doc.pdf', source: 'sharepoint' },
|
|
1086
|
+
];
|
|
1087
|
+
|
|
1088
|
+
const block = buildFileManifestBlock(manifest);
|
|
1089
|
+
expect(block).toContain('[sharepoint]');
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('handles entries without contentId or source', () => {
|
|
1093
|
+
const manifest: FileManifestEntry[] = [
|
|
1094
|
+
{ fileId: 'f1', filename: 'image.png' },
|
|
1095
|
+
];
|
|
1096
|
+
|
|
1097
|
+
const block = buildFileManifestBlock(manifest);
|
|
1098
|
+
expect(block).toContain('- image.png');
|
|
1099
|
+
// No per-file content_id or source brackets in the file line
|
|
1100
|
+
expect(block).not.toContain('(content_id:');
|
|
1101
|
+
expect(block).not.toContain('[local]');
|
|
1102
|
+
});
|
|
952
1103
|
});
|
package/src/types/graph.ts
CHANGED
|
@@ -555,6 +555,26 @@ export interface PruneCalibrationState {
|
|
|
555
555
|
iterations: number;
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Lightweight file metadata entry for conversation-level file awareness.
|
|
560
|
+
* Contains only IDs and names — NOT full content — so the agent always knows
|
|
561
|
+
* what files exist in the conversation even after compaction pushes old messages
|
|
562
|
+
* behind the summary window. The agent can retrieve full content on-demand
|
|
563
|
+
* via file_search (RAG) or content_tool read (by contentId).
|
|
564
|
+
*/
|
|
565
|
+
export interface FileManifestEntry {
|
|
566
|
+
/** Unique file identifier (e.g., MongoDB ObjectId or UUID) */
|
|
567
|
+
fileId: string;
|
|
568
|
+
/** Original filename (e.g., "quarterly-report.pdf") */
|
|
569
|
+
filename: string;
|
|
570
|
+
/** Content identifier for on-demand retrieval via content_tool read */
|
|
571
|
+
contentId?: string;
|
|
572
|
+
/** File source (e.g., "local", "sharepoint", "onedrive") */
|
|
573
|
+
source?: string;
|
|
574
|
+
/** Index of the message that introduced this file (0-based in the original message array) */
|
|
575
|
+
messageIndex?: number;
|
|
576
|
+
}
|
|
577
|
+
|
|
558
578
|
export interface AgentInputs {
|
|
559
579
|
agentId: string;
|
|
560
580
|
/** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */
|
|
@@ -632,4 +652,20 @@ export interface AgentInputs {
|
|
|
632
652
|
* @see SummarizationConfig
|
|
633
653
|
*/
|
|
634
654
|
summarizationConfig?: SummarizationConfig;
|
|
655
|
+
/**
|
|
656
|
+
* Lightweight file manifest for the conversation.
|
|
657
|
+
* Contains file IDs, names, and metadata — NOT full content.
|
|
658
|
+
*
|
|
659
|
+
* Used by the compaction engine to inject a [Conversation Files] block
|
|
660
|
+
* into the windowed view, ensuring the LLM always knows what files exist
|
|
661
|
+
* even when old messages (with full file content) are behind the summary.
|
|
662
|
+
*
|
|
663
|
+
* The agent can retrieve full content on-demand via:
|
|
664
|
+
* - file_search (RAG semantic search over embedded files)
|
|
665
|
+
* - content_tool read (by contentId for exact file retrieval)
|
|
666
|
+
*
|
|
667
|
+
* Built by the orchestrator (e.g., Ranger) from message_file_map
|
|
668
|
+
* and metadata.context_files across all conversation messages.
|
|
669
|
+
*/
|
|
670
|
+
fileManifest?: FileManifestEntry[];
|
|
635
671
|
}
|