mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
feat(native-chat): show each subagent's model, current tool and result in the chat (#41766)
* feat(native-chat): show each subagent's model, current tool and result in the roster Subagent roster rows now carry the child's role, model, reasoning effort, the tool it is running and the last thing it said, so a row says what the subagent is doing and, once done, what it found. Codex children take model and effort from the spawn call; Claude children take the model from their own messages. A hover card shows the longer text. * feat(native-chat): name subagent models and roles the way people say them Show Opus 5.5 · High instead of claude-opus-5-5 · high, reusing the model-id label and effort picker copy, and General purpose instead of general-purpose. * fix(native-chat): read Codex subagent model and effort from its started activity Current Codex announces a spawn only through subAgentActivity, which carries the model and reasoning effort the child resolved to. * fix(native-chat): use the hover card's own spacing variant for the subagent preview * fix(native-chat): show a subagent's reply as plain text in its roster row and preview Its last message is markdown; the row now drops backticks, emphasis, list and heading marks and joins lines into one readable line.
This commit is contained in:
@@ -6,7 +6,12 @@ import {
|
||||
subagentStateLabel
|
||||
} from '../../../../../src/shared/native-chat/transcript/native-chat-subagent-group-header'
|
||||
import { sayMobileNativeChatSubagentGroup as say } from './native-chat-subagent-group-copy'
|
||||
import { sayMobileNativeChatSessionOption as sayOption } from '../input/native-chat-session-option-copy'
|
||||
import { normalizeSubagentState } from '../../../../../src/shared/native-chat/transcript/native-chat-subagent-summary'
|
||||
import {
|
||||
subagentEntryDetail,
|
||||
subagentEntryRunsOn
|
||||
} from '../../../../../src/shared/native-chat/transcript/native-chat-subagent-entry-detail'
|
||||
import { formatNativeChatDuration } from '../../../../../src/shared/native-chat/transcript/native-chat-turn-status'
|
||||
import type {
|
||||
NativeChatSubagentGroupBlock,
|
||||
@@ -113,21 +118,31 @@ export function MobileNativeChatSubagentGroup({
|
||||
{open
|
||||
? block.agents.map((agent) => {
|
||||
const state = normalizeSubagentState(agent.state)
|
||||
const detail = [subagentEntryRunsOn(agent, sayOption), subagentEntryDetail(agent)]
|
||||
.filter((part): part is string => part !== null)
|
||||
.join(' · ')
|
||||
return (
|
||||
<View key={agent.id} testID="subagent-group-entry" style={styles.entry}>
|
||||
<View style={[styles.dot, stateDotStyle(state)]} />
|
||||
<Text
|
||||
style={[styles.entryLabel, state === 'idle' && styles.entryLabelIdle]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{agent.label}
|
||||
</Text>
|
||||
<Text style={styles.entryState} numberOfLines={1}>
|
||||
{subagentStateLabel(state, 1, 1, say)}
|
||||
{typeof agent.tokens === 'number'
|
||||
? ` · ${formatSubagentTokens(agent.tokens)}`
|
||||
: null}
|
||||
</Text>
|
||||
<View key={agent.id} testID="subagent-group-entry" style={styles.entryBlock}>
|
||||
<View style={styles.entry}>
|
||||
<View style={[styles.dot, stateDotStyle(state)]} />
|
||||
<Text
|
||||
style={[styles.entryLabel, state === 'idle' && styles.entryLabelIdle]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{agent.label}
|
||||
</Text>
|
||||
<Text style={styles.entryState} numberOfLines={1}>
|
||||
{subagentStateLabel(state, 1, 1, say)}
|
||||
{typeof agent.tokens === 'number'
|
||||
? ` · ${formatSubagentTokens(agent.tokens)}`
|
||||
: null}
|
||||
</Text>
|
||||
</View>
|
||||
{detail ? (
|
||||
<Text style={styles.entryDetail} numberOfLines={2}>
|
||||
{detail}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
@@ -179,12 +194,20 @@ const styles = themedStyleSheet(() => ({
|
||||
caretOpen: {
|
||||
transform: [{ rotate: '90deg' }]
|
||||
},
|
||||
entryBlock: {
|
||||
paddingLeft: spacing.lg + spacing.xs
|
||||
},
|
||||
entry: {
|
||||
minHeight: 24,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
paddingLeft: spacing.lg + spacing.xs
|
||||
gap: spacing.sm
|
||||
},
|
||||
// Aligns under the label, past the 6pt dot and its gap.
|
||||
entryDetail: {
|
||||
paddingLeft: 6 + spacing.sm,
|
||||
color: colors.textMuted,
|
||||
fontSize: typography.metaSize
|
||||
},
|
||||
entryLabel: {
|
||||
flexShrink: 1,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { claudeChildOperation } from './claude-child-work-evidence'
|
||||
|
||||
describe('Claude child operation evidence', () => {
|
||||
it("carries the model the subagent's own message ran on", () => {
|
||||
const frame = {
|
||||
type: 'assistant',
|
||||
session_id: 'provider-1',
|
||||
uuid: 'uuid-1',
|
||||
parent_tool_use_id: 'toolu_spawn',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
model: 'claude-sonnet-5-5',
|
||||
content: [{ type: 'tool_use', id: 'toolu_read', name: 'Read', input: {} }]
|
||||
}
|
||||
}
|
||||
const evidence = claudeChildOperation(
|
||||
frame,
|
||||
() => ({ agentId: 'agent-1', openTool: null }),
|
||||
100
|
||||
)
|
||||
expect(evidence).toEqual([
|
||||
expect.objectContaining({ type: 'operation', childId: 'agent-1', model: 'claude-sonnet-5-5' })
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -152,6 +152,7 @@ export function claudeChildOperation(
|
||||
}
|
||||
const { agentId, openTool } = activityOf(parentRef)
|
||||
const input = openTool ? deriveToolInputPreview(openTool.name, openTool.input) : undefined
|
||||
const model = claudeChildMessageModel(message)
|
||||
return [
|
||||
{
|
||||
type: 'operation',
|
||||
@@ -159,11 +160,18 @@ export function claudeChildOperation(
|
||||
childId: agentId,
|
||||
operation: openTool
|
||||
? { toolName: openTool.name, ...(input ? { input } : {}), basis: 'open', observedAt }
|
||||
: null
|
||||
: null,
|
||||
...(model ? { model } : {})
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/** Claude names the model on each assistant message; `<synthetic>` marks Claude Code's own repair text. */
|
||||
function claudeChildMessageModel(frame: Record<string, unknown>): string | undefined {
|
||||
const model = frame.type === 'assistant' ? claudeRecord(frame.message)?.model : undefined
|
||||
return typeof model === 'string' && model.trim() && model !== '<synthetic>' ? model : undefined
|
||||
}
|
||||
|
||||
/** Waiting requires both a written journal card and a registry request still awaiting an answer. */
|
||||
function claudeWaitingChildIds(
|
||||
session: Pick<ClaudeSession, 'prompts' | 'translator'>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { NativeChatSubagentState } from '../../../../shared/native-chat/native-chat-types'
|
||||
import { readCodexSubagentAnnouncements } from './codex-subagent-activity'
|
||||
import { readCodexSubagentAnnouncements, type CodexChildLaunch } from './codex-subagent-activity'
|
||||
import { codexChildTurnState, type CodexSubagentExecutions } from './codex-subagent-executions'
|
||||
import { readRecord } from '../../app-server/codex-item-field-readers'
|
||||
import '../items/codex-structured-item-translation'
|
||||
@@ -11,6 +11,7 @@ export type CodexAnnouncedChild = {
|
||||
label: string | null
|
||||
/** Set only by spawn announcements, whose reporting thread is the spawner. */
|
||||
spawnerThreadId: string | undefined
|
||||
launch?: CodexChildLaunch
|
||||
}
|
||||
|
||||
export type CodexBackgroundTaskFrame =
|
||||
@@ -78,7 +79,8 @@ export function readCodexBackgroundTaskFrame(
|
||||
agentThreadId: announcement.agentThreadId,
|
||||
label: announcement.label,
|
||||
// Only a spawn names the spawner: other announcements ride whichever agent acted.
|
||||
spawnerThreadId: announcement.spawned ? event.threadId : undefined
|
||||
spawnerThreadId: announcement.spawned ? event.threadId : undefined,
|
||||
launch: announcement.launch
|
||||
})
|
||||
)
|
||||
return children.length > 0 ? { kind: 'subagents', children } : null
|
||||
@@ -92,7 +94,7 @@ export function observeCodexExecutionFrame(
|
||||
): void {
|
||||
if (frame?.kind === 'subagents') {
|
||||
for (const child of frame.children) {
|
||||
executions.register(child.agentThreadId, child.label, child.spawnerThreadId)
|
||||
executions.register(child.agentThreadId, child.label, child.spawnerThreadId, child.launch)
|
||||
}
|
||||
} else if (frame?.kind === 'thread-closed') {
|
||||
executions.closeThread(frame.threadId)
|
||||
|
||||
@@ -266,6 +266,8 @@ export class CodexChildWorkEvidence {
|
||||
residency: 'background',
|
||||
state: facts.waiting ? 'waiting' : 'working',
|
||||
...(child.label ? { description: child.label } : {}),
|
||||
...(child.model ? { model: child.model } : {}),
|
||||
...(child.effort ? { effort: child.effort } : {}),
|
||||
...(facts.totalTokens !== undefined ? { totalTokens: facts.totalTokens } : {}),
|
||||
...(lastMessage ? { lastMessage } : {}),
|
||||
...(spawner && spawner !== this.primaryThreadId ? { ownerId: spawner } : {}),
|
||||
|
||||
+18
@@ -198,6 +198,24 @@ describe('Codex structured child-work producer', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('records the model and reasoning effort Codex resolved for the child', async () => {
|
||||
const { send, records } = await producer()
|
||||
send(turn('turn/started', THREAD_ID, 'p1'))
|
||||
send(
|
||||
item('item/completed', THREAD_ID, 'p1', {
|
||||
type: 'subAgentActivity',
|
||||
id: 'spawn-reviewer',
|
||||
kind: 'started',
|
||||
agentThreadId: REVIEWER,
|
||||
agentPath: '/root/inspect_folder',
|
||||
model: 'gpt-6.1-sol',
|
||||
reasoning_effort: 'xhigh'
|
||||
})
|
||||
)
|
||||
send(turn('turn/started', REVIEWER, 'r1'))
|
||||
expect(records()).toEqual([expect.objectContaining({ model: 'gpt-6.1-sol', effort: 'xhigh' })])
|
||||
})
|
||||
|
||||
it('ends an approval left unanswered when its turn ends: Codex never ran the command', async () => {
|
||||
const { adapter, codex, send, records, recordedLiveness, display, byDescription } =
|
||||
await producer()
|
||||
|
||||
@@ -17,6 +17,8 @@ export type CodexSubagentActivity = {
|
||||
kind: string
|
||||
agentThreadId: string
|
||||
agentPath: string | null
|
||||
/** What the child resolved to at creation, reported on `started`. */
|
||||
launch: CodexChildLaunch
|
||||
}
|
||||
|
||||
function nonEmptyString(value: unknown): string | null {
|
||||
@@ -40,10 +42,18 @@ export function readCodexSubagentActivity(item: CodexThreadItem): CodexSubagentA
|
||||
return {
|
||||
kind: nonEmptyString(item.kind) ?? '',
|
||||
agentThreadId,
|
||||
agentPath: nonEmptyString(item.agentPath)
|
||||
agentPath: nonEmptyString(item.agentPath),
|
||||
launch: codexChildLaunch(
|
||||
nonEmptyString(item.model),
|
||||
nonEmptyString(item.reasoningEffort) ?? nonEmptyString(item.reasoning_effort)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function codexChildLaunch(model: string | null, effort: string | null): CodexChildLaunch {
|
||||
return { ...(model ? { model } : {}), ...(effort ? { effort } : {}) }
|
||||
}
|
||||
|
||||
const CODEX_ROOT_AGENT_SEGMENT = 'root'
|
||||
|
||||
/** Normalize root paths like labels; missing paths remain children to avoid losing a real spawn. */
|
||||
@@ -61,8 +71,12 @@ export type CodexSubagentAnnouncement = {
|
||||
label: string | null
|
||||
/** Only spawn items establish the carrying thread as the parent. */
|
||||
spawned: boolean
|
||||
launch?: CodexChildLaunch
|
||||
}
|
||||
|
||||
/** What a spawn asked the child to run on. */
|
||||
export type CodexChildLaunch = { model?: string; effort?: string }
|
||||
|
||||
/** Collab references recover compacted-out spawns; even failed spawns can name a runnable child. */
|
||||
export function readCodexSubagentAnnouncements(
|
||||
item: CodexThreadItem,
|
||||
@@ -80,7 +94,8 @@ function readAnnouncements(item: CodexThreadItem): CodexSubagentAnnouncement[] {
|
||||
{
|
||||
agentThreadId: activity.agentThreadId,
|
||||
label: codexSubagentLabel(activity),
|
||||
spawned: activity.kind === 'started'
|
||||
spawned: activity.kind === 'started',
|
||||
launch: activity.launch
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -91,7 +106,8 @@ function readAnnouncements(item: CodexThreadItem): CodexSubagentAnnouncement[] {
|
||||
const spawned = call.tool === 'spawnAgent'
|
||||
// Follow-up prompts are messages, not the child's task label.
|
||||
const label = spawned ? codexCollabHelperLabel(call.prompt) : null
|
||||
return call.helperThreadIds.map((agentThreadId) => ({ agentThreadId, label, spawned }))
|
||||
const launch = spawned ? codexChildLaunch(call.model, call.reasoningEffort) : undefined
|
||||
return call.helperThreadIds.map((agentThreadId) => ({ agentThreadId, label, spawned, launch }))
|
||||
}
|
||||
|
||||
export type CodexThreadTokenTotal = { threadId: string; totalTokens: number }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NativeChatSubagentState } from '../../../../shared/native-chat/native-chat-types'
|
||||
import type { CodexChildLaunch } from './codex-subagent-activity'
|
||||
import {
|
||||
MAX_SUBAGENT_FIELD_CHARS,
|
||||
canReplaceSubagentState
|
||||
@@ -20,6 +21,8 @@ export type CodexExecutionChild = {
|
||||
label: string | null
|
||||
/** Spawn announcements arrive on the parent's thread. */
|
||||
spawnerThreadId: string | null
|
||||
model: string | null
|
||||
effort: string | null
|
||||
execution: CodexChildExecution | null
|
||||
turnOrdinals: Map<string, number>
|
||||
turnCount: number
|
||||
@@ -37,12 +40,15 @@ export class CodexSubagentExecutions {
|
||||
register(
|
||||
agentThreadId: string,
|
||||
label: string | null,
|
||||
spawnerThreadId?: string
|
||||
spawnerThreadId?: string,
|
||||
launch?: CodexChildLaunch
|
||||
): CodexExecutionChild {
|
||||
const child = this.child(agentThreadId)
|
||||
const previousLabel = child.label
|
||||
const previous = [child.label, child.model, child.effort]
|
||||
// Keep the original spawner across duplicate announcement frames.
|
||||
child.spawnerThreadId ??= spawnerThreadId ?? null
|
||||
child.model ??= launch?.model ?? null
|
||||
child.effort ??= launch?.effort ?? null
|
||||
child.registered = true
|
||||
// Retain one overflow unit so the journal can append its per-row truncation marker.
|
||||
child.label ??=
|
||||
@@ -51,7 +57,9 @@ export class CodexSubagentExecutions {
|
||||
.replace(/\s+/g, ' ')
|
||||
.slice(0, MAX_SUBAGENT_FIELD_CHARS + 1) || null
|
||||
this.updateWorking(child)
|
||||
if (previousLabel !== child.label) {
|
||||
if (
|
||||
[child.label, child.model, child.effort].some((value, index) => value !== previous[index])
|
||||
) {
|
||||
this.changed.add(agentThreadId)
|
||||
}
|
||||
return child
|
||||
@@ -210,6 +218,8 @@ export class CodexSubagentExecutions {
|
||||
observationOrder: this.children.size,
|
||||
registered: false,
|
||||
label: null,
|
||||
model: null,
|
||||
effort: null,
|
||||
spawnerThreadId: null,
|
||||
execution: null,
|
||||
turnOrdinals: new Map(),
|
||||
|
||||
@@ -20,6 +20,9 @@ export type CodexCollabAgentToolCall = {
|
||||
/** The receivers that are helpers: every one but those the call reports `notFound`. */
|
||||
helperThreadIds: string[]
|
||||
prompt: string | null
|
||||
/** What a spawn asked the child to run on. */
|
||||
model: string | null
|
||||
reasoningEffort: string | null
|
||||
/** Caller snapshots in receiverThreadIds order, excluding receivers with no snapshot. */
|
||||
states: { threadId: string; status: string | null; message: string | null }[]
|
||||
}
|
||||
@@ -53,6 +56,8 @@ export function readCodexCollabAgentToolCall(
|
||||
(threadId) => readString(readRecord(snapshots[threadId]), 'status') !== 'notFound'
|
||||
),
|
||||
prompt: readString(item, 'prompt'),
|
||||
model: readString(item, 'model'),
|
||||
reasoningEffort: readString(item, 'reasoningEffort'),
|
||||
states
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,18 @@ export function subagentJournalBody(block: NativeChatSubagentBlock): AgentJourna
|
||||
label: boundSubagentLabel(block.label),
|
||||
groupId: boundSubagentEntryId(block.groupId),
|
||||
...(block.parentId !== undefined ? { parentId: boundSubagentEntryId(block.parentId) } : {}),
|
||||
...(block.run !== undefined ? { run: boundSubagentEntryId(block.run) } : {})
|
||||
...(block.run !== undefined ? { run: boundSubagentEntryId(block.run) } : {}),
|
||||
...boundOptionalLabels(block),
|
||||
...(block.operation !== undefined
|
||||
? {
|
||||
operation: {
|
||||
toolName: boundSubagentLabel(block.operation.toolName),
|
||||
...(block.operation.input !== undefined
|
||||
? { input: boundSubagentLabel(block.operation.input) }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
return {
|
||||
kind: 'message',
|
||||
@@ -35,6 +46,18 @@ export function subagentJournalBody(block: NativeChatSubagentBlock): AgentJourna
|
||||
}
|
||||
}
|
||||
|
||||
const OPTIONAL_LABELS = ['agentType', 'model', 'effort', 'lastMessage'] as const
|
||||
|
||||
function boundOptionalLabels(
|
||||
block: NativeChatSubagentBlock
|
||||
): Pick<NativeChatSubagentBlock, (typeof OPTIONAL_LABELS)[number]> {
|
||||
return Object.fromEntries(
|
||||
OPTIONAL_LABELS.flatMap((key) =>
|
||||
block[key] === undefined ? [] : [[key, boundSubagentLabel(block[key])]]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function journaledSubagent(
|
||||
journal: { itemBody: (itemId: string) => AgentJournalItemBody | null },
|
||||
id: string
|
||||
|
||||
+19
-1
@@ -181,7 +181,7 @@ export class StructuredAgentSessionSubagentRows {
|
||||
}
|
||||
}
|
||||
|
||||
/** Retain saved name, tokens and parent when a later run lacks them. */
|
||||
/** Retain saved name, tokens, parent, role and model when a later run lacks them. */
|
||||
function subagentRowBlock(
|
||||
child: AgentChildWorkRowView,
|
||||
id: string,
|
||||
@@ -201,6 +201,12 @@ function subagentRowBlock(
|
||||
: saved.run === run
|
||||
? (saved.startedAt ?? child.firstObservedAt)
|
||||
: child.observedAt
|
||||
const agentType = child.agentType ?? saved?.agentType
|
||||
const model = child.model ?? saved?.model
|
||||
const effort = child.effort ?? saved?.effort
|
||||
const operation = state === 'working' ? child.operation : undefined
|
||||
// A new run's silence must not repeat the previous run's result.
|
||||
const lastMessage = child.lastMessage ?? (saved?.run === run ? saved.lastMessage : undefined)
|
||||
return {
|
||||
type: 'subagent',
|
||||
id,
|
||||
@@ -209,6 +215,18 @@ function subagentRowBlock(
|
||||
...(tokens !== undefined ? { tokens } : {}),
|
||||
startedAt,
|
||||
...(state !== 'working' && child.settledAt !== undefined ? { settledAt: child.settledAt } : {}),
|
||||
...(agentType !== undefined ? { agentType } : {}),
|
||||
...(model !== undefined ? { model } : {}),
|
||||
...(effort !== undefined ? { effort } : {}),
|
||||
...(operation !== undefined
|
||||
? {
|
||||
operation: {
|
||||
toolName: operation.toolName,
|
||||
...(operation.input !== undefined ? { input: operation.input } : {})
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
...(lastMessage !== undefined ? { lastMessage } : {}),
|
||||
groupId: saved?.groupId ?? (scope.kind === 'turn' ? scope.turnItemId : THREAD_GROUP),
|
||||
...(parent !== undefined ? { parentId: parent } : {}),
|
||||
run,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { defineFixture } from '../../../component-fixtures/component-fixture'
|
||||
import type { NativeChatSubagentGroupBlock } from '../../../../../shared/native-chat/native-chat-types'
|
||||
import { NativeChatSubagentRun } from './NativeChatSubagentRun'
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const block: NativeChatSubagentGroupBlock = {
|
||||
type: 'subagent-group',
|
||||
groupId: 'turn-1',
|
||||
agents: [
|
||||
{
|
||||
id: 'a1',
|
||||
label: 'Audit auth flow',
|
||||
state: 'working',
|
||||
tokens: 31_200,
|
||||
startedAt: now - 42_000,
|
||||
agentType: 'general-purpose',
|
||||
model: 'claude-opus-5-5',
|
||||
effort: 'high',
|
||||
operation: { toolName: 'Read', input: 'src/auth/session.ts' }
|
||||
},
|
||||
{
|
||||
id: 'a2',
|
||||
label: 'Fix flaky login test',
|
||||
state: 'working',
|
||||
tokens: 48_900,
|
||||
startedAt: now - 63_000,
|
||||
model: 'gpt-5.5',
|
||||
effort: 'xhigh',
|
||||
operation: { toolName: 'Bash', input: 'pnpm test login.spec.ts' }
|
||||
},
|
||||
{
|
||||
id: 'a3',
|
||||
label: 'Map token refresh',
|
||||
state: 'completed',
|
||||
tokens: 8_100,
|
||||
startedAt: now - 80_000,
|
||||
settledAt: now - 62_000,
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
lastMessage:
|
||||
'Refresh happens in **two** places:\n- `middleware.ts` retries once on 401\n- the client refreshes on a timer 60 s before expiry'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function Roster() {
|
||||
return (
|
||||
<div className="native-chat-appearance p-4 pt-36">
|
||||
<NativeChatSubagentRun block={block} open onSetOpen={() => {}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const enriched = defineFixture({
|
||||
width: 560,
|
||||
render: () => <Roster />
|
||||
})
|
||||
|
||||
export const hovered = defineFixture({
|
||||
width: 560,
|
||||
render: () => <Roster />,
|
||||
interact: async (frame) => {
|
||||
frame
|
||||
.querySelector('[data-native-chat-subagent-entry="a1"] > div')
|
||||
?.dispatchEvent(new PointerEvent('pointerover', { bubbles: true, pointerType: 'mouse' }))
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Bot, ChevronRight } from 'lucide-react'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
|
||||
import { useNow } from '@/hooks/use-now'
|
||||
import {
|
||||
formatSubagentTokens,
|
||||
@@ -8,6 +9,12 @@ import {
|
||||
subagentStateLabel
|
||||
} from '../../../../../shared/native-chat/transcript/native-chat-subagent-group-header'
|
||||
import { normalizeSubagentState } from '../../../../../shared/native-chat/transcript/native-chat-subagent-summary'
|
||||
import {
|
||||
subagentEntryDetail,
|
||||
subagentEntryRunsOn,
|
||||
subagentMessageText,
|
||||
subagentRoleLabel
|
||||
} from '../../../../../shared/native-chat/transcript/native-chat-subagent-entry-detail'
|
||||
import type {
|
||||
NativeChatSubagentEntry,
|
||||
NativeChatSubagentGroupBlock,
|
||||
@@ -16,6 +23,7 @@ import type {
|
||||
import { formatNativeChatDuration } from '../../../../../shared/native-chat/transcript/native-chat-turn-status'
|
||||
import { nativeChatSubagentEntryRuns } from './native-chat-subagent-sections'
|
||||
import { sayNativeChatSubagentGroupTranslated as say } from './native-chat-subagent-group-words-text'
|
||||
import { sayNativeChatSessionOptionTranslated as sayOption } from '../session/options/controls/native-chat-session-option-labels-text'
|
||||
|
||||
const STATE_DOT_CLASS: Record<NativeChatSubagentState, string> = {
|
||||
working: 'bg-foreground/70',
|
||||
@@ -87,47 +95,65 @@ export function NativeChatSubagentEntries({
|
||||
{agents.map((agent) => {
|
||||
const state = normalizeSubagentState(agent.state)
|
||||
const sectionOpen = sections?.get(agent.id)
|
||||
const detail = subagentEntryDetail(agent)
|
||||
const runsOn = subagentEntryRunsOn(agent, sayOption)
|
||||
const entry = (
|
||||
<>
|
||||
<StatusDot state={state} pulsing={state === 'working'} />
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 truncate font-sans text-sm',
|
||||
state === 'idle' ? 'text-chat-foreground-faint' : 'text-chat-foreground'
|
||||
)}
|
||||
>
|
||||
{agent.label}
|
||||
<span className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<StatusDot state={state} pulsing={state === 'working'} />
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 truncate font-sans text-sm',
|
||||
state === 'idle' ? 'text-chat-foreground-faint' : 'text-chat-foreground'
|
||||
)}
|
||||
>
|
||||
{agent.label}
|
||||
</span>
|
||||
{runsOn ? (
|
||||
<span className="min-w-0 shrink truncate rounded-full border border-border px-1.5 font-sans text-3xs text-chat-foreground-faint">
|
||||
{runsOn}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="ml-auto shrink-0 font-sans text-xs tabular-nums text-chat-foreground-faint">
|
||||
{subagentStateLabel(state, 1, 1, say)}
|
||||
{typeof agent.tokens === 'number'
|
||||
? ` · ${formatSubagentTokens(agent.tokens)}`
|
||||
: null}
|
||||
</span>
|
||||
</span>
|
||||
<span className="ml-auto shrink-0 font-sans text-xs tabular-nums text-chat-foreground-faint">
|
||||
{subagentStateLabel(state, 1, 1, say)}
|
||||
{typeof agent.tokens === 'number' ? ` · ${formatSubagentTokens(agent.tokens)}` : null}
|
||||
</span>
|
||||
</>
|
||||
{detail ? (
|
||||
<span className="truncate pl-3 font-sans text-xs text-chat-foreground-faint">
|
||||
{detail}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
// Named so a reveal from the agents strip can bring this entry into view.
|
||||
<li key={agent.id} data-native-chat-subagent-entry={agent.id}>
|
||||
{sectionOpen === undefined ? (
|
||||
<div className="flex items-center gap-1.5 py-0.5">{entry}</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSetSectionOpen?.(agent.id, !sectionOpen)}
|
||||
aria-expanded={sectionOpen}
|
||||
className="group/subagent-entry flex w-full items-center gap-1.5 rounded-md py-0.5 text-left hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
||||
>
|
||||
{entry}
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'size-3.5 shrink-0 text-chat-foreground-faint transition-all motion-reduce:transition-none',
|
||||
sectionOpen
|
||||
? 'rotate-90 opacity-100'
|
||||
: 'opacity-0 group-hover/subagent-entry:opacity-100'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<SubagentEntryPreview agent={agent} state={state} detail={detail} runsOn={runsOn}>
|
||||
{sectionOpen === undefined ? (
|
||||
<div className="flex items-start gap-1.5 py-0.5">{entry}</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSetSectionOpen?.(agent.id, !sectionOpen)}
|
||||
aria-expanded={sectionOpen}
|
||||
className="group/subagent-entry flex w-full items-start gap-1.5 rounded-md py-0.5 text-left hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
||||
>
|
||||
{entry}
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'mt-1 size-3.5 shrink-0 text-chat-foreground-faint transition-all motion-reduce:transition-none',
|
||||
sectionOpen
|
||||
? 'rotate-90 opacity-100'
|
||||
: 'opacity-0 group-hover/subagent-entry:opacity-100'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</SubagentEntryPreview>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
@@ -135,6 +161,57 @@ export function NativeChatSubagentEntries({
|
||||
)
|
||||
}
|
||||
|
||||
/** Hover preview: role, model, timing and the longer text the row truncates. */
|
||||
function SubagentEntryPreview({
|
||||
agent,
|
||||
state,
|
||||
detail,
|
||||
runsOn,
|
||||
children
|
||||
}: {
|
||||
agent: NativeChatSubagentEntry
|
||||
state: NativeChatSubagentState
|
||||
detail: string | null
|
||||
runsOn: string | null
|
||||
children: React.ReactElement
|
||||
}): React.JSX.Element {
|
||||
const role = [subagentRoleLabel(agent.agentType), runsOn].filter(Boolean).join(' · ')
|
||||
const text =
|
||||
state === 'working' && agent.operation
|
||||
? detail
|
||||
: subagentMessageText(agent.lastMessage ?? '') || null
|
||||
return (
|
||||
<HoverCard openDelay={300} closeDelay={100}>
|
||||
<HoverCardTrigger asChild>{children}</HoverCardTrigger>
|
||||
<HoverCardContent side="top" align="start" variant="workspace-details" className="w-72">
|
||||
<div className="space-y-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">{agent.label}</div>
|
||||
{role ? <div className="truncate text-muted-foreground">{role}</div> : null}
|
||||
<div className="tabular-nums text-muted-foreground">
|
||||
{subagentStateLabel(state, 1, 1, say)}
|
||||
{agent.startedAt !== undefined ? (
|
||||
<>
|
||||
{' · '}
|
||||
<SubagentElapsed
|
||||
startedAt={agent.startedAt}
|
||||
settledAt={agent.settledAt ?? null}
|
||||
counting={state === 'working'}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{typeof agent.tokens === 'number' ? ` · ${formatSubagentTokens(agent.tokens)}` : null}
|
||||
</div>
|
||||
{text ? (
|
||||
<p className="line-clamp-6 whitespace-pre-wrap break-words pt-1 text-foreground">
|
||||
{text}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
/** Trust journal child states: children outlive their spawning turn, and only host/provider settlement proves lost contact. */
|
||||
export function NativeChatSubagentRun({
|
||||
block,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AgentSessionContextUsageSchema } from '../agent-session-context-usage-s
|
||||
import { AgentJournalThreadGoalStateSchema } from './agent-session-journal-thread-goal-schema'
|
||||
import { AgentSessionFailureFactSchema } from '../failures/agent-session-failure-fact-schema'
|
||||
import { knownTags, openDiscriminatedUnion } from './agent-session-journal-open-union'
|
||||
import { SubagentEntry } from './agent-session-journal-subagent-schema'
|
||||
import type {
|
||||
AgentJournalItemBody,
|
||||
AgentJournalMessageItem,
|
||||
@@ -43,16 +44,6 @@ const ProviderCallId = z
|
||||
.string()
|
||||
.refine((value) => value.trim().length > 0, 'callId must contain a non-whitespace character')
|
||||
|
||||
/** Keep lifecycle strings open so newer states do not make persisted rows malformed. */
|
||||
const SubagentEntry = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
state: z.string().min(1),
|
||||
tokens: z.number().optional(),
|
||||
startedAt: z.number().optional(),
|
||||
settledAt: z.number().optional()
|
||||
})
|
||||
|
||||
/** Skip unknown block types, but reject malformed payloads of known types. */
|
||||
const Block = openDiscriminatedUnion(
|
||||
z.discriminatedUnion('type', [
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
/** Keep lifecycle strings open so newer states do not make persisted rows malformed. */
|
||||
export const SubagentEntry = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
state: z.string().min(1),
|
||||
tokens: z.number().optional(),
|
||||
startedAt: z.number().optional(),
|
||||
settledAt: z.number().optional(),
|
||||
agentType: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
effort: z.string().optional(),
|
||||
operation: z.object({ toolName: z.string(), input: z.string().optional() }).optional(),
|
||||
lastMessage: z.string().optional()
|
||||
})
|
||||
@@ -180,6 +180,7 @@ function parseObservationFacts(
|
||||
),
|
||||
agentType: normalizeChildWorkText(request.agentType, AGENT_CHILD_WORK_LABEL_MAX_LENGTH),
|
||||
model: normalizeChildWorkText(request.model, AGENT_CHILD_WORK_LABEL_MAX_LENGTH),
|
||||
effort: normalizeChildWorkText(request.effort, AGENT_CHILD_WORK_LABEL_MAX_LENGTH),
|
||||
totalTokens: isChildWorkTokenCount(request.totalTokens) ? request.totalTokens : undefined,
|
||||
providerTiming: timing ?? undefined,
|
||||
parentChildWorkId: isAgentChildWorkOwner(request.parentChildWorkId, host.childWorkId)
|
||||
@@ -206,6 +207,7 @@ function mergeObservationFacts(
|
||||
description: said.description ?? prior?.description,
|
||||
agentType: said.agentType ?? prior?.agentType,
|
||||
model: said.model ?? prior?.model,
|
||||
effort: said.effort ?? prior?.effort,
|
||||
residency: said.residency ?? prior?.residency,
|
||||
// Cumulative, so a late or duplicate frame never shrinks it.
|
||||
totalTokens:
|
||||
|
||||
@@ -41,6 +41,7 @@ export type AgentChildWorkObservationFields = {
|
||||
description?: string
|
||||
agentType?: string
|
||||
model?: string
|
||||
effort?: string
|
||||
totalTokens?: number
|
||||
providerTiming?: AgentChildWorkProviderTiming
|
||||
parentChildWorkId?: AgentChildWorkId
|
||||
|
||||
@@ -162,6 +162,7 @@ export function parseAgentChildWorkInput(value: unknown): AgentChildWorkInput |
|
||||
'description',
|
||||
'agentType',
|
||||
'model',
|
||||
'effort',
|
||||
'totalTokens',
|
||||
'providerTiming',
|
||||
'parentChildWorkId',
|
||||
@@ -204,7 +205,8 @@ export function parseAgentChildWorkInput(value: unknown): AgentChildWorkInput |
|
||||
name: parseOptionalLabel(value.name),
|
||||
description: parseOptionalLabel(value.description, AGENT_CHILD_WORK_DESCRIPTION_MAX_LENGTH),
|
||||
agentType: parseOptionalLabel(value.agentType),
|
||||
model: parseOptionalLabel(value.model)
|
||||
model: parseOptionalLabel(value.model),
|
||||
effort: parseOptionalLabel(value.effort)
|
||||
}
|
||||
const activity = parseAgentChildWorkActivityFields(value, {
|
||||
childWorkId: value.childWorkId,
|
||||
@@ -268,6 +270,7 @@ export function parseAgentChildWorkInput(value: unknown): AgentChildWorkInput |
|
||||
...(labels.description ? { description: labels.description } : {}),
|
||||
...(labels.agentType ? { agentType: labels.agentType } : {}),
|
||||
...(labels.model ? { model: labels.model } : {}),
|
||||
...(labels.effort ? { effort: labels.effort } : {}),
|
||||
...(typeof value.totalTokens === 'number' ? { totalTokens: value.totalTokens } : {}),
|
||||
...(timing ? { providerTiming: timing } : {}),
|
||||
...activity,
|
||||
|
||||
@@ -101,6 +101,8 @@ function liveFields(
|
||||
...(child.name !== undefined ? { name: child.name } : {}),
|
||||
...(child.description !== undefined ? { description: child.description } : {}),
|
||||
...(child.agentType !== undefined ? { agentType: child.agentType } : {}),
|
||||
...(child.model !== undefined ? { model: child.model } : {}),
|
||||
...(child.effort !== undefined ? { effort: child.effort } : {}),
|
||||
...(child.totalTokens !== undefined ? { totalTokens: child.totalTokens } : {}),
|
||||
...(owner !== undefined ? { parentChildWorkId: owner } : {}),
|
||||
residency: child.residency,
|
||||
|
||||
@@ -27,6 +27,8 @@ export type AgentChildWorkLiveObservation = {
|
||||
name?: string
|
||||
description?: string
|
||||
agentType?: string
|
||||
model?: string
|
||||
effort?: string
|
||||
totalTokens?: number
|
||||
/** `null`: the operation that was open has ended. Absent: this edge says nothing about it. */
|
||||
operation?: AgentChildWorkOperation | null
|
||||
@@ -52,6 +54,8 @@ export type AgentChildWorkOperationEvidence = {
|
||||
childId: string
|
||||
/** `null`: the call it had open has ended. */
|
||||
operation: AgentChildWorkOperation | null
|
||||
/** The model the child's own message ran on. */
|
||||
model?: string
|
||||
}
|
||||
|
||||
/** The child's own terminal frame. `unknown` is an ending whose status the provider did not say. */
|
||||
|
||||
@@ -70,7 +70,7 @@ function applyStopAcknowledged(
|
||||
}
|
||||
}
|
||||
|
||||
/** A live child's current operation, and nothing else about it. */
|
||||
/** A live child's current operation and the model it runs on, and nothing else about it. */
|
||||
function applyOperation(ctx: ReconcileContext, edge: AgentChildWorkOperationEvidence): void {
|
||||
const resolution = resolveAgentChildWorkHandle(
|
||||
ctx,
|
||||
@@ -93,7 +93,8 @@ function applyOperation(ctx: ReconcileContext, edge: AgentChildWorkOperationEvid
|
||||
residency: record.residency,
|
||||
state: record.state,
|
||||
stoppable: record.stoppable,
|
||||
operation: edge.operation
|
||||
operation: edge.operation,
|
||||
...(edge.model !== undefined ? { model: edge.model } : {})
|
||||
},
|
||||
edge.observedAt,
|
||||
false
|
||||
|
||||
@@ -87,6 +87,7 @@ function decodeView(value: unknown): AgentChildWorkView | null {
|
||||
const description = text(view.description)
|
||||
const agentType = text(view.agentType)
|
||||
const model = text(view.model)
|
||||
const effort = text(view.effort)
|
||||
const lastMessage = text(view.lastMessage)
|
||||
const parentChildWorkId = text(view.parentChildWorkId)
|
||||
const totalTokens = clock(view.totalTokens)
|
||||
@@ -97,6 +98,7 @@ function decodeView(value: unknown): AgentChildWorkView | null {
|
||||
...(description ? { description } : {}),
|
||||
...(agentType ? { agentType } : {}),
|
||||
...(model ? { model } : {}),
|
||||
...(effort ? { effort } : {}),
|
||||
...(lastMessage ? { lastMessage } : {}),
|
||||
...(parentChildWorkId ? { parentChildWorkId } : {}),
|
||||
kind: arm(KINDS, view.kind) ?? 'unknown',
|
||||
@@ -150,6 +152,7 @@ function viewsEqual(a: AgentChildWorkView, b: AgentChildWorkView, clockTolerance
|
||||
a.description === b.description &&
|
||||
a.agentType === b.agentType &&
|
||||
a.model === b.model &&
|
||||
a.effort === b.effort &&
|
||||
a.state === b.state &&
|
||||
a.membership === b.membership &&
|
||||
a.outcome === b.outcome &&
|
||||
|
||||
@@ -27,6 +27,7 @@ export type AgentChildWorkView = {
|
||||
description?: string
|
||||
agentType?: string
|
||||
model?: string
|
||||
effort?: string
|
||||
state: AgentChildWorkState
|
||||
membership: AgentChildWorkMembership
|
||||
outcome?: AgentChildWorkOutcome
|
||||
@@ -126,6 +127,7 @@ export function projectAgentChildWorkViews(
|
||||
...(record.description !== undefined ? { description: record.description } : {}),
|
||||
...(record.agentType !== undefined ? { agentType: record.agentType } : {}),
|
||||
...(record.model !== undefined ? { model: record.model } : {}),
|
||||
...(record.effort !== undefined ? { effort: record.effort } : {}),
|
||||
state: record.state,
|
||||
membership: record.membership,
|
||||
...(record.outcome !== undefined ? { outcome: record.outcome } : {}),
|
||||
|
||||
@@ -84,6 +84,8 @@ export type AgentChildWorkInput = {
|
||||
description?: string
|
||||
agentType?: string
|
||||
model?: string
|
||||
/** Reasoning effort, in the provider's own words (`high`, `medium`). */
|
||||
effort?: string
|
||||
totalTokens?: number
|
||||
providerTiming?: AgentChildWorkProviderTiming
|
||||
/** Invocation owner (spawner or launching agent); absence identifies the session lead. */
|
||||
|
||||
@@ -137,8 +137,19 @@ export type NativeChatSubagentEntry = {
|
||||
startedAt?: number
|
||||
/** Epoch ms the entry latched terminal. */
|
||||
settledAt?: number
|
||||
/** The child's role (Claude's `subagent_type`), when it has one. */
|
||||
agentType?: string
|
||||
model?: string
|
||||
/** Reasoning effort, in the provider's own words. */
|
||||
effort?: string
|
||||
/** The tool it is running now; only while it works. */
|
||||
operation?: NativeChatSubagentOperation
|
||||
/** The newest thing it said; once settled, its result. */
|
||||
lastMessage?: string
|
||||
}
|
||||
|
||||
export type NativeChatSubagentOperation = { toolName: string; input?: string }
|
||||
|
||||
/** Provider-neutral roster revised in place by Claude/Codex activity. */
|
||||
export type NativeChatSubagentGroupBlock = {
|
||||
type: 'subagent-group'
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// What a subagent roster entry says under its name, shared by desktop and mobile.
|
||||
|
||||
import { formatAgentToolPreviewLine } from '../../agent-status/presentation/agent-tool-preview'
|
||||
import { claudeListedModelLabel } from '../../claude-model-list-probe'
|
||||
import {
|
||||
nativeChatSessionChoiceShortLabel,
|
||||
type NativeChatSessionOptionSay
|
||||
} from '../input/native-chat-session-option-labels'
|
||||
import type { NativeChatSubagentEntry } from '../native-chat-types'
|
||||
import { normalizeSubagentState } from './native-chat-subagent-summary'
|
||||
|
||||
/** While it works: the tool it runs, else what it last said. Once settled: what it said last, its result. */
|
||||
export function subagentEntryDetail(entry: NativeChatSubagentEntry): string | null {
|
||||
const working = normalizeSubagentState(entry.state) === 'working'
|
||||
const operation =
|
||||
working && entry.operation
|
||||
? formatAgentToolPreviewLine(
|
||||
{ toolName: entry.operation.toolName, toolInput: entry.operation.input },
|
||||
false
|
||||
)
|
||||
: ''
|
||||
// Lines become separate clauses, so list items don't run together on one row.
|
||||
const text =
|
||||
operation ||
|
||||
subagentMessageText(entry.lastMessage ?? '')
|
||||
.replace(/:\s*\n\s*/g, ': ')
|
||||
.replace(/\s*\n\s*/g, ' · ')
|
||||
.replace(/\s+/g, ' ')
|
||||
return text || null
|
||||
}
|
||||
|
||||
/** What the subagent said, without the markdown marks a plain row or preview can't render. */
|
||||
export function subagentMessageText(markdown: string): string {
|
||||
return markdown
|
||||
.replace(/^\s*```.*$/gm, '')
|
||||
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
||||
.replace(/^\s{0,3}(?:#{1,6}\s+|>\s?|[-*+]\s+)/gm, '')
|
||||
.replace(/[`*~]|__/g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
/** `claude-opus-5-5` reads as `Opus 5.5`, `gpt-5.5` as `GPT-5.5`: the chat already names the provider. */
|
||||
export function subagentModelLabel(model: string): string {
|
||||
return claudeListedModelLabel(model.trim())
|
||||
.replace(/^Claude /, '')
|
||||
.replace(/^GPT (\d)/, 'GPT-$1')
|
||||
}
|
||||
|
||||
/** `general-purpose` reads as `General purpose`; Codex's bare `default` role says nothing. */
|
||||
export function subagentRoleLabel(agentType: string | undefined): string | null {
|
||||
const words = agentType?.replace(/[-_]+/g, ' ').trim()
|
||||
return words && words !== 'default' ? words.charAt(0).toUpperCase() + words.slice(1) : null
|
||||
}
|
||||
|
||||
/** `Opus 5.5 · High`, from whichever the provider reported. */
|
||||
export function subagentEntryRunsOn(
|
||||
entry: NativeChatSubagentEntry,
|
||||
say: NativeChatSessionOptionSay
|
||||
): string | null {
|
||||
const model = entry.model?.trim() ? subagentModelLabel(entry.model) : null
|
||||
const effort = entry.effort?.trim()
|
||||
? nativeChatSessionChoiceShortLabel({ value: entry.effort, label: entry.effort }, say, 'effort')
|
||||
: null
|
||||
const parts = [model, effort].filter((part): part is string => part !== null)
|
||||
return parts.length > 0 ? parts.join(' · ') : null
|
||||
}
|
||||
@@ -32,7 +32,12 @@ export function subagentEntryOf(
|
||||
state: block.state,
|
||||
...(block.tokens !== undefined ? { tokens: block.tokens } : {}),
|
||||
...(block.startedAt !== undefined ? { startedAt: block.startedAt } : {}),
|
||||
...(block.settledAt !== undefined ? { settledAt: block.settledAt } : {})
|
||||
...(block.settledAt !== undefined ? { settledAt: block.settledAt } : {}),
|
||||
...(block.agentType !== undefined ? { agentType: block.agentType } : {}),
|
||||
...(block.model !== undefined ? { model: block.model } : {}),
|
||||
...(block.effort !== undefined ? { effort: block.effort } : {}),
|
||||
...(block.operation !== undefined ? { operation: block.operation } : {}),
|
||||
...(block.lastMessage !== undefined ? { lastMessage: block.lastMessage } : {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user