Skip to content

Commit 6d87aff

Browse files
committed
add skelly tool output summarization via gemma for every tool call
1 parent e095fa6 commit 6d87aff

3 files changed

Lines changed: 104 additions & 1 deletion

File tree

packages/core/src/agents/executor.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ import {
7171
// DOUBLE_INTERRUPT,
7272
} from '../common/abort-signal-manager.js';
7373

74+
import { SummarizationService } from '../services/summarizer.js';
75+
7476
/** A callback function to report on agent activity. */
7577
export type ActivityCallback = (activity: SubagentActivityEvent) => void;
7678

@@ -103,6 +105,7 @@ export class AgentExecutor<TOutput extends z.ZodTypeAny> {
103105
private readonly runtimeContext: Config;
104106
private readonly onActivity?: ActivityCallback;
105107
private readonly compressionService: ChatCompressionService;
108+
private readonly summarizationService: SummarizationService;
106109
private hasFailedCompressionAttempt = false;
107110

108111
/**
@@ -181,6 +184,7 @@ export class AgentExecutor<TOutput extends z.ZodTypeAny> {
181184
this.toolRegistry = toolRegistry;
182185
this.onActivity = onActivity;
183186
this.compressionService = new ChatCompressionService();
187+
this.summarizationService = new SummarizationService();
184188

185189
const randomIdPart = Math.random().toString(36).slice(2, 8);
186190
// parentPromptId will be undefined if this agent is invoked directly
@@ -1339,7 +1343,36 @@ export class AgentExecutor<TOutput extends z.ZodTypeAny> {
13391343
});
13401344
}
13411345

1342-
return toolResponse.responseParts;
1346+
const toolResponsePartsToReturn: GeminiPart[] =
1347+
toolResponse.responseParts;
1348+
1349+
// Apply summarization if enabled and content is present
1350+
if (this.definition.runConfig.summarizeToolOutput) {
1351+
const summary = await this.summarizationService.summarize(
1352+
toolResponse.responseParts,
1353+
this.definition.modelConfig,
1354+
);
1355+
if (summary) {
1356+
debugLogger.log(
1357+
`[AgentExecutor] Summarized output for tool: ${functionCall.name} (ID: ${callId})`,
1358+
);
1359+
for (const part of toolResponsePartsToReturn) {
1360+
if ('functionResponse' in part && part.functionResponse) {
1361+
// This is a bit awkward, but `response` is just `object`
1362+
// in the type, so we have to cast.
1363+
const responseObject = part.functionResponse.response as {
1364+
llmContent?: unknown;
1365+
};
1366+
if (responseObject.llmContent) {
1367+
responseObject.llmContent = summary;
1368+
break; // Assume only one functionResponse part
1369+
}
1370+
}
1371+
}
1372+
}
1373+
}
1374+
1375+
return toolResponsePartsToReturn;
13431376
})();
13441377

13451378
toolExecutionPromises.push(executionPromise);

packages/core/src/agents/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,6 @@ export interface RunConfig {
191191
max_time_minutes: number;
192192
/** The maximum number of conversational turns. */
193193
max_turns?: number;
194+
/** Whether to summarize tool output before sending it to the model. */
195+
summarizeToolOutput?: boolean;
194196
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type { OllamaModelConfig, ModelConfig } from '../agents/types.js';
8+
import { type Part as OllamaPart, OllamaChat } from '../core/ollamaChat.js';
9+
import type { Part as GeminiPart } from '@google/genai';
10+
import { StreamEventType } from '../core/geminiChat.js';
11+
12+
const SUMMARIZER_SYSTEM_PROMPT = `You are a text summarizer. Your sole purpose is to receive text and provide a concise, factual summary of it. Do not add any commentary or analysis. Focus on the key information presented in the text.`;
13+
14+
export class SummarizationService {
15+
constructor() {}
16+
17+
async summarize(
18+
tollResponseParts: GeminiPart[],
19+
modelConfig: OllamaModelConfig | ModelConfig,
20+
): Promise<string | null> {
21+
if ('host' in modelConfig) {
22+
const userParts: OllamaPart[] = [];
23+
for (const part of tollResponseParts) {
24+
if ('functionResponse' in part) {
25+
// Singe gemma can't handle function responses, we convert it into a string.
26+
userParts.push({ text: JSON.stringify(part.functionResponse) });
27+
} else {
28+
userParts.push(part as OllamaPart);
29+
}
30+
}
31+
32+
const chat = new OllamaChat(
33+
modelConfig as OllamaModelConfig,
34+
SUMMARIZER_SYSTEM_PROMPT,
35+
);
36+
37+
const messageParams = {
38+
message: userParts,
39+
};
40+
41+
const responseStream = await chat.sendMessageStream(
42+
modelConfig?.model,
43+
messageParams,
44+
);
45+
46+
let textResponse = '';
47+
48+
for await (const resp of responseStream) {
49+
if (resp.type === StreamEventType.CHUNK) {
50+
const chunk = resp.value;
51+
const parts = chunk.candidates?.[0]?.content?.parts;
52+
53+
const text = parts?.map((p) => p.text).join('') || '';
54+
55+
if (text) {
56+
textResponse = text;
57+
}
58+
}
59+
}
60+
61+
// Just return the raw text response from the summarizer model.
62+
return textResponse;
63+
} else {
64+
// Summarization for non-Ollama models can be implemented here.
65+
throw new Error('Summarization using Gemini Model is not implemented.');
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)