Skip to content

Commit 0e6753e

Browse files
committed
fix(search): budget batched kinds like separate searches and hide only retried search failures
1 parent b75c2ee commit 0e6753e

6 files changed

Lines changed: 71 additions & 10 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity.test.tsx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,21 @@ describe('search in shared tool activity', () => {
236236
)
237237
})
238238

239+
it('keeps a failed search that no later search retried', () => {
240+
const read: ToolCallData = {
241+
id: 'read',
242+
toolName: 'read_document',
243+
displayTitle: 'Reading document',
244+
activityDescription: 'Reading the launch plan',
245+
status: 'success',
246+
}
247+
render([read])
248+
expect(header()).toBeNull()
249+
render([{ ...completedSearch('one', 'First query'), status: 'error' }, read])
250+
expect(header()).not.toBeNull()
251+
expect(container.textContent).not.toMatch(/failed/i)
252+
})
253+
239254
it('keeps the last search visible when every search failed', () => {
240255
render([
241256
{ ...completedSearch('one', 'First query'), status: 'error' },

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,19 @@ function isFailedTool(tool: ToolCallData): boolean {
3636
}
3737

3838
/**
39-
* Drops a search that errored when another call follows it: the model corrected the query
40-
* and moved on, so the failed attempt is not part of what the user reads.
41-
* The last call always stays, so a run whose searches all failed still shows its outcome.
39+
* Drops a search that errored when a later search follows it: the model corrected the query
40+
* and searched again, so the failed attempt is not part of what the user reads. The last search
41+
* always stays, so a failure that no search retried, or a run whose searches all failed, still
42+
* shows its outcome.
4243
*/
43-
function withoutRecoveredSearchFailures(tools: ToolCallData[]): ToolCallData[] {
44+
function withoutRetriedSearchFailures(tools: ToolCallData[]): ToolCallData[] {
45+
let lastSearch = -1
46+
for (const [index, tool] of tools.entries()) {
47+
if (tool.toolName === SearchWorkspace.id) lastSearch = index
48+
}
4449
return tools.filter(
4550
(tool, index) =>
46-
index === tools.length - 1 ||
51+
index >= lastSearch ||
4752
tool.toolName !== SearchWorkspace.id ||
4853
tool.status !== ToolCallStatus.error
4954
)
@@ -216,7 +221,7 @@ export function ToolActivityGroup({
216221
isLive = false,
217222
}: ToolActivityGroupProps) {
218223
const [expanded, setExpanded] = useState(false)
219-
const tools = withoutRecoveredSearchFailures(calls)
224+
const tools = withoutRetriedSearchFailures(calls)
220225
const statusTool = getActivityStatusTool(tools)
221226
if (!statusTool) return null
222227
const headerTool = getActivityHeaderTool(tools, statusTool)

‎apps/sim/lib/sim-search/live/account-session.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { PinnedConnectionPool } from '@/lib/core/security/input-validation.
44
import type { ResolvedLiveAccount } from '@/lib/sim-search/live/accounts'
55
import { createCodaMcpClient, readCodaMcp, searchCodaMcp } from '@/lib/sim-search/live/coda-mcp'
66
import { createAdminGitLabSession } from '@/lib/sim-search/live/gitlab-admin'
7-
import { createNativeClient } from '@/lib/sim-search/live/http'
7+
import { createNativeClient, NATIVE_SEARCH_REQUEST_BUDGET } from '@/lib/sim-search/live/http'
88
import { createPolicyVerifier } from '@/lib/sim-search/live/policy'
99
import type { LiveSearchPolicy } from '@/lib/sim-search/live/policy-schema'
1010
import { livePolicyFor, loadLiveSearchPolicies } from '@/lib/sim-search/live/policy-store'
@@ -40,6 +40,8 @@ interface OpenLiveAccountSessionInput {
4040
policies: Record<string, unknown>
4141
signal: AbortSignal
4242
pool?: PinnedConnectionPool
43+
/** Native searches this session serves; each gets the budget a separate call would have. */
44+
searches?: number
4345
}
4446

4547
/**
@@ -58,7 +60,13 @@ export async function openLiveAccountSession(
5860
const client =
5961
account.type === 'managed_mcp'
6062
? null
61-
: createNativeClient({ origin, accessToken: resolved.accessToken, signal, pool: input.pool })
63+
: createNativeClient({
64+
origin,
65+
accessToken: resolved.accessToken,
66+
signal,
67+
pool: input.pool,
68+
requestBudget: NATIVE_SEARCH_REQUEST_BUDGET * (input.searches ?? 1),
69+
})
6270
const admin =
6371
'adminSource' in resolved && client
6472
? await createAdminGitLabSession({

‎apps/sim/lib/sim-search/live/application.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,15 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
474474
resolveListedLiveAccount(input, userId, account)
475475
)
476476
const session = await measureSearchStage('live.session', () =>
477-
openLiveAccountSession({ owner: input, userId, resolved, policies, signal, pool })
477+
openLiveAccountSession({
478+
owner: input,
479+
userId,
480+
resolved,
481+
policies,
482+
signal,
483+
pool,
484+
searches: natives.length,
485+
})
478486
)
479487
return await Promise.all(
480488
natives.map((native) =>

‎apps/sim/lib/sim-search/live/http.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,26 @@ describe('native search network boundary', () => {
3030
})
3131
)
3232
})
33+
it('stops at its request budget, which defaults to one search', async () => {
34+
mocks.fetch.mockImplementation(async () => new Response('{}', { status: 200 }))
35+
const clientWith = (requestBudget?: number) =>
36+
createNativeClient({
37+
origin: 'https://api.github.com',
38+
accessToken: 'private',
39+
signal: new AbortController().signal,
40+
requestBudget,
41+
})
42+
const exhaust = async (client: ReturnType<typeof clientWith>, requests: number) => {
43+
for (let request = 0; request < requests; request++) await client.json('/user')
44+
}
45+
await exhaust(clientWith(), 30)
46+
const single = clientWith()
47+
await exhaust(single, 30)
48+
await expect(single.json('/user')).rejects.toThrow('Request budget reached')
49+
const shared = clientWith(60)
50+
await exhaust(shared, 60)
51+
await expect(shared.json('/user')).rejects.toThrow('Request budget reached')
52+
})
3353
it('does not send a token to an absolute or protocol-relative model URL', async () => {
3454
const client = createNativeClient({
3555
origin: 'https://api.github.com',

‎apps/sim/lib/sim-search/live/http.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ export class NativeSearchError extends Error {
1515
}
1616
}
1717

18+
/** Provider requests one native search may make, including discovery and verification. */
19+
export const NATIVE_SEARCH_REQUEST_BUDGET = 30
20+
1821
/** Tokens only go to a code-selected provider origin; redirects never carry credentials. */
1922
export function createNativeClient(input: {
2023
origin: string
2124
accessToken: string
2225
signal: AbortSignal
2326
/** Reuses connections across this client's requests; the caller owns its lifetime. */
2427
pool?: PinnedConnectionPool
28+
/** Requests this client may make; defaults to one search's budget. */
29+
requestBudget?: number
2530
}): NativeClient {
2631
let requests = 0
2732
async function request(
@@ -33,7 +38,7 @@ export function createNativeClient(input: {
3338
}
3439
) {
3540
input.signal.throwIfAborted()
36-
if (++requests > 30)
41+
if (++requests > (input.requestBudget ?? NATIVE_SEARCH_REQUEST_BUDGET))
3742
throw new NativeSearchError('unavailable', 'Request budget reached. Narrow the query.')
3843
const url = new URL(input.origin)
3944
if (options?.googleService === 'sheets') {

0 commit comments

Comments
 (0)