Skip to content

Commit 698fc44

Browse files
jirispilkaclaude
andauthored
fix: Serve task and schedule instructions only with their tools (#1579)
## Why Closes #1576. `getServerInstructions()` always rendered `## Actor tasks` and `## Schedules`, so sessions without any task or schedule tool were told about them, including "the create tool here enables them". ## What changed Server instructions contract change. The render with every tool served is byte-identical to master (default and apps mode). - `## Actor tasks` renders when any of the five task tools is served; `## Schedules` when any of the four schedule tools is. - Inside them: - "Creating or updating a task does not make it public." needs `create-actor-task` or `update-actor-task`. - The two publishing bullets need `publish-actor-task`. - The schedule creation sentence needs `create-schedule`. - "send every action to keep" needs `update-schedule`. - The cron advice "so use an explicit expression when the user names a time" needs `create-schedule` or `update-schedule`. - The other bullets are facts about tasks and schedules and stay whenever the section renders. Default sessions (`actors`, `docs`) and the Claude connector `?tools=` set now get neither section. With only `get-schedule` and `get-actor-task`: ``` ## Actor tasks - An Actor task is a saved, reusable configuration of an Actor. It stores the Actor input and run options such as the build, memory, and timeout. - Tasks are useful for repeated or scheduled jobs, because the user does not have to configure the Actor again for every run. ## Schedules - A schedule runs Actors and/or saved tasks automatically on a cron expression, evaluated in the schedule's time zone (default UTC). - A cron expression has 5 fields (minute hour day-of-month month day-of-week), with an optional leading seconds field. The shortcuts @hourly, @daily, @Weekly, @monthly and @Yearly fire at a random offset inside their period. - Disabling a schedule pauses it without losing its configuration. - With isExclusive (the default), a run is skipped while the previous run from the same action is still running. - A schedule holds at most 10 Actor actions and 10 task actions. ``` ## Notes for reviewers (human-written) ## Proof it works mcpc against a local stdio build (0.18.3 + this branch): - Claude connector tool set (`--tools` with the 23 selectors, `--ui apps`): instruction headings are `Actor`, `Actor discovery and selection`, `Actor execution workflow`, `Storage types`, `Apify API resources`, `Tool dependencies and disambiguation`. No `## Actor tasks`, no `## Schedules`, no `MUST`. - `--tools get-schedule,get-actor-task`: both sections, exactly as quoted above. The new unit tests failed on master (10 failures). A review script rendered all 512 subsets of the nine task and schedule tools in both modes, with and without the other tools: no tripled newlines, a blank line before every heading, text outside the two sections identical to master. AI-assisted: Claude Code wrote the change, tests and this description, and ran the mcpc probe. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01CPzYK17FoL2F3cuVonqnej --- _Generated by [Claude Code](https://claude.ai/code/session_01CPzYK17FoL2F3cuVonqnej)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 62f30e6 commit 698fc44

2 files changed

Lines changed: 137 additions & 28 deletions

File tree

‎src/utils/server-instructions/index.ts‎

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,46 @@ export function getServerInstructions(
189189
190190
${renderedBlocks.map((block, index) => (index > 0 && block.startsWith('###') ? `\n${block}` : block)).join('')}`;
191191

192+
const hasTaskCreate = hasTool(HELPER_TOOLS.ACTOR_TASK_CREATE);
193+
const hasTaskUpdate = hasTool(HELPER_TOOLS.ACTOR_TASK_UPDATE);
194+
const hasTaskPublish = hasTool(HELPER_TOOLS.ACTOR_TASK_PUBLISH);
195+
const taskPrivacyLine =
196+
hasTaskCreate || hasTaskUpdate ? '- Creating or updating a task does not make it public.\n' : '';
197+
const taskPublishLines = hasTaskPublish
198+
? `- Publishing a task creates a public landing page for one specific use case. The page shows what the task does, the selected input values, and the expected output. Published tasks appear in the Actor's Examples tab, where users, search engines, and AI agents can discover them, which can help people understand the Actor and increase its runs.
199+
- Publish only tasks that represent a useful, reliable, and specific use case. Not every saved task needs to be public.
200+
`
201+
: '';
202+
const actorTasksSection =
203+
hasTaskCreate ||
204+
hasTaskUpdate ||
205+
hasTaskPublish ||
206+
hasTool(HELPER_TOOLS.ACTOR_TASK_GET) ||
207+
hasTool(HELPER_TOOLS.ACTOR_TASK_UNPUBLISH)
208+
? `
209+
## Actor tasks
210+
- An Actor task is a saved, reusable configuration of an Actor. It stores the Actor input and run options such as the build, memory, and timeout.
211+
- Tasks are useful for repeated or scheduled jobs, because the user does not have to configure the Actor again for every run.
212+
${taskPrivacyLine}${taskPublishLines}`
213+
: '';
214+
215+
const hasScheduleCreate = hasTool(HELPER_TOOLS.SCHEDULE_CREATE);
216+
const hasScheduleUpdate = hasTool(HELPER_TOOLS.SCHEDULE_UPDATE);
217+
const schedulesSection =
218+
hasScheduleCreate ||
219+
hasScheduleUpdate ||
220+
hasTool(HELPER_TOOLS.SCHEDULE_GET) ||
221+
hasTool(HELPER_TOOLS.SCHEDULE_DELETE)
222+
? `
223+
## Schedules
224+
- A schedule runs Actors and/or saved tasks automatically on a cron expression, evaluated in the schedule's time zone (default UTC).
225+
- A cron expression has 5 fields (minute hour day-of-month month day-of-week), with an optional leading seconds field. The shortcuts @hourly, @daily, @weekly, @monthly and @yearly fire at a random offset inside their period${hasScheduleCreate || hasScheduleUpdate ? ', so use an explicit expression when the user names a time' : ''}.
226+
- ${hasScheduleCreate ? 'The platform creates schedules disabled by default, but the create tool here enables them unless told otherwise. ' : ''}Disabling a schedule pauses it without losing its configuration.
227+
- With isExclusive (the default), a run is skipped while the previous run from the same action is still running.
228+
${hasScheduleUpdate ? "- Updating a schedule's actions replaces the whole list, so send every action to keep.\n" : ''}- A schedule holds at most 10 Actor actions and 10 task actions.
229+
`
230+
: '';
231+
192232
return `
193233
Apify is the world's largest marketplace of tools for web scraping, data extraction, and web automation.
194234
These tools are called **Actors**. They enable you to extract structured data from social media, e-commerce, search engines, maps, travel sites, and many other sources.
@@ -210,22 +250,7 @@ ${runLimitationLine}- When multiple options exist, prefer Actors with higher usa
210250
- Every Actor run generates **dataset** and **key-value store** outputs (even if empty).
211251
- Actor execution may take time, and outputs can be large.
212252
- Large datasets can be paginated to retrieve results efficiently.
213-
214-
## Actor tasks
215-
- An Actor task is a saved, reusable configuration of an Actor. It stores the Actor input and run options such as the build, memory, and timeout.
216-
- Tasks are useful for repeated or scheduled jobs, because the user does not have to configure the Actor again for every run.
217-
- Creating or updating a task does not make it public.
218-
- Publishing a task creates a public landing page for one specific use case. The page shows what the task does, the selected input values, and the expected output. Published tasks appear in the Actor's Examples tab, where users, search engines, and AI agents can discover them, which can help people understand the Actor and increase its runs.
219-
- Publish only tasks that represent a useful, reliable, and specific use case. Not every saved task needs to be public.
220-
221-
## Schedules
222-
- A schedule runs Actors and/or saved tasks automatically on a cron expression, evaluated in the schedule's time zone (default UTC).
223-
- A cron expression has 5 fields (minute hour day-of-month month day-of-week), with an optional leading seconds field. The shortcuts @hourly, @daily, @weekly, @monthly and @yearly fire at a random offset inside their period, so use an explicit expression when the user names a time.
224-
- The platform creates schedules disabled by default, but the create tool here enables them unless told otherwise. Disabling a schedule pauses it without losing its configuration.
225-
- With isExclusive (the default), a run is skipped while the previous run from the same action is still running.
226-
- Updating a schedule's actions replaces the whole list, so send every action to keep.
227-
- A schedule holds at most 10 Actor actions and 10 task actions.
228-
253+
${actorTasksSection}${schedulesSection}
229254
## Storage types
230255
- **Dataset:** Structured, append-only storage ideal for tabular or list data (e.g., scraped items).
231256
- **Key-value store:** Flexible storage for unstructured data or auxiliary files.

‎tests/unit/server-instructions.test.ts‎

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -211,24 +211,101 @@ describe('getServerInstructions()', () => {
211211
});
212212
});
213213

214-
describe('"## Schedules" section', () => {
215-
const instructions = getServerInstructions(SERVER_MODE.DEFAULT, ALL_TOOLS_PRESENT);
214+
/** The `## <heading>` block, up to the next `## ` heading; '' when the heading is absent. */
215+
const sectionOf = (instructions: string, heading: string): string => {
216+
const start = instructions.indexOf(heading);
217+
if (start === -1) return '';
218+
const end = instructions.indexOf('\n## ', start + 1);
219+
return instructions.slice(start, end === -1 ? undefined : end);
220+
};
221+
222+
it.each([SERVER_MODE.DEFAULT, SERVER_MODE.APPS])(
223+
'preserves all-tools task and schedule text in %s mode',
224+
(mode) => {
225+
const instructions = getServerInstructions(mode, ALL_TOOLS_PRESENT);
226+
const sections = instructions.slice(
227+
instructions.indexOf('## Actor tasks'),
228+
instructions.indexOf('\n## Storage types'),
229+
);
230+
expect(sections).toBe(
231+
[
232+
'## Actor tasks',
233+
'- An Actor task is a saved, reusable configuration of an Actor. It stores the Actor input and run options such as the build, memory, and timeout.',
234+
'- Tasks are useful for repeated or scheduled jobs, because the user does not have to configure the Actor again for every run.',
235+
'- Creating or updating a task does not make it public.',
236+
"- Publishing a task creates a public landing page for one specific use case. The page shows what the task does, the selected input values, and the expected output. Published tasks appear in the Actor's Examples tab, where users, search engines, and AI agents can discover them, which can help people understand the Actor and increase its runs.",
237+
'- Publish only tasks that represent a useful, reliable, and specific use case. Not every saved task needs to be public.',
238+
'',
239+
'## Schedules',
240+
"- A schedule runs Actors and/or saved tasks automatically on a cron expression, evaluated in the schedule's time zone (default UTC).",
241+
'- A cron expression has 5 fields (minute hour day-of-month month day-of-week), with an optional leading seconds field. The shortcuts @hourly, @daily, @weekly, @monthly and @yearly fire at a random offset inside their period, so use an explicit expression when the user names a time.',
242+
'- The platform creates schedules disabled by default, but the create tool here enables them unless told otherwise. Disabling a schedule pauses it without losing its configuration.',
243+
'- With isExclusive (the default), a run is skipped while the previous run from the same action is still running.',
244+
"- Updating a schedule's actions replaces the whole list, so send every action to keep.",
245+
'- A schedule holds at most 10 Actor actions and 10 task actions.',
246+
'',
247+
].join('\n'),
248+
);
249+
},
250+
);
251+
252+
it.each(['## Actor tasks', '## Schedules'])('omits %s when its tools are absent', (heading) => {
253+
expect(getServerInstructions(SERVER_MODE.DEFAULT, only(HELPER_TOOLS.STORE_SEARCH))).not.toContain(heading);
254+
});
216255

217-
it('renders for ALL_TOOLS_PRESENT', () => {
218-
expect(instructions).toContain('## Schedules');
256+
describe('"## Actor tasks" section', () => {
257+
it.each([
258+
[HELPER_TOOLS.ACTOR_TASK_CREATE, true, false],
259+
[HELPER_TOOLS.ACTOR_TASK_GET, false, false],
260+
[HELPER_TOOLS.ACTOR_TASK_UPDATE, true, false],
261+
[HELPER_TOOLS.ACTOR_TASK_PUBLISH, false, true],
262+
[HELPER_TOOLS.ACTOR_TASK_UNPUBLISH, false, false],
263+
] as const)('gates task guidance with %s alone', (toolName, hasPrivacy, hasPublish) => {
264+
const section = sectionOf(getServerInstructions(SERVER_MODE.DEFAULT, only(toolName)), '## Actor tasks');
265+
expect(section).toContain('- An Actor task is a saved, reusable configuration of an Actor.');
266+
expect(section).toContain('Tasks are useful for repeated or scheduled jobs');
267+
expect(section.includes('Creating or updating a task')).toBe(hasPrivacy);
268+
expect(section.includes('Publishing a task')).toBe(hasPublish);
269+
expect(section.includes('Publish only tasks')).toBe(hasPublish);
219270
});
271+
});
220272

221-
// The section is static and ungated, so a tool name slipping in would reach sessions without that tool.
222-
it('names no tool', () => {
223-
const start = instructions.indexOf('## Schedules');
224-
const end = instructions.indexOf('\n## ', start + 1);
225-
const block = instructions.slice(start, end === -1 ? undefined : end);
226-
for (const toolName of Object.values(HELPER_TOOLS)) {
227-
expect(block).not.toContain(toolName);
228-
}
273+
describe('"## Schedules" section', () => {
274+
it.each([
275+
[HELPER_TOOLS.SCHEDULE_CREATE, true, false, true],
276+
[HELPER_TOOLS.SCHEDULE_GET, false, false, false],
277+
[HELPER_TOOLS.SCHEDULE_UPDATE, false, true, true],
278+
[HELPER_TOOLS.SCHEDULE_DELETE, false, false, false],
279+
] as const)('gates schedule guidance with %s alone', (toolName, hasCreate, hasUpdate, hasCronAdvice) => {
280+
const section = sectionOf(getServerInstructions(SERVER_MODE.DEFAULT, only(toolName)), '## Schedules');
281+
expect(section).toContain('- A schedule runs Actors and/or saved tasks automatically on a cron expression');
282+
expect(section).toContain('Disabling a schedule pauses it without losing its configuration.');
283+
expect(section).toContain('- A schedule holds at most 10 Actor actions and 10 task actions.');
284+
expect(section.includes('the create tool here')).toBe(hasCreate);
285+
expect(section.includes("Updating a schedule's actions")).toBe(hasUpdate);
286+
expect(section.includes('so use an explicit expression')).toBe(hasCronAdvice);
229287
});
230288
});
231289

290+
// Group-level gating must not expose names of individual tools that may be absent.
291+
it.each(['## Actor tasks', '## Schedules'])('names no tool in %s', (heading) => {
292+
const block = sectionOf(getServerInstructions(SERVER_MODE.DEFAULT, ALL_TOOLS_PRESENT), heading);
293+
expect(block).not.toBe('');
294+
for (const toolName of Object.values(HELPER_TOOLS)) {
295+
expect(block).not.toContain(toolName);
296+
}
297+
});
298+
299+
it('separates the read-only task and schedule sections with blank lines', () => {
300+
const instructions = getServerInstructions(
301+
SERVER_MODE.DEFAULT,
302+
only(HELPER_TOOLS.ACTOR_TASK_GET, HELPER_TOOLS.SCHEDULE_GET),
303+
);
304+
for (const heading of ['## Actor tasks', '## Schedules', '## Storage types']) {
305+
expect(instructions).toContain(`\n\n${heading}\n`);
306+
}
307+
});
308+
232309
describe('"## Apify API resources" section', () => {
233310
const API_READ_LINE =
234311
`- Use \`${HELPER_TOOLS.API_READ}\` for API GETs. If your client can read resources, \`resources/read\` ` +
@@ -379,4 +456,11 @@ describe('Claude-connector tool surface (no call-actor)', () => {
379456
expect(instructions).not.toContain(HELPER_TOOLS.ACTOR_CALL);
380457
expect(instructions).not.toContain('MUST');
381458
});
459+
460+
it('omits the task and schedule sections', () => {
461+
const resolved = resolveToolNamesFromInput(parseInputParamsFromUrl(listedUrl), SERVER_MODE.APPS);
462+
const instructions = getServerInstructions(SERVER_MODE.APPS, { hasTool: (name) => resolved.has(name) });
463+
expect(instructions).not.toContain('## Actor tasks');
464+
expect(instructions).not.toContain('## Schedules');
465+
});
382466
});

0 commit comments

Comments
 (0)