Repository source: docs/specs/agent_reply_one_step_analyze_publish_contract_v1.md
Agent Reply One-Step Analyze/Publish Contract V1
This document defines the implementation contract for moving automatic
reply-agent publishing into POST /api/analyze_post.
Source plan:
docs/plans/agent_reply_one_step_analyze_publish_plan_v1.md
Scope
V1 covers the production behavior for:
automatic browser work rendered on newly created post cards
POST /api/analyze_postPOST /api/generate_agent_replygenerated-response row interpretation for the current target post/content hash
compact agent reply status embedded in successful analyze responses
The intended automatic path is:
A new post renders with
data-agent-reply-work="analyze"only when automatic agent reply work is enabled and no current analysis exists.The browser calls
POST /api/analyze_post.The backend stores the post analysis.
If configuration, analysis completion, and reply gates all allow posting, the backend posts the stored
engagement.suggested_responseasreply-agent.The analyze response reports both analysis status and compact agent reply status.
Non-Goals
Do not add a second model call for reply generation.
Do not remove
POST /api/generate_agent_reply.Do not add automatic retries for failed, pending, or posting generated-response rows.
Do not expose generated reply internals through
POST /api/analyze_post.Do not change the canonical reply record format.
Configuration Contract
DEDALUS_AGENT_REPLIES_ENABLED=false disables automatic agent reply work.
When disabled:
newly rendered post pages must not render
data-agent-reply-worksolely to report disabled agent reply statusa direct
POST /api/analyze_poststill performs analysis according to the existing analyze endpoint behaviorPOST /api/analyze_postmust not post an agent replysuccessful analyze responses must include:
{
"agent_reply_generation_allowed": false,
"agent_reply_generation_status": "not_recommended",
"agent_reply_posted": false,
"agent_reply_post_id": null,
"agent_reply_post_url": null,
"agent_reply_reason": "config_disabled",
"agent_reply_failure_code": null
}
POST /api/generate_agent_reply keeps its existing compatibility response shape
and must continue returning not_recommended with reason config_disabled when
agent replies are disabled.
Generated-Response Row Contract
For the current target post/content hash, existing generated-response rows have the following meanings when agent replies are enabled.
| Existing row state | Automatic render work | /api/analyze_post one-step behavior | /api/generate_agent_reply behavior |
|---|---|---|---|
| no row, no current analysis | analyze | analyze, then post if gates pass | analysis_required |
| no row, completed current analysis, gates pass | publish | post from stored suggestion if called directly | post from stored suggestion |
| no row, completed current analysis, gates fail | none | do not post; return compact not_recommended | existing not_recommended |
complete with no agent_post_id | publish | post existing stored suggestion if called directly | post existing stored suggestion |
pending or posting | none | do not start duplicate work; return compact in_progress if reached | existing in_progress |
failed | none | do not auto-retry; return compact failed if reached | existing failed response |
any status with agent_post_id | none | return compact already_posted if reached | existing already_posted |
Automatic browser work must not retry generated-response rows in pending,
posting, or failed states.
Shared Posting Helper Contract
PostWorkflowApiController::generateAgentReply() and
PostWorkflowApiController::analyzePost() must share one reply-posting
helper:
public function agentReplyResultForPost(array $post): array
(on src/ForumRewrite/Agent/PostWorkflowService.php)
The helper is an application helper, not an HTTP responder.
It may perform posting side effects, but it must not call sendJson() or
terminate request handling.
The helper must preserve existing POST /api/generate_agent_reply behavior:
check
DEDALUS_AGENT_REPLIES_ENABLEDcompute the current analysis context and content hash
inspect
SqliteAgentReplyGenerationStorefor the current target tuplereturn
already_posted,failed, orin_progresswhen appropriaterequire a completed current analysis
apply
agentReplyGateFailure()build the generated-response row from stored
engagement.suggested_responsereserve posting before creating the canonical reply
ensure the
reply-agentidentitycreate the canonical reply through
LocalWriteService::createReply()mark the generated-response row as posted
return the same full response shape currently returned by
POST /api/generate_agent_reply
POST /api/generate_agent_reply must validate method, post_id, and post
existence, then delegate to this helper and serialize the full helper result.
Analyze Response Contract
Every successful POST /api/analyze_post response with status: "ok" must
include all existing successful analysis fields plus these keys:
agent_reply_generation_allowedagent_reply_generation_statusagent_reply_postedagent_reply_post_idagent_reply_post_urlagent_reply_reasonagent_reply_failure_code
agent_reply_generation_allowed means that posting is allowed by configuration,
completed analysis, and reply gates. It is true only when:
agent replies are enabled
analysis completed for the current post/content hash
agentReplyGateFailure()returnsnull
It is false for disabled configuration, incomplete analysis, missing analysis,
and gate failures.
The compact fields must always use null for unavailable IDs, URLs, reasons,
and failure codes. Successful analyze responses must not omit these keys.
Analyze Posting Contract
PostWorkflowApiController::analyzePost() must:
run post analysis exactly as it does today
compute
agent_reply_generation_allowedfrom configuration, analysis completion, and reply gate resultbuild compact skipped reply status directly for disabled config, incomplete analysis, missing analysis, and gate-failed cases
call
agentReplyResultForPost()only whenagent_reply_generation_allowed === trueembed a compact agent reply result in the analysis JSON response
return the analysis response even if reply posting fails, unless an existing hard analysis failure path already behaves differently
The analyze endpoint must not call the posting helper for known skipped cases.
Compact Summary Mapper Contract
Add an allowlisted mapper:
public function agentReplySummaryForAnalysisResponse(array $replyResult): array
(on src/ForumRewrite/Agent/PostWorkflowService.php)
The mapper translates a full reply helper result into compact analyze response fields:
| Full or local status | agent_reply_generation_status | agent_reply_posted | ID and URL fields | Reason and failure fields |
|---|---|---|---|---|
generated | generated | true when helper result has posted === true | from agent_post_id and agent_post_url | nullable |
already_posted | already_posted | true | from agent_post_id and agent_post_url | nullable |
not_recommended | not_recommended | false | null | agent_reply_reason from reason |
analysis_required | analysis_required | false | null | agent_reply_reason from reason; no analysis-provider failure code |
in_progress | in_progress | false | null | nullable |
failed | failed | false | null unless a posted ID already exists | agent_reply_failure_code from failure_code |
For local skipped cases in POST /api/analyze_post:
disabled config maps to
not_recommendedwith reasonconfig_disabledmissing analysis maps to
analysis_requiredwith reasonmissing_analysisincomplete analysis maps to
analysis_requiredwith reasonanalysis_not_completegate failure maps to
not_recommended
The mapper must intentionally drop full helper fields that are not allowlisted for analyze responses.
Privacy Contract
POST /api/analyze_post must never expose generated reply internals through the
compact agent_reply_* fields.
Forbidden compact analyze fields include:
response_textreply
providerreply
provider_modelprovider_request_idraw_responsefailure_messageretry_after
Unauthorized viewers must not receive concrete gate-failure reasons from
POST /api/analyze_post. If a gate fails and viewer_can_see_analysis is false,
return:
{
"agent_reply_generation_status": "not_recommended",
"agent_reply_reason": "not_recommended"
}
Authorized viewers may receive concrete gate-failure reasons, including:
respondability_score_lowresponse_risk_highmoderation_severity_highresponse_not_publicagent_loop_prevention
Analysis-provider failure details remain governed by existing analysis response
visibility rules. Do not copy analysis-provider failure codes into
agent_reply_failure_code.
The compact agent_reply_failure_code field is only for reply posting or stored
suggestion failures, such as analysis_suggestion_error or posting_error.
Browser Automation Contract
public/assets/post_analysis.js must treat work === "analyze" as a single
backend operation:
call only
analyzePost(postId)adapt embedded
agent_reply_*fields into the existing generation-result shapepass the adapted result into the existing feedback renderer
do not call
generateAgentReply(postId)after successful analysis
Add this small adapter:
agentReplyResultFromAnalysis(analysis)
The adapter maps:
analysis.status === "ok"plus embedded agent reply fields tostatus: "ok"agent_reply_generation_statustogeneration_statusagent_reply_postedtopostedagent_reply_post_idtoagent_post_idagent_reply_post_urltoagent_post_urlagent_reply_reasontoreasonagent_reply_failure_codetofailure_code
For work === "publish", the browser must continue calling
generateAgentReply(postId). This covers pages where current analysis already
existed before render.
The existing idempotency guard around created post IDs must remain in place.
Rendering Work Contract
PostWorkflowService::agentReplyWorkForPost() and the surrounding
agentReplyWorkByPostId() configuration gate must follow this contract:
disabled
DEDALUS_AGENT_REPLIES_ENABLED: render no automatic worktarget post authored by
reply-agent: render no automatic workno current analysis: render
analyzecompleted current analysis, gates pass, and no generated-response row: render
publishcompleted current analysis, gates pass, and a
completegenerated-response row exists with noagent_post_id: renderpublishcompleted current analysis and gates fail: render no automatic work
analysis exists but is not complete: render no automatic work
generated-response row has
agent_post_id: render no automatic workgenerated-response row is
pending,posting, orfailed: render no automatic work
After a successful one-step analyze/publish call, a reload should find a
generated-response row with agent_post_id, so the created post card must not
render data-agent-reply-work.
Idempotency Contract
For the same target post/content hash:
successful one-step analyze/publish must create at most one canonical
reply-agentreplyrepeated
POST /api/analyze_postcalls must not post duplicate repliesrepeated
POST /api/generate_agent_replycalls must continue returning the existing idempotent compatibility resultconcurrent or repeated work must honor existing
pending,posting,complete,failed, andagent_post_idgenerated-response states
Required Verification
Tests should be added or updated in tests/WriteApiSmokeTest.php to cover:
POST /api/analyze_postposts an agent reply when analysis passes gatesthe analyze response includes compact embedded agent reply status and post URL
the canonical reply file exists
the canonical reply has
Parent-IDequal to the analyzed post IDthe canonical reply is authored by the
reply-agentidentitythe generated-response row uses the analysis provider and model
decoded
raw_response.sourceon the generated-response row isanalysis_suggested_responserepeating
POST /api/analyze_postfor the same content does not post a duplicate replyPOST /api/generate_agent_replykeeps its full compatibility response shape and idempotency behavior