Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2d58123
feat(artifacts): add create/finalize/fail helpers + error field (#2778)
oxoxDev May 29, 2026
82e8fa9
feat(runtime_python): add run + venv helpers for Python-backed tools …
oxoxDev May 29, 2026
026fe48
feat(tools): add generate_presentation tool backed by python-pptx (#2…
oxoxDev May 29, 2026
63310f5
test(presentation): end-to-end integration + CI python-pptx provision…
oxoxDev May 29, 2026
aa1469c
feat(agent/orchestrator): add generate_presentation to named tool lis…
oxoxDev May 29, 2026
471d181
fix(presentation): validate theme length with MAX_TEXT_CHARS cap (#2778)
oxoxDev May 30, 2026
2f51f8c
fix(presentation): redact title from logs + fail artifact on setup er…
oxoxDev May 30, 2026
bec8888
fix(presentation): harden temp script write against symlink/clobber (…
oxoxDev May 30, 2026
4511a74
fix(presentation): enforce absolute --output path at runtime (#2778)
oxoxDev May 30, 2026
d81b59f
fix(runtime_python): bound stdin write+shutdown by the deadline (#2778)
oxoxDev May 30, 2026
17c57b6
fix(runtime_python): redact requirements + bound venv creation (#2778)
oxoxDev May 30, 2026
69c5e5a
test(presentation): pin preflight to one resolved python binary (#2778)
oxoxDev May 30, 2026
f577c9c
test(playwright): force-remove lingering joyride portal in dismiss he…
oxoxDev May 30, 2026
f03565e
fix(runtime_python): drain stdout/stderr concurrently with stdin writ…
oxoxDev May 30, 2026
4a3c1ae
test(playwright): install MutationObserver to scrub re-mounted joyrid…
oxoxDev May 30, 2026
c5866e4
fix(runtime_python): reject pre-release Python versions in selector (…
oxoxDev May 30, 2026
687940f
chore(fmt): apply pre-push auto-fixes (#2778)
oxoxDev May 30, 2026
866c091
feat(presentation): add UnsupportedFileType error variant
oxoxDev May 30, 2026
ad1f720
feat(orchestrator): require grounding before generate_presentation
oxoxDev May 30, 2026
897c5bd
test(orchestrator): pin generate_presentation wiring contract
oxoxDev May 30, 2026
0cc9785
fix(orchestrator): align grounding rule with researcher delegate_name…
oxoxDev May 30, 2026
ca4a1b4
test(presentation): align python probe with runtime_python resolver (…
oxoxDev May 30, 2026
3b6b7bc
Merge remote-tracking branch 'upstream/main' into feat/2780-orchestra…
oxoxDev Jun 1, 2026
e89393c
Merge remote-tracking branch 'upstream/main' into feat/2780-orchestra…
oxoxDev Jun 1, 2026
d642c80
Merge remote-tracking branch 'upstream/main' into pr/3029
senamakel Jun 1, 2026
ce8d232
fix(tools/ops): use root_config.workspace_dir post-#3074 refactor
oxoxDev Jun 2, 2026
a8169bb
test(orchestrator): pin all 4 grounding-rule waiver cases (#2780)
oxoxDev Jun 2, 2026
5218a14
Merge remote-tracking branch 'upstream/main' into feat/2780-orchestra…
oxoxDev Jun 2, 2026
3908bc8
Merge remote-tracking branch 'upstream/main' into feat/2780-orchestra…
oxoxDev Jun 3, 2026
6284d07
docs(orchestrator): drop stale python-pptx + dead memory_tree mode re…
oxoxDev Jun 3, 2026
4707b94
chore: restore version 0.57.12 (drop merge regression)
oxoxDev Jun 3, 2026
491fdad
Merge remote-tracking branch 'upstream/main' into feat/2780-orchestra…
oxoxDev Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/openhuman/agent_registry/agents/orchestrator/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,27 @@ Modes:

Start cheap (`query_source` / `smart_walk` summaries), only drill_down/fetch_leaves when you need verbatim content.

## Presentation generation

`generate_presentation` builds a `.pptx` deck from a structured slide spec via a native Rust engine (`ppt-rs`) running in-process — no Python subprocess, no managed venv. Use it for any "make slides", "build a deck", "draft a presentation", "create a pitch" request.

**Grounding rule (do not skip).** Before calling `generate_presentation` on a topical or factual deck — anything where the slides need real-world facts, current events, statistics, names, dates, quotes, or domain context — you MUST first establish a grounding context. Pick at least one:

- `memory_tree` (`query_source` / `smart_walk`) — when the topic plausibly lives in the user's ingested history (their notes, prior chats, emails on the subject).
- `research` — when the topic needs live web facts (current events, recent stats, comparative product data, anything time-sensitive).
- `query_memory` — when the user has previously summarised the exact topic in this thread or in a saved memory.

Only after the grounding tool returns may you call `generate_presentation`, and the slide bullets / body / speaker_notes you pass MUST be drawn from the grounding output — not invented from priors.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**When to skip grounding.** You may dispatch `generate_presentation` directly when:

- The user pasted source material in the same turn (text, doc summary, bullet list to convert).
- A prior turn in this same thread already established the source material (and you can quote from it).
- The deck is content-free or structural (e.g. "make me a 3-slide blank template titled 'Q3 Review'", "an empty deck with a title slide and two body slides").
- The user explicitly waived grounding ("don't research, just generate from your priors", "I know it'll be approximate").

**Why this rule exists.** Without grounding, the model invents slide bullets and speaker notes from training-data priors. That confabulates statistics, misattributes quotes, and ages out fast. A single `research` or `memory_tree` call up front grounds the deck in verifiable sources and lets the slides cite real material instead of fabricated text.

## Citations

When your answer is informed by retrieved memory, cite it with footnote markers:
Expand Down
68 changes: 68 additions & 0 deletions src/openhuman/agent_registry/agents/orchestrator/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,74 @@ mod tests {
assert!(!body.contains("- **linear**"));
}

#[test]
fn build_requires_grounding_before_generate_presentation() {
// #2780 grounding rule: orchestrator must ground a topical/factual
// deck via `memory_tree` / `research` / `query_memory` before
// dispatching `generate_presentation`. The rule is expressed in
// `prompt.md` and must reach the assembled body verbatim — the
// test pins both the section header and the load-bearing "do
// not skip" instruction so a future prompt refactor can't
// quietly drop the guardrail. The live-web grounding option is
// named `research` (the researcher agent's `delegate_name`
// override, not the default `delegate_researcher` /
// `delegate_research`); it must match the actual tool name the
// orchestrator exposes so the model can dispatch it instead of
// hallucinating a non-tool.
let body = build(&ctx_with(&[])).unwrap();
assert!(
body.contains("## Presentation generation"),
"presentation section header missing from orchestrator prompt"
);
assert!(
body.contains("Grounding rule (do not skip)"),
"grounding rule missing from presentation section"
);
assert!(
body.contains("Before calling `generate_presentation`"),
"grounding pre-condition phrasing missing from presentation section"
);
// Each of the three permitted grounding tools must be named so
// the model knows the menu, not just the constraint.
assert!(body.contains("`memory_tree`"));
assert!(
Comment thread
oxoxDev marked this conversation as resolved.
body.contains("`research`"),
"grounding rule must name the researcher tool by its actual \
delegate_name (`research`), not `delegate_research`"
);
assert!(
!body.contains("`delegate_research`"),
"stale `delegate_research` reference — researcher's \
delegate_name override is `research`"
);
assert!(body.contains("`query_memory`"));
// All four waiver escape hatches must reach the assembled body
// verbatim — without each one the rule blocks a legitimate
// dispatch case. Per graycyrus on #3029: pinning only one waiver
// lets a future prompt edit silently drop or expand the list and
// the safety-critical guardrail decays unnoticed.
// (1) user pasted source material in the same turn.
assert!(
body.contains("user pasted source material"),
"waiver clause for pasted-source decks missing"
);
// (2) prior turn in same thread already established source material.
assert!(
body.contains("prior turn in this same thread"),
"waiver clause for prior-turn established material missing"
);
// (3) structural / content-free deck (blank template, layout-only).
assert!(
body.contains("content-free or structural"),
"waiver clause for structural / content-free decks missing"
);
// (4) explicit user waiver ("don't research, just generate…").
assert!(
body.contains("explicitly waived grounding"),
"waiver clause for explicit user waiver missing"
);
}

#[test]
fn build_omits_guide_when_no_integrations_connected() {
let integrations = vec![ConnectedIntegration {
Expand Down
16 changes: 16 additions & 0 deletions src/openhuman/tools/impl/presentation/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,19 @@ fn truncate_stderr_caps_payload_with_suffix() {
let short = "tiny stderr";
assert_eq!(PresentationError::truncate_stderr(short), short);
}

#[test]
fn unsupported_file_type_display_includes_extension_and_supported() {
// Reserved-for-future variant (#2780): confirms the Display impl
// renders both the rejected extension and the supported set so a
// downstream mapper can surface a user-correctable message verbatim
// once a `format` selector lands.
let err = PresentationError::UnsupportedFileType {
extension: "key".to_string(),
supported: "pptx".to_string(),
};
let rendered = err.to_string();
assert!(rendered.contains("unsupported file type"));
assert!(rendered.contains("'key'"));
assert!(rendered.contains("supported: pptx"));
}
19 changes: 16 additions & 3 deletions src/openhuman/tools/impl/presentation/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ pub enum PresentationError {

#[error("presentation generation exceeded {timeout_secs}s timeout")]
GenerationTimeout { timeout_secs: u64 },

/// Reserved for the planned `format` selector that will let callers
/// request alternative deck formats (`.pdf` / `.key` / image
/// strips). Today the tool only emits `.pptx`, so this variant is
/// not constructed by `execute` — it exists ahead of #2780's
/// follow-up wiring so downstream error-handling sites can pattern-
/// match exhaustively without a churn-y enum bump later.
#[allow(dead_code)]
#[error("unsupported file type '{extension}'; supported: {supported}")]
UnsupportedFileType {
extension: String,
supported: String,
},
}

impl PresentationError {
Expand All @@ -111,9 +124,9 @@ impl PresentationError {
}
}

/// Validate the input early — before spawning Python — so the agent
/// gets a structured `InvalidInput` it can self-correct on instead of
/// a generic Python traceback.
/// Validate the input early — before invoking the `ppt-rs` engine — so
/// the agent gets a structured `InvalidInput` it can self-correct on
/// instead of a generic engine error.
pub(super) fn validate_input(input: &GeneratePresentationInput) -> Result<(), PresentationError> {
if input.title.trim().is_empty() {
return Err(PresentationError::InvalidInput {
Expand Down
58 changes: 58 additions & 0 deletions tests/orchestrator_presentation_wiring.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! Pins the orchestrator's `generate_presentation` wiring contract
//! (#2780 — companion to the tool itself shipped in #2778).
//!
//! Two invariants:
//!
//! 1. The orchestrator's `agent.toml` MUST list `generate_presentation`
//! in `[tools].named`. Without this entry the orchestrator's
//! function-calling schema does not include the tool and the
//! "create slides" routing case from parent epic #1535 silently
//! falls back to refusing the request.
//!
//! 2. The `code_executor` agent MUST NOT list `generate_presentation`.
//! Presentation rendering is not a code-exec task: it runs in-process
//! via the native Rust `ppt-rs` engine (no Python, no subprocess,
//! distinct from code_executor's `node_exec` / shell surface), and
//! exposing the tool to code_executor would create a second,
//! duplicate dispatch path that bypasses the orchestrator's
//! grounding-rule prompt.
//!
//! Exact-line matching (not substring) so commented-out entries or
//! prefixed names (`generate_presentation_v2`, `generate_presentation_legacy`)
//! cannot satisfy the assertion accidentally.

const ORCHESTRATOR_TOML: &str =
include_str!("../src/openhuman/agent_registry/agents/orchestrator/agent.toml");

const CODE_EXECUTOR_TOML: &str =
include_str!("../src/openhuman/agent_registry/agents/code_executor/agent.toml");

const TOOL_NAME: &str = "generate_presentation";

fn lists_named_tool(toml: &str, name: &str) -> bool {
let bare = format!("\"{name}\"");
let trailing = format!("\"{name}\",");
toml.lines()
.map(str::trim)
.any(|line| line == bare || line == trailing)
}

#[test]
fn orchestrator_lists_generate_presentation() {
assert!(
lists_named_tool(ORCHESTRATOR_TOML, TOOL_NAME),
"orchestrator agent.toml must list '{TOOL_NAME}' as a named tool entry — \
removing it silently disables the 'create slides' routing case (#1535)"
);
}

#[test]
fn code_executor_does_not_list_generate_presentation() {
assert!(
!lists_named_tool(CODE_EXECUTOR_TOML, TOOL_NAME),
"code_executor agent.toml must NOT list '{TOOL_NAME}' — pptx rendering \
is not a code-exec task; it runs in-process via the native Rust ppt-rs \
engine and adding it here would bypass the orchestrator grounding-rule \
prompt (#2780)"
);
}
Loading