diff --git a/core/src/a2a/a2a_remote_agent_utils.ts b/core/src/a2a/a2a_remote_agent_utils.ts index aa8c2694..75e78a58 100644 --- a/core/src/a2a/a2a_remote_agent_utils.ts +++ b/core/src/a2a/a2a_remote_agent_utils.ts @@ -19,7 +19,13 @@ export interface UserFunctionCall { } /** - * Returns a UserFunctionCall when the event at index has a FunctionResponse. + * Returns a UserFunctionCall when the event at `index` contains a + * FunctionResponse that can be traced back to a preceding FunctionCall event. + * + * @param session - The session whose event history to inspect. + * @param index - Index of the candidate event to examine. + * @returns The matching `UserFunctionCall`, or `undefined` if the event at + * `index` is not a user function-response event or has no preceding call. */ export function getUserFunctionCallAt( session: Session, @@ -62,6 +68,10 @@ export function getUserFunctionCallAt( /** * Checks if an event contains a function call with the given ID. + * + * @param event - The event to inspect. + * @param callId - The function call ID to look for. + * @returns `true` if a part in the event has a matching `functionCall.id`. */ export function isFunctionCallEvent(event: AdkEvent, callId: string): boolean { if (!event || !event.content || !event.content.parts) { @@ -75,6 +85,10 @@ export function isFunctionCallEvent(event: AdkEvent, callId: string): boolean { /** * Finds the first part with a FunctionResponse and returns the call ID. + * + * @param event - The event to inspect. + * @returns The `id` of the first FunctionResponse part, or `undefined` if + * none is found. */ export function getFunctionResponseCallId(event: AdkEvent): string | undefined { if (!event || !event.content || !event.content.parts) { @@ -89,8 +103,13 @@ export function getFunctionResponseCallId(event: AdkEvent): string | undefined { } /** - * Returns content parts for all events not present in the remote session - * and a2a contextId if found in a remote agent event metadata. + * Returns A2A content parts for all events not yet seen by the remote agent, + * along with the A2A context ID found in the most recent remote agent event. + * + * @param ctx - The current invocation context, used to identify the remote + * agent's authored events. + * @param session - The local session whose event history to diff. + * @returns An object with the missing `parts` and an optional `contextId`. */ export function toMissingRemoteSessionParts( ctx: InvocationContext, @@ -137,7 +156,13 @@ export function toMissingRemoteSessionParts( } /** - * Wraps an agent event as a user message for context. + * Wraps an agent event as a user message so it can be sent as context to a + * remote agent that only accepts user-role messages. + * + * @param ctx - The current invocation context. + * @param agentEvent - The agent-authored event to reframe as a user message. + * @returns A new event with `author: 'user'` whose parts summarise the + * original agent event's text, function calls, and function responses. */ export function presentAsUserMessage( ctx: InvocationContext, diff --git a/core/src/a2a/event_converter_utils.ts b/core/src/a2a/event_converter_utils.ts index eb63068e..13f84cee 100644 --- a/core/src/a2a/event_converter_utils.ts +++ b/core/src/a2a/event_converter_utils.ts @@ -43,6 +43,12 @@ import {toA2AParts, toGenAIPart, toGenAIParts} from './part_converter_utils.js'; /** * Converts a session Event to an A2A Message. + * + * @param event - The ADK event to convert. + * @param appName - The name of the ADK application. + * @param userId - The ID of the current user. + * @param sessionId - The ID of the current session. + * @returns An A2A message with the event's parts and metadata. */ export function toA2AMessage( event: AdkEvent, @@ -63,7 +69,14 @@ export function toA2AMessage( } /** - * Converts an A2A Event to a Session Event. + * Converts an A2A Event to an ADK Session Event. + * + * @param event - The A2A event to convert (message, task, artifact update, or + * status update). + * @param invocationId - The ADK invocation ID to attach to the resulting event. + * @param agentName - The name of the agent to use as the event author. + * @returns The converted ADK event, or `undefined` if the A2A event type + * produces no content. */ export function toAdkEvent( event: A2AEvent, diff --git a/core/src/a2a/metadata_converter_utils.ts b/core/src/a2a/metadata_converter_utils.ts index 60c864c3..21689099 100644 --- a/core/src/a2a/metadata_converter_utils.ts +++ b/core/src/a2a/metadata_converter_utils.ts @@ -46,6 +46,9 @@ export enum AdkMetadataKeys { /** * Creates ADK Event metadata from an A2A Event. + * + * @param a2aEvent - The A2A event to extract metadata from. + * @returns A record containing the ADK task ID and context ID keys. */ export function getAdkEventMetadata( a2aEvent: A2AEvent, @@ -58,6 +61,12 @@ export function getAdkEventMetadata( /** * Creates A2A Event metadata from an ADK Event. + * + * @param adkEvent - The ADK event to extract metadata from. + * @param appName - The name of the ADK application. + * @param userId - The ID of the current user. + * @param sessionId - The ID of the current session. + * @returns A record of A2A metadata keys populated from the ADK event. */ export function getA2AEventMetadata( adkEvent: AdkEvent, @@ -91,6 +100,11 @@ export function getA2AEventMetadata( /** * Creates A2A Session metadata from ADK Event invocation metadata. + * + * @param appName - The name of the ADK application. + * @param userId - The ID of the current user. + * @param sessionId - The ID of the current session. + * @returns A record of A2A metadata keys for app name, user ID, and session ID. */ export function getA2ASessionMetadata({ appName, @@ -110,6 +124,10 @@ export function getA2ASessionMetadata({ /** * Creates A2A Event metadata from ADK Event actions. + * + * @param actions - The ADK event actions to extract escalation and transfer + * metadata from. + * @returns A record with escalate and transfer-to-agent A2A metadata keys. */ export function getA2AEventMetadataFromActions( actions: AdkEventActions, diff --git a/core/src/a2a/part_converter_utils.ts b/core/src/a2a/part_converter_utils.ts index fd9f80e4..8ec45dbf 100644 --- a/core/src/a2a/part_converter_utils.ts +++ b/core/src/a2a/part_converter_utils.ts @@ -35,7 +35,11 @@ enum DataPartType { } /** - * Converts GenAI Parts to A2A Parts. + * Converts an array of GenAI Parts to A2A Parts. + * + * @param parts - The GenAI parts to convert. Defaults to an empty array. + * @param longRunningToolIDs - IDs of function calls that are long-running. + * @returns An array of A2A parts. */ export function toA2AParts( parts: GenAIPart[] = [], @@ -45,7 +49,11 @@ export function toA2AParts( } /** - * Converts a GenAI Part to an A2A Part. + * Converts a single GenAI Part to the appropriate A2A Part type. + * + * @param part - The GenAI part to convert. + * @param longRunningToolIDs - IDs of function calls that are long-running. + * @returns The corresponding A2A part (text, file, or data). */ export function toA2APart( part: GenAIPart, @@ -64,6 +72,9 @@ export function toA2APart( /** * Converts a GenAI Text Part to an A2A Text Part. + * + * @param part - The GenAI part containing a text field. + * @returns An A2A text part, with thought metadata attached if applicable. */ export function toA2ATextPart(part: GenAIPart): A2APart { const a2aPart: A2APart = {kind: 'text', text: part.text || ''}; @@ -79,6 +90,10 @@ export function toA2ATextPart(part: GenAIPart): A2APart { /** * Converts a GenAI File Part to an A2A File Part. + * + * @param part - The GenAI part containing `fileData` or `inlineData`. + * @returns An A2A file part with URI or bytes depending on the source. + * @throws {Error} If the part contains neither `fileData` nor `inlineData`. */ export function toA2AFilePart(part: GenAIPart): A2APart { const metadata: Record = {}; @@ -112,7 +127,11 @@ export function toA2AFilePart(part: GenAIPart): A2APart { } /** - * Converts a GenAI Data Part to an A2A Data Part. + * Converts a GenAI Data Part (function call/response or code) to an A2A Data Part. + * + * @param part - The GenAI part containing structured data. + * @param longRunningToolIDs - IDs of function calls that are long-running. + * @returns An A2A data part with the appropriate type metadata. */ export function toA2ADataPart( part: GenAIPart, @@ -172,6 +191,12 @@ export function toA2ADataPart( }; } +/** + * Converts an A2A Message to a GenAI Content object. + * + * @param a2aMessage - The A2A message to convert. + * @returns A GenAI user or model content object based on the message role. + */ export function toGenAIContent(a2aMessage: Message): GenAIContent { const parts = toGenAIParts(a2aMessage.parts); @@ -181,14 +206,21 @@ export function toGenAIContent(a2aMessage: Message): GenAIContent { } /** - * Converts an A2A Part to a GenAI Part. + * Converts an array of A2A Parts to GenAI Parts. + * + * @param a2aParts - The A2A parts to convert. + * @returns An array of GenAI parts. */ export function toGenAIParts(a2aParts: A2APart[]): GenAIPart[] { return a2aParts.map((a2aPart) => toGenAIPart(a2aPart)); } /** - * Converts an A2A Part to a GenAI Part. + * Converts a single A2A Part to the appropriate GenAI Part type. + * + * @param a2aPart - The A2A part to convert. + * @returns The corresponding GenAI part. + * @throws {Error} If the A2A part has an unrecognized `kind`. */ export function toGenAIPart(a2aPart: A2APart): GenAIPart { if (a2aPart.kind === 'text') { @@ -208,6 +240,9 @@ export function toGenAIPart(a2aPart: A2APart): GenAIPart { /** * Converts an A2A Text Part to a GenAI Part. + * + * @param a2aPart - The A2A text part to convert. + * @returns A GenAI part with `text` and optional `thought` flag. */ export function toGenAIPartText(a2aPart: A2ATextPart): GenAIPart { return { @@ -218,6 +253,11 @@ export function toGenAIPartText(a2aPart: A2ATextPart): GenAIPart { /** * Converts an A2A File Part to a GenAI Part. + * + * @param a2aPart - The A2A file part containing bytes or a URI. + * @returns A GenAI part with `inlineData` or `fileData` depending on the + * source, with optional video metadata attached. + * @throws {Error} If the file part contains neither bytes nor a URI. */ export function toGenAIPartFile(a2aPart: A2AFilePart): GenAIPart { const part: GenAIPart = {}; @@ -247,7 +287,12 @@ export function toGenAIPartFile(a2aPart: A2AFilePart): GenAIPart { } /** - * Converts an A2A Data Part to a GenAI Part. + * Converts an A2A Data Part to the appropriate GenAI Part. + * + * @param a2aPart - The A2A data part containing structured data and type metadata. + * @returns A GenAI part with the appropriate function call, function response, + * or code execution fields, falling back to a JSON text part if the type is + * unrecognized. */ export function toGenAIPartData(a2aPart: A2ADataPart): GenAIPart { if (!a2aPart.data) { diff --git a/core/src/agents/processors/basic_llm_request_processor.ts b/core/src/agents/processors/basic_llm_request_processor.ts index 56eb9b47..552c9855 100644 --- a/core/src/agents/processors/basic_llm_request_processor.ts +++ b/core/src/agents/processors/basic_llm_request_processor.ts @@ -16,6 +16,13 @@ import {BaseLlmRequestProcessor} from './base_llm_processor.js'; * connect settings. */ export class BasicLlmRequestProcessor extends BaseLlmRequestProcessor { + /** + * Populates model name, generation config, output schema, and live connect + * settings on the request from the agent and run config. + * + * @param invocationContext - The current invocation context. + * @param llmRequest - The request object to populate in place. + */ // eslint-disable-next-line require-yield override async *runAsync( invocationContext: InvocationContext, diff --git a/core/src/agents/processors/identity_llm_request_processor.ts b/core/src/agents/processors/identity_llm_request_processor.ts index 3c5c58a0..eb06b432 100644 --- a/core/src/agents/processors/identity_llm_request_processor.ts +++ b/core/src/agents/processors/identity_llm_request_processor.ts @@ -14,6 +14,13 @@ import {BaseLlmRequestProcessor} from './base_llm_processor.js'; * informing the model of the agent's name and description. */ export class IdentityLlmRequestProcessor extends BaseLlmRequestProcessor { + /** + * Appends agent name and description as identity instructions to the system + * prompt of the request. + * + * @param invocationContext - The current invocation context. + * @param llmRequest - The request object to append instructions to. + */ // eslint-disable-next-line require-yield override async *runAsync( invocationContext: InvocationContext, diff --git a/core/src/agents/processors/request_confirmation_llm_request_processor.ts b/core/src/agents/processors/request_confirmation_llm_request_processor.ts index 6d607ec7..bff4d1a9 100644 --- a/core/src/agents/processors/request_confirmation_llm_request_processor.ts +++ b/core/src/agents/processors/request_confirmation_llm_request_processor.ts @@ -26,7 +26,15 @@ import {BaseLlmRequestProcessor} from './base_llm_processor.js'; * corresponding tools before the next LLM turn. */ export class RequestConfirmationLlmRequestProcessor extends BaseLlmRequestProcessor { - /** Handles tool confirmation information to build the LLM request. */ + /** + * Resumes tool calls that were paused for user confirmation, re-invoking + * them with the confirmed or denied decision before the next LLM turn. + * + * @param invocationContext - The current invocation context, including the + * session event history used to locate pending confirmation responses. + * @yields Function response events for tools that have been confirmed and + * are ready to resume. + */ override async *runAsync( invocationContext: InvocationContext, ): AsyncGenerator { diff --git a/core/src/context/summarizers/llm_summarizer.ts b/core/src/context/summarizers/llm_summarizer.ts index 78a9e289..bc3a66db 100644 --- a/core/src/context/summarizers/llm_summarizer.ts +++ b/core/src/context/summarizers/llm_summarizer.ts @@ -13,8 +13,14 @@ import {BaseLlm} from '../../models/base_llm.js'; import {LlmRequest} from '../../models/llm_request.js'; import {BaseSummarizer} from './base_summarizer.js'; +/** Options for constructing an {@link LlmSummarizer}. */ export interface LlmSummarizerOptions { + /** The LLM instance used to generate the summary. */ llm: BaseLlm; + /** + * Optional system prompt prepended to the formatted events. Defaults to a + * built-in summarization prompt when omitted. + */ prompt?: string; } @@ -32,11 +38,22 @@ export class LlmSummarizer implements BaseSummarizer { private readonly llm: BaseLlm; private readonly prompt: string; + /** + * @param options - Configuration specifying the LLM and optional prompt. + */ constructor(options: LlmSummarizerOptions) { this.llm = options.llm; this.prompt = options.prompt || DEFAULT_PROMPT; } + /** + * Summarizes a list of events into a single {@link CompactedEvent} using the + * configured LLM. + * + * @param events - The events to summarize. Must be non-empty. + * @returns A promise resolving to the compacted representation. + * @throws {Error} If `events` is empty or the LLM returns no content. + */ async summarize(events: Event[]): Promise { if (events.length === 0) { throw new Error('Cannot summarize an empty list of events.'); diff --git a/core/src/plugins/security_plugin.ts b/core/src/plugins/security_plugin.ts index 3f166838..a3727af1 100644 --- a/core/src/plugins/security_plugin.ts +++ b/core/src/plugins/security_plugin.ts @@ -36,21 +36,40 @@ export enum PolicyOutcome { ALLOW = 'ALLOW', } +/** The result returned by a policy engine after evaluating a tool call. */ export interface PolicyCheckResult { + /** The policy decision: `ALLOW`, `DENY`, or `CONFIRM`. */ outcome: string; + /** Optional human-readable explanation of the decision. */ reason?: string; } +/** Context passed to a policy engine when evaluating a tool call. */ export interface ToolCallPolicyContext { + /** The tool being invoked. */ tool: BaseTool; + /** The arguments supplied to the tool call. */ toolArgs: Record; } +/** Interface for policy engines that gate tool call execution. */ export interface BasePolicyEngine { + /** + * Evaluates whether a tool call should be allowed, denied, or confirmed. + * + * @param context - The tool and its arguments to evaluate. + * @returns A promise resolving to the policy decision. + */ evaluate(context: ToolCallPolicyContext): Promise; } +/** In-memory policy engine that permits all tool calls. Intended for prototyping. */ export class InMemoryPolicyEngine implements BasePolicyEngine { + /** + * Always returns {@link PolicyOutcome.ALLOW} for every tool call. + * + * @returns A promise resolving to an ALLOW result. + */ async evaluate(): Promise { // Default permissive implementation return Promise.resolve({ @@ -69,11 +88,25 @@ export class InMemoryPolicyEngine implements BasePolicyEngine { export class SecurityPlugin extends BasePlugin { private readonly policyEngine: BasePolicyEngine; + /** + * @param params - Optional configuration. Defaults to {@link InMemoryPolicyEngine} + * when no policy engine is provided. + */ constructor(params?: {policyEngine?: BasePolicyEngine}) { super('security_plugin'); this.policyEngine = params?.policyEngine ?? new InMemoryPolicyEngine(); } + /** + * Intercepts tool calls, evaluating them against the policy engine before + * execution and handling confirmation flows for calls that require it. + * + * @param tool - The tool about to be invoked. + * @param toolArgs - The arguments supplied to the tool call. + * @param toolContext - The current tool execution context. + * @returns A partial or error response object if the call is blocked or + * awaiting confirmation, or `undefined` to allow execution to proceed. + */ override async beforeToolCallback({ tool, toolArgs, diff --git a/core/test/agents/processors/code_execution_request_processor_test.ts b/core/test/agents/processors/code_execution_request_processor_test.ts new file mode 100644 index 00000000..cf67f308 --- /dev/null +++ b/core/test/agents/processors/code_execution_request_processor_test.ts @@ -0,0 +1,221 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + BaseAgent, + InvocationContext, + LlmAgent, + LlmRequest, + PluginManager, + createSession, +} from '@google/adk'; +import {describe, expect, it} from 'vitest'; +import { + CODE_EXECUTION_REQUEST_PROCESSOR, + CodeExecutionResponseProcessor, +} from '../../../src/agents/processors/code_execution_request_processor.js'; +import { + BaseCodeExecutor, + ExecuteCodeParams, +} from '../../../src/code_executors/base_code_executor.js'; +import {CodeExecutionResult} from '../../../src/code_executors/code_execution_utils.js'; + +class MockBaseAgent extends BaseAgent { + constructor(name: string) { + super({name}); + } + protected async *runAsyncImpl(_context: InvocationContext) {} + protected async *runLiveImpl(_context: InvocationContext) {} +} + +class TestCodeExecutor extends BaseCodeExecutor { + async executeCode(_params: ExecuteCodeParams): Promise { + return {stdout: '', stderr: '', outputFiles: []}; + } +} + +function createMockInvocationContext(agent: BaseAgent): InvocationContext { + return new InvocationContext({ + invocationId: 'test-invocation', + agent, + session: createSession({ + id: 'test-session', + events: [], + appName: 'test-app', + userId: 'test-user', + }), + pluginManager: new PluginManager([]), + }); +} + +function createLlmRequest(overrides: Partial = {}): LlmRequest { + return { + contents: [], + toolsDict: {}, + liveConnectConfig: {}, + ...overrides, + }; +} + +async function collectEvents(gen: AsyncGenerator): Promise { + const results: T[] = []; + for await (const event of gen) { + results.push(event); + } + return results; +} + +describe('CodeExecutionRequestProcessor', () => { + describe('early-exit paths', () => { + it('yields no events and leaves request unchanged for a non-LlmAgent', async () => { + const agent = new MockBaseAgent('non-llm-agent'); + const ctx = createMockInvocationContext(agent); + const llmRequest = createLlmRequest({ + contents: [{role: 'user', parts: [{text: 'hello'}]}], + }); + + const events = await collectEvents( + CODE_EXECUTION_REQUEST_PROCESSOR.runAsync(ctx, llmRequest), + ); + + expect(events).toHaveLength(0); + expect(llmRequest.contents).toHaveLength(1); + }); + + it('yields no events when LlmAgent has no codeExecutor', async () => { + const agent = new LlmAgent({ + name: 'agent-no-executor', + model: 'gemini-2.5-flash', + }); + const ctx = createMockInvocationContext(agent); + const llmRequest = createLlmRequest({ + contents: [{role: 'user', parts: [{text: 'hello'}]}], + }); + + const events = await collectEvents( + CODE_EXECUTION_REQUEST_PROCESSOR.runAsync(ctx, llmRequest), + ); + + expect(events).toHaveLength(0); + }); + + it('calls runPreProcessor and proceeds to convertCodeExecutionParts when codeExecutor is BaseCodeExecutor', async () => { + const executor = new TestCodeExecutor(); + const agent = new LlmAgent({ + name: 'agent-with-executor', + model: 'gemini-2.5-flash', + codeExecutor: executor, + }); + const ctx = createMockInvocationContext(agent); + const llmRequest = createLlmRequest({ + contents: [{role: 'user', parts: [{text: 'hello'}]}], + }); + + // Should not throw — runPreProcessor exits early because + // isBuiltInCodeExecutor is false and optimizeDataFile is false + const events = await collectEvents( + CODE_EXECUTION_REQUEST_PROCESSOR.runAsync(ctx, llmRequest), + ); + + expect(events).toHaveLength(0); + // Content should still be present after processing + expect(llmRequest.contents).toHaveLength(1); + }); + }); +}); + +describe('CodeExecutionResponseProcessor', () => { + const responseProcessor = new CodeExecutionResponseProcessor(); + + describe('early-exit paths', () => { + it('yields no events for a partial response', async () => { + const agent = new LlmAgent({ + name: 'agent', + model: 'gemini-2.5-flash', + codeExecutor: new TestCodeExecutor(), + }); + const ctx = createMockInvocationContext(agent); + const partialResponse = { + partial: true, + content: {role: 'model', parts: [{text: 'thinking...'}]}, + }; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, partialResponse), + ); + + expect(events).toHaveLength(0); + }); + + it('yields no events for a non-LlmAgent', async () => { + const agent = new MockBaseAgent('non-llm'); + const ctx = createMockInvocationContext(agent); + const llmResponse = { + partial: false, + content: {role: 'model', parts: [{text: 'done'}]}, + }; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, llmResponse), + ); + + expect(events).toHaveLength(0); + }); + + it('yields no events when LlmAgent has no codeExecutor', async () => { + const agent = new LlmAgent({ + name: 'agent-no-executor', + model: 'gemini-2.5-flash', + }); + const ctx = createMockInvocationContext(agent); + const llmResponse = { + partial: false, + content: {role: 'model', parts: [{text: 'done'}]}, + }; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, llmResponse), + ); + + expect(events).toHaveLength(0); + }); + + it('yields no events when response has no content', async () => { + const agent = new LlmAgent({ + name: 'agent-with-executor', + model: 'gemini-2.5-flash', + codeExecutor: new TestCodeExecutor(), + }); + const ctx = createMockInvocationContext(agent); + const llmResponse = {partial: false}; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, llmResponse), + ); + + expect(events).toHaveLength(0); + }); + + it('yields no events when response content has no code block', async () => { + const agent = new LlmAgent({ + name: 'agent-with-executor', + model: 'gemini-2.5-flash', + codeExecutor: new TestCodeExecutor(), + }); + const ctx = createMockInvocationContext(agent); + const llmResponse = { + partial: false, + content: {role: 'model', parts: [{text: 'plain text response'}]}, + }; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, llmResponse), + ); + + expect(events).toHaveLength(0); + }); + }); +}); diff --git a/core/test/events/compacted_event_test.ts b/core/test/events/compacted_event_test.ts new file mode 100644 index 00000000..f2fe00b8 --- /dev/null +++ b/core/test/events/compacted_event_test.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {createEvent} from '@google/adk'; +import {describe, expect, it} from 'vitest'; +import { + createCompactedEvent, + isCompactedEvent, +} from '../../src/events/compacted_event.js'; + +describe('isCompactedEvent', () => { + it('returns true for a valid CompactedEvent', () => { + const event = createCompactedEvent({ + author: 'system', + startTime: 1000, + endTime: 2000, + compactedContent: 'summary', + }); + expect(isCompactedEvent(event)).toBe(true); + }); + + it('returns false for a plain Event', () => { + const event = createEvent({author: 'user'}); + expect(isCompactedEvent(event)).toBe(false); + }); + + it('returns false when isCompacted field is missing', () => { + const event = createEvent({author: 'agent'}) as object; + expect(isCompactedEvent(event as ReturnType)).toBe( + false, + ); + }); + + it('returns false when isCompacted is false', () => { + const event = { + ...createEvent({author: 'agent'}), + isCompacted: false, + } as ReturnType; + expect(isCompactedEvent(event)).toBe(false); + }); +}); + +describe('createCompactedEvent', () => { + it('creates a CompactedEvent with isCompacted set to true', () => { + const event = createCompactedEvent({ + startTime: 100, + endTime: 200, + compactedContent: 'summary text', + }); + expect(event.isCompacted).toBe(true); + }); + + it('carries startTime, endTime, and compactedContent', () => { + const event = createCompactedEvent({ + startTime: 500, + endTime: 1500, + compactedContent: 'key decisions were made', + }); + expect(event.startTime).toBe(500); + expect(event.endTime).toBe(1500); + expect(event.compactedContent).toBe('key decisions were made'); + }); + + it('merges base event defaults (id, invocationId, actions)', () => { + const event = createCompactedEvent({ + startTime: 0, + endTime: 0, + compactedContent: '', + }); + expect(event.id).toBeDefined(); + expect(event.id.length).toBeGreaterThan(0); + expect(event.invocationId).toBe(''); + expect(event.actions).toBeDefined(); + }); + + it('accepts author and content from params', () => { + const event = createCompactedEvent({ + author: 'system', + content: {role: 'model', parts: [{text: 'summary'}]}, + startTime: 0, + endTime: 10, + compactedContent: 'summary', + }); + expect(event.author).toBe('system'); + expect(event.content?.parts?.[0]?.text).toBe('summary'); + }); + + it('satisfies the isCompactedEvent type guard after creation', () => { + const event = createCompactedEvent({ + startTime: 0, + endTime: 0, + compactedContent: '', + }); + expect(isCompactedEvent(event)).toBe(true); + }); +});