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
4 changes: 2 additions & 2 deletions src/tools/actors/call_actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export function buildCallActorErrorResponse(params: CallActorErrorResponseParams
return respondServerError(
[
`Failed to call Actor '${actorName}': ${errMsg}`,
`Account memory quota exceeded for your plan. Retry with a smaller callOptions.memory, or wait for current runs to finish before retrying.`,
`Account memory quota exceeded for your plan. A smaller callOptions.memory may fit the remaining quota; memory is freed when current runs finish.`,
],
{ error, detail: APIFY_ERROR_TYPE_MEMORY_LIMIT_EXCEEDED, actorId },
);
Expand Down Expand Up @@ -283,7 +283,7 @@ export const callOptionsSchema = z.object({
.max(32768, 'Memory cannot exceed 32 GB (32768 MB)')
.optional().describe(dedent`
Memory per run in MB. Power of 2 from 128 to 32768.
Apify also caps total memory across all your concurrent runs (account plan limit); if a run is rejected because that quota would be exceeded, retry with a smaller value.
Apify also caps total memory across all your concurrent runs (account plan limit); a run that would exceed that quota is rejected.
`),
timeout: z.number().min(0, 'Timeout must be 0 or greater').optional().describe(dedent`
Maximum runtime for the Actor in seconds. After this time elapses, the Actor will be automatically terminated.
Expand Down
4 changes: 2 additions & 2 deletions src/tools/actors/delete_actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ export const deleteActor: ToolEntry = Object.freeze({
const more = unfinishedRunCount > unfinishedRuns.items.length ? ', and more' : '';
return respondUserError(
`${fullName} has ${unfinishedRunCount} unfinished ${unfinishedRunCount === 1 ? 'run' : 'runs'} ` +
`(${runIds}${more}), and deleting it aborts them. Ask the user whether to abort them, and if ` +
'so call again with abortRunningRuns set to true; otherwise wait until they finish.',
`(${runIds}${more}), and deleting it aborts them. Ask the user whether to abort them. ` +
'abortRunningRuns=true aborts them before deleting; otherwise the Actor can be deleted once they finish.',
);
}
await client.actor(actor.id).delete();
Expand Down
10 changes: 5 additions & 5 deletions src/utils/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,16 +282,16 @@ export function computeToolResponseBytes(result: unknown): {
}

/**
* Actionable hint for an HTTP failure status, or `undefined` when the status carries no specific
* remedy. Shared by the tool-call and resources/read error paths so both differentiate auth, rate-limit
* Hint for an HTTP failure status (auth, access, rate limit, server failure), or `undefined` when the
* status has none. Shared by the tool-call and resources/read error paths so both differentiate auth, rate-limit
* and server failures the same way (the model's only lever is the text it gets back).
*/
export function getHttpErrorHint(status: number | undefined): string | undefined {
if (status === 403) return 'The resource may be private or your token may lack access.';
if (status === 401) return 'Authentication failed, check APIFY_TOKEN is set and valid.';
if (status === 429) return 'Rate limit exceeded, wait before retrying.';
if (status === 429) return 'The Apify API rate limit was reached.';
if (status !== undefined && status >= 500 && status < 600) {
return 'The server failed to process the request, likely a temporary issue. Retry later.';
return 'The Apify API failed to process the request; this is usually temporary.';
}
return undefined;
}
Expand All @@ -312,7 +312,7 @@ export function getToolCallErrorUserText(toolName: string, error: unknown): stri
return (
`Error calling tool "${toolName}": ${msg}.\n\n` +
'The API validates supplied `publicConfig` fields on create and update, and all publication ' +
'requirements on publish. Fix the stated requirement before retrying. If `datasetView` is invalid, ' +
'requirements on publish. The error message names the requirement that is not met. If `datasetView` is invalid, ' +
'ask the user for its key; no tool can list dataset views.'
);
}
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/resources.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ describe('readApiResource()', () => {

expect(error).toBeInstanceOf(InternalError);
expect(error.message).toContain(`Failed to read ${uri}: HTTP 500: Internal Server Error`);
expect(error.message).toContain('Retry later');
expect(error.message).toContain('this is usually temporary');
});

it('falls back to statusText when a non-2xx error body itself crosses the limit', async () => {
Expand Down Expand Up @@ -499,7 +499,7 @@ describe('readApiResource()', () => {

expect(error).toBeInstanceOf(InternalError);
expect(error.message).toContain('HTTP 429: Too many requests');
expect(error.message).toContain('Rate limit exceeded');
expect(error.message).toContain('rate limit was reached');
});

it('appends the private-resource hint for a 403 response', async () => {
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/tool_call_error_mapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ describe('buildToolCallErrorResult()', () => {
expect('userText' in result && result.userText).toContain(
`Error calling tool "${HELPER_TOOLS.ACTOR_TASK_PUBLISH}":`,
);
expect('userText' in result && result.userText).toContain(
'all publication requirements on publish. The error message names the requirement that is not met.',
);
expect('userText' in result && result.userText).not.toMatch(/retry/i);
});

it('returns ABORTED toolStatus for an aborted execution error', () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/tools.apify_api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ describe('apify-api-write', () => {
it.each([
{ statusCode: 300, hint: '' },
{ statusCode: 400, hint: '' },
{ statusCode: 429, hint: ' Rate limit exceeded, wait before retrying.' },
{ statusCode: 429, hint: ' The Apify API rate limit was reached.' },
])(
'reports a failed write whose error body is over the inline limit with its status $statusCode',
async ({ statusCode, hint }) => {
Expand Down
17 changes: 15 additions & 2 deletions tests/unit/tools.call_actor_common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
buildCallActorDescription,
buildCallActorErrorResponse,
callActorArgs,
callOptionsSchema,
handleMcpToolCall,
resolveAndValidateActor,
} from '../../src/tools/actors/call_actor.js';
Expand Down Expand Up @@ -260,8 +261,11 @@ describe('call_actor_common', () => {
expect(response.isError).toBe(true);
const allText = (response.content ?? []).map(textOf).join('\n');
expect(allText).toContain('memory limit of 8192MB');
expect(allText).toContain('Account memory quota exceeded');
expect(allText).toContain('callOptions.memory');
expect(allText).toContain(
'Account memory quota exceeded for your plan. A smaller callOptions.memory may fit the remaining ' +
'quota; memory is freed when current runs finish.',
);
expect(allText).not.toMatch(/retry/i);
// Regression: must not nudge the LLM toward aborting unrelated runs to free capacity.
expect(allText).not.toContain(HELPER_TOOLS.ACTOR_RUNS_ABORT);
expect(allText).not.toContain('verify the Actor name');
Expand Down Expand Up @@ -401,6 +405,15 @@ describe('call_actor_common', () => {
});
expect(result.success).toBe(false);
});

it('states the account memory quota without asking for a retry', () => {
const { description } = callOptionsSchema.shape.memory;
expect(description).toContain(
'Apify also caps total memory across all your concurrent runs (account plan limit); a run that ' +
'would exceed that quota is rejected.',
);
expect(description).not.toMatch(/retry/i);
});
});

describe('resolveAndValidateActor', () => {
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/tools.delete_actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ describe('delete-actor', () => {
});
expect(text).toBe(
'john/my-actor has 2 unfinished runs (run-1, run-2), and deleting it aborts them. Ask the user whether ' +
'to abort them, and if so call again with abortRunningRuns set to true; otherwise wait until they finish.',
'to abort them. abortRunningRuns=true aborts them before deleting; otherwise the Actor ' +
'can be deleted once they finish.',
);
expect(actorDeleteMock).not.toHaveBeenCalled();
});
Expand Down
14 changes: 8 additions & 6 deletions tests/unit/utils.mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,14 @@ describe('getHttpErrorHint()', () => {
});

it('returns the rate-limit hint for 429', () => {
expect(getHttpErrorHint(429)).toContain('Rate limit exceeded');
expect(getHttpErrorHint(429)).toBe('The Apify API rate limit was reached.');
});

it('returns the server-error hint for 5xx', () => {
for (const status of [500, 503, 599]) {
expect(getHttpErrorHint(status)).toContain('server failed');
expect(getHttpErrorHint(status)).toBe(
'The Apify API failed to process the request; this is usually temporary.',
);
}
});

Expand Down Expand Up @@ -270,14 +272,14 @@ describe('getToolCallErrorUserText()', () => {
it('replaces the input hint with the rate-limit hint for 429', () => {
const error = Object.assign(new Error('Too Many Requests'), { statusCode: 429 });
const text = getToolCallErrorUserText('apify/instagram-scraper', error);
expect(text).toContain('Rate limit exceeded');
expect(text).toContain('rate limit was reached');
expect(text).not.toContain('Verify the tool name');
});

it('replaces the input hint with the server-error hint for 5xx', () => {
const error = Object.assign(new Error('Service Unavailable'), { statusCode: 503 });
const text = getToolCallErrorUserText('apify/instagram-scraper', error);
expect(text).toContain('server failed');
expect(text).toContain('failed to process the request');
expect(text).not.toContain('Verify the tool name');
});

Expand All @@ -293,8 +295,8 @@ describe('getToolCallErrorUserText()', () => {
it.each([
[401, 'Authentication failed, check APIFY_TOKEN is set and valid.'],
[403, 'The resource may be private or your token may lack access.'],
[429, 'Rate limit exceeded, wait before retrying.'],
[500, 'The server failed to process the request, likely a temporary issue. Retry later.'],
[429, 'The Apify API rate limit was reached.'],
[500, 'The Apify API failed to process the request; this is usually temporary.'],
])('keeps the HTTP %i hint', (statusCode, hint) => {
const error = Object.assign(new Error('Request failed'), { statusCode });
const text = getToolCallErrorUserText('get-actor-run', error);
Expand Down
Loading