feat(context): emit ContextAssembled manifest event per stage build (#307)
Root cause: context-assembly (unconfirmedFixEntries, promotedConceptEntries,
buildRetryFeedbackEntry, etc.) computes ContextEntry lists at stage-build time
but never records what was actually injected into a turn. The only way to
answer "did hint X fire in session Y" was decoding the CAS prompt artifact and
grepping raw JSON — the event log alone couldn't say.
Scope check: no existing event carries this (ContextTruncatedEvent only
reports drop counts, not the injected set) — added new ContextAssembledEvent
rather than extending an existing one.
Fix: ContextAssembledEvent (core/events/events/ContextEvents.kt) records ONE
manifest per stage's initial ContextPack build — sessionId, stageId,
contextPackId, and a List<ContextManifestEntry> of {sourceType, sourceId,
tokenEstimate, layer, role} pulled from the pack's actual (post-budget)
entries. No entry content — content stays a pure derived projection of the
event log (invariant #9/#6) and already lives in CAS. Registered in
eventModule (Serialization.kt). Emitted via
SessionOrchestrator.emitContextAssembled (SessionOrchestratorRepoContext.kt),
wired at the initial contextPackBuilder.build() call site in
SessionOrchestratorExecution.kt (mirrors emitContextTruncationIfNeeded).
Tests: ContextAssembledEventSerializationTest (round-trip + no-content-leak),
SessionOrchestratorIntegrationTest "stage context build emits
ContextAssembledEvent with a manifest (#307)".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
This commit is contained in:
@@ -19,6 +19,7 @@ import com.correx.core.context.compression.ContextCompressor
|
||||
import com.correx.core.context.model.ContextEntry
|
||||
import com.correx.core.context.model.TokenBudget
|
||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||
import com.correx.core.events.events.ContextAssembledEvent
|
||||
import com.correx.core.events.events.ContextTruncatedEvent
|
||||
import com.correx.core.events.events.InferenceCompletedEvent
|
||||
import com.correx.core.events.events.InferenceStartedEvent
|
||||
@@ -535,6 +536,22 @@ class SessionOrchestratorIntegrationTest {
|
||||
assertTrue(truncated.first().entriesDropped >= 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stage context build emits ContextAssembledEvent with a manifest (#307)`(): Unit = runBlocking {
|
||||
val sessionId = SessionId("s-manifest")
|
||||
val config = OrchestrationConfig(retryPolicy = RetryPolicy(maxAttempts = 3, backoffMs = 0))
|
||||
|
||||
orchestrator.run(sessionId, graph, config)
|
||||
|
||||
val assembled = eventStore.read(sessionId).mapNotNull { it.payload as? ContextAssembledEvent }
|
||||
assertTrue(assembled.isNotEmpty(), "expected a ContextAssembledEvent per stage context build")
|
||||
val first = assembled.first()
|
||||
assertEquals(StageId("A"), first.stageId)
|
||||
assertTrue(first.entries.isNotEmpty(), "manifest should list the entries injected into the stage")
|
||||
// Manifest-only: identifying fields present, never the content (content lives in CAS/replay).
|
||||
assertTrue(first.entries.all { it.sourceType.isNotBlank() && it.layer.isNotBlank() && it.role.isNotBlank() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `artifactStore put is called for prompt and response and ids appear on inference events`(): Unit = runBlocking {
|
||||
val recordingStore = RecordingArtifactStore()
|
||||
|
||||
Reference in New Issue
Block a user