feat(toolintent): record the session's active task as a session-local fact
Claiming a task now emits SessionWorkingTaskEvent{taskId, affectedPaths} into the
session's own stream (via a SessionFactRecorder port + event-store adapter), and
SessionContextProjection folds it into SessionContext.activeTask. So a gate learns
"which task is this session working, and its scope" by reading the session stream
— no cross-stream scan, no kernel->tasks dependency. Re-emitted on an affected_paths
edit by the claimant, so the snapshot scope stays current; latest wins on replay.
This is the data source the write-scope gate needs (next).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.correx.core.events.events
|
||||
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.TaskId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -10,6 +11,20 @@ data class ChatSessionStartedEvent(
|
||||
val sessionId: SessionId,
|
||||
) : EventPayload
|
||||
|
||||
/**
|
||||
* Records, in the session's own stream, that this run claimed [taskId] and is working it with the
|
||||
* declared [affectedPaths] write scope — a session-local fact so the gates know the active task
|
||||
* without scanning task streams. Re-emitted when the claimant edits affected_paths, so the folded
|
||||
* scope stays current; SessionContextProjection takes the most recent.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("SessionWorkingTask")
|
||||
data class SessionWorkingTaskEvent(
|
||||
val sessionId: SessionId,
|
||||
val taskId: TaskId,
|
||||
val affectedPaths: List<String> = emptyList(),
|
||||
) : EventPayload
|
||||
|
||||
@Serializable
|
||||
@SerialName("SessionWorkspaceBound")
|
||||
data class SessionWorkspaceBoundEvent(
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.correx.core.events.events.ArtifactValidatingEvent
|
||||
import com.correx.core.events.events.BriefEchoMismatchEvent
|
||||
import com.correx.core.events.events.BriefGroundingCheckedEvent
|
||||
import com.correx.core.events.events.ChatSessionStartedEvent
|
||||
import com.correx.core.events.events.SessionWorkingTaskEvent
|
||||
import com.correx.core.events.events.ClarificationAnsweredEvent
|
||||
import com.correx.core.events.events.ClarificationRequestedEvent
|
||||
import com.correx.core.events.events.ChatTurnEvent
|
||||
@@ -139,6 +140,7 @@ val eventModule = SerializersModule {
|
||||
subclass(BriefEchoMismatchEvent::class)
|
||||
subclass(RiskAssessedEvent::class)
|
||||
subclass(ChatSessionStartedEvent::class)
|
||||
subclass(SessionWorkingTaskEvent::class)
|
||||
subclass(ChatTurnEvent::class)
|
||||
subclass(RouterNarrationEvent::class)
|
||||
subclass(SessionWorkspaceBoundEvent::class)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.correx.core.toolintent
|
||||
|
||||
import com.correx.core.events.events.SessionWorkingTaskEvent
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
@@ -40,6 +41,12 @@ class SessionContextProjection(private val sessionId: SessionId) : Projection<Se
|
||||
when (val payload = event.payload) {
|
||||
is ToolInvocationRequestedEvent -> recordPendingRead(state, payload)
|
||||
is ToolExecutionCompletedEvent -> recordCompletion(state, payload)
|
||||
is SessionWorkingTaskEvent ->
|
||||
if (payload.sessionId == sessionId) {
|
||||
state.copy(activeTask = ActiveTask(payload.taskId.value, payload.affectedPaths))
|
||||
} else {
|
||||
state
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
|
||||
|
||||
+14
@@ -6,9 +6,11 @@ import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.events.SessionWorkingTaskEvent
|
||||
import com.correx.core.events.events.ToolReceipt
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
import com.correx.core.events.types.EventId
|
||||
import com.correx.core.events.types.TaskId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.types.ToolInvocationId
|
||||
@@ -76,4 +78,16 @@ class SessionContextProjectionTest {
|
||||
assertEquals(setOf("core/A.kt", "core/B.kt"), ctx.writes)
|
||||
assertTrue(ctx.reads.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a SessionWorkingTask event sets the active task and the latest wins`() {
|
||||
val ctx = fold(
|
||||
listOf(
|
||||
stored(SessionWorkingTaskEvent(session, TaskId("auth-1"), listOf("core/a/**"))),
|
||||
stored(SessionWorkingTaskEvent(session, TaskId("auth-2"), listOf("core/auth/**"))),
|
||||
),
|
||||
)
|
||||
assertEquals("auth-2", ctx.activeTask?.taskId)
|
||||
assertEquals(listOf("core/auth/**"), ctx.activeTask?.scope)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user