fix: stabilize web observe trace views (#849)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -38,12 +38,15 @@ if(view==='files'){
|
||||
const samples=readJsonl('samples.jsonl');
|
||||
const control=readJsonl('control.jsonl');
|
||||
const manifest=readJson('manifest.json')||{};
|
||||
const report=readJson('analysis/report.json')||{};
|
||||
function unique(values){return Array.from(new Set(values.filter(Boolean)));}
|
||||
function numOrNull(value){const n=Number(value); return Number.isFinite(n)?n:null}
|
||||
function promptIndexOf(value){const n=Number(value); return Number.isInteger(n)&&n>0?n:null}
|
||||
function tsMs(value){const ms=Date.parse(String(value||'')); return Number.isFinite(ms)?ms:null}
|
||||
function promptCommands(){
|
||||
const map=new Map();
|
||||
for(const item of control){
|
||||
if((item.type!=='sendPrompt'&&item.type!=='steer')||(item.phase!=='started'&&item.phase!=='completed'&&item.phase!=='failed')) continue;
|
||||
if(item.type!=='sendPrompt'||(item.phase!=='started'&&item.phase!=='completed'&&item.phase!=='failed')) continue;
|
||||
const id=item.commandId||item.seq||String(map.size+1);
|
||||
const existing=map.get(id)||{};
|
||||
map.set(id,{...existing,...item,input:{...(existing.input||{}),...(item.input||{})},firstTs:existing.firstTs||item.ts,lastTs:item.ts});
|
||||
@@ -59,14 +62,95 @@ function controlsFor(index,prompts){
|
||||
return control.filter((item)=>{const ms=tsMs(item.ts); return ms!==null&&ms>=start&&ms<end});
|
||||
}
|
||||
function firstTraceId(text){const match=String(text||'').match(/\\btrc_[A-Za-z0-9_-]+\\b/u); return match?match[0]:null}
|
||||
function traceIdsFromSamples(items){
|
||||
const ids=[];
|
||||
function itemTraceId(item){return item?.traceId||firstTraceId(textOf(item))||null}
|
||||
function entryGroups(sample){
|
||||
return [
|
||||
...(Array.isArray(sample.turns)?sample.turns.map((item)=>({group:'turn',item})):[]),
|
||||
...(Array.isArray(sample.traceRows)?sample.traceRows.map((item)=>({group:'traceRow',item})):[]),
|
||||
...(Array.isArray(sample.messages)?sample.messages.map((item)=>({group:'message',item})):[]),
|
||||
];
|
||||
}
|
||||
function traceIdStats(items){
|
||||
const map=new Map();
|
||||
for(const sample of items){
|
||||
for(const turn of Array.isArray(sample.turns)?sample.turns:[]) ids.push(turn.traceId);
|
||||
for(const row of Array.isArray(sample.traceRows)?sample.traceRows:[]) ids.push(row.traceId||firstTraceId(textOf(row)));
|
||||
for(const message of Array.isArray(sample.messages)?sample.messages:[]) ids.push(message.traceId||firstTraceId(textOf(message)));
|
||||
const seq=numOrNull(sample.seq);
|
||||
for(const entry of entryGroups(sample)){
|
||||
const id=itemTraceId(entry.item); if(!id) continue;
|
||||
const current=map.get(id)||{traceId:id,count:0,firstSeq:null,lastSeq:null,terminalCount:0};
|
||||
current.count+=1;
|
||||
current.firstSeq=current.firstSeq===null?seq:seq===null?current.firstSeq:Math.min(current.firstSeq,seq);
|
||||
current.lastSeq=current.lastSeq===null?seq:seq===null?current.lastSeq:Math.max(current.lastSeq,seq);
|
||||
if(terminalText(textOf(entry.item))||String(entry.item?.status||'').match(/complete|fail|cancel|terminal/iu)) current.terminalCount+=1;
|
||||
map.set(id,current);
|
||||
}
|
||||
}
|
||||
return unique(ids);
|
||||
return Array.from(map.values()).sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||String(a.traceId).localeCompare(String(b.traceId)));
|
||||
}
|
||||
function traceIdsFromSamples(items){return traceIdStats(items).map((item)=>item.traceId)}
|
||||
function collectReportTurnColumns(value,out=[]){
|
||||
if(!value||typeof value!=='object') return out;
|
||||
if(Array.isArray(value)){for(const item of value) collectReportTurnColumns(item,out); return out;}
|
||||
if(Array.isArray(value.turnColumns)){
|
||||
for(const item of value.turnColumns){
|
||||
if(!item||typeof item!=='object') continue;
|
||||
const traceId=String(item.traceId||'').trim();
|
||||
if(!traceId) continue;
|
||||
out.push({...item,traceId,promptIndex:promptIndexOf(item.promptIndex),lastPromptIndex:promptIndexOf(item.lastPromptIndex),firstSeq:numOrNull(item.firstSeq),lastSeq:numOrNull(item.lastSeq)});
|
||||
}
|
||||
}
|
||||
for(const key of ['sampleMetrics','analysis','analysisWindow','recentWindow','windows','recent','archive','summary']){
|
||||
if(value[key]&&value[key]!==value) collectReportTurnColumns(value[key],out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const reportTurnColumns=(()=>{const seen=new Set(); const rows=[]; for(const item of collectReportTurnColumns(report)){const key=[item.traceId,item.promptIndex,item.lastPromptIndex,item.firstSeq,item.lastSeq,item.source,item.pageRole,item.messageId].join('|'); if(seen.has(key)) continue; seen.add(key); rows.push(item);} return rows.sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||(a.promptIndex??9999)-(b.promptIndex??9999));})();
|
||||
function columnOverlapsSegment(column,segment){
|
||||
if(segment.length===0) return true;
|
||||
const first=numOrNull(segment[0]?.seq); const last=numOrNull(segment[segment.length-1]?.seq);
|
||||
if(first===null||last===null||column.firstSeq===null||column.lastSeq===null) return true;
|
||||
return column.lastSeq>=first&&column.firstSeq<=last;
|
||||
}
|
||||
function reportColumnsForPrompt(index,segment){
|
||||
const promptIndex=index+1;
|
||||
const segmentIds=new Set(traceIdsFromSamples(segment));
|
||||
return reportTurnColumns
|
||||
.filter((column)=>(column.promptIndex===promptIndex||column.lastPromptIndex===promptIndex)&&columnOverlapsSegment(column,segment))
|
||||
.filter((column)=>segmentIds.size===0||segmentIds.has(column.traceId)||column.source==='turn')
|
||||
.sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||(a.lastSeq??0)-(b.lastSeq??0)||String(a.label||'').localeCompare(String(b.label||'')));
|
||||
}
|
||||
function choosePrimaryTraceId(index,segment){
|
||||
const columns=reportColumnsForPrompt(index,segment);
|
||||
const promptIndex=index+1;
|
||||
const promptColumns=columns.filter((column)=>column.promptIndex===promptIndex);
|
||||
const preferred=promptColumns.length>0?promptColumns:columns;
|
||||
if(preferred.length>0) return {traceId:preferred[0].traceId,columns};
|
||||
const stats=traceIdStats(segment).sort((a,b)=>(b.lastSeq??0)-(a.lastSeq??0)||(b.firstSeq??0)-(a.firstSeq??0));
|
||||
return {traceId:stats[0]?.traceId||null,columns:[]};
|
||||
}
|
||||
function samplesForTrace(items,traceId,maxSeq=null){
|
||||
if(!traceId) return items;
|
||||
return items.filter((sample)=>{
|
||||
const seq=numOrNull(sample.seq);
|
||||
return (maxSeq===null||seq===null||seq<=maxSeq)&&traceIdsFromSamples([sample]).includes(traceId);
|
||||
});
|
||||
}
|
||||
function latestSampleForTrace(items,traceId,maxSeq=null){
|
||||
return samplesForTrace(items,traceId,maxSeq).slice(-1)[0]||null;
|
||||
}
|
||||
function traceEntries(items,traceId){
|
||||
const entries=[];
|
||||
for(const sample of items){
|
||||
for(const entry of entryGroups(sample)){
|
||||
const text=textOf(entry.item);
|
||||
const id=itemTraceId(entry.item);
|
||||
if(!traceId||id===traceId||text.includes(traceId)) entries.push({...entry,sample,text});
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
function textsFor(items,traceId=null){
|
||||
const entries=traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})));
|
||||
return entries.length>0?entries.map((entry)=>entry.text):[];
|
||||
}
|
||||
function parseElapsed(text){
|
||||
const values=[]; const raw=String(text||'');
|
||||
@@ -81,10 +165,11 @@ function parseElapsed(text){
|
||||
function parseRecent(text){const m=String(text||'').match(/最近\\s*(\\d+)\\s*(秒|分钟|分|小时)前/u); if(!m)return null; const n=Number(m[1]); return m[2]==='小时'?n*3600:m[2]==='秒'?n:n*60}
|
||||
function fmtDuration(seconds){if(seconds===null||seconds===undefined||!Number.isFinite(Number(seconds)))return '-'; const value=Math.max(0,Math.round(Number(seconds))); const h=Math.floor(value/3600), m=Math.floor((value%3600)/60), s=value%60; return (h>0?String(h).padStart(2,'0')+':':'')+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')}
|
||||
function terminalText(text){return /轮次完成|轮次失败|轮次取消|已记录|final response|sealed final response|turn completed|turn failed|turn canceled|completed|failed|canceled|cancelled|terminal/iu.test(String(text||''))}
|
||||
function statusFor(items){
|
||||
const texts=items.flatMap((sample)=>[...(Array.isArray(sample.turns)?sample.turns:[]),...(Array.isArray(sample.traceRows)?sample.traceRows:[]),...(Array.isArray(sample.messages)?sample.messages:[])].map(textOf));
|
||||
function statusFor(items,traceId=null){
|
||||
const texts=textsFor(items,traceId);
|
||||
const joined=texts.join('\\n');
|
||||
const lastTurn=[].concat(...items.map((sample)=>Array.isArray(sample.turns)?sample.turns:[])).slice(-1)[0]||null;
|
||||
const matchedEntries=traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})));
|
||||
const lastTurn=matchedEntries.filter((entry)=>entry.group==='turn').slice(-1)[0]?.item||null;
|
||||
const raw=String(lastTurn?.status||'').toLowerCase();
|
||||
if(/cancel/iu.test(joined)||raw.includes('cancel'))return 'canceled';
|
||||
if(/failed|error|agentrun:error/iu.test(joined)||raw.includes('fail'))return 'failed';
|
||||
@@ -101,15 +186,39 @@ function userMessageFor(items,prompt){
|
||||
}
|
||||
return {preview:short(prompt?.input?.textPreview||'',90)||'(hash only)',textHash:hash,textBytes:prompt?.input?.textBytes??null};
|
||||
}
|
||||
function finalResponseFor(items,traceId){
|
||||
const turns=[].concat(...items.map((sample)=>Array.isArray(sample.turns)?sample.turns:[]));
|
||||
const candidates=(traceId?turns.filter((turn)=>turn.traceId===traceId):turns).slice().reverse();
|
||||
for(const turn of candidates){
|
||||
const status=String(turn.status||'').toLowerCase(); const text=textOf(turn);
|
||||
if(status.includes('running')&&!terminalText(text))continue;
|
||||
if(!terminalText(text)&&!status.match(/complete|fail|cancel|block/u))continue;
|
||||
function cleanFinalResponseText(text){
|
||||
const raw=String(text||'').trim();
|
||||
if(!raw) return '';
|
||||
if(/Code Agent\\s*耗时[\\s\\S]*运行记录/iu.test(raw)) return '';
|
||||
if(/^sent\\s+当前 AgentRun 请求已取消/u.test(raw)) return '';
|
||||
if(/^(admitted|run|ok|error)\\s+/iu.test(raw)) return '';
|
||||
if(/^(AgentRun|Code Agent turn was durably admitted|commandExecution|runner-job|command -v|node --version|npm --version|cd |mkdir -p|npm install|npx )/iu.test(raw)) return '';
|
||||
if(/(?:^|\\s)轮次(完成|失败|取消)(?总耗时/iu.test(raw)) return '';
|
||||
if(/^(轮次完成|轮次失败|轮次取消|已记录)$/u.test(raw.replace(/\\s+/gu,' ').trim())) return '';
|
||||
return raw;
|
||||
}
|
||||
function entryRole(entry){return String(entry.item?.role||entry.item?.authorRole||entry.item?.messageRole||entry.item?.speaker||'').toLowerCase()}
|
||||
function sameAsUserPrompt(entry,text,user){
|
||||
if(!user) return false;
|
||||
if(user.textHash&&entry.item?.textHash===user.textHash) return true;
|
||||
const userPrefix=short(user.preview||'',48).replace(/\\s+/gu,' ').trim().slice(0,32);
|
||||
const textPrefix=String(text||'').replace(/\\s+/gu,' ').trim().slice(0,32);
|
||||
return userPrefix.length>=18&&textPrefix===userPrefix;
|
||||
}
|
||||
function finalResponseFor(items,traceId,user=null){
|
||||
const candidates=(traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})))).slice().reverse();
|
||||
for(const entry of candidates){
|
||||
const role=entryRole(entry);
|
||||
const entryStatus=String(entry.item?.status||'');
|
||||
if(entry.group==='message'&&!/assistant|agent|system/u.test(role)) continue;
|
||||
if(/用户|user/iu.test(role)||/用户|user/iu.test(entryStatus)) continue;
|
||||
const status=String(entry.item?.status||'').toLowerCase(); const text=cleanFinalResponseText(entry.text);
|
||||
if(!text) continue;
|
||||
if(/^第\\d+轮\\/\\d+[::]?\\s*请/u.test(text)||/^请(执行|帮|回答|修复|调查|用|不要|直接)/u.test(text)) continue;
|
||||
if(sameAsUserPrompt(entry,text,user)) continue;
|
||||
if(status.includes('running')&&!terminalText(entry.text)&&entry.group==='turn')continue;
|
||||
const preview=short(text,180);
|
||||
return preview?{preview,textHash:turn.textHash||sha(text),textBytes:turn.textBytes??Buffer.byteLength(text),empty:false}:{preview:'(空内容)',textHash:null,textBytes:0,empty:true};
|
||||
if(preview)return {preview,textHash:entry.item?.textHash||sha(text),textBytes:entry.item?.textBytes??Buffer.byteLength(text),empty:false};
|
||||
}
|
||||
return {preview:'(空内容)',textHash:null,textBytes:0,empty:true};
|
||||
}
|
||||
@@ -117,14 +226,16 @@ function turnSummaryRows(){
|
||||
const prompts=promptCommands();
|
||||
if(prompts.length===0){
|
||||
const allTraceIds=traceIdsFromSamples(samples);
|
||||
return [{round:0,commandId:null,commandType:null,userPreview:'(无 sendPrompt/steer control 记录)',userHash:null,userBytes:null,traceId:allTraceIds[0]||null,status:statusFor(samples),elapsedSeconds:null,recentUpdateSeconds:null,marks:'-',firstSeq:samples[0]?.seq??null,lastSeq:samples[samples.length-1]?.seq??null,lastTs:samples[samples.length-1]?.ts??null,finalResponse:finalResponseFor(samples,allTraceIds[0]||null),valuesRedacted:true}];
|
||||
return [{round:0,commandId:null,commandType:null,userPreview:'(无 sendPrompt control 记录)',userHash:null,userBytes:null,traceId:allTraceIds[0]||null,status:statusFor(samples,allTraceIds[0]||null),elapsedSeconds:null,recentUpdateSeconds:null,marks:'-',firstSeq:samples[0]?.seq??null,lastSeq:samples[samples.length-1]?.seq??null,lastTs:samples[samples.length-1]?.ts??null,finalResponse:finalResponseFor(samples,allTraceIds[0]||null),valuesRedacted:true}];
|
||||
}
|
||||
return prompts.map((prompt,index)=>{
|
||||
const segment=segmentFor(index,prompts); const segmentControls=controlsFor(index,prompts); const ids=traceIdsFromSamples(segment); const traceId=ids[0]||null; const user=userMessageFor(segment,prompt);
|
||||
const texts=segment.flatMap((sample)=>[...(Array.isArray(sample.turns)?sample.turns:[]),...(Array.isArray(sample.traceRows)?sample.traceRows:[]),...(Array.isArray(sample.messages)?sample.messages:[])].map(textOf));
|
||||
const segment=segmentFor(index,prompts); const segmentControls=controlsFor(index,prompts); const selected=choosePrimaryTraceId(index,segment); const traceId=selected.traceId; const user=userMessageFor(segment,prompt);
|
||||
const primaryColumn=selected.columns.find((column)=>column.traceId===traceId)||null;
|
||||
const traceSample=latestSampleForTrace(segment,traceId,primaryColumn?.lastSeq??null);
|
||||
const texts=textsFor(segment,traceId);
|
||||
const elapsedValues=texts.map(parseElapsed).filter((value)=>value!==null); const recentValues=texts.map(parseRecent).filter((value)=>value!==null);
|
||||
const marks=unique([prompt.type==='steer'?'steer':null,...segmentControls.map((item)=>item.type==='cancel'?'cancel':item.type==='steer'?'steer':null)]).join(',')||'-';
|
||||
return {round:index+1,commandId:prompt.commandId||null,commandType:prompt.type||null,userPreview:user.preview,userHash:user.textHash,userBytes:user.textBytes,traceId,status:statusFor(segment),elapsedSeconds:elapsedValues.length?Math.max(...elapsedValues):null,recentUpdateSeconds:recentValues.length?Math.max(...recentValues):null,marks,firstSeq:segment[0]?.seq??null,lastSeq:segment[segment.length-1]?.seq??null,lastTs:segment[segment.length-1]?.ts??null,finalResponse:finalResponseFor(segment,traceId),valuesRedacted:true};
|
||||
const marks=unique(segmentControls.map((item)=>item.type==='cancel'?'cancel':item.type==='steer'?'steer':null)).join(',')||'-';
|
||||
return {round:index+1,commandId:prompt.commandId||null,commandType:prompt.type||null,userPreview:user.preview,userHash:user.textHash,userBytes:user.textBytes,traceId,traceIds:unique([traceId,...selected.columns.map((column)=>column.traceId),...traceIdsFromSamples(segment)]).slice(0,8),status:statusFor(segment,traceId),elapsedSeconds:elapsedValues.length?Math.max(...elapsedValues):null,recentUpdateSeconds:recentValues.length?Math.max(...recentValues):null,marks,firstSeq:segment[0]?.seq??primaryColumn?.firstSeq??null,lastSeq:traceSample?.seq??primaryColumn?.lastSeq??segment[segment.length-1]?.seq??null,lastTs:traceSample?.ts??segment[segment.length-1]?.ts??null,finalResponse:finalResponseFor(segment,traceId,user),valuesRedacted:true};
|
||||
});
|
||||
}
|
||||
function pad(value,width){const text=short(value,width); return text+Array(Math.max(0,width-text.length)+1).join(' ')}
|
||||
@@ -137,18 +248,19 @@ function renderTurnSummary(rows){
|
||||
function selectSample(rows){
|
||||
if(requestedSampleSeq!==null){const exact=samples.find((sample)=>Number(sample.seq)===requestedSampleSeq); if(exact)return exact;}
|
||||
if(requestedTimestamp){const target=tsMs(requestedTimestamp); if(target!==null){const before=samples.filter((sample)=>{const ms=tsMs(sample.ts); return ms!==null&&ms<=target}).slice(-1)[0]; if(before)return before;}}
|
||||
if(requestedTurn!==null&&rows[requestedTurn-1]?.traceId){const row=rows[requestedTurn-1]; const byTrace=latestSampleForTrace(samples,row.traceId,numOrNull(row.lastSeq)); if(byTrace)return byTrace;}
|
||||
if(requestedTurn!==null&&rows[requestedTurn-1]?.lastSeq!==null){const byTurn=samples.find((sample)=>Number(sample.seq)===Number(rows[requestedTurn-1].lastSeq)); if(byTurn)return byTurn;}
|
||||
if(requestedTraceId){const byTrace=samples.filter((sample)=>traceIdsFromSamples([sample]).includes(requestedTraceId)).slice(-1)[0]; if(byTrace)return byTrace;}
|
||||
return samples[samples.length-1]||null;
|
||||
}
|
||||
function renderTraceFrame(sample,rows){
|
||||
if(!sample)return {ok:false,renderedText:'TRACE_FRAME_BLOCKER\\nno sample matched --sample-seq/--timestamp/--trace-id/--turn',blocker:'sample-not-found'};
|
||||
const sampleTraceIds=traceIdsFromSamples([sample]); const traceId=requestedTraceId||sampleTraceIds[0]||rows.find((row)=>row.lastSeq===sample.seq)?.traceId||null;
|
||||
const sampleTraceIds=traceIdsFromSamples([sample]); const traceId=requestedTraceId||(requestedTurn!==null?rows[requestedTurn-1]?.traceId:null)||rows.find((row)=>row.lastSeq===sample.seq)?.traceId||sampleTraceIds[0]||null;
|
||||
const traceRows=(Array.isArray(sample.traceRows)?sample.traceRows:[]).filter((row)=>!traceId||row.traceId===traceId||!row.traceId||textOf(row).includes(traceId));
|
||||
const turns=(Array.isArray(sample.turns)?sample.turns:[]).filter((turn)=>!traceId||turn.traceId===traceId||textOf(turn).includes(traceId));
|
||||
const texts=[...turns.map(textOf),...traceRows.map(textOf)];
|
||||
const elapsed=Math.max(-1,...texts.map(parseElapsed).filter((value)=>value!==null)); const recent=Math.max(-1,...texts.map(parseRecent).filter((value)=>value!==null));
|
||||
const status=statusFor([sample]); const finalResponse=finalResponseFor([sample],traceId);
|
||||
const status=statusFor([sample],traceId); const finalResponse=finalResponseFor([sample],traceId);
|
||||
const visibleTraceRows=traceRows.slice(-24);
|
||||
const rowLines=visibleTraceRows.map((row,index)=>{const text=textOf(row); return short((row.status?row.status+' ':'')+text,180)||('row#'+index+' '+(row.textHash||'-'));});
|
||||
if(traceRows.length>visibleTraceRows.length) rowLines.unshift('(已省略 '+(traceRows.length-visibleTraceRows.length)+' 条较早 trace rows;需要原始数据请看 samples.jsonl)');
|
||||
|
||||
Reference in New Issue
Block a user