Skip to content

Commit 6baa662

Browse files
committed
Merge remote-tracking branch 'origin/master' into feat/improve-actor-tool-output
2 parents ad7b772 + 9fc9094 commit 6baa662

File tree

4 files changed

+202
-110
lines changed

4 files changed

+202
-110
lines changed

src/main.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ if (STANDBY_MODE) {
4444
await Actor.fail('If you need to debug a specific Actor, please provide the debugActor and debugActorInput fields in the input');
4545
}
4646
const options = { memory: input.maxActorMemoryBytes } as ActorCallOptions;
47-
const { previewItems } = await callActorGetDataset(input.debugActor!, input.debugActorInput!, process.env.APIFY_TOKEN, options);
47+
const callResult = await callActorGetDataset(input.debugActor!, input.debugActorInput!, process.env.APIFY_TOKEN, options);
4848

49-
await Actor.pushData(previewItems);
50-
log.info('Pushed items to dataset', { itemCount: previewItems.length });
49+
if (callResult && callResult.previewItems.length > 0) {
50+
await Actor.pushData(callResult.previewItems);
51+
log.info('Pushed items to dataset', { itemCount: callResult.previewItems.length });
52+
}
5153
await Actor.exit();
5254
}
5355

src/mcp/server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,8 +531,15 @@ export class ActorsMcpServer {
531531
apifyToken as string,
532532
callOptions,
533533
progressTracker,
534+
extra.signal,
534535
);
535536

537+
if (!callResult) {
538+
// Receivers of cancellation notifications SHOULD NOT send a response for the cancelled request
539+
// https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements
540+
return { };
541+
}
542+
536543
const content = buildActorResponseContent(actorTool.actorFullName, callResult);
537544
return { content };
538545
} finally {

src/tools/actor.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ export type CallActorGetDatasetResult = {
5050
* @param {unknown} input - The input to pass to the actor.
5151
* @param {string} apifyToken - The Apify token to use for authentication.
5252
* @param {ProgressTracker} progressTracker - Optional progress tracker for real-time updates.
53-
* @returns {Promise<CallActorGetDatasetResult>} - A promise that resolves to metadata about the Actor run and dataset.
53+
* @param {AbortSignal} abortSignal - Optional abort signal to cancel the actor run.
54+
* @returns {Promise<CallActorGetDatasetResult | null>} - A promise that resolves to an object containing the actor run and dataset items.
5455
* @throws {Error} - Throws an error if the `APIFY_TOKEN` is not set
5556
*/
5657
export async function callActorGetDataset(
@@ -59,22 +60,48 @@ export async function callActorGetDataset(
5960
apifyToken: string,
6061
callOptions: ActorCallOptions | undefined = undefined,
6162
progressTracker?: ProgressTracker | null,
62-
): Promise<CallActorGetDatasetResult> {
63+
abortSignal?: AbortSignal,
64+
): Promise<CallActorGetDatasetResult | null> {
65+
const CLIENT_ABORT = Symbol('CLIENT_ABORT'); // Just internal symbol to identify client abort
6366
try {
6467
const client = new ApifyClient({ token: apifyToken });
6568
const actorClient = client.actor(actorName);
6669

67-
// Start the Actor run but don't wait for completion
70+
// Start the actor run
6871
const actorRun: ActorRun = await actorClient.start(input, callOptions);
6972

7073
// Start progress tracking if tracker is provided
7174
if (progressTracker) {
7275
progressTracker.startActorRunUpdates(actorRun.id, apifyToken, actorName);
7376
}
7477

75-
// Wait for the actor to complete
76-
const completedRun = await client.run(actorRun.id).waitForFinish();
78+
// Create abort promise that handles both API abort and race rejection
79+
const abortPromise = async () => new Promise<typeof CLIENT_ABORT>((resolve) => {
80+
abortSignal?.addEventListener('abort', async () => {
81+
// Abort the actor run via API
82+
try {
83+
await client.run(actorRun.id).abort({ gracefully: false });
84+
} catch (e) {
85+
log.error('Error aborting Actor run', { error: e, runId: actorRun.id });
86+
}
87+
// Reject to stop waiting
88+
resolve(CLIENT_ABORT);
89+
}, { once: true });
90+
});
91+
92+
// Wait for completion or cancellation
93+
const potentialAbortedRun = await Promise.race([
94+
client.run(actorRun.id).waitForFinish(),
95+
...(abortSignal ? [abortPromise()] : []),
96+
]);
7797

98+
if (potentialAbortedRun === CLIENT_ABORT) {
99+
log.info('Actor run aborted by client', { actorName, input });
100+
return null;
101+
}
102+
const completedRun = potentialAbortedRun as ActorRun;
103+
104+
// Process the completed run
78105
const dataset = client.dataset(completedRun.defaultDatasetId);
79106
const [datasetItems, defaultBuild] = await Promise.all([
80107
dataset.listItems(),
@@ -309,7 +336,7 @@ The step parameter enforces this workflow - you cannot call an Actor without fir
309336
inputSchema: zodToJsonSchema(callActorArgs),
310337
ajvValidate: ajv.compile(zodToJsonSchema(callActorArgs)),
311338
call: async (toolArgs) => {
312-
const { args, apifyToken, progressTracker } = toolArgs;
339+
const { args, apifyToken, progressTracker, extra } = toolArgs;
313340
const { actor: actorName, step, input, callOptions } = callActorArgs.parse(args);
314341

315342
try {
@@ -364,8 +391,15 @@ The step parameter enforces this workflow - you cannot call an Actor without fir
364391
apifyToken,
365392
callOptions,
366393
progressTracker,
394+
extra.signal,
367395
);
368396

397+
if (!callResult) {
398+
// Receivers of cancellation notifications SHOULD NOT send a response for the cancelled request
399+
// https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements
400+
return { };
401+
}
402+
369403
const content = buildActorResponseContent(actorName, callResult);
370404

371405
return { content };

0 commit comments

Comments
 (0)