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_reply- generated-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_response as reply-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 status
- a direct
POST /api/analyze_poststill performs analysis according to the
existing analyze endpoint behavior
POST /api/analyze_postmust not post an agent reply- successful 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_ENABLED - compute the current analysis context and content hash
- inspect
SqliteAgentReplyGenerationStorefor the current target tuple - return
already_posted,failed, orin_progresswhen appropriate - require a completed current analysis
- apply
agentReplyGateFailure() - build the generated-response row from stored
engagement.suggested_response
- reserve posting before creating the canonical reply
- ensure the
reply-agentidentity - create 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 result
- build compact skipped reply status directly for disabled config, incomplete
analysis, missing analysis, and gate-failed cases
- call
agentReplyResultForPost()only when
agent_reply_generation_allowed === true
- embed 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_disabled - missing analysis maps to
analysis_requiredwith reasonmissing_analysis - incomplete analysis maps to
analysis_requiredwith reason
analysis_not_complete
- gate 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_text- reply
provider - reply
provider_model provider_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
shape
- pass 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 work - target post authored by
reply-agent: render no automatic work - no current analysis: render
analyze - completed current analysis, gates pass, and no generated-response row: render
publish
- completed current analysis, gates pass, and a
completegenerated-response
row exists with no agent_post_id: render publish
- completed 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 work - generated-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-agent reply
- repeated
POST /api/analyze_postcalls must not post duplicate replies - repeated
POST /api/generate_agent_replycalls must continue returning the
existing idempotent compatibility result
- concurrent or repeated work must honor existing
pending,posting,
complete, failed, and agent_post_id generated-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 gates- the 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 ID - the canonical reply is authored by the
reply-agentidentity - the generated-response row uses the analysis provider and model
- decoded
raw_response.sourceon the generated-response row is
analysis_suggested_response
- repeating
POST /api/analyze_postfor the same content does not post a
duplicate reply
POST /api/generate_agent_replykeeps its full compatibility response shape
and idempotency behavior