From 55a66024fa45161e20261659419c0388abea2386 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sun, 19 Jul 2026 18:38:35 +0800 Subject: [PATCH 1/2] fix: contain result observer rejections and correct bundle attribution --- packages/core/tools/src/index.ts | 8 ++++++-- packages/core/tools/tests/scoped.spec.ts | 9 +++++++-- packages/examples/agent-spine-demo/src/index.ts | 2 +- .../examples/agent-spine-demo/tests/agent-core.spec.ts | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 5fc045ace2..223ae863d7 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -958,14 +958,18 @@ export class ToolRegistry extends Service { // Freeze the remaining mutable signal slot before observers receive the // shared WeakMap-keyable execution object. Object.freeze(exec) + const reportFailure = (error: unknown): void => { + this.ctx.logger.warn(`tool "${exec.name}" (${exec.callId}): tools/result observer failed: ${errorMessage(error)}`) + } const callbacks = this.ctx.events.dispatch('emit', [ scopeTarget(this, exec.agent), 'tools/result', exec, result, ]) for (const callback of callbacks) { try { - callback(exec, result) + const returned: unknown = callback(exec, result) + void Promise.resolve(returned).catch(reportFailure) } catch (error: unknown) { - this.ctx.logger.warn(`tool "${exec.name}" (${exec.callId}): tools/result observer failed: ${errorMessage(error)}`) + reportFailure(error) } } } diff --git a/packages/core/tools/tests/scoped.spec.ts b/packages/core/tools/tests/scoped.spec.ts index 49ffc0bac9..843aeb3837 100644 --- a/packages/core/tools/tests/scoped.spec.ts +++ b/packages/core/tools/tests/scoped.spec.ts @@ -582,13 +582,18 @@ describe('scoped execution dispatch', () => { ctx.on('tools/result', () => { throw { toString: () => { throw new Error('coercion trap') } } }) + ctx.on('tools/result', () => Promise.reject(new Error('async observer failure')) as never) ctx.on('tools/result', (_exec, result) => { seen.push(result.isError) }) const result = await ctx.tools.execute({ callId: CallId('final'), name: 't', arguments: {}, agent: key }) + await Promise.resolve() expect(result).toMatchObject({ isError: true, content: [{ type: 'text', text: 'outer failure' }] }) expect(seen).toEqual([true, true]) expect(dispatchModes).toEqual(['emit']) - expect(warn).toHaveBeenCalledOnce() - expect(String(warn.mock.calls[0]?.[0])).toContain('') + expect(warn).toHaveBeenCalledTimes(2) + expect(warn.mock.calls.map(call => String(call[0]))).toEqual(expect.arrayContaining([ + expect.stringContaining(''), + expect.stringContaining('async observer failure'), + ])) }) }) diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts index a4965ca457..dae21aae76 100644 --- a/packages/examples/agent-spine-demo/src/index.ts +++ b/packages/examples/agent-spine-demo/src/index.ts @@ -137,7 +137,7 @@ export function apply(ctx: Context, config: Config): void { const nestedDshHome = config.skills?.local?.dshHome if (config.dshHome !== undefined && nestedDshHome !== undefined && resolveDshHome(config.dshHome) !== resolveDshHome(nestedDshHome)) { - throw new Error('agent-core: dshHome and skills.local.dshHome must resolve to the same directory') + throw new Error('agent-spine-demo: dshHome and skills.local.dshHome must resolve to the same directory') } const dshHome = resolveDshHome(config.dshHome ?? nestedDshHome) diff --git a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts index 5d5b336ff9..2c2445bfb4 100644 --- a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts +++ b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts @@ -280,7 +280,7 @@ describe('dsh-agent-spine-demo bundle', () => { workspaceContext: false, skills: { local: { dshHome: '/nested-dsh-home' } }, }) - }).toThrow(/must resolve to the same directory/) + }).toThrow('agent-spine-demo: dshHome and skills.local.dshHome must resolve to the same directory') }) it('places workspace instructions before the skill catalog in the session prefix', async () => { From d49808c6dd60c24e05199c375a1483898de0d9fb Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sun, 19 Jul 2026 20:14:32 +0800 Subject: [PATCH 2/2] fix(tools): avoid retaining result executions --- packages/core/tools/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 223ae863d7..239d5210bb 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -958,8 +958,9 @@ export class ToolRegistry extends Service { // Freeze the remaining mutable signal slot before observers receive the // shared WeakMap-keyable execution object. Object.freeze(exec) + const { name: toolName, callId } = exec const reportFailure = (error: unknown): void => { - this.ctx.logger.warn(`tool "${exec.name}" (${exec.callId}): tools/result observer failed: ${errorMessage(error)}`) + this.ctx.logger.warn(`tool "${toolName}" (${callId}): tools/result observer failed: ${errorMessage(error)}`) } const callbacks = this.ctx.events.dispatch('emit', [ scopeTarget(this, exec.agent), 'tools/result', exec, result,