mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
复核修正(前端):节点计数、并行数、「当前在跑」不把子工作流里的步骤另算;子图那一步写「子工作流 › 那一步」
- B5 run/trace:子工作流里的步骤(isSubStep:节点 id「子工作流节点 id/…」)算在子工作流那一步里——完成数不另算(以前运行详情 会写「7/3」),没有 run.started 时的分母也不算它们;Telemetry 的「当前「X」等 N 个」同一个规矩,子工作流和它里面那一步不算成 两个同时在跑 - B6 run/trace.stepLabel:图上没有的子图步骤接上子工作流的名字(「取数子流程 › 查订单」,和服务端终态事件的 label 同一个拼法, 名字取 node.started 记的);运行胶囊(RunHud)、泳道(RunTimeline)的失败、阻断说法和泳道名、记录页的失败横幅与航迹 (RunDetailView.labelOf)都用它,不再写「失败于「sub/x」」 检查脚本:check-decode 加两段(子工作流步骤的完成数、并行数、当前在跑;出错在子图里那一步时的名字)
This commit is contained in:
@@ -7,7 +7,7 @@ import { formatClock, formatDuration, formatLapse, formatOffset, formatSpan, for
|
||||
import { statusMeta } from '../lib/status'
|
||||
import { topology, withSubSteps, type GraphLike } from '../run/derive'
|
||||
import {
|
||||
isActivePhase, isTerminal, lastEventAt, liveAt, project, type NodeState, type NodeTrace, type Projection,
|
||||
isActivePhase, isTerminal, lastEventAt, liveAt, project, stepLabel, type NodeState, type NodeTrace, type Projection,
|
||||
type Segment, type Trace,
|
||||
} from '../run/trace'
|
||||
import { projectCached } from '../run/useNodeView'
|
||||
@@ -341,7 +341,9 @@ function buildRows(trace: Trace, graph: TimelineGraph, at: number, labels: Map<s
|
||||
const byId = new Map(graph.nodes.map((n) => [n.id, n]))
|
||||
// 子工作流里的步骤排在子工作流那一行下面(withSubSteps),老运行里改过图的照旧排在最后
|
||||
const ids = withSubSteps(topo.order, Object.keys(trace.nodes).filter((id) => !byId.has(id)))
|
||||
const failedLabel = trace.failedNodeId ? labels.get(trace.failedNodeId) ?? trace.failedNodeId : ''
|
||||
const own = (id: string) => labels.get(id)
|
||||
// 子工作流里的那一步图上没有:接上子工作流的名字(stepLabel)
|
||||
const failedLabel = trace.failedNodeId ? stepLabel(trace, trace.failedNodeId, own) : ''
|
||||
const rows: Row[] = []
|
||||
let top = 0
|
||||
for (const id of ids) {
|
||||
@@ -350,7 +352,7 @@ function buildRows(trace: Trace, graph: TimelineGraph, at: number, labels: Map<s
|
||||
const p = states?.[id]
|
||||
const state: NodeState = states ? p?.state ?? 'idle' : n?.state ?? 'idle'
|
||||
const type = nodeTypeOf(byId.get(id))
|
||||
const label = labels.get(id) ?? id
|
||||
const label = stepLabel(trace, id, own)
|
||||
const tokens = states ? (p?.tokensIn ?? 0) + (p?.tokensOut ?? 0) : undefined
|
||||
rows.push({
|
||||
key: id, kind: 'node', nodeId: id, label, type,
|
||||
@@ -780,7 +782,7 @@ export function RunTimeline({
|
||||
<button type="button" className="tl-alert" onClick={() => onFocusNode?.(trace.failedNodeId!)}
|
||||
title="在画布上定位到失败的节点">
|
||||
<StatusBadge status="failed" size={11} animate={false} decorative />
|
||||
<span className="min-w-0 truncate">失败于「{labels.get(trace.failedNodeId) ?? trace.failedNodeId}」</span>
|
||||
<span className="min-w-0 truncate">失败于「{stepLabel(trace, trace.failedNodeId, (x) => labels.get(x))}」</span>
|
||||
</button>
|
||||
)}
|
||||
{trace.phase === 'waiting' && trace.waitingNodeId && (
|
||||
|
||||
@@ -13,7 +13,7 @@ import { catalogDrift, decodePhase, decodeRun, exitLabels, schemaDrift, summariz
|
||||
import { sourceHandles } from '../../canvas/nodeDefs'
|
||||
import { EffectsPane, RunTaskCards, useRunTasks } from './RunTasks'
|
||||
import { OriginChip } from './RunList'
|
||||
import { runStatusOf, type RunPhase } from '../../run/trace'
|
||||
import { runStatusOf, stepLabel, type RunPhase } from '../../run/trace'
|
||||
import { useCatalog } from '../../store/catalog'
|
||||
import type { Approval, Run, RunEvent } from '../../types'
|
||||
import { CatalogDriftBanner, FailedBanner, FeedbackStrip, HeldBanner, WaitingBanner, type Feedback } from './Banners'
|
||||
@@ -161,9 +161,9 @@ export function RunDetailView({ runId, onChange, onDeleted }: {
|
||||
|
||||
const labelOf = useCallback((id?: string | null) => {
|
||||
if (!id) return undefined
|
||||
const n = d.graph?.nodes.find((x) => x.id === id)
|
||||
return n?.data?.label || id
|
||||
}, [d.graph])
|
||||
// 子工作流里的那一步(出错、被驳回在它上面时运行记下的节点是「sub/x」)图上没有:接上子工作流的名字(stepLabel)
|
||||
return stepLabel(d.trace, id, (x) => d.graph?.nodes.find((n) => n.id === x)?.data?.label || undefined)
|
||||
}, [d.graph, d.trace])
|
||||
|
||||
if (!run) {
|
||||
if (d.state === 'error') {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
NONE, formatClock, formatCost, formatDateTime, formatNumber, formatSpan, formatTime, formatTokens, shortId,
|
||||
} from '../../lib/format'
|
||||
import { type StatusCode } from '../../lib/status'
|
||||
import { isExecuting, liveAt, project, type Projection, type RunPhase, type Trace } from '../../run/trace'
|
||||
import { isExecuting, isSubStep, liveAt, project, type Projection, type RunPhase, type Trace } from '../../run/trace'
|
||||
import { useRunClock } from '../../run/useRunClock'
|
||||
import type { Approval, Run } from '../../types'
|
||||
import { BUNDLE_TEXT, COST_TEXT, REJECTED_TEXT, UNSAVED_HINT, isUnsaved, runCostText, runName } from '../../lib/terms'
|
||||
@@ -96,7 +96,8 @@ export function Telemetry({ run, trace, phase, code, streaming, pending, labelOf
|
||||
const unpriced = (settled || !streaming ? (isNum(usage.unpriced_calls) ? usage.unpriced_calls : undefined) : undefined)
|
||||
?? trace.unpricedCalls
|
||||
|
||||
const running = Object.values(trace.nodes).filter(isExecuting)
|
||||
// 子工作流里的那一步算在子工作流那一步上(isSubStep),不和它一起算成两个同时在跑
|
||||
const running = Object.values(trace.nodes).filter((n) => isExecuting(n) && !isSubStep(n.id))
|
||||
const waitingId = trace.waitingNodeId ?? pending?.[0]?.node_id
|
||||
const waitedMs = trace.waitingSince != null ? liveAt(trace, now) - trace.waitingSince
|
||||
: ageMs(pending?.[0]?.created_at, now)
|
||||
|
||||
@@ -14,7 +14,7 @@ import { structureSig, useStudio } from '../store/studio'
|
||||
import type { RunEvent } from '../types'
|
||||
import { topology } from './derive'
|
||||
import {
|
||||
isActivePhase, isSettled, isTerminal, liveAt, project, type NodeState, type RunPhase, type Trace,
|
||||
isActivePhase, isSettled, isTerminal, liveAt, project, stepLabel, type NodeState, type RunPhase, type Trace,
|
||||
} from './trace'
|
||||
import { useRunClock } from './useRunClock'
|
||||
|
||||
@@ -204,7 +204,8 @@ export function useRunGlance(active: boolean): RunGlance {
|
||||
|
||||
const order = useMemo(() => topology({ nodes, edges }).order, [nodes, edges])
|
||||
const labels = useMemo(() => new Map(nodes.map((n) => [n.id, n.data.label || n.id])), [nodes])
|
||||
const label = (id: string) => labels.get(id) ?? id
|
||||
// 子工作流里的那一步(失败、被驳回在它上面)图上没有:接上子工作流的名字(stepLabel)
|
||||
const label = (id: string) => stepLabel(trace, id, (x) => labels.get(x))
|
||||
|
||||
const live = isActivePhase(phaseLive)
|
||||
const at = replayAt ?? (live ? liveAt(trace, now) : lastStampOf(trace))
|
||||
|
||||
@@ -91,6 +91,8 @@ export interface NodeTrace {
|
||||
*/
|
||||
exited?: boolean
|
||||
exitTo?: string[]
|
||||
/** node.started 上记的名字。子工作流里的步骤不在画布的图上,名字从这里取(stepLabel) */
|
||||
label?: string
|
||||
/** 用量随时间的累计,回放时按游标取那一刻的 tokens 和工具数。没调过模型和工具的节点没有 */
|
||||
marks?: UsageMark[]
|
||||
}
|
||||
@@ -277,6 +279,25 @@ function openIndex(segs: Segment[], kind: Segment['kind'], agent?: string): numb
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 子工作流里的一步(节点 id「子工作流节点 id/…」,docs/design/orchestration-round/00-总方案.md §2):它在子工作流节点那一步
|
||||
* 里面跑,节点计数、并行数、「当前在跑」都算在子工作流那一步上,不另算
|
||||
*/
|
||||
export const isSubStep = (id: string): boolean => id.includes('/')
|
||||
|
||||
/**
|
||||
* 一个节点给人看的名字:own 是画布的图上的名字(找不到为 undefined)。子工作流里的步骤(isSubStep)图上没有,接上子工作流
|
||||
* 那一步的名字:「取数子流程 › 查订单」——和服务端终态事件的 label 同一个拼法(runner._label_of),失败横幅、泳道、
|
||||
* 运行胶囊不再写裸的「sub/fetch」。别的找不到的照旧写 id
|
||||
*/
|
||||
export function stepLabel(t: Trace, id: string, own: (id: string) => string | undefined): string {
|
||||
const mine = own(id)
|
||||
if (mine) return mine
|
||||
if (!isSubStep(id)) return id
|
||||
const cut = id.lastIndexOf('/')
|
||||
return `${stepLabel(t, id.slice(0, cut), own)} › ${t.nodes[id]?.label || id.slice(cut + 1)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 此刻同时在跑的数量。
|
||||
*
|
||||
@@ -286,8 +307,7 @@ function openIndex(segs: Segment[], kind: Segment['kind'], agent?: string): numb
|
||||
function parallelOf(nodes: Record<string, NodeTrace>): number {
|
||||
let total = 0
|
||||
for (const n of Object.values(nodes)) {
|
||||
// 子工作流里的那几步(「子工作流节点 id/…」)在子工作流节点那一格里跑,不另算一路并行
|
||||
if (n.id.includes('/')) continue
|
||||
if (isSubStep(n.id)) continue
|
||||
let inner = 0
|
||||
let run = 0
|
||||
for (const s of n.segments) {
|
||||
@@ -521,6 +541,7 @@ export function foldEvent(t: Trace, ev: RunEvent): Trace {
|
||||
case 'node.started': {
|
||||
if (!nodeId) break
|
||||
const n = node(nodeId)
|
||||
if (typeof d.label === 'string' && d.label && n.label !== d.label) n.label = d.label
|
||||
const open = openIndex(n.segments, 'run')
|
||||
const lastSeg = n.segments[n.segments.length - 1]
|
||||
if (open >= 0) {
|
||||
@@ -1012,7 +1033,8 @@ export function project(t: Trace, at: number): Projection {
|
||||
toolsRunning: state === 'running' ? m?.[5] ?? 0 : 0,
|
||||
}
|
||||
}
|
||||
if (view.state === 'done' || view.state === 'skipped') nodesDone += 1
|
||||
// 分母是工作流里的节点数:子工作流里的步骤算在子工作流那一步里(isSubStep),不另算完成数
|
||||
if ((view.state === 'done' || view.state === 'skipped') && !isSubStep(n.id)) nodesDone += 1
|
||||
nodes[n.id] = view
|
||||
}
|
||||
|
||||
@@ -1039,7 +1061,7 @@ export function project(t: Trace, at: number): Projection {
|
||||
|
||||
return {
|
||||
phase, elapsedMs, activeMs, waitMs, nodesDone,
|
||||
nodesTotal: t.nodesTotal ?? Object.keys(t.nodes).length,
|
||||
nodesTotal: t.nodesTotal ?? Object.keys(t.nodes).filter((id) => !isSubStep(id)).length,
|
||||
parallelNow, ...usage, nodes,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user