fix(server): steer scheduled system prompts

Finish notifications and agent-target schedules now use the same steering-first delivery path as user prompts. Scheduling still decides whether a busy target should be prompted before delivery begins.
This commit is contained in:
Mohamed Boudra
2026-08-18 14:36:27 +02:00
parent ce9026f264
commit 05ee7a1d31
4 changed files with 81 additions and 14 deletions
@@ -53,6 +53,7 @@ interface FinishNotificationScenario {
finishChildAndReadParentPrompt(): Promise<string>;
closeChildAndReadParentPrompt(): Promise<string>;
parentPrompts(): string[];
steerAttemptCount(): number;
wasParentPrompted(): boolean;
}
@@ -62,6 +63,7 @@ function createFinishNotificationScenario(
let subscriber: ((event: AgentManagerEvent) => void) | null = null;
let resolveParentPrompt: ((prompt: string) => void) | null = null;
let parentPrompted = false;
let steerAttemptCount = 0;
const parentPrompts: string[] = [];
const childAgent: ManagedAgent = Object.create(null);
@@ -96,6 +98,10 @@ function createFinishNotificationScenario(
});
Reflect.set(agentManager, "tryRunOutOfBand", () => false);
Reflect.set(agentManager, "hasInFlightRun", () => Boolean(options?.parentPromptError));
Reflect.set(agentManager, "steerOrReplaceActiveTurn", async () => {
steerAttemptCount += 1;
return { status: "inactive" };
});
Reflect.set(agentManager, "streamAgent", (_agentId: string, prompt: string) => {
parentPrompted = true;
parentPrompts.push(prompt);
@@ -233,6 +239,9 @@ function createFinishNotificationScenario(
parentPrompts() {
return parentPrompts;
},
steerAttemptCount() {
return steerAttemptCount;
},
wasParentPrompted() {
return parentPrompted;
},
@@ -257,6 +266,7 @@ test("finish notifications tell the parent the child's last assistant message",
"Agent child-agent (Child Agent) finished.\n\n<agent-response>\nImplemented the cleanup and all checks pass.\n</agent-response>",
),
);
expect(scenario.steerAttemptCount()).toBe(1);
});
test("finish notifications truncate oversized child responses", async () => {
@@ -402,6 +402,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
agentStorage,
agentId: callerAgentId,
prompt: formatSystemNotificationPrompt(body),
activeTurnBehavior: "steer",
unarchive: false,
logger,
});
@@ -463,6 +463,40 @@ describe("ScheduleService", () => {
);
});
test("delivers agent-target schedules through the steer-or-interrupt path", async () => {
const manager = new AgentManager({
logger: createTestLogger(),
clients: createTestAgentClients(),
registry: agentStorage,
});
const agent = await manager.createAgent({ provider: "claude", cwd: tempDir }, undefined, {
workspaceId: undefined,
});
const steerOrReplace = vi.spyOn(manager, "steerOrReplaceActiveTurn");
const service = createScheduleService({
paseoHome: tempDir,
logger: createTestLogger(),
agentManager: manager,
agentStorage,
providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY,
now: () => now,
});
const schedule = await service.create({
prompt: "Check scheduled work",
cadence: { type: "every", everyMs: 60_000 },
target: { type: "agent", agentId: agent.id },
});
await service.runOnce(schedule.id);
expect(steerOrReplace).toHaveBeenCalledTimes(1);
expect(steerOrReplace.mock.calls[0]).toEqual([
agent.id,
expect.stringContaining(`Schedule fired (id=${schedule.id}, run=`),
undefined,
]);
});
test("titles scheduled new agents from the schedule prompt", async () => {
const manager = new AgentManager({
logger: createTestLogger(),
+29 -7
View File
@@ -7,7 +7,11 @@ import type { AgentSessionConfig } from "../agent/agent-sdk-types.js";
import type { AgentStorage } from "../agent/agent-storage.js";
import { curateAgentActivity } from "../agent/activity-curator.js";
import { ensureAgentLoaded } from "../agent/agent-loading.js";
import { formatSystemNotificationPrompt } from "../agent/agent-prompt.js";
import {
formatSystemNotificationPrompt,
startAgentRun,
type AgentRunController,
} from "../agent/agent-prompt.js";
import { resolveCreateAgentTitles } from "../agent/create-agent-title.js";
import { type BoundCreateAgentCommand, formatProviderModel } from "../agent/create-agent/create.js";
import type { PersistedWorkspaceRecord } from "../workspace-registry.js";
@@ -196,11 +200,18 @@ function buildRunOutput(params: {
}
type ScheduleAgentManager = Pick<
AgentRunController,
| "getAgent"
| "tryRunOutOfBand"
| "hasInFlightRun"
| "replaceAgentRun"
| "steerOrReplaceActiveTurn"
| "streamAgent"
> &
Pick<
AgentManager,
| "createAgent"
| "getAgent"
| "getRegisteredProviderIds"
| "hasInFlightRun"
| "hydrateTimelineFromProvider"
| "resumeAgentFromPersistence"
| "runAgent"
@@ -843,14 +854,25 @@ export class ScheduleService {
if (this.agentManager.hasInFlightRun(agent.id)) {
throw new Error(`Agent ${agent.id} already has an active run`);
}
const result = await this.agentManager.runAgent(agent.id, wrappedPrompt);
const timelineText = curateAgentActivity(result.timeline);
await startAgentRun(this.agentManager, agent.id, wrappedPrompt, this.logger, {
replaceRunning: true,
activeTurnBehavior: "steer",
});
const waitResult = await this.agentManager.waitForAgentEvent(agent.id, {
waitForActive: true,
});
if (waitResult.permission) {
throw new Error(`Scheduled agent ${agent.id} is waiting for permission`);
}
if (waitResult.status === "error") {
throw new Error(waitResult.lastMessage ?? `Scheduled agent ${agent.id} failed`);
}
return {
agentId: agent.id,
output: buildRunOutput({
output: null,
timelineText,
finalText: result.finalText,
timelineText: "",
finalText: waitResult.lastMessage ?? "",
}),
};
}