feat(talkie): ground narration in the stage's actual tool actions
Narration was generic ("Stage X completed. Summarise.") because nothing about
what the stage actually did reached the narrator — the L2 summary is a
placeholder and StageCompletedEvent carries no output. narrate() now reads the
stage's ToolInvocationRequested events from the log and feeds the narrator a
concrete 'Actions taken this stage: file_read(path), ...' entry, and the system
prompt asks it to prefer those concrete actions over restating the stage name.
This commit is contained in:
@@ -6,6 +6,10 @@ import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.events.TalkieNarrationEvent
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
import com.correx.core.events.types.ToolInvocationId
|
||||
import com.correx.core.approvals.Tier
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.ContextPackId
|
||||
import com.correx.core.events.types.EventId
|
||||
@@ -78,6 +82,62 @@ class TalkieNarrationTest {
|
||||
assertEquals("stage_completed", event.trigger)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `narrate context carries the stage's concrete tool actions`(): Unit = runBlocking {
|
||||
val eventStore = MapBackedEventStore()
|
||||
val stage = StageId("s1") // matches buildFacade's currentStageId
|
||||
val session = SessionId("sess-activity")
|
||||
eventStore.append(
|
||||
NewEvent(
|
||||
metadata = EventMetadata(
|
||||
eventId = EventId("e1"), sessionId = session,
|
||||
timestamp = kotlinx.datetime.Clock.System.now(), schemaVersion = 1,
|
||||
causationId = null, correlationId = null,
|
||||
),
|
||||
payload = ToolInvocationRequestedEvent(
|
||||
invocationId = ToolInvocationId("inv-1"), sessionId = session, stageId = stage,
|
||||
toolName = "file_read", tier = Tier.T1,
|
||||
request = ToolRequest(
|
||||
invocationId = ToolInvocationId("inv-1"), sessionId = session, stageId = stage,
|
||||
toolName = "file_read", parameters = mapOf("path" to "apps/server/ServerModule.kt"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
var captured: InferenceRequest? = null
|
||||
val facade = DefaultTalkieFacade(
|
||||
routerRepository = object : TalkieRepository {
|
||||
override suspend fun getTalkieState(sessionId: SessionId) =
|
||||
TalkieState(sessionId = sessionId, workflowStatus = WorkflowStatus.RUNNING, currentStageId = stage)
|
||||
},
|
||||
routerContextBuilder = DefaultTalkieContextBuilder(TalkieConfig()),
|
||||
inferenceRouter = object : InferenceRouter {
|
||||
override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>) =
|
||||
object : InferenceProvider {
|
||||
override val id = ProviderId("mock"); override val name = "Mock"
|
||||
override val tokenizer = MockTokenizer()
|
||||
override suspend fun infer(request: InferenceRequest): InferenceResponse {
|
||||
captured = request
|
||||
return InferenceResponse(request.requestId, "line", FinishReason.Stop, TokenUsage(1, 1), 1L)
|
||||
}
|
||||
override suspend fun healthCheck() = ProviderHealth.Healthy
|
||||
override fun capabilities() = setOf(CapabilityScore(ModelCapability.General, 1.0))
|
||||
}
|
||||
},
|
||||
eventStore = eventStore,
|
||||
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
|
||||
embedder = NoopEmbedder(dimension = 8),
|
||||
l3MemoryStore = InMemoryL3MemoryStore(),
|
||||
)
|
||||
|
||||
facade.narrate(session, NarrationTrigger(kind = "stage_completed", instruction = "Summarise.", stageId = "s1"))
|
||||
|
||||
val contents = captured!!.contextPack.layers.values.flatten().map { it.content }
|
||||
val activity = contents.firstOrNull { "file_read" in it }
|
||||
assertNotNull(activity, "narration context must include the stage's tool actions; got: $contents")
|
||||
assertTrue("apps/server/ServerModule.kt" in activity!!, "activity must name the file: $activity")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `narrate does not emit ChatTurnEvent`(): Unit = runBlocking {
|
||||
val eventStore = MapBackedEventStore()
|
||||
@@ -143,7 +203,7 @@ class TalkieNarrationTest {
|
||||
},
|
||||
routerContextBuilder = object : TalkieContextBuilder {
|
||||
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
|
||||
override suspend fun buildNarrationContext(state: TalkieState, trigger: NarrationTrigger, budget: TokenBudget): ContextPack = emptyContextPack()
|
||||
override suspend fun buildNarrationContext(state: TalkieState, trigger: NarrationTrigger, budget: TokenBudget, recentActivity: String?): ContextPack = emptyContextPack()
|
||||
},
|
||||
inferenceRouter = mockInferenceRouter("stage narration", latencyMs = 10L, tokensUsed = TokenUsage(1, 1)),
|
||||
eventStore = eventStore,
|
||||
|
||||
Reference in New Issue
Block a user