fix(agent-loop): preserve unified send contracts
This commit is contained in:
@@ -1,11 +1,7 @@
|
||||
/**
|
||||
* Agent-scoped dispatch helpers. An agent-subject event travels with the
|
||||
* agent's scope carrier as `thisArg` (so scoped listeners filter to their own
|
||||
* agent) and the agent itself as the first argument. Composable seams are
|
||||
* plain `ctx.waterfall(carrier, name, agent, …, next)` calls at the machine's
|
||||
* call sites — concrete event names type-check against the real Cordis
|
||||
* overloads, so no generic wrapper (and none of its casts) is needed. The one
|
||||
* helper here is {@link emitAgentEvent}: a contained fire-and-forget emit.
|
||||
* Agent-scoped dispatch and prompt assembly helpers. Ordinary events use the
|
||||
* fused dispatcher so subject and scope key cannot diverge; registry lifecycle
|
||||
* code instead captures one stable carrier for both edges.
|
||||
* @module @deepseek-ai/dsh-agent/dispatch
|
||||
*/
|
||||
|
||||
@@ -15,6 +11,11 @@ import type { Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import type { AssembleContext } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { Agent } from './types.ts'
|
||||
|
||||
/** Extract the parameter tuple from an event handler type (its `this` is not part of the tuple). */
|
||||
type Params<F> = F extends (...args: infer P) => unknown ? P : never
|
||||
/** Extract the return type from an event handler type. */
|
||||
type Return<F> = F extends (...args: never[]) => infer R ? R : never
|
||||
|
||||
/**
|
||||
* The event names whose subject is an agent: handler parameters start with an
|
||||
* `Agent` AND the handler declares a `Scoped<Agent>` `this` (the scope-carrier
|
||||
@@ -29,46 +30,102 @@ export type AgentSubjectEvent = {
|
||||
}[keyof Events]
|
||||
|
||||
/** The event arguments AFTER the injected agent subject. */
|
||||
type Tail<K extends AgentSubjectEvent> = Events[K] extends (...args: infer P) => unknown
|
||||
? P extends [Agent, ...infer R] ? R : never
|
||||
: never
|
||||
type Tail<K extends AgentSubjectEvent> = Params<Events[K]> extends [Agent, ...infer R] ? R : never
|
||||
|
||||
/**
|
||||
* The scope carrier for an agent-subject dispatch: the agent fused as both
|
||||
* the carrier key and the event subject, so the two cannot diverge. Pass it
|
||||
* as the `thisArg` of `ctx.serial` / `ctx.waterfall` for agent events.
|
||||
* @param agent - the subject agent.
|
||||
* @returns the fused carrier.
|
||||
* The fused dispatcher {@link agentEvents} returns: each method dispatches the
|
||||
* named agent-subject event with the agent's scope carrier as `thisArg` and
|
||||
* the agent itself injected as the first event argument.
|
||||
*/
|
||||
export interface AgentEventDispatch {
|
||||
/**
|
||||
* Fire-and-forget notification in the agent's scope. Every listener is
|
||||
* invoked; synchronous throws and returned-promise rejections are logged and
|
||||
* contained per listener, so a notification cannot veto lifecycle progress
|
||||
* or starve a later observer.
|
||||
* @param name - the agent-subject event to emit.
|
||||
* @param rest - the event's arguments after the injected agent.
|
||||
*/
|
||||
emit<K extends AgentSubjectEvent>(name: K, ...rest: Tail<K>): void
|
||||
/**
|
||||
* Awaited in-order dispatch (Cordis `serial`) in the agent's scope.
|
||||
* @param name - the agent-subject event to dispatch.
|
||||
* @param rest - the event's arguments after the injected agent.
|
||||
* @returns the serial chain's result (the first bail value, if any).
|
||||
*/
|
||||
serial<K extends AgentSubjectEvent>(name: K, ...rest: Tail<K>): Promise<Awaited<Return<Events[K]>>>
|
||||
/**
|
||||
* Around-middleware dispatch (Cordis `waterfall`) in the agent's scope. The
|
||||
* declared event parameters already end with the `next` callback, so `rest`
|
||||
* is exactly the event's arguments after the injected agent — the final
|
||||
* element being the innermost `next` (the default the listener chain wraps).
|
||||
* @param name - the agent-subject event to dispatch.
|
||||
* @param rest - the event's arguments after the injected agent.
|
||||
* @returns the waterfall's composed result.
|
||||
*/
|
||||
waterfall<K extends AgentSubjectEvent>(name: K, ...rest: Tail<K>): Return<Events[K]>
|
||||
}
|
||||
|
||||
/** Return the fused scope carrier for one agent subject. */
|
||||
export function agentCarrier(agent: Agent): Scoped<Agent> {
|
||||
return scopeTarget(agent, agent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire-and-forget notification in the agent's scope. Every listener is
|
||||
* invoked; synchronous throws and returned-promise rejections are logged and
|
||||
* contained per listener, so a notification cannot veto lifecycle progress or
|
||||
* starve a later observer. (Raw Cordis `emit` maps callbacks unguarded — one
|
||||
* synchronous throw would starve the rest and escape into the caller.)
|
||||
* Build a dispatcher that couples the agent subject to its scope carrier.
|
||||
* @param ctx - the context to dispatch through (any context of the app).
|
||||
* @param agent - the subject agent; also the scope-carrier key.
|
||||
* @param name - the agent-subject event to emit.
|
||||
* @param rest - the event's arguments after the injected agent.
|
||||
* @returns the fused dispatcher.
|
||||
*/
|
||||
export function emitAgentEvent<K extends AgentSubjectEvent>(ctx: Context, agent: Agent, name: K, ...rest: Tail<K>): void {
|
||||
const args: unknown[] = [agentCarrier(agent), name, agent, ...rest]
|
||||
for (const callback of ctx.events.dispatch('emit', args)) {
|
||||
try {
|
||||
const returned: unknown = callback(...args)
|
||||
void Promise.resolve(returned).catch((error: unknown) => {
|
||||
ctx.logger.warn(`agent event "${name}" listener rejected: ${String(error)}`)
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
ctx.logger.warn(`agent event "${name}" listener threw: ${String(error)}`)
|
||||
}
|
||||
export function agentEvents(ctx: Context, agent: Agent): AgentEventDispatch {
|
||||
const carrier = agentCarrier(agent)
|
||||
// The ordinary dispatch methods forward through Cordis' variadic mixins. The
|
||||
// fused (carrier, name, agent, ...rest) tuple is provably a valid argument
|
||||
// list for the matching thisArg overload, but TypeScript cannot relate the
|
||||
// generic Tail<K> spread back to that overload's conditional parameter
|
||||
// tuple — hence one contained, shape-preserving cast per method.
|
||||
return {
|
||||
emit(name, ...rest) {
|
||||
// Cordis emit invokes callbacks through Array.map: one synchronous throw
|
||||
// starves later listeners, and returned promises are discarded. Agent
|
||||
// notifications are non-vetoing, so resolve the same filtered callback
|
||||
// set ourselves and contain both failure modes independently.
|
||||
const args: unknown[] = [carrier, name, agent, ...rest]
|
||||
const callbacks = ctx.events.dispatch('emit', args)
|
||||
for (const callback of callbacks) {
|
||||
try {
|
||||
const returned: unknown = callback(...args)
|
||||
void Promise.resolve(returned).catch((error: unknown) => {
|
||||
ctx.logger.warn(`agent event "${name}" listener rejected: ${String(error)}`)
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
ctx.logger.warn(`agent event "${name}" listener threw: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
},
|
||||
async serial(name, ...rest) {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method -- the events mixin accessor returns a pre-bound function
|
||||
const serial = ctx.serial as (thisArg: Scoped<Agent>, name: string, ...args: unknown[]) => Promise<never>
|
||||
return await serial(carrier, name, agent, ...rest)
|
||||
},
|
||||
waterfall(name, ...rest) {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method -- the events mixin accessor returns a pre-bound function
|
||||
const waterfall = ctx.waterfall as (thisArg: Scoped<Agent>, name: string, ...args: unknown[]) => never
|
||||
return waterfall(carrier, name, agent, ...rest)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Emit one contained agent notification without allocating a retained dispatcher. */
|
||||
export function emitAgentEvent<K extends AgentSubjectEvent>(
|
||||
ctx: Context,
|
||||
agent: Agent,
|
||||
name: K,
|
||||
...rest: Tail<K>
|
||||
): void {
|
||||
agentEvents(ctx, agent).emit(name, ...rest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the prompt assembly context with agent and scope set together, so
|
||||
* agent-scoped prompt and tool contributions cannot be silently omitted.
|
||||
|
||||
@@ -17,8 +17,8 @@ import type { Agent, AgentOptions } from './types.ts'
|
||||
export * from './types.ts'
|
||||
export { agentInterruptReasonOf } from './cancellation.ts'
|
||||
export * from './llm-target.ts'
|
||||
export { agentCarrier, assembleContextFor, emitAgentEvent } from './dispatch.ts'
|
||||
export type { AgentSubjectEvent } from './dispatch.ts'
|
||||
export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from './dispatch.ts'
|
||||
export type { AgentEventDispatch, AgentSubjectEvent } from './dispatch.ts'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
import type { Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import type { ContentBlock, LlmCallConfig, LlmFailure, MessageSource } from '@deepseek-ai/dsh-llm'
|
||||
import type { JsonValue, Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -42,47 +43,125 @@ export interface AgentOptions {
|
||||
model?: string
|
||||
}
|
||||
|
||||
/** One queued prompt or steering item and its atomic model-facing context. */
|
||||
export interface SendOptions {
|
||||
/** Explicit producer attribution; callers may not inherit human authority by omission. */
|
||||
source: MessageSource
|
||||
/** Context snapshotted with this item and admitted at the same boundary. */
|
||||
contexts?: HookContext[]
|
||||
}
|
||||
/**
|
||||
* Which inbox queue a {@link Agent.send} item joins:
|
||||
* - `next-turn` — the item becomes its own turn, claimed at a turn boundary.
|
||||
* - `next-step` — the item joins the active turn between steps as steering,
|
||||
* or, when no turn is active, is promoted per its `wakeup` flag.
|
||||
*/
|
||||
export type SendTarget = 'next-turn' | 'next-step'
|
||||
|
||||
/** Options for synthetic context injection. */
|
||||
export interface InjectOptions {
|
||||
/** Explicit producer attribution. */
|
||||
source: MessageSource
|
||||
/** Opaque durable state omitted from the model projection. */
|
||||
/**
|
||||
* Options for the unified {@link Agent.send} primitive over the
|
||||
* (`target` × `wakeup`) matrix. Named presets: {@link Agent.followup}
|
||||
* (`next-turn`/wakeup), {@link Agent.steer} (`next-step`/wakeup), and
|
||||
* {@link Agent.inject} (`next-step`/no-wakeup).
|
||||
*
|
||||
* An omitted source attests direct human input as `{ kind: 'user' }` and may
|
||||
* authorize policy consumers, so non-human producers must label their content.
|
||||
*/
|
||||
export interface SendOptions {
|
||||
/** Queue the item joins; defaults to `next-turn`. */
|
||||
target?: SendTarget
|
||||
/**
|
||||
* Whether this item makes the model run: wake a parked driver (`next-turn`)
|
||||
* or force a continuation step (`next-step` while running). Defaults to
|
||||
* `true`. A `false` `next-turn` item queues without waking; a `false`
|
||||
* `next-step` item attaches durable context without forcing another step
|
||||
* (the injection preset).
|
||||
*/
|
||||
wakeup?: boolean
|
||||
source?: MessageSource
|
||||
/**
|
||||
* Model-facing contexts captured with this inbox item. A queued prompt exposes
|
||||
* them through the default `agent/prompt-submit` allow decision, while steering
|
||||
* records them directly at its next checkpoint.
|
||||
*/
|
||||
contexts?: HookContext[]
|
||||
/** Opaque JSON state retained on the durable message but hidden from the model. */
|
||||
meta?: JsonValue
|
||||
}
|
||||
|
||||
/** Options accepted by the fixed-preset aliases, which own `target` and `wakeup`. */
|
||||
export type AliasSendOptions = Omit<SendOptions, 'target' | 'wakeup'>
|
||||
|
||||
/**
|
||||
* An agent's ACTIVITY state, emitted on every transition as `agent/status`:
|
||||
* `idle` (parked, waiting for queued work) or `running` (the machine is
|
||||
* draining work). Lifecycle is a separate axis: an agent leaving its host is
|
||||
* announced by `agent/disposed` and observable as `ctx.agents.get(id)` no
|
||||
* longer returning it — not as a status value.
|
||||
* Opaque id assigned to one accepted {@link Agent.send} message; returned by
|
||||
* `send` and carried on its `agent/inbox/*` events for correlation.
|
||||
*/
|
||||
export type AgentMessageId = Branded<'AgentMessageId'>
|
||||
|
||||
/**
|
||||
* Brand a string as an {@link AgentMessageId}.
|
||||
* @param id - the generated message id.
|
||||
* @returns the same string, branded; no validation is performed.
|
||||
*/
|
||||
export function AgentMessageId(id: string): AgentMessageId {
|
||||
return id as AgentMessageId
|
||||
}
|
||||
|
||||
/**
|
||||
* One accepted {@link Agent.send} message, carried by the `agent/inbox/*` live
|
||||
* events. `id` is the value `send` returned to the caller, stable across this
|
||||
* message's enqueue, dequeue, and discard events. Source defaults are already
|
||||
* applied, so these are the exact values the item was accepted with. `steering`
|
||||
* is true for a `next-step` item drained between steps; a `next-turn` item is
|
||||
* claimed at a turn boundary. `SendOptions.meta` is intentionally omitted: it is
|
||||
* durable model-hidden state that lands on the eventual `user/message`/
|
||||
* `steering/message`, not live-event routing data.
|
||||
*/
|
||||
export interface AgentMessage {
|
||||
/** The id `send` returned for this message. */
|
||||
id: AgentMessageId
|
||||
content: ContentBlock[]
|
||||
source: MessageSource
|
||||
contexts: HookContext[]
|
||||
/** Whether the item joined the steering FIFO (`next-step`) rather than the queued FIFO. */
|
||||
steering: boolean
|
||||
/** Whether the item is marked to wake the driver or force a continuation. */
|
||||
wakeup: boolean
|
||||
}
|
||||
|
||||
/** Options for {@link Agent.cancel}. */
|
||||
export interface CancelOptions {
|
||||
/**
|
||||
* Preserve queued and steering inbox items instead of discarding them. The
|
||||
* active turn is still aborted, but un-started and pending work survives for a
|
||||
* later turn and no `agent/inbox/discard` fires.
|
||||
*/
|
||||
keepInbox?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* An agent's lifecycle state, emitted on every transition as `agent/status`:
|
||||
* `idle` (parked, waiting for queued work), `running` (the driver is draining
|
||||
* work and may be closing or checkpointing a turn), `disposed` (terminal — no
|
||||
* transition leaves it, and `send`/`followup`/`steer`/`inject` throw).
|
||||
*/
|
||||
export type AgentStatus = 'idle' | 'running'
|
||||
|
||||
/** Model-facing context injected by a listener or atomically attached to one inbox item. */
|
||||
/** Model-facing context injected by a listener or atomically attached to one inbox message. */
|
||||
export interface HookContext {
|
||||
content: ContentBlock[]
|
||||
source: MessageSource
|
||||
/** `prompt-prefix` bakes this context into its prompt; absent/`separate` records an independent message. */
|
||||
/**
|
||||
* Model placement. Absent or `separate` records an independent injected
|
||||
* `user/message`; `prompt-prefix` prepends this context and a stable
|
||||
* request delimiter to the same user-role message as its attached prompt.
|
||||
*/
|
||||
placement?: 'separate' | 'prompt-prefix'
|
||||
/** Opaque durable state omitted from the model projection. */
|
||||
/** Opaque JSON state retained in the session event but hidden from the model. */
|
||||
meta?: JsonValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt interception result. `allow.content` replaces the prompt. Each
|
||||
* `additionalContexts` entry follows its declared placement. `block` records
|
||||
* a durable `prompt/blocked` and ends the claimed prompt's zero-step turn as
|
||||
* rejected. A listener wrapping `next()` preserves downstream fields unless
|
||||
* it intentionally replaces them.
|
||||
* `additionalContexts` entry follows its declared placement: separate context
|
||||
* message by default, or a prefix inside the prompt's user-role message.
|
||||
* `block` records a durable `prompt/blocked` and ends the claimed prompt's
|
||||
* zero-step turn as rejected. An `allow` returned by a listener is
|
||||
* authoritative: a listener wrapping `next()` preserves downstream `content`
|
||||
* and `additionalContexts` unless it intentionally replaces them.
|
||||
*/
|
||||
export type PromptDecision =
|
||||
| { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: HookContext[] }
|
||||
@@ -110,47 +189,98 @@ export type AgentCancelCause =
|
||||
/** Runtime reason carried by the signal that controls one live turn. */
|
||||
export type AgentInterruptReason = AgentCancelCause | { readonly kind: 'disposed' }
|
||||
|
||||
/** Public live-agent handle; driving methods have no contract after disposal. */
|
||||
export interface Agent {
|
||||
/** Public live-agent handle with aliases over the unified delivery primitive. */
|
||||
export abstract class Agent {
|
||||
/** The single identity shared with {@link session}. */
|
||||
readonly id: SessionId
|
||||
readonly options: AgentOptions
|
||||
readonly session: Session
|
||||
readonly status: AgentStatus
|
||||
abstract readonly id: SessionId
|
||||
/** The provider route and model this agent's requests use. */
|
||||
abstract readonly options: AgentOptions
|
||||
/** The live session this agent drives; its log is the durable source of truth. */
|
||||
abstract readonly session: Session
|
||||
/** The current lifecycle state, mirrored on every `agent/status` transition. */
|
||||
abstract readonly status: AgentStatus
|
||||
/** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
|
||||
readonly ctx: Context
|
||||
abstract readonly ctx: Context
|
||||
|
||||
/**
|
||||
* Queue one detached, frozen lossless-JSON prompt. Each claimed prompt is
|
||||
* the sole ordinary message in its FIFO-ordered turn; the next claimed
|
||||
* prompt waits for that turn's checkpoint.
|
||||
* Invalid input throws synchronously before notification or enqueue.
|
||||
* The unified delivery primitive over the (`target` × `wakeup`) matrix.
|
||||
* Detaches, validates, and freezes one lossless-JSON item, then routes it:
|
||||
*
|
||||
* - `next-turn` (default) queues an item that becomes the sole ordinary
|
||||
* message of its own FIFO-ordered turn; `wakeup` (default `true`) wakes a
|
||||
* parked driver, while `wakeup:false` queues without waking.
|
||||
* - `next-step` with `wakeup:true` submits steering into the active turn
|
||||
* (idle falls back to a woken `next-turn`).
|
||||
* - `next-step` with `wakeup:false` injects durable model-facing context
|
||||
* without running the model: an open turn joins at the current log position
|
||||
* (deferred behind an executing tool batch until it settles), and an idle
|
||||
* inject records a one-shot turn with its own durability checkpoint.
|
||||
*
|
||||
* Attached contexts share the same snapshot and ownership boundary. Invalid
|
||||
* input throws synchronously before any notification, enqueue, or append.
|
||||
* @param content - the model-facing content blocks to deliver.
|
||||
* @param options - target queue, wakeup decision, source, contexts, and meta.
|
||||
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
|
||||
*/
|
||||
send(content: ContentBlock[], options: SendOptions): void
|
||||
abstract send(content: ContentBlock[], options?: SendOptions): AgentMessageId
|
||||
|
||||
/**
|
||||
* Submit steering while the agent is `running`: it enters the outbox and is
|
||||
* taken whole at the next step boundary, before the next request. Steering
|
||||
* left over when the turn closes queues for a turn of its own. When idle,
|
||||
* delegates to {@link send}.
|
||||
* Clear queued and steering work — unless `keepInbox` — and abort the active
|
||||
* turn. An effective call first emits `agent/cancel-requested` with the
|
||||
* resolved typed cause. The first cause wins for the active turn, and
|
||||
* `whenIdle()` resolves after cancellation reaches quiescence. Omitted cause
|
||||
* means `{ kind: 'user' }`. Idle cancellation is a no-op and does not arm
|
||||
* later work. The active turn snapshots and freezes the cause.
|
||||
* @param cause - the stable caller intent carried by the current turn signal.
|
||||
* @param options - cancellation options; `keepInbox` preserves pending work.
|
||||
*/
|
||||
steer(content: ContentBlock[], options: SendOptions): void
|
||||
abstract cancel(cause: AgentCancelCause, options?: CancelOptions): void
|
||||
|
||||
/** Resolve at idle quiescence; disposal waits for driver exit rather than only the status transition. */
|
||||
abstract whenIdle(): Promise<void>
|
||||
|
||||
/**
|
||||
* Stage detached model-facing context without running the model: it enters
|
||||
* the outbox and rides along with whatever runs next — the next step of the
|
||||
* running turn (never between a tool-call batch and its results), or the
|
||||
* next turn when idle.
|
||||
* Queue an ordinary follow-up turn and wake the driver — the
|
||||
* `next-turn`/wakeup preset of {@link send}. The item becomes the sole
|
||||
* ordinary message of its own turn.
|
||||
* @param content - the prompt content blocks.
|
||||
* @param options - source and attached contexts.
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
inject(content: ContentBlock[], options: InjectOptions): void
|
||||
followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
|
||||
return this.send(content, { ...options, target: 'next-turn', wakeup: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all queued and outbox work and abort the active turn. An effective
|
||||
* call first emits `agent/cancel-requested` with the resolved typed cause;
|
||||
* the first cause wins for the active turn. Omission means `{ kind: 'user' }`.
|
||||
* Idle cancellation is a no-op and does not arm later work.
|
||||
* Submit steering into the running turn — the `next-step`/wakeup preset of
|
||||
* {@link send}. An open turn records it at the next steering checkpoint before
|
||||
* a request or continuation decision; policy may stop before another step.
|
||||
* After turn close and its checkpoint, any remainder is queued for a later
|
||||
* turn; terminal `agent/turn-stop`, cancellation, or disposal may discard it.
|
||||
* Idle steering falls back to a woken follow-up turn.
|
||||
* @param content - the steering content blocks.
|
||||
* @param options - source and attached contexts.
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
cancel(cause?: AgentInterruptReason): void
|
||||
steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
|
||||
return this.send(content, { ...options, target: 'next-step', wakeup: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Append detached model-facing context without running the model — the
|
||||
* `next-step`/no-wakeup preset of {@link send}. An open-turn injection joins
|
||||
* at the current log position unless the current tool batch is executing;
|
||||
* then it waits FIFO until that batch settles and drains before turn close
|
||||
* even when interrupted. Idle injection uses a one-shot turn and durability
|
||||
* checkpoint. Disposal awaits idle checkpoints; flush failures report through
|
||||
* `agent/error`. An omitted source defaults to `{ kind: 'plugin', plugin: '' }`.
|
||||
* @param content - the injected context content blocks.
|
||||
* @param options - source and durable model-hidden meta.
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
|
||||
return this.send(content, { ...options, target: 'next-step', wakeup: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-open a turn on the current session log without a new prompt — the
|
||||
@@ -160,10 +290,7 @@ export interface Agent {
|
||||
* `agent/idle` listener is legal — the machine is already idle there.
|
||||
* @throws while a turn is running because there is nothing to retry yet.
|
||||
*/
|
||||
retry(): void
|
||||
|
||||
/** Resolve at idle quiescence; disposal waits for machine exit rather than only the status transition. */
|
||||
whenIdle(): Promise<void>
|
||||
abstract retry(): void
|
||||
}
|
||||
|
||||
declare module 'cordis' {
|
||||
@@ -181,7 +308,7 @@ declare module 'cordis' {
|
||||
*/
|
||||
'agent/created'(this: Scoped<Agent>, agent: Agent): void
|
||||
/**
|
||||
* An agent left the registry; AgentLoop emits this after machine quiescence
|
||||
* An agent left the registry; AgentLoop emits this after driver quiescence
|
||||
* but before session detachment and scoped-registration unwind. Custom
|
||||
* registry users own their driver-ordering contract.
|
||||
* @param agent - the exact agent removed from the registry.
|
||||
@@ -190,7 +317,7 @@ declare module 'cordis' {
|
||||
*/
|
||||
'agent/disposed'(this: Scoped<Agent>, agent: Agent): void
|
||||
/**
|
||||
* Agent activity changed (`idle` ⇄ `running`). `send()` does
|
||||
* Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does
|
||||
* not enter `running` synchronously; drive lifecycle from this event.
|
||||
* @param agent - the agent whose status flipped.
|
||||
* @param status - the status just entered (the transition's destination).
|
||||
@@ -199,15 +326,33 @@ declare module 'cordis' {
|
||||
*/
|
||||
'agent/status'(this: Scoped<Agent>, agent: Agent, status: AgentStatus): void
|
||||
/**
|
||||
* Detached, frozen content entered the agent's inbox (prompt queue or
|
||||
* steering outbox). These are the exact values retained for the log.
|
||||
* @param agent - the agent whose inbox received the message.
|
||||
* @param content - the accepted content blocks retained by the inbox.
|
||||
* @param info - the accepted source, contexts, and steering classification.
|
||||
* A frozen item entered the queued or steering inbox.
|
||||
* @param agent - the owning agent.
|
||||
* @param message - accepted routing data and correlation identity.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/queued'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], info: { source: MessageSource; contexts: HookContext[]; steering: boolean }): void
|
||||
'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void
|
||||
/**
|
||||
* The driver claimed one item out of the inbox: a queued item at a turn
|
||||
* boundary, or steering drained between steps. Fires after the item leaves
|
||||
* its FIFO and before it becomes a durable message.
|
||||
* @param agent - the agent whose inbox item was claimed.
|
||||
* @param message - the claimed message (matching the `id` from its `agent/inbox/enqueue`).
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/dequeue'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void
|
||||
/**
|
||||
* `cancel()` (without `keepInbox`) dropped pending inbox items without
|
||||
* delivering them. Fires once per effective clearing call with every
|
||||
* discarded item, after `agent/cancel-requested` and before the abort.
|
||||
* @param agent - the agent whose inbox was cleared.
|
||||
* @param messages - the discarded messages in FIFO order (queued then steering); empty when nothing was pending.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/discard'(this: Scoped<Agent>, agent: Agent, messages: AgentMessage[]): void
|
||||
/**
|
||||
* Effective broad cancellation was requested, before queued/outbox work
|
||||
* is cleared or the active turn is aborted. This observe-only notification
|
||||
@@ -217,12 +362,14 @@ declare module 'cordis' {
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/cancel-requested'(this: Scoped<Agent>, agent: Agent, cause: AgentInterruptReason): void
|
||||
'agent/cancel-requested'(this: Scoped<Agent>, agent: Agent, cause: AgentCancelCause): void
|
||||
|
||||
// ---- session lifecycle (emit) ----
|
||||
/**
|
||||
* The session lifecycle began, once before the first turn. Use
|
||||
* `agent.inject()` to seed model-facing context. This is a notification, not
|
||||
* a veto; disposal requested by a lifecycle owner is rechecked before the
|
||||
* machine starts.
|
||||
* driver starts.
|
||||
* @param agent - the agent whose session lifecycle began.
|
||||
* @param source - why the session started (fresh startup, resume, …).
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
Reference in New Issue
Block a user