Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`, `verify-translation-pairing --write` for the touched bilingual pairs, `gen-doc-graphs`, and one typert snapshot whose ids embed character offsets. `pnpm run rescope-vendor --check` verifies the result. Renames nine vendored packages (cordis, cosmokit, schemastery and the six @cordisjs plugins) and every reference that resolves them: manifest names and dependency keys, module specifiers including declare-module merges, cordis.yml plugin names, tsconfig paths, every Markdown fence, and `docs/` prose. Directory names, upstream versions, and dependency ranges are unchanged, so vendor/README.md still reads as an upstream snapshot; its manifest table gains an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed at each fork's origin. The tutorial tier follows the rename end to end: its yaml fences named plugins the Loader can no longer resolve, its `ts ignore-check` fences disagreed with the compiled fences beside them, and its prose quoted both. The contracts that told readers to keep upstream names — the root convention and the vendoring cookbook's tree comment and manifest invariant — now say to rescope instead. Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle purity gate now names the vendored libraries a browser bundle inlines, and the files where a bare `cordis` is an agent-preset id keep that product data.
177 lines
8.4 KiB
TypeScript
177 lines
8.4 KiB
TypeScript
/**
|
|
* Agent-scoped dispatch and prompt assembly helpers. The fused dispatcher
|
|
* {@link agentEvents} couples the agent subject to its scope carrier, so the
|
|
* scope key and the payload's `agent` cannot diverge; repeat dispatchers (the
|
|
* loop driver) build it once in the agent's constructor and reuse it.
|
|
* @module @deepseek-ai/dsh-agent/dispatch
|
|
*/
|
|
|
|
import type { Context, Events } from '@deepseek-ai/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 './runtime-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: the handler's first parameter is
|
|
* a payload object carrying the `agent` subject AND the handler declares a
|
|
* `Scoped<Agent>` `this` (the scope-carrier contract). The `this` check keeps
|
|
* accidental payload-happens-to-carry-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 [infer Payload, ...unknown[]]
|
|
? Payload extends { agent: Agent } ? K : never
|
|
: never
|
|
: never
|
|
}[keyof Events]
|
|
|
|
/** The full payload object of one agent-subject event. */
|
|
type PayloadOf<K extends AgentSubjectEvent> = Params<Events[K]> extends [infer Payload, ...unknown[]] ? Payload : never
|
|
|
|
/** The event arguments AFTER the payload: the waterfall `next` when present. */
|
|
type Tail<K extends AgentSubjectEvent> = Params<Events[K]> extends [unknown, ...infer R] ? R : never
|
|
|
|
/**
|
|
* The payload as emit-side callers pass it: the full payload minus the agent
|
|
* field, which the fused dispatcher injects so subject and scope key cannot
|
|
* diverge.
|
|
*/
|
|
type PayloadRest<K extends AgentSubjectEvent> = Omit<PayloadOf<K> & object, 'agent'>
|
|
|
|
/**
|
|
* 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 into the payload.
|
|
*/
|
|
export interface AgentEventDispatch {
|
|
/**
|
|
* Fire-and-forget notification in the agent's scope. Every listener is
|
|
* invoked; synchronous throws and returned-promise rejections are logged and
|
|
* contained per listener, so a notification cannot veto lifecycle progress
|
|
* or starve a later observer.
|
|
* @param name - the agent-subject event to emit.
|
|
* @param payload - the event's payload fields; `agent` is injected.
|
|
*/
|
|
emit<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<K>): void
|
|
/**
|
|
* Awaited in-order dispatch (Cordis `serial`) in the agent's scope.
|
|
* @param name - the agent-subject event to dispatch.
|
|
* @param payload - the event's payload fields; `agent` is injected.
|
|
* @returns the serial chain's result (the first bail value, if any).
|
|
*/
|
|
serial<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<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 payload — the final element
|
|
* being the innermost `next` (the default the listener chain wraps).
|
|
* @param name - the agent-subject event to dispatch.
|
|
* @param payload - the event's payload fields; `agent` is injected.
|
|
* @param rest - the event's arguments after the payload (the `next` callback).
|
|
* @returns the waterfall's composed result.
|
|
*/
|
|
waterfall<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<K>, ...rest: Tail<K>): Return<Events[K]>
|
|
}
|
|
|
|
/**
|
|
* Build the fused scope carrier for one agent subject.
|
|
*
|
|
* The carrier is a stateless routing object. {@link agentEvents} accepts an
|
|
* existing carrier, so callers that dispatch repeatedly for the same agent
|
|
* (the loop driver) build it once in the agent's constructor and reuse it,
|
|
* keeping hot-path dispatches allocation-free.
|
|
* @param agent - the subject agent and scope key.
|
|
* @returns the carrier passed as the event dispatcher `this` value.
|
|
*/
|
|
export function agentCarrier(agent: Agent): Scoped<Agent> {
|
|
return scopeTarget(agent, agent)
|
|
}
|
|
|
|
/**
|
|
* Build a dispatcher that couples the agent subject to its scope carrier.
|
|
* @param ctx - the context to dispatch through (any context of the app).
|
|
* @param agent - the subject agent; also the scope-carrier key.
|
|
* @param carrier - the scope carrier to dispatch through; defaults to
|
|
* {@link agentCarrier} for the agent. Pass a constructor-built carrier to
|
|
* avoid rebuilding it for every dispatch.
|
|
* @returns the fused dispatcher.
|
|
*/
|
|
export function agentEvents(ctx: Context, agent: Agent, carrier: Scoped<Agent> = agentCarrier(agent)): AgentEventDispatch {
|
|
// The ordinary dispatch methods forward through Cordis' variadic mixins. The
|
|
// fused (carrier, name, payload, ...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.
|
|
const fused = <K extends AgentSubjectEvent>(payload: PayloadRest<K>): PayloadOf<K> =>
|
|
// The dispatcher owns the subject injection; callers pass PayloadRest, so
|
|
// the fused record is exactly the declared payload. The spread comes
|
|
// first, so a structurally acceptable payload that happens to carry an
|
|
// `agent` field can never override the injected subject.
|
|
({ ...payload, agent } as PayloadOf<K>)
|
|
return {
|
|
emit(name, payload) {
|
|
// Cordis emit invokes callbacks through Array.map: one synchronous throw
|
|
// starves later listeners, and returned promises are discarded. Agent
|
|
// notifications are non-vetoing, so resolve the same filtered callback
|
|
// set ourselves and contain both failure modes independently.
|
|
const args: unknown[] = [carrier, name, fused(payload)]
|
|
const callbacks = ctx.events.dispatch('emit', args)
|
|
for (const callback of callbacks) {
|
|
try {
|
|
const returned: unknown = callback(...args)
|
|
void Promise.resolve(returned).catch((error: unknown) => {
|
|
ctx.logger.warn(`agent event "${name}" listener rejected: ${String(error)}`)
|
|
})
|
|
} catch (error: unknown) {
|
|
ctx.logger.warn(`agent event "${name}" listener threw: ${String(error)}`)
|
|
}
|
|
}
|
|
},
|
|
async serial(name, payload) {
|
|
// oxlint-disable-next-line typescript/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, fused(payload))
|
|
},
|
|
waterfall(name, payload, ...rest) {
|
|
// oxlint-disable-next-line typescript/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, fused(payload), ...rest)
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit one contained agent notification without allocating a retained dispatcher.
|
|
* @param ctx - the context to dispatch through.
|
|
* @param agent - the subject agent and scope key.
|
|
* @param name - the agent-subject event to emit.
|
|
* @param payload - the event's payload fields; `agent` is injected.
|
|
*/
|
|
export function emitAgentEvent<K extends AgentSubjectEvent>(
|
|
ctx: Context,
|
|
agent: Agent,
|
|
name: K,
|
|
payload: PayloadRest<K>,
|
|
): void {
|
|
agentEvents(ctx, agent).emit(name, payload)
|
|
}
|
|
|
|
/**
|
|
* Build the prompt assembly context with agent and scope set together, so
|
|
* agent-scoped prompt and tool contributions cannot be silently omitted.
|
|
* @param agent - the agent the assembly is for.
|
|
* @param signal - the current turn's explicit control signal, when assembly belongs to a turn.
|
|
* @returns the context to pass to `assemble()`.
|
|
*/
|
|
export function assembleContextFor(agent: Agent, signal?: AbortSignal): AssembleContext {
|
|
return { agent, scope: agent, ...signal === undefined ? {} : { signal } }
|
|
}
|