Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
@@ -293,6 +293,7 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
|
|||||||
for msg_idx, m in enumerate(s.messages):
|
for msg_idx, m in enumerate(s.messages):
|
||||||
if m.get('role') == 'assistant':
|
if m.get('role') == 'assistant':
|
||||||
c = m.get('content', '')
|
c = m.get('content', '')
|
||||||
|
# Anthropic format: content is a list with type=tool_use blocks
|
||||||
if isinstance(c, list):
|
if isinstance(c, list):
|
||||||
for p in c:
|
for p in c:
|
||||||
if isinstance(p, dict) and p.get('type') == 'tool_use':
|
if isinstance(p, dict) and p.get('type') == 'tool_use':
|
||||||
@@ -300,6 +301,22 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
|
|||||||
pending_names[tid] = p.get('name', '')
|
pending_names[tid] = p.get('name', '')
|
||||||
pending_args[tid] = p.get('input', {})
|
pending_args[tid] = p.get('input', {})
|
||||||
pending_asst_idx[tid] = msg_idx
|
pending_asst_idx[tid] = msg_idx
|
||||||
|
# OpenAI format: tool_calls as top-level field on the message
|
||||||
|
for tc in m.get('tool_calls', []):
|
||||||
|
if not isinstance(tc, dict):
|
||||||
|
continue
|
||||||
|
tid = tc.get('id', '') or tc.get('call_id', '')
|
||||||
|
fn = tc.get('function', {})
|
||||||
|
name = fn.get('name', '')
|
||||||
|
try:
|
||||||
|
import json as _j
|
||||||
|
args = _j.loads(fn.get('arguments', '{}') or '{}')
|
||||||
|
except Exception:
|
||||||
|
args = {}
|
||||||
|
if tid and name:
|
||||||
|
pending_names[tid] = name
|
||||||
|
pending_args[tid] = args
|
||||||
|
pending_asst_idx[tid] = msg_idx
|
||||||
elif m.get('role') == 'tool':
|
elif m.get('role') == 'tool':
|
||||||
tid = m.get('tool_call_id') or m.get('tool_use_id', '')
|
tid = m.get('tool_call_id') or m.get('tool_use_id', '')
|
||||||
name = pending_names.get(tid, '')
|
name = pending_names.get(tid, '')
|
||||||
|
|||||||
31
static/ui.js
31
static/ui.js
@@ -477,12 +477,17 @@ function renderMessages(){
|
|||||||
});
|
});
|
||||||
$('emptyState').style.display=vis.length?'none':'';
|
$('emptyState').style.display=vis.length?'none':'';
|
||||||
inner.innerHTML='';
|
inner.innerHTML='';
|
||||||
// Track original indices (in S.messages) so truncate knows the cut point
|
// Track original indices (in S.messages) so truncate knows the cut point.
|
||||||
|
// Also include assistant messages that have tool_calls (OpenAI format) or
|
||||||
|
// tool_use content (Anthropic format) even when their text is empty — these
|
||||||
|
// rows serve as DOM anchors for tool card insertion on page reload.
|
||||||
const visWithIdx=[];
|
const visWithIdx=[];
|
||||||
let rawIdx=0;
|
let rawIdx=0;
|
||||||
for(const m of S.messages){
|
for(const m of S.messages){
|
||||||
if(!m||!m.role||m.role==='tool'){rawIdx++;continue;}
|
if(!m||!m.role||m.role==='tool'){rawIdx++;continue;}
|
||||||
if(msgContent(m)||m.attachments?.length) visWithIdx.push({m,rawIdx});
|
const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
|
||||||
|
const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
|
||||||
|
if(msgContent(m)||m.attachments?.length||(m.role==='assistant'&&(hasTc||hasTu))) visWithIdx.push({m,rawIdx});
|
||||||
rawIdx++;
|
rawIdx++;
|
||||||
}
|
}
|
||||||
for(let vi=0;vi<visWithIdx.length;vi++){
|
for(let vi=0;vi<visWithIdx.length;vi++){
|
||||||
@@ -521,6 +526,28 @@ function renderMessages(){
|
|||||||
// Insert settled tool call cards (history view only).
|
// Insert settled tool call cards (history view only).
|
||||||
// During live streaming, tool cards are rendered in #liveToolCards by the
|
// During live streaming, tool cards are rendered in #liveToolCards by the
|
||||||
// tool SSE handler and never mixed into the message list until done fires.
|
// tool SSE handler and never mixed into the message list until done fires.
|
||||||
|
//
|
||||||
|
// Fallback: if S.toolCalls is empty (sessions that predate session-level tool
|
||||||
|
// tracking, or runs that didn't go through the normal streaming path), build
|
||||||
|
// a display list from per-message tool_calls (OpenAI format) stored in each
|
||||||
|
// assistant message. This covers the reload case described in issue #140.
|
||||||
|
if(!S.busy && (!S.toolCalls||!S.toolCalls.length)){
|
||||||
|
const derived=[];
|
||||||
|
S.messages.forEach((m,rawIdx)=>{
|
||||||
|
if(m.role!=='assistant') return;
|
||||||
|
(m.tool_calls||[]).forEach(tc=>{
|
||||||
|
if(!tc||typeof tc!=='object') return;
|
||||||
|
const fn=tc.function||{};
|
||||||
|
const name=fn.name||tc.name||'tool';
|
||||||
|
let args={};
|
||||||
|
try{ args=JSON.parse(fn.arguments||'{}'); }catch(e){}
|
||||||
|
let argsSnap={};
|
||||||
|
Object.keys(args).slice(0,4).forEach(k=>{ const v=String(args[k]); argsSnap[k]=v.slice(0,120)+(v.length>120?'...':''); });
|
||||||
|
derived.push({name,snippet:'',tid:tc.id||tc.call_id||'',assistant_msg_idx:rawIdx,args:argsSnap,done:true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if(derived.length) S.toolCalls=derived;
|
||||||
|
}
|
||||||
if(!S.busy && S.toolCalls && S.toolCalls.length){
|
if(!S.busy && S.toolCalls && S.toolCalls.length){
|
||||||
inner.querySelectorAll('.tool-card-row').forEach(el=>el.remove());
|
inner.querySelectorAll('.tool-card-row').forEach(el=>el.remove());
|
||||||
const byAssistant = {};
|
const byAssistant = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user