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:
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user