Merge remote-tracking branch 'origin/worktree-agent-scope-design' into codex/trim-ai-prose

This commit is contained in:
Tianyi Cui
2026-07-13 12:07:23 +08:00
33 changed files with 148 additions and 126 deletions
+20 -17
View File
@@ -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)}`)
}
}))
}
}
/**