mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
* test(agent-status): pin the legacy child-work projection of published background tasks * feat(agent-status): child work records say what the child is doing, how it ended, and when A child-work record gains the facts every surface needs from one host-owned record: the child that owns it (parentChildWorkId), whether the provider said it may outlive its launch turn (residency, host-only), what it is doing now (operation, with an open/reported basis), the newest thing it said (lastMessage), and when its current invocation settled (settledAt, stamped by admission, never by a producer). The codec enforces one membership x state legality matrix: live work is never done and carries no outcome or settle time; only a shell or monitor stores monitoring; settled work is done with an outcome and a settle time inside its own evidence window; an operation exists only while live and working, waiting or blocked. A settled record written without an outcome reads as unknown, never success. Malformed descriptive fields drop and keep the record. A new read-only view (AgentChildWorkView) is the one projection surfaces read; the legacy subagent and background-task shapes are derived from it with today's output unchanged for today's inputs. deriveAgentChildDisplayState folds a child's own state and the liveness of the work it owns through the same fold a parent row uses, so a child whose own work is idle or done reads monitoring while a shell it launched runs. Codex children get a thread_id alias kind. * fix(agent-status): an unknown child ending can gain its real outcome; operation clock clamped A settled child whose ending was first recorded as unknown (a roster omission can land a tick before the frame naming the outcome) now accepts the definite outcome for the same invocation and keeps its original settle time. A definite ending still never changes, and a later unknown ending is ignored rather than downgrading it. Admission clamps operation.observedAt into the child's evidence window, so an operation stamped in provider time is kept instead of silently dropped. The record codec is pinned as host-internal: it rejects a whole record over one unknown key, so a ratchet test fails if anything outside the host store and admission path imports it. * test(agent-status): pass the fold-parity input as a value; name the hook lane's alias kinds * fix(agent-status): a sparse child observation never erases what the record already knows Admission merged a later observation by replacing the whole record, so an ending that knew only that the child was gone dropped its name, model and token count, and an outcome refinement dropped the recorded last message. Labels now fill or replace but never clear, tokens never shrink, and a settled ending keeps its last message unless new evidence carries one. The provider-id preference is keyed by alias kind so a new kind cannot compile without a rank. * fix(agent-status): a child's owner, residency and last message outlive a sparse observation A settle that knows only that the child is gone dropped who owned it and whether it ran in the background, and the last thing the child said while live. They now survive like the labels do: the last message for its invocation, owner and residency for the child. * fix(agent-status): an unknown ending keeps a definite outcome and still lands its evidence A settled child's later `unknown` (or omitted) ending was acknowledged without a write, so a late last message, token count, alias or reclassification it carried was dropped while the caller was told it was accepted. The outcome now merges like every other sparse fact: an `unknown` claims nothing and keeps the stored definite outcome, and only a different definite ending conflicts. * fix(agent-status): group child aliases and owned work in one pass Appending by spread copied each bucket on every insert, quadratic in a bucket's size on the projection and per-row liveness paths. * fix(agent-status): group child aliases and owned work without Map.groupBy The relay runs this core on Node 18, which lacks Map.groupBy; a plain loop into a Map is equally linear and portable. * fix(agent-status): a child's activity and last message survive the codec Admission folded raw provider text with the status-row normalizer, which can leave a tab or other control character and can end a truncation on a space. The record codec drops such a field, so a long command cut at a space, a tab in a command, or an escape in a message silently erased the child's current operation or last message. Admission now folds control characters to spaces and trims the cut, with the codec's own control-character predicate. * refactor(agent-status): parse child facts, merge, then check the record Admission merged provider values before anything knew they were valid, and the codec then either rejected the whole record or silently dropped the field depending on how old the field was. A malformed owner erased the stored one, a label cut on a space rejected the announce, and a bad token count blocked a settle. Admission now parses every descriptive fact into a value the codec accepts or "not said", merges it over the stored record with one rule per fact (a typed map, so a new request field without a rule fails to compile), and the codec checks the result. Text goes through one normalizer and the codec accepts exactly its image; any value outside it is a writer bug and rejects. The owner is now a fact of the invocation, like the last message. * refactor(agent-status): name each erasure row by what makes its value malformed * refactor(agent-status): pin the token parse where the max-merge cannot hide it * fix(agent-status): provider timing lasts only for its own run providerTiming records the provider's start and end of one run. Keeping it across a resume left a live restarted child claiming the previous run's completion time. It now follows the owner and last message: kept within an invocation, reset by a new one.
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
import {
|
|
AGENT_CHILD_WORK_LAST_MESSAGE_MAX_LENGTH,
|
|
AGENT_CHILD_WORK_OPERATION_BASES,
|
|
AGENT_CHILD_WORK_RESIDENCIES,
|
|
type AgentChildWorkInput,
|
|
type AgentChildWorkOperation,
|
|
type AgentChildWorkOperationBasis,
|
|
type AgentChildWorkResidency
|
|
} from './agent-status-child-work'
|
|
import {
|
|
AGENT_STATUS_TOOL_INPUT_MAX_LENGTH,
|
|
AGENT_STATUS_TOOL_NAME_MAX_LENGTH
|
|
} from './agent-status-types'
|
|
import {
|
|
hasOnlyKeys,
|
|
isBoundedString,
|
|
isChildWorkText,
|
|
isRecord,
|
|
isTimestamp
|
|
} from './agent-status-child-work-value-guards'
|
|
|
|
const RESIDENCY_SET: ReadonlySet<string> = new Set(AGENT_CHILD_WORK_RESIDENCIES)
|
|
const OPERATION_BASIS_SET: ReadonlySet<string> = new Set(AGENT_CHILD_WORK_OPERATION_BASES)
|
|
|
|
export type AgentChildWorkActivityFields = Pick<
|
|
AgentChildWorkInput,
|
|
'parentChildWorkId' | 'residency' | 'operation' | 'lastMessage'
|
|
>
|
|
|
|
type AgentChildWorkActivityClock = Pick<
|
|
AgentChildWorkInput,
|
|
'childWorkId' | 'firstObservedAt' | 'observedAt'
|
|
>
|
|
|
|
export function isAgentChildWorkResidency(value: unknown): value is AgentChildWorkResidency {
|
|
return typeof value === 'string' && RESIDENCY_SET.has(value)
|
|
}
|
|
|
|
export function isAgentChildWorkOperationBasis(
|
|
value: unknown
|
|
): value is AgentChildWorkOperationBasis {
|
|
return typeof value === 'string' && OPERATION_BASIS_SET.has(value)
|
|
}
|
|
|
|
/** An owner is another child's id; a child never owns itself. */
|
|
export function isAgentChildWorkOwner(value: unknown, childWorkId: string): value is string {
|
|
return isBoundedString(value) && value !== childWorkId
|
|
}
|
|
|
|
function parseOperation(
|
|
value: unknown,
|
|
clock: AgentChildWorkActivityClock
|
|
): AgentChildWorkOperation | null {
|
|
if (
|
|
!isRecord(value) ||
|
|
!hasOnlyKeys(value, ['toolName', 'basis', 'observedAt'], ['input']) ||
|
|
!isChildWorkText(value.toolName, AGENT_STATUS_TOOL_NAME_MAX_LENGTH) ||
|
|
(value.input !== undefined &&
|
|
!isChildWorkText(value.input, AGENT_STATUS_TOOL_INPUT_MAX_LENGTH)) ||
|
|
!isAgentChildWorkOperationBasis(value.basis) ||
|
|
!isTimestamp(value.observedAt) ||
|
|
// The record's own clock is the newest evidence for this child, so it bounds the operation's.
|
|
value.observedAt < clock.firstObservedAt ||
|
|
value.observedAt > clock.observedAt
|
|
) {
|
|
return null
|
|
}
|
|
return {
|
|
toolName: value.toolName,
|
|
...(typeof value.input === 'string' ? { input: value.input } : {}),
|
|
basis: value.basis,
|
|
observedAt: value.observedAt
|
|
}
|
|
}
|
|
|
|
/** Null when any present field is outside what admission can produce. */
|
|
export function parseAgentChildWorkActivityFields(
|
|
value: Record<string, unknown>,
|
|
clock: AgentChildWorkActivityClock
|
|
): AgentChildWorkActivityFields | null {
|
|
const operation =
|
|
value.operation === undefined ? undefined : parseOperation(value.operation, clock)
|
|
if (
|
|
operation === null ||
|
|
(value.parentChildWorkId !== undefined &&
|
|
!isAgentChildWorkOwner(value.parentChildWorkId, clock.childWorkId)) ||
|
|
(value.residency !== undefined && !isAgentChildWorkResidency(value.residency)) ||
|
|
(value.lastMessage !== undefined &&
|
|
!isChildWorkText(value.lastMessage, AGENT_CHILD_WORK_LAST_MESSAGE_MAX_LENGTH))
|
|
) {
|
|
return null
|
|
}
|
|
return {
|
|
...(typeof value.parentChildWorkId === 'string'
|
|
? { parentChildWorkId: value.parentChildWorkId }
|
|
: {}),
|
|
...(isAgentChildWorkResidency(value.residency) ? { residency: value.residency } : {}),
|
|
...(operation ? { operation } : {}),
|
|
...(typeof value.lastMessage === 'string' ? { lastMessage: value.lastMessage } : {})
|
|
}
|
|
}
|