refactor(events): record tool capabilities on the invocation event
Capability is the canonical signal the plane-2 gates dispatch on, but ToolInvocationRequestedEvent only carried the tool name — so ReadFilesProjection classified reads by a hardcoded "file_read" string, bypassing the abstraction and (worse) re-deriving a semantic fact at replay from whatever the name meant. That violates the event-sourcing invariant that replay reads recorded facts, not live state. Now the tool's requiredCapabilities are recorded on the event at emission, and the projection classifies by ToolCapability.FILE_READ. To let the lowest module carry it, ToolCapability moves from core:tools into core:events keeping its package, so every existing import resolves unchanged and core:tools imports it from below. The field is defaulted, so pre-existing events deserialize as empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.sessions.projections.Projection
|
||||
import com.correx.core.toolintent.rules.candidatePathStrings
|
||||
import com.correx.core.tools.contract.ToolCapability
|
||||
|
||||
/** Reads still awaiting their completion event, plus the paths confirmed read so far. */
|
||||
data class ReadFilesState(
|
||||
@@ -14,11 +15,12 @@ data class ReadFilesState(
|
||||
)
|
||||
|
||||
/**
|
||||
* Folds one session's tool events into the set of file paths it has successfully read. A `file_read`
|
||||
* counts only once its [ToolExecutionCompletedEvent] lands — a read that failed (file not found)
|
||||
* never enters the set, so it can't satisfy [com.correx.core.toolintent.rules.ReadBeforeWriteRule].
|
||||
* Paths are kept as the raw arguments; the rule resolves both sides through the probe, so
|
||||
* `./Foo.kt` read and `Foo.kt` written still match. The same projection precedent as
|
||||
* Folds one session's tool events into the set of file paths it has successfully read. A call counts
|
||||
* as a read by its recorded [ToolCapability.FILE_READ] capability (not its tool name), and only once
|
||||
* its [ToolExecutionCompletedEvent] lands — a read that failed (file not found) never enters the
|
||||
* set, so it can't satisfy [com.correx.core.toolintent.rules.ReadBeforeWriteRule]. Paths are kept as
|
||||
* the raw arguments; the rule resolves both sides through the probe, so `./Foo.kt` read and
|
||||
* `Foo.kt` written still match. Same projection precedent as
|
||||
* [com.correx.core.sessions.projections.EgressAllowlistProjection].
|
||||
*/
|
||||
class ReadFilesProjection(private val sessionId: SessionId) : Projection<ReadFilesState> {
|
||||
@@ -33,7 +35,7 @@ class ReadFilesProjection(private val sessionId: SessionId) : Projection<ReadFil
|
||||
}
|
||||
|
||||
private fun recordPending(state: ReadFilesState, payload: ToolInvocationRequestedEvent): ReadFilesState {
|
||||
if (payload.sessionId != sessionId || payload.toolName != FILE_READ_TOOL) return state
|
||||
if (payload.sessionId != sessionId || ToolCapability.FILE_READ !in payload.capabilities) return state
|
||||
val paths = candidatePathStrings(emptyMap(), payload.request.parameters)
|
||||
if (paths.isEmpty()) return state
|
||||
return state.copy(pending = state.pending + (payload.invocationId.value to paths))
|
||||
@@ -47,8 +49,4 @@ class ReadFilesProjection(private val sessionId: SessionId) : Projection<ReadFil
|
||||
readPaths = state.readPaths + paths,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FILE_READ_TOOL = "file_read"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.correx.core.events.types.EventId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.types.ToolInvocationId
|
||||
import com.correx.core.tools.contract.ToolCapability
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@@ -38,6 +39,7 @@ class ReadFilesProjectionTest {
|
||||
toolName = "file_read",
|
||||
tier = Tier.T1,
|
||||
request = ToolRequest(ToolInvocationId(invId), session, StageId("st"), "file_read", mapOf("path" to path)),
|
||||
capabilities = setOf(ToolCapability.FILE_READ),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user