mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +00:00
* feat(agent-status): combine Codex child work through the shared main-agent status fold * docs(agent-status): correct two comments the waiting child-work arm made stale A child failure reported in place as `blocked` now pins the row `waiting`, not `working`; and no relay ever sent an unfolded `working` beside a waiting child. * fix(agent-status): only a waiting child asks for a human A child's `blocked` state means its task failed (the only producer maps a failed background task to it, and the background-task view labels it "failed"), not that a human must act. Folding it into the waiting arm would surface a failed child as needs-you. It stays live work, as before this series. * docs(agent-status): say a waiting child, not a blocked one, makes the row wait A child's blocked state means it failed; only its waiting state feeds the waiting arm. Two fold comments, a test describe and two parity story names still called the waiting child blocked. * docs(agent-status): name where a child's wait is still lost, and pin the structured lane's real input The doc said the Claude hook lane's rows match Codex and that every lane feeds a child's wait into the fold. Neither holds: Claude keeps the wait in one slot the next main agent event overwrites, the structured lane turns a child's prompt into the main agent's own attention, and Codex drops its roster on a root Stop when it tracks no child transcripts. The parity story now drives the structured lane with the input it actually receives.
142 lines
4.0 KiB
TypeScript
142 lines
4.0 KiB
TypeScript
import {
|
|
AGENT_MODEL_MAX_LENGTH,
|
|
AGENT_STATUS_MAX_SUBAGENTS,
|
|
AGENT_STATUS_TOOL_INPUT_MAX_LENGTH,
|
|
AGENT_TYPE_MAX_LENGTH,
|
|
type AgentSubagentSnapshot
|
|
} from './agent-status-types'
|
|
import { normalizeOptionalField } from './agent-status-field-normalization'
|
|
import {
|
|
agentChildWorkLiveness,
|
|
type AgentChildWorkLiveness
|
|
} from './agent-status-child-work-liveness'
|
|
|
|
const CODEX_SUBAGENT_ID_MAX_LENGTH = 64
|
|
|
|
export type CodexSubagentRoster = Map<string, TrackedCodexSubagent>
|
|
|
|
type TrackedCodexSubagent = {
|
|
agentType?: string
|
|
description?: string
|
|
model?: string
|
|
state: 'working' | 'waiting'
|
|
startedAt: number
|
|
}
|
|
|
|
export function upsertCodexSubagent(
|
|
roster: CodexSubagentRoster,
|
|
id: string,
|
|
fields: {
|
|
agentType?: string
|
|
description?: string
|
|
model?: string
|
|
state: 'working' | 'waiting'
|
|
},
|
|
now: number
|
|
): void {
|
|
const normalizedId = id.trim()
|
|
if (normalizedId.length === 0 || normalizedId.length > CODEX_SUBAGENT_ID_MAX_LENGTH) {
|
|
return
|
|
}
|
|
const agentType = normalizeOptionalField(fields.agentType, AGENT_TYPE_MAX_LENGTH)
|
|
const description = normalizeOptionalField(fields.description, AGENT_STATUS_TOOL_INPUT_MAX_LENGTH)
|
|
const model = normalizeOptionalField(fields.model, AGENT_MODEL_MAX_LENGTH)
|
|
const existing = roster.get(normalizedId)
|
|
if (existing) {
|
|
existing.agentType = agentType ?? existing.agentType
|
|
existing.description = description ?? existing.description
|
|
existing.model = model ?? existing.model
|
|
existing.state = fields.state
|
|
return
|
|
}
|
|
if (roster.size >= AGENT_STATUS_MAX_SUBAGENTS) {
|
|
return
|
|
}
|
|
roster.set(normalizedId, {
|
|
agentType,
|
|
description,
|
|
model,
|
|
state: fields.state,
|
|
startedAt: now
|
|
})
|
|
}
|
|
|
|
export function finishCodexSubagent(roster: CodexSubagentRoster, id: string): void {
|
|
roster.delete(id.trim())
|
|
}
|
|
|
|
/**
|
|
* Record the model a already-tracked child is running. Deliberately narrower
|
|
* than `upsertCodexSubagent`: it never creates a roster entry and never touches
|
|
* `state`, so late model discovery from a child rollout cannot resurrect a
|
|
* finished child nor move any child's lifecycle.
|
|
*/
|
|
export function setCodexSubagentModel(
|
|
roster: CodexSubagentRoster,
|
|
id: string,
|
|
model: string | undefined
|
|
): void {
|
|
const normalizedModel = normalizeOptionalField(model, AGENT_MODEL_MAX_LENGTH)
|
|
if (!normalizedModel) {
|
|
return
|
|
}
|
|
const existing = roster.get(id.trim())
|
|
if (!existing) {
|
|
return
|
|
}
|
|
existing.model = normalizedModel
|
|
}
|
|
|
|
export function seedCodexSubagentRoster(
|
|
roster: CodexSubagentRoster,
|
|
snapshots: readonly AgentSubagentSnapshot[]
|
|
): void {
|
|
for (const snapshot of snapshots) {
|
|
if (snapshot.state !== 'working' && snapshot.state !== 'waiting') {
|
|
continue
|
|
}
|
|
upsertCodexSubagent(
|
|
roster,
|
|
snapshot.id,
|
|
{
|
|
agentType: snapshot.agentType,
|
|
description: snapshot.description,
|
|
model: snapshot.model,
|
|
state: snapshot.state
|
|
},
|
|
snapshot.startedAt
|
|
)
|
|
}
|
|
}
|
|
|
|
export function codexRosterToSnapshots(
|
|
roster: CodexSubagentRoster | undefined
|
|
): AgentSubagentSnapshot[] | undefined {
|
|
if (!roster || roster.size === 0) {
|
|
return undefined
|
|
}
|
|
const snapshots = Array.from(roster, ([id, tracked]) => ({
|
|
id,
|
|
agentType: tracked.agentType,
|
|
description: tracked.description,
|
|
model: tracked.model,
|
|
state: tracked.state,
|
|
startedAt: tracked.startedAt
|
|
}))
|
|
snapshots.sort((a, b) => a.startedAt - b.startedAt || a.id.localeCompare(b.id))
|
|
return snapshots
|
|
}
|
|
|
|
/** The roster as child-work evidence for the shared fold. Every tracked child is a spawned
|
|
* agent thread, classified by the one kind test the other lanes use, so a waiting child
|
|
* reads `waiting` and a live one `working`; nothing here is a watch loop. */
|
|
export function codexRosterChildWorkLiveness(
|
|
roster: CodexSubagentRoster | undefined
|
|
): AgentChildWorkLiveness {
|
|
return agentChildWorkLiveness(
|
|
roster
|
|
? Array.from(roster.values(), (tracked) => ({ kind: 'agent' as const, state: tracked.state }))
|
|
: undefined
|
|
)
|
|
}
|