deepagents 0.0.11__py3-none-any.whl → 0.0.12rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
deepagents/graph.py CHANGED
@@ -2,6 +2,7 @@ from typing import Sequence, Union, Callable, Any, Type, Optional
2
2
  from langchain_core.tools import BaseTool
3
3
  from langchain_core.language_models import LanguageModelLike
4
4
  from langgraph.types import Checkpointer
5
+ from langgraph.store.base import BaseStore
5
6
  from langchain.agents import create_agent
6
7
  from langchain.agents.middleware import AgentMiddleware, SummarizationMiddleware, HumanInTheLoopMiddleware
7
8
  from langchain.agents.middleware.human_in_the_loop import ToolConfig
@@ -20,6 +21,8 @@ def agent_builder(
20
21
  subagents: Optional[list[SubAgent | CustomSubAgent]] = None,
21
22
  context_schema: Optional[Type[Any]] = None,
22
23
  checkpointer: Optional[Checkpointer] = None,
24
+ store: Optional[BaseStore] = None,
25
+ use_longterm_memory: bool = False,
23
26
  is_async: bool = False,
24
27
  ):
25
28
  if model is None:
@@ -27,7 +30,9 @@ def agent_builder(
27
30
 
28
31
  deepagent_middleware = [
29
32
  PlanningMiddleware(),
30
- FilesystemMiddleware(),
33
+ FilesystemMiddleware(
34
+ use_longterm_memory=use_longterm_memory,
35
+ ),
31
36
  SubAgentMiddleware(
32
37
  default_subagent_tools=tools, # NOTE: These tools are piped to the general-purpose subagent.
33
38
  subagents=subagents if subagents is not None else [],
@@ -55,6 +60,7 @@ def agent_builder(
55
60
  middleware=deepagent_middleware,
56
61
  context_schema=context_schema,
57
62
  checkpointer=checkpointer,
63
+ store=store,
58
64
  )
59
65
 
60
66
  def create_deep_agent(
@@ -65,6 +71,8 @@ def create_deep_agent(
65
71
  subagents: Optional[list[SubAgent | CustomSubAgent]] = None,
66
72
  context_schema: Optional[Type[Any]] = None,
67
73
  checkpointer: Optional[Checkpointer] = None,
74
+ store: Optional[BaseStore] = None,
75
+ use_longterm_memory: bool = False,
68
76
  tool_configs: Optional[dict[str, bool | ToolConfig]] = None,
69
77
  ):
70
78
  """Create a deep agent.
@@ -85,6 +93,8 @@ def create_deep_agent(
85
93
  - (optional) `middleware` (list of AgentMiddleware)
86
94
  context_schema: The schema of the deep agent.
87
95
  checkpointer: Optional checkpointer for persisting agent state between runs.
96
+ store: Optional store for persisting longterm memories.
97
+ use_longterm_memory: Whether to use longterm memory - you must provide a store in order to use longterm memory.
88
98
  tool_configs: Optional Dict[str, HumanInTheLoopConfig] mapping tool names to interrupt configs.
89
99
  """
90
100
  return agent_builder(
@@ -95,6 +105,8 @@ def create_deep_agent(
95
105
  subagents=subagents,
96
106
  context_schema=context_schema,
97
107
  checkpointer=checkpointer,
108
+ store=store,
109
+ use_longterm_memory=use_longterm_memory,
98
110
  tool_configs=tool_configs,
99
111
  is_async=False,
100
112
  )
@@ -107,6 +119,8 @@ def async_create_deep_agent(
107
119
  subagents: Optional[list[SubAgent | CustomSubAgent]] = None,
108
120
  context_schema: Optional[Type[Any]] = None,
109
121
  checkpointer: Optional[Checkpointer] = None,
122
+ store: Optional[BaseStore] = None,
123
+ use_longterm_memory: bool = False,
110
124
  tool_configs: Optional[dict[str, bool | ToolConfig]] = None,
111
125
  ):
112
126
  """Create a deep agent.
@@ -127,6 +141,8 @@ def async_create_deep_agent(
127
141
  - (optional) `middleware` (list of AgentMiddleware)
128
142
  context_schema: The schema of the deep agent.
129
143
  checkpointer: Optional checkpointer for persisting agent state between runs.
144
+ use_longterm_memory: Whether to use longterm memory - you must provide a store in order to use longterm memory.
145
+ store: Optional store for persisting longterm memories.
130
146
  tool_configs: Optional Dict[str, HumanInTheLoopConfig] mapping tool names to interrupt configs.
131
147
  """
132
148
  return agent_builder(
@@ -137,6 +153,8 @@ def async_create_deep_agent(
137
153
  subagents=subagents,
138
154
  context_schema=context_schema,
139
155
  checkpointer=checkpointer,
156
+ store=store,
157
+ use_longterm_memory=use_longterm_memory,
140
158
  tool_configs=tool_configs,
141
159
  is_async=True,
142
160
  )
deepagents/middleware.py CHANGED
@@ -11,8 +11,8 @@ from langgraph.runtime import Runtime
11
11
  from langchain.tools.tool_node import InjectedState
12
12
  from typing import Annotated
13
13
  from deepagents.state import PlanningState, FilesystemState
14
- from deepagents.tools import write_todos, ls, read_file, write_file, edit_file
15
- from deepagents.prompts import WRITE_TODOS_SYSTEM_PROMPT, TASK_SYSTEM_PROMPT, FILESYSTEM_SYSTEM_PROMPT, TASK_TOOL_DESCRIPTION, BASE_AGENT_PROMPT
14
+ from deepagents.tools import write_todos, get_filesystem_tools
15
+ from deepagents.prompts import WRITE_TODOS_SYSTEM_PROMPT, TASK_SYSTEM_PROMPT, FILESYSTEM_SYSTEM_PROMPT, FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT, TASK_TOOL_DESCRIPTION, BASE_AGENT_PROMPT
16
16
  from deepagents.types import SubAgent, CustomSubAgent
17
17
 
18
18
  ###########################
@@ -24,7 +24,10 @@ class PlanningMiddleware(AgentMiddleware):
24
24
  tools = [write_todos]
25
25
 
26
26
  def modify_model_request(self, request: ModelRequest, agent_state: PlanningState, runtime: Runtime) -> ModelRequest:
27
- request.system_prompt = request.system_prompt + "\n\n" + WRITE_TODOS_SYSTEM_PROMPT
27
+ if request.system_prompt is None:
28
+ request.system_prompt = WRITE_TODOS_SYSTEM_PROMPT
29
+ else:
30
+ request.system_prompt = request.system_prompt + "\n\n" + WRITE_TODOS_SYSTEM_PROMPT
28
31
  return request
29
32
 
30
33
  ###########################
@@ -33,10 +36,21 @@ class PlanningMiddleware(AgentMiddleware):
33
36
 
34
37
  class FilesystemMiddleware(AgentMiddleware):
35
38
  state_schema = FilesystemState
36
- tools = [ls, read_file, write_file, edit_file]
39
+
40
+ def __init__(self, *, use_longterm_memory: bool = False, system_prompt: str = None, custom_tool_descriptions: dict[str, str] = {}) -> None:
41
+ self.system_prompt = FILESYSTEM_SYSTEM_PROMPT
42
+ if system_prompt is not None:
43
+ self.system_prompt = system_prompt
44
+ elif use_longterm_memory:
45
+ self.system_prompt += FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT
46
+
47
+ self.tools = get_filesystem_tools(use_longterm_memory, custom_tool_descriptions)
37
48
 
38
49
  def modify_model_request(self, request: ModelRequest, agent_state: FilesystemState, runtime: Runtime) -> ModelRequest:
39
- request.system_prompt = request.system_prompt + "\n\n" + FILESYSTEM_SYSTEM_PROMPT
50
+ if request.system_prompt is None:
51
+ request.system_prompt = self.system_prompt
52
+ else:
53
+ request.system_prompt = request.system_prompt + "\n\n" + self.system_prompt
40
54
  return request
41
55
 
42
56
  ###########################
@@ -61,7 +75,10 @@ class SubAgentMiddleware(AgentMiddleware):
61
75
  self.tools = [task_tool]
62
76
 
63
77
  def modify_model_request(self, request: ModelRequest, agent_state: AgentState, runtime: Runtime) -> ModelRequest:
64
- request.system_prompt = request.system_prompt + "\n\n" + TASK_SYSTEM_PROMPT
78
+ if request.system_prompt is None:
79
+ request.system_prompt = TASK_SYSTEM_PROMPT
80
+ else:
81
+ request.system_prompt = request.system_prompt + "\n\n" + TASK_SYSTEM_PROMPT
65
82
  return request
66
83
 
67
84
  def _get_agents(
deepagents/prompts.py CHANGED
@@ -323,14 +323,15 @@ Since the user is greeting, use the greeting-responder agent to respond with a f
323
323
  assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
324
324
  </example>"""
325
325
 
326
- LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the local filesystem.
326
+ LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the filesystem.
327
327
 
328
328
  Usage:
329
- - The list_files tool will return a list of all files in the local filesystem.
329
+ - The list_files tool will return a list of all files in the filesystem.
330
330
  - This is very useful for exploring the file system and finding the right file to read or edit.
331
331
  - You should almost ALWAYS use this tool before using the Read or Edit tools."""
332
+ LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = "\n- Files from the longterm filesystem will be prefixed with the memories/ path."
332
333
 
333
- READ_FILE_TOOL_DESCRIPTION = """Reads a file from the local filesystem. You can access any file directly by using this tool.
334
+ READ_FILE_TOOL_DESCRIPTION = """Reads a file from the filesystem. You can access any file directly by using this tool.
334
335
  Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
335
336
 
336
337
  Usage:
@@ -339,27 +340,47 @@ Usage:
339
340
  - You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
340
341
  - Any lines longer than 2000 characters will be truncated
341
342
  - Results are returned using cat -n format, with line numbers starting at 1
342
- - You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
343
+ - You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
343
344
  - If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
344
345
  - You should ALWAYS make sure a file has been read before editing it."""
346
+ READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = "\n- file_paths prefixed with the memories/ path will be read from the longterm filesystem."
345
347
 
346
- EDIT_FILE_TOOL_DESCRIPTION = """Performs exact string replacements in files.
348
+ EDIT_FILE_TOOL_DESCRIPTION = """Performs exact string replacements in files.
347
349
 
348
350
  Usage:
349
- - You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
351
+ - You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
350
352
  - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
351
353
  - ALWAYS prefer editing existing files. NEVER write new files unless explicitly required.
352
354
  - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
353
- - The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.
355
+ - The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.
354
356
  - Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance."""
357
+ EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = "\n- You can edit files in the longterm filesystem by prefixing the filename with the memories/ path."
355
358
 
356
- WRITE_FILE_TOOL_DESCRIPTION = """Writes to a file in the local filesystem.
359
+ WRITE_FILE_TOOL_DESCRIPTION = """Writes to a new file in the filesystem.
357
360
 
358
361
  Usage:
359
362
  - The file_path parameter must be an absolute path, not a relative path
360
363
  - The content parameter must be a string
361
364
  - The write_file tool will create the a new file.
362
- - Prefer to edit existing files over creating new ones when possible."""
365
+ - Prefer to edit existing files over creating new ones when possible.
366
+ - file_paths prefixed with the memories/ path will be written to the longterm filesystem."""
367
+ WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = "\n- file_paths prefixed with the memories/ path will be written to the longterm filesystem."
368
+
369
+ FILESYSTEM_SYSTEM_PROMPT = """## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`
370
+
371
+ You have access to a filesystem which you can interact with using these tools.
372
+ Do not prepend a / to file_paths.
373
+
374
+ - ls: list all files in the filesystem
375
+ - read_file: read a file from the filesystem
376
+ - write_file: write to a file in the filesystem
377
+ - edit_file: edit a file in the filesystem"""
378
+ FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT = """
379
+
380
+ You also have access to a longterm filesystem in which you can store files that you want to keep around for longer than the current conversation.
381
+ In order to interact with the longterm filesystem, you can use those same tools, but filenames must be prefixed with the memories/ path.
382
+ Remember, to interact with the longterm filesystem, you must prefix the filename with the memories/ path."""
383
+
363
384
 
364
385
  WRITE_TODOS_SYSTEM_PROMPT = """## `write_todos`
365
386
 
@@ -403,14 +424,6 @@ When NOT to use the task tool:
403
424
  - Remember to use the `task` tool to silo independent tasks within a multi-part objective.
404
425
  - You should use the `task` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient."""
405
426
 
406
- FILESYSTEM_SYSTEM_PROMPT = """## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`
407
-
408
- You have access to a local, private filesystem which you can interact with using these tools.
409
- - ls: list all files in the local filesystem
410
- - read_file: read a file from the local filesystem
411
- - write_file: write to a file in the local filesystem
412
- - edit_file: edit a file in the local filesystem"""
413
-
414
427
  BASE_AGENT_PROMPT = """
415
428
  In order to complete the objective that the user asks of you, you have access to a number of standard tools.
416
429
  """
deepagents/tools.py CHANGED
@@ -1,8 +1,10 @@
1
+ from re import L
1
2
  from langchain_core.tools import tool, InjectedToolCallId
2
3
  from langchain_core.messages import ToolMessage
3
4
  from langgraph.types import Command
5
+ from langgraph.runtime import get_runtime, Runtime
4
6
  from langchain.tools.tool_node import InjectedState
5
- from typing import Annotated, Union
7
+ from typing import Annotated, Any
6
8
  from deepagents.state import Todo, FilesystemState
7
9
  from deepagents.prompts import (
8
10
  WRITE_TODOS_TOOL_DESCRIPTION,
@@ -10,8 +12,29 @@ from deepagents.prompts import (
10
12
  READ_FILE_TOOL_DESCRIPTION,
11
13
  WRITE_FILE_TOOL_DESCRIPTION,
12
14
  EDIT_FILE_TOOL_DESCRIPTION,
15
+ LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT,
16
+ READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT,
17
+ WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT,
18
+ EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT,
13
19
  )
14
20
 
21
+ def has_memories_prefix(file_path: str) -> bool:
22
+ return file_path.startswith("memories/")
23
+
24
+ def append_memories_prefix(file_path: str) -> str:
25
+ return f"memories/{file_path}"
26
+
27
+ def strip_memories_prefix(file_path: str) -> str:
28
+ return file_path.replace("memories/", "")
29
+
30
+ def get_namespace(runtime: Runtime[Any]) -> tuple[str, str]:
31
+ namespace = ("filesystem")
32
+ if runtime.context is None:
33
+ return namespace
34
+ assistant_id = runtime.context.get("assistant_id")
35
+ if assistant_id is None:
36
+ return namespace
37
+ return (assistant_id, "filesystem")
15
38
 
16
39
  @tool(description=WRITE_TODOS_TOOL_DESCRIPTION)
17
40
  def write_todos(
@@ -26,123 +49,265 @@ def write_todos(
26
49
  }
27
50
  )
28
51
 
52
+ def ls_tool_generator(has_longterm_memory: bool, custom_description: str = None) -> tool:
53
+ tool_description = LIST_FILES_TOOL_DESCRIPTION
54
+ if custom_description:
55
+ tool_description = custom_description
56
+ elif has_longterm_memory:
57
+ tool_description += LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
29
58
 
30
- @tool(description=LIST_FILES_TOOL_DESCRIPTION)
31
- def ls(state: Annotated[FilesystemState, InjectedState]) -> list[str]:
32
- """List all files"""
33
- return list(state.get("files", {}).keys())
34
-
35
-
36
- @tool(description=READ_FILE_TOOL_DESCRIPTION)
37
- def read_file(
38
- file_path: str,
39
- state: Annotated[FilesystemState, InjectedState],
40
- offset: int = 0,
41
- limit: int = 2000,
42
- ) -> str:
43
- mock_filesystem = state.get("files", {})
44
- if file_path not in mock_filesystem:
45
- return f"Error: File '{file_path}' not found"
46
-
47
- # Get file content
48
- content = mock_filesystem[file_path]
59
+ if has_longterm_memory:
60
+ # Tool with Long-term memory
61
+ @tool(description=tool_description)
62
+ def ls(state: Annotated[FilesystemState, InjectedState]) -> list[str]:
63
+ files = []
64
+ files.extend(list(state.get("files", {}).keys()))
49
65
 
50
- # Handle empty file
51
- if not content or content.strip() == "":
52
- return "System reminder: File exists but has empty contents"
53
-
54
- # Split content into lines
55
- lines = content.splitlines()
66
+ runtime = get_runtime()
67
+ store = runtime.store
68
+ if store is None:
69
+ raise ValueError("Longterm memory is enabled, but no store is available")
70
+ namespace = get_namespace(runtime)
71
+ file_data_list = store.search(namespace)
72
+ memories_files = [append_memories_prefix(f.key) for f in file_data_list]
73
+ files.extend(memories_files)
74
+ return files
75
+ else:
76
+ # Tool without long-term memory
77
+ @tool(description=tool_description)
78
+ def ls(state: Annotated[FilesystemState, InjectedState]) -> list[str]:
79
+ files = list(state.get("files", {}).keys())
80
+ return files
81
+ return ls
82
+
56
83
 
57
- # Apply line offset and limit
58
- start_idx = offset
59
- end_idx = min(start_idx + limit, len(lines))
84
+ def read_file_tool_generator(has_longterm_memory: bool, custom_description: str = None) -> tool:
85
+ tool_description = READ_FILE_TOOL_DESCRIPTION
86
+ if custom_description:
87
+ tool_description = custom_description
88
+ elif has_longterm_memory:
89
+ tool_description += READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
60
90
 
61
- # Handle case where offset is beyond file length
62
- if start_idx >= len(lines):
63
- return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
91
+ if has_longterm_memory:
92
+ # Tool with Long-term memory
93
+ @tool(description=tool_description)
94
+ def read_file(
95
+ file_path: str,
96
+ state: Annotated[FilesystemState, InjectedState],
97
+ offset: int = 0,
98
+ limit: int = 2000,
99
+ ) -> str:
100
+ if has_memories_prefix(file_path):
101
+ stripped_file_path = strip_memories_prefix(file_path)
102
+ runtime = get_runtime()
103
+ store = runtime.store
104
+ if store is None:
105
+ raise ValueError("Longterm memory is enabled, but no store is available")
106
+ namespace = get_namespace(runtime)
107
+ item = store.get(namespace, stripped_file_path)
108
+ if item is None:
109
+ return f"Error: File '{file_path}' not found"
110
+ content = item.value
111
+ else:
112
+ mock_filesystem = state.get("files", {})
113
+ if file_path not in mock_filesystem:
114
+ return f"Error: File '{file_path}' not found"
115
+ content = mock_filesystem[file_path]
116
+ if not content or content.strip() == "":
117
+ return "System reminder: File exists but has empty contents"
118
+ lines = content.splitlines()
119
+ start_idx = offset
120
+ end_idx = min(start_idx + limit, len(lines))
121
+ if start_idx >= len(lines):
122
+ return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
123
+ result_lines = []
124
+ for i in range(start_idx, end_idx):
125
+ line_content = lines[i]
126
+ if len(line_content) > 2000:
127
+ line_content = line_content[:2000]
128
+ line_number = i + 1
129
+ result_lines.append(f"{line_number:6d}\t{line_content}")
64
130
 
65
- # Format output with line numbers (cat -n format)
66
- result_lines = []
67
- for i in range(start_idx, end_idx):
68
- line_content = lines[i]
131
+ return "\n".join(result_lines)
132
+ else:
133
+ # Tool without long-term memory
134
+ @tool(description=tool_description)
135
+ def read_file(
136
+ file_path: str,
137
+ state: Annotated[FilesystemState, InjectedState],
138
+ offset: int = 0,
139
+ limit: int = 2000,
140
+ ) -> str:
141
+ mock_filesystem = state.get("files", {})
142
+ if file_path not in mock_filesystem:
143
+ return f"Error: File '{file_path}' not found"
144
+ content = mock_filesystem[file_path]
145
+ if not content or content.strip() == "":
146
+ return "System reminder: File exists but has empty contents"
147
+ lines = content.splitlines()
148
+ start_idx = offset
149
+ end_idx = min(start_idx + limit, len(lines))
150
+ if start_idx >= len(lines):
151
+ return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
152
+ result_lines = []
153
+ for i in range(start_idx, end_idx):
154
+ line_content = lines[i]
155
+ if len(line_content) > 2000:
156
+ line_content = line_content[:2000]
157
+ line_number = i + 1
158
+ result_lines.append(f"{line_number:6d}\t{line_content}")
69
159
 
70
- # Truncate lines longer than 2000 characters
71
- if len(line_content) > 2000:
72
- line_content = line_content[:2000]
160
+ return "\n".join(result_lines)
161
+ return read_file
73
162
 
74
- # Line numbers start at 1, so add 1 to the index
75
- line_number = i + 1
76
- result_lines.append(f"{line_number:6d}\t{line_content}")
77
163
 
78
- return "\n".join(result_lines)
164
+ def write_file_tool_generator(has_longterm_memory: bool, custom_description: str = None) -> tool:
165
+ tool_description = WRITE_FILE_TOOL_DESCRIPTION
166
+ if custom_description:
167
+ tool_description = custom_description
168
+ elif has_longterm_memory:
169
+ tool_description += WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
79
170
 
171
+ if has_longterm_memory:
172
+ # Tool with Long-term memory
173
+ @tool(description=tool_description)
174
+ def write_file(file_path: str, content: str, state: Annotated[FilesystemState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId]) -> Command:
175
+ if has_memories_prefix(file_path):
176
+ stripped_file_path = strip_memories_prefix(file_path)
177
+ runtime = get_runtime()
178
+ store = runtime.store
179
+ if store is None:
180
+ raise ValueError("Longterm memory is enabled, but no store is available")
181
+ namespace = get_namespace(runtime)
182
+ store.put(namespace, stripped_file_path, content)
183
+ return Command(
184
+ update={
185
+ "messages": [ToolMessage(f"Updated longterm memories file {file_path}", tool_call_id=tool_call_id)]
186
+ }
187
+ )
188
+ else:
189
+ mock_filesystem = state.get("files", {})
190
+ mock_filesystem[file_path] = content
191
+ return Command(
192
+ update={
193
+ "files": mock_filesystem,
194
+ "messages": [ToolMessage(f"Updated file {file_path}", tool_call_id=tool_call_id)]
195
+ }
196
+ )
197
+ else:
198
+ # Tool without long-term memory
199
+ @tool(description=tool_description)
200
+ def write_file(file_path: str, content: str, state: Annotated[FilesystemState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId]) -> Command:
201
+ mock_filesystem = state.get("files", {})
202
+ mock_filesystem[file_path] = content
203
+ return Command(
204
+ update={
205
+ "files": mock_filesystem,
206
+ "messages": [ToolMessage(f"Updated file {file_path}", tool_call_id=tool_call_id)]
207
+ }
208
+ )
209
+ return write_file
80
210
 
81
- @tool(description=WRITE_FILE_TOOL_DESCRIPTION)
82
- def write_file(
83
- file_path: str,
84
- content: str,
85
- state: Annotated[FilesystemState, InjectedState],
86
- tool_call_id: Annotated[str, InjectedToolCallId],
87
- ) -> Command:
88
- files = state.get("files", {})
89
- files[file_path] = content
90
- return Command(
91
- update={
92
- "files": files,
93
- "messages": [
94
- ToolMessage(f"Updated file {file_path}", tool_call_id=tool_call_id)
95
- ],
96
- }
97
- )
98
211
 
212
+ def edit_file_tool_generator(has_longterm_memory: bool, custom_description: str = None) -> tool:
213
+ tool_description = EDIT_FILE_TOOL_DESCRIPTION
214
+ if custom_description:
215
+ tool_description = custom_description
216
+ elif has_longterm_memory:
217
+ tool_description += EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
99
218
 
100
- @tool(description=EDIT_FILE_TOOL_DESCRIPTION)
101
- def edit_file(
102
- file_path: str,
103
- old_string: str,
104
- new_string: str,
105
- state: Annotated[FilesystemState, InjectedState],
106
- tool_call_id: Annotated[str, InjectedToolCallId],
107
- replace_all: bool = False,
108
- ) -> Union[Command, str]:
109
- """Write to a file."""
110
- mock_filesystem = state.get("files", {})
111
- # Check if file exists in mock filesystem
112
- if file_path not in mock_filesystem:
113
- return f"Error: File '{file_path}' not found"
114
-
115
- # Get current file content
116
- content = mock_filesystem[file_path]
117
-
118
- # Check if old_string exists in the file
119
- if old_string not in content:
120
- return f"Error: String not found in file: '{old_string}'"
121
-
122
- # If not replace_all, check for uniqueness
123
- if not replace_all:
124
- occurrences = content.count(old_string)
125
- if occurrences > 1:
126
- return f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
127
- elif occurrences == 0:
128
- return f"Error: String not found in file: '{old_string}'"
129
-
130
- # Perform the replacement
131
- if replace_all:
132
- new_content = content.replace(old_string, new_string)
133
- replacement_count = content.count(old_string)
134
- result_msg = f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'"
219
+ if has_longterm_memory:
220
+ # Tool with Long-term memory
221
+ @tool(description=tool_description)
222
+ def edit_file(file_path: str, old_string: str, new_string: str, state: Annotated[FilesystemState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId], replace_all: bool = False) -> Command:
223
+ if has_memories_prefix(file_path):
224
+ stripped_file_path = strip_memories_prefix(file_path)
225
+ runtime = get_runtime()
226
+ store = runtime.store
227
+ if store is None:
228
+ raise ValueError("Longterm memory is enabled, but no store is available")
229
+ namespace = get_namespace(runtime)
230
+ item = store.get(namespace, stripped_file_path)
231
+ if item is None:
232
+ return f"Error: File '{file_path}' not found"
233
+ content = item.value
234
+ if old_string not in content:
235
+ return f"Error: String not found in file: '{old_string}'"
236
+ if not replace_all:
237
+ occurrences = content.count(old_string)
238
+ if occurrences > 1:
239
+ return f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
240
+ elif occurrences == 0:
241
+ return f"Error: String not found in file: '{old_string}'"
242
+ new_content = content.replace(old_string, new_string)
243
+ replacement_count = content.count(old_string)
244
+ store.put(namespace, stripped_file_path, new_content)
245
+ return Command(
246
+ update={
247
+ "messages": [ToolMessage(f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'", tool_call_id=tool_call_id)]
248
+ }
249
+ )
250
+ else:
251
+ mock_filesystem = state.get("files", {})
252
+ if file_path not in mock_filesystem:
253
+ return f"Error: File '{file_path}' not found"
254
+ content = mock_filesystem[file_path]
255
+ if old_string not in content:
256
+ return f"Error: String not found in file: '{old_string}'"
257
+ if not replace_all:
258
+ occurrences = content.count(old_string)
259
+ if occurrences > 1:
260
+ return f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
261
+ elif occurrences == 0:
262
+ return f"Error: String not found in file: '{old_string}'"
263
+ new_content = content.replace(old_string, new_string)
264
+ replacement_count = content.count(old_string)
265
+ result_msg = f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'"
266
+ mock_filesystem[file_path] = new_content
267
+ return Command(
268
+ update={
269
+ "files": mock_filesystem,
270
+ "messages": [ToolMessage(result_msg, tool_call_id=tool_call_id)],
271
+ }
272
+ )
135
273
  else:
136
- new_content = content.replace(
137
- old_string, new_string, 1
138
- ) # Replace only first occurrence
139
- result_msg = f"Successfully replaced string in '{file_path}'"
274
+ # Tool without long-term memory
275
+ @tool(description=tool_description)
276
+ def edit_file(file_path: str, old_string: str, new_string: str, state: Annotated[FilesystemState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId], replace_all: bool = False) -> Command:
277
+ mock_filesystem = state.get("files", {})
278
+ if file_path not in mock_filesystem:
279
+ return f"Error: File '{file_path}' not found"
280
+ content = mock_filesystem[file_path]
281
+ if old_string not in content:
282
+ return f"Error: String not found in file: '{old_string}'"
283
+ if not replace_all:
284
+ occurrences = content.count(old_string)
285
+ if occurrences > 1:
286
+ return f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
287
+ elif occurrences == 0:
288
+ return f"Error: String not found in file: '{old_string}'"
289
+ new_content = content.replace(old_string, new_string)
290
+ replacement_count = content.count(old_string)
291
+ result_msg = f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'"
292
+ mock_filesystem[file_path] = new_content
293
+ return Command(
294
+ update={
295
+ "files": mock_filesystem,
296
+ "messages": [ToolMessage(result_msg, tool_call_id=tool_call_id)],
297
+ }
298
+ )
299
+ return edit_file
140
300
 
141
- # Update the mock filesystem
142
- mock_filesystem[file_path] = new_content
143
- return Command(
144
- update={
145
- "files": mock_filesystem,
146
- "messages": [ToolMessage(result_msg, tool_call_id=tool_call_id)],
147
- }
148
- )
301
+ TOOL_GENERATORS = {
302
+ "ls": ls_tool_generator,
303
+ "read_file": read_file_tool_generator,
304
+ "write_file": write_file_tool_generator,
305
+ "edit_file": edit_file_tool_generator,
306
+ }
307
+
308
+ def get_filesystem_tools(has_longterm_memory: bool, custom_tool_descriptions: dict[str, str] = {}) -> list[tool]:
309
+ tools = []
310
+ for tool_name, tool_generator in TOOL_GENERATORS.items():
311
+ tool = tool_generator(has_longterm_memory, custom_tool_descriptions.get(tool_name, None))
312
+ tools.append(tool)
313
+ return tools
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.0.11
3
+ Version: 0.0.12rc1
4
4
  Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
5
5
  License: MIT
6
6
  Requires-Python: <4.0,>=3.11
@@ -0,0 +1,18 @@
1
+ deepagents/__init__.py,sha256=fA_91ByxPb3e8aPfci43zOXrWz8ylh_CFQALo7EUKi8,312
2
+ deepagents/graph.py,sha256=1zK8l-kKwpyoAtNXyB0HfEFPEp5aF7G1dMN0xsm4q18,7163
3
+ deepagents/middleware.py,sha256=Yovjyr3Zls3vNIeY7mawFk-9tAWpqCQbAN1vaegEk3I,8180
4
+ deepagents/model.py,sha256=VyRIkdeXJH8HqLrudTKucHpBTtrwMFTQGRlXBj0kUyo,155
5
+ deepagents/prompts.py,sha256=nnLAiPNY20-GNwhOzxe9Ia2CzcdrJqILSchPpPKNuZU,26228
6
+ deepagents/state.py,sha256=8so3MgL-zRPYP8Ci_OuVg4wHrs5uAXCErKF1AjjCSt8,726
7
+ deepagents/tools.py,sha256=lspab_EnKxVY4_SJP9sFJJBDks9EmHGXNcR4ebX8lHM,14567
8
+ deepagents/types.py,sha256=5KBSUPlWOnv9It3SnJCMHrOtp9Y4_NQGtGCp69JsEjE,694
9
+ deepagents-0.0.12rc1.dist-info/licenses/LICENSE,sha256=c__BaxUCK69leo2yEKynf8lWndu8iwYwge1CbyqAe-E,1071
10
+ tests/test_deepagents.py,sha256=nTILGu2lsM908sq6MRy0PEXV0wuVMyvXS6Bm2NmEAm4,7655
11
+ tests/test_filesystem.py,sha256=jrYkUqCDw2OHD1rpv3Vux1eKjT_d8CqM25g1KuvZBwk,9119
12
+ tests/test_hitl.py,sha256=B16ZFiyaVSOcDLz7mh1RTaQZ93EMTKOPUY-IEslkcfM,2460
13
+ tests/test_middleware.py,sha256=3HYmTx0Jw4XTNJjqLYeyGS_QZzcqkFuKfShtajIDhF4,2146
14
+ tests/utils.py,sha256=B7Mc6VMzEkCmx8lORjdYNXp82tn-W3nwBkrusfJjAx0,2912
15
+ deepagents-0.0.12rc1.dist-info/METADATA,sha256=W6_O9UQhpExMT9swmLRc4Nj55pI1Omegr0sPeCswGG8,17334
16
+ deepagents-0.0.12rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ deepagents-0.0.12rc1.dist-info/top_level.txt,sha256=_w9VMQtG4YDNg5A5eAeUre7dF7x7hk9zRpT9zsFaukY,17
18
+ deepagents-0.0.12rc1.dist-info/RECORD,,
@@ -0,0 +1,196 @@
1
+ from deepagents.graph import create_deep_agent
2
+ from deepagents.middleware import FilesystemMiddleware
3
+ from deepagents.prompts import WRITE_FILE_TOOL_DESCRIPTION, WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
4
+ from langchain.agents import create_agent
5
+ import pytest
6
+ from langchain_core.messages import HumanMessage
7
+ from langchain_anthropic import ChatAnthropic
8
+ from langgraph.store.memory import InMemoryStore
9
+ from langgraph.checkpoint.memory import MemorySaver
10
+ import uuid
11
+
12
+ class TestFilesystem:
13
+ def test_create_deepagent_without_store_and_with_longterm_memory_should_fail(self):
14
+ with pytest.raises(ValueError):
15
+ deepagent = create_deep_agent(tools=[], use_longterm_memory=True)
16
+ deepagent.invoke({"messages": [HumanMessage(content="List all of the files in your filesystem?")]})
17
+
18
+ def test_filesystem_system_prompt_override(self):
19
+ agent = create_agent(
20
+ model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
21
+ middleware=[
22
+ FilesystemMiddleware(
23
+ use_longterm_memory=False,
24
+ system_prompt="In every single response, you must say the word 'pokemon'! You love it!"
25
+ )
26
+ ]
27
+ )
28
+ response = agent.invoke({"messages": [HumanMessage(content="What do you like?")]})
29
+ assert "pokemon" in response["messages"][1].text.lower()
30
+
31
+ def test_filesystem_system_prompt_override_with_longterm_memory(self):
32
+ agent = create_agent(
33
+ model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
34
+ middleware=[
35
+ FilesystemMiddleware(
36
+ use_longterm_memory=True,
37
+ system_prompt="In every single response, you must say the word 'pokemon'! You love it!"
38
+ )
39
+ ],
40
+ store=InMemoryStore()
41
+ )
42
+ response = agent.invoke({"messages": [HumanMessage(content="What do you like?")]})
43
+ assert "pokemon" in response["messages"][1].text.lower()
44
+
45
+ def test_filesystem_tool_prompt_override(self):
46
+ agent = create_agent(
47
+ model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
48
+ middleware=[
49
+ FilesystemMiddleware(
50
+ use_longterm_memory=False,
51
+ custom_tool_descriptions={
52
+ "ls": "Charmander",
53
+ "read_file": "Bulbasaur",
54
+ "edit_file": "Squirtle"
55
+ }
56
+ )
57
+ ],
58
+ )
59
+ tools = agent.nodes["tools"].bound._tools_by_name
60
+ assert "ls" in tools
61
+ assert tools["ls"].description == "Charmander"
62
+ assert "read_file" in tools
63
+ assert tools["read_file"].description == "Bulbasaur"
64
+ assert "write_file" in tools
65
+ assert tools["write_file"].description == WRITE_FILE_TOOL_DESCRIPTION
66
+ assert "edit_file" in tools
67
+ assert tools["edit_file"].description == "Squirtle"
68
+
69
+ def test_filesystem_tool_prompt_override_with_longterm_memory(self):
70
+ agent = create_agent(
71
+ model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
72
+ middleware=[
73
+ FilesystemMiddleware(
74
+ use_longterm_memory=True,
75
+ custom_tool_descriptions={
76
+ "ls": "Charmander",
77
+ "read_file": "Bulbasaur",
78
+ "edit_file": "Squirtle"
79
+ }
80
+ )
81
+ ],
82
+ store=InMemoryStore()
83
+ )
84
+ tools = agent.nodes["tools"].bound._tools_by_name
85
+ assert "ls" in tools
86
+ assert tools["ls"].description == "Charmander"
87
+ assert "read_file" in tools
88
+ assert tools["read_file"].description == "Bulbasaur"
89
+ assert "write_file" in tools
90
+ assert tools["write_file"].description == WRITE_FILE_TOOL_DESCRIPTION + WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
91
+ assert "edit_file" in tools
92
+ assert tools["edit_file"].description == "Squirtle"
93
+
94
+
95
+ def test_longterm_memory_tools(self):
96
+ checkpointer = MemorySaver()
97
+ store = InMemoryStore()
98
+ agent = create_agent(
99
+ model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
100
+ middleware=[
101
+ FilesystemMiddleware(
102
+ use_longterm_memory=True,
103
+ )
104
+ ],
105
+ checkpointer=checkpointer,
106
+ store=store
107
+ )
108
+ assert_longterm_mem_tools(agent, store)
109
+
110
+
111
+ def test_longterm_memory_tools_deepagent(self):
112
+ checkpointer = MemorySaver()
113
+ store = InMemoryStore()
114
+ agent = create_deep_agent(
115
+ use_longterm_memory=True,
116
+ checkpointer=checkpointer,
117
+ store=store
118
+ )
119
+ assert_longterm_mem_tools(agent, store)
120
+
121
+
122
+ def test_shortterm_memory_tools_deepagent(self):
123
+ checkpointer = MemorySaver()
124
+ store = InMemoryStore()
125
+ agent = create_deep_agent(
126
+ use_longterm_memory=False,
127
+ checkpointer=checkpointer,
128
+ store=store
129
+ )
130
+ assert_shortterm_mem_tools(agent)
131
+
132
+
133
+ def assert_longterm_mem_tools(agent, store):
134
+ config = {"configurable": {"thread_id": uuid.uuid4()}}
135
+ agent.invoke({"messages": [HumanMessage(content="Write a haiku about Charmander to longterm memory in charmander.txt, use the word 'fiery'")]}, config=config)
136
+
137
+ namespaces = store.list_namespaces()
138
+ assert len(namespaces) == 1
139
+ assert namespaces[0] == "filesystem"
140
+ file_item = store.get(("filesystem"), "charmander.txt")
141
+ assert file_item is not None
142
+ assert file_item.key == "charmander.txt"
143
+
144
+ config2 = {"configurable": {"thread_id": uuid.uuid4()}}
145
+ response = agent.invoke({"messages": [HumanMessage(content="Read the haiku about Charmander from longterm memory at charmander.txt")]}, config=config2)
146
+
147
+ messages = response["messages"]
148
+ read_file_message = next(message for message in messages if message.type == "tool" and message.name == "read_file")
149
+ assert "fiery" in read_file_message.content or "Fiery" in read_file_message.content
150
+
151
+ config3 = {"configurable": {"thread_id": uuid.uuid4()}}
152
+ response = agent.invoke({"messages": [HumanMessage(content="List all of the files in longterm memory")]}, config=config3)
153
+ messages = response["messages"]
154
+ ls_message = next(message for message in messages if message.type == "tool" and message.name == "ls")
155
+ assert "memories/charmander.txt" in ls_message.content
156
+
157
+ config4 = {"configurable": {"thread_id": uuid.uuid4()}}
158
+ response = agent.invoke({"messages": [HumanMessage(content="Edit the haiku about Charmander in longterm memory to use the word 'ember'")]}, config=config4)
159
+ file_item = store.get(("filesystem"), "charmander.txt")
160
+ assert file_item is not None
161
+ assert file_item.key == "charmander.txt"
162
+ assert "ember" in file_item.value or "Ember" in file_item.value
163
+
164
+ config5 = {"configurable": {"thread_id": uuid.uuid4()}}
165
+ response = agent.invoke({"messages": [HumanMessage(content="Read the haiku about Charmander from longterm memory at charmander.txt")]}, config=config5)
166
+ messages = response["messages"]
167
+ read_file_message = next(message for message in messages if message.type == "tool" and message.name == "read_file")
168
+ assert "ember" in read_file_message.content or "Ember" in read_file_message.content
169
+
170
+
171
+ def assert_shortterm_mem_tools(agent):
172
+ config = {"configurable": {"thread_id": uuid.uuid4()}}
173
+ response = agent.invoke({"messages": [HumanMessage(content="Write a haiku about Charmander to charmander.txt, use the word 'fiery'")]}, config=config)
174
+ files = response["files"]
175
+ assert "charmander.txt" in files
176
+
177
+ response = agent.invoke({"messages": [HumanMessage(content="Read the haiku about Charmander from charmander.txt")]}, config=config)
178
+ messages = response["messages"]
179
+ read_file_message = next(message for message in reversed(messages) if message.type == "tool" and message.name == "read_file")
180
+ assert "fiery" in read_file_message.content or "Fiery" in read_file_message.content
181
+
182
+ response = agent.invoke({"messages": [HumanMessage(content="List all of the files in memory")]}, config=config)
183
+ messages = response["messages"]
184
+ ls_message = next(message for message in messages if message.type == "tool" and message.name == "ls")
185
+ assert "charmander.txt" in ls_message.content
186
+
187
+ response = agent.invoke({"messages": [HumanMessage(content="Edit the haiku about Charmander to use the word 'ember'")]}, config=config)
188
+ files = response["files"]
189
+ assert "charmander.txt" in files
190
+ assert "ember" in files["charmander.txt"] or "Ember" in files["charmander.txt"]
191
+
192
+ response = agent.invoke({"messages": [HumanMessage(content="Read the haiku about Charmander at charmander.txt")]}, config=config)
193
+ messages = response["messages"]
194
+ read_file_message = next(message for message in reversed(messages) if message.type == "tool" and message.name == "read_file")
195
+ assert "ember" in read_file_message.content or "Ember" in read_file_message.content
196
+
@@ -1,17 +0,0 @@
1
- deepagents/__init__.py,sha256=fA_91ByxPb3e8aPfci43zOXrWz8ylh_CFQALo7EUKi8,312
2
- deepagents/graph.py,sha256=gnheqwT2Q2_YviDtsFRcCEivWHTzJWyBiJoDd7n3R-8,6295
3
- deepagents/middleware.py,sha256=QMQK4E6VaFr86DfmTvwzCHpaswO-vXPhlYyzfQe-ZSY,7360
4
- deepagents/model.py,sha256=VyRIkdeXJH8HqLrudTKucHpBTtrwMFTQGRlXBj0kUyo,155
5
- deepagents/prompts.py,sha256=mCnNGTRljfDUMXEfVRiBNGvhoc_wQjt0epiGMJkktgo,25150
6
- deepagents/state.py,sha256=8so3MgL-zRPYP8Ci_OuVg4wHrs5uAXCErKF1AjjCSt8,726
7
- deepagents/tools.py,sha256=KJw1lr6uO0Bw7mUNFmZ6lSuavJA_qx-3hJa3fjK5G6I,4776
8
- deepagents/types.py,sha256=5KBSUPlWOnv9It3SnJCMHrOtp9Y4_NQGtGCp69JsEjE,694
9
- deepagents-0.0.11.dist-info/licenses/LICENSE,sha256=c__BaxUCK69leo2yEKynf8lWndu8iwYwge1CbyqAe-E,1071
10
- tests/test_deepagents.py,sha256=nTILGu2lsM908sq6MRy0PEXV0wuVMyvXS6Bm2NmEAm4,7655
11
- tests/test_hitl.py,sha256=B16ZFiyaVSOcDLz7mh1RTaQZ93EMTKOPUY-IEslkcfM,2460
12
- tests/test_middleware.py,sha256=3HYmTx0Jw4XTNJjqLYeyGS_QZzcqkFuKfShtajIDhF4,2146
13
- tests/utils.py,sha256=B7Mc6VMzEkCmx8lORjdYNXp82tn-W3nwBkrusfJjAx0,2912
14
- deepagents-0.0.11.dist-info/METADATA,sha256=nUvc5cEmyGg83MF5KOVFeLuYB2kAXEb0wRA1tBVjL_U,17331
15
- deepagents-0.0.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- deepagents-0.0.11.dist-info/top_level.txt,sha256=_w9VMQtG4YDNg5A5eAeUre7dF7x7hk9zRpT9zsFaukY,17
17
- deepagents-0.0.11.dist-info/RECORD,,