feat(run): print agent/model/duration on assistant completion
This commit is contained in:
parent
6bf365595f
commit
b0e8f5ec7b
@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect, spyOn } from "bun:test"
|
import { describe, it, expect, spyOn } from "bun:test"
|
||||||
import type { RunContext } from "./types"
|
import type { RunContext } from "./types"
|
||||||
import { createEventState } from "./events"
|
import { createEventState } from "./events"
|
||||||
import { handleSessionStatus, handleMessagePartUpdated, handleTuiToast } from "./event-handlers"
|
import { handleSessionStatus, handleMessagePartUpdated, handleMessageUpdated, handleTuiToast } from "./event-handlers"
|
||||||
|
|
||||||
const createMockContext = (sessionID: string = "test-session"): RunContext => ({
|
const createMockContext = (sessionID: string = "test-session"): RunContext => ({
|
||||||
sessionID,
|
sessionID,
|
||||||
@ -232,6 +232,80 @@ describe("handleMessagePartUpdated", () => {
|
|||||||
expect(state.lastPartText).toBe("Legacy text")
|
expect(state.lastPartText).toBe("Legacy text")
|
||||||
stdoutSpy.mockRestore()
|
stdoutSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("prints completion metadata once when assistant text part is completed", () => {
|
||||||
|
// given
|
||||||
|
const nowSpy = spyOn(Date, "now")
|
||||||
|
nowSpy.mockReturnValueOnce(1000)
|
||||||
|
nowSpy.mockReturnValueOnce(3400)
|
||||||
|
|
||||||
|
const ctx = createMockContext("ses_main")
|
||||||
|
const state = createEventState()
|
||||||
|
const stdoutSpy = spyOn(process.stdout, "write").mockImplementation(() => true)
|
||||||
|
|
||||||
|
handleMessageUpdated(
|
||||||
|
ctx,
|
||||||
|
{
|
||||||
|
type: "message.updated",
|
||||||
|
properties: {
|
||||||
|
info: {
|
||||||
|
id: "msg_1",
|
||||||
|
sessionID: "ses_main",
|
||||||
|
role: "assistant",
|
||||||
|
agent: "Sisyphus",
|
||||||
|
modelID: "claude-sonnet-4-6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
|
||||||
|
// when
|
||||||
|
handleMessagePartUpdated(
|
||||||
|
ctx,
|
||||||
|
{
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: {
|
||||||
|
part: {
|
||||||
|
id: "part_1",
|
||||||
|
sessionID: "ses_main",
|
||||||
|
messageID: "msg_1",
|
||||||
|
type: "text",
|
||||||
|
text: "done",
|
||||||
|
time: { end: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
|
||||||
|
handleMessagePartUpdated(
|
||||||
|
ctx,
|
||||||
|
{
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: {
|
||||||
|
part: {
|
||||||
|
id: "part_1",
|
||||||
|
sessionID: "ses_main",
|
||||||
|
messageID: "msg_1",
|
||||||
|
type: "text",
|
||||||
|
text: "done",
|
||||||
|
time: { end: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
|
||||||
|
// then
|
||||||
|
const output = stdoutSpy.mock.calls.map(call => String(call[0])).join("")
|
||||||
|
const metaCount = output.split("Sisyphus · claude-sonnet-4-6 · 2.4s").length - 1
|
||||||
|
expect(metaCount).toBe(1)
|
||||||
|
expect(state.completionMetaPrintedByMessageId["msg_1"]).toBe(true)
|
||||||
|
|
||||||
|
stdoutSpy.mockRestore()
|
||||||
|
nowSpy.mockRestore()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("handleTuiToast", () => {
|
describe("handleTuiToast", () => {
|
||||||
|
|||||||
@ -51,6 +51,19 @@ function getDeltaMessageId(props?: {
|
|||||||
return props?.messageID
|
return props?.messageID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderCompletionMetaLine(state: EventState, messageID: string): void {
|
||||||
|
if (state.completionMetaPrintedByMessageId[messageID]) return
|
||||||
|
|
||||||
|
const startedAt = state.messageStartedAtById[messageID]
|
||||||
|
const elapsedSec = startedAt ? ((Date.now() - startedAt) / 1000).toFixed(1) : "0.0"
|
||||||
|
const agent = state.currentAgent ?? "assistant"
|
||||||
|
const model = state.currentModel ?? "unknown-model"
|
||||||
|
const variant = state.currentVariant ? ` (${state.currentVariant})` : ""
|
||||||
|
|
||||||
|
process.stdout.write(pc.dim(`\n ${displayChars.treeEnd} ${agent} · ${model}${variant} · ${elapsedSec}s \n`))
|
||||||
|
state.completionMetaPrintedByMessageId[messageID] = true
|
||||||
|
}
|
||||||
|
|
||||||
export function handleSessionIdle(ctx: RunContext, payload: EventPayload, state: EventState): void {
|
export function handleSessionIdle(ctx: RunContext, payload: EventPayload, state: EventState): void {
|
||||||
if (payload.type !== "session.idle") return
|
if (payload.type !== "session.idle") return
|
||||||
|
|
||||||
@ -133,6 +146,13 @@ export function handleMessagePartUpdated(ctx: RunContext, payload: EventPayload,
|
|||||||
state.hasReceivedMeaningfulWork = true
|
state.hasReceivedMeaningfulWork = true
|
||||||
}
|
}
|
||||||
state.lastPartText = part.text
|
state.lastPartText = part.text
|
||||||
|
|
||||||
|
if (part.time?.end) {
|
||||||
|
const messageID = part.messageID ?? state.currentMessageId
|
||||||
|
if (messageID) {
|
||||||
|
renderCompletionMetaLine(state, messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.type === "tool") {
|
if (part.type === "tool") {
|
||||||
@ -238,6 +258,10 @@ export function handleMessageUpdated(ctx: RunContext, payload: EventPayload, sta
|
|||||||
state.textAtLineStart = true
|
state.textAtLineStart = true
|
||||||
state.thinkingAtLineStart = false
|
state.thinkingAtLineStart = false
|
||||||
closeThinkBlockIfNeeded(state)
|
closeThinkBlockIfNeeded(state)
|
||||||
|
if (messageID) {
|
||||||
|
state.messageStartedAtById[messageID] = Date.now()
|
||||||
|
state.completionMetaPrintedByMessageId[messageID] = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = props?.info?.agent ?? null
|
const agent = props?.info?.agent ?? null
|
||||||
|
|||||||
@ -39,6 +39,10 @@ export interface EventState {
|
|||||||
thinkingAtLineStart: boolean
|
thinkingAtLineStart: boolean
|
||||||
/** Current assistant message ID — prevents counter resets on repeated message.updated for same message */
|
/** Current assistant message ID — prevents counter resets on repeated message.updated for same message */
|
||||||
currentMessageId: string | null
|
currentMessageId: string | null
|
||||||
|
/** Assistant message start timestamp by message ID */
|
||||||
|
messageStartedAtById: Record<string, number>
|
||||||
|
/** Prevent duplicate completion metadata lines per message */
|
||||||
|
completionMetaPrintedByMessageId: Record<string, boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEventState(): EventState {
|
export function createEventState(): EventState {
|
||||||
@ -66,5 +70,7 @@ export function createEventState(): EventState {
|
|||||||
textAtLineStart: true,
|
textAtLineStart: true,
|
||||||
thinkingAtLineStart: false,
|
thinkingAtLineStart: false,
|
||||||
currentMessageId: null,
|
currentMessageId: null,
|
||||||
|
messageStartedAtById: {},
|
||||||
|
completionMetaPrintedByMessageId: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user