Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,42 @@ describe('search in shared tool activity', () => {
expect(container.innerHTML).not.toContain('--text-error')
})

it('replaces a failed search with the search that followed it', () => {
const retry = completedSearch('two', 'Second query')
render([retry])
expand()
const alone = { text: container.textContent, links: container.querySelectorAll('a').length }
render([{ ...completedSearch('one', 'First query'), status: 'error' }, retry])
expand()
expect({ text: container.textContent, links: container.querySelectorAll('a').length }).toEqual(
alone
)
})

it('keeps a failed search that no later search retried', () => {
const read: ToolCallData = {
id: 'read',
toolName: 'read_document',
displayTitle: 'Reading document',
activityDescription: 'Reading the launch plan',
status: 'success',
}
render([read])
expect(header()).toBeNull()
render([{ ...completedSearch('one', 'First query'), status: 'error' }, read])
expect(header()).not.toBeNull()
expect(container.textContent).not.toMatch(/failed/i)
})

it('keeps the last search visible when every search failed', () => {
render([
{ ...completedSearch('one', 'First query'), status: 'error' },
{ ...completedSearch('two', 'Second query'), status: 'error' },
])
expect(headerText()).toBe('Searching documents')
expect(container.textContent).not.toMatch(/failed/i)
})

it.each([
{ success: false, data: { results: [] } },
{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import { type ComponentType, Fragment, useState } from 'react'
import { ActivityStatus } from '@/components/ui/activity-status'
import type { ToolActivity } from '@/lib/mothership/generated/protocol'
import { CallIntegrationTool, RunCode } from '@/lib/mothership/generated/tool-catalog-v1'
import {
CallIntegrationTool,
RunCode,
SearchWorkspace,
} from '@/lib/mothership/generated/tool-catalog-v1'
import { extractStreamingStringArgument } from '@/lib/mothership/tools/streaming-args'
import {
getToolActivitySummaryActions,
Expand Down Expand Up @@ -31,6 +35,25 @@ function isFailedTool(tool: ToolCallData): boolean {
return tool.status === ToolCallStatus.error || tool.status === ToolCallStatus.rejected
}

/**
* Drops a search that errored when a later search follows it: the model corrected the query
* and searched again, so the failed attempt is not part of what the user reads. The last search
* always stays, so a failure that no search retried, or a run whose searches all failed, still
* shows its outcome.
*/
function withoutRetriedSearchFailures(tools: ToolCallData[]): ToolCallData[] {
let lastSearch = -1
for (const [index, tool] of tools.entries()) {
if (tool.toolName === SearchWorkspace.id) lastSearch = index
}
return tools.filter(
(tool, index) =>
index >= lastSearch ||
tool.toolName !== SearchWorkspace.id ||
tool.status !== ToolCallStatus.error
)
Comment thread
waleedlatif1 marked this conversation as resolved.
}

function toolCountLabel(tools: ToolCallData[]): string {
return `${tools.length} tool ${tools.length === 1 ? 'call' : 'calls'}`
}
Expand Down Expand Up @@ -192,12 +215,13 @@ interface ToolActivityGroupProps {

export function ToolActivityGroup({
activity,
tools,
tools: calls,
ToolCallComponent,
autoScrollActivity = true,
isLive = false,
}: ToolActivityGroupProps) {
const [expanded, setExpanded] = useState(false)
const tools = withoutRetriedSearchFailures(calls)
const statusTool = getActivityStatusTool(tools)
if (!statusTool) return null
const headerTool = getActivityHeaderTool(tools, statusTool)
Expand Down
32 changes: 32 additions & 0 deletions apps/sim/lib/api/contracts/mothership-assistant-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,38 @@ describe('Assistant execution contracts', () => {
const nativeQueries = [{ provider: 'github', query: 'author:@me', kind: 'commits' }]
expect(searchWorkspaceInputSchema.parse({ nativeQueries })).toMatchObject({ query: '' })
expect(searchWorkspaceInputSchema.safeParse({}).success).toBe(false)
})

it('accepts one native query per provider account and kind', () => {
const accepts = (nativeQueries: Record<string, string>[]) =>
searchWorkspaceInputSchema.safeParse({ query: 'launch', nativeQueries }).success
const github = { provider: 'github', accountId: 'account', query: 'repo:org/repo launch' }
expect(
accepts([
{ ...github, kind: 'issues' },
{ ...github, kind: 'commits' },
])
).toBe(true)
expect(
accepts([
{ ...github, kind: 'issues' },
{ ...github, kind: 'issues' },
])
).toBe(false)
expect(accepts([{ ...github, kind: 'issues' }, github])).toBe(false)
expect(
accepts([
{ ...github, kind: 'issues' },
{ ...github, accountId: 'other', kind: 'issues' },
])
).toBe(true)
const gmail = { provider: 'gmail', query: 'subject:launch' }
expect(
accepts([
{ ...gmail, kind: 'issues' },
{ ...gmail, kind: 'code' },
])
).toBe(false)
expect(
searchWorkspaceInputSchema.safeParse({ nativeQueries: [{ provider: 'github', query: '' }] })
.success
Expand Down
32 changes: 26 additions & 6 deletions apps/sim/lib/api/contracts/mothership-assistant-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@ import { LIVE_SEARCH_PROVIDER_IDS } from '@/lib/sim-search/live/provider-catalog
export const liveSearchProviderSchema = z.enum(LIVE_SEARCH_PROVIDER_IDS)
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>

const nativeSearchKindSchema = z.enum([
'issues',
'code',
'repositories',
'commits',
'merge_requests',
'wiki',
])

/** Providers whose `kind` selects a separate search endpoint; others ignore it. */
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab'])

/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
export const nativeSearchQuerySchema = z
.object({
provider: liveSearchProviderSchema,
query: z.string().trim().max(2000),
accountId: z.string().min(1).max(200).optional(),
kind: z
.enum(['issues', 'code', 'repositories', 'commits', 'merge_requests', 'wiki'])
.optional(),
kind: nativeSearchKindSchema.optional(),
project: z.string().min(1).max(300).optional(),
cursor: z.string().max(4000).optional(),
termClauses: z.array(z.string().max(500)).max(10).optional(),
Expand All @@ -27,28 +37,38 @@ export const nativeSearchQueriesSchema = z
.min(1)
.max(9)
.superRefine((queries, context) => {
/**
* Each account runs one query per kind: GitHub and GitLab kinds are separate endpoints, so
* one call can search several of them for the same account. A query without a kind covers
* the provider's default kinds and conflicts with any other query for that account.
*/
const kindOf = (query: NativeSearchQuery) =>
KIND_PROVIDERS.has(query.provider) ? query.kind : undefined
for (const [index, query] of queries.entries()) {
if (
queries
.slice(0, index)
.some(
(previous) =>
previous.provider === query.provider &&
(!previous.accountId || !query.accountId || previous.accountId === query.accountId)
(!previous.accountId || !query.accountId || previous.accountId === query.accountId) &&
(!kindOf(previous) || !kindOf(query) || kindOf(previous) === kindOf(query))
)
)
context.addIssue({
code: 'custom',
path: [index],
message:
'Use one query per provider/account per call; refine in another call or combine native query clauses.',
'Use one query per provider account and kind per call. Combine alternatives with OR in one query, or refine in another call.',
})
}
})

export const liveSearchAccountStatusSchema = z.object({
accountId: z.string(),
provider: liveSearchProviderSchema,
/** The native query kind this status and its cursor belong to. */
kind: nativeSearchKindSchema.optional(),
displayName: z.string(),
status: z.enum(['ok', 'partial', 'reconnect', 'rate_limited', 'unavailable', 'timeout']),
message: z.string().optional(),
Expand Down Expand Up @@ -117,7 +137,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
nativeQueries: nativeSearchQueriesSchema
.optional()
.describe(
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
),
query: z
.string()
Expand Down
32 changes: 26 additions & 6 deletions apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,25 @@ export const liveSearchProviderSchema = z.enum([
])
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>

const nativeSearchKindSchema = z.enum([
'issues',
'code',
'repositories',
'commits',
'merge_requests',
'wiki',
])

/** Providers whose `kind` selects a separate search endpoint; others ignore it. */
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab'])

/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
export const nativeSearchQuerySchema = z
.object({
provider: liveSearchProviderSchema,
query: z.string().trim().max(2000),
accountId: z.string().min(1).max(200).optional(),
kind: z
.enum(['issues', 'code', 'repositories', 'commits', 'merge_requests', 'wiki'])
.optional(),
kind: nativeSearchKindSchema.optional(),
project: z.string().min(1).max(300).optional(),
cursor: z.string().max(4000).optional(),
termClauses: z.array(z.string().max(500)).max(10).optional(),
Expand All @@ -39,28 +49,38 @@ export const nativeSearchQueriesSchema = z
.min(1)
.max(9)
.superRefine((queries, context) => {
/**
* Each account runs one query per kind: GitHub and GitLab kinds are separate endpoints, so
* one call can search several of them for the same account. A query without a kind covers
* the provider's default kinds and conflicts with any other query for that account.
*/
const kindOf = (query: NativeSearchQuery) =>
KIND_PROVIDERS.has(query.provider) ? query.kind : undefined
for (const [index, query] of queries.entries()) {
if (
queries
.slice(0, index)
.some(
(previous) =>
previous.provider === query.provider &&
(!previous.accountId || !query.accountId || previous.accountId === query.accountId)
(!previous.accountId || !query.accountId || previous.accountId === query.accountId) &&
(!kindOf(previous) || !kindOf(query) || kindOf(previous) === kindOf(query))
)
)
context.addIssue({
code: 'custom',
path: [index],
message:
'Use one query per provider/account per call; refine in another call or combine native query clauses.',
'Use one query per provider account and kind per call. Combine alternatives with OR in one query, or refine in another call.',
})
}
})

export const liveSearchAccountStatusSchema = z.object({
accountId: z.string(),
provider: liveSearchProviderSchema,
/** The native query kind this status and its cursor belong to. */
kind: nativeSearchKindSchema.optional(),
displayName: z.string(),
status: z.enum(['ok', 'partial', 'reconnect', 'rate_limited', 'unavailable', 'timeout']),
message: z.string().optional(),
Expand Down Expand Up @@ -129,7 +149,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
nativeQueries: nativeSearchQueriesSchema
.optional()
.describe(
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
),
query: z
.string()
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/mothership/generated/tool-catalog-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6041,7 +6041,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
},
nativeQueries: {
description:
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
minItems: 1,
maxItems: 9,
type: 'array',
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/mothership/generated/tool-schemas-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5972,7 +5972,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
nativeQueries: {
description:
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
minItems: 1,
maxItems: 9,
type: 'array',
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/sim-search/live/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Member mode only. Search uses the connected user's Slack real-time search grant

### GitHub

Member mode searches issues, code, repositories, and commits permitted by the connected token. Commits are searched only with an explicit `commits` kind; they cover each repository's default branch, and a commit read lists up to 30 changed files. Explicit repository/organization/user qualifiers narrow the user's query. Default discovery is bounded to up to 100 affiliated repositories, sent as `repo:` qualifiers in at most four batches per search kind. Code batches stay under code search's 1,000-byte query limit, issue batches are larger, and a long query that cannot fit every repository reports the searched subset. Date bounds become one `updated:start..end` range (`author-date:` for commits), since GitHub ORs repeated qualifiers; a native query that already sets that qualifier keeps its own range, and results are still checked against the date filters. Provider pagination/search caps still apply. REST code search returns no file dates and accepts no date qualifier, so date-filtered searches cover issues and pull requests only.
Member mode searches issues, code, repositories, and commits permitted by the connected token. One call may send one native query per GitHub or GitLab kind for the same account (for example issues and commits); the account opens one session, lists its affiliated repositories once, and reports each kind's status and cursor separately. Commits are searched only with an explicit `commits` kind; they cover each repository's default branch, and a commit read lists up to 30 changed files. Explicit repository/organization/user qualifiers narrow the user's query. Default discovery is bounded to up to 100 affiliated repositories, sent as `repo:` qualifiers in at most four batches per search kind. Code batches stay under code search's 1,000-byte query limit, issue batches are larger, and a long query that cannot fit every repository reports the searched subset. Date bounds become one `updated:start..end` range (`author-date:` for commits), since GitHub ORs repeated qualifiers; a native query that already sets that qualifier keeps its own range, and results are still checked against the date filters. Provider pagination/search caps still apply. REST code search returns no file dates and accepts no date qualifier, so date-filtered searches cover issues and pull requests only.

In service mode, an administrator connects a GitHub App installation and selects repositories one by one in Sources. Each source pins the provider-verified repository ID and may narrow code files by directory and extension. Search queries the member's own GitHub connection with `repo:` qualifiers drawn only from active sources. For each candidate, Sim checks that the current App installation still covers that repository, mints a repository-scoped read token, and compares repository and owner IDs returned under both the App and member tokens. It then checks the per-repository code filters. Reads use the member token and repeat these checks. A personal repository outside the selected sources is never searched, even if the member can access it. GitHub REST code search covers the default branch; live Sources therefore do not offer a branch setting.

Expand Down
12 changes: 10 additions & 2 deletions apps/sim/lib/sim-search/live/account-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { PinnedConnectionPool } from '@/lib/core/security/input-validation.
import type { ResolvedLiveAccount } from '@/lib/sim-search/live/accounts'
import { createCodaMcpClient, readCodaMcp, searchCodaMcp } from '@/lib/sim-search/live/coda-mcp'
import { createAdminGitLabSession } from '@/lib/sim-search/live/gitlab-admin'
import { createNativeClient } from '@/lib/sim-search/live/http'
import { createNativeClient, NATIVE_SEARCH_REQUEST_BUDGET } from '@/lib/sim-search/live/http'
import { createPolicyVerifier } from '@/lib/sim-search/live/policy'
import type { LiveSearchPolicy } from '@/lib/sim-search/live/policy-schema'
import { livePolicyFor, loadLiveSearchPolicies } from '@/lib/sim-search/live/policy-store'
Expand Down Expand Up @@ -40,6 +40,8 @@ interface OpenLiveAccountSessionInput {
policies: Record<string, unknown>
signal: AbortSignal
pool?: PinnedConnectionPool
/** Native searches this session serves; each gets the budget a separate call would have. */
searches?: number
}

/**
Expand All @@ -58,7 +60,13 @@ export async function openLiveAccountSession(
const client =
account.type === 'managed_mcp'
? null
: createNativeClient({ origin, accessToken: resolved.accessToken, signal, pool: input.pool })
: createNativeClient({
origin,
accessToken: resolved.accessToken,
signal,
pool: input.pool,
requestBudget: NATIVE_SEARCH_REQUEST_BUDGET * (input.searches ?? 1),
})
const admin =
'adminSource' in resolved && client
? await createAdminGitLabSession({
Expand Down
Loading
Loading