refactor(agent): require explicit send options
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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' }], {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' } },
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user