feat(freestyle): two-phase planning->execution driver (Slice 4)
- freestyle_planning.toml: analyst -> (approval) -> architect, producing execution_plan; analyst_freestyle.md surfaces open questions. - TomlWorkflowLoader: requires_approval stage flag -> metadata. - SessionOrchestrator: inline-prompt branch (metadata[promptInline]) + requestStageApproval helper reusing the existing pause/approve path. - DefaultSessionOrchestrator: enterStage gates on requiresApproval (idempotent via resolved-approval lookup), preview derived from the gated stage's needs artifact; validatedArtifactContent accessor. - FreestyleDriver: compiles validated plan, emits ExecutionPlanLockedEvent, runs phase 2 in-session via a runPhase2 seam; wired through ServerModule + Main (execution_plan kind registered).
This commit is contained in:
+2
@@ -39,6 +39,7 @@ private data class StageSection(
|
||||
@param:JsonProperty("allowed_tools") val allowedTools: List<String> = emptyList(),
|
||||
@param:JsonProperty("token_budget") val tokenBudget: Int = 4096,
|
||||
@param:JsonProperty("max_retries") val maxRetries: Int = 3,
|
||||
@param:JsonProperty("requires_approval") val requiresApproval: Boolean = false,
|
||||
)
|
||||
|
||||
// condition fields flattened into the transition row
|
||||
@@ -94,6 +95,7 @@ class TomlWorkflowLoader(
|
||||
metadata = buildMap {
|
||||
s.prompt?.let { put("prompt", resolvePromptPath(it, workflowDir)) }
|
||||
s.systemPrompt?.let { put("systemPrompt", resolvePromptPath(it, workflowDir)) }
|
||||
if (s.requiresApproval) put("requiresApproval", "true")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.correx.infrastructure.workflow
|
||||
|
||||
import com.correx.core.artifacts.kind.ConfigArtifactKind
|
||||
import com.correx.core.artifacts.kind.DefaultArtifactKindRegistry
|
||||
import com.correx.core.artifacts.kind.JsonSchema
|
||||
import com.correx.core.artifacts.kind.JsonSchemaProperty
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.transitions.conditions.ArtifactValidated
|
||||
import org.junit.jupiter.api.Assumptions.assumeTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FreestylePlanningWorkflowTest {
|
||||
|
||||
private fun llmKind(id: String) = ConfigArtifactKind(
|
||||
id = id,
|
||||
schema = JsonSchema(
|
||||
type = "object",
|
||||
properties = mapOf("summary" to JsonSchemaProperty(type = "string")),
|
||||
additionalProperties = true,
|
||||
),
|
||||
llmEmitted = true,
|
||||
)
|
||||
|
||||
private val registry = DefaultArtifactKindRegistry().apply {
|
||||
register(llmKind("analysis"))
|
||||
register(llmKind("execution_plan"))
|
||||
}
|
||||
|
||||
private fun repoFile(rel: String): Path? {
|
||||
var dir: Path? = Path.of(System.getProperty("user.dir")).toAbsolutePath()
|
||||
while (dir != null) {
|
||||
val candidate = dir.resolve(rel)
|
||||
if (candidate.exists()) return candidate
|
||||
dir = dir.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `freestyle_planning toml parses with two non-terminal stages and both edges`() {
|
||||
val path = repoFile("examples/workflows/freestyle_planning.toml")
|
||||
assumeTrue(path != null, "freestyle_planning.toml not found from ${System.getProperty("user.dir")}")
|
||||
|
||||
val graph = TomlWorkflowLoader(registry).load(path!!)
|
||||
|
||||
assertEquals(
|
||||
setOf("analyst", "architect"),
|
||||
graph.stages.keys.map { it.value }.toSet(),
|
||||
)
|
||||
|
||||
val architect = graph.stages[StageId("architect")]!!
|
||||
assertTrue(
|
||||
architect.produces.any { it.name.value == "execution_plan" && it.kind.llmEmitted },
|
||||
"architect must produce execution_plan",
|
||||
)
|
||||
|
||||
assertTrue(architect.needs.map { it.value }.contains("analysis"), "architect needs analysis")
|
||||
|
||||
val analystToArchitect = graph.transitions.first { it.from == StageId("analyst") }
|
||||
assertEquals(StageId("architect"), analystToArchitect.to)
|
||||
assertTrue(analystToArchitect.condition is ArtifactValidated)
|
||||
|
||||
val architectToDone = graph.transitions.first { it.from == StageId("architect") }
|
||||
assertEquals(StageId("done"), architectToDone.to)
|
||||
assertTrue(architectToDone.condition is ArtifactValidated)
|
||||
|
||||
assertEquals(2, graph.transitions.size)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user