refactor(agent): require explicit send options

This commit is contained in:
_Kerman
2026-07-24 18:23:24 +08:00
parent b56628ced7
commit 879bc71864
21 changed files with 69 additions and 73 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-22-unified-send-and-coalesced-user-messages.md: 2fd77df51ebaaad2fa0bbe17d3f8493148b761bc
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 2d4b678b6b3efb4c9ca68c79d8cadaf13b3eac04
2026-07-22-unified-send-and-coalesced-user-messages.md: ae2d25f56fedc8fc05fbad1793003b393e3a8feb
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 77fa248f50867faa3747c5dcea70e63032c98113

View File

@@ -12,7 +12,7 @@ Separately, `context/message` and `user/message` had converged: the surface proj
## Decision
**One primitive, three preset aliases.** `Agent` is now an abstract class whose single abstract `send(content, { target, wakeup, source })` covers the (`target` × `wakeup`) matrix. `followup` (`next-turn`/wakeup), `steer` (`next-step`/wakeup), and `inject` (`next-step`/no-wakeup) are concrete delegates on the base class, so concrete drivers implement `send` once and inherit the ergonomic presets. `wakeup` means "make the model run": wake a parked driver for a `next-turn` item, or force a continuation for a running `next-step` item. `send` defaults to `{ target: 'next-turn', wakeup: true }`, so every prior bare `agent.send(content)` call keeps its exact behavior. `next-turn`/no-wakeup (queue without waking) is now representable with no alias and no current caller.
**One primitive, three preset aliases.** `Agent` is now an abstract class whose single abstract `send(content, { target, wakeup, source })` covers the (`target` × `wakeup`) matrix. Its complete options object is required, so routing and provenance remain explicit. `followup` (`next-turn`/wakeup), `steer` (`next-step`/wakeup), and `inject` (`next-step`/no-wakeup) are concrete delegates on the base class, so concrete drivers implement `send` once and callers use `followup(content)` for the ordinary user-message preset. `wakeup` means "make the model run": wake a parked driver for a `next-turn` item, or force a continuation for a running `next-step` item. `next-turn`/no-wakeup (queue without waking) is representable with no alias and no current caller.
**inject keeps its mechanism.** The `next-step`/no-wakeup path is exactly the old `inject`: durable model-facing context appended at the current log position, deferred behind an executing tool batch while a turn is open and appended directly between turns while idle. It bypasses the FIFOs entirely and defaults its source to `{ kind: 'plugin', plugin: '' }`, never `{ kind: 'user' }`.

View File

@@ -12,7 +12,7 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
## 决策
**一个原语,三个预设别名。** `Agent` 现在是一个抽象类,其唯一的抽象方法 `send(content, { target, wakeup, source })` 覆盖 (`target` × `wakeup`) 矩阵。`followup``next-turn`/wakeup`steer``next-step`/wakeup`inject``next-step`/no-wakeup是基类上的具体委托方法因此具体驱动器只需实现一次 `send`就能继承这些好用的预设。`wakeup` 意为“让模型运行”:为一个 `next-turn` 队列项唤醒处于停泊状态的驱动器,或为一个运行中的 `next-step` 队列项强制继续执行。`send` 默认使用 `{ target: 'next-turn', wakeup: true }`,因此此前每一次裸调用 `agent.send(content)` 都保持完全相同的行为。`next-turn`/no-wakeup入队但不唤醒现在可以表达,只是没有别名,也没有当前调用方。
**一个原语,三个预设别名。** `Agent` 现在是一个抽象类,其唯一的抽象方法 `send(content, { target, wakeup, source })` 覆盖 (`target` × `wakeup`) 矩阵。完整的选项对象为必填,因此路由和 provenance 保持显式。`followup``next-turn`/wakeup`steer``next-step`/wakeup`inject``next-step`/no-wakeup是基类上的具体委托方法因此具体驱动器只需实现一次 `send`调用方则使用 `followup(content)` 作为普通用户消息预设。`wakeup` 意为“让模型运行”:为一个 `next-turn` 队列项唤醒处于停泊状态的驱动器,或为一个运行中的 `next-step` 队列项强制继续执行。`next-turn`/no-wakeup入队但不唤醒可以表达只是没有别名也没有当前调用方。
**inject 保留其机制。** `next-step`/no-wakeup 路径正是旧的 `inject`:持久的面向模型上下文会追加到当前日志位置;轮次打开时,它会延迟到执行中的工具批处理之后,空闲时则直接追加在两个轮次之间。它完全绕过 FIFO 队列,并把来源默认设为 `{ kind: 'plugin', plugin: '' }`,绝不是 `{ kind: 'user' }`

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
architecture.md: 164b019855b371172d40564af447d6359f5e4ff6
architecture.zh.md: 586dc8a52afb9487df3f801cfd07fcca4afa4cf9
architecture.md: c8f328560600d8a7e1f1ab659fab39c13c18dee3
architecture.zh.md: 61fba67a8585220e1ef59f02844271fee8a46401

View File

@@ -126,7 +126,7 @@ Turn and step execution events are turn-enclosed; an idle injected `user/message
### Agent Handles
`ctx.agents` owns live agents and returns `AgentHandle { agent, dispose() }`. Plugins use `send()`, `steer()`, `inject()`, `cancel()`, and `whenIdle()`. The caller fiber, factory provider, and consumer handle co-own teardown through one awaited disposer.
`ctx.agents` owns live agents and returns `AgentHandle { agent, dispose() }`. Plugins use `send(content, completeOptions)` when routing must be explicit, or the `followup()`, `steer()`, and `inject()` presets; `cancel()` and `whenIdle()` control lifecycle. The caller fiber, factory provider, and consumer handle co-own teardown through one awaited disposer.
### Agent Scope

View File

@@ -126,7 +126,7 @@ idle inject:
### Agent 句柄
`ctx.agents` 拥有活跃 agent并返回 `AgentHandle { agent, dispose() }`。插件使用 `send()``steer()``inject()``cancel()` `whenIdle()`。调用方 fiber、工厂提供方和消费方句柄通过同一个需等待完成的 disposer 共同拥有拆卸过程。
`ctx.agents` 拥有活跃 agent并返回 `AgentHandle { agent, dispose() }`。插件需要显式路由时使用 `send(content, completeOptions)`,否则使用 `followup()``steer()``inject()` 预设;`cancel()` `whenIdle()` 控制生命周期。调用方 fiber、工厂提供方和消费方句柄通过同一个需等待完成的 disposer 共同拥有拆卸过程。
### Agent 作用域

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
core.md: 36d65522201bddfd491a4f4f26b4f4b3303e4a77
core.zh.md: f065a67a2f279944ba77d25076e4b190d09f6cd0
core.md: 1d7a06405bbda523f37389e1b09c62549ece6750
core.zh.md: 2048a59451c3064eefdfe4225cc186c77921a9c0

View File

@@ -378,27 +378,22 @@ type SendTarget = 'next-turn' | 'next-step'
* (`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.
* The object is complete so routing and provenance are explicit; callers that
* want the ordinary user-message preset use {@link Agent.followup}.
*/
interface SendOptions {
/** Queue the item joins; defaults to `next-turn`. */
target?: SendTarget
/** Queue the item joins. */
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`
* or force a continuation step (`next-step` while running). 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[]
wakeup: boolean
/** Producer provenance; direct human input uses `{ kind: 'user' }`. */
source: MessageSource
}
```
@@ -406,7 +401,10 @@ The fixed-preset aliases own `target` and `wakeup`, so they accept only the rema
```ts type-equiv
/** Options accepted by the fixed-preset aliases, which own `target` and `wakeup`. */
type AliasSendOptions = Omit<SendOptions, 'target' | 'wakeup'>
interface AliasSendOptions {
/** Producer provenance; each alias supplies its documented default when omitted. */
source?: MessageSource
}
```
`send` returns the accepted message's opaque `AgentMessageId`, stable across that message's `agent/inbox/*` events:
@@ -479,8 +477,8 @@ abstract class Agent {
* 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
* - `next-turn` queues an item that becomes the sole ordinary message of its
* own FIFO-ordered turn; `wakeup: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`).
@@ -492,10 +490,10 @@ abstract class Agent {
* 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, and contexts.
* @param options - target queue, wakeup decision, and source.
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
*/
abstract send(content: ContentBlock[], options?: SendOptions): AgentMessageId
abstract send(content: ContentBlock[], options: SendOptions): AgentMessageId
/**
* Clear queued and steering work — unless `keepInbox` — and abort the active

View File

@@ -380,27 +380,22 @@ type SendTarget = 'next-turn' | 'next-step'
* (`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.
* The object is complete so routing and provenance are explicit; callers that
* want the ordinary user-message preset use {@link Agent.followup}.
*/
interface SendOptions {
/** Queue the item joins; defaults to `next-turn`. */
target?: SendTarget
/** Queue the item joins. */
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`
* or force a continuation step (`next-step` while running). 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[]
wakeup: boolean
/** Producer provenance; direct human input uses `{ kind: 'user' }`. */
source: MessageSource
}
```
@@ -408,7 +403,10 @@ interface SendOptions {
```ts type-equiv
/** Options accepted by the fixed-preset aliases, which own `target` and `wakeup`. */
type AliasSendOptions = Omit<SendOptions, 'target' | 'wakeup'>
interface AliasSendOptions {
/** Producer provenance; each alias supplies its documented default when omitted. */
source?: MessageSource
}
```
`send` 返回被接受消息的不透明 `AgentMessageId`,并在该消息的 `agent/inbox/*` 事件中保持稳定:
@@ -481,8 +479,8 @@ abstract class Agent {
* 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
* - `next-turn` queues an item that becomes the sole ordinary message of its
* own FIFO-ordered turn; `wakeup: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`).
@@ -494,10 +492,10 @@ abstract class Agent {
* 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, and contexts.
* @param options - target queue, wakeup decision, and source.
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
*/
abstract send(content: ContentBlock[], options?: SendOptions): AgentMessageId
abstract send(content: ContentBlock[], options: SendOptions): AgentMessageId
/**
* Clear queued and steering work — unless `keepInbox` — and abort the active

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1157,7 +1157,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'Agent',
declaration: 'export abstract class Agent {\n abstract readonly id: SessionId;\n abstract readonly options: AgentOptions;\n abstract readonly session: Session;\n abstract readonly status: AgentStatus;\n abstract readonly ctx: Context;\n abstract send(content: ContentBlock[], options?: SendOptions): AgentMessageId;\n abstract cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n abstract whenIdle(): Promise<void>;\n followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n abstract retry(): void;\n}',
declaration: 'export abstract class Agent {\n abstract readonly id: SessionId;\n abstract readonly options: AgentOptions;\n abstract readonly session: Session;\n abstract readonly status: AgentStatus;\n abstract readonly ctx: Context;\n abstract send(content: ContentBlock[], options: SendOptions): AgentMessageId;\n abstract cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n abstract whenIdle(): Promise<void>;\n followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n abstract retry(): void;\n}',
},
{
name: 'AgentCancelCause',

View File

@@ -90,10 +90,10 @@ export class ReactLoopAgent extends Agent {
/** Accept and route one unified send item. */
send(
content: ContentBlock[],
options: SendOptions = { target: 'next-turn', wakeup: true, source: { kind: 'user' } },
options: SendOptions,
): AgentMessageId {
const id = AgentMessageId(randomUUID())
const { target, wakeup, source } = options
const id = AgentMessageId(randomUUID())
if (target === 'next-step' && !wakeup) {
if (this.turnOpen) {
this.outbox.push({ content, source })

View File

@@ -48,7 +48,7 @@ function waitForStatus(ctx: Context, agent: Agent, expected: Agent['status']): P
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
describe('Agent', () => {
@@ -275,7 +275,7 @@ describe('Agent', () => {
prepared.markPublished()
prepared.start()
const dispose = prepared.dispose
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await new Promise(r => setTimeout(r, 30))
expect(agent.status).toBe('running')

View File

@@ -33,7 +33,7 @@ async function harness(adapter: MockAdapter) {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
/** Resolve on the agent's next idle transition (event-based, not status poll). */
@@ -63,7 +63,7 @@ describe('Agent.cancel()', () => {
ctx.on('agent/cancel-requested', (subject, cause) => {
if (subject !== agent) return
seen.push(`first:${cause.kind}`)
subject.send([{ type: 'text', text: 'queued by cancel observer' }])
subject.followup([{ type: 'text', text: 'queued by cancel observer' }])
throw new Error('observer failed')
})
ctx.on('agent/cancel-requested', (subject, cause) => {

View File

@@ -50,7 +50,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
describe('session log records what agent/step-result actually produced', () => {
@@ -851,7 +851,7 @@ describe('adapter registration, routing, and accepted-input ownership', () => {
notifiedSource = info.source
})
agent.send([{ type: 'text', text: 'start' }])
agent.followup([{ type: 'text', text: 'start' }])
await entered.promise
expect(agent.status).toBe('running')
const content = [{ type: 'text' as const, text: 'accepted-steer' }]
@@ -909,7 +909,7 @@ describe('turn numbering continues across seeded sessions', () => {
const turns: number[] = []
ctx2.on('session/event', (_s, event) => { if (event.type === 'turn/start') turns.push(event.data.turn) })
forked.send([{ type: 'text', text: 'continue' }])
forked.followup([{ type: 'text', text: 'continue' }])
await new Promise<void>((resolve) => {
ctx2.on('agent/status', (subject, status) => {
if (subject === forked && status === 'idle') resolve()

View File

@@ -38,7 +38,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
describe('inbox acceptance', () => {
@@ -50,7 +50,7 @@ describe('inbox acceptance', () => {
ctx.on('agent/inbox/enqueue', () => { queued += 1 })
expect(() => {
agent.send([{ type: 'text', text: 'first', bad: 1n } as never])
agent.followup([{ type: 'text', text: 'first', bad: 1n } as never])
}).toThrow(/losslessly JSON-serializable/)
expect(() => {
agent.send([{ type: 'text', text: 'first' }], {

View File

@@ -42,7 +42,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
function events(agent: Agent): SessionEvent[] {
@@ -151,7 +151,7 @@ describe('agent/prompt-submit', () => {
const reasons: TurnEndReason[] = []
ctx.on('session/event', (_s, event: SessionEvent) => { if (event.type === 'turn/end') reasons.push(event.data.reason) })
agent.send([{ type: 'text', text: 'do something' }])
agent.followup([{ type: 'text', text: 'do something' }])
await waitForIdle(ctx, agent)
// the model was never called

View File

@@ -42,7 +42,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
describe('agent loop', () => {
@@ -1068,9 +1068,9 @@ describe('agent loop', () => {
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
const idle = waitForIdle(ctx, agent)
agent.send([{ type: 'text', text: 'user message' }])
agent.followup([{ type: 'text', text: 'user message' }])
await Promise.resolve()
agent.send(
agent.followup(
[{ type: 'text', text: 'plugin message' }],
{ source: { kind: 'plugin', plugin: 'test' } },
)

View File

@@ -56,7 +56,7 @@ Turn and step boundaries and the model token stream are durable `session/event`
The handle every plugin programs against:
- `agent.send(content, options?)` — the one delivery primitive over the (`target` × `wakeup`) matrix; `Agent` is an abstract class whose `followup`/`steer`/`inject` aliases are fixed-preset delegates to it. It returns the accepted message's opaque `AgentMessageId`, which the message's `agent/inbox/enqueue`/`dequeue`/`discard` events carry so a caller can correlate a queued item with its lifecycle. Omitting the whole options object selects `{ target: 'next-turn', wakeup: true, source: { kind: 'user' } }`; a supplied `SendOptions` must provide all three fields. `target: 'next-turn'` queues one independent FIFO item that, if admitted, becomes the sole ordinary prompt in its turn. `target: 'next-step'` with `wakeup: true` submits steering, while `target: 'next-step'` with `wakeup: false` injects durable context without running the model. The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the turn rationale.
- `agent.send(content, options)` — the one delivery primitive over the (`target` × `wakeup`) matrix; `Agent` is an abstract class whose `followup`/`steer`/`inject` aliases are fixed-preset delegates to it. It returns the accepted message's opaque `AgentMessageId`, which the message's `agent/inbox/enqueue`/`dequeue`/`discard` events carry so a caller can correlate a queued item with its lifecycle. `SendOptions` requires `target`, `wakeup`, and `source`; callers wanting the ordinary user-message preset use `followup(content)`. `target: 'next-turn'` queues one independent FIFO item that, if admitted, becomes the sole ordinary prompt in its turn. `target: 'next-step'` with `wakeup: true` submits steering, while `target: 'next-step'` with `wakeup: false` injects durable context without running the model. The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the turn rationale.
- `agent.followup(content, options?)` — the `next-turn`/wakeup preset of `send()`: queue an ordinary follow-up turn and wake the driver.
- `agent.steer(content, options?)` — the `next-step`/wakeup preset: while a turn is open, stage steering for its next safe boundary without dispatching `agent/prompt-submit`; when idle, delegate to a woken follow-up. Cancellation or disposal may discard pending steering.
- `agent.inject(content, options?)` — the `next-step`/no-wakeup preset: append model-facing context without running the model; the next request sees a verbatim user-role message whose provenance is carried by `source`. While a turn is open, injection waits in the outbox for the next safe boundary. While idle, it appends immediately without opening a turn; persistence reacts to `session/event` independently. Injection emits no `agent/inbox/*` event.

View File

@@ -40,8 +40,8 @@ export type SendTarget = 'next-turn' | 'next-step'
* (`next-turn`/wakeup), {@link Agent.steer} (`next-step`/wakeup), and
* {@link Agent.inject} (`next-step`/no-wakeup).
*
* Omitting the whole options object selects the ordinary user-message preset.
* A supplied object is complete so its routing and provenance are explicit.
* The object is complete so routing and provenance are explicit; callers that
* want the ordinary user-message preset use {@link Agent.followup}.
*/
export interface SendOptions {
/** Queue the item joins. */
@@ -160,8 +160,8 @@ export abstract class Agent {
* The unified delivery primitive over the (`target` × `wakeup`) matrix.
* It routes the caller's typed content and source as follows:
*
* - `next-turn` (default) queues an item that becomes the sole ordinary
* message of its own FIFO-ordered turn; `wakeup` (default `true`) wakes a
* - `next-turn` queues an item that becomes the sole ordinary message of its
* own FIFO-ordered turn; `wakeup: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`).
@@ -173,7 +173,7 @@ export abstract class Agent {
* @param options - target queue, wakeup decision, and source.
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
*/
abstract send(content: ContentBlock[], options?: SendOptions): AgentMessageId
abstract send(content: ContentBlock[], options: SendOptions): AgentMessageId
/**
* Clear queued and steering work — unless `keepInbox` — and abort the active