feat(kernel,artifacts): inject artifact-kind vocabulary into architect context
This commit is contained in:
@@ -272,6 +272,10 @@ fun main() {
|
|||||||
// Supplier reads the live threshold each compaction check (no orchestrator rebuild needed).
|
// Supplier reads the live threshold each compaction check (no orchestrator rebuild needed).
|
||||||
tokenThreshold = { configHolder.get().orchestration.journalCompactionTokenThreshold },
|
tokenThreshold = { configHolder.get().orchestration.journalCompactionTokenThreshold },
|
||||||
)
|
)
|
||||||
|
val configArtifactKindsEarly = loadConfigArtifactKinds(correxConfig)
|
||||||
|
val artifactKindRegistry = DefaultArtifactKindRegistry().also { reg ->
|
||||||
|
configArtifactKindsEarly.forEach { reg.register(it) }
|
||||||
|
}
|
||||||
val orchestrator = DefaultSessionOrchestrator(
|
val orchestrator = DefaultSessionOrchestrator(
|
||||||
repositories = repositories,
|
repositories = repositories,
|
||||||
engines = engines,
|
engines = engines,
|
||||||
@@ -280,6 +284,7 @@ fun main() {
|
|||||||
tokenizer = firstProvider.tokenizer,
|
tokenizer = firstProvider.tokenizer,
|
||||||
decisionJournalRepository = decisionJournalRepository,
|
decisionJournalRepository = decisionJournalRepository,
|
||||||
compactionService = journalCompactionService,
|
compactionService = journalCompactionService,
|
||||||
|
artifactKindRegistry = artifactKindRegistry,
|
||||||
)
|
)
|
||||||
val embedder = InfrastructureModule.createEmbedderFromConfig(correxConfig.router.embedder)
|
val embedder = InfrastructureModule.createEmbedderFromConfig(correxConfig.router.embedder)
|
||||||
val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(correxConfig.router.l3)
|
val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(correxConfig.router.l3)
|
||||||
@@ -365,10 +370,6 @@ fun main() {
|
|||||||
val projectMemory = buildProjectMemory(correxConfig)
|
val projectMemory = buildProjectMemory(correxConfig)
|
||||||
val profileAdaptationService: ProfileAdaptationService? = buildProfileAdaptation(correxConfig)
|
val profileAdaptationService: ProfileAdaptationService? = buildProfileAdaptation(correxConfig)
|
||||||
|
|
||||||
val configArtifactKinds = loadConfigArtifactKinds(correxConfig)
|
|
||||||
val artifactKindRegistry = DefaultArtifactKindRegistry().also { reg ->
|
|
||||||
configArtifactKinds.forEach { reg.register(it) }
|
|
||||||
}
|
|
||||||
val freestyleDriver = FreestyleDriver(
|
val freestyleDriver = FreestyleDriver(
|
||||||
eventStore = eventStore,
|
eventStore = eventStore,
|
||||||
compiler = ExecutionPlanCompiler(artifactKindRegistry),
|
compiler = ExecutionPlanCompiler(artifactKindRegistry),
|
||||||
@@ -383,7 +384,7 @@ fun main() {
|
|||||||
artifactStore = artifactStore,
|
artifactStore = artifactStore,
|
||||||
sessionRepository = repositories.sessionRepository,
|
sessionRepository = repositories.sessionRepository,
|
||||||
workflowRegistry = FileSystemWorkflowRegistry(
|
workflowRegistry = FileSystemWorkflowRegistry(
|
||||||
InfrastructureModule.createWorkflowLoader(configArtifactKinds),
|
InfrastructureModule.createWorkflowLoader(configArtifactKindsEarly),
|
||||||
),
|
),
|
||||||
providerRegistry = infraRegistry.asServerRegistry(),
|
providerRegistry = infraRegistry.asServerRegistry(),
|
||||||
defaultOrchestrationConfig = defaultOrchestrationConfig,
|
defaultOrchestrationConfig = defaultOrchestrationConfig,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.correx.core.artifacts.kind
|
|||||||
interface ArtifactKindRegistry {
|
interface ArtifactKindRegistry {
|
||||||
fun register(kind: ArtifactKind)
|
fun register(kind: ArtifactKind)
|
||||||
fun get(id: String): ArtifactKind?
|
fun get(id: String): ArtifactKind?
|
||||||
|
fun list(): Collection<ArtifactKind>
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultArtifactKindRegistry : ArtifactKindRegistry {
|
class DefaultArtifactKindRegistry : ArtifactKindRegistry {
|
||||||
@@ -18,4 +19,6 @@ class DefaultArtifactKindRegistry : ArtifactKindRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun get(id: String): ArtifactKind? = kinds[id]
|
override fun get(id: String): ArtifactKind? = kinds[id]
|
||||||
|
|
||||||
|
override fun list(): Collection<ArtifactKind> = kinds.values.toList()
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -51,4 +51,25 @@ class ArtifactKindRegistryTest {
|
|||||||
assert("path" in schema.required)
|
assert("path" in schema.required)
|
||||||
assert("content" in schema.required)
|
assert("content" in schema.required)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `list returns all registered kinds`() {
|
||||||
|
val ids = registry.list().map { it.id }.toSet()
|
||||||
|
assert("file_written" in ids) { "expected file_written in $ids" }
|
||||||
|
assert("process_result" in ids) { "expected process_result in $ids" }
|
||||||
|
assertEquals(2, ids.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `list reflects newly registered kind`() {
|
||||||
|
val schema = JsonSchema(
|
||||||
|
type = "object",
|
||||||
|
properties = mapOf("x" to JsonSchemaProperty(type = "string")),
|
||||||
|
required = listOf("x"),
|
||||||
|
)
|
||||||
|
registry.register(ConfigArtifactKind(id = "extra", schema = schema, llmEmitted = true))
|
||||||
|
val ids = registry.list().map { it.id }.toSet()
|
||||||
|
assert("extra" in ids) { "expected extra in $ids" }
|
||||||
|
assertEquals(3, ids.size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.correx.core.kernel.orchestration
|
|||||||
// Context-entry builders kept top-level (not orchestrator members) for unit-testability
|
// Context-entry builders kept top-level (not orchestrator members) for unit-testability
|
||||||
// and to stay under detekt's function-count threshold (same pattern as isBackEdge).
|
// and to stay under detekt's function-count threshold (same pattern as isBackEdge).
|
||||||
|
|
||||||
|
import com.correx.core.artifacts.kind.ArtifactKind
|
||||||
import com.correx.core.context.model.ContextEntry
|
import com.correx.core.context.model.ContextEntry
|
||||||
import com.correx.core.context.model.ContextLayer
|
import com.correx.core.context.model.ContextLayer
|
||||||
import com.correx.core.context.model.EntryRole
|
import com.correx.core.context.model.EntryRole
|
||||||
@@ -47,6 +48,24 @@ fun criticArtifactIds(
|
|||||||
return graph.stages[fromStage]?.produces?.map { it.name }?.toSet() ?: emptySet()
|
return graph.stages[fromStage]?.produces?.map { it.name }?.toSet() ?: emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildArtifactKindVocabularyEntry(kinds: Collection<ArtifactKind>): ContextEntry {
|
||||||
|
val listing = kinds.joinToString(", ") { k ->
|
||||||
|
if (k.llmEmitted) "${k.id} (llm-emitted)" else k.id
|
||||||
|
}
|
||||||
|
val content = "## Available artifact kinds\n" +
|
||||||
|
"Every stage's \"produces\" MUST be exactly one of: $listing\n" +
|
||||||
|
"Do not invent kinds — a plan referencing an unknown kind fails to compile."
|
||||||
|
return ContextEntry(
|
||||||
|
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||||
|
layer = ContextLayer.L1,
|
||||||
|
content = content,
|
||||||
|
sourceType = "artifactKindVocabulary",
|
||||||
|
sourceId = "artifact-kinds",
|
||||||
|
tokenEstimate = content.length / 4,
|
||||||
|
role = EntryRole.SYSTEM,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
|
fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
|
||||||
val content = buildString {
|
val content = buildString {
|
||||||
append("## Project profile\n")
|
append("## Project profile\n")
|
||||||
|
|||||||
+3
-1
@@ -7,6 +7,7 @@ import com.correx.core.inference.Tokenizer
|
|||||||
import com.correx.core.journal.DefaultDecisionJournalRepository
|
import com.correx.core.journal.DefaultDecisionJournalRepository
|
||||||
import com.correx.core.journal.DecisionJournalRenderer
|
import com.correx.core.journal.DecisionJournalRenderer
|
||||||
import com.correx.core.artifacts.ArtifactState
|
import com.correx.core.artifacts.ArtifactState
|
||||||
|
import com.correx.core.artifacts.kind.ArtifactKindRegistry
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
@@ -55,7 +56,8 @@ class DefaultSessionOrchestrator(
|
|||||||
tokenizer: Tokenizer? = null,
|
tokenizer: Tokenizer? = null,
|
||||||
private val decisionJournalRepository: DefaultDecisionJournalRepository,
|
private val decisionJournalRepository: DefaultDecisionJournalRepository,
|
||||||
private val compactionService: JournalCompactionService? = null,
|
private val compactionService: JournalCompactionService? = null,
|
||||||
) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository), ApprovalGateway {
|
artifactKindRegistry: ArtifactKindRegistry? = null,
|
||||||
|
) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry), ApprovalGateway {
|
||||||
override val tokenizer: Tokenizer? = tokenizer
|
override val tokenizer: Tokenizer? = tokenizer
|
||||||
override val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean> =
|
override val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean> =
|
||||||
ConcurrentHashMap<SessionId, AtomicBoolean>()
|
ConcurrentHashMap<SessionId, AtomicBoolean>()
|
||||||
|
|||||||
+6
-1
@@ -11,6 +11,7 @@ import com.correx.core.approvals.model.ApprovalDecision
|
|||||||
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
||||||
import com.correx.core.approvals.model.DomainApprovalRequest
|
import com.correx.core.approvals.model.DomainApprovalRequest
|
||||||
import com.correx.core.artifacts.ArtifactState
|
import com.correx.core.artifacts.ArtifactState
|
||||||
|
import com.correx.core.artifacts.kind.ArtifactKindRegistry
|
||||||
import com.correx.core.artifacts.kind.JsonSchema
|
import com.correx.core.artifacts.kind.JsonSchema
|
||||||
import com.correx.core.artifacts.kind.FileWrittenArtifact
|
import com.correx.core.artifacts.kind.FileWrittenArtifact
|
||||||
import com.correx.core.artifacts.kind.ProcessResultArtifact
|
import com.correx.core.artifacts.kind.ProcessResultArtifact
|
||||||
@@ -153,6 +154,7 @@ abstract class SessionOrchestrator(
|
|||||||
protected val artifactStore: ArtifactStore,
|
protected val artifactStore: ArtifactStore,
|
||||||
private val decisionJournalRepository: DefaultDecisionJournalRepository,
|
private val decisionJournalRepository: DefaultDecisionJournalRepository,
|
||||||
private val decisionJournalRenderer: DecisionJournalRenderer = DecisionJournalRenderer(),
|
private val decisionJournalRenderer: DecisionJournalRenderer = DecisionJournalRenderer(),
|
||||||
|
private val artifactKindRegistry: ArtifactKindRegistry? = null,
|
||||||
) {
|
) {
|
||||||
private val log = LoggerFactory.getLogger(this::class.java)
|
private val log = LoggerFactory.getLogger(this::class.java)
|
||||||
private val eventStore: EventStore = repositories.eventStore
|
private val eventStore: EventStore = repositories.eventStore
|
||||||
@@ -374,9 +376,12 @@ abstract class SessionOrchestrator(
|
|||||||
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
|
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
|
||||||
val retryFeedbackEntries = buildRetryFeedbackEntry(sessionEvents, stageId)
|
val retryFeedbackEntries = buildRetryFeedbackEntry(sessionEvents, stageId)
|
||||||
?.let { listOf(it) } ?: emptyList()
|
?.let { listOf(it) } ?: emptyList()
|
||||||
|
val vocabularyEntries = artifactKindRegistry
|
||||||
|
?.takeIf { stageConfig.metadata["injectArtifactKinds"] == "true" }
|
||||||
|
?.let { listOf(buildArtifactKindVocabularyEntry(it.list())) } ?: emptyList()
|
||||||
var accumulatedEntries =
|
var accumulatedEntries =
|
||||||
systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries +
|
systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries +
|
||||||
needsEntries + schemaEntries + promptEntries + steeringEntries + retryFeedbackEntries
|
needsEntries + schemaEntries + vocabularyEntries + promptEntries + steeringEntries + retryFeedbackEntries
|
||||||
val contextPack = contextPackBuilder.build(
|
val contextPack = contextPackBuilder.build(
|
||||||
id = ContextPackId(UUID.randomUUID().toString()),
|
id = ContextPackId(UUID.randomUUID().toString()),
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ max_retries = 2
|
|||||||
[[stages]]
|
[[stages]]
|
||||||
id = "architect"
|
id = "architect"
|
||||||
requires_approval = true
|
requires_approval = true
|
||||||
|
inject_artifact_kinds = true
|
||||||
prompt = "prompts/architect_freestyle.md"
|
prompt = "prompts/architect_freestyle.md"
|
||||||
needs = ["analysis"]
|
needs = ["analysis"]
|
||||||
produces = [{ name = "execution_plan", kind = "execution_plan" }]
|
produces = [{ name = "execution_plan", kind = "execution_plan" }]
|
||||||
|
|||||||
+2
@@ -41,6 +41,7 @@ private data class StageSection(
|
|||||||
@param:JsonProperty("token_budget") val tokenBudget: Int = 4096,
|
@param:JsonProperty("token_budget") val tokenBudget: Int = 4096,
|
||||||
@param:JsonProperty("max_retries") val maxRetries: Int = 3,
|
@param:JsonProperty("max_retries") val maxRetries: Int = 3,
|
||||||
@param:JsonProperty("requires_approval") val requiresApproval: Boolean = false,
|
@param:JsonProperty("requires_approval") val requiresApproval: Boolean = false,
|
||||||
|
@param:JsonProperty("inject_artifact_kinds") val injectArtifactKinds: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
// condition fields flattened into the transition row
|
// condition fields flattened into the transition row
|
||||||
@@ -105,6 +106,7 @@ class TomlWorkflowLoader(
|
|||||||
s.prompt?.let { put("prompt", resolvePromptPath(it, workflowDir)) }
|
s.prompt?.let { put("prompt", resolvePromptPath(it, workflowDir)) }
|
||||||
s.systemPrompt?.let { put("systemPrompt", resolvePromptPath(it, workflowDir)) }
|
s.systemPrompt?.let { put("systemPrompt", resolvePromptPath(it, workflowDir)) }
|
||||||
if (s.requiresApproval) put("requiresApproval", "true")
|
if (s.requiresApproval) put("requiresApproval", "true")
|
||||||
|
if (s.injectArtifactKinds) put("injectArtifactKinds", "true")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+20
@@ -97,4 +97,24 @@ class TomlWorkflowLoaderTest {
|
|||||||
val path = Files.createTempFile("workflow", ".toml").also { it.writeText(bad) }
|
val path = Files.createTempFile("workflow", ".toml").also { it.writeText(bad) }
|
||||||
assertThrows<IllegalStateException> { loader.load(path) }
|
assertThrows<IllegalStateException> { loader.load(path) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `inject_artifact_kinds true maps to metadata key`() {
|
||||||
|
val toml = validToml.replace(
|
||||||
|
"prompt = \"prompts/collect.md\"",
|
||||||
|
"prompt = \"prompts/collect.md\"\ninject_artifact_kinds = true",
|
||||||
|
)
|
||||||
|
val path = Files.createTempFile("workflow", ".toml").also { it.writeText(toml) }
|
||||||
|
val graph = loader.load(path)
|
||||||
|
val collectStage = graph.stages[graph.start]!!
|
||||||
|
assertEquals("true", collectStage.metadata["injectArtifactKinds"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `inject_artifact_kinds absent means no metadata key`() {
|
||||||
|
val path = Files.createTempFile("workflow", ".toml").also { it.writeText(validToml) }
|
||||||
|
val graph = loader.load(path)
|
||||||
|
val collectStage = graph.stages[graph.start]!!
|
||||||
|
assertEquals(null, collectStage.metadata["injectArtifactKinds"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import com.correx.core.artifacts.kind.ConfigArtifactKind
|
||||||
import com.correx.core.artifacts.kind.FileWrittenKind
|
import com.correx.core.artifacts.kind.FileWrittenKind
|
||||||
|
import com.correx.core.artifacts.kind.JsonSchema
|
||||||
import com.correx.core.artifacts.kind.TypedArtifactSlot
|
import com.correx.core.artifacts.kind.TypedArtifactSlot
|
||||||
import com.correx.core.context.model.ContextLayer
|
import com.correx.core.context.model.ContextLayer
|
||||||
import com.correx.core.context.model.EntryRole
|
import com.correx.core.context.model.EntryRole
|
||||||
@@ -8,6 +10,7 @@ import com.correx.core.events.types.ArtifactId
|
|||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.events.types.TransitionId
|
import com.correx.core.events.types.TransitionId
|
||||||
|
import com.correx.core.kernel.orchestration.buildArtifactKindVocabularyEntry
|
||||||
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
|
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
|
||||||
import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry
|
import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry
|
||||||
import com.correx.core.kernel.orchestration.criticArtifactIds
|
import com.correx.core.kernel.orchestration.criticArtifactIds
|
||||||
@@ -94,6 +97,18 @@ class ContextFeedbackTest {
|
|||||||
assertEquals(setOf(ArtifactId("review_report")), criticArtifactIds(events, graph, StageId("implement")))
|
assertEquals(setOf(ArtifactId("review_report")), criticArtifactIds(events, graph, StageId("implement")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vocabulary entry enumerates kind ids and flags llm-emitted`() {
|
||||||
|
val plain = ConfigArtifactKind(id = "plan", schema = JsonSchema(type = "object"), llmEmitted = false)
|
||||||
|
val emitted = ConfigArtifactKind(id = "report", schema = JsonSchema(type = "object"), llmEmitted = true)
|
||||||
|
val entry = buildArtifactKindVocabularyEntry(listOf(plain, emitted))
|
||||||
|
assertEquals("artifactKindVocabulary", entry.sourceType)
|
||||||
|
assertEquals(ContextLayer.L1, entry.layer)
|
||||||
|
assertEquals(EntryRole.SYSTEM, entry.role)
|
||||||
|
assertTrue(entry.content.contains("plan"), "content should contain plain id: ${entry.content}")
|
||||||
|
assertTrue(entry.content.contains("report (llm-emitted)"), "content should flag llm-emitted: ${entry.content}")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `no refinement events yields empty set`() {
|
fun `no refinement events yields empty set`() {
|
||||||
assertTrue(criticArtifactIds(emptyList(), graphWith(), StageId("implement")).isEmpty())
|
assertTrue(criticArtifactIds(emptyList(), graphWith(), StageId("implement")).isEmpty())
|
||||||
|
|||||||
Reference in New Issue
Block a user