fix(cron): publish agent response to outbound bus for cron-triggered jobs#2100
fix(cron): publish agent response to outbound bus for cron-triggered jobs#2100yinwm merged 5 commits intosipeed:mainfrom
Conversation
…jobs When a cron job triggers agent execution via ProcessDirectWithChannel, the agent response was silently discarded — the code assumed AgentLoop would auto-publish it, but SendResponse is false on this path. Delegate to PublishResponseIfNeeded (exported from AgentLoop) so the response reaches the originating channel (e.g. Telegram) only when the message tool did not already deliver content in the same round. Also adds a "directive" message type to CronPayload, allowing cron jobs to instruct the agent to execute a task rather than echo static text.
yinwm
left a comment
There was a problem hiding this comment.
Thanks for this well-structured fix — this is the cleanest approach among the five PRs addressing the same cron response bug. The PublishResponseIfNeeded delegation with the HasSentInRound() guard is the right pattern to prevent duplicate messages. The stubJobExecutor test design is also solid.
Two blocking issues before we can merge:
1. type parameter lacks server-side validation
The enum in Parameters() is only an LLM schema hint — any string value gets persisted. Please add a whitelist check in addJob:
msgType, _ := args["type"].(string)
if msgType != "" && msgType != "message" && msgType != "directive" {
return ErrorResult(fmt.Sprintf("invalid type %q, must be 'message' or 'directive'", msgType))
}2. Missing tests for the new directive feature
The core branch logic in ExecuteJob changed (deliver=true && !isDirective), but no test covers the directive path. Please add at least:
- Test that
type=directiveadds the prompt prefix toProcessDirectWithChannel - Test that
deliver=true+type=directiveroutes through the agent (not direct publish) - Test that the directive prompt content is passed correctly
Non-blocking suggestions (happy to see these in a follow-up):
- Merge the two
UpdateJobcalls inaddJobinto one to avoid redundant I/O - Consider whether
PublishResponseIfNeededshould be internal toProcessDirectWithChannelrather than exposed on the interface — the caller shouldn't need to know about the message-tool dedup internals - The
omitemptyonCronPayload.Typecreates asymmetric JSON output (empty string omitted,"message"serialized) — consider a consistent default
One note: PR #1839 raises an additional concern about Peer not being set on the inbound message in ProcessDirectWithChannel, which causes peer-based bindings to never match for cron jobs. That's a separate bug worth tracking.
Looking forward to v2!
Address reviewer blocking feedback:
1. Server-side whitelist for `type` parameter — the `enum` in
Parameters() is only an LLM schema hint; any string was persisted.
Now `addJob` rejects values other than "message" and "directive".
2. Comprehensive test coverage for the directive code path:
- directive adds prompt prefix to ProcessDirectWithChannel
- deliver=true + directive routes through agent (not direct publish)
- directive prompt content, sessionKey, channel, chatID are correct
- invalid type is rejected; valid types ("", "message", "directive") pass
- deliver=true message type goes directly to bus (regression)
- agent error path does not trigger publish (regression)
Also merge the two UpdateJob calls in addJob into one to avoid
redundant disk I/O (non-blocking suggestion from review).
|
Thanks for the thorough review! Both blocking issues are addressed in the latest commit. Changes in v21. Server-side validation for Added a whitelist check in 2. Directive test coverage Added 7 new tests covering the full
3. Merged redundant The two sequential Regarding the other non-blocking suggestions — happy to address in a follow-up:
|
Empty string and "message" are semantically equivalent defaults; always serializing the field avoids asymmetric JSON output.
15d1c0f to
ea2a3f7
Compare
|
Small follow-up: also removed the Also dropped the inline comment on that field ( |
- Remove ExecuteJobDirectivePassesCorrectContent: its assertions on sessionKey/channel/chatID duplicate ExecuteJobPublishesAgentResponse; its prompt check duplicates DirectiveAddsPromptPrefix. - Strengthen DirectiveAddsPromptPrefix with exact prompt match and publish response assertion. - Fix ReturnsErrorWithoutPublish: set non-empty stub response so the test verifies the error branch early-return, not the response=="" guard.
|
One more follow-up: reviewed the new tests for redundancy and made a cleanup pass (5d803d3).
Net result: 7 tests → 6 tests, same coverage, stronger assertions. Coverage progression across revisions:
|
… path The agent path now publishes to outbound bus directly (since sipeed#2100), making the deliver=true direct-to-bus shortcut and the directive type prompt wrapping redundant. All cron jobs now uniformly route through the agent. This is an intentional behavior change: old jobs with deliver=true will execute through the agent instead of bypassing it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem
When a cron job triggers agent execution via
ProcessDirectWithChannel, the agent's text response is silently discarded. The original code assumedAgentLoopwould auto-publish the response:This assumption is incorrect —
processMessagesetsSendResponse: false, sorunAgentLoopnever publishes the final content to the outbound bus. As a result, users on channels like Telegram never receive the agent's response for cron-triggered tasks.The
commandbranch and thedeliver=truebranch ofExecuteJobboth correctly callPublishOutbound, but the agent branch does not.Fix
AgentLoop.PublishResponseIfNeeded(previously unexportedpublishResponseIfNeeded) and add it to theJobExecutorinterface.CronTool.ExecuteJob, callPublishResponseIfNeededafter the agent returns a non-empty response.PublishResponseIfNeededchecksMessageTool.HasSentInRound()before publishing — if the agent already delivered content through themessagetool during this round, the final status response (e.g. "已发送。") is correctly skipped, preventing duplicate messages.Additional:
directivemessage typeAdds a
Typefield ("message"|"directive") toCronPayload:"message"(default): static content, sent as-is whendeliver=true."directive": content is treated as instructions for the agent to execute. Forces agent processing even whendeliver=true.This enables cron jobs like "check the weather and report" where the agent needs to perform work rather than echo a fixed string.
Changed files
pkg/agent/loop.goPublishResponseIfNeeded(rename only)pkg/cron/service.goTypefield toCronPayloadpkg/tools/cron.goPublishResponseIfNeededtoJobExecutorinterface; call it inExecuteJob; addtypeparameter and directive handlingpkg/tools/cron_test.goTest plan
go test ./pkg/tools -run TestCronTool_— all 13 tests passgo build ./...— compiles cleanly