From 82719b923d91bd20626eb11c2fdf7ca40deccb51 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Tue, 25 Aug 2026 18:58:17 -0700 Subject: [PATCH] feat(fold-core): expose post-tool execution context HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a03b8a-eaab-7c87-b441-5a165baa7bd4 --- packages/fold-core/src/HookRunner/Schema.ts | 3 + .../src/ToolRuntime/ToolRuntimeLayer.ts | 7 +++ .../test/HookRunner/PassThrough.vi.test.ts | 3 + .../HookRunner/PostToolUseHook.vi.test.ts | 9 +++ .../ToolRuntimeLiveHooks.vi.test.ts | 59 +++++++++++++++++++ 5 files changed, 81 insertions(+) diff --git a/packages/fold-core/src/HookRunner/Schema.ts b/packages/fold-core/src/HookRunner/Schema.ts index 61c7632..3129f83 100644 --- a/packages/fold-core/src/HookRunner/Schema.ts +++ b/packages/fold-core/src/HookRunner/Schema.ts @@ -70,6 +70,9 @@ export const PostToolUseHookInput = Schema.Struct({ parentAgentId: Schema.NullOr(AgentId), toolCallId: ToolCallId, toolName: Schema.String, + originalInput: Schema.Unknown, + executedInput: Schema.Unknown, + handlerResult: Schema.Unknown, result: Schema.Unknown, isFailure: Schema.Boolean, }).annotate({ identifier: 'PostToolUseHookInput' }) diff --git a/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts b/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts index 2d92449..13eecd4 100644 --- a/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts +++ b/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts @@ -349,6 +349,8 @@ const finalOutputAfterPostToolHooks = (input: { readonly parentAgentId: AgentId | null readonly toolCallId: ToolCallId readonly toolName: string + readonly originalInput: unknown + readonly executedInput: unknown readonly output: FinalToolOutput }): Effect.Effect => Effect.gen(function* () { @@ -360,6 +362,9 @@ const finalOutputAfterPostToolHooks = (input: { parentAgentId: input.parentAgentId, toolCallId: input.toolCallId, toolName: input.toolName, + originalInput: input.originalInput, + executedInput: input.executedInput, + handlerResult: input.output.result, result: input.output.result, isFailure: input.output.isFailure, }) @@ -458,6 +463,8 @@ const settlePreparedToolCall = (input: { parentAgentId: input.parentAgentId, toolCallId, toolName, + originalInput: input.prepared.original.params, + executedInput: input.prepared.params, output: handlerOutput, }) diff --git a/packages/fold-core/test/HookRunner/PassThrough.vi.test.ts b/packages/fold-core/test/HookRunner/PassThrough.vi.test.ts index 7aa603a..332d9cd 100644 --- a/packages/fold-core/test/HookRunner/PassThrough.vi.test.ts +++ b/packages/fold-core/test/HookRunner/PassThrough.vi.test.ts @@ -48,6 +48,9 @@ it.effect('layerNoHooks returns pass-through decisions', () => parentAgentId: null, toolCallId, toolName: 'echo', + originalInput: params, + executedInput: params, + handlerResult: { echoed: 'hello' }, result: { echoed: 'hello' }, isFailure: false, }) diff --git a/packages/fold-core/test/HookRunner/PostToolUseHook.vi.test.ts b/packages/fold-core/test/HookRunner/PostToolUseHook.vi.test.ts index 6a80f48..102320e 100644 --- a/packages/fold-core/test/HookRunner/PostToolUseHook.vi.test.ts +++ b/packages/fold-core/test/HookRunner/PostToolUseHook.vi.test.ts @@ -34,6 +34,9 @@ describe('HookRunner postToolUse hooks', () => { parentAgentId: null, toolCallId: ToolCallId.create(), toolName: 'echo', + originalInput: { text: 'original' }, + executedInput: { text: 'executed' }, + handlerResult: { step: 0 }, result: { step: 0 }, isFailure: false, }) @@ -71,6 +74,9 @@ describe('HookRunner postToolUse hooks', () => { parentAgentId: null, toolCallId: ToolCallId.create(), toolName: 'echo', + originalInput: { text: 'original' }, + executedInput: { text: 'executed' }, + handlerResult: { original: true }, result: { original: true }, isFailure: false, }) @@ -112,6 +118,9 @@ describe('HookRunner postToolUse hooks', () => { parentAgentId: null, toolCallId: ToolCallId.create(), toolName: 'echo', + originalInput: { text: 'original' }, + executedInput: { text: 'executed' }, + handlerResult: { original: true }, result: { original: true }, isFailure: false, }) diff --git a/packages/fold-core/test/ToolRuntime/ToolRuntimeLiveHooks.vi.test.ts b/packages/fold-core/test/ToolRuntime/ToolRuntimeLiveHooks.vi.test.ts index 218e90f..6d54744 100644 --- a/packages/fold-core/test/ToolRuntime/ToolRuntimeLiveHooks.vi.test.ts +++ b/packages/fold-core/test/ToolRuntime/ToolRuntimeLiveHooks.vi.test.ts @@ -103,3 +103,62 @@ it.effect('live preToolUse hook can update execution params without changing pro expect(projectedToolResult).not.toHaveProperty('executedInput') }), ) + +it.effect('postToolUse receives original, executed, and handler values while hooks compose in order', () => + Effect.gen(function* () { + const recorder = yield* makeEchoRecorder() + const observed = yield* Ref.make>([]) + const hookLayer = makeHookRunner({ + preToolUse: [ + { + name: 'mutate-echo', + tools: ['echo'], + handler: () => Effect.succeed({ _tag: 'continue' as const, params: { text: 'executed' } }), + }, + ], + postToolUse: [ + { + name: 'decorate-echo', + tools: ['echo'], + handler: (input) => + Ref.update(observed, (values) => [...values, input]).pipe( + Effect.as({ _tag: 'replace' as const, result: { decorated: true }, isFailure: false }), + ), + }, + { + name: 'observe-decorated-echo', + tools: ['echo'], + handler: (input) => + Ref.update(observed, (values) => [...values, input]).pipe(Effect.as({ _tag: 'keep' as const })), + }, + ], + }) + const layer = toolRuntimeBaseLayer(hookLayer, layerEchoTool(recorder)) + + const settlement = yield* Effect.gen(function* () { + const runtime = yield* ToolRuntime + return yield* runtime.settle({ + agentId, + parentAgentId: null, + assistantMessage: makeAssistantToolCall({ text: 'original' }), + }) + }).pipe(Effect.provide(layer)) + + const calls = yield* Ref.get(observed) + expect(calls).toHaveLength(2) + expect(calls[0]).toMatchObject({ + originalInput: { text: 'original' }, + executedInput: { text: 'executed' }, + handlerResult: { echoed: 'executed' }, + result: { echoed: 'executed' }, + }) + expect(calls[1]).toMatchObject({ + originalInput: { text: 'original' }, + executedInput: { text: 'executed' }, + handlerResult: { echoed: 'executed' }, + result: { decorated: true }, + }) + expect(settlement.toolResults[0]?.executedInput).toEqual({ text: 'executed' }) + expect(settlement.toolResults[0]?.message.content[0]).toMatchObject({ result: { decorated: true } }) + }), +)