feat(stage-workingset): stage-scoped rejection feedback (slice 7)
A rejected tool call produced one generic, context-free warning per rejection
("a previous tool call was rejected — choose a different approach"), repeated
and session-scoped. With no tool, args, tier, or reason, it read to the model
as noise rather than a correction, and could not tell it which call to avoid.
Replace with a single stage-scoped entry (buildRejectionFeedbackEntry) joining
each rejected ApprovalDecisionResolvedEvent back to its ApprovalRequestedEvent
by requestId: tool name, args preview, tier, and the operator's reason. Scoped
to the stage whose calls were declined; steering notes on a decision are kept
separately. Pure (events, stageId) like buildRetryFeedbackEntry, so unit-tested
directly. Keeps sourceType "rejectionFeedback" (REQUIRED bucket) unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,17 @@ import com.correx.core.events.events.RetryAttemptedEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
import com.correx.core.events.types.ToolInvocationId
|
||||
import com.correx.core.approvals.ApprovalOutcome
|
||||
import com.correx.core.approvals.ApprovalStatus
|
||||
import com.correx.core.approvals.Tier
|
||||
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||
import com.correx.core.events.types.ApprovalDecisionId
|
||||
import com.correx.core.events.types.ApprovalRequestId
|
||||
import com.correx.core.events.types.ArtifactId
|
||||
import com.correx.core.events.types.ValidationReportId
|
||||
import com.correx.core.kernel.orchestration.buildRejectionFeedbackEntry
|
||||
import kotlinx.datetime.Instant
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.types.TransitionId
|
||||
@@ -100,6 +109,58 @@ class ContextFeedbackTest {
|
||||
assertNull(buildRetryFeedbackEntry(events, StageId("impl")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejection feedback is one stage-scoped entry naming tool preview tier and reason`() {
|
||||
val req = ApprovalRequestId("req-1")
|
||||
val events = listOf(
|
||||
stored(
|
||||
payload = ApprovalRequestedEvent(
|
||||
requestId = req, tier = Tier.T2,
|
||||
validationReportId = ValidationReportId("vr-1"), riskSummaryId = null,
|
||||
sessionId = SessionId("s"), stageId = StageId("impl"), projectId = null,
|
||||
toolName = "shell", preview = "rm -rf build/",
|
||||
),
|
||||
),
|
||||
stored(
|
||||
payload = ApprovalDecisionResolvedEvent(
|
||||
decisionId = ApprovalDecisionId("d-1"), requestId = req,
|
||||
outcome = ApprovalOutcome.REJECTED, status = ApprovalStatus.COMPLETED,
|
||||
tier = Tier.T2, resolutionTimestamp = Instant.fromEpochMilliseconds(0),
|
||||
reason = "destructive; do not delete build output",
|
||||
),
|
||||
),
|
||||
)
|
||||
val entry = buildRejectionFeedbackEntry(events, StageId("impl"))!!
|
||||
assertEquals("rejectionFeedback", entry.sourceType)
|
||||
assertEquals(ContextLayer.L2, entry.layer)
|
||||
assertTrue(entry.content.contains("## Rejected tool calls this stage"), entry.content)
|
||||
assertTrue(entry.content.contains("shell (tier T2) rm -rf build/"), entry.content)
|
||||
assertTrue(entry.content.contains("destructive; do not delete build output"), entry.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejection feedback ignores other stages and non-rejected decisions`() {
|
||||
val reqOther = ApprovalRequestId("req-o")
|
||||
val events = listOf(
|
||||
stored(
|
||||
payload = ApprovalRequestedEvent(
|
||||
requestId = reqOther, tier = Tier.T1,
|
||||
validationReportId = ValidationReportId("vr-2"), riskSummaryId = null,
|
||||
sessionId = SessionId("s"), stageId = StageId("other"), projectId = null,
|
||||
toolName = "file_write", preview = "x",
|
||||
),
|
||||
),
|
||||
stored(
|
||||
payload = ApprovalDecisionResolvedEvent(
|
||||
decisionId = ApprovalDecisionId("d-2"), requestId = reqOther,
|
||||
outcome = ApprovalOutcome.REJECTED, status = ApprovalStatus.COMPLETED,
|
||||
tier = Tier.T1, resolutionTimestamp = Instant.fromEpochMilliseconds(0), reason = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertNull(buildRejectionFeedbackEntry(events, StageId("impl")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `project profile renders as single L0 entry`() {
|
||||
val entry = buildProjectProfileEntry(
|
||||
|
||||
Reference in New Issue
Block a user