Repository: langchain-ai/langchainjs
Pull request #8056: fix(core): Infer tool return types, with related tool type fixes
Author: login: hntrl
Labels: name: size:XL
description: This PR changes 500-999 lines, ignoring generated files.
Created: 2025-04-23T18:12:37Z | Merged: 2025-05-07T01:39:51Z

Fixes #8010. Instead of just using an `any` alias as the return type to invoke, this adds a generic `ToolOutputT` type param on the tool classes that decides the return type of invoke. 

A later enhancement could be to restrict the return type of the provided function if "content_and_artifact" is provided as the response format (i.e. instead of `any[]`, the return type is forced to be `extends [any, any]` in the tool function)

cc @benjamincburns

Linked issue #8010: `tool().invoke()` widens to `any` always
### Checked other resources

- [x] I added a very descriptive title to this issue.
- [x] I searched the LangChain.js documentation with the integrated search.
- [x] I used the GitHub search to find a similar question and didn't find it.
- [x] I am sure that this is a bug in LangChain.js rather than my code.
- [x] The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).

### Example Code

[TypeScript Playground Link](https://www.typescriptlang.org/play/?target=11#code/JYWwDg9gTgLgBAbzjCEA2cC+cBmUIhwBEAAmgIYB2A5gMYAW5wlA9LdAKYsroDORAbgBQoSLERwAXllz5CAckkQAJvOFD2lXvBABXNDGBg0ATzgBeZKjQAKIXDg2k5ADRwARlgBcE8j8q6IO4cUAIe-oHBUFgAlBFBIRYAfIj2DnAsAFSZaemZcACy+obGZuRwVMoeAHS5DpksdVAcMLpQlBVw+e7CDpguaQh1lOQgHD5EegZGpkQD6XDKHLy0UEaGEJQTRdOlyADuEHABCVD88+kr9BwgflLVEO4AVhy0ME51DneS1SdRNjELgt3D4fn8QgCgbELpghDF1JptHBmrxihYKvsmDpijMTNVmAA3CAAaw4TjuAEY3CC4BTYsIWCwFgA9AD8QkZxwghloHDg9Ag+zgwHgvHIJl4cAABlQTFKAIRwXgC-TKSjyeAijx87RrGhwAA+cAAKtYCssxdQOKygA)

But in summary:

```ts
import { tool } from "@langchain/core/tools";
import { z } from 'zod';

const multiply = tool(
  ({ a, b }): number => a * b,
  {
    name: "multiply",
    description: "Multiply two numbers",
    schema: z.object({
      a: z.number(),
      b: z.number(),
    }),
  }
);

const result = await multiply.invoke({a: 1, b: 1 });
//    ^? this is `any` instead of something useful
```

### Error Message and Stack Trace (if applicable)

_No response_

### Description

My hope was that a manual tool invocation would be somewhat type-safe, but it resolves to `any`.

https://github.com/langchain-ai/langchainjs/blob/626247f65e88fc6a8d1f592d5f38680fc1ac3923/langchain-core/src/tools/index.ts#L610-L640

Unfortunately `ToolReturnType` is typed as `any`, which seems to erase all the hard work the types are doing! I'm not sure why this would be.

### System Info

```
@langchain/community 0.3.34
└── langchain 0.3.21
agentevals 0.0.4
├── langchain 0.3.21
└─┬ openevals 0.0.3
  └── langchain 0.3.21
langchain 0.3.21
```

Diff:
diff --git a/langchain-core/src/messages/tool.ts b/langchain-core/src/messages/tool.ts
index a22217a8b34c..9dfe48f6ad76 100644
--- a/langchain-core/src/messages/tool.ts
+++ b/langchain-core/src/messages/tool.ts
@@ -41,7 +41,7 @@ export interface ToolMessageFieldsWithToolCallId extends ToolMessageFields {
  * a string and wrapped in a ToolMessage.
  */
 export interface DirectToolOutput {
-  readonly lc_direct_tool_output: boolean;
+  readonly lc_direct_tool_output: true;
 }
 
 export function isDirectToolOutput(x: unknown): x is DirectToolOutput {
@@ -68,7 +68,7 @@ export class ToolMessage extends BaseMessage implements DirectToolOutput {
     return { tool_call_id: "tool_call_id" };
   }
 
-  lc_direct_tool_output = true;
+  lc_direct_tool_output = true as const;
 
   /**
    * Status of the tool invocation.
diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts
index 5ee5a099223c..942cfa749bca 100644
--- a/langchain-core/src/tools/index.ts
+++ b/langchain-core/src/tools/index.ts
@@ -6,7 +6,6 @@ import {
 import {
   CallbackManager,
   CallbackManagerForToolRun,
-  Callbacks,
   parseCallbackConfigArg,
 } from "../callbacks/manager.js";
 import { BaseLangChain } from "../language_models/base.js";
@@ -19,7 +18,11 @@ import {
 import type { RunnableFunc } from "../runnables/base.js";
 import { isDirectToolOutput, ToolCall, ToolMessage } from "../messages/tool.js";
 import { AsyncLocalStorageProviderSingleton } from "../singletons/index.js";
-import { _isToolCall, ToolInputParsingException } from "./utils.js";
+import {
+  _configHasToolCallId,
+  _isToolCall,
+  ToolInputParsingException,
+} from "./utils.js";
 import { isZodSchema } from "../utils/types/is_zod_schema.js";
 import type {
   StructuredToolCallInput,
@@ -34,6 +37,9 @@ import type {
   DynamicToolInput,
   DynamicStructuredToolInput,
   ZodObjectAny,
+  StringInputToolSchema,
+  ToolInterface,
+  ToolOutputType,
 } from "./types.js";
 import { type JSONSchema, validatesOnlyStrings } from "../utils/json_schema.js";
 
@@ -65,13 +71,17 @@ export { ToolInputParsingException };
  * Base class for Tools that accept input of any shape defined by a Zod schema.
  */
 export abstract class StructuredTool<
-  SchemaT extends ToolInputSchemaBase = ZodObjectAny,
-  SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
-  SchemaInputT = ToolInputSchemaInputType<SchemaT>
-> extends BaseLangChain<
-  StructuredToolCallInput<SchemaT, SchemaInputT>,
-  ToolReturnType
-> {
+    SchemaT extends ToolInputSchemaBase = ToolInputSchemaBase,
+    SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
+    SchemaInputT = ToolInputSchemaInputType<SchemaT>,
+    ToolOutputT = ToolOutputType
+  >
+  extends BaseLangChain<
+    StructuredToolCallInput<SchemaT, SchemaInputT>,
+    ToolOutputT | ToolMessage
+  >
+  implements StructuredToolInterface<SchemaT, SchemaInputT, ToolOutputT>
+{
   abstract name: string;
 
   abstract description: string;
@@ -115,19 +125,21 @@ export abstract class StructuredTool<
     arg: SchemaOutputT,
     runManager?: CallbackManagerForToolRun,
     parentConfig?: ToolRunnableConfig
-  ): Promise<ToolReturnType>;
+  ): Promise<ToolOutputT>;
 
   /**
    * Invokes the tool with the provided input and configuration.
    * @param input The input for the tool.
    * @param config Optional configuration for the tool.
-   * @returns A Promise that resolves with a string.
+   * @returns A Promise that resolves with the tool's output.
    */
-  async invoke(
-    input: StructuredToolCallInput<SchemaT, SchemaInputT>,
-    config?: RunnableConfig
-  ): Promise<ToolReturnType> {
-    let tool_call_id: string | undefined;
+  async invoke<
+    TInput extends StructuredToolCallInput<SchemaT, SchemaInputT>,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    input: TInput,
+    config?: TConfig
+  ): Promise<ToolReturnType<TInput, TConfig, ToolOutputT>> {
     let toolInput: Exclude<
       StructuredToolCallInput<SchemaT, SchemaInputT>,
       ToolCall
@@ -135,7 +147,6 @@ export abstract class StructuredTool<
 
     let enrichedConfig: ToolRunnableConfig = ensureConfig(config);
     if (_isToolCall(input)) {
-      tool_call_id = input.id;
       toolInput = input.args as Exclude<
         StructuredToolCallInput<SchemaT, SchemaInputT>,
         ToolCall
@@ -143,10 +154,6 @@ export abstract class StructuredTool<
       enrichedConfig = {
         ...enrichedConfig,
         toolCall: input,
-        configurable: {
-          ...enrichedConfig.configurable,
-          tool_call_id,
-        },
       };
     } else {
       toolInput = input as Exclude<
@@ -155,7 +162,9 @@ export abstract class StructuredTool<
       >;
     }
 
-    return this.call(toolInput, enrichedConfig);
+    return this.call(toolInput, enrichedConfig) as Promise<
+      ToolReturnType<TInput, TConfig, ToolOutputT>
+    >;
   }
 
   /**
@@ -169,25 +178,39 @@ export abstract class StructuredTool<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  async call(
-    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
-    configArg?: Callbacks | ToolRunnableConfig,
+  async call<
+    TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    arg: TArg,
+    configArg?: TConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<ToolReturnType> {
-    let parsed = arg;
+  ): Promise<ToolReturnType<TArg, TConfig, ToolOutputT>> {
+    // Determine the actual input that needs parsing/validation.
+    // If arg is a ToolCall, use its args; otherwise, use arg directly.
+    const inputForValidation = _isToolCall(arg) ? arg.args : arg;
+
+    let parsed: SchemaOutputT; // This will hold the successfully parsed input of the expected output type.
     if (isZodSchema(this.schema)) {
       try {
-        parsed = await (this.schema as z.ZodSchema).parseAsync(arg);
+        // Validate the inputForValidation - TS needs help here as it can't exclude ToolCall based on the check
+        parsed = await (this.schema as z.ZodSchema).parseAsync(
+          inputForValidation as Exclude<TArg, ToolCall>
+        );
       } catch (e) {
         let message = `Received tool input did not match expected schema`;
         if (this.verboseParsingErrors) {
           message = `${message}\nDetails: ${(e as Error).message}`;
         }
+        // Pass the original raw input arg to the exception
         throw new ToolInputParsingException(message, JSON.stringify(arg));
       }
     } else {
-      const result = validate(arg, this.schema as ValidationSchema);
+      const result = validate(
+        inputForValidation,
+        this.schema as ValidationSchema
+      );
       if (!result.valid) {
         let message = `Received tool input did not match expected schema`;
         if (this.verboseParsingErrors) {
@@ -195,8 +218,12 @@ export abstract class StructuredTool<
             .map((e) => `${e.keywordLocation}: ${e.error}`)
             .join("\n")}`;
         }
+        // Pass the original raw input arg to the exception
         throw new ToolInputParsingException(message, JSON.stringify(arg));
       }
+      // Assign the validated input to parsed
+      // We cast here because validate() doesn't narrow the type sufficiently for TS, but we know it's valid.
+      parsed = inputForValidation as SchemaOutputT;
     }
 
     const config = parseCallbackConfigArg(configArg);
@@ -211,7 +238,8 @@ export abstract class StructuredTool<
     );
     const runManager = await callbackManager_?.handleToolStart(
       this.toJSON(),
-      typeof parsed === "string" ? parsed : JSON.stringify(parsed),
+      // Log the original raw input arg
+      typeof arg === "string" ? arg : JSON.stringify(arg),
       config.runId,
       undefined,
       undefined,
@@ -221,7 +249,8 @@ export abstract class StructuredTool<
     delete config.runId;
     let result;
     try {
-      result = await this._call(parsed as SchemaOutputT, runManager, config);
+      // Pass the correctly typed parsed input to _call
+      result = await this._call(parsed, runManager, config);
     } catch (e) {
       await runManager?.handleToolError(e);
       throw e;
@@ -243,41 +272,43 @@ export abstract class StructuredTool<
     }
 
     let toolCallId: string | undefined;
-    if (config && "configurable" in config) {
-      toolCallId = (config.configurable as Record<string, string | undefined>)
-        .tool_call_id;
+    // Extract toolCallId ONLY if the original arg was a ToolCall
+    if (_isToolCall(arg)) {
+      toolCallId = arg.id;
     }
-    const formattedOutput = _formatToolOutput({
+    // Or if it was provided in the config's toolCall property
+    if (!toolCallId && _configHasToolCallId(config)) {
+      toolCallId = config.toolCall.id;
+    }
+
+    const formattedOutput = _formatToolOutput<ToolOutputT>({
       content,
       artifact,
       toolCallId,
       name: this.name,
     });
     await runManager?.handleToolEnd(formattedOutput);
-    return formattedOutput;
+    return formattedOutput as ToolReturnType<TArg, TConfig, ToolOutputT>;
   }
 }
 
 /**
  * Base class for Tools that accept input as a string.
  */
-export abstract class Tool extends StructuredTool<
-  z.ZodEffects<
-    z.ZodObject<
-      { input: z.ZodOptional<z.ZodString> },
-      "strip",
-      z.ZodTypeAny,
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      any,
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      any
-    >,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    any,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    any
+export abstract class Tool<ToolOutputT = ToolOutputType>
+  extends StructuredTool<
+    StringInputToolSchema,
+    ToolInputSchemaOutputType<StringInputToolSchema>,
+    ToolInputSchemaInputType<StringInputToolSchema>,
+    ToolOutputT
   >
-> {
+  implements
+    ToolInterface<
+      StringInputToolSchema,
+      ToolInputSchemaInputType<StringInputToolSchema>,
+      ToolOutputT
+    >
+{
   schema = z
     .object({ input: z.string().optional() })
     .transform((obj) => obj.input);
@@ -295,21 +326,30 @@ export abstract class Tool extends StructuredTool<
    * @param callbacks Optional callbacks for the tool.
    * @returns A Promise that resolves with a string.
    */
-  call(
-    arg: string | undefined | z.input<this["schema"]> | ToolCall,
-    callbacks?: Callbacks | RunnableConfig
-  ): Promise<ToolReturnType> {
-    return super.call(
-      typeof arg === "string" || !arg ? { input: arg } : arg,
-      callbacks
-    );
+  // Match the base class signature including the generics and conditional return type
+  call<
+    TArg extends string | undefined | z.input<this["schema"]> | ToolCall,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    arg: TArg,
+    callbacks?: TConfig
+  ): Promise<ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>> {
+    // Prepare the input for the base class call method.
+    // If arg is string or undefined, wrap it; otherwise, pass ToolCall or { input: ... } directly.
+    const structuredArg =
+      typeof arg === "string" || arg == null ? { input: arg } : arg;
+
+    // Ensure TConfig is passed to super.call
+    return super.call(structuredArg, callbacks);
   }
 }
 
 /**
  * A tool that can be created dynamically from a function, name, and description.
  */
-export class DynamicTool extends Tool {
+export class DynamicTool<
+  ToolOutputT = ToolOutputType
+> extends Tool<ToolOutputT> {
   static lc_name() {
     return "DynamicTool";
   }
@@ -318,9 +358,9 @@ export class DynamicTool extends Tool {
 
   description: string;
 
-  func: DynamicToolInput["func"];
+  func: DynamicToolInput<ToolOutputT>["func"];
 
-  constructor(fields: DynamicToolInput) {
+  constructor(fields: DynamicToolInput<ToolOutputT>) {
     super(fields);
     this.name = fields.name;
     this.description = fields.description;
@@ -331,23 +371,28 @@ export class DynamicTool extends Tool {
   /**
    * @deprecated Use .invoke() instead. Will be removed in 0.3.0.
    */
-  async call(
-    arg: string | undefined | z.input<this["schema"]> | ToolCall,
-    configArg?: ToolRunnableConfig | Callbacks
-  ): Promise<ToolReturnType> {
+  async call<
+    TArg extends string | undefined | z.input<this["schema"]> | ToolCall,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    arg: TArg,
+    configArg?: TConfig
+  ): Promise<ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>> {
     const config = parseCallbackConfigArg(configArg);
     if (config.runName === undefined) {
       config.runName = this.name;
     }
-    return super.call(arg, config);
+    // Call the Tool class's call method, passing generics through
+    // Cast config to TConfig to satisfy the super.call signature
+    return super.call<TArg, TConfig>(arg, config as TConfig);
   }
 
   /** @ignore */
   async _call(
-    input: string,
+    input: string, // DynamicTool's _call specifically expects a string after schema transformation
     runManager?: CallbackManagerForToolRun,
     parentConfig?: ToolRunnableConfig
-  ): Promise<ToolReturnType> {
+  ): Promise<ToolOutputT> {
     return this.func(input, runManager, parentConfig);
   }
 }
@@ -362,10 +407,11 @@ export class DynamicTool extends Tool {
  * input if JSON schema is passed.
  */
 export class DynamicStructuredTool<
-  SchemaT extends ToolInputSchemaBase = ZodObjectAny,
+  SchemaT extends ToolInputSchemaBase = ToolInputSchemaBase,
   SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
-  SchemaInputT = ToolInputSchemaInputType<SchemaT>
-> extends StructuredTool<SchemaT, SchemaOutputT, SchemaInputT> {
+  SchemaInputT = ToolInputSchemaInputType<SchemaT>,
+  ToolOutputT = ToolOutputType
+> extends StructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT> {
   static lc_name() {
     return "DynamicStructuredTool";
   }
@@ -374,11 +420,13 @@ export class DynamicStructuredTool<
 
   description: string;
 
-  func: DynamicStructuredToolInput<SchemaT, SchemaOutputT>["func"];
+  func: DynamicStructuredToolInput<SchemaT, SchemaOutputT, ToolOutputT>["func"];
 
   schema: SchemaT;
 
-  constructor(fields: DynamicStructuredToolInput<SchemaT, SchemaOutputT>) {
+  constructor(
+    fields: DynamicStructuredToolInput<SchemaT, SchemaOutputT, ToolOutputT>
+  ) {
     super(fields);
     this.name = fields.name;
     this.description = fields.description;
@@ -390,18 +438,24 @@ export class DynamicStructuredTool<
   /**
    * @deprecated Use .invoke() instead. Will be removed in 0.3.0.
    */
-  async call(
-    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
-    configArg?: RunnableConfig | Callbacks,
+  // Match the base class signature
+  async call<
+    TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    arg: TArg,
+    configArg?: TConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<ToolReturnType> {
+  ): Promise<ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>> {
     const config = parseCallbackConfigArg(configArg);
     if (config.runName === undefined) {
       config.runName = this.name;
     }
 
-    return super.call(arg, config, tags);
+    // Call the base class method, passing generics through
+    // Cast config to TConfig to satisfy the super.call signature
+    return super.call<TArg, TConfig>(arg, config as TConfig, tags);
   }
 
   protected _call(
@@ -410,7 +464,7 @@ export class DynamicStructuredTool<
     >[0],
     runManager?: CallbackManagerForToolRun,
     parentConfig?: RunnableConfig
-  ): Promise<ToolReturnType> {
+  ): Promise<ToolOutputT> {
     return this.func(arg, runManager, parentConfig);
   }
 }
@@ -432,10 +486,12 @@ export abstract class BaseToolkit {
  * Parameters for the tool function.
  * Schema can be provided as Zod or JSON schema.
  * Both schema types will be validated.
- * @template {ZodObjectAny | z.ZodString | JSONSchema = ZodObjectAny} RunInput The input schema for the tool. Either any Zod object, a Zod string, or JSON schema.
+ * @template {ToolInputSchemaBase} RunInput The input schema for the tool.
  */
 interface ToolWrapperParams<
-  RunInput extends ZodObjectAny | z.ZodString | JSONSchema = ZodObjectAny
+  RunInput extends ToolInputSchemaBase | undefined =
+    | ToolInputSchemaBase
+    | undefined
 > extends ToolParams {
   /**
    * The name of the tool. If using with an LLM, this
@@ -478,9 +534,10 @@ interface ToolWrapperParams<
  * Schema can be provided as Zod or JSON schema, and both will be validated.
  *
  * @function
- * @template {ZodObjectAny | z.ZodString | JSONSchema = ZodObjectAny} SchemaT The input schema for the tool. Either any Zod object, a Zod string, or JSON schema instance.
+ * @template {ToolInputSchemaBase} SchemaT The input schema for the tool.
+ * @template {ToolReturnType} ToolOutputT The output type of the tool.
  *
- * @param {RunnableFunc<z.output<SchemaT>, ToolReturnType>} func - The function to invoke when the tool is called.
+ * @param {RunnableFunc<z.output<SchemaT>, ToolOutputT>} func - The function to invoke when the tool is called.
  * @param {ToolWrapperParams<SchemaT>} fields - An object containing the following properties:
  * @param {string} fields.name The name of the tool.
  * @param {string | undefined} fields.description The description of the tool. Defaults to either the description on the Zod schema, or `${fields.name} tool`.
@@ -488,50 +545,55 @@ interface ToolWrapperParams<
  *
  * @returns {DynamicStructuredTool<SchemaT>} A new StructuredTool instance.
  */
-export function tool<SchemaT extends z.ZodString>(
+export function tool<SchemaT extends z.ZodString, ToolOutputT = ToolOutputType>(
   func: RunnableFunc<
     SchemaT extends z.ZodString ? z.output<SchemaT> : string,
-    ToolReturnType,
+    ToolOutputT,
     ToolRunnableConfig
   >,
   fields: ToolWrapperParams<SchemaT>
-): DynamicTool;
+): DynamicTool<ToolOutputT>;
 
 export function tool<
   SchemaT extends ZodObjectAny,
   SchemaOutputT = z.output<SchemaT>,
-  SchemaInputT = z.input<SchemaT>
+  SchemaInputT = z.input<SchemaT>,
+  ToolOutputT = ToolOutputType
 >(
-  func: RunnableFunc<SchemaOutputT, ToolReturnType, ToolRunnableConfig>,
+  func: RunnableFunc<SchemaOutputT, ToolOutputT, ToolRunnableConfig>,
   fields: ToolWrapperParams<SchemaT>
-): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT>;
+): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>;
 
 export function tool<
   SchemaT extends JSONSchema,
   SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
-  SchemaInputT = ToolInputSchemaInputType<SchemaT>
+  SchemaInputT = ToolInputSchemaInputType<SchemaT>,
+  ToolOutputT = ToolOutputType
 >(
   func: RunnableFunc<
     Parameters<DynamicStructuredToolInput<SchemaT>["func"]>[0],
-    ToolReturnType,
+    ToolOutputT,
     ToolRunnableConfig
   >,
   fields: ToolWrapperParams<SchemaT>
-): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT>;
+): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>;
 
 export function tool<
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   SchemaT extends ZodObjectAny | z.ZodString | JSONSchema = ZodObjectAny,
   SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
-  SchemaInputT = ToolInputSchemaInputType<SchemaT>
+  SchemaInputT = ToolInputSchemaInputType<SchemaT>,
+  ToolOutputT = ToolOutputType
 >(
-  func: RunnableFunc<SchemaOutputT, ToolReturnType, ToolRunnableConfig>,
+  func: RunnableFunc<SchemaOutputT, ToolOutputT, ToolRunnableConfig>,
   fields: ToolWrapperParams<SchemaT>
 ): SchemaT extends z.ZodString
-  ? DynamicTool
+  ? DynamicTool<ToolOutputT>
   : SchemaT extends ZodObjectAny
-  ? DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT>
-  : DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT> | DynamicTool {
+  ? DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>
+  :
+      | DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>
+      | DynamicTool<ToolOutputT> {
   const isShapelessZodSchema =
     fields.schema &&
     isZodSchema(fields.schema) &&
@@ -541,14 +603,14 @@ export function tool<
 
   // If the schema is not provided, or it's a shapeless schema (e.g. a ZodString), create a DynamicTool
   if (!fields.schema || isShapelessZodSchema || isStringJSONSchema) {
-    return new DynamicTool({
+    return new DynamicTool<ToolOutputT>({
       ...fields,
       description:
         fields.description ??
         (fields.schema as { description?: string } | undefined)?.description ??
         `${fields.name} tool`,
       func: async (input, runManager, config) => {
-        return new Promise((resolve, reject) => {
+        return new Promise<ToolOutputT>((resolve, reject) => {
           const childConfig = patchConfig(config, {
             callbacks: runManager?.getChild(),
           });
@@ -567,12 +629,17 @@ export function tool<
         });
       },
     }) as SchemaT extends z.ZodString
-      ? DynamicTool
+      ? DynamicTool<ToolOutputT>
       : SchemaT extends ZodObjectAny
-      ? DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT>
+      ? DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>
       :
-          | DynamicTool
-          | DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT>;
+          | DynamicTool<ToolOutputT>
+          | DynamicStructuredTool<
+              SchemaT,
+              SchemaOutputT,
+              SchemaInputT,
+              ToolOutputT
+            >;
   }
 
   const schema = fields.schema as ZodObjectAny | JSONSchema;
@@ -582,12 +649,17 @@ export function tool<
     (fields.schema as { description?: string }).description ??
     `${fields.name} tool`;
 
-  return new DynamicStructuredTool<typeof schema, SchemaOutputT, SchemaInputT>({
+  return new DynamicStructuredTool<
+    typeof schema,
+    SchemaOutputT,
+    SchemaInputT,
+    ToolOutputT
+  >({
     ...fields,
     description,
     schema,
     func: async (input, runManager, config) => {
-      return new Promise((resolve, reject) => {
+      return new Promise<ToolOutputT>((resolve, reject) => {
         const childConfig = patchConfig(config, {
           callbacks: runManager?.getChild(),
         });
@@ -604,18 +676,25 @@ export function tool<
       });
     },
   }) as SchemaT extends z.ZodString
-    ? DynamicTool
+    ? DynamicTool<ToolOutputT>
     : SchemaT extends ZodObjectAny
-    ? DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT>
-    : DynamicTool | DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT>;
+    ? DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>
+    :
+        | DynamicTool<ToolOutputT>
+        | DynamicStructuredTool<
+            SchemaT,
+            SchemaOutputT,
+            SchemaInputT,
+            ToolOutputT
+          >;
 }
 
-function _formatToolOutput(params: {
-  content: unknown;
+function _formatToolOutput<TOutput extends ToolOutputType>(params: {
+  content: TOutput;
   name: string;
   artifact?: unknown;
   toolCallId?: string;
-}): ToolReturnType {
+}): ToolMessage | TOutput {
   const { content, artifact, toolCallId } = params;
   if (toolCallId && !isDirectToolOutput(content)) {
     if (
diff --git a/langchain-core/src/tools/tests/types.test.ts b/langchain-core/src/tools/tests/types.test.ts
new file mode 100644
index 000000000000..871c49b2e1fa
--- /dev/null
+++ b/langchain-core/src/tools/tests/types.test.ts
@@ -0,0 +1,425 @@
+import { describe, it, expect } from "@jest/globals";
+import { z } from "zod";
+import {
+  DynamicStructuredTool,
+  DynamicTool,
+  StructuredTool,
+  StructuredToolInterface,
+  Tool,
+  ToolInterface,
+} from "../index.js";
+import { ToolCall, ToolMessage } from "../../messages/tool.js";
+
+const testDynamicTool = new DynamicTool({
+  name: "test",
+  description: "test",
+  func: async (input: string) => `test ${input}`,
+});
+
+const testDynamicStructuredTool = new DynamicStructuredTool({
+  name: "test",
+  description: "test",
+  func: async (input: { input: string }) => `test ${input.input}`,
+  schema: z.object({ input: z.string() }),
+});
+
+const testDynamicStructuredToolWithZodEffects = new DynamicStructuredTool({
+  name: "test",
+  description: "test",
+  func: async (input: string) => `test ${input}`,
+  schema: z
+    .object({ input: z.string().optional() })
+    .transform((data) => data.input),
+});
+
+describe("tool type tests", () => {
+  describe("DynamicTool", () => {
+    it("should be assignable to Tool", () => {
+      const tool: Tool = testDynamicTool;
+      expect(tool).toBe(testDynamicTool);
+    });
+    it("should be assignable to ToolInterface", () => {
+      const toolInterface: ToolInterface = testDynamicTool;
+      expect(toolInterface).toBe(testDynamicTool);
+    });
+    it("should be assignable to StructuredTool", () => {
+      const structuredTool: StructuredTool = testDynamicTool;
+      expect(structuredTool).toBe(testDynamicTool);
+    });
+    it("should be assignable to StructuredToolInterface", () => {
+      const structuredToolInterface: StructuredToolInterface = testDynamicTool;
+      expect(structuredToolInterface).toBe(testDynamicTool);
+    });
+
+    describe("invoke return type", () => {
+      it("should be assignable to ToolMessage when input is ToolCall", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+        const output: ToolMessage = await testDynamicTool.invoke(toolCall);
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be assignable to ToolOutputT when input is not ToolCall", async () => {
+        const output: string = await testDynamicTool.invoke("test");
+        expect(output).toBe("test test");
+      });
+
+      it("should be ToolMessage | TOutput when toolCall is present in config with ambiguous ID type", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        const output: ToolMessage | string = await testDynamicTool.invoke(
+          toolCall.args,
+          { toolCall }
+        );
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be ToolMessage when toolCall is present in config with string ID type", async () => {
+        const toolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        } as const;
+
+        const output: ToolMessage = await testDynamicTool.invoke(
+          toolCall.args,
+          { toolCall }
+        );
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be assignable to ToolOutputT when input is a ToolCall but direct output is requested", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        type ToolOutputT = {
+          lc_direct_tool_output: true;
+          output: string;
+        };
+
+        const directOutputDynamicTool = new DynamicTool({
+          name: "test",
+          description: "test",
+          func: async (input: string) =>
+            ({
+              lc_direct_tool_output: true,
+              output: `test ${input}`,
+            } as ToolOutputT),
+        });
+
+        const output: ToolOutputT = await directOutputDynamicTool.invoke(
+          toolCall
+        );
+
+        expect(output).toEqual({
+          lc_direct_tool_output: true,
+          output: "test test",
+        });
+      });
+    });
+
+    describe("call return type", () => {
+      it("should be assignable to ToolMessage when input is ToolCall", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        const output: ToolMessage = await testDynamicTool.call(toolCall);
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be ToolMessage | TOutput when toolCall is present in config with ambiguous ID type", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        const output: ToolMessage | string = await testDynamicTool.call(
+          toolCall.args,
+          { toolCall }
+        );
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be ToolMessage when toolCall is present in config with string ID type", async () => {
+        const toolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        } as const;
+
+        const output: ToolMessage = await testDynamicTool.call(toolCall.args, {
+          toolCall,
+        });
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be assignable to ToolOutputT when input is a ToolCall but direct output is requested", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        type ToolOutputT = {
+          lc_direct_tool_output: true;
+          output: string;
+        };
+
+        const directOutputDynamicTool = new DynamicTool({
+          name: "test",
+          description: "test",
+          func: async (input: string) =>
+            ({
+              lc_direct_tool_output: true,
+              output: `test ${input}`,
+            } as ToolOutputT),
+        });
+
+        const output: ToolOutputT = await directOutputDynamicTool.call(
+          toolCall
+        );
+
+        expect(output).toEqual({
+          lc_direct_tool_output: true,
+          output: "test test",
+        });
+      });
+    });
+  });
+
+  describe("DynamicStructuredTool", () => {
+    it("should not be assignable to Tool", () => {
+      // @ts-expect-error DynamicStructuredTool uses a string schema and is not compatible with Tool
+      const tool: Tool = testDynamicStructuredTool;
+      expect(tool).toBe(testDynamicStructuredTool);
+    });
+    it("should not be assignable to ToolInterface if not narrowed to have input type `{ input: string }`", () => {
+      // @ts-expect-error DynamicStructuredTool uses a string schema and is not compatible with ToolInterface without proper narrowing
+      const toolInterface: ToolInterface = testDynamicStructuredTool;
+      expect(toolInterface).toBe(testDynamicStructuredTool);
+    });
+    it("should be assignable to ToolInterface if narrowed to use ZodEffects schema`", () => {
+      const toolInterface: ToolInterface =
+        testDynamicStructuredToolWithZodEffects;
+      expect(toolInterface).toBe(testDynamicStructuredToolWithZodEffects);
+    });
+    it("should be assignable to StructuredTool", () => {
+      const structuredTool: StructuredTool = testDynamicStructuredTool;
+      expect(structuredTool).toBe(testDynamicStructuredTool);
+    });
+    it("should be assignable to StructuredToolInterface with default type params", () => {
+      const structuredToolInterface: StructuredToolInterface =
+        testDynamicStructuredTool;
+      expect(structuredToolInterface).toBe(testDynamicStructuredTool);
+    });
+
+    describe("invoke return type", () => {
+      it("should be assignable to ToolMessage when input is ToolCall", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+        const output: ToolMessage = await testDynamicStructuredTool.invoke(
+          toolCall
+        );
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be assignable to ToolOutputT when input is not ToolCall", async () => {
+        const output: string = await testDynamicStructuredTool.invoke({
+          input: "test",
+        });
+        expect(output).toBe("test test");
+      });
+
+      it("should be ToolMessage | TOutput when toolCall is present in config with ambiguous ID type", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        const output: ToolMessage | string =
+          await testDynamicStructuredTool.invoke(
+            toolCall.args as { input: string },
+            {
+              toolCall,
+            }
+          );
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be ToolMessage when toolCall is present in config with string ID type", async () => {
+        const toolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        } as const;
+
+        const output: ToolMessage = await testDynamicStructuredTool.invoke(
+          toolCall.args,
+          { toolCall }
+        );
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be assignable to ToolOutputT when input is a ToolCall but direct output is requested", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        type ToolOutputT = {
+          lc_direct_tool_output: true;
+          output: string;
+        };
+
+        const directOutputDynamicTool = new DynamicTool({
+          name: "test",
+          description: "test",
+          func: async (input: string) =>
+            ({
+              lc_direct_tool_output: true,
+              output: `test ${input}`,
+            } as ToolOutputT),
+        });
+
+        const output: ToolOutputT = await directOutputDynamicTool.invoke(
+          toolCall
+        );
+
+        expect(output).toEqual({
+          lc_direct_tool_output: true,
+          output: "test test",
+        });
+      });
+
+      it("should be the transformed schema output type when using ZodEffects schema", async () => {
+        const output: string =
+          await testDynamicStructuredToolWithZodEffects.invoke({
+            input: "test",
+          });
+        expect(output).toBe("test test");
+      });
+    });
+
+    describe("call return type", () => {
+      it("should be assignable to ToolMessage when input is ToolCall", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        const output: ToolMessage = await testDynamicStructuredTool.call(
+          toolCall
+        );
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be ToolMessage | TOutput when toolCall is present in config with ambiguous ID type", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        const output = await testDynamicStructuredTool.call(
+          toolCall.args as { input: string },
+          {
+            toolCall,
+          }
+        );
+
+        // @ts-expect-error string | ToolMessage isn't assignable to string
+        const _strOutput: string = output;
+
+        // @ts-expect-error ToolMessage isn't assignable to string
+        const _toolMessageOutput: ToolMessage = output;
+
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be ToolMessage when toolCall is present in config with string ID type", async () => {
+        const toolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        } as const;
+
+        const output: ToolMessage = await testDynamicStructuredTool.call(
+          toolCall.args,
+          {
+            toolCall,
+          }
+        );
+        expect(output).toBeInstanceOf(ToolMessage);
+      });
+
+      it("should be assignable to ToolOutputT when input is a ToolCall but direct output is requested", async () => {
+        const toolCall: ToolCall = {
+          id: "1",
+          name: "test",
+          args: { input: "test" },
+          type: "tool_call",
+        };
+
+        type ToolOutputT = {
+          lc_direct_tool_output: true;
+          output: string;
+        };
+
+        const directOutputDynamicTool = new DynamicStructuredTool({
+          name: "test",
+          description: "test",
+          func: async (input: { input: string }) =>
+            ({
+              lc_direct_tool_output: true,
+              output: `test ${input.input}`,
+            } as ToolOutputT),
+          schema: z.object({ input: z.string() }),
+        });
+
+        const output: ToolOutputT = await directOutputDynamicTool.call(
+          toolCall.args as { input: string }
+        );
+
+        expect(output).toEqual({
+          lc_direct_tool_output: true,
+          output: "test test",
+        });
+      });
+    });
+  });
+});
diff --git a/langchain-core/src/tools/types.ts b/langchain-core/src/tools/types.ts
index 666f058da548..79b0d1805219 100644
--- a/langchain-core/src/tools/types.ts
+++ b/langchain-core/src/tools/types.ts
@@ -1,8 +1,5 @@
 import { z } from "zod";
-import {
-  CallbackManagerForToolRun,
-  type Callbacks,
-} from "../callbacks/manager.js";
+import { CallbackManagerForToolRun } from "../callbacks/manager.js";
 import type {
   BaseLangChainParams,
   ToolDefinition,
@@ -13,7 +10,11 @@ import {
   RunnableToolLike,
   type RunnableInterface,
 } from "../runnables/base.js";
-import type { ToolCall } from "../messages/tool.js";
+import {
+  type DirectToolOutput,
+  type ToolCall,
+  type ToolMessage,
+} from "../messages/tool.js";
 import type { MessageContent } from "../messages/base.js";
 import { isZodSchema } from "../utils/types/is_zod_schema.js";
 import { JSONSchema } from "../utils/json_schema.js";
@@ -21,7 +22,7 @@ import { JSONSchema } from "../utils/json_schema.js";
 export type ResponseFormat = "content" | "content_and_artifact" | string;
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type ToolReturnType = any;
+export type ToolOutputType = any;
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export type ContentAndArtifact = [MessageContent, any];
@@ -29,14 +30,30 @@ export type ContentAndArtifact = [MessageContent, any];
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export type ZodObjectAny = z.ZodObject<any, any, any, any>;
 
+/**
+ * Conditional type that determines the return type of the {@link StructuredTool.invoke} method.
+ * - If the input is a ToolCall, it returns a ToolMessage
+ * - If the config is a runnable config and contains a toolCall property, it returns a ToolMessage
+ * - Otherwise, it returns the original output type
+ */
+export type ToolReturnType<TInput, TConfig, TOutput> =
+  TOutput extends DirectToolOutput
+    ? TOutput
+    : TConfig extends { toolCall: { id: string } }
+    ? ToolMessage
+    : TConfig extends { toolCall: { id: undefined } }
+    ? TOutput
+    : TConfig extends { toolCall: { id?: string } }
+    ? TOutput | ToolMessage
+    : TInput extends ToolCall
+    ? ToolMessage
+    : TOutput;
+
 /**
  * Base type that establishes the types of input schemas that can be used for LangChain tool
  * definitions.
  */
-export type ToolInputSchemaBase =
-  | ZodObjectAny
-  | z.ZodEffects<ZodObjectAny>
-  | JSONSchema;
+export type ToolInputSchemaBase = z.ZodAny | JSONSchema;
 
 /**
  * Parameters for the Tool classes.
@@ -113,13 +130,54 @@ export type ToolInputSchemaInputType<T extends ToolInputSchemaBase> =
  * @param SchemaInputT - The TypeScript type representing the structure of the tool arguments generated by the LLM. Useful for type checking tool handler functions when using JSONSchema.
  */
 export type StructuredToolCallInput<
-  SchemaT extends ToolInputSchemaBase = ZodObjectAny,
+  SchemaT extends ToolInputSchemaBase = ToolInputSchemaBase,
   SchemaInputT = ToolInputSchemaInputType<SchemaT>
 > =
   | (ToolInputSchemaOutputType<SchemaT> extends string ? string : never)
   | SchemaInputT
   | ToolCall;
 
+/**
+ * An input schema type for tools that accept a single string input.
+ *
+ * This schema defines a tool that takes an optional string parameter named "input".
+ * It uses Zod's effects to transform the input and strip any extra properties.
+ *
+ * This is primarily used for creating simple string-based tools where the LLM
+ * only needs to provide a single text value as input to the tool.
+ */
+export type StringInputToolSchema = z.ZodEffects<
+  z.ZodObject<
+    { input: z.ZodOptional<z.ZodString> },
+    "strip",
+    z.ZodTypeAny,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    any,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    any
+  >,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  any,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  any
+>;
+
+/**
+ * Defines the type for input to a tool's call method.
+ *
+ * This type is a convenience alias for StructuredToolCallInput with the input type
+ * derived from the schema. It represents the possible inputs that can be passed to a tool,
+ * which can be either:
+ * - A string (if the tool accepts string input)
+ * - A structured input matching the tool's schema
+ * - A ToolCall object (typically from an LLM)
+ *
+ * @param SchemaT - The schema type for the tool input, defaults to StringInputToolSchema
+ */
+export type ToolCallInput<
+  SchemaT extends ToolInputSchemaBase = StringInputToolSchema
+> = StructuredToolCallInput<SchemaT, ToolInputSchemaInputType<SchemaT>>;
+
 /**
  * Interface that defines the shape of a LangChain structured tool.
  *
@@ -130,14 +188,12 @@ export type StructuredToolCallInput<
  * @param SchemaInputT - The TypeScript type representing the structure of the tool arguments generated by the LLM. Useful for type checking tool handler functions when using JSONSchema.
  */
 export interface StructuredToolInterface<
-  SchemaT extends ToolInputSchemaBase =
-    | ZodObjectAny
-    | z.ZodEffects<ZodObjectAny>
-    | JSONSchema,
-  SchemaInputT = ToolInputSchemaInputType<SchemaT>
+  SchemaT extends ToolInputSchemaBase = ToolInputSchemaBase,
+  SchemaInputT = ToolInputSchemaInputType<SchemaT>,
+  ToolOutputT = ToolOutputType
 > extends RunnableInterface<
     StructuredToolCallInput<SchemaT, SchemaInputT>,
-    ToolReturnType
+    ToolOutputT | ToolMessage
   > {
   lc_namespace: string[];
 
@@ -146,6 +202,20 @@ export interface StructuredToolInterface<
    */
   schema: SchemaT;
 
+  /**
+   * Invokes the tool with the provided argument and configuration.
+   * @param arg The input argument for the tool.
+   * @param configArg Optional configuration for the tool call.
+   * @returns A Promise that resolves with the tool's output.
+   */
+  invoke<
+    TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    arg: TArg,
+    configArg?: TConfig
+  ): Promise<ToolReturnType<TArg, TConfig, ToolOutputT>>;
+
   /**
    * @deprecated Use .invoke() instead. Will be removed in 0.3.0.
    *
@@ -157,12 +227,15 @@ export interface StructuredToolInterface<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  call(
-    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
-    configArg?: Callbacks | RunnableConfig,
+  call<
+    TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    arg: TArg,
+    configArg?: TConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<ToolReturnType>;
+  ): Promise<ToolReturnType<TArg, TConfig, ToolOutputT>>;
 
   /**
    * The name of the tool.
@@ -190,12 +263,10 @@ export interface StructuredToolInterface<
  * @param SchemaInputT - The TypeScript type representing the structure of the tool arguments generated by the LLM. Useful for type checking tool handler functions when using JSONSchema.
  */
 export interface ToolInterface<
-  SchemaT extends ToolInputSchemaBase =
-    | ZodObjectAny
-    | z.ZodEffects<ZodObjectAny>
-    | JSONSchema,
-  SchemaInputT = ToolInputSchemaInputType<SchemaT>
-> extends StructuredToolInterface<SchemaT, SchemaInputT> {
+  SchemaT extends StringInputToolSchema = StringInputToolSchema,
+  SchemaInputT = ToolInputSchemaInputType<SchemaT>,
+  ToolOutputT = ToolOutputType
+> extends StructuredToolInterface<SchemaT, SchemaInputT, ToolOutputT> {
   /**
    * @deprecated Use .invoke() instead. Will be removed in 0.3.0.
    *
@@ -205,11 +276,14 @@ export interface ToolInterface<
    * @param callbacks Optional callbacks for the tool.
    * @returns A Promise that resolves with a string.
    */
-  call(
+  call<
+    TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
     // TODO: shouldn't this be narrowed based on SchemaT?
-    arg: string | undefined | SchemaInputT | ToolCall,
-    callbacks?: Callbacks | RunnableConfig
-  ): Promise<ToolReturnType>;
+    arg: TArg,
+    callbacks?: TConfig
+  ): Promise<ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>>;
 }
 
 /**
@@ -231,12 +305,13 @@ export interface BaseDynamicToolInput extends ToolParams {
 /**
  * Interface for the input parameters of the DynamicTool class.
  */
-export interface DynamicToolInput extends BaseDynamicToolInput {
+export interface DynamicToolInput<ToolOutputT = ToolOutputType>
+  extends BaseDynamicToolInput {
   func: (
     input: string,
     runManager?: CallbackManagerForToolRun,
     config?: ToolRunnableConfig
-  ) => Promise<ToolReturnType>;
+  ) => Promise<ToolOutputT>;
 }
 
 /**
@@ -246,8 +321,9 @@ export interface DynamicToolInput extends BaseDynamicToolInput {
  * @param SchemaOutputT - The TypeScript type representing the result of applying the schema to the tool arguments. Useful for type checking tool handler functions when using JSONSchema.
  */
 export interface DynamicStructuredToolInput<
-  SchemaT extends ToolInputSchemaBase = ZodObjectAny,
-  SchemaOutputT = ToolInputSchemaOutputType<SchemaT>
+  SchemaT extends ToolInputSchemaBase = ToolInputSchemaBase,
+  SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
+  ToolOutputT = ToolOutputType
 > extends BaseDynamicToolInput {
   /**
    * Tool handler function - the function that will be called when the tool is invoked.
@@ -258,13 +334,11 @@ export interface DynamicStructuredToolInput<
    * @returns The result of the tool.
    */
   func: (
-    input: BaseDynamicToolInput["responseFormat"] extends "content_and_artifact"
-      ? ToolCall
-      : SchemaOutputT,
+    input: SchemaOutputT,
     runManager?: CallbackManagerForToolRun,
     config?: RunnableConfig
-  ) => Promise<ToolReturnType>;
-  schema: SchemaT extends ZodObjectAny ? SchemaT : SchemaT;
+  ) => Promise<ToolOutputT>;
+  schema: SchemaT;
 }
 
 /**
diff --git a/langchain-core/src/tools/utils.ts b/langchain-core/src/tools/utils.ts
index b9c5bd8b384d..6b8b777a1c2d 100644
--- a/langchain-core/src/tools/utils.ts
+++ b/langchain-core/src/tools/utils.ts
@@ -9,6 +9,19 @@ export function _isToolCall(toolCall?: unknown): toolCall is ToolCall {
   );
 }
 
+export function _configHasToolCallId(
+  config?: unknown
+): config is { toolCall: { id?: string } } {
+  return !!(
+    config &&
+    typeof config === "object" &&
+    "toolCall" in config &&
+    config.toolCall != null &&
+    typeof config.toolCall === "object" &&
+    "id" in config.toolCall &&
+    typeof config.toolCall.id === "string"
+  );
+}
 /**
  * Custom error class used to handle exceptions related to tool input parsing.
  * It extends the built-in `Error` class and adds an optional `output`
diff --git a/libs/langchain-community/src/agents/toolkits/stagehand.ts b/libs/langchain-community/src/agents/toolkits/stagehand.ts
index 763560cf1b4b..086e6d7a879a 100644
--- a/libs/langchain-community/src/agents/toolkits/stagehand.ts
+++ b/libs/langchain-community/src/agents/toolkits/stagehand.ts
@@ -1,8 +1,8 @@
 import {
   Tool,
   BaseToolkit as Toolkit,
-  ToolInterface,
   StructuredTool,
+  StructuredToolInterface,
 } from "@langchain/core/tools";
 import { Stagehand } from "@browserbasehq/stagehand";
 import { AnyZodObject, z } from "zod";
@@ -152,7 +152,7 @@ export class StagehandObserveTool extends StagehandToolBase {
 }
 
 export class StagehandToolkit extends Toolkit {
-  tools: ToolInterface[];
+  tools: StructuredToolInterface[];
 
   stagehand?: Stagehand;
 
@@ -162,7 +162,7 @@ export class StagehandToolkit extends Toolkit {
     this.tools = this.initializeTools();
   }
 
-  private initializeTools(): ToolInterface[] {
+  private initializeTools(): StructuredToolInterface[] {
     return [
       new StagehandNavigateTool(this.stagehand),
       new StagehandActTool(this.stagehand),


Reviews:
user: login: hntrl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T01:17:37Z
user: login: benjamincburns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T01:39:45Z
user: login: benjamincburns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T01:47:16Z
user: login: hntrl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T02:04:04Z
user: login: hntrl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T02:15:09Z
user: login: benjamincburns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T02:22:12Z
user: login: hntrl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T02:37:57Z
user: login: benjamincburns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T06:06:19Z
user: login: benjamincburns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T06:42:46Z
user: login: hntrl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-04T20:16:08Z
user: login: hntrl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-04T20:44:43Z
user: login: benjamincburns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T00:51:42Z
user: login: benjamincburns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T01:00:16Z
user: login: benjamincburns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T01:06:02Z
user: login: benjamincburns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T01:11:39Z

Review comments:
diff_hunk: @@ -513,12 +513,7 @@ test("Test ChatBedrockConverse can respond to tool invocations with thinking ena
   expect(typeof result.tool_calls![0].args.location).toBe("string");
   expect(result.tool_calls![0].args.location.length).toBeGreaterThan(0);
 
-  messages.push(
-    new ToolMessage({
-      tool_call_id: result.tool_calls![0].id!,
-      content: await tools[0].invoke(result.tool_calls![0]),
-    })
-  );
+  messages.push(await tools[0].invoke(result.tool_calls![0]));
path: libs/langchain-aws/src/tests/chat_models.int.test.ts
user: login: hntrl
body: StructuredTool.invoke will return a tool message when a tool call is passed, so we don't have to create a new tool message here
created_at: 2025-04-24T01:17:37Z
author_association: MEMBER
diff_hunk: @@ -183,12 +183,12 @@ export interface StructuredToolInterface<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  call<TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>>(
-    arg: TArg,
+  call(
+    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
     configArg?: Callbacks | RunnableConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<TArg extends ToolCall ? ToolMessage : ToolOutputT>;
+  ): Promise<ToolOutputT>;
path: langchain-core/src/tools/types.ts
user: login: benjamincburns
body: Still assessing, but I'm not sure that this change is valid.

It's a bit confusing because we don't have `StructuredTool` actually implementing `StructuredToolInterface` (I forget why that is), but the latter is meant to reflect the public interface of the former. Same goes for `ToolInterface` and `Tool`.

Additionally, extensions of `StructuredTool` should always be assignable to `StructuredToolInterface`, and extensions of `Tool` and `DynamicTool` should be assignable to `ToolInterface`. We should probably have a test that guards this property if we can't make it so the aforementioned classes actually implement their corresponding interfaces 🤔.
created_at: 2025-04-24T01:39:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -183,12 +183,12 @@ export interface StructuredToolInterface<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  call<TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>>(
-    arg: TArg,
+  call(
+    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
     configArg?: Callbacks | RunnableConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<TArg extends ToolCall ? ToolMessage : ToolOutputT>;
+  ): Promise<ToolOutputT>;
path: langchain-core/src/tools/types.ts
user: login: benjamincburns
body: I think additionally all of those should be assignable to `StructuredToolInterface` (lack of type params is intentional on this and all of the above - default params should be wide enough to allow this).
created_at: 2025-04-24T01:47:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -183,12 +183,12 @@ export interface StructuredToolInterface<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  call<TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>>(
-    arg: TArg,
+  call(
+    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
     configArg?: Callbacks | RunnableConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<TArg extends ToolCall ? ToolMessage : ToolOutputT>;
+  ): Promise<ToolOutputT>;
path: langchain-core/src/tools/types.ts
user: login: hntrl
body: Makes sense. Two paths I can see going from here (both of which I'm happy to do, more just a matter of preference):
(1) Regress the tool classes to return just `ToolOutputT` instead of switching based on input (which doesn't really have any impact since it was just returning an `any` alias before, at least this way it's an optional type param on the classes + interfaces that we can use to control the output type)
(2) Bring the output type switching back to the interface and fix the side effects in the provider packages

Option 1 gets this PR merged quicker, option 2 is more "feature complete".

Unrelated but I'd be interested to know why that direct inheritance isn't there.
created_at: 2025-04-24T02:04:03Z
author_association: MEMBER
diff_hunk: @@ -183,12 +183,12 @@ export interface StructuredToolInterface<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  call<TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>>(
-    arg: TArg,
+  call(
+    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
     configArg?: Callbacks | RunnableConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<TArg extends ToolCall ? ToolMessage : ToolOutputT>;
+  ): Promise<ToolOutputT>;
path: langchain-core/src/tools/types.ts
user: login: hntrl
body: I think the diff makes it confusing but this change specifically is reverting a previous change I made. On main the return type is set to `Promise<ToolReturnType>`
created_at: 2025-04-24T02:15:09Z
author_association: MEMBER
diff_hunk: @@ -183,12 +183,12 @@ export interface StructuredToolInterface<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  call<TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>>(
-    arg: TArg,
+  call(
+    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
     configArg?: Callbacks | RunnableConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<TArg extends ToolCall ? ToolMessage : ToolOutputT>;
+  ): Promise<ToolOutputT>;
path: langchain-core/src/tools/types.ts
user: login: benjamincburns
body: > Unrelated but I'd be interested to know why that inheritance isn't there.

We've been iterating in this area recently (see #7973), so the types are a bit more well-aligned between the classes and interfaces now than when they initially came together. Looking into these issues myself, I see one bug that I introduced in that PR, where I should've aligned the constraint for `SchemaT` on `ToolInterface` with the constraint for `SchemaT` on `Tool`. I _think_ that's all that's necessary to make `Tool` implement `ToolInterface` and `StructuredTool` implement `StructuredToolInterface`.

> Two paths I can see going from here (both of which I'm happy to do, more just a matter of preference):
> 
> (1) Regress the tool classes to return just ToolOutputT instead of switching based on input (which doesn't really have any impact since it was just returning an any alias before, at least this way it's an optional type param on the classes + interfaces that we can use to control the output type)

My concern here would be that we'd drop the `ToolMessage` type from the output, or am I mistaken? That would trigger cases where we return something that's not expressed by the return type. Unfortunately I think it'd be kinder to consumers to regress all the way back to `any` as it'd be better to have no "documentation" of types here than to be misleading.

> (2) Bring the output type switching back to the interface and fix the side effects in the provider packages

I think this would be the preferred approach, with a bias toward better alignment of the type hierarchy and against using casts to fix the errors outside of `@langchain/core` (if possible). I can probably pick this up Monday if you don't have the time for it, though.
created_at: 2025-04-24T02:22:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -183,12 +183,12 @@ export interface StructuredToolInterface<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  call<TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>>(
-    arg: TArg,
+  call(
+    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
     configArg?: Callbacks | RunnableConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<TArg extends ToolCall ? ToolMessage : ToolOutputT>;
+  ): Promise<ToolOutputT>;
path: langchain-core/src/tools/types.ts
user: login: hntrl
body: > My concern here would be that we'd drop the ToolMessage type from the output, or am I mistaken? That would trigger cases where we return something that's not expressed by the return type.

Good catch. I guess I was thinking more-so on the lines of consuming from the interface end instead of the consumer end (i.e. not creating tools using the `tool` method) where the output type is always implicitly any since the default for the type param is `ToolReturnType`. Agree this would be a bad change.

I'll have some more time to work on this tomorrow. If you don't hear from me by Monday then feel free to work off this PR
created_at: 2025-04-24T02:37:57Z
author_association: MEMBER
diff_hunk: @@ -166,34 +172,52 @@ export abstract class StructuredTool<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  async call(
-    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
-    configArg?: Callbacks | ToolRunnableConfig,
+  async call<
+    TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>,
+    TConfig extends Callbacks | ToolRunnableConfig | undefined
+  >(
+    arg: TArg,
+    configArg?: TConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<ToolReturnType> {
-    let parsed = arg;
+  ): Promise<ToolReturnType<TArg, TConfig, ToolOutputT>> {
path: langchain-core/src/tools/index.ts
user: login: benjamincburns
body: Unfortunately I think this return type is invalid here - `call` returns `ToolMessage` only if the `toolCall` property of the `config` is set and contains a nonempty ID.
created_at: 2025-05-02T06:06:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -166,34 +172,52 @@ export abstract class StructuredTool<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  async call(
-    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
-    configArg?: Callbacks | ToolRunnableConfig,
+  async call<
+    TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>,
+    TConfig extends Callbacks | ToolRunnableConfig | undefined
+  >(
+    arg: TArg,
+    configArg?: TConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<ToolReturnType> {
-    let parsed = arg;
+  ): Promise<ToolReturnType<TArg, TConfig, ToolOutputT>> {
path: langchain-core/src/tools/index.ts
user: login: benjamincburns
body: Argh, it's more complex than that, even - if the output content is an object that contains `lc_direct_tool_output: true` we always output the `content` without wrapping in a `ToolMessage` - fun fun.
created_at: 2025-05-02T06:42:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,14 +19,14 @@ const testDynamicTool = new DynamicTool({
 const testDynamicStructuredTool = new DynamicStructuredTool({
   name: "test",
   description: "test",
-  func: async (input: string) => `test ${input}`,
-  schema: z.string(),
+  func: async (input: { input: string }) => `test ${input.input}`,
+  schema: z.object({ input: z.string() }),
 });
 
 const testDynamicStructuredToolWithZodEffects = new DynamicStructuredTool({
   name: "test",
   description: "test",
-  func: async (input: string) => `test ${input}`,
+  func: async (input: { input: string }) => `test ${input.input}`,
path: langchain-core/src/tools/tests/types.test.ts
user: login: hntrl
body: is this accurate? I was under the impression that the type passed in would be the input type after transformations [1]. This is the case if you take off the argument assertion here. (i.e. just leave `(input) =>`). Maybe it makes more sense to test the argument type inside of the function instead of washing it:

```ts
func: async (input) => {
  // input will be unassignable to arg if the input type changes
  const arg: string | undefined = input;
  return `test ${arg}`;
}
```

[1] https://github.com/hntrl/langchainjs/blob/ce46fe215c57d063f31ce551d75ee2d0d8908e3b/langchain-core/src/tools/types.ts#L337
created_at: 2025-05-04T19:23:41Z
author_association: MEMBER
diff_hunk: @@ -166,34 +172,52 @@ export abstract class StructuredTool<
    * @param tags Optional tags for the tool.
    * @returns A Promise that resolves with a string.
    */
-  async call(
-    arg: StructuredToolCallInput<SchemaT, SchemaInputT>,
-    configArg?: Callbacks | ToolRunnableConfig,
+  async call<
+    TArg extends StructuredToolCallInput<SchemaT, SchemaInputT>,
+    TConfig extends Callbacks | ToolRunnableConfig | undefined
+  >(
+    arg: TArg,
+    configArg?: TConfig,
     /** @deprecated */
     tags?: string[]
-  ): Promise<ToolReturnType> {
-    let parsed = arg;
+  ): Promise<ToolReturnType<TArg, TConfig, ToolOutputT>> {
path: langchain-core/src/tools/index.ts
user: login: hntrl
body: > `call` returns `ToolMessage` only if the `toolCall` property of the `config` is set and contains a nonempty ID.

Doesn't `call` also cover the case of returning a tool message if `arg` is a tool call? The continuation i'm following is after the validation logic (starting at `let toolCallId: string | undefined;`), toolCallId gets assigned if arg is a tool call, then `_formatToolOutput` returns a ToolMessage if toolCallId is defined.

I could very well be missing something here...
created_at: 2025-05-04T20:15:21Z
author_association: MEMBER
diff_hunk: @@ -283,6 +320,18 @@ export abstract class Tool extends StructuredTool<
     super(fields);
   }
 
+  override invoke<
+    TArg extends string | undefined | z.input<this["schema"]> | ToolCall,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    input: TArg,
+    config?: TConfig
+  ): Promise<ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>> {
+    return super.invoke(input, config) as Promise<
+      ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>
+    >;
+  }
+
path: langchain-core/src/tools/index.ts
user: login: hntrl
body: This new override is causing CI to fail. On main there is no invoke override for `tool`, it just assumes the signature from extending StructuredTool. Removing this gets my build to work locally

The TArg union is covered by all of the types in StructuredToolCallInput (except for undefined), but I'm lost as to why undefined is acceptable as input. Wouldn't an undefined input fail validation every time?
created_at: 2025-05-04T20:44:42Z
author_association: MEMBER
diff_hunk: @@ -283,6 +320,18 @@ export abstract class Tool extends StructuredTool<
     super(fields);
   }
 
+  override invoke<
+    TArg extends string | undefined | z.input<this["schema"]> | ToolCall,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    input: TArg,
+    config?: TConfig
+  ): Promise<ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>> {
+    return super.invoke(input, config) as Promise<
+      ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>
+    >;
+  }
+
path: langchain-core/src/tools/index.ts
user: login: benjamincburns
body: Edit - ignore me - this change wasn't necessary!

Original comment:
Unfortunately `undefined` was part of the input signature here originally. If I were writing this from scratch I definitely wouldn't allow `undefined` input, but we're trying to avoid making input types any narrower than they were before this PR, as otherwise this would be a breaking change.

I suppose there's a valid argument that narrowing output types is breaking here in the strictest sense, as the change can introduce new compiler errors where there weren't any previously. However I think it's reasonable to consider a change to be not breaking if the only compiler errors it introduces are ones that expose structural and/or logical errors that existed prior to the change in question.
created_at: 2025-05-07T00:51:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,6 +320,18 @@ export abstract class Tool extends StructuredTool<
     super(fields);
   }
 
+  override invoke<
+    TArg extends string | undefined | z.input<this["schema"]> | ToolCall,
+    TConfig extends ToolRunnableConfig | undefined
+  >(
+    input: TArg,
+    config?: TConfig
+  ): Promise<ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>> {
+    return super.invoke(input, config) as Promise<
+      ToolReturnType<NonNullable<TArg>, TConfig, ToolOutputT>
+    >;
+  }
+
path: langchain-core/src/tools/index.ts
user: login: benjamincburns
body: I did some spelunking and it's been that way since at least [2 years ago](https://github.com/langchain-ai/langchainjs/blame/da9b1d922170a0b2e237d233b43da6a0f89e7150/langchain-core/src/tools.ts#L209).

IIRC I added the `invoke` override to align the invoke input type with the `call` input type for adherence to the interface. Maybe that wasn't necessary, though. 🤔 
created_at: 2025-05-07T01:00:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,14 +19,14 @@ const testDynamicTool = new DynamicTool({
 const testDynamicStructuredTool = new DynamicStructuredTool({
   name: "test",
   description: "test",
-  func: async (input: string) => `test ${input}`,
-  schema: z.string(),
+  func: async (input: { input: string }) => `test ${input.input}`,
+  schema: z.object({ input: z.string() }),
 });
 
 const testDynamicStructuredToolWithZodEffects = new DynamicStructuredTool({
   name: "test",
   description: "test",
-  func: async (input: string) => `test ${input}`,
+  func: async (input: { input: string }) => `test ${input.input}`,
path: langchain-core/src/tools/tests/types.test.ts
user: login: benjamincburns
body: Yeah - I think I must've modified this in error. Will double check by adding a test that validates the output on invoke
created_at: 2025-05-07T01:06:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,14 +19,14 @@ const testDynamicTool = new DynamicTool({
 const testDynamicStructuredTool = new DynamicStructuredTool({
   name: "test",
   description: "test",
-  func: async (input: string) => `test ${input}`,
-  schema: z.string(),
+  func: async (input: { input: string }) => `test ${input.input}`,
+  schema: z.object({ input: z.string() }),
 });
 
 const testDynamicStructuredToolWithZodEffects = new DynamicStructuredTool({
   name: "test",
   description: "test",
-  func: async (input: string) => `test ${input}`,
+  func: async (input: { input: string }) => `test ${input.input}`,
path: langchain-core/src/tools/tests/types.test.ts
user: login: benjamincburns
body: Yeah you were right - good catch!
created_at: 2025-05-07T01:11:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-23T18:12:41Z
body: [vc]: #kKepQ1NDdVnarM3CEp2FhZhnZPKvMSnxx1MTQsvyb1Y=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJsYW5nY2hhaW5qcy1kb2NzIiwicm9vdERpcmVjdG9yeSI6ImRvY3MvY29yZV9kb2NzLyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9sYW5nY2hhaW4vbGFuZ2NoYWluanMtZG9jcy8zUkVKdU1hQ2F0M20xUk5RUUh1UFdrb1JkZHBwIiwicHJldmlld1VybCI6ImxhbmdjaGFpbmpzLWRvY3MtZ2l0LWZvcmstaG50cmwtZml4LWdoLTgwMTAtbGFuZ2NoYWluLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifSx7Im5hbWUiOiJsYW5nY2hhaW5qcy1hcGktcmVmcyIsInJvb3REaXJlY3RvcnkiOiJkb2NzL2FwaV9yZWZzIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbGFuZ2NoYWluL2xhbmdjaGFpbmpzLWFwaS1yZWZzLzlwWG5Fb0NGQldpQnZ2VWVnWWFVYlZGc3BvQjQiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **langchainjs-docs** | ✅ Ready ([Inspect](https://vercel.com/langchain/langchainjs-docs/3REJuMaCat3m1RNQQHuPWkoRddpp)) | [Visit Preview](https://langchainjs-docs-git-fork-hntrl-fix-gh-8010-langchain.vercel.app) |  |  May 7, 2025 1:39am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **langchainjs-api-refs** | ⬜️ Ignored ([Inspect](https://vercel.com/langchain/langchainjs-api-refs/9pXnEoCFBWiBvvUegYaUbVFspoB4)) |  |  | May 7, 2025 1:39am |
</details>



author_association: NONE
user: login: benjamincburns
created_at: 2025-04-24T00:02:20Z
body: Hi @hntrl - looks like you might've forgotten to do a top-level `yarn build`? The [exports tests appear to be failing](https://github.com/langchain-ai/langchainjs/actions/runs/14625119435/job/41050709347?pr=8056#step:5:2301) due to type errors in the provider libs.
author_association: CONTRIBUTOR
user: login: benjamincburns
created_at: 2025-04-24T00:09:44Z
body: > Hi @hntrl - looks like you might've forgotten to do a top-level `yarn build`? The [exports tests appear to be failing](https://github.com/langchain-ai/langchainjs/actions/runs/14625119435/job/41050709347?pr=8056#step:5:2301) due to type errors in the provider libs.

Ah quick correction. I think the exports tests are actually just smoke testing bundled versions of the project, and they tend to catch breakages. I suspect this may be a case where the code in question is making invalid assumptions about the return type of `StructuredTool`, so it's maybe a false positive, but you'd need to evaluate.

Apologies for the confusion on my part - still learning the finer details about our build/test pipeline process.
author_association: CONTRIBUTOR
user: login: hntrl
created_at: 2025-04-24T01:32:23Z
body: > > Hi @hntrl - looks like you might've forgotten to do a top-level `yarn build`? The [exports tests appear to be failing](https://github.com/langchain-ai/langchainjs/actions/runs/14625119435/job/41050709347?pr=8056#step:5:2301) due to type errors in the provider libs.
> 
> Ah quick correction. I think the exports tests are actually just smoke testing bundled versions of the project, and they tend to catch breakages. I suspect this may be a case where the code in question is making invalid assumptions about the return type of `StructuredTool`, so it's maybe a false positive, but you'd need to evaluate.
> 
> Apologies for the confusion on my part - still learning the finer details about our build/test pipeline process.

No worries and thanks for the callout! I think you're right in that the failing test was a false positive. The most recent commits I pushed fix the CI failures.

[a0da856](https://github.com/langchain-ai/langchainjs/pull/8056/commits/a0da856d0eecb8506e631ee87c95e0f37f184454) fixes the failing test from the latest CI run. I added comments explaining the change.

[3cbaceb](https://github.com/langchain-ai/langchainjs/pull/8056/commits/3cbacebc8f44a43c5a57afad25bdd9f864fb64eb) fixes some other build failures I found that come from changing the interface type specifically. From what I can gather, changing that type has a (maybe unnecessary?) rippling effect through a bunch of provider packages, so I'll avoid trying to tackle that for now. At least this way the tool method works as expected when imported from the core package directly.
author_association: MEMBER
user: login: hntrl
created_at: 2025-04-25T19:34:41Z
body: @benjamincburns Some updates on my findings:

It turns out the error I was trying to corner has to do more with how typescript computes type inheritance rather than integration problems. When I add the conditional return types back to the interfaces ([284f3e5](https://github.com/langchain-ai/langchainjs/pull/8056/commits/284f3e53edc319171f66afc836e621a2d6ad348d)), the error that occurs downstream is with the prebuilt agents and how it passes tools around. I haven't been able to address this yet so I imagine CI is failing atm.

The crux of the error is `DynamicStructuredTool<...> is not assignable to StructuredToolInterface`.

If you try to add the interface inheritance directly on the class, the error comes from the `call` method where it's saying that the two arg types are incompatible. `<TArg extends StructuredToolCallInput<...>>(...) is not assignable to <TArg extends unknown>(...)`

It's almost like when evaluating if the dynamic tool conforms to the interface, TypeScript is computing the `SchemaT` param on the interface (which will always fall down to `unknown` bc of how the tool call input type is defined), assuming that's the reference type for the generic in the call method, and complaining that `StructuredToolCallInput != unknown`. If that's the case, this doesn't lend itself well to comparing generic methods like we're doing here to get the conditional return type. 

This is just specifically for `DynamicStructuredTool`. I can add inheritance from `StructuredToolInterface` to `Tool` and `DynamicTool` without any problems.
author_association: MEMBER
user: login: benjamincburns
created_at: 2025-04-28T00:57:03Z
body: So I confess that I got a bit [nerd sniped](https://xkcd.com/356/) by this last week and worked up a fix separately. I [branched](https://github.com/langchain-ai/langchainjs/commits/ben/fix-gh-8010/) off of an earlier version of this PR, so unfortunately our work has diverged. My fixes are in c8d08fe and 84cf083, with the latter commit having the more substantial adjustment.

The main problem that needed fixing was that I defined the default schema type on `StructuredTool` and `StructuredToolInterface` too narrowly when I added support for JSON Schema. Since `StructuredTool` is essentially the base type for all LangChain style tools, its default schema type should be the union of all possible type schemas. Prior to that change, in cases where the type system couldn't infer what the schema type may be (e.g. because the tool was passed in across some external boundary), we were incorrectly assuming that the tool would've been a zod schema.

My branch includes your initial submission, so I'd be happy to merge it as-is, but if you want to review and reconcile this work against those changes I'd be happy to wait for you to do that. I suspect that you may have made some improvements to the types on internal functions that I'm missing.
author_association: CONTRIBUTOR
user: login: hntrl
created_at: 2025-04-28T17:45:07Z
body: No shame in that! I know I'm guilty of that sometimes too. I missed the detail about the config object also determining the output type, so I appreciate you taking a crack at it.

I merged your branch here, added some changes for readabilities sake in how the return type is defined/used, and fixed the override for the invoke method on both the interface and the class.
author_association: MEMBER
user: login: benjamincburns
created_at: 2025-05-02T07:14:55Z
body: Rebased on current `main` and linearized history, which dropped a couple of duplicated commits.
author_association: CONTRIBUTOR
user: login: benjamincburns
created_at: 2025-05-02T07:17:07Z
body: Build passes locally - will merge if CI goes green 🤞.
author_association: CONTRIBUTOR
user: login: benjamincburns
created_at: 2025-05-02T09:01:24Z
body: Realized that I didn't have type tests for the tool methods (namely `call` and `invoke`). Added those and found some more issues. Still need to add them for `Tool`, however.
author_association: CONTRIBUTOR
user: login: hntrl
created_at: 2025-05-04T20:50:49Z
body: Added some nit changes more than anything:

I tied the return type to `DirectToolOutput` instead of using an explicit object type. As far as I can tell, there's no case where `lc_direct_tool_output` is ever false, so I changed the type there to always assume that property is true. [ce46fe2](https://github.com/langchain-ai/langchainjs/pull/8056/commits/ce46fe215c57d063f31ce551d75ee2d0d8908e3b)

Also hoisted the decision logic for determining if a tool call id exists in a config out to a utility just to match existing patterns. [1f10cf8](https://github.com/langchain-ai/langchainjs/pull/8056/commits/1f10cf8fc0934d5ac027bdf93c1894c4bdc2abf3)
author_association: MEMBER
user: login: benjamincburns
created_at: 2025-05-05T23:18:53Z
body: > > call returns ToolMessage only if the toolCall property of the config is set and contains a nonempty ID.

> Doesn't call also cover the case of returning a tool message if arg is a tool call? The continuation i'm following is after the validation logic (starting at let toolCallId: string | undefined;), toolCallId gets assigned if arg is a tool call, then _formatToolOutput returns a ToolMessage if toolCallId is defined.

> I could very well be missing something here...

Nah you're right - I meant to go back and strike out that comment. In the end I wound up doubling back on this and making the typing a bit more explicit as you saw, based on the devX that I was seeing when writing the tests.


author_association: CONTRIBUTOR
user: login: benjamincburns
created_at: 2025-05-07T01:16:27Z
body: > Still need to add them for `Tool`, however.

This was silly of me - `Tool` is abstract, and `DynamicTool` extends it - I think this is complete now, provided CI is green.
author_association: CONTRIBUTOR
user: login: benjamincburns
created_at: 2025-05-07T01:38:43Z
body: Thanks a ton for all your help on this one @hntrl! It was a pleasure working with you to get this over the finish line!
author_association: CONTRIBUTOR