Skip to content

Commit c9f6ffe

Browse files
committed
fix: allow silent cron empty replies
1 parent 462a056 commit c9f6ffe

4 files changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { beforeEach, describe, expect, it } from "vitest";
2+
import type { SkillSnapshot } from "../../agents/skills.js";
3+
import type { CronJob } from "../types.js";
4+
import {
5+
mockRunCronFallbackPassthrough,
6+
resetRunCronIsolatedAgentTurnHarness,
7+
runEmbeddedPiAgentMock,
8+
} from "./run.test-harness.js";
9+
10+
const { createCronPromptExecutor } = await import("./run-executor.js");
11+
12+
type ExecutorParams = Parameters<typeof createCronPromptExecutor>[0];
13+
type AgentTurnPayload = Extract<CronJob["payload"], { kind: "agentTurn" }>;
14+
15+
const agentPayload: AgentTurnPayload = { kind: "agentTurn", message: "do it" };
16+
17+
const job: CronJob = {
18+
id: "job-1",
19+
name: "silent delivery",
20+
enabled: true,
21+
createdAtMs: 0,
22+
updatedAtMs: 0,
23+
schedule: { kind: "every", everyMs: 60_000 },
24+
sessionTarget: "isolated",
25+
wakeMode: "now",
26+
payload: agentPayload,
27+
delivery: { mode: "none" },
28+
state: {},
29+
};
30+
31+
function makeExecutorParams(
32+
overrides?: Partial<ExecutorParams>,
33+
): ExecutorParams {
34+
return {
35+
cfg: {},
36+
cfgWithAgentDefaults: {},
37+
job,
38+
agentId: "agent-1",
39+
agentDir: "/tmp/agent",
40+
agentSessionKey: "agent:agent-1:main",
41+
runSessionKey: "agent:agent-1:cron:job-1:run:run-1",
42+
workspaceDir: "/tmp/workspace",
43+
lane: "cron",
44+
resolvedVerboseLevel: "off",
45+
thinkLevel: undefined,
46+
timeoutMs: 60_000,
47+
messageChannel: undefined,
48+
suppressExecNotifyOnExit: true,
49+
senderIsOwner: true,
50+
allowEmptyAssistantReplyAsSilent: true,
51+
resolvedDelivery: {},
52+
toolPolicy: {
53+
requireExplicitMessageTarget: false,
54+
disableMessageTool: true,
55+
forceMessageTool: false,
56+
},
57+
skillsSnapshot: { prompt: "", skills: [] } satisfies SkillSnapshot,
58+
agentPayload,
59+
liveSelection: { provider: "openai", model: "gpt-5.5" },
60+
cronSession: {
61+
storePath: "/tmp/store.json",
62+
store: {},
63+
systemSent: false,
64+
isNewSession: true,
65+
previousSessionId: undefined,
66+
sessionEntry: {
67+
sessionId: "run-1",
68+
updatedAt: 0,
69+
systemSent: false,
70+
skillsSnapshot: undefined,
71+
},
72+
},
73+
abortReason: () => "aborted",
74+
...overrides,
75+
};
76+
}
77+
78+
describe("createCronPromptExecutor silent delivery", () => {
79+
beforeEach(() => {
80+
resetRunCronIsolatedAgentTurnHarness();
81+
mockRunCronFallbackPassthrough();
82+
});
83+
84+
it("passes delivery.mode none through as empty assistant silent success", async () => {
85+
const executor = createCronPromptExecutor(makeExecutorParams());
86+
87+
await executor.runPrompt("do it");
88+
89+
const call = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0] as
90+
| { allowEmptyAssistantReplyAsSilent?: boolean }
91+
| undefined;
92+
expect(call?.allowEmptyAssistantReplyAsSilent).toBe(true);
93+
});
94+
});

src/cron/isolated-agent/run-executor.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export function createCronPromptExecutor(params: {
8585
senderIsOwner: boolean;
8686
messageChannel: string | undefined;
8787
suppressExecNotifyOnExit: boolean;
88+
allowEmptyAssistantReplyAsSilent: boolean;
8889
resolvedDelivery: {
8990
accountId?: string;
9091
to?: string;
@@ -244,6 +245,7 @@ export function createCronPromptExecutor(params: {
244245
requireExplicitMessageTarget: params.toolPolicy.requireExplicitMessageTarget,
245246
disableMessageTool: params.toolPolicy.disableMessageTool,
246247
forceMessageTool: params.toolPolicy.forceMessageTool,
248+
allowEmptyAssistantReplyAsSilent: params.allowEmptyAssistantReplyAsSilent,
247249
allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe,
248250
abortSignal: params.abortSignal,
249251
onExecutionStarted: params.onExecutionStarted,
@@ -317,6 +319,7 @@ export async function executeCronRun(params: {
317319
timeoutMs: number;
318320
senderIsOwner: boolean;
319321
suppressExecNotifyOnExit: boolean;
322+
allowEmptyAssistantReplyAsSilent: boolean;
320323
runStartedAt?: number;
321324
}): Promise<CronExecutionResult> {
322325
const resolvedVerboseLevel: VerboseLevel =
@@ -342,6 +345,7 @@ export async function executeCronRun(params: {
342345
timeoutMs: params.timeoutMs,
343346
messageChannel: params.resolvedDelivery.channel,
344347
suppressExecNotifyOnExit: params.suppressExecNotifyOnExit,
348+
allowEmptyAssistantReplyAsSilent: params.allowEmptyAssistantReplyAsSilent,
345349
resolvedDelivery: params.resolvedDelivery,
346350
toolPolicy: params.toolPolicy,
347351
skillsSnapshot: params.skillsSnapshot,

src/cron/isolated-agent/run.message-tool-policy.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => {
337337
abortReason: () => "aborted",
338338
...overrides,
339339
resolvedDelivery,
340+
allowEmptyAssistantReplyAsSilent: overrides.allowEmptyAssistantReplyAsSilent ?? false,
340341
});
341342
}
342343

src/cron/isolated-agent/run.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ type PreparedCronRunContext = {
463463
deliveryRequested: boolean;
464464
suppressExecNotifyOnExit: boolean;
465465
senderIsOwner: boolean;
466+
allowEmptyAssistantReplyAsSilent: boolean;
466467
toolPolicy: ReturnType<typeof resolveCronToolPolicy>;
467468
skillsSnapshot: SkillSnapshot;
468469
liveSelection: CronLiveSelection;
@@ -794,6 +795,7 @@ async function prepareCronRunContext(params: {
794795
deliveryRequested,
795796
suppressExecNotifyOnExit: deliveryPlan.mode === "none",
796797
senderIsOwner: !isExternalHook,
798+
allowEmptyAssistantReplyAsSilent: deliveryPlan.mode === "none",
797799
toolPolicy,
798800
skillsSnapshot,
799801
liveSelection,
@@ -1148,6 +1150,7 @@ export async function runCronIsolatedAgentTurn(params: {
11481150
timeoutMs: prepared.context.timeoutMs,
11491151
suppressExecNotifyOnExit: prepared.context.suppressExecNotifyOnExit,
11501152
senderIsOwner: prepared.context.senderIsOwner,
1153+
allowEmptyAssistantReplyAsSilent: prepared.context.allowEmptyAssistantReplyAsSilent,
11511154
});
11521155
if (isAborted()) {
11531156
return prepared.context.withRunSession({

0 commit comments

Comments
 (0)