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:
@@ -26,6 +26,7 @@ import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||
import com.correx.core.events.events.RetryAttemptedEvent
|
||||
import com.correx.core.events.events.WorkflowFailedEvent
|
||||
import com.correx.core.events.events.WorkflowStartedEvent
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
@@ -70,6 +71,7 @@ import com.correx.core.sessions.projections.replay.EventReplayer
|
||||
import com.correx.core.tools.contract.ToolExecutor
|
||||
import com.correx.core.tools.contract.ToolResult
|
||||
import com.correx.core.transitions.evaluation.PromptResolver
|
||||
import com.correx.core.transitions.resolution.DefaultTransitionResolver
|
||||
import com.correx.core.transitions.graph.StageConfig
|
||||
import com.correx.core.transitions.graph.TransitionEdge
|
||||
import com.correx.core.transitions.graph.WorkflowGraph
|
||||
@@ -214,6 +216,46 @@ class SessionOrchestratorIntegrationTest {
|
||||
assertTrue(failed.retryExhausted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stage that matches no transition retries through the budget before failing`(): Unit = runBlocking {
|
||||
val sessionId = SessionId("s-no-transition-retry")
|
||||
// Stage A succeeds (no produces, no tools) but its only outgoing edge never matches, so the
|
||||
// resolver returns Stay. That must consume the retry budget, not fail the run on first sight.
|
||||
val graph = WorkflowGraph(
|
||||
id = "no-transition-retry-test",
|
||||
stages = mapOf(StageId("A") to StageConfig()),
|
||||
transitions = setOf(
|
||||
TransitionEdge(TransitionId("t1"), StageId("A"), StageId("done"), condition = { false }),
|
||||
),
|
||||
start = StageId("A"),
|
||||
)
|
||||
val config = OrchestrationConfig(retryPolicy = RetryPolicy(maxAttempts = 2, backoffMs = 0))
|
||||
// The default fixture evaluator returns true unconditionally; use one that honours the
|
||||
// condition so the { false } edge actually produces a Stay.
|
||||
val orchestrator = DefaultSessionOrchestrator(
|
||||
repositories = repositories,
|
||||
engines = engines.copy(
|
||||
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
|
||||
),
|
||||
retryCoordinator = retryCoordinator,
|
||||
artifactStore = artifactStore,
|
||||
decisionJournalRepository = decisionJournalRepository,
|
||||
)
|
||||
|
||||
orchestrator.run(sessionId, graph, config)
|
||||
|
||||
val events = eventStore.read(sessionId)
|
||||
val retries = events.count { it.payload is RetryAttemptedEvent }
|
||||
assertEquals(2, retries, "expected the no-transition outcome to exhaust the retry budget")
|
||||
val failed = events.find { it.payload is WorkflowFailedEvent }?.payload as? WorkflowFailedEvent
|
||||
assertNotNull(failed)
|
||||
assertTrue(failed!!.retryExhausted, "expected retryExhausted=true once the budget is spent")
|
||||
assertTrue(
|
||||
failed.reason.contains("no transition condition matched"),
|
||||
"unexpected failure reason: ${failed.reason}",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `workflow pauses for approval when required`(): Unit = runBlocking {
|
||||
val sessionId = SessionId("s3")
|
||||
|
||||
Reference in New Issue
Block a user