mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-31 03:47:12 +08:00
checkpoint
This commit is contained in:
-3
@@ -110,7 +110,6 @@ describe('AgentGroup browser takeover', () => {
|
||||
agentLabel: 'Browser Agent',
|
||||
items: [tool('success'), browserTakeover(reason)],
|
||||
isStreaming: true,
|
||||
isCurrentSection: true,
|
||||
isLaneOpen: true,
|
||||
})
|
||||
)
|
||||
@@ -184,7 +183,6 @@ describe('AgentGroup browser takeover', () => {
|
||||
agentLabel: 'Browser Agent',
|
||||
items: [takeover],
|
||||
isStreaming: true,
|
||||
isCurrentSection: true,
|
||||
isLaneOpen: true,
|
||||
})
|
||||
)
|
||||
@@ -206,7 +204,6 @@ describe('AgentGroup browser takeover', () => {
|
||||
agentLabel: 'Browser Agent',
|
||||
items: [completedTakeover],
|
||||
isStreaming: true,
|
||||
isCurrentSection: true,
|
||||
isLaneOpen: true,
|
||||
})
|
||||
)
|
||||
|
||||
+12
-15
@@ -37,8 +37,6 @@ interface AgentGroupProps {
|
||||
items: AgentGroupItem[]
|
||||
isDelegating?: boolean
|
||||
isStreaming?: boolean
|
||||
/** This group is the latest section in its parent sequence (drives collapse). */
|
||||
isCurrentSection?: boolean
|
||||
/** The subagent lane is still open (no subagent_end yet) — i.e. actively running. */
|
||||
isLaneOpen?: boolean
|
||||
}
|
||||
@@ -110,7 +108,6 @@ export function AgentGroup({
|
||||
items,
|
||||
isDelegating = false,
|
||||
isStreaming = false,
|
||||
isCurrentSection = false,
|
||||
isLaneOpen = false,
|
||||
}: AgentGroupProps) {
|
||||
const AgentIcon = getAgentIcon(agentName)
|
||||
@@ -123,17 +120,18 @@ export function AgentGroup({
|
||||
const isWorking =
|
||||
!activeBrowserTakeover && ((isDelegating && !resolved) || (isStreaming && isLaneOpen))
|
||||
|
||||
// Expand while the turn is live and any of: the lane is open (the subagent is
|
||||
// actively running), this is the current/latest section, or there is unresolved
|
||||
// work. A finished group stays open until the NEXT section starts (it is no
|
||||
// longer the latest), instead of collapsing the instant its own work resolves.
|
||||
// Keying "still running" off the lane-open signal (not `resolved` alone) avoids
|
||||
// a collapse/reopen flicker on parallel siblings: a subagent's tools all
|
||||
// momentarily read "done" in the gap between its last search and its `respond`
|
||||
// ("Gathering thoughts") tool, transiently flipping `resolved` true; the open
|
||||
// lane bridges that gap so the row never collapses mid-run. The turn ending
|
||||
// (isStreaming false) collapses everything; a manual toggle pins the choice.
|
||||
const autoExpanded = isStreaming && (isCurrentSection || isLaneOpen || !resolved)
|
||||
// Expand while the turn is live and the subagent is still working: the lane
|
||||
// is open, or there is unresolved work. When the lane closes and the work
|
||||
// resolves the group collapses — with parallel subagents, finished siblings
|
||||
// fold away while the still-running ones stay open, instead of every group
|
||||
// lingering expanded until the next section starts. Keying "still running"
|
||||
// off the lane-open signal (not `resolved` alone) avoids a collapse/reopen
|
||||
// flicker mid-run: a subagent's tools all momentarily read "done" in the gap
|
||||
// between its last search and its `respond` ("Gathering thoughts") tool,
|
||||
// transiently flipping `resolved` true; the open lane bridges that gap. The
|
||||
// turn ending (isStreaming false) collapses everything; a manual toggle pins
|
||||
// the choice.
|
||||
const autoExpanded = isStreaming && (isLaneOpen || !resolved)
|
||||
const [manualExpanded, setManualExpanded] = useState<boolean | null>(null)
|
||||
const [expandedTakeoverId, setExpandedTakeoverId] = useState<string | null>(null)
|
||||
// An outstanding permission prompt overrides a manual collapse: the turn
|
||||
@@ -219,7 +217,6 @@ export function AgentGroup({
|
||||
items={item.group.items}
|
||||
isDelegating={item.group.isDelegating}
|
||||
isStreaming={isStreaming}
|
||||
isCurrentSection={idx === items.length - 1}
|
||||
isLaneOpen={item.group.isOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
-1
@@ -957,7 +957,6 @@ function MessageContentInner({
|
||||
items={segment.items}
|
||||
isDelegating={segment.isDelegating}
|
||||
isStreaming={isStreaming}
|
||||
isCurrentSection={i === segments.length - 1}
|
||||
isLaneOpen={segment.isOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -556,3 +556,93 @@ describe('reduceEvent — span-start owner reconciliation', () => {
|
||||
expect((lane as AgentNode).agentId).toBe('workflow')
|
||||
})
|
||||
})
|
||||
|
||||
describe('reduceEvent — span end settles stale lane tools', () => {
|
||||
const laneScope = { lane: 'subagent', spanId: 'S1', parentToolCallId: 'd1' } as Scope
|
||||
|
||||
it('marks still-running tools success when their lane ends cleanly', () => {
|
||||
const model = apply([
|
||||
envelope(
|
||||
1,
|
||||
'span',
|
||||
{ kind: 'subagent', event: 'start', agent: 'browser', data: { tool_call_id: 'd1' } },
|
||||
laneScope
|
||||
),
|
||||
toolCall(2, 'click-1', 'browser_click', laneScope),
|
||||
// No result for click-1 — dropped/reordered past the lane end.
|
||||
envelope(
|
||||
3,
|
||||
'span',
|
||||
{ kind: 'subagent', event: 'end', agent: 'browser', data: {} },
|
||||
laneScope
|
||||
),
|
||||
])
|
||||
|
||||
const click = model.nodes.get('click-1')
|
||||
if (click?.kind !== 'tool') throw new Error('expected tool node')
|
||||
expect(click.status).toBe('success')
|
||||
|
||||
const laneId = model.agentBySpanId.get('S1')
|
||||
const lane = laneId ? model.nodes.get(laneId) : undefined
|
||||
if (lane?.kind !== 'agent') throw new Error('expected agent lane')
|
||||
expect(lane.status).toBe('success')
|
||||
})
|
||||
|
||||
it('marks still-running tools error when the lane ends with an error', () => {
|
||||
const model = apply([
|
||||
envelope(
|
||||
1,
|
||||
'span',
|
||||
{ kind: 'subagent', event: 'start', agent: 'browser', data: { tool_call_id: 'd1' } },
|
||||
laneScope
|
||||
),
|
||||
toolCall(2, 'click-1', 'browser_click', laneScope),
|
||||
envelope(
|
||||
3,
|
||||
'span',
|
||||
{ kind: 'subagent', event: 'end', agent: 'browser', data: { error: 'boom' } },
|
||||
laneScope
|
||||
),
|
||||
])
|
||||
|
||||
const click = model.nodes.get('click-1')
|
||||
if (click?.kind !== 'tool') throw new Error('expected tool node')
|
||||
expect(click.status).toBe('error')
|
||||
})
|
||||
|
||||
it('leaves settled tools alone and lets a late result overwrite the settle', () => {
|
||||
const model = apply([
|
||||
envelope(
|
||||
1,
|
||||
'span',
|
||||
{ kind: 'subagent', event: 'start', agent: 'browser', data: { tool_call_id: 'd1' } },
|
||||
laneScope
|
||||
),
|
||||
toolCall(2, 'click-1', 'browser_click', laneScope),
|
||||
envelope(
|
||||
3,
|
||||
'span',
|
||||
{ kind: 'subagent', event: 'end', agent: 'browser', data: {} },
|
||||
laneScope
|
||||
),
|
||||
// Late result arrives after the settle — it must win.
|
||||
envelope(
|
||||
4,
|
||||
'tool',
|
||||
{
|
||||
phase: 'result',
|
||||
toolCallId: 'click-1',
|
||||
toolName: 'browser_click',
|
||||
success: false,
|
||||
error: 'nope',
|
||||
},
|
||||
laneScope
|
||||
),
|
||||
])
|
||||
|
||||
const click = model.nodes.get('click-1')
|
||||
if (click?.kind !== 'tool') throw new Error('expected tool node')
|
||||
expect(click.status).toBe('error')
|
||||
expect(click.result?.error).toBe('nope')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -611,10 +611,25 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve
|
||||
if (data?.pending === true) break
|
||||
breakLane(model, resolvedSpanId, tsMs)
|
||||
const node = model.nodes.get(resolvedSpanId)
|
||||
const spanErrored = Boolean(data && asString(data.error))
|
||||
if (node && node.kind === 'agent' && !isNodeTerminal(node.status)) {
|
||||
node.status = data && asString(data.error) ? 'error' : 'success'
|
||||
node.status = spanErrored ? 'error' : 'success'
|
||||
node.endSeq = seq
|
||||
}
|
||||
// The lane is over: settle any tool row still `running` in it (its
|
||||
// result was dropped or reordered past the end). Left open, the row
|
||||
// pins the whole group expanded and shimmering for the rest of the
|
||||
// turn even though the subagent already returned. A late result event
|
||||
// still corrects this — applyToolResult overwrites unconditionally.
|
||||
for (const id of model.order) {
|
||||
const stale = model.nodes.get(id)
|
||||
if (stale?.kind === 'tool' && stale.spanId === resolvedSpanId) {
|
||||
if (stale.status === 'running') {
|
||||
stale.status = spanErrored ? 'error' : 'success'
|
||||
stale.streamingArgs = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user