fix(kernel,tools,workflow): session-robustness QA sweep
Uncommitted work from the session-robustness-and-dox branch sweep
(docs/qa/QA-session-robustness-and-dox.md), verified alongside the
compression/context fixes:
- SandboxedToolExecutor: validate tool args centrally before dispatch. A
malformed/missing-arg call becomes a recoverable ERROR: (surfaced with the
tool's arg schema so the model can correct + retry) instead of stranding
the stage with no artifact. + validation test.
- PlanLinter: seed artifacts (analysis) produced by the planning phase count
as available producers, so a plan stage that `needs` them isn't flagged as
an unproduced-need; H1 unproduced-needs + trap-state checks. + tests.
- DefaultSessionOrchestrator: live-QA robustness fixes (event-tail /
per-stage budget + retry handling).
- workflow prompts/configs: DOX AGENTS.md alignment + freestyle/task/role
prompt tweaks.
- SessionOrchestratorIntegrationTest: coverage for the above.
- FreestylePlanningWorkflowTest: allow list_dir in analyst tools (follows the
list_dir wiring in 968cbfa).
- QA plan doc for the branch sweep.
This commit is contained in:
+13
-6
@@ -20,7 +20,7 @@ data class PlanLintResult(
|
||||
|
||||
/**
|
||||
* Deterministic, pure-Kotlin lint over a compiled plan graph (plan-pipeline-spec §5). Zero inference,
|
||||
* zero I/O — a function of the graph alone, so it is replay-safe by construction. It complements
|
||||
* zero I/O — a function of the graph and a fixed seed set, so it is replay-safe by construction. It complements
|
||||
* [ExecutionPlanCompiler] (which already throws on unreachable-from-start / unknown-kind / bad-edge):
|
||||
* the lint adds the checks the compiler does NOT make.
|
||||
*
|
||||
@@ -32,15 +32,22 @@ object PlanLinter {
|
||||
private const val STAGE_CEILING = 12
|
||||
private const val FAN_OUT_THRESHOLD = 4
|
||||
|
||||
fun lint(graph: WorkflowGraph): PlanLintResult =
|
||||
/**
|
||||
* Artifacts that pre-exist in the session before the execution plan runs — produced by the
|
||||
* planning phase, not by any plan stage. The architect prompt explicitly allows `needs` to
|
||||
* reference these (notably `analysis`), so the linter must treat them as available producers.
|
||||
*/
|
||||
private val seedArtifacts = setOf("analysis")
|
||||
|
||||
fun lint(graph: WorkflowGraph, seeds: Set<String> = seedArtifacts): PlanLintResult =
|
||||
PlanLintResult(
|
||||
hardFailures = unproducedNeeds(graph) + trapStates(graph),
|
||||
hardFailures = unproducedNeeds(graph, seeds) + trapStates(graph),
|
||||
softFindings = stageCount(graph) + fanOut(graph) + emptyBriefs(graph) + duplicateBriefs(graph),
|
||||
)
|
||||
|
||||
/** H1: a stage `needs` an artifact that no stage `produces`. */
|
||||
private fun unproducedNeeds(graph: WorkflowGraph): List<PlanLintFinding> {
|
||||
val produced = graph.stages.values.flatMap { it.produces }.map { it.name.value }.toSet()
|
||||
/** H1: a stage `needs` an artifact that neither a plan stage `produces` nor a seed provides. */
|
||||
private fun unproducedNeeds(graph: WorkflowGraph, seeds: Set<String>): List<PlanLintFinding> {
|
||||
val produced = graph.stages.values.flatMap { it.produces }.map { it.name.value }.toSet() + seeds
|
||||
return graph.stages.entries.flatMap { (id, stage) ->
|
||||
stage.needs.map { it.value }.filterNot { it in produced }.map { need ->
|
||||
PlanLintFinding(
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ class FreestylePlanningWorkflowTest {
|
||||
// analyst frames the work: search + open one task or decompose into a graph (the architect
|
||||
// threads the named task into the plan).
|
||||
assertEquals(
|
||||
setOf("file_read", "ShellTool", "task_search", "task_context", "task_create", "task_decompose"),
|
||||
setOf("file_read", "list_dir", "shell", "task_search", "task_context", "task_create", "task_decompose"),
|
||||
graph.stages[StageId("analyst")]!!.allowedTools,
|
||||
)
|
||||
}
|
||||
|
||||
+14
@@ -78,6 +78,20 @@ class PlanLinterTest {
|
||||
assertTrue(h.detail.contains("z"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a need satisfied by the analysis seed is not a failure`() {
|
||||
val g = graph(
|
||||
stages = mapOf(
|
||||
"a" to stage(produces = listOf("x"), needs = listOf("analysis"), prompt = "research"),
|
||||
"b" to stage(needs = listOf("x"), prompt = "build"),
|
||||
),
|
||||
edges = listOf("a" to "b", "b" to "done"),
|
||||
start = "a",
|
||||
)
|
||||
val result = PlanLinter.lint(g)
|
||||
assertTrue(result.passed, "analysis is a planning-phase seed, not an unproduced need")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a cycle with no exit is a trap-state hard failure`() {
|
||||
val g = graph(
|
||||
|
||||
Reference in New Issue
Block a user