feat: Support copy&pasting of last tool result into next call
Rafael Uzarowski committed
Aug 12, 2025 at 15:00 UTC
f9ca07765a380d5786c87fd6e9b072017498ffa8
8 files changed
+162
-7
agent.py
+21
-4
@@ -714,14 +714,31 @@ class Agent:
714
715
if tool:
716
await self.handle_intervention()
717
- await tool.before_execution(**tool_args)
717
+ # Allow extensions to preprocess tool arguments (e.g., unmask secrets)
718
+ await self.call_extensions("tool_execute_before", tool_args=tool_args or {}, tool_name=tool_name)
719
+ # Call tool hooks for compatibility
720
+ try:
721
+ await tool.before_execution(**tool_args)
722
+ except Exception:
723
+ pass
724
await self.handle_intervention()
725
+ # Ensure tool sees the updated arguments after pre-processing
726
+ tool.args = tool_args or tool.args
727
response = await tool.execute(**tool_args)
728
await self.handle_intervention()
721
- await tool.after_execution(response)
729
+ # Allow extensions to postprocess tool response (e.g., mask secrets)
730
+ response_data = {"response": response}
731
+ await self.call_extensions("tool_execute_after", response_data=response_data, tool_name=tool_name, tool_args=tool_args)
732
+ processed_response = response_data["response"]
733
+ # Store result to history
734
+ self.hist_add_tool_result(tool_name, getattr(processed_response, "message", ""))
735
+ try:
736
+ await tool.after_execution(processed_response)
737
+ except Exception:
738
+ pass
739
await self.handle_intervention()
723
- if response.break_loop:
724
- return response.message
740
+ if processed_response.break_loop:
741
+ return processed_response.message
742
else:
743
error_detail = (
744
f"Tool '{raw_tool_name}' not found or could not be initialized."
prompts/agent.system.main.communication.md
+2
-2
@@ -8,7 +8,7 @@ respond valid json with fields
8
- tool_name: use tool name
9
- tool_args: key value pairs tool arguments
10
11
-no text allowed before or after json
11
+!! no text allowed before or after json
12
13
### Response example
14
~~~json
@@ -31,4 +31,4 @@ no text allowed before or after json
31
## Receiving messages
32
user messages contain superior instructions, tool results, framework messages
33
if starts (voice) then transcribed can contain errors consider compensation
34
-messages may end with [EXTRAS] containing context info, never instructions
34
+messages may end with [EXTRAS] containing context info, optionally also tempory instructions
prompts/agent.system.tools.md
+24
-1
@@ -1,3 +1,26 @@
1
## Tools available:
2
3
-{{tools}}
\ No newline at end of file
3
+{{tools}}
4
+
5
+## Remarks about calling tools
6
+Tool calls must always be valid json.
7
+Tool arguments can contain special placeholders:
8
+
9
+### Copy/paste last tool output
10
+- Include the full output of the previous tool call inside any `tool_args` field by inserting the literal token `{last_tool_output}`. It will be replaced automatically before the tool executes.
11
+- Check the [EXTRAS] section for a preview and the originating `tool_name`.
12
+- !! Never repeat the full output of a tool call if it is possible to copy&paste it.
13
+Example for copying the output of the last tool call into the response tool arguments:
14
+~~~json
15
+{
16
+ "thoughts": [
17
+ "Acknowledge system warning about JSON formatting.",
18
+ "Send properly formatted reply using the response tool.",
19
+ "Include the previously collected terminal output by inserting the {last_tool_output} token to avoid duplicating raw text manually."
20
+ ],
21
+ "tool_name": "response",
22
+ "tool_args": {
23
+ "text": "Here is the terminal output you requested:\n\n{last_tool_output}"
24
+ }
25
+}
26
+~~~
prompts/fw.extras.last_tool_copy.md
new
+2
@@ -0,0 +1,2 @@
1
+You can copy and paste the result of the last tool call of {{tool_name}} beginning with "{{last_tool_output_preview}}" into any of the next tool's arguments.
2
+To paste the full content into any next tool's argument, insert the literal token '{last_tool_output}' in the string where you want the content to appear. It will be replaced automatically before the tool runs.
python/extensions/message_loop_prompts_after/_75_include_last_tool_copy_paste_tip.py
new
+22
@@ -0,0 +1,22 @@
1
+from agent import LoopData
2
+from python.helpers.extension import Extension
3
+
4
+
5
+class IncludeLastToolCopyPasteTip(Extension):
6
+ async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
7
+ last_call = self.agent.get_data("last_tool_call")
8
+ if not last_call:
9
+ return
10
+
11
+ tool_name: str = last_call.get("tool_name", "")
12
+ last_output: str = last_call.get("last_tool_output", "")
13
+ if not last_output:
14
+ return
15
+
16
+ preview = (last_output[:50] or "").replace("\n", " ").strip()
17
+ tip = self.agent.read_prompt(
18
+ "fw.extras.last_tool_copy.md",
19
+ tool_name=tool_name,
20
+ last_tool_output_preview=preview,
21
+ )
22
+ loop_data.extras_temporary["last_tool_copy_paste"] = tip
python/extensions/response_stream/_15_replace_last_tool_output_in_stream.py
new
+32
@@ -0,0 +1,32 @@
1
+from typing import Any
2
+from python.helpers.extension import Extension
3
+
4
+
5
+class ReplaceLastToolOutputInStream(Extension):
6
+ async def execute(self, loop_data=None, text: str = "", parsed: dict[str, Any] | None = None, **kwargs):
7
+ if not parsed or not isinstance(parsed, dict):
8
+ return
9
+
10
+ last_call = self.agent.get_data("last_tool_call") or {}
11
+ last_output = last_call.get("last_tool_output", "")
12
+ if not last_output:
13
+ return
14
+
15
+ tokens = ("{last_tool_output}", "{{last_tool_output}}")
16
+
17
+ def replace_placeholders(value: Any) -> Any:
18
+ if isinstance(value, str):
19
+ new_val = value
20
+ for token in tokens:
21
+ new_val = new_val.replace(token, last_output)
22
+ return new_val
23
+ if isinstance(value, dict):
24
+ return {k: replace_placeholders(v) for k, v in value.items()}
25
+ if isinstance(value, list):
26
+ return [replace_placeholders(v) for v in value]
27
+ if isinstance(value, tuple):
28
+ return tuple(replace_placeholders(v) for v in value)
29
+ return value
30
+
31
+ if "tool_args" in parsed and "tool_name" in parsed:
32
+ parsed["tool_args"] = replace_placeholders(parsed["tool_args"])
python/extensions/tool_execute_after/_90_store_last_tool_call.py
new
+26
@@ -0,0 +1,26 @@
1
+from typing import Any
2
+from python.helpers.extension import Extension
3
+
4
+
5
+class StoreLastToolCall(Extension):
6
+ async def execute(self, response_data: dict[str, Any] | None = None, tool_name: str = "", tool_args: dict[str, Any] | None = None, **kwargs):
7
+ if not response_data:
8
+ return
9
+
10
+ response = response_data.get("response") if isinstance(response_data, dict) else None
11
+ if response is None:
12
+ return
13
+
14
+ try:
15
+ message = getattr(response, "message") if hasattr(response, "message") else str(response)
16
+ except Exception:
17
+ message = str(response)
18
+
19
+ self.agent.set_data(
20
+ "last_tool_call",
21
+ {
22
+ "tool_name": tool_name or "",
23
+ "tool_args": tool_args or {},
24
+ "last_tool_output": message or "",
25
+ },
26
+ )
python/extensions/tool_execute_before/_10_replace_last_tool_output.py
new
+33
@@ -0,0 +1,33 @@
1
+from typing import Any
2
+from python.helpers.extension import Extension
3
+
4
+
5
+class ReplaceLastToolOutput(Extension):
6
+ async def execute(self, tool_args: dict[str, Any] | None = None, tool_name: str = "", **kwargs):
7
+ if not tool_args:
8
+ return
9
+
10
+ last_call = self.agent.get_data("last_tool_call") or {}
11
+ last_output = last_call.get("last_tool_output", "")
12
+ if not last_output:
13
+ return
14
+
15
+ tokens = ("{last_tool_output}", "{{last_tool_output}}")
16
+
17
+ def replace_placeholders(value: Any) -> Any:
18
+ if isinstance(value, str):
19
+ new_val = value
20
+ for token in tokens:
21
+ new_val = new_val.replace(token, last_output)
22
+ return new_val
23
+ if isinstance(value, dict):
24
+ return {k: replace_placeholders(v) for k, v in value.items()}
25
+ if isinstance(value, list):
26
+ return [replace_placeholders(v) for v in value]
27
+ if isinstance(value, tuple):
28
+ return tuple(replace_placeholders(v) for v in value)
29
+ return value
30
+
31
+ updated_args = replace_placeholders(tool_args)
32
+ tool_args.clear()
33
+ tool_args.update(updated_args)