feat(research): dedicated SourceFetched + LowQualityExtraction events (D)

- promote research fetch quality + content_sha256 from ToolExecutionCompletedEvent
  metadata to first-class SourceFetchedEvent / LowQualityExtractionEvent (registered
  + serialization test); existing metadata write kept intact (additive)
- emitted from SandboxedToolExecutor on research-fetch tool completion, guarded on
  the url/content_sha256/quality markers; reuses the extractor's LOW_QUALITY marker

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 11:49:26 +00:00
parent eae0a0ccf6
commit b098d87075
5 changed files with 259 additions and 0 deletions
@@ -0,0 +1,41 @@
package com.correx.core.events.events
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* A research T2 fetch completed and produced content at [contentSha256] (research-workflow-spec §3/§5).
*
* Promotes the fetch-quality + content-hash that previously lived only inside
* [ToolExecutionCompletedEvent] metadata into a first-class event, so "what sources were fetched"
* is queryable and renderable directly from the journal. Additive: the metadata write is retained
* for existing consumers. [quality] mirrors the extractor's quality marker as recorded in metadata.
*/
@Serializable
@SerialName("SourceFetched")
data class SourceFetchedEvent(
val sessionId: SessionId,
val stageId: StageId,
val url: String,
val contentSha256: String,
val quality: String,
val byteCount: Int = 0,
) : EventPayload
/**
* An extraction was retained but flagged low-quality — below the quality bar (research-workflow-spec §5)
* — for operator visibility. Emitted alongside [SourceFetchedEvent] when the fetch cleared rejection
* but fell under the minimum-content threshold (JS-rendered SPA, paywall); the content is still kept,
* the operator is simply warned that synthesis may want to route around it.
*/
@Serializable
@SerialName("LowQualityExtraction")
data class LowQualityExtractionEvent(
val sessionId: SessionId,
val stageId: StageId,
val url: String,
val contentSha256: String,
val reason: String,
) : EventPayload
@@ -31,6 +31,7 @@ import com.correx.core.events.events.IdeaDiscardedEvent
import com.correx.core.events.events.JournalCompactedEvent
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.L3MemoryRetrievedEvent
import com.correx.core.events.events.LowQualityExtractionEvent
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InitialIntentEvent
import com.correx.core.events.events.InferenceFailedEvent
@@ -46,6 +47,7 @@ import com.correx.core.events.events.RepoMapComputedEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.WorkspaceStateObservedEvent
import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.SourceFetchedEvent
import com.correx.core.events.events.StageCheckpointFailedEvent
import com.correx.core.events.events.StageCheckpointPassedEvent
import com.correx.core.events.events.StageCompletedEvent
@@ -137,6 +139,8 @@ val eventModule = SerializersModule {
subclass(StageCheckpointPassedEvent::class)
subclass(StageCheckpointFailedEvent::class)
subclass(PossibleContradictionFlaggedEvent::class)
subclass(SourceFetchedEvent::class)
subclass(LowQualityExtractionEvent::class)
}
}
@@ -0,0 +1,47 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.LowQualityExtractionEvent
import com.correx.core.events.events.SourceFetchedEvent
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ResearchSourceEventSerializationTest {
private val sessionId = SessionId("s")
private val stageId = StageId("st")
@Test
fun `SourceFetchedEvent round-trips as polymorphic EventPayload`() {
val sample: EventPayload = SourceFetchedEvent(
sessionId = sessionId,
stageId = stageId,
url = "https://example.com/a",
contentSha256 = "abc123",
quality = "OK",
byteCount = 4096,
)
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
assertTrue(encoded.contains("\"type\":\"SourceFetched\""), "SerialName must be present: $encoded")
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
assertEquals(sample, decoded)
}
@Test
fun `LowQualityExtractionEvent round-trips as polymorphic EventPayload`() {
val sample: EventPayload = LowQualityExtractionEvent(
sessionId = sessionId,
stageId = stageId,
url = "https://example.com/spa",
contentSha256 = "def456",
reason = "extracted 12 chars, below minimum 200",
)
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
assertTrue(encoded.contains("\"type\":\"LowQualityExtraction\""), "SerialName must be present: $encoded")
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
assertEquals(sample, decoded)
}
}