refactor(agent): unify agent-scoped event signatures as payload objects

All agent/* and agent-loop/config-start-failed events take one payload
object carrying the agent subject; waterfall/serial payloads require a
signal and keep next as the final argument. PreStepContext and
RequestFailureContext are unfolded into payloads and retired.
goal/changed follows the same shape so agentEvents keeps its listener
error containment. ReactLoopAgent builds its scope carrier once in the
constructor. Regenerates scope resolvers, tool-cordis api catalog, and
docs catalogs; updates all affected listeners, tests, and the
core-data-structures docs (en + zh).
This commit is contained in:
_Kerman
2026-08-06 12:13:14 +08:00
parent bb53e25ed0
commit ccebba2349
91 changed files with 574 additions and 618 deletions

View File

@@ -17,25 +17,38 @@ type Params<F> = F extends (...args: infer P) => unknown ? P : never
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.
* 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 [Agent, ...unknown[]] ? K : never
? P extends [infer Payload, ...unknown[]]
? Payload extends { agent: Agent } ? K : never
: 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 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 as the first event argument.
* the agent itself injected into the payload.
*/
export interface AgentEventDispatch {
/**
@@ -44,30 +57,35 @@ export interface AgentEventDispatch {
* contained per listener, so a notification cannot veto lifecycle progress
* or starve a later observer.
* @param name - the agent-subject event to emit.
* @param rest - the event's arguments after the injected agent.
* @param payload - the event's payload fields; `agent` is injected.
*/
emit<K extends AgentSubjectEvent>(name: K, ...rest: Tail<K>): void
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 rest - the event's arguments after the injected agent.
* @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, ...rest: Tail<K>): Promise<Awaited<Return<Events[K]>>>
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 injected agent — the final
* element being the innermost `next` (the default the listener chain wraps).
* 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 rest - the event's arguments after the injected agent.
* @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, ...rest: Tail<K>): Return<Events[K]>
waterfall<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<K>, ...rest: Tail<K>): Return<Events[K]>
}
/**
* Return the fused scope carrier for one agent subject.
* Build the fused scope carrier for one agent subject.
*
* The carrier is a stateless routing object; callers that dispatch repeatedly
* for the same agent (the loop driver) build it once in the agent's
* constructor and reuse it, so hot-path dispatches never allocate.
* @param agent - the subject agent and scope key.
* @returns the carrier passed as the event dispatcher `this` value.
*/
@@ -84,17 +102,21 @@ export function agentCarrier(agent: Agent): Scoped<Agent> {
export function agentEvents(ctx: Context, agent: Agent): AgentEventDispatch {
const carrier = agentCarrier(agent)
// The ordinary dispatch methods forward through Cordis' variadic mixins. The
// fused (carrier, name, agent, ...rest) tuple is provably a valid argument
// 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.
({ agent, ...payload } as PayloadOf<K>)
return {
emit(name, ...rest) {
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, agent, ...rest]
const args: unknown[] = [carrier, name, fused(payload)]
const callbacks = ctx.events.dispatch('emit', args)
for (const callback of callbacks) {
try {
@@ -107,15 +129,15 @@ export function agentEvents(ctx: Context, agent: Agent): AgentEventDispatch {
}
}
},
async serial(name, ...rest) {
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, agent, ...rest)
return await serial(carrier, name, fused(payload))
},
waterfall(name, ...rest) {
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, agent, ...rest)
return waterfall(carrier, name, fused(payload), ...rest)
},
}
}
@@ -125,15 +147,15 @@ export function agentEvents(ctx: Context, agent: Agent): AgentEventDispatch {
* @param ctx - the context to dispatch through.
* @param agent - the subject agent and scope key.
* @param name - the agent-subject event to emit.
* @param rest - the event arguments after the injected agent.
* @param payload - the event's payload fields; `agent` is injected.
*/
export function emitAgentEvent<K extends AgentSubjectEvent>(
ctx: Context,
agent: Agent,
name: K,
...rest: Tail<K>
payload: PayloadRest<K>,
): void {
agentEvents(ctx, agent).emit(name, ...rest)
agentEvents(ctx, agent).emit(name, payload)
}
/**