Skip to content

Commit 32294d4

Browse files
authored
Merge pull request #1048 from MyGO-Mujica/fix/smartfetch-secondary-parent-id
fix(smartfetch): set parentID on secondary sessions to suppress TUI notifications (#1046)
2 parents 83cb13c + de95122 commit 32294d4

4 files changed

Lines changed: 111 additions & 2 deletions

File tree

src/tools/smartfetch/secondary-model.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,4 +362,44 @@ describe('smartfetch/secondary-model', () => {
362362
_testConfig.secondaryModelTimeoutMs = originalTimeout;
363363
}
364364
});
365+
366+
test('passes parentID to session.create when parentSessionID is provided', async () => {
367+
mockV2Client = createV2ClientMock([{ text: 'Answer' }]);
368+
369+
const result = await runSecondaryModelWithFallback(
370+
testInput,
371+
[models[0]],
372+
'Summarize',
373+
'This is enough fetched content to clear the short-content guard.',
374+
'parent-session-id',
375+
);
376+
377+
expect(result.text).toBe('Answer');
378+
expect(mockV2Session.create).toHaveBeenCalledWith(
379+
expect.objectContaining({
380+
body: expect.objectContaining({
381+
title: 'smartfetch-secondary',
382+
parentID: 'parent-session-id',
383+
}),
384+
}),
385+
);
386+
});
387+
388+
test('omits parentID from session.create when parentSessionID is undefined', async () => {
389+
mockV2Client = createV2ClientMock([{ text: 'Answer' }]);
390+
391+
const result = await runSecondaryModelWithFallback(
392+
testInput,
393+
[models[0]],
394+
'Summarize',
395+
'This is enough fetched content to clear the short-content guard.',
396+
);
397+
398+
expect(result.text).toBe('Answer');
399+
expect(mockV2Session.create).toHaveBeenCalledWith(
400+
expect.objectContaining({
401+
body: { title: 'smartfetch-secondary' },
402+
}),
403+
);
404+
});
365405
});

src/tools/smartfetch/secondary-model.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ async function runSecondaryModel(
204204
model: SecondaryModel,
205205
prompt: string,
206206
content: string,
207+
parentSessionID?: string,
207208
) {
208209
const client = getClient(input);
209210
const directory = input.directory;
@@ -215,7 +216,10 @@ async function runSecondaryModel(
215216
try {
216217
const sessionResponse = await client.session.create({
217218
query: { directory },
218-
body: { title: 'smartfetch-secondary' },
219+
body: {
220+
title: 'smartfetch-secondary',
221+
...(parentSessionID ? { parentID: parentSessionID } : {}),
222+
},
219223
throwOnError: true,
220224
});
221225

@@ -326,11 +330,18 @@ export async function runSecondaryModelWithFallback(
326330
models: SecondaryModel[],
327331
prompt: string,
328332
content: string,
333+
parentSessionID?: string,
329334
) {
330335
let lastError: unknown;
331336
for (const model of models) {
332337
try {
333-
const result = await runSecondaryModel(input, model, prompt, content);
338+
const result = await runSecondaryModel(
339+
input,
340+
model,
341+
prompt,
342+
content,
343+
parentSessionID,
344+
);
334345
if (!isUsableSecondaryText(result.text)) {
335346
lastError = new Error('Secondary model returned no usable text');
336347
continue;

src/tools/smartfetch/tool.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import { afterEach, describe, expect, mock, test } from 'bun:test';
22
import { CACHE } from './cache';
33
import { createWebfetchTool } from './tool';
44

5+
let mockV2Client: Record<string, unknown>;
6+
7+
mock.module('../../utils/opencode-client', () => ({
8+
getClient: () => mockV2Client,
9+
}));
10+
511
function createExecutionContext() {
612
return {
713
ask: mock(async () => undefined),
@@ -120,4 +126,55 @@ describe('smartfetch/tool', () => {
120126
'requested_url: "https://example.com/docs#sec2"',
121127
);
122128
});
129+
130+
test('passes ctx.sessionID as parentID to the secondary-model session', async () => {
131+
const fetchMock = mock(async () => {
132+
return new Response(
133+
'Article about smartfetch: it fetches pages, extracts the main ' +
134+
'content, caches results, and summarizes them with a secondary ' +
135+
'model whenever a prompt is provided by the tool caller.',
136+
{ status: 200, headers: { 'content-type': 'text/plain' } },
137+
);
138+
});
139+
globalThis.fetch = fetchMock as unknown as typeof fetch;
140+
141+
const session = {
142+
create: mock(async () => ({ data: { id: 'secondary-session' } })),
143+
prompt: mock(async () => ({
144+
data: { parts: [{ type: 'text', text: 'Extracted answer' }] },
145+
})),
146+
delete: mock(async () => ({ data: true })),
147+
abort: mock(async () => ({ data: true })),
148+
};
149+
const toolIds = { ids: mock(async () => ({ data: ['read'] })) };
150+
mockV2Client = { session, tool: toolIds };
151+
152+
const webfetch = createWebfetchTool({ client: mockV2Client } as any, {
153+
webfetchModels: [{ id: 'provider/small-model' }],
154+
});
155+
const ctx = createExecutionContext();
156+
ctx.sessionID = 'main-session-id';
157+
const result = await webfetch.execute(
158+
{
159+
url: 'https://example.com/article',
160+
format: 'markdown',
161+
extract_main: true,
162+
prefer_llms_txt: 'auto',
163+
include_metadata: false,
164+
save_binary: false,
165+
prompt: 'Extract the answer',
166+
},
167+
ctx,
168+
);
169+
170+
expect(session.create).toHaveBeenCalledWith(
171+
expect.objectContaining({
172+
body: expect.objectContaining({
173+
title: 'smartfetch-secondary',
174+
parentID: 'main-session-id',
175+
}),
176+
}),
177+
);
178+
expect(result).toContain('Extracted answer');
179+
});
123180
});

src/tools/smartfetch/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,7 @@ export function createWebfetchTool(
722722
secondaryModels,
723723
args.prompt || '',
724724
fetchResult.markdown,
725+
ctx.sessionID,
725726
);
726727
} catch (error: unknown) {
727728
secondaryModelError =

0 commit comments

Comments
 (0)