feat(agent): the agent is a registration scope — Agent.ctx, setup slot, fused scoped dispatch
Every live agent owns a dsh-scope context (Agent.ctx, key = the agent), minted inside the loop's composite lifecycle effect: registrations through it are agent-visible and agent-lifetime, and agent.ctx listeners hear only that agent's dispatches. The composite yields the scope's raw disposer first (identity-nested, no un-nested window), then session entry (scoped enter captures the session carrier), then registration; teardown runs stop/drain -> unregister -> detach session -> unwind scope, keeping store/registry rollback synchronous on every failure path. CreateAgentOptions.setup(agentCtx) runs after the scope is minted and the agent registered, before agent/session-start and the loop start — the slot where a creator composes the agent's scoped world (persona sections, restrict(), scoped tools); a throwing setup unwinds inside the rollback boundary. Setup registers, it never drives. agentEvents(ctx, agent) fuses the scope carrier with the injected subject argument for every agent/* dispatch (the correct dispatch is the shortest spelling); assembleContextFor(agent) pairs the agent DX field with the scope layer selector. All loop/agent/registry dispatch sites converted; agent/* event declarations carry this: Scoped<Agent>; ctx.agent is a safe root accessor defaulting undefined, shadowed by each agent context.
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-brand": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-scope": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
@@ -31,6 +32,7 @@
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-scope": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
|
||||
117
packages/core/agent/src/dispatch.ts
Normal file
117
packages/core/agent/src/dispatch.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Fused scope-carrier dispatch for agent-subject events, plus the assembly
|
||||
* context builder. The ONE sanctioned spelling for dispatching `agent/*`
|
||||
* events: `agentEvents(ctx, agent).waterfall('agent/request', …)` builds the
|
||||
* scope carrier ({@link scopeTarget} keyed by the agent) AND injects the
|
||||
* subject as the first event argument in one move, so the correct dispatch is
|
||||
* also the shortest — a dispatch site cannot pass a carrier keyed to one
|
||||
* agent while naming another as the subject, which is the invariant the
|
||||
* dev-mode scoped-dispatch check asserts at runtime.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-agent/dispatch
|
||||
*/
|
||||
|
||||
import type { Context, Events } from 'cordis'
|
||||
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
||||
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
|
||||
* contract). The `this` check keeps accidental first-parameter-happens-to-be-
|
||||
* an-Agent events (or zero-arg events, whose parameter tuple would satisfy a
|
||||
* bare rest-tuple check via callability) out of the fused-dispatch surface.
|
||||
*/
|
||||
export type AgentSubjectEvent = {
|
||||
[K in keyof Events]: Events[K] extends (this: Scoped<Agent>, ...args: infer P) => unknown
|
||||
? P extends [Agent, ...unknown[]] ? K : never
|
||||
: never
|
||||
}[keyof Events]
|
||||
|
||||
/** The event arguments AFTER the injected agent subject. */
|
||||
type Tail<K extends AgentSubjectEvent> = Params<Events[K]> extends [Agent, ...infer R] ? R : never
|
||||
|
||||
/**
|
||||
* 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 (Cordis `emit`) in the agent's scope.
|
||||
* @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]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the fused dispatcher for `agent`'s events (see the module doc). Cheap
|
||||
* (one carrier + one small object) — dispatch sites create it per run/turn
|
||||
* rather than caching it on the agent.
|
||||
* @param ctx - the context to dispatch through (any context of the app).
|
||||
* @param agent - the subject agent; also the scope-carrier key.
|
||||
* @returns the fused dispatcher.
|
||||
*/
|
||||
export function agentEvents(ctx: Context, agent: Agent): AgentEventDispatch {
|
||||
const carrier: Scoped<Agent> = scopeTarget(agent, agent)
|
||||
// The three 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) {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method -- the events mixin accessor returns a pre-bound function
|
||||
const emit = ctx.emit as (thisArg: Scoped<Agent>, name: string, ...args: unknown[]) => void
|
||||
emit(carrier, name, agent, ...rest)
|
||||
},
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The assembly context for one agent's prompt: the typed `agent` DX field and
|
||||
* the `scope` layer selector, set together (setting `agent` without `scope`
|
||||
* silently drops the agent's scoped sections/tools from the assembly — the
|
||||
* dev invariants flag it). THE way the loop (and any custom driver) builds
|
||||
* its per-step `ctx.systemPrompt.assemble(…)` input.
|
||||
* @param agent - the agent the assembly is for.
|
||||
* @returns the context to pass to `assemble()`.
|
||||
*/
|
||||
export function assembleContextFor(agent: Agent): AssembleContext {
|
||||
return { agent, scope: agent }
|
||||
}
|
||||
@@ -6,14 +6,28 @@
|
||||
*/
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
||||
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Agent, AgentId, AgentOptions } from './types.ts'
|
||||
|
||||
export * from './types.ts'
|
||||
export { agentEvents, assembleContextFor } from './dispatch.ts'
|
||||
export type { AgentEventDispatch, AgentSubjectEvent } from './dispatch.ts'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
agents: AgentRegistry
|
||||
/**
|
||||
* The agent whose scope this context belongs to, or `undefined` on any
|
||||
* context not derived from an agent scope. Pure DX sugar over the
|
||||
* `dsh-scope` tag: the agent loop sets it as an own property on each
|
||||
* `Agent.ctx`, and {@link AgentRegistry} registers a root accessor
|
||||
* defaulting to `undefined` so the read is safe on every context (a plain
|
||||
* plugin context answers `undefined` instead of throwing the Cordis
|
||||
* unknown-property error). Core packages below the agent layer read the
|
||||
* `dsh-scope` tag (`scopeOf`) instead, never this field.
|
||||
*/
|
||||
agent?: Agent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +65,20 @@ export interface CreateAgentOptions {
|
||||
seed?: SessionEvent[]
|
||||
/** Per-agent options (model, …). */
|
||||
agentOptions?: AgentOptions
|
||||
/**
|
||||
* Creation-time composition of the agent's scoped world. The factory runs it
|
||||
* inside the agent's composite lifecycle effect — after the scope is minted
|
||||
* and the agent registered, before `agent/session-start` fires and the loop
|
||||
* starts — so everything it registers through `agentCtx` (scoped tools,
|
||||
* prompt sections/variables, `restrict()`, listeners, `agentCtx.plugin(…)`
|
||||
* profiles) exists before the first prompt assembly, and a THROWING setup
|
||||
* unwinds inside the rollback boundary instead of leaking a half-created
|
||||
* agent. **Setup registers, it never drives**: calling
|
||||
* `send`/`steer`/`inject` here would open a turn before `agent/session-start`
|
||||
* (the dev invariants flag a `turn/start` logged before session-start as a
|
||||
* teaching error) — drive the agent after creation returns.
|
||||
*/
|
||||
setup?: (agentCtx: Context) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +148,13 @@ export class AgentRegistry extends Service {
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'agents')
|
||||
// The `ctx.agent` DX accessor: default `undefined` on every context, so a
|
||||
// plain plugin context reads cleanly instead of hitting the Cordis
|
||||
// unknown-property throw. Each Agent.ctx shadows it with an own property
|
||||
// (own properties resolve before the context proxy is consulted), so the
|
||||
// accessor body never needs to resolve a scope itself. Effect-scoped:
|
||||
// unwinds with this service's fiber.
|
||||
ctx.accessor('agent', { get: () => undefined })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +202,11 @@ export class AgentRegistry extends Service {
|
||||
/**
|
||||
* Register a live agent. Throws if an agent with the same id is already
|
||||
* registered. Emits `agent/created` on registration and `agent/disposed`
|
||||
* when the calling fiber is disposed. Returns the disposer.
|
||||
* when the calling fiber is disposed — both with the agent's scope carrier
|
||||
* (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
|
||||
* emits are scope-filtered regardless of which context invoked `register`
|
||||
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
|
||||
* requires passing the carrier). Returns the disposer.
|
||||
* @param agent - the already-constructed agent to record in the store.
|
||||
* @returns the disposer that removes the agent and emits `agent/disposed`.
|
||||
*/
|
||||
@@ -196,12 +235,12 @@ export class AgentRegistry extends Service {
|
||||
// logging the listener bug and continuing is correct (mirrors the
|
||||
// guarded `agent/status` emit in dsh-agent-loop's ReactLoopAgent).
|
||||
try {
|
||||
this.ctx.emit('agent/disposed', agent)
|
||||
this.ctx.emit(scopeTarget(agent, agent), 'agent/disposed', agent)
|
||||
} catch (error: unknown) {
|
||||
this.ctx.logger.warn(`agent "${agent.id}": agent/disposed listener threw: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
this.ctx.emit('agent/created', agent)
|
||||
this.ctx.emit(scopeTarget(agent, agent), 'agent/created', agent)
|
||||
}.bind(this), 'agents.register()')
|
||||
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
||||
// synchronous fire-and-forget — discard the (always-resolved) promise.
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
*/
|
||||
|
||||
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
import type { Context } from 'cordis'
|
||||
import type { Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import type { ContentBlock, LlmCallConfig, Message, MessageSource } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
|
||||
@@ -64,10 +66,14 @@ declare module '@deepseek-ai/dsh-system-prompt' {
|
||||
interface AssembleContext {
|
||||
/**
|
||||
* The agent this assembly is for. The agent loop passes it on every
|
||||
* per-step `assemble({ agent })`; variable providers project per-agent
|
||||
* facts from it (`options.model` → `{{model}}`, `session.header.cwd` →
|
||||
* per-step assembly (via its `assembleContextFor(agent)` helper, which
|
||||
* also sets the `scope` field to the same agent — the layer selector
|
||||
* `dsh-system-prompt` reads); variable providers project per-agent facts
|
||||
* from it (`options.model` → `{{model}}`, `session.header.cwd` →
|
||||
* `{{cwd}}`). Optional because a bare `assemble()` (tests, diagnostics)
|
||||
* has no agent — providers must tolerate its absence.
|
||||
* has no agent — providers must tolerate its absence. Never set `agent`
|
||||
* without `scope`: the assembly would silently miss the agent's scoped
|
||||
* sections/tools (the dev invariants flag it).
|
||||
*/
|
||||
agent?: Agent
|
||||
}
|
||||
@@ -175,6 +181,17 @@ export interface Agent {
|
||||
readonly options: AgentOptions
|
||||
readonly session: Session
|
||||
readonly status: AgentStatus
|
||||
/**
|
||||
* The agent's scope context (`@deepseek-ai/dsh-scope`, key = this agent).
|
||||
* Registrations through it — tools, prompt sections/variables, event
|
||||
* listeners, restrictions — are visible to THIS agent only and unwind when
|
||||
* the agent is disposed; `agent.ctx.on('agent/…')` listeners fire only for
|
||||
* this agent's dispatches (zero self-filtering). Service resolution through
|
||||
* it flows through the loop plugin's dependency surface — handing out
|
||||
* `agent.ctx` hands out that capability. Live for exactly the agent's
|
||||
* lifetime: registrations after disposal throw Cordis's INACTIVE_EFFECT.
|
||||
*/
|
||||
readonly ctx: Context
|
||||
|
||||
/** Queue a user message. Starts a turn when idle; otherwise waits for the next turn. */
|
||||
send(content: ContentBlock[], options?: SendOptions): void
|
||||
@@ -259,34 +276,54 @@ declare module 'cordis' {
|
||||
* An agent was registered in the {@link AgentRegistry} and is ready to
|
||||
* receive messages.
|
||||
* @param agent - the newly registered agent, already resolvable in the registry.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/created'(agent: Agent): void
|
||||
'agent/created'(this: Scoped<Agent>, agent: Agent): void
|
||||
/**
|
||||
* An agent was disposed and removed from the registry; its fiber and any
|
||||
* in-flight turn have been torn down.
|
||||
* @param agent - the agent that was torn down; its handle is now inert.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/disposed'(agent: Agent): void
|
||||
'agent/disposed'(this: Scoped<Agent>, agent: Agent): void
|
||||
/**
|
||||
* Agent status changed (`idle` ⇄ `running`, or → `disposed`). Drive
|
||||
* lifecycle off this transition, never off a status you just requested —
|
||||
* `send()` does not flip status to `running` before it returns.
|
||||
* @param agent - the agent whose status flipped.
|
||||
* @param status - the status just entered (the transition's destination).
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/status'(agent: Agent, status: AgentStatus): void
|
||||
'agent/status'(this: Scoped<Agent>, agent: Agent, status: AgentStatus): void
|
||||
/**
|
||||
* A message entered the agent's inbox (queued or steering). `source` is
|
||||
* the resolved source (defaults applied), not the caller's raw options.
|
||||
* @param agent - the agent whose inbox received the message.
|
||||
* @param content - the enqueued content blocks, verbatim.
|
||||
* @param info - the resolved source plus whether it entered as steering.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/queued'(agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void
|
||||
'agent/queued'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void
|
||||
|
||||
// ---- session lifecycle (emit) ----
|
||||
/**
|
||||
@@ -299,9 +336,14 @@ declare module 'cordis' {
|
||||
* is deliberate (a bridge logs/injects, it does not gate startup).
|
||||
* @param agent - the agent whose session lifecycle began.
|
||||
* @param source - why the session started (fresh startup, resume, …).
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/session-start'(agent: Agent, source: SessionStartSource): void
|
||||
'agent/session-start'(this: Scoped<Agent>, agent: Agent, source: SessionStartSource): void
|
||||
|
||||
// Turn and step boundaries are NOT mirrored as agent/* emits: a consumer
|
||||
// that needs them reads the durable `turn/start`/`turn/end`/`step/start`/
|
||||
@@ -334,6 +376,11 @@ declare module 'cordis' {
|
||||
* listener needs to measure pressure (the system prompt counts toward the
|
||||
* budget). `signal` cancels any in-flight work a listener starts (e.g. a
|
||||
* summarization model call).
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @param agent - the agent about to open the step.
|
||||
* @param turn - the already-open turn this step belongs to.
|
||||
* @param step - the number of the step about to start.
|
||||
@@ -346,7 +393,7 @@ declare module 'cordis' {
|
||||
// reads. Revisit if no second consumer appears: e.g. hand listeners a lazy
|
||||
// prompt provider, or move token-pressure measurement behind a
|
||||
// compaction-specific seam instead of the shared pre-step checkpoint.
|
||||
'agent/pre-step'(agent: Agent, turn: number, step: number, fullSystemPrompt: string, signal: AbortSignal): Promise<void> | void
|
||||
'agent/pre-step'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, fullSystemPrompt: string, signal: AbortSignal): Promise<void> | void
|
||||
/**
|
||||
* Waterfall: decide what happens to ONE drained queued message before it
|
||||
* becomes a `user/message` — allow (optionally rewriting the prompt bytes or
|
||||
@@ -357,9 +404,14 @@ declare module 'cordis' {
|
||||
* @param agent - the agent draining its inbox.
|
||||
* @param content - the drained message's blocks, as queued.
|
||||
* @param source - the message's resolved source.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'agent/prompt-submit'(agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise<PromptDecision>): Promise<PromptDecision>
|
||||
'agent/prompt-submit'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise<PromptDecision>): Promise<PromptDecision>
|
||||
/**
|
||||
* Waterfall: shape the step's call configuration — model switching,
|
||||
* sampling overrides — by returning a replacement {@link LlmCallConfig}
|
||||
@@ -380,9 +432,14 @@ declare module 'cordis' {
|
||||
* @param turn - the open turn number.
|
||||
* @param step - the step whose request this is.
|
||||
* @param config - the config the loop would use (frozen); return a replacement to switch.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'agent/request'(agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
|
||||
'agent/request'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
|
||||
/**
|
||||
* Waterfall: post-process the assembled assistant {@link Message} before
|
||||
* tool dispatch (validation, content rewriting, …).
|
||||
@@ -390,9 +447,14 @@ declare module 'cordis' {
|
||||
* @param turn - the open turn number.
|
||||
* @param step - the step that produced the message.
|
||||
* @param message - the assistant message as assembled from the stream.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'agent/step-result'(agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message>
|
||||
'agent/step-result'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message>
|
||||
/**
|
||||
* Waterfall: override the turn-continuation decision via a typed
|
||||
* {@link ContinuationDecision}. The loop's `defaultDecision` is `continue`
|
||||
@@ -403,9 +465,14 @@ declare module 'cordis' {
|
||||
* @param agent - the agent deciding whether to run another step.
|
||||
* @param turn - the turn being continued or stopped.
|
||||
* @param defaultDecision - what the loop would do absent an override.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'agent/turn-continuation'(agent: Agent, turn: number, defaultDecision: ContinuationDecision, next: () => Promise<ContinuationDecision>): Promise<ContinuationDecision>
|
||||
'agent/turn-continuation'(this: Scoped<Agent>, agent: Agent, turn: number, defaultDecision: ContinuationDecision, next: () => Promise<ContinuationDecision>): Promise<ContinuationDecision>
|
||||
|
||||
// ---- error notifications (emit) ----
|
||||
/**
|
||||
@@ -415,8 +482,13 @@ declare module 'cordis' {
|
||||
* @param turn - the turn in which the failure surfaced.
|
||||
* @param step - the step at which the failure surfaced.
|
||||
* @param error - the failure, verbatim.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): a listener registered
|
||||
* through `agent.ctx` fires only for that agent's dispatches; a listener on a
|
||||
* plain plugin context fires for every agent. The dispatch `this` is the
|
||||
* scope carrier (`Scoped<Agent>`), built by the emitting side via
|
||||
* `scopeTarget`/`agentEvents`.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/error'(agent: Agent, turn: number, step: number, error: Error): void
|
||||
'agent/error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: Error): void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ function stubAgent(rawId: string): Agent {
|
||||
options: {},
|
||||
session: new Session(SessionId(`${id}-session`)),
|
||||
status: 'idle',
|
||||
// A bare context stands in for the agent scope: registry tests never
|
||||
// register through it, they only need the field present.
|
||||
ctx: new Context(),
|
||||
send() {},
|
||||
steer() {},
|
||||
inject() {},
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
{
|
||||
"path": "../../util/brand"
|
||||
},
|
||||
{
|
||||
"path": "../../core/scope"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user