feat(router): rubber-duck idea capture + feed
The rubber-duck path emits a fenced {"ideas":[…]} block; Ideas.parse strips it
from the chat turn and onUserInput records an IdeaCapturedEvent each (CHAT only).
IdeaReader folds eventStore.allEvents() into the active cross-session board, and
DefaultRouterContextBuilder injects a capped "## Ideas" entry — router-only, so
workflow stages never see it. Extended SYSTEM_PROMPT option (d) to capture ideas.
This commit is contained in:
@@ -33,6 +33,7 @@ import com.correx.core.router.ChatMode
|
||||
import com.correx.core.router.DefaultRouterContextBuilder
|
||||
import com.correx.core.router.DefaultRouterFacade
|
||||
import com.correx.core.router.DefaultRouterReducer
|
||||
import com.correx.core.router.IdeaReader
|
||||
import com.correx.core.router.RouterContextBuilder
|
||||
import com.correx.core.router.RouterFacade
|
||||
import com.correx.core.router.RouterProjector
|
||||
@@ -664,6 +665,50 @@ class RouterFacadeTest {
|
||||
assertTrue(store.appendedEvents.map { it.payload }.filterIsInstance<WorkflowProposedEvent>().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rubber-duck ideas are captured, readable cross-session, and removed by a discard`(): Unit = runBlocking {
|
||||
val store = mockEventStore()
|
||||
val reply = "Right — worth keeping.\n```json\n{\"ideas\":[\"cache the repo map\",\"add --dry-run\"]}\n```"
|
||||
val facade = facadeProposing(store, reply, emptyList())
|
||||
|
||||
facade.onUserInput(SessionId("chat-1"), "how do we speed this up?")
|
||||
|
||||
val reader = IdeaReader(store)
|
||||
val ideas = reader.activeIdeas()
|
||||
assertEquals(2, ideas.size)
|
||||
assertTrue(ideas.any { it.text == "cache the repo map" } && ideas.any { it.text == "add --dry-run" })
|
||||
|
||||
// The visible chat turn must not leak the json block.
|
||||
val routerTurn = store.appendedEvents.map { it.payload }
|
||||
.filterIsInstance<ChatTurnEvent>().first { it.role == ChatTurnRole.ROUTER }
|
||||
assertFalse(routerTurn.content.contains("```"))
|
||||
|
||||
// Discard one (from a *different* session) → it drops off the board, the other stays.
|
||||
val drop = ideas.first { it.text == "add --dry-run" }
|
||||
store.append(
|
||||
NewEvent(
|
||||
metadata = EventMetadata(
|
||||
eventId = EventId("disc-1"), sessionId = SessionId("chat-2"),
|
||||
timestamp = Instant.parse("2026-01-01T00:00:00Z"), schemaVersion = 1, causationId = null, correlationId = null,
|
||||
),
|
||||
payload = com.correx.core.events.events.IdeaDiscardedEvent(ideaId = drop.id, sessionId = SessionId("chat-2")),
|
||||
),
|
||||
)
|
||||
val after = reader.activeIdeas()
|
||||
assertEquals(listOf("cache the repo map"), after.map { it.text })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a steering turn never captures ideas`(): Unit = runBlocking {
|
||||
val store = mockEventStore()
|
||||
val reply = "x\n```json\n{\"ideas\":[\"nope\"]}\n```"
|
||||
val facade = facadeProposing(store, reply, emptyList())
|
||||
|
||||
facade.onUserInput(SessionId("s"), "steer", ChatMode.STEERING)
|
||||
|
||||
assertTrue(IdeaReader(store).activeIdeas().isEmpty())
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user