Skip to content

Commit ae2f7e2

Browse files
sanil-23claude
andcommitted
feat(tools): csv_export tool + skills_agent oversized output handling
When a Composio tool returns a large payload inside skills_agent, the agent now has two prompt-driven paths: Path A — user wants an answer derived from the data: skills_agent extracts the answer in its next iteration and returns a targeted response (no file I/O needed). Path B — user wants the actual raw dataset: skills_agent calls csv_export (tabular data) or file_write (non-tabular) to persist the output to workspace/exports/, then returns a summary + file path. Changes: * NEW: src/openhuman/tools/impl/filesystem/csv_export.rs — CsvExportTool: parses JSON array, formats as CSV, writes to workspace/exports/{filename}. Handles missing keys (empty cells), nested values (JSON-serialised), and optional column ordering. Sandboxed via SecurityPolicy. * src/openhuman/tools/impl/filesystem/mod.rs — wire module * src/openhuman/tools/ops.rs — register CsvExportTool * src/openhuman/agent/harness/definition.rs — new extra_tools field on AgentDefinition, allowing named system tools to bypass category_filter * src/openhuman/agent/harness/subagent_runner.rs — inject extra_tools into allowed_indices after category filtering * src/openhuman/agent/agents/skills_agent/agent.toml — add file_write + csv_export via extra_tools * src/openhuman/agent/agents/skills_agent/prompt.md — new "Handling Oversized Tool Results" section with Path A (extract answer) vs Path B (export file) decision tree, intent-detection heuristics, and examples Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5f370c1 commit ae2f7e2

14 files changed

Lines changed: 614 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/openhuman/agent/agents/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,15 @@ mod tests {
300300
Some(crate::openhuman::tools::ToolCategory::Skill)
301301
);
302302
assert!(!def.omit_safety_preamble);
303+
// extra_tools lets file_write + csv_export bypass category_filter
304+
assert!(
305+
def.extra_tools.contains(&"file_write".to_string()),
306+
"skills_agent needs file_write in extra_tools for oversized payload export"
307+
);
308+
assert!(
309+
def.extra_tools.contains(&"csv_export".to_string()),
310+
"skills_agent needs csv_export in extra_tools for oversized payload export"
311+
);
303312
}
304313

305314
#[test]

src/openhuman/agent/agents/skills_agent/agent.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ omit_safety_preamble = false
1010
omit_skills_catalog = true
1111
category_filter = "skill"
1212

13+
# These system tools bypass category_filter so the agent can export
14+
# oversized tool payloads to workspace files instead of pasting them
15+
# inline (see "Handling Oversized Tool Results" in prompt.md).
16+
extra_tools = ["file_write", "csv_export"]
17+
1318
[model]
1419
hint = "agentic"
1520

src/openhuman/agent/agents/skills_agent/prompt.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,43 @@ You are the **Skills Agent**. You interact with connected external services prim
2525
- **Use memory context** — consult the injected memory context for details about the user's integrations and preferences.
2626
- **Be precise** — every tool expects a specific argument shape. Validate against the schema from `composio_list_tools` before calling.
2727
- **Report results** — state what action was taken and the outcome, including any cost reported by Composio.
28+
29+
## Handling Oversized Tool Results
30+
31+
When a tool returns a very large result (roughly 100 KB or more — you'll recognize it by the sheer volume of data in the response), decide which path to take based on what the user actually asked for:
32+
33+
### Path A — User wants an answer, not the raw data
34+
35+
Examples: "how many unread emails do I have?", "which GitHub issues are labeled P0?", "what's the most recent Slack message in #general?"
36+
37+
The data is a means to an answer. Do NOT dump the raw output. Instead:
38+
1. Scan the tool result for the specific facts that answer the user's question.
39+
2. Synthesize a concise answer referencing specific identifiers (issue numbers, email subjects, message timestamps).
40+
3. If you can't find the answer in one pass, use your remaining iterations to refine.
41+
42+
### Path B — User wants the actual data
43+
44+
Examples: "show me all open issues", "export my contacts", "give me the full email thread", "list all files in the drive folder"
45+
46+
The user wants the dataset itself, not a derivative. Do NOT try to paste it all inline — it won't fit. Instead:
47+
1. For **tabular data** (lists of issues, contacts, emails, files): call `csv_export` with the JSON array and a descriptive filename. Example:
48+
```
49+
csv_export(data=<the JSON array>, filename="github-open-issues-2026-04-16.csv")
50+
```
51+
2. For **non-tabular data** (full email bodies, document content, long threads): call `file_write` to save as `.md`. Example:
52+
```
53+
file_write(path="exports/slack-thread-general-2026-04-16.md", content=<formatted markdown>)
54+
```
55+
3. Return to the user: a brief summary of what's in the file (count of items, key highlights) plus the file path so they can access it.
56+
57+
### When in doubt
58+
59+
If you're unsure whether the user wants an answer or the data:
60+
- Default to **Path A** (extract and answer) for questions that start with "how", "which", "what", "when", "who", "is there", "are there", "find", "check".
61+
- Default to **Path B** (export) for requests that start with "show", "list", "export", "get", "fetch", "give me", "pull", "download".
62+
63+
### Important
64+
65+
- Never paste more than ~2000 characters of raw tool output directly in your response. If the output is larger, always use Path A or Path B.
66+
- The `csv_export` tool handles the CSV formatting — just pass it the JSON array string and a filename. Don't try to format CSV yourself.
67+
- File paths are relative to the workspace root. The `exports/` directory will be created automatically.

src/openhuman/agent/harness/builtin_definitions.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ pub fn fork_definition() -> AgentDefinition {
6464
disallowed_tools: vec![],
6565
skill_filter: None,
6666
category_filter: None,
67+
extra_tools: vec![],
6768
// Fork inherits the parent's max iterations from the runtime.
6869
max_iterations: 15,
6970
timeout_secs: None,
@@ -118,6 +119,20 @@ mod tests {
118119
assert!(!def.omit_memory_md);
119120
}
120121

122+
#[test]
123+
fn skills_agent_has_extra_tools_for_export() {
124+
let defs = all();
125+
let skills = defs.iter().find(|d| d.id == "skills_agent").unwrap();
126+
assert!(
127+
skills.extra_tools.contains(&"file_write".to_string()),
128+
"skills_agent must include file_write in extra_tools"
129+
);
130+
assert!(
131+
skills.extra_tools.contains(&"csv_export".to_string()),
132+
"skills_agent must include csv_export in extra_tools"
133+
);
134+
}
135+
121136
#[test]
122137
fn expected_builtin_ids_are_present() {
123138
let ids: Vec<String> = all().into_iter().map(|d| d.id).collect();

src/openhuman/agent/harness/definition.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ pub struct AgentDefinition {
117117
#[serde(default)]
118118
pub category_filter: Option<ToolCategory>,
119119

120+
/// Additional system tool names to include even when `category_filter`
121+
/// restricts to a different category. This allows an agent that is
122+
/// primarily scoped to `Skill` tools (e.g. `skills_agent`) to also
123+
/// access a handful of named system tools (e.g. `file_write`,
124+
/// `csv_export`) without removing the category filter entirely.
125+
///
126+
/// Tools listed here bypass the `category_filter` check but are still
127+
/// subject to `disallowed_tools` and `ToolScope` restrictions.
128+
#[serde(default)]
129+
pub extra_tools: Vec<String>,
130+
120131
// ── runtime limits ──────────────────────────────────────────────────
121132
/// Maximum number of tool iterations for this sub-agent's task.
122133
#[serde(default = "defaults::max_iterations")]
@@ -509,6 +520,7 @@ mod tests {
509520
disallowed_tools: vec![],
510521
skill_filter: None,
511522
category_filter: None,
523+
extra_tools: vec![],
512524
max_iterations: 8,
513525
timeout_secs: None,
514526
sandbox_mode: SandboxMode::None,

src/openhuman/agent/harness/payload_summarizer.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ mod tests {
338338
disallowed_tools: vec![],
339339
skill_filter: None,
340340
category_filter: None,
341+
extra_tools: vec![],
341342
max_iterations: 1,
342343
timeout_secs: None,
343344
sandbox_mode: SandboxMode::None,

src/openhuman/agent/harness/subagent_runner.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,29 @@ async fn run_typed_mode(
235235
category_filter,
236236
);
237237

238+
// ── Force-include extra_tools that bypass category_filter ──────────
239+
//
240+
// `extra_tools` lets an agent definition request specific system tools
241+
// even when `category_filter` restricts to a different category. For
242+
// example, `skills_agent` sets `category_filter = "skill"` but still
243+
// needs `file_write` and `csv_export` for exporting oversized payloads.
244+
if !definition.extra_tools.is_empty() {
245+
let disallow_set: std::collections::HashSet<&str> = definition
246+
.disallowed_tools
247+
.iter()
248+
.map(|s| s.as_str())
249+
.collect();
250+
for (i, tool) in parent.all_tools.iter().enumerate() {
251+
let name = tool.name();
252+
if definition.extra_tools.iter().any(|n| n == name)
253+
&& !allowed_indices.contains(&i)
254+
&& !disallow_set.contains(name)
255+
{
256+
allowed_indices.push(i);
257+
}
258+
}
259+
}
260+
238261
// ── Dynamic per-action toolkit tools (skills_agent + toolkit) ──────
239262
//
240263
// When `skills_agent` is spawned with a `toolkit` argument (e.g.
@@ -945,6 +968,7 @@ mod tests {
945968
disallowed_tools: vec![],
946969
skill_filter: None,
947970
category_filter: None,
971+
extra_tools: vec![],
948972
max_iterations: 5,
949973
timeout_secs: None,
950974
sandbox_mode: super::super::definition::SandboxMode::None,

src/openhuman/channels/runtime/dispatch.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ mod scoping_tests {
439439
disallowed_tools: vec![],
440440
skill_filter: None,
441441
category_filter: None,
442+
extra_tools: vec![],
442443
max_iterations: 8,
443444
timeout_secs: None,
444445
sandbox_mode: SandboxMode::None,

src/openhuman/context/debug_dump.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ mod tests {
726726
disallowed_tools: vec![],
727727
skill_filter: None,
728728
category_filter: Some(ToolCategory::Skill),
729+
extra_tools: vec![],
729730
max_iterations: 8,
730731
timeout_secs: None,
731732
sandbox_mode: SandboxMode::None,
@@ -908,6 +909,7 @@ mod tests {
908909
disallowed_tools: vec![],
909910
skill_filter: None,
910911
category_filter: None,
912+
extra_tools: vec![],
911913
max_iterations: 8,
912914
timeout_secs: None,
913915
sandbox_mode: SandboxMode::None,
@@ -1075,6 +1077,7 @@ mod tests {
10751077
disallowed_tools: vec![],
10761078
skill_filter: None,
10771079
category_filter: None,
1080+
extra_tools: vec![],
10781081
max_iterations: 2,
10791082
timeout_secs: None,
10801083
sandbox_mode: SandboxMode::None,
@@ -1124,6 +1127,7 @@ mod tests {
11241127
disallowed_tools: vec![],
11251128
skill_filter: None,
11261129
category_filter: None,
1130+
extra_tools: vec![],
11271131
max_iterations: 2,
11281132
timeout_secs: None,
11291133
sandbox_mode: SandboxMode::None,
@@ -1181,6 +1185,7 @@ mod tests {
11811185
disallowed_tools: vec![],
11821186
skill_filter: None,
11831187
category_filter: None,
1188+
extra_tools: vec![],
11841189
max_iterations: 2,
11851190
timeout_secs: None,
11861191
sandbox_mode: SandboxMode::None,

0 commit comments

Comments
 (0)