Skip to content

Commit 151d53d

Browse files
mr-danyacodex
andcommitted
chore: merge main and resolve test import conflict
Preserve package-selection test imports alongside upstream tool failure telemetry tests and the latest issue-details replay limits. Co-Authored-By: GPT-6 (Codex) <noreply@openai.com>
2 parents 2a7311a + 858d972 commit 151d53d

29 files changed

Lines changed: 814 additions & 106 deletions

‎docs/specs/ai-conversations.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ Each result should include:
220220
- AI call count.
221221
- Tool call count and tool error count.
222222
- Tool names.
223-
- Total tokens and total cost.
223+
- Total tokens and a display-formatted USD total cost. Use `—` for zero, `<$0.01` for positive sub-cent costs, and compact currency formatting otherwise.
224224
- Trace count and a small sample of trace IDs.
225225

226226
Search and detail structured outputs use Unix millisecond timestamps for

‎packages/mcp-core/src/api-client/client.ts‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from "zod";
22
import { DEFAULT_SEARCH_ISSUES_PERIOD } from "../constants";
33
import { ConfigurationError } from "../errors";
4+
import { retryWithBackoff } from "../internal/fetch-utils";
45
import { logIssue, logWarn } from "../telem/logging";
56
import type { SentryProtocol } from "../types";
67
import {
@@ -30,7 +31,6 @@ import {
3031
isSentryHost,
3132
type TraceMetricIdentifier,
3233
} from "../utils/url-utils";
33-
import { retryWithBackoff } from "../internal/fetch-utils";
3434
import { USER_AGENT } from "../version";
3535
import { apiPath } from "./api-path";
3636
import {
@@ -70,6 +70,7 @@ import {
7070
MonitorListSchema,
7171
MonitorSchema,
7272
MonitorStatsSchema,
73+
OrganizationEnvironmentListSchema,
7374
OrganizationListSchema,
7475
OrganizationSchema,
7576
ProfileChunkResponseSchema,
@@ -130,6 +131,7 @@ import type {
130131
MonitorCheckInList,
131132
MonitorList,
132133
MonitorStats,
134+
OrganizationEnvironmentList,
133135
OrganizationList,
134136
ProfileChunk,
135137
Project,
@@ -3416,6 +3418,37 @@ export class SentryApiService {
34163418
return EventsValidationResponseSchema.parse(body);
34173419
}
34183420

3421+
/**
3422+
* List the organization's visible environments, optionally scoped to a
3423+
* project. The endpoint (`GET /organizations/{org}/environments/`) already
3424+
* filters to visible environments and excludes the empty-name "No Environment"
3425+
* pseudo-env; passing `project` narrows the list (verified against
3426+
* getsentry/sentry `OrganizationEnvironmentsEndpoint`).
3427+
*/
3428+
async listEnvironments(
3429+
{
3430+
organizationSlug,
3431+
projectId,
3432+
}: {
3433+
organizationSlug: string;
3434+
projectId?: string;
3435+
},
3436+
opts?: RequestOptions,
3437+
): Promise<OrganizationEnvironmentList> {
3438+
const queryParams = new URLSearchParams();
3439+
if (projectId) {
3440+
queryParams.set("project", projectId);
3441+
}
3442+
const suffix = queryParams.toString();
3443+
const body = await this.requestJSON(
3444+
apiPath`/organizations/${organizationSlug}/environments/` +
3445+
(suffix ? `?${suffix}` : ""),
3446+
undefined,
3447+
opts,
3448+
);
3449+
return OrganizationEnvironmentListSchema.parse(body);
3450+
}
3451+
34193452
private async fetchTraceItemAttributes(
34203453
organizationSlug: string,
34213454
itemType: TraceItemType,
@@ -4109,7 +4142,7 @@ export class SentryApiService {
41094142
organizationSlug: string;
41104143
issueId: string;
41114144
status?: string;
4112-
assignedTo?: string;
4145+
assignedTo?: string | null;
41134146
substatus?: string;
41144147
ignoreDuration?: number;
41154148
ignoreCount?: number;
@@ -4130,7 +4163,7 @@ export class SentryApiService {
41304163
ignoreUserWindow?: number;
41314164
} = {};
41324165
if (status !== undefined) updateData.status = status;
4133-
if (assignedTo !== undefined) updateData.assignedTo = assignedTo;
4166+
if (assignedTo !== undefined) updateData.assignedTo = assignedTo ?? "";
41344167
if (substatus !== undefined) updateData.substatus = substatus;
41354168
if (ignoreDuration !== undefined)
41364169
updateData.ignoreDuration = ignoreDuration;

‎packages/mcp-core/src/api-client/schema.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,17 @@ export const EventAttachmentSchema = z.object({
13501350

13511351
export const EventAttachmentListSchema = z.array(EventAttachmentSchema);
13521352

1353+
// GET /organizations/{org}/environments/ — visible environments (the endpoint
1354+
// excludes the empty-name "No Environment" and hidden environments by default).
1355+
export const OrganizationEnvironmentSchema = z.object({
1356+
id: z.string(),
1357+
name: z.string(),
1358+
});
1359+
1360+
export const OrganizationEnvironmentListSchema = z.array(
1361+
OrganizationEnvironmentSchema,
1362+
);
1363+
13531364
/**
13541365
* Schema for individual tag values within an issue's tag distribution.
13551366
*

‎packages/mcp-core/src/api-client/types.ts‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ import type { z } from "zod";
4242
import type {
4343
AgenticOnboardingRunSchema,
4444
AgenticOnboardingStatusUpdateSchema,
45-
AssignedToSchema,
46-
AIConversationSummaryListSchema,
47-
AIConversationSummarySchema,
4845
AIConversationDetailsResponseSchema,
4946
AIConversationSpanListSchema,
5047
AIConversationSpanSchema,
48+
AIConversationSummaryListSchema,
49+
AIConversationSummarySchema,
5150
AIConversationUserSchema,
51+
AssignedToSchema,
5252
AutofixRunSchema,
5353
AutofixRunStateSchema,
5454
ClientKeyListSchema,
@@ -82,18 +82,15 @@ import type {
8282
IssueListSchema,
8383
IssueSchema,
8484
IssueTagValuesSchema,
85+
MetricAlertRuleListSchema,
86+
MetricAlertRuleSchema,
8587
MonitorCheckInListSchema,
8688
MonitorCheckInSchema,
8789
MonitorListSchema,
8890
MonitorSchema,
89-
MonitorStatsSchema,
9091
MonitorStatSchema,
91-
UptimeCheckListSchema,
92-
UptimeCheckSchema,
93-
UptimeMonitorListSchema,
94-
UptimeMonitorSchema,
95-
MetricAlertRuleListSchema,
96-
MetricAlertRuleSchema,
92+
MonitorStatsSchema,
93+
OrganizationEnvironmentListSchema,
9794
OrganizationListSchema,
9895
OrganizationSchema,
9996
ProfileChunkResponseSchema,
@@ -123,8 +120,12 @@ import type {
123120
TransactionProfileSampleSchema,
124121
TransactionProfileSchema,
125122
UnknownEventSchema,
126-
UserSchema,
123+
UptimeCheckListSchema,
124+
UptimeCheckSchema,
125+
UptimeMonitorListSchema,
126+
UptimeMonitorSchema,
127127
UserReportListSchema,
128+
UserSchema,
128129
} from "./schema";
129130

130131
export type AgenticOnboardingRun = z.infer<typeof AgenticOnboardingRunSchema>;
@@ -202,6 +203,9 @@ export type MonitorStats = z.infer<typeof MonitorStatsSchema>;
202203
export type UptimeMonitorList = z.infer<typeof UptimeMonitorListSchema>;
203204
export type UptimeCheckList = z.infer<typeof UptimeCheckListSchema>;
204205
export type EventAttachmentList = z.infer<typeof EventAttachmentListSchema>;
206+
export type OrganizationEnvironmentList = z.infer<
207+
typeof OrganizationEnvironmentListSchema
208+
>;
205209
export type TagList = z.infer<typeof TagListSchema>;
206210
export type ClientKeyList = z.infer<typeof ClientKeyListSchema>;
207211

‎packages/mcp-core/src/internal/agents/callEmbeddedAgent.test.ts‎

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
LLMProviderError,
1616
UserInputError,
1717
} from "../../errors";
18-
import { logIssue } from "../../telem/logging";
18+
import { logIssue, logWarn } from "../../telem/logging";
1919
import { getAgentProvider } from "./provider-factory";
2020

2121
vi.mock("../../telem/logging", () => ({
@@ -243,7 +243,8 @@ describe("callEmbeddedAgent", () => {
243243

244244
it("throws LLMProviderError for provider budget/quota failures", async () => {
245245
const budgetError = new APICallError({
246-
message: "Workspace monthly budget of $15000.00 exceeded. Contact your org admin.",
246+
message:
247+
"Workspace monthly budget of $15000.00 exceeded. Contact your org admin.",
247248
url: "https://openrouter.ai/api/v1/chat/completions",
248249
requestBodyValues: {},
249250
statusCode: 402,
@@ -370,49 +371,32 @@ describe("callEmbeddedAgent", () => {
370371
);
371372
});
372373

373-
it("converts NoOutputGeneratedError into AgentExecutionError after filing a Sentry issue", async () => {
374+
it("treats NoOutputGeneratedError as a recoverable UserInputError without filing a Sentry issue", async () => {
374375
const noOutputError = new NoOutputGeneratedError({
375376
message: "No output generated.",
376377
});
377378

378379
mockGenerateText.mockRejectedValue(noOutputError);
379380

381+
// Recoverable model limitation (usually repeated tool/validation failures),
382+
// not a system fault — surface as user input and log a warning, not an issue.
380383
await expect(
381384
callEmbeddedAgent({
382385
system: "You are a test agent",
383386
prompt: "Test prompt",
384387
tools: {},
385388
schema: testSchema,
386389
}),
387-
).rejects.toBeInstanceOf(AgentExecutionError);
388-
389-
await expect(
390-
callEmbeddedAgent({
391-
system: "You are a test agent",
392-
prompt: "Test prompt",
393-
tools: {},
394-
schema: testSchema,
395-
}),
396-
).rejects.toMatchObject({
397-
message: expect.stringContaining("No output generated."),
398-
eventId: "mock-event-id",
399-
cause: noOutputError,
400-
});
390+
).rejects.toBeInstanceOf(UserInputError);
401391

402-
expect(logIssue).toHaveBeenCalledWith(
403-
noOutputError,
404-
expect.objectContaining({
405-
loggerScope: ["agents", "embedded"],
406-
contexts: {
407-
embeddedAgent: expect.objectContaining({
408-
isNoOutputGenerated: true,
409-
}),
410-
},
411-
}),
392+
expect(logIssue).not.toHaveBeenCalled();
393+
expect(logWarn).toHaveBeenCalledWith(
394+
"Embedded agent produced no output",
395+
expect.objectContaining({ loggerScope: ["agents", "embedded"] }),
412396
);
413397
});
414398

415-
it("treats missing experimental_output as NoOutputGeneratedError", async () => {
399+
it("treats missing experimental_output as a recoverable UserInputError", async () => {
416400
mockGenerateText.mockResolvedValue({
417401
experimental_output: undefined,
418402
} as never);
@@ -424,17 +408,9 @@ describe("callEmbeddedAgent", () => {
424408
tools: {},
425409
schema: testSchema,
426410
}),
427-
).rejects.toBeInstanceOf(AgentExecutionError);
411+
).rejects.toBeInstanceOf(UserInputError);
428412

429-
expect(logIssue).toHaveBeenCalledWith(
430-
expect.objectContaining({
431-
name: "AI_NoOutputGeneratedError",
432-
message: "No output generated.",
433-
}),
434-
expect.objectContaining({
435-
loggerScope: ["agents", "embedded"],
436-
}),
437-
);
413+
expect(logIssue).not.toHaveBeenCalled();
438414
});
439415

440416
it("rethrows ConfigurationError from getProviderOptions without filing an issue", async () => {

‎packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import {
2-
generateText,
3-
Output,
4-
type Tool,
52
APICallError,
3+
generateText,
64
NoObjectGeneratedError,
75
NoOutputGeneratedError,
6+
Output,
87
RetryError,
98
stepCountIs,
9+
type Tool,
1010
} from "ai";
11-
import { getAgentProvider } from "./provider-factory";
11+
import type { z } from "zod";
1212
import {
1313
AgentExecutionError,
1414
ConfigurationError,
15-
UserInputError,
1615
LLMProviderError,
16+
UserInputError,
1717
} from "../../errors";
1818
import { logIssue, logWarn } from "../../telem/logging";
19-
import type { z } from "zod";
19+
import { getAgentProvider } from "./provider-factory";
2020

2121
/**
2222
* Resolve the underlying provider failure from an AI SDK error.
@@ -210,6 +210,21 @@ export async function callEmbeddedAgent<
210210
);
211211
}
212212

213+
// NoOutputGeneratedError: the model exhausted its steps without emitting the
214+
// structured output (typically after repeated tool/validation failures). This
215+
// is a recoverable model limitation, not a system fault — surface it as user
216+
// input like its NoObjectGeneratedError sibling and log a warning, instead of
217+
// filing a Sentry issue for every occurrence.
218+
if (NoOutputGeneratedError.isInstance(error)) {
219+
logWarn("Embedded agent produced no output", {
220+
loggerScope: ["agents", "embedded"],
221+
extra: { errorMessage: error.message },
222+
});
223+
throw new UserInputError(
224+
"The AI could not construct a valid query for this request. Please rephrase or narrow it — for example, specify the fields, a real environment name, or a time range.",
225+
);
226+
}
227+
213228
// Handle LLM provider errors with user-friendly messages.
214229
// These are operational availability failures that should NOT create Sentry
215230
// issues per request (budget exhaustion, rate limits, provider outages).
@@ -242,9 +257,9 @@ export async function callEmbeddedAgent<
242257
throw error;
243258
}
244259

245-
// Unexpected agent failures (including NoOutputGeneratedError): file one
246-
// Sentry issue, then throw a typed error so AI-powered tools can fall back
247-
// or return a graceful response instead of hard-failing the MCP tool.
260+
// Genuinely unexpected agent failures: file one Sentry issue, then throw a
261+
// typed error so AI-powered tools can fall back or return a graceful response
262+
// instead of hard-failing the MCP tool.
248263
throw toAgentExecutionError(error);
249264
}
250265
}

‎packages/mcp-core/src/internal/error-handling.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
} from "../api-client";
1313
import { logIssue, logWarn } from "../telem/logging";
1414
import { APICallError, NoObjectGeneratedError, RetryError } from "ai";
15-
import type { TransportType } from "../types";
15+
import type { ToolConfig } from "../tools/types";
16+
import type { ServerContext, TransportType } from "../types";
1617

1718
/**
1819
* Type guard to identify user input validation errors.
@@ -337,3 +338,19 @@ export async function formatErrorForUser(
337338
}
338339
return parts.join("\n\n");
339340
}
341+
342+
/**
343+
* Invoke a tool's onError hook for failure telemetry, isolated so a misbehaving
344+
* hook can't disrupt the surrounding error handling. Call this at every point
345+
* where a tool handler is run and may throw.
346+
*/
347+
export function recordToolFailure(
348+
tool: Pick<ToolConfig, "onError">,
349+
error: unknown,
350+
params: Record<string, unknown>,
351+
context: ServerContext,
352+
): void {
353+
try {
354+
tool.onError?.(error, params, context);
355+
} catch {}
356+
}

‎packages/mcp-core/src/schema.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,10 @@ export const ParamIssueIgnoreMode = z
185185
export const ParamAssignedTo = z
186186
.string()
187187
.trim()
188+
.min(1)
189+
.nullable()
188190
.describe(
189-
"The assignee in format 'user:ID' or 'team:ID_OR_SLUG' where ID is numeric. Example: 'user:123456', 'team:789', or 'team:my-team-slug'. Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID.",
191+
"The assignee in format 'user:ID' or 'team:ID_OR_SLUG' where ID is numeric. Pass null to unassign the issue. Example: 'user:123456', 'team:789', or 'team:my-team-slug'. Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID.",
190192
);
191193

192194
export const ParamIgnoreDurationMinutes = z

0 commit comments

Comments
 (0)