fix: 回合活跃判定放宽——压缩/通知行尾随不再误收起
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* 回合事件时序冒烟:验证 agent_settled 是否会在多回合工作(含 >600ms 慢工具)中途发出。
|
||||||
|
* 背景:渲染层回合折叠绑定 agent_settled(54b99a1),若 settled 在回合间被发出,
|
||||||
|
* 折叠会随 live 抖动反复展开/收起。本脚本在隔离的临时 cwd 下跑一轮真实多回合工作。
|
||||||
|
* 运行:node scripts/smoke-turn-events.mjs
|
||||||
|
*/
|
||||||
|
import { mkdtempSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import {
|
||||||
|
createAgentSessionServices,
|
||||||
|
createAgentSessionFromServices,
|
||||||
|
defineTool,
|
||||||
|
getAgentDir,
|
||||||
|
SessionManager
|
||||||
|
} from '@earendil-works/pi-coding-agent'
|
||||||
|
import { Type } from '@earendil-works/pi-ai'
|
||||||
|
|
||||||
|
const cwd = mkdtempSync(join(tmpdir(), 'aso-smoke-'))
|
||||||
|
const t0 = Date.now()
|
||||||
|
const ts = () => `${((Date.now() - t0) / 1000).toFixed(2).padStart(7)}s`
|
||||||
|
|
||||||
|
const slowTool = defineTool({
|
||||||
|
name: 'slow_probe',
|
||||||
|
label: '慢探针',
|
||||||
|
description: '等待 1500ms 后返回 done(模拟浏览器工具耗时,测试回合折叠时序)',
|
||||||
|
promptSnippet: 'slow_probe: 测试用慢工具',
|
||||||
|
parameters: Type.Object({}),
|
||||||
|
async execute() {
|
||||||
|
await new Promise((r) => setTimeout(r, 1500))
|
||||||
|
return { content: [{ type: 'text', text: 'done' }] }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loggingExtension = {
|
||||||
|
name: 'smoke-log',
|
||||||
|
factory: (pi) => {
|
||||||
|
for (const type of ['session_start', 'agent_start', 'agent_end', 'agent_settled', 'message_start', 'message_end', 'tool_call']) {
|
||||||
|
pi.on(type, (event) => console.log(ts(), `[ext] ${type}${event?.toolName ? ` (${event.toolName})` : ''}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolExtension = {
|
||||||
|
name: 'smoke-tools',
|
||||||
|
factory: (pi) => {
|
||||||
|
pi.registerTool(slowTool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const services = await createAgentSessionServices({
|
||||||
|
cwd,
|
||||||
|
agentDir: getAgentDir(),
|
||||||
|
resourceLoaderOptions: { extensionFactories: [loggingExtension, toolExtension] }
|
||||||
|
})
|
||||||
|
|
||||||
|
const sessionManager = SessionManager.continueRecent(cwd)
|
||||||
|
const { session } = await createAgentSessionFromServices({ services, sessionManager })
|
||||||
|
|
||||||
|
const unsubscribe = session.subscribe((event) => {
|
||||||
|
const message = event.message ?? {}
|
||||||
|
const detail =
|
||||||
|
event.type === 'tool_execution_start' || event.type === 'tool_execution_end'
|
||||||
|
? ` (${event.toolName})`
|
||||||
|
: event.type === 'message_start' || event.type === 'message_end'
|
||||||
|
? ` (role=${message.role ?? '?'})`
|
||||||
|
: ''
|
||||||
|
console.log(ts(), `[session] ${event.type}${detail}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(ts(), '--- prompt 开始:两次串行 slow_probe + 总结 ---')
|
||||||
|
try {
|
||||||
|
await session.prompt('调用 slow_probe 工具两次(串行调用,不要并行),然后只回复"完成"两个字。')
|
||||||
|
console.log(ts(), '--- prompt 返回(整个工作结束) ---')
|
||||||
|
} finally {
|
||||||
|
unsubscribe()
|
||||||
|
await session.abort?.().catch(() => undefined)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
@@ -108,7 +108,14 @@ function renderTurns(
|
|||||||
// turnActive/streaming 是会话级信号:只有延伸到消息列表末尾的最后一个
|
// turnActive/streaming 是会话级信号:只有延伸到消息列表末尾的最后一个
|
||||||
// assistant 组是「活跃回合」。透传给历史回合会让它们误显示
|
// assistant 组是「活跃回合」。透传给历史回合会让它们误显示
|
||||||
// 「已工作/处理中」并保持过程区展开。
|
// 「已工作/处理中」并保持过程区展开。
|
||||||
const isLive = j === messages.length
|
// 放宽:会话级 turnActive 期间,组后仅尾随非真实用户消息(自动压缩/分支
|
||||||
|
// 摘要通知行)时不算离开末尾——否则压缩间隙 live 跌落,600ms 自动收起后
|
||||||
|
// 续跑又展开,形成展开/收起反复横跳(慢工具/长会话反复压缩尤甚)。
|
||||||
|
let liveEnd = j
|
||||||
|
while (liveEnd < messages.length && !(messages[liveEnd].role === 'user' && !messages[liveEnd].local)) {
|
||||||
|
liveEnd += 1
|
||||||
|
}
|
||||||
|
const isLive = j === messages.length || (turnActive && liveEnd === messages.length)
|
||||||
// 本回合之后第一条真实用户消息(跳过压缩/摘要行):分叉锚点,'before' 含本回合答案为止
|
// 本回合之后第一条真实用户消息(跳过压缩/摘要行):分叉锚点,'before' 含本回合答案为止
|
||||||
let nextUser = j
|
let nextUser = j
|
||||||
while (nextUser < messages.length && !(messages[nextUser].role === 'user' && !messages[nextUser].local)) {
|
while (nextUser < messages.length && !(messages[nextUser].role === 'user' && !messages[nextUser].local)) {
|
||||||
@@ -183,7 +190,8 @@ export function ChatView({
|
|||||||
onFork,
|
onFork,
|
||||||
onForkAt,
|
onForkAt,
|
||||||
onPickSuggestion,
|
onPickSuggestion,
|
||||||
onQuoteSelection
|
onQuoteSelection,
|
||||||
|
visible = true
|
||||||
}: {
|
}: {
|
||||||
messages: UiMessage[]
|
messages: UiMessage[]
|
||||||
tools: Record<string, ToolState>
|
tools: Record<string, ToolState>
|
||||||
@@ -212,6 +220,8 @@ export function ChatView({
|
|||||||
onForkAt: (entryId: string) => void
|
onForkAt: (entryId: string) => void
|
||||||
onPickSuggestion: (text: string) => void
|
onPickSuggestion: (text: string) => void
|
||||||
onQuoteSelection: (selection: string) => void
|
onQuoteSelection: (selection: string) => void
|
||||||
|
/** 聊天列是否可见(切到插件视图时 display:none 保活;隐藏期滚动测量为 0,切回重新吸底) */
|
||||||
|
visible?: boolean
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
const pinnedRef = useRef(true)
|
const pinnedRef = useRef(true)
|
||||||
@@ -280,7 +290,7 @@ export function ChatView({
|
|||||||
el.scrollTop = el.scrollHeight
|
el.scrollTop = el.scrollHeight
|
||||||
})
|
})
|
||||||
return () => cancelAnimationFrame(raf)
|
return () => cancelAnimationFrame(raf)
|
||||||
}, [messages, tools, streaming])
|
}, [messages, tools, streaming, visible])
|
||||||
|
|
||||||
// 压缩结果通知 8 秒后自动消退
|
// 压缩结果通知 8 秒后自动消退
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user