fix(agent): restore public send method

This commit is contained in:
_Kerman
2026-07-31 10:01:35 +08:00
parent c891cb6f6f
commit 060c0f78f0
28 changed files with 76 additions and 32 deletions

View File

@@ -33,7 +33,7 @@ A fully configured agent and live session were published. Setup is composition-o
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:151`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:160`](../../packages/core/agent/src/types.ts)
### `agent/disposed` — emit ### `agent/disposed` — emit
@@ -53,7 +53,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence and sco
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:160`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:169`](../../packages/core/agent/src/types.ts)
### `agent/error` — emit ### `agent/error` — emit
@@ -75,7 +75,7 @@ A step or turn errored. The machine reports a failure here (plus the logger) eve
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:261`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:270`](../../packages/core/agent/src/types.ts)
### `agent/prompt-submit` — waterfall ### `agent/prompt-submit` — waterfall
@@ -98,7 +98,7 @@ Allow, rewrite, or block one claimed inbox batch before it becomes model-visible
Types: [Agent](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md) Types: [Agent](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md)
Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:205`](../../packages/core/agent/src/types.ts)
### `agent/request` — waterfall ### `agent/request` — waterfall
@@ -122,7 +122,7 @@ Replace the frozen call configuration. `await next()` yields the config the mach
Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:222`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:231`](../../packages/core/agent/src/types.ts)
### `agent/request-error` — waterfall ### `agent/request-error` — waterfall
@@ -145,7 +145,7 @@ Handle one failed model-request attempt before the loop retries or closes its st
Types: [Agent](../core-data-structures/core.md) · [RequestErrorAction](../core-data-structures/core.md) · [RequestFailureContext](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) Types: [Agent](../core-data-structures/core.md) · [RequestErrorAction](../core-data-structures/core.md) · [RequestFailureContext](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:234`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts)
### `agent/session-start` — emit ### `agent/session-start` — emit
@@ -167,7 +167,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md) Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:182`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:191`](../../packages/core/agent/src/types.ts)
### `agent/status` — emit ### `agent/status` — emit
@@ -188,7 +188,7 @@ Agent status changed (`idle` ⇄ `running`). A waking delivery enters `running`
Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:170`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:179`](../../packages/core/agent/src/types.ts)
### `agent/step` — serial ### `agent/step` — serial
@@ -212,7 +212,7 @@ Awaited serial checkpoint before EVERY request of a turn is built (the first as
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:209`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:218`](../../packages/core/agent/src/types.ts)
### `agent/turn-stopping` — serial ### `agent/turn-stopping` — serial
@@ -238,7 +238,7 @@ The turn is about to close: the model owes no response (no live tool calls, no f
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:249`](../../packages/core/agent/src/types.ts) Source: [`packages/core/agent/src/types.ts:258`](../../packages/core/agent/src/types.ts)
## `agent-loop/*` ## `agent-loop/*`

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/core.md # pnpm run verify-translation-pairing --write docs/core-data-structures/core.md
core.md: 3bbfe3c4c763a24edb6ec1362344cef6ce50f650 core.md: 9c1015ddeedcf3355d03678088ec7bcc6759d677
core.zh.md: d54159c25659a89baded31b8e3aa5207715e0c12 core.zh.md: 840cc929a7c63b63b3eb2d860b59ace2f1a63ed5

View File

@@ -441,7 +441,7 @@ type AgentCancelCause =
| { readonly kind: 'disposed' } | { readonly kind: 'disposed' }
``` ```
`Agent` is an interface over the public live-agent contract. Concrete drivers implement `followup`, `steer`, and `inject`; routing policy remains private to the driver. `Agent` is an interface over the public live-agent contract. Its unified `send` method exposes target and wakeup routing directly; `followup`, `steer`, and `inject` are fixed-preset aliases.
```ts type-equiv ```ts type-equiv
/** Public live-agent handle. */ /** Public live-agent handle. */
@@ -476,6 +476,15 @@ interface Agent {
*/ */
whenIdle(): Promise<void> whenIdle(): Promise<void>
/**
* Route identified input to an inbox boundary and optionally wake the driver.
* Waking input submitted after active cancellation is queued for the next turn.
* @param message - identified content and its producer provenance.
* @param target - the preferred next-turn or next-step inbox boundary.
* @param wakeup - whether delivery may wake the driver.
*/
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void
/** /**
* Queue an ordinary follow-up turn and wake the driver. The item becomes the * Queue an ordinary follow-up turn and wake the driver. The item becomes the
* sole ordinary message of its own turn. * sole ordinary message of its own turn.

View File

@@ -449,7 +449,7 @@ type AgentCancelCause =
| { readonly kind: 'disposed' } | { readonly kind: 'disposed' }
``` ```
`Agent` 是覆盖公开活跃 agent 契约的接口。具体驱动器实现 `followup`、`steer` 和 `inject`;路由策略仍为驱动器私有。 `Agent` 是覆盖公开活跃 agent 契约的接口。它的统一 `send` 方法直接公开目标与唤醒路由;`followup`、`steer` 和 `inject` 是固定预设别名。
```ts type-equiv ```ts type-equiv
/** Public live-agent handle. */ /** Public live-agent handle. */
@@ -484,6 +484,15 @@ interface Agent {
*/ */
whenIdle(): Promise<void> whenIdle(): Promise<void>
/**
* Route identified input to an inbox boundary and optionally wake the driver.
* Waking input submitted after active cancellation is queued for the next turn.
* @param message - identified content and its producer provenance.
* @param target - the preferred next-turn or next-step inbox boundary.
* @param wakeup - whether delivery may wake the driver.
*/
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void
/** /**
* Queue an ordinary follow-up turn and wake the driver. The item becomes the * Queue an ordinary follow-up turn and wake the driver. The item becomes the
* sole ordinary message of its own turn. * sole ordinary message of its own turn.

View File

@@ -43,6 +43,7 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'running', status: 'running',
ctx: new Context(), ctx: new Context(),
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => { throw new Error('time-context must append directly to the open step') }, inject: () => { throw new Error('time-context must append directly to the open step') },

View File

@@ -99,6 +99,7 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'running', status: 'running',
ctx: new Context(), ctx: new Context(),
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => { throw new Error('tmux-context must append directly to the open step') }, inject: () => { throw new Error('tmux-context must append directly to the open step') },

View File

@@ -178,6 +178,7 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
session, session,
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'idle', status: 'idle',
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => { throw new Error('workspace-context must append directly to the open step') }, inject: () => { throw new Error('workspace-context must append directly to the open step') },

View File

@@ -1419,7 +1419,7 @@ export const EVENT_API: readonly EventApiEntry[] = [
export const TYPE_API: readonly TypeApiEntry[] = [ export const TYPE_API: readonly TypeApiEntry[] = [
{ {
name: 'Agent', name: 'Agent',
declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly inbox: Inbox;\n readonly status: AgentStatus;\n readonly ctx: Context;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise<void>;\n followup(message: UserMessage): void;\n steer(message: UserMessage): void;\n inject(message: UserMessage): void;\n}', declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly inbox: Inbox;\n readonly status: AgentStatus;\n readonly ctx: Context;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise<void>;\n send(message: UserMessage, target: InboxTarget, wakeup: boolean): void;\n followup(message: UserMessage): void;\n steer(message: UserMessage): void;\n inject(message: UserMessage): void;\n}',
}, },
{ {
name: 'AgentCancelCause', name: 'AgentCancelCause',

View File

@@ -81,7 +81,7 @@ export class ReactLoopAgent implements Agent {
} }
} }
private send(message: UserMessage, target: InboxTarget, wakeup: boolean): void { send(message: UserMessage, target: InboxTarget, wakeup: boolean): void {
// Waking input cannot join an aborted admission or turn, so it starts the next turn. // Waking input cannot join an aborted admission or turn, so it starts the next turn.
const wakingAfterAbort = wakeup && this.phase.kind !== 'idle' && this.phase.abort.signal.aborted const wakingAfterAbort = wakeup && this.phase.kind !== 'idle' && this.phase.abort.signal.aborted
const resolvedTarget = wakingAfterAbort ? 'next-turn' : target const resolvedTarget = wakingAfterAbort ? 'next-turn' : target
@@ -180,7 +180,7 @@ export class ReactLoopAgent implements Agent {
if (admission.kind !== 'admitted') return false if (admission.kind !== 'admitted') return false
signal.throwIfAborted() signal.throwIfAborted()
} catch (error: unknown) { } catch (error: unknown) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cancel may abort while admission awaits // oxlint-disable-next-line typescript/no-unnecessary-condition -- cancel may abort while admission awaits
if (signal.aborted) return this.inbox.hasPending if (signal.aborted) return this.inbox.hasPending
throw error throw error
} }
@@ -216,11 +216,11 @@ export class ReactLoopAgent implements Agent {
if (admission.kind === 'empty' && turnEnds) break if (admission.kind === 'empty' && turnEnds) break
} }
} catch (error: unknown) { } catch (error: unknown) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cancel may abort during any awaited turn operation // oxlint-disable-next-line typescript/no-unnecessary-condition -- cancel may abort during any awaited turn operation
if (signal.aborted) turnEnds = { kind: 'aborted', reason: signal.reason as AgentCancelCause } if (signal.aborted) turnEnds = { kind: 'aborted', reason: signal.reason as AgentCancelCause }
else turnEnds = { kind: 'error', error: errorChain(error) } else turnEnds = { kind: 'error', error: errorChain(error) }
} finally { } finally {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the turn is always ended in this block // oxlint-disable-next-line typescript/no-non-null-assertion -- the turn is always ended in this block
this.session.append('turn/end', { turn, reason: turnEnds! }) this.session.append('turn/end', { turn, reason: turnEnds! })
} }
return this.inbox.hasPending return this.inbox.hasPending
@@ -317,7 +317,7 @@ export class ReactLoopAgent implements Agent {
: undefined : undefined
const maxTokens = this.options.maxTokens const maxTokens = this.options.maxTokens
const seedConfig = this.requestHeaderLogged const seedConfig = this.requestHeaderLogged
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the instance logged the frozen header it now folds // oxlint-disable-next-line typescript/no-non-null-assertion -- the instance logged the frozen header it now folds
? persistedConfig! ? persistedConfig!
: deepFreeze({ : deepFreeze({
...route, ...route,

View File

@@ -109,6 +109,15 @@ export interface Agent {
*/ */
whenIdle(): Promise<void> whenIdle(): Promise<void>
/**
* Route identified input to an inbox boundary and optionally wake the driver.
* Waking input submitted after active cancellation is queued for the next turn.
* @param message - identified content and its producer provenance.
* @param target - the preferred next-turn or next-step inbox boundary.
* @param wakeup - whether delivery may wake the driver.
*/
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void
/** /**
* Queue an ordinary follow-up turn and wake the driver. The item becomes the * Queue an ordinary follow-up turn and wake the driver. The item becomes the
* sole ordinary message of its own turn. * sole ordinary message of its own turn.

View File

@@ -24,6 +24,7 @@ function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'idle', status: 'idle',
ctx: new Context(), ctx: new Context(),
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => {}, inject: () => {},

View File

@@ -36,6 +36,7 @@ function agent(ctx: Context, cwd: string): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'idle', status: 'idle',
ctx: scope.ctx, ctx: scope.ctx,
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => {}, inject: () => {},

View File

@@ -37,6 +37,7 @@ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session }
inbox: new Inbox(session), inbox: new Inbox(session),
ctx: new Context(), ctx: new Context(),
get status() { return status }, get status() { return status },
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject(input) { appendInjection(session, input) }, inject(input) { appendInjection(session, input) },

View File

@@ -47,6 +47,7 @@ function stubAgentForSession(session: Session): StubAgent {
inbox: new Inbox(session), inbox: new Inbox(session),
ctx: new Context(), ctx: new Context(),
get status() { return status }, get status() { return status },
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject(input) { inject(input) {

View File

@@ -38,6 +38,7 @@ function liveAgent(ctx: Context, session: Session): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
ctx, ctx,
get status() { return status }, get status() { return status },
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject(input: UserMessage) { inject(input: UserMessage) {

View File

@@ -32,6 +32,7 @@ function stubAgent(rawId: string, supplied?: Session): StubAgent {
inbox: new Inbox(session), inbox: new Inbox(session),
get status() { return status }, get status() { return status },
ctx: new Context(), ctx: new Context(),
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject(input) { inject(input) {

View File

@@ -47,6 +47,7 @@ function stubAgent(session: Session): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'idle', status: 'idle',
ctx: new Context(), ctx: new Context(),
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => {}, inject: () => {},

View File

@@ -43,7 +43,7 @@ function agent(ctx: Context): Agent {
const session = new Session(id) const session = new Session(id)
return { return {
id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx, id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
} }
} }
@@ -250,7 +250,7 @@ describe('pty-local plugin shape', () => {
const ownerFiber = await ctx.plugin(() => {}) const ownerFiber = await ctx.plugin(() => {})
const owner: Agent = { const owner: Agent = {
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: ownerFiber.ctx, id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: ownerFiber.ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
} }
ctx.agents.register(owner) ctx.agents.register(owner)
const providerFiber = await registerStubLocalBackend(ctx, () => stubLocalSession()) const providerFiber = await registerStubLocalBackend(ctx, () => stubLocalSession())
@@ -293,7 +293,7 @@ describe('pty-local plugin shape', () => {
const ownerFiber = await ctx.plugin(() => {}) const ownerFiber = await ctx.plugin(() => {})
const owner: Agent = { const owner: Agent = {
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: ownerFiber.ctx, id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: ownerFiber.ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
} }
ctx.agents.register(owner) ctx.agents.register(owner)
const gate = Promise.withResolvers<undefined>() const gate = Promise.withResolvers<undefined>()

View File

@@ -36,7 +36,7 @@ function stubAgent(ctx: Context, rawId: string): Agent {
const session = new Session(id) const session = new Session(id)
return { return {
id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: scope.ctx, id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: scope.ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
} }
} }

View File

@@ -29,6 +29,7 @@ function stubAgent(ctx: Context, rawId: string): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'idle', status: 'idle',
ctx: scopeFiber.ctx, ctx: scopeFiber.ctx,
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => {}, inject: () => {},

View File

@@ -46,6 +46,7 @@ function agent(ctx: Context, cwd: string): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'idle', status: 'idle',
ctx: scope.ctx, ctx: scope.ctx,
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => {}, inject: () => {},

View File

@@ -42,6 +42,7 @@ function agent(ctx: Context, cwd: string | undefined): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'idle', status: 'idle',
ctx: scope.ctx, ctx: scope.ctx,
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => {}, inject: () => {},

View File

@@ -41,7 +41,7 @@ function agent(ctx: Context): Agent {
const session = new Session(id) const session = new Session(id)
const value: Agent = { const value: Agent = {
id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: scope.ctx, id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: scope.ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
} }
ctx.agents.register(value) ctx.agents.register(value)
return value return value

View File

@@ -19,7 +19,7 @@ function fakeAgent(ctx: Context, rawId: string): Agent {
const session = new Session(id) const session = new Session(id)
const agent: Agent = { const agent: Agent = {
id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: scope.ctx, id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx: scope.ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
} }
ctx.agents.register(agent) ctx.agents.register(agent)
return agent return agent

View File

@@ -46,6 +46,7 @@ function agentForCwd(cwd: string): Agent {
session, session,
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'idle', status: 'idle',
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => { throw new Error('step-boundary catalog must not use agent.inject()') }, inject: () => { throw new Error('step-boundary catalog must not use agent.inject()') },
@@ -62,6 +63,7 @@ function sessionAgent(session: Session, id = 'tool-skill-agent'): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'running', status: 'running',
ctx: new Context(), ctx: new Context(),
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => { throw new Error('step-boundary catalog must not use agent.inject()') }, inject: () => { throw new Error('step-boundary catalog must not use agent.inject()') },

View File

@@ -26,6 +26,7 @@ function stubAgent(ctx: Context, rawId: string): Agent {
inbox: new Inbox(session), inbox: new Inbox(session),
status: 'idle' as const, status: 'idle' as const,
ctx: scopeFiber.ctx, ctx: scopeFiber.ctx,
send: () => {},
followup: () => {}, followup: () => {},
steer: () => {}, steer: () => {},
inject: () => {}, inject: () => {},

View File

@@ -202,6 +202,7 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
injected, injected,
injectedOptions, injectedOptions,
cancelled, cancelled,
send() {},
followup(input) { followup(input) {
sent.push(input.content) sent.push(input.content)
sentMessages.push(input) sentMessages.push(input)

View File

@@ -4905,7 +4905,7 @@ describe('terminal mounting', () => {
const session = ctx.sessions.create(SessionId('main')) const session = ctx.sessions.create(SessionId('main'))
ctx.agents.register({ ctx.agents.register({
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx, id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
}) })
const terminal = new FakeTerminal() const terminal = new FakeTerminal()
mountTui(ctx, { theme: { color: false } }, { terminal, exit: vi.fn() }) mountTui(ctx, { theme: { color: false } }, { terminal, exit: vi.fn() })
@@ -4930,7 +4930,7 @@ describe('terminal mounting', () => {
const session = ctx.sessions.create(SessionId('main')) const session = ctx.sessions.create(SessionId('main'))
ctx.agents.register({ ctx.agents.register({
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx, id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
}) })
const terminal = new FakeTerminal() const terminal = new FakeTerminal()
// Mirror dsh-tui's own inject (minus loader, the absence under test). // Mirror dsh-tui's own inject (minus loader, the absence under test).
@@ -4965,14 +4965,14 @@ describe('terminal mounting', () => {
const otherSession = ctx.sessions.create(SessionId('other-session')) const otherSession = ctx.sessions.create(SessionId('other-session'))
ctx.agents.register({ ctx.agents.register({
id: otherSession.id, options: {}, session: otherSession, inbox: new Inbox(otherSession), status: 'idle', ctx, id: otherSession.id, options: {}, session: otherSession, inbox: new Inbox(otherSession), status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
}) })
expect(terminal.started).toBe(0) expect(terminal.started).toBe(0)
const session = ctx.sessions.create(SessionId('late-session')) const session = ctx.sessions.create(SessionId('late-session'))
const agent = { const agent = {
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx, id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
} as Agent } as Agent
ctx.agents.register(agent) ctx.agents.register(agent)
await tick() await tick()
@@ -5003,7 +5003,7 @@ describe('terminal mounting', () => {
const session = ctx.sessions.create(SessionId('main-session')) const session = ctx.sessions.create(SessionId('main-session'))
ctx.agents.register({ ctx.agents.register({
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx, id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
}) })
await tick() await tick()
expect(terminal.started).toBe(0) expect(terminal.started).toBe(0)
@@ -5047,7 +5047,7 @@ describe('terminal mounting', () => {
session.append('step/start', { turn: 1, step: 1 }) session.append('step/start', { turn: 1, step: 1 })
ctx.agents.register({ ctx.agents.register({
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'running', ctx, id: session.id, options: {}, session, inbox: new Inbox(session), status: 'running', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(), send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
}) })
const terminal = new FakeTerminal() const terminal = new FakeTerminal()
terminal.start = () => { throw new Error('terminal startup failed') } terminal.start = () => { throw new Error('terminal startup failed') }