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)