fix(talkie): surface narration actions in the user turn, session-wide
Live run showed the actions were in the prompt but the 1.2B narrator ignored
them: they sat in a system-role entry (small models under-weight system context)
and were stage-scoped to the pausing stage, which usually isn't the one that ran
the tools. Fold the recent actions into the USER trigger instruction instead, and
take the last N tool calls across the whole run rather than per-stage. Narration
now names the concrete actions ("reviewing server directories and reading the
specified file") instead of generic filler.
This commit is contained in:
@@ -424,38 +424,30 @@ class DefaultTalkieContextBuilder(
|
|||||||
layer = ContextLayer.L0,
|
layer = ContextLayer.L0,
|
||||||
role = EntryRole.SYSTEM,
|
role = EntryRole.SYSTEM,
|
||||||
)
|
)
|
||||||
// Concrete actions the stage took (tool calls), so the narrator names what actually
|
// Concrete actions taken (tool calls), folded into the USER instruction below rather than a
|
||||||
// happened instead of a generic "stage completed". Null/blank when nothing ran yet.
|
// separate system entry: small narrator models attend to the user turn far more than system
|
||||||
val activityEntry = recentActivity?.takeIf { it.isNotBlank() }?.let {
|
// context, so this is what actually makes the narration name what happened instead of a
|
||||||
buildContextEntry(
|
// generic "stage completed". Null/blank when nothing ran yet.
|
||||||
sourceType = "stageActivity",
|
val activityLine = recentActivity?.takeIf { it.isNotBlank() }
|
||||||
sourceId = trigger.stageId ?: state.currentStageId?.value ?: "none",
|
|
||||||
content = it,
|
|
||||||
layer = ContextLayer.L0,
|
|
||||||
role = EntryRole.SYSTEM,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// L1, not L0: PromptRenderer folds every L0 entry into the single system message
|
// L1, not L0: PromptRenderer folds every L0 entry into the single system message
|
||||||
// regardless of role, so an L0 user turn would vanish into the system block and the
|
// regardless of role, so an L0 user turn would vanish into the system block and the
|
||||||
// request would go out with no user message (the chat template then rejects it).
|
// request would go out with no user message (the chat template then rejects it).
|
||||||
val triggerEntry = buildContextEntry(
|
val triggerEntry = buildContextEntry(
|
||||||
sourceType = "narrationTrigger",
|
sourceType = "narrationTrigger",
|
||||||
sourceId = trigger.kind,
|
sourceId = trigger.kind,
|
||||||
content = trigger.instruction,
|
content = activityLine?.let { "${trigger.instruction}\n\n$it" } ?: trigger.instruction,
|
||||||
layer = ContextLayer.L1,
|
layer = ContextLayer.L1,
|
||||||
role = EntryRole.USER,
|
role = EntryRole.USER,
|
||||||
)
|
)
|
||||||
|
|
||||||
val protectedTokens = systemPromptEntry.tokenEstimate +
|
val protectedTokens = systemPromptEntry.tokenEstimate +
|
||||||
workflowStatusEntry.tokenEstimate +
|
workflowStatusEntry.tokenEstimate +
|
||||||
(activityEntry?.tokenEstimate ?: 0) +
|
|
||||||
triggerEntry.tokenEstimate
|
triggerEntry.tokenEstimate
|
||||||
var remainingBudget = (budget.limit - protectedTokens).coerceAtLeast(0)
|
var remainingBudget = (budget.limit - protectedTokens).coerceAtLeast(0)
|
||||||
|
|
||||||
val allEntries = mutableListOf<ContextEntry>()
|
val allEntries = mutableListOf<ContextEntry>()
|
||||||
allEntries += systemPromptEntry
|
allEntries += systemPromptEntry
|
||||||
allEntries += workflowStatusEntry
|
allEntries += workflowStatusEntry
|
||||||
activityEntry?.let { allEntries += it }
|
|
||||||
|
|
||||||
var droppedCount = 0
|
var droppedCount = 0
|
||||||
val truncatedLayers = mutableSetOf<ContextLayer>()
|
val truncatedLayers = mutableSetOf<ContextLayer>()
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ class DefaultTalkieFacade(
|
|||||||
val state = routerRepository.getTalkieState(sessionId)
|
val state = routerRepository.getTalkieState(sessionId)
|
||||||
val effectiveStageId = trigger.stageId?.let { StageId(it) } ?: state.currentStageId ?: StageId.NONE
|
val effectiveStageId = trigger.stageId?.let { StageId(it) } ?: state.currentStageId ?: StageId.NONE
|
||||||
|
|
||||||
val recentActivity = recentStageActivity(sessionId, effectiveStageId)
|
val recentActivity = recentActivity(sessionId)
|
||||||
val contextPack = routerContextBuilder.buildNarrationContext(state, trigger, config.tokenBudget, recentActivity)
|
val contextPack = routerContextBuilder.buildNarrationContext(state, trigger, config.tokenBudget, recentActivity)
|
||||||
|
|
||||||
val provider = inferenceRouter.route(effectiveStageId, setOf(ModelCapability.General), config.narrationModelId)
|
val provider = inferenceRouter.route(effectiveStageId, setOf(ModelCapability.General), config.narrationModelId)
|
||||||
@@ -255,15 +255,15 @@ class DefaultTalkieFacade(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Concrete tool actions taken during [stageId], read straight from the event log so the
|
* The most recent concrete tool actions in the session, read straight from the event log so
|
||||||
* narrator can say what actually happened (files read/written, tools run) instead of a
|
* the narrator can say what actually happened (files read/written, tools run) instead of a
|
||||||
* generic "stage completed". Returns null when nothing ran. Capped at the last few calls.
|
* generic "stage completed". Not stage-scoped: a narration commonly fires from a later stage
|
||||||
|
* (e.g. an approval pause) than the one that did the work, so the recent actions across the
|
||||||
|
* whole run are what "just happened". Returns null when nothing ran; capped at the last few.
|
||||||
*/
|
*/
|
||||||
private suspend fun recentStageActivity(sessionId: SessionId, stageId: StageId): String? {
|
private suspend fun recentActivity(sessionId: SessionId): String? {
|
||||||
if (stageId == StageId.NONE) return null
|
|
||||||
val actions = eventStore.read(sessionId)
|
val actions = eventStore.read(sessionId)
|
||||||
.mapNotNull { it.payload as? ToolInvocationRequestedEvent }
|
.mapNotNull { it.payload as? ToolInvocationRequestedEvent }
|
||||||
.filter { it.stageId == stageId }
|
|
||||||
.takeLast(MAX_NARRATED_ACTIONS)
|
.takeLast(MAX_NARRATED_ACTIONS)
|
||||||
.map { req ->
|
.map { req ->
|
||||||
val params = req.request.parameters
|
val params = req.request.parameters
|
||||||
|
|||||||
Reference in New Issue
Block a user