refactor: narrow synchronous extension contracts
This commit is contained in:
@@ -147,7 +147,7 @@ declare module 'cordis' {
|
||||
*/
|
||||
'tools/post-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
|
||||
/**
|
||||
* Awaited notification of the authoritative FINAL tool outcome, after the
|
||||
* Synchronous notification of the authoritative FINAL tool outcome, after the
|
||||
* complete pre/execute/post pipeline, final lossless-JSON validation, and
|
||||
* outer error normalization.
|
||||
* Unlike the three waterfalls, this seam cannot transform the result: each
|
||||
@@ -158,9 +158,9 @@ declare module 'cordis' {
|
||||
* `exec.agent`, using the same carrier as the pipeline.
|
||||
* @param exec - the execution object that traversed the pipeline.
|
||||
* @param result - a deep-frozen snapshot of the final returned result.
|
||||
* @mode parallel
|
||||
* @mode emit
|
||||
*/
|
||||
'tools/result'(this: Scoped<ToolRegistry>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): Promise<void> | void
|
||||
'tools/result'(this: Scoped<ToolRegistry>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
|
||||
/**
|
||||
* A tool was registered or unregistered, or a scoped restriction changed
|
||||
* (the available tool set changed — possibly for one scope only). An
|
||||
@@ -623,7 +623,7 @@ export class ToolRegistry extends Service {
|
||||
* Cordis effect disposer (single-shot): composite (generator) effects may
|
||||
* yield it directly — exact identity nests the teardown in order.
|
||||
*/
|
||||
register(definition: ToolDefinition): () => Promise<void> | void {
|
||||
register(definition: ToolDefinition): () => void {
|
||||
const scope = scopeOf(this.ctx)
|
||||
const name = definition.name
|
||||
const timeoutMs = definition.timeoutMs
|
||||
@@ -669,8 +669,9 @@ export class ToolRegistry extends Service {
|
||||
// effect that owns a teardown ORDER must be able to yield THIS function —
|
||||
// cordis nests a disposer out of the fiber's concurrent sibling list by
|
||||
// exact function identity, so a wrapper would silently break the nesting
|
||||
// (the agents.register() lesson). Fire-and-forget callers may still
|
||||
// discard the (always-resolved) promise.
|
||||
// (the agents.register() lesson). Cleanup is synchronous because this
|
||||
// registration installs only synchronous state and notifications.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return dispose
|
||||
}
|
||||
|
||||
@@ -697,7 +698,7 @@ export class ToolRegistry extends Service {
|
||||
* Cordis effect disposer (single-shot): composite (generator) effects may
|
||||
* yield it directly — exact identity nests the teardown in order.
|
||||
*/
|
||||
restrict(filter: ToolRestriction): () => Promise<void> | void {
|
||||
restrict(filter: ToolRestriction): () => void {
|
||||
const scope = scopeOf(this.ctx)
|
||||
if (scope === undefined) {
|
||||
throw new Error('tools.restrict() requires a scoped context (agent.ctx): a context-global restriction would mask every agent — deny the tool for the intended agent instead')
|
||||
@@ -737,8 +738,9 @@ export class ToolRegistry extends Service {
|
||||
// effect that owns a teardown ORDER must be able to yield THIS function —
|
||||
// cordis nests a disposer out of the fiber's concurrent sibling list by
|
||||
// exact function identity, so a wrapper would silently break the nesting
|
||||
// (the agents.register() lesson). Fire-and-forget callers may still
|
||||
// discard the (always-resolved) promise.
|
||||
// (the agents.register() lesson). Cleanup is synchronous because this
|
||||
// registration installs only synchronous state and notifications.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return dispose
|
||||
}
|
||||
|
||||
@@ -752,7 +754,7 @@ export class ToolRegistry extends Service {
|
||||
* @param guard - synchronous check; a returned string denies the execution.
|
||||
* @returns the exact disposer that unregisters the guard.
|
||||
*/
|
||||
guard(guard: ToolGuard): () => Promise<void> | void {
|
||||
guard(guard: ToolGuard): () => void {
|
||||
const scope = scopeOf(this.ctx)
|
||||
const registration = { guard }
|
||||
const dispose = this.ctx.effect(function* (this: ToolRegistry) {
|
||||
@@ -763,6 +765,7 @@ export class ToolRegistry extends Service {
|
||||
if (scope !== undefined && layer.size === 0) this.scopedGuards.delete(scope)
|
||||
}
|
||||
}.bind(this), 'tools.guard()')
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return dispose
|
||||
}
|
||||
|
||||
@@ -939,7 +942,7 @@ export class ToolRegistry extends Service {
|
||||
} catch (error: unknown) {
|
||||
execution = { ...base, arguments: undefined }
|
||||
const result = this.materializeFinalResult(toolErrorResult(callId, error))
|
||||
await this.notifyResult(execution, result)
|
||||
this.notifyResult(execution, result)
|
||||
return result
|
||||
}
|
||||
let result: ToolExecutionResult
|
||||
@@ -950,7 +953,7 @@ export class ToolRegistry extends Service {
|
||||
// waterfall machinery becomes an isError result, never a turn failure.
|
||||
result = this.materializeFinalResult(toolErrorResult(execution.callId, error))
|
||||
}
|
||||
await this.notifyResult(execution, result)
|
||||
this.notifyResult(execution, result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1018,20 +1021,20 @@ export class ToolRegistry extends Service {
|
||||
}
|
||||
|
||||
/** Notify final-result observers without giving them a mutation/error channel into the outcome. */
|
||||
private async notifyResult(exec: ToolExecution, result: ToolExecutionResult): Promise<void> {
|
||||
private notifyResult(exec: ToolExecution, result: ToolExecutionResult): void {
|
||||
// The pipeline is over: freeze the remaining mutable signal slot so every
|
||||
// observer sees the SAME WeakMap-keyable execution without a mutation race.
|
||||
Object.freeze(exec)
|
||||
const callbacks = this.ctx.events.dispatch('parallel', [
|
||||
const callbacks = this.ctx.events.dispatch('emit', [
|
||||
scopeTarget(this, exec.agent), 'tools/result', exec, result,
|
||||
])
|
||||
await Promise.all(callbacks.map(async (callback) => {
|
||||
for (const callback of callbacks) {
|
||||
try {
|
||||
await callback(exec, result)
|
||||
callback(exec, result)
|
||||
} catch (error: unknown) {
|
||||
this.ctx.logger.warn(`tool "${exec.name}" (${exec.callId}): tools/result observer failed: ${errorMessage(error)}`)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user