feat(kernel): inject needs-artifact content into stage context

Stages declaring needs=[...] now receive each needed artifact's validated
JSON (from artifactContentCache) as an L1/USER neededArtifact context entry,
so a one-shot subagent sees its inputs.

Also emit llm-emitted artifact lifecycle events (Created/Validating/Validated)
on successful validation via emitLlmArtifacts, closing a latent gap where
verifyProduces would fail for stages producing only an llm-emitted artifact.
Events are emitted only after validation passes, preserving invariant #7.
This commit is contained in:
2026-06-08 01:12:38 +04:00
parent 638f57709d
commit 63e5bcea93
2 changed files with 271 additions and 1 deletions
@@ -306,8 +306,9 @@ abstract class SessionOrchestrator(
),
)
val repoMapEntries = buildRepoMapEntries(sessionId)
val needsEntries = buildNeedsArtifactEntries(sessionId, stageConfig)
var accumulatedEntries =
systemPrompt + journalEntries + repoMapEntries + schemaEntries + promptEntries + steeringEntries
systemPrompt + journalEntries + repoMapEntries + needsEntries + schemaEntries + promptEntries + steeringEntries
val contextPack = contextPackBuilder.build(
id = ContextPackId(UUID.randomUUID().toString()),
sessionId = sessionId,
@@ -384,6 +385,7 @@ abstract class SessionOrchestrator(
when (val outcome = mapValidationOutcome(sessionId, stageId, validationCtx)) {
is StageExecutionResult.Success -> {
emitToolArtifacts(sessionId, stageId, stageConfig)
emitLlmArtifacts(sessionId, stageId, stageConfig)
verifyProduces(sessionId, stageId, stageConfig)
}
@@ -843,6 +845,26 @@ abstract class SessionOrchestrator(
)
}
private suspend fun buildNeedsArtifactEntries(
sessionId: SessionId,
stageConfig: StageConfig,
): List<ContextEntry> {
return stageConfig.needs.mapNotNull { needed ->
val cached = artifactContentCache["${sessionId.value}:${needed.value}"]
?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
val content = "## Input artifact: ${needed.value}\n$cached"
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L1,
content = content,
sourceType = "neededArtifact",
sourceId = needed.value,
tokenEstimate = estimateTokens(content),
role = EntryRole.USER,
)
}
}
private suspend fun emitToolArtifacts(
sessionId: SessionId,
stageId: StageId,
@@ -857,6 +879,20 @@ abstract class SessionOrchestrator(
}
}
private suspend fun emitLlmArtifacts(
sessionId: SessionId,
stageId: StageId,
stageConfig: StageConfig,
) {
stageConfig.produces
.filter { it.kind.llmEmitted }
.forEach { slot ->
emit(sessionId, ArtifactCreatedEvent(slot.name, sessionId, stageId, schemaVersion = 1))
emit(sessionId, ArtifactValidatingEvent(slot.name, sessionId, stageId))
emit(sessionId, ArtifactValidatedEvent(slot.name, sessionId, stageId))
}
}
private fun verifyProduces(
sessionId: SessionId,
stageId: StageId,