diff --git a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts index da230777a..d029ea47e 100644 --- a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts +++ b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts @@ -358,6 +358,68 @@ describe('buildFastAgentSystemPrompt', () => { ); }); + it('gates ongoing-process offers on an unresolved outcome and verification capability', () => { + const prompt = buildFastAgentSystemPrompt({ availableEnvironments: [] }); + + expect(prompt).toContain( + 'ongoing process has a concrete unresolved outcome', + ); + expect(prompt).toContain('Verify capability before offering'); + expect(prompt).toContain( + 'if unavailable or uncertain, do not promise monitoring', + ); + expect(prompt).toContain('outcome, evidence source, timing and stop bound'); + expect(prompt).toContain('with confirmed deployment and telemetry access'); + expect(prompt).toContain( + 'Never imply a release or process started or completed without evidence', + ); + }); + + it('requires consent for offers but preserves direct explicit monitoring requests', () => { + const prompt = buildFastAgentSystemPrompt({ availableEnvironments: [] }); + + expect(prompt).toContain('create no wakeup until the user accepts'); + expect(prompt).toContain( + 'Explicit user monitoring requests already authorize scheduling', + ); + expect(prompt).toContain('do not require another opt-in'); + expect(prompt).toContain('revalidate capability and list active wakeups'); + expect(prompt).toContain('finite schedule and stop condition'); + expect(prompt).toContain('without automatic renewal'); + expect(prompt).toContain('Missing evidence is not success'); + expect(prompt).toContain( + 'Use "manage_wakeups" when the user wants a reminder', + ); + }); + + it('keeps monitoring quiet, nonduplicative and subject to platform-event restrictions', () => { + const prompt = buildFastAgentSystemPrompt({ + availableEnvironments: [], + turnSource: 'platform_event', + platformEventKind: 'scheduled_wakeup', + }); + + expect(prompt).toContain('"only_when_notable" for monitoring'); + expect(prompt).toContain('stay quiet on unchanged results'); + expect(prompt).toContain( + 'duplicate existing task, PR lifecycle/review, or other notifications and monitors', + ); + expect(prompt).toContain( + 'Offer at most once for the same unresolved outcome', + ); + expect(prompt).toContain('do not repeat an ignored or declined offer'); + expect(prompt).toContain( + 'Do not make proactive offers on automation or scheduled-wakeup turns', + ); + expect(prompt).toContain( + 'Presentation-only events remain presentation-only', + ); + expect(prompt).toContain('do not inspect or schedule from them'); + expect(prompt).toContain( + 'not an offer to save work as a deployment automation', + ); + }); + it('lists on-demand servers by name with their tool names instead of mounting them', () => { const prompt = buildFastAgentSystemPrompt({ availableEnvironments: [], diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts index 19fc00595..e6544e2aa 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts @@ -337,6 +337,11 @@ ${reactionGuidance} - Answer directly from conversation context when it is reliable. Use deployment MCP servers as relevant sources of truth, and delegate repository or workspace work when inspection, editing, execution, or validation is required. - Ask for clarification only when ambiguity blocks meaningful investigation, materially different plausible outcomes remain, or the next action is destructive, irreversible, or externally consequential. Otherwise inspect what is available and proceed. +## Ongoing Process Follow-Up +- Proactively offer a specific bounded check only when an ongoing process has a concrete unresolved outcome worth verifying later and available tools can actually verify it. Verify capability before offering; if unavailable or uncertain, do not promise monitoring. Name the outcome, evidence source, timing and stop bound in one short consent question, not a generic "I can monitor this" footer. For example, with confirmed deployment and telemetry access: "Want me to check this deployment's error rate in 30 minutes?" Never imply a release or process started or completed without evidence. +- An offer is not authorization: create no wakeup until the user accepts. Explicit user monitoring requests already authorize scheduling; do not require another opt-in. Before scheduling, revalidate capability and list active wakeups to reuse an equivalent check. Store the specific target, evidence source, finite schedule and stop condition; use "only_when_notable" for monitoring, stay quiet on unchanged results, and stop on resolution, irrelevance, capability loss or the agreed bound without automatic renewal. Missing evidence is not success. +- Do not offer or schedule checks that duplicate existing task, PR lifecycle/review, or other notifications and monitors. Offer at most once for the same unresolved outcome; do not repeat an ignored or declined offer or append boilerplate after every fix or update. Do not make proactive offers on automation or scheduled-wakeup turns. Presentation-only events remain presentation-only: do not inspect or schedule from them. This is conversation-scoped follow-up, not an offer to save work as a deployment automation. + ## Orchestration Policy - User-supplied corrections, status updates, acknowledgements, and opinions are conversation state, not requests for external verification. Do not launch a task or call an integration merely to re-check user-supplied facts unless the user asks for verification. If the message actually requires repository or workspace inspection, execution, change, or validation, delegate it under the rules below. - Use "launch_task" for new independent repository or workspace work when external inspection, editing, execution, or validation is required, regardless of whether the message is phrased as a question, request, or declarative feedback. Existing active tasks do not block a new independent task. diff --git a/packages/types/src/session-wakeups.test.ts b/packages/types/src/session-wakeups.test.ts index 040a84c50..a41c5b784 100644 --- a/packages/types/src/session-wakeups.test.ts +++ b/packages/types/src/session-wakeups.test.ts @@ -65,6 +65,33 @@ describe('manage wakeups tool contract', () => { ); }); + it('requires finite ongoing-process monitoring in both tool and schedule guidance', () => { + const description = MANAGE_WAKEUPS_TOOL.description; + const schedule = MANAGE_WAKEUPS_TOOL.inputSchema.schedule.description; + + expect(description).toContain( + 'Ongoing-process monitors must use a one-shot or a recurring schedule with "x" or "until "', + ); + expect(description).toContain('"every 10m x12"'); + expect(description).toContain('"cron 0 9 * * 1-5 x5"'); + expect(description).toContain( + 'Stop monitoring at the agreed finite bound without automatic renewal', + ); + expect(description).toContain( + 'Reaching the bound without evidence of resolution is not success', + ); + expect(description).not.toContain('A monitor keeps running until'); + expect(description).not.toContain('optionally with'); + expect(schedule).toContain( + 'always include "x" or "until " on recurring schedules, regardless of cadence', + ); + for (const guidance of [description, schedule]) { + expect(guidance).toContain( + 'explicitly requested recurring reminders or reports, not ongoing-process monitors', + ); + } + }); + it('takes the schedule as one string and nothing else schedule-shaped', () => { expect(Object.keys(MANAGE_WAKEUPS_TOOL.inputSchema).sort()).toEqual([ 'action', diff --git a/packages/types/src/session-wakeups.ts b/packages/types/src/session-wakeups.ts index bb9b58339..e1b1e6f1e 100644 --- a/packages/types/src/session-wakeups.ts +++ b/packages/types/src/session-wakeups.ts @@ -102,7 +102,8 @@ export const SESSION_WAKEUP_SCHEDULE_GRAMMAR = `One of: - "in s|m|h|d" for a one-shot delay with a positive whole number, e.g. "in 30s", "in 2m", "in 90m", "in 3h" (preferred for reminders and delayed follow-ups; use "in 30s", not "in 0.5m") - "at " for a one-shot at an absolute time, e.g. "at 2026-09-04T15:00:00-04:00" - "every s|m|h|d" for a repeating interval with a positive whole number, e.g. "every 10m", "every 6h"; add "x" to stop after that many runs ("every 30s x3") or "until " to stop after a time ("every 10m until 2026-09-04T18:00:00Z"); intervals under 5 minutes require one of these bounds -- "cron [IANA timezone]" for a calendar schedule, e.g. "cron 0 9 * * 1-5 America/New_York" (timezone defaults to the deployment timezone); "x" and "until " work here too, and a cron that fires more often than every 5 minutes requires one of them`; +- "cron [IANA timezone]" for a calendar schedule, e.g. "cron 0 9 * * 1-5 America/New_York" (timezone defaults to the deployment timezone); "x" and "until " work here too, and a cron that fires more often than every 5 minutes requires one of them +For ongoing-process monitoring, always include "x" or "until " on recurring schedules, regardless of cadence. Unbounded schedules remain available for explicitly requested recurring reminders or reports, not ongoing-process monitors.`; export const MANAGE_WAKEUPS_ACTIONS = [ 'create', @@ -165,9 +166,9 @@ export const MANAGE_WAKEUPS_TOOL_NAME = 'manage_wakeups' as const; export const MANAGE_WAKEUPS_TOOL_DESCRIPTION = `Schedule this conversation to wake itself up later, once or on a cadence. When a wakeup fires, you receive a scheduled_wakeup platform event in this same conversation with the full history still in context, so the prompt can be brief and refer to things discussed here. Use it for reminders ("remind me in 20 minutes", "ping me at 3pm") and for monitors ("check every 10 minutes whether CI is green", "every weekday at 9am summarize open PRs"). -The schedule is one short string with positive whole-number s/m/h/d durations. Reminders and delayed follow-ups use "in 30s" or "in 20m", not fractional units such as "in 0.5m"; use "at " only for an explicit absolute time. Repeating checks use "every 30s x3", "every 10m" or "cron ...", optionally with "x" or "until ". Pick an interval that matches how fast the monitored thing actually changes, not how soon you want an answer; intervals under ${SESSION_WAKEUP_UNCAPPED_MIN_INTERVAL_MINUTES} minutes need "x" or "until". +The schedule is one short string with positive whole-number s/m/h/d durations. Reminders and delayed follow-ups use "in 30s" or "in 20m", not fractional units such as "in 0.5m"; use "at " only for an explicit absolute time. Ongoing-process monitors must use a one-shot or a recurring schedule with "x" or "until ", such as "every 10m x12" or "cron 0 9 * * 1-5 x5". Pick an interval that matches how fast the monitored thing actually changes, not how soon you want an answer. Unbounded schedules are for explicitly requested recurring reminders or reports, not ongoing-process monitors; intervals under ${SESSION_WAKEUP_UNCAPPED_MIN_INTERVAL_MINUTES} minutes always need "x" or "until". -- A monitor keeps running until the user cancels it or the condition definitively resolves. A run that finds nothing new is still useful. When a monitored condition resolves, tell the user and cancel the wakeup. +- Stop monitoring at the agreed finite bound without automatic renewal, or earlier on resolution, irrelevance, capability loss or user cancellation. A run that finds nothing new stays quiet under "only_when_notable". When a monitored condition resolves, tell the user and cancel the wakeup; a finished wakeup needs no cancel. Reaching the bound without evidence of resolution is not success. - Results arrive automatically as a new turn in this conversation. Delivery is best effort, not an exact-time guarantee. Never poll, sleep, or wait for a wakeup inside a turn. - When the user says stop, cancel, remove, delete, or end a wakeup, use cancel. There is no pause. - Creating a wakeup that matches an active one (same prompt and schedule) returns the existing wakeup instead of a duplicate. At most ${MAX_ACTIVE_SESSION_WAKEUPS} wakeups may be active per conversation.