[GH-ISSUE #80] Feature Request: Simplified API for storing tool results in agent filesystem #27

Closed
opened 2026-02-16 06:16:54 -05:00 by yindo · 1 comment
Owner

Originally created by @Abdurihim on GitHub (Dec 14, 2025).
Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/80

Problem

When using custom tools with createDeepAgent, I need to manually store tool results to the agent's filesystem. The current approach requires understanding several internal concepts:

  1. Extracting StateAndStore from the tool's config parameter
  2. Creating a StateBackend instance
  3. Handling the WriteResult.filesUpdate field
  4. Returning a Command object to update LangGraph state

Current Implementation (verbose)

import { getCurrentTaskInput, Command } from "@langchain/langgraph";
import { ToolMessage } from "langchain";
import { type StateAndStore, StateBackend } from "deepagents";

tool(
  async (args, config) => {
    const result = await myExternalTool.handler(args);
    
    // Manual state extraction
    const stateAndStore: StateAndStore = {
      state: getCurrentTaskInput(config),
      store: (config as any).store,
    };
    
    // Manual backend creation
    const backend = new StateBackend(stateAndStore);
    
    // Store result
    const filePath = `/logs/${args.logid}.json`;
    const writeResult = await backend.write(filePath, JSON.stringify(result));
    
    // Handle filesUpdate manually
    if (writeResult.filesUpdate) {
      const message = new ToolMessage({
        content: `Saved to ${filePath}`,
        tool_call_id: config.toolCall?.id as string,
        name: "my_tool",
      });
      
      return new Command({
        update: { files: writeResult.filesUpdate, messages: [message] },
      });
    }
    
    return result;
  },
  { name: "my_tool", description: "...", schema: z.object({...}) }
)

Questions

  1. Is this the intended pattern? The middleware handles this internally, but for custom tools that need to store results, is there a simpler API?

  2. Why must we use Command? I understand LangGraph state is immutable, but could deepagents provide a helper that abstracts this?

  3. Potential simplified API? Would something like this be feasible?

import { toolWithFilesystem } from "deepagents";

// Option A: Helper wrapper
const myTool = toolWithFilesystem(
  async (args, { backend }) => {
    const result = await myExternalTool.handler(args);
    await backend.write(`/logs/${args.id}.json`, result);
    return result;
  },
  { name: "my_tool", ... }
);

// Option B: Utility function
import { saveToFilesystem } from "deepagents";

tool(
  async (args, config) => {
    const result = await myExternalTool.handler(args);
    return saveToFilesystem(config, `/logs/${args.id}.json`, result);
  },
  { name: "my_tool", ... }
);

Use Case

I'm building an AI-SRE agent that queries external log and trace APIs. I want to automatically store query results in the agent's filesystem so they can be referenced later in the conversation, without the agent needing to explicitly call write_file.

Environment

  • deepagents version: latest
  • langchain version: latest
  • Node.js version: 20+

Thank you for the great library! Looking forward to your guidance.

Originally created by @Abdurihim on GitHub (Dec 14, 2025). Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/80 ## Problem When using custom tools with `createDeepAgent`, I need to manually store tool results to the agent's filesystem. The current approach requires understanding several internal concepts: 1. Extracting `StateAndStore` from the tool's `config` parameter 2. Creating a `StateBackend` instance 3. Handling the `WriteResult.filesUpdate` field 4. Returning a `Command` object to update LangGraph state ### Current Implementation (verbose) ```typescript import { getCurrentTaskInput, Command } from "@langchain/langgraph"; import { ToolMessage } from "langchain"; import { type StateAndStore, StateBackend } from "deepagents"; tool( async (args, config) => { const result = await myExternalTool.handler(args); // Manual state extraction const stateAndStore: StateAndStore = { state: getCurrentTaskInput(config), store: (config as any).store, }; // Manual backend creation const backend = new StateBackend(stateAndStore); // Store result const filePath = `/logs/${args.logid}.json`; const writeResult = await backend.write(filePath, JSON.stringify(result)); // Handle filesUpdate manually if (writeResult.filesUpdate) { const message = new ToolMessage({ content: `Saved to ${filePath}`, tool_call_id: config.toolCall?.id as string, name: "my_tool", }); return new Command({ update: { files: writeResult.filesUpdate, messages: [message] }, }); } return result; }, { name: "my_tool", description: "...", schema: z.object({...}) } ) ``` ## Questions 1. **Is this the intended pattern?** The middleware handles this internally, but for custom tools that need to store results, is there a simpler API? 2. **Why must we use `Command`?** I understand LangGraph state is immutable, but could deepagents provide a helper that abstracts this? 3. **Potential simplified API?** Would something like this be feasible? ```typescript import { toolWithFilesystem } from "deepagents"; // Option A: Helper wrapper const myTool = toolWithFilesystem( async (args, { backend }) => { const result = await myExternalTool.handler(args); await backend.write(`/logs/${args.id}.json`, result); return result; }, { name: "my_tool", ... } ); // Option B: Utility function import { saveToFilesystem } from "deepagents"; tool( async (args, config) => { const result = await myExternalTool.handler(args); return saveToFilesystem(config, `/logs/${args.id}.json`, result); }, { name: "my_tool", ... } ); ``` ## Use Case I'm building an AI-SRE agent that queries external log and trace APIs. I want to automatically store query results in the agent's filesystem so they can be referenced later in the conversation, without the agent needing to explicitly call `write_file`. ## Environment - deepagents version: latest - langchain version: latest - Node.js version: 20+ Thank you for the great library! Looking forward to your guidance.
yindo closed this issue 2026-02-16 06:16:54 -05:00
Author
Owner

@christian-bromann commented on GitHub (Feb 2, 2026):

This is a great question. Let me address each part:

  1. Is this the intended pattern?

Yes, unfortunately the verbose pattern is currently the intended approach for custom tools that need to store results to the agent's filesystem when using StateBackend. It operates on a snapshot of state and returns filesUpdate which must be applied via a Command to actually update LangGraph's state. This is by design - LangGraph maintains state immutability for checkpointing and parallel execution safety.

  1. Why must we use Command?

The Command requirement is fundamental to how LangGraph works.

  1. Potential simplified API

Yes totally, something like this could work:

import { z } from "zod/v4";
import { tool, ToolMessage, ToolRuntime } from "langchain";
import {
  Command,
  getCurrentTaskInput,
  type LangGraphRunnableConfig,
} from "@langchain/langgraph";
import { StateBackend, type BackendProtocol } from "deepagents";

type FilesystemToolContext = {
  backend: BackendProtocol;
  config: ToolRuntime;
};

export function toolWithFilesystem<TArgs, TResult>(
  handler: (args: TArgs, ctx: FilesystemToolContext) => Promise<TResult>,
  options: { name: string; description: string; schema: z.ZodSchema<TArgs> },
) {
  return tool(async (args: TArgs, config: ToolRuntime) => {
    const stateAndStore = {
      state: getCurrentTaskInput(config as unknown as LangGraphRunnableConfig),
      store: (config as any).store,
    };
    const backend = new StateBackend(stateAndStore);

    const result = await handler(args, { backend, config });

    // If result is already a Command or ToolMessage, pass through
    if (result instanceof Command || ToolMessage.isInstance(result)) {
      return result;
    }

    const id =
      typeof args === "object" && args && "id" in args
        ? args.id
        : crypto.randomUUID();
    await backend.write(`/logs/${id}.json`, JSON.stringify(result));
    return result;
  }, options);
}

At this point however we don't feel confident to introduce this interface as part of the package. Would love to get more feedback on this from others. I will go ahead and close this. Let me know if you have any further question.

@christian-bromann commented on GitHub (Feb 2, 2026): This is a great question. Let me address each part: > 1. Is this the intended pattern? Yes, unfortunately the verbose pattern is currently the intended approach for custom tools that need to store results to the agent's filesystem when using StateBackend. It operates on a snapshot of state and returns filesUpdate which must be applied via a Command to actually update LangGraph's state. This is by design - LangGraph maintains state immutability for checkpointing and parallel execution safety. > 2. Why must we use Command? The Command requirement is fundamental to how LangGraph works. > 3. Potential simplified API Yes totally, something like this could work: ```ts import { z } from "zod/v4"; import { tool, ToolMessage, ToolRuntime } from "langchain"; import { Command, getCurrentTaskInput, type LangGraphRunnableConfig, } from "@langchain/langgraph"; import { StateBackend, type BackendProtocol } from "deepagents"; type FilesystemToolContext = { backend: BackendProtocol; config: ToolRuntime; }; export function toolWithFilesystem<TArgs, TResult>( handler: (args: TArgs, ctx: FilesystemToolContext) => Promise<TResult>, options: { name: string; description: string; schema: z.ZodSchema<TArgs> }, ) { return tool(async (args: TArgs, config: ToolRuntime) => { const stateAndStore = { state: getCurrentTaskInput(config as unknown as LangGraphRunnableConfig), store: (config as any).store, }; const backend = new StateBackend(stateAndStore); const result = await handler(args, { backend, config }); // If result is already a Command or ToolMessage, pass through if (result instanceof Command || ToolMessage.isInstance(result)) { return result; } const id = typeof args === "object" && args && "id" in args ? args.id : crypto.randomUUID(); await backend.write(`/logs/${id}.json`, JSON.stringify(result)); return result; }, options); } ``` At this point however we don't feel confident to introduce this interface as part of the package. Would love to get more feedback on this from others. I will go ahead and close this. Let me know if you have any further question.
yindo changed title from Feature Request: Simplified API for storing tool results in agent filesystem to [GH-ISSUE #80] Feature Request: Simplified API for storing tool results in agent filesystem 2026-06-05 17:21:02 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#27