diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 9bf02a32c..2eff479f6 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -97,6 +97,9 @@ export type { PermissionHandler, PermissionRequest, PermissionRequestResult, + PostUserPromptSubmittedHandler, + PostUserPromptSubmittedHookInput, + PostUserPromptSubmittedHookOutput, ProviderConfig, ProviderModelConfig, RemoteSessionMode, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 0ba42ab76..59ce094fe 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -1140,6 +1140,9 @@ export class CopilotSession { postToolUse: this.hooks.onPostToolUse as GenericHandler | undefined, postToolUseFailure: this.hooks.onPostToolUseFailure as GenericHandler | undefined, userPromptSubmitted: this.hooks.onUserPromptSubmitted as GenericHandler | undefined, + postUserPromptSubmitted: this.hooks.onPostUserPromptSubmitted as + | GenericHandler + | undefined, sessionStart: this.hooks.onSessionStart as GenericHandler | undefined, sessionEnd: this.hooks.onSessionEnd as GenericHandler | undefined, errorOccurred: this.hooks.onErrorOccurred as GenericHandler | undefined, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index bdf02a7b0..ff358c43b 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1272,6 +1272,34 @@ export type UserPromptSubmittedHandler = ( invocation: { sessionId: string } ) => Promise | UserPromptSubmittedHookOutput | void; +/** + * Input for post-user-prompt-submitted hook. + * + * This hook runs after the runtime has transformed the submitted prompt with + * generated context such as ``, but before the transformed + * prompt is persisted to session history or sent to the model. + */ +export interface PostUserPromptSubmittedHookInput extends BaseHookInput { + prompt: string; + transformedPrompt: string; +} + +/** + * Output for post-user-prompt-submitted hook. + */ +export interface PostUserPromptSubmittedHookOutput { + modifiedTransformedPrompt?: string; + suppressOutput?: boolean; +} + +/** + * Handler for post-user-prompt-submitted hook. + */ +export type PostUserPromptSubmittedHandler = ( + input: PostUserPromptSubmittedHookInput, + invocation: { sessionId: string } +) => Promise | PostUserPromptSubmittedHookOutput | void; + /** * Input for session-start hook */ @@ -1385,6 +1413,11 @@ export interface SessionHooks { */ onUserPromptSubmitted?: UserPromptSubmittedHandler; + /** + * Called after the runtime transforms a submitted prompt and before it is stored. + */ + onPostUserPromptSubmitted?: PostUserPromptSubmittedHandler; + /** * Called when a session starts */ diff --git a/nodejs/test/e2e/hooks_extended.e2e.test.ts b/nodejs/test/e2e/hooks_extended.e2e.test.ts index caa69399e..a9954fa1c 100644 --- a/nodejs/test/e2e/hooks_extended.e2e.test.ts +++ b/nodejs/test/e2e/hooks_extended.e2e.test.ts @@ -7,6 +7,7 @@ import { z } from "zod"; import { approveAll, defineTool } from "../../src/index.js"; import type { ErrorOccurredHookInput, + PostUserPromptSubmittedHookInput, PostToolUseFailureHookInput, PostToolUseHookInput, PreToolUseHookInput, @@ -149,6 +150,40 @@ describe("Extended session hooks", async () => { await session.disconnect(); }); + it("should invoke postUserPromptSubmitted hook and modify transformed prompt", async () => { + const inputs: PostUserPromptSubmittedHookInput[] = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + hooks: { + onPostUserPromptSubmitted: async (input, invocation) => { + inputs.push(input); + expect(invocation.sessionId).toBeTruthy(); + expect(input.prompt).toContain("Answer the arithmetic question above"); + expect(input.transformedPrompt).toContain("Answer the arithmetic question above"); + expect(input.transformedPrompt).toContain(""); + + return { + modifiedTransformedPrompt: input.transformedPrompt.replace( + /.*?<\/current_datetime>\n*/s, + "What is 19 + 23? Reply with just the number.\n" + ), + }; + }, + }, + }); + + const response = await session.sendAndWait({ + prompt: "Answer the arithmetic question above.", + }); + + expect(inputs.length).toBeGreaterThan(0); + expect(inputs[0].timestamp).toBeInstanceOf(Date); + expect(inputs[0].workingDirectory).toBeDefined(); + expect(response?.data.content ?? "").toContain("42"); + + await session.disconnect(); + }); + it("should invoke sessionStart hook", async () => { const inputs: SessionStartHookInput[] = []; const session = await client.createSession({ diff --git a/test/snapshots/hooks_extended/should_invoke_postuserpromptsubmitted_hook_and_modify_transformed_prompt.yaml b/test/snapshots/hooks_extended/should_invoke_postuserpromptsubmitted_hook_and_modify_transformed_prompt.yaml new file mode 100644 index 000000000..eb8e2f6de --- /dev/null +++ b/test/snapshots/hooks_extended/should_invoke_postuserpromptsubmitted_hook_and_modify_transformed_prompt.yaml @@ -0,0 +1,12 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: |- + What is 19 + 23? Reply with just the number. + Answer the arithmetic question above. + - role: assistant + content: "42"