feat(tools): propose_scope recalibration valve for the execution loop
When a write is blocked for being outside the claimed task's affected_paths, the implementer can propose widening scope via a new T2 propose_scope tool. The operator decides (invariant #4 — the agent can't self-grant). Approve unions the proposed paths into the task's affected_paths (existing TaskAffectedPathsSetEvent); activeScope re-derives the wider manifest from events so the same write then passes. Reject blocks the task (via TaskClaimCoordinator.blockActiveTask, hooked on both approval-denial branches, scoped to propose_scope) so the loop advances. No new event types.
This commit is contained in:
+14
@@ -154,6 +154,7 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
private const val MAX_TOOL_ROUNDS = 10
|
||||
private const val STAGE_COMPLETE_TOOL = "stage_complete"
|
||||
private const val SCOPE_PROPOSAL_TOOL = "propose_scope"
|
||||
private const val READ_BEFORE_WRITE_CODE = "READ_BEFORE_WRITE"
|
||||
private const val OUTPUT_SUMMARY_LIMIT = 500
|
||||
private const val REPO_MAP_INJECT_TOP_K = 30
|
||||
@@ -812,6 +813,7 @@ abstract class SessionOrchestrator(
|
||||
emitDecisionResolved(sessionId, domainRequest, engineDecision)
|
||||
if (!engineDecision.isApproved) {
|
||||
val rejectReason = engineDecision.reason ?: "denied"
|
||||
blockTaskOnScopeRejection(sessionId, toolCall.function.name, rejectReason)
|
||||
emit(sessionId, ToolExecutionRejectedEvent(
|
||||
invocationId = invocationId,
|
||||
sessionId = sessionId,
|
||||
@@ -869,6 +871,7 @@ abstract class SessionOrchestrator(
|
||||
emitDecisionResolved(sessionId, domainRequest, userDecision)
|
||||
if (!userDecision.isApproved) {
|
||||
val rejectReason = userDecision.reason ?: "approval denied"
|
||||
blockTaskOnScopeRejection(sessionId, toolCall.function.name, rejectReason)
|
||||
emit(sessionId, ToolExecutionRejectedEvent(
|
||||
invocationId = invocationId,
|
||||
sessionId = sessionId,
|
||||
@@ -992,6 +995,17 @@ abstract class SessionOrchestrator(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A rejected [SCOPE_PROPOSAL_TOOL] call means the operator denied widening the claimed task's
|
||||
* scope — block the task so the loop advances instead of the implementer retrying the same
|
||||
* out-of-scope write. Only fires for the scope-proposal tool; other denied tools are unaffected.
|
||||
*/
|
||||
private suspend fun blockTaskOnScopeRejection(sessionId: SessionId, toolName: String, reason: String) {
|
||||
if (toolName == SCOPE_PROPOSAL_TOOL) {
|
||||
taskClaimCoordinator?.blockActiveTask(sessionId, "scope amendment rejected: $reason")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runPlane2Assessment(
|
||||
sessionId: SessionId,
|
||||
stageId: StageId,
|
||||
|
||||
+8
@@ -19,4 +19,12 @@ interface TaskClaimCoordinator {
|
||||
|
||||
/** Write-scope (affected paths) of the task the session currently has claimed; empty when none. */
|
||||
fun activeScope(sessionId: SessionId): List<String>
|
||||
|
||||
/**
|
||||
* Block the session's currently-claimed task — used when the operator rejects a scope-amendment
|
||||
* proposal: the task can't proceed within scope, so it leaves the active/ready set and the loop
|
||||
* advances to the next task instead of the implementer retrying the blocked write. No-op when
|
||||
* nothing is claimed.
|
||||
*/
|
||||
suspend fun blockActiveTask(sessionId: SessionId, reason: String)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user