Skip to content
This repository was archived by the owner on Sep 2, 2026. It is now read-only.

Commit de990b4

Browse files
committed
2 parents b2c937b + 20e9a8b commit de990b4

6 files changed

Lines changed: 111 additions & 42 deletions

File tree

README.md

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,10 @@
11
# openai-agents-rust
22

3-
![CI](https://github.com/scalarian/openai-agents-rust/actions/workflows/ci.yml/badge.svg)
4-
[![License](https://img.shields.io/badge/license-Apache--2.0-4B5563.svg)](LICENSE)
3+
Rust-native agents runtime with a single ergonomic facade, async-first execution, OpenAI integrations, MCP, realtime sessions, voice workflows, and extension hooks.
54

6-
Rust-native agents runtime for OpenAI-style agent systems: typed agents, tools, sessions, MCP, realtime, voice, and extensions.
5+
![Runtime overview](docs/assets/runtime-overview.svg)
76

8-
This repository is for teams that want native Rust building blocks for agent workflows without wrapping another SDK and without giving up typed runtime control.
9-
10-
## Why This Project
11-
12-
- async-first runtime with a small facade and lower-level primitives
13-
- one-shot, session-backed, and streamed execution paths
14-
- OpenAI Responses and Chat Completions integrations
15-
- MCP tools and resources
16-
- realtime sessions and voice workflows
17-
- extensions for transports, adapters, and optional backends
18-
19-
## Quick Start
20-
21-
```toml
22-
[dependencies]
23-
openai-agents = { package = "openai-agents-rs", version = "0.1.2" }
24-
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
25-
```
26-
27-
```rust
28-
use openai_agents::{run, Agent};
29-
30-
#[tokio::main]
31-
async fn main() -> Result<(), openai_agents::AgentsError> {
32-
let agent = Agent::builder("assistant")
33-
.instructions("Be concise, practical, and structured.")
34-
.build();
35-
36-
let result = run(&agent, "Give me three production readiness checks.").await?;
37-
println!("{}", result.final_output.unwrap_or_default());
38-
Ok(())
39-
}
40-
```
7+
This repository is for teams that want to build agent systems in Rust without wrapping another SDK and without giving up typed runtime building blocks.
418

429
## Start Here
4310

@@ -151,7 +118,4 @@ The project is pre-1.0. The runtime is already broad, but APIs may still tighten
151118

152119
## License
153120

154-
Apache-2.0. See [LICENSE](LICENSE).
155-
156-
> [!WARNING]
157-
> This project is not an official OpenAI product. It is not affiliated with, endorsed by, or maintained by OpenAI.
121+
MIT. See [LICENSE](LICENSE).

crates/agents-core/src/agent.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ impl Agent {
514514
}
515515
let resolved_max_turns = nested_run_config.max_turns;
516516
let mut nested_context = tool_context.run_context.clone();
517+
nested_context.approvals.clear();
517518
if should_capture_tool_input {
518519
nested_context.tool_input = Some(params_value.clone());
519520
} else {
@@ -757,7 +758,7 @@ mod tests {
757758
use crate::agent_tool_state::{
758759
drop_agent_tool_run_result, peek_agent_tool_run_result, set_agent_tool_state_scope,
759760
};
760-
use crate::run_context::RunContext;
761+
use crate::run_context::{ApprovalRecord, RunContext};
761762
use crate::tool::Tool;
762763
use crate::tool::function_tool;
763764

@@ -829,6 +830,13 @@ mod tests {
829830
.expect("agent tool should build");
830831
let mut run_context = RunContextWrapper::new(RunContext::default());
831832
set_agent_tool_state_scope(&mut run_context, Some("scope-a".to_owned()));
833+
run_context.approvals.insert(
834+
"call-123".to_owned(),
835+
ApprovalRecord {
836+
approved: true,
837+
reason: Some("approved in parent".to_owned()),
838+
},
839+
);
832840
run_context.tool_input = Some(json!({"stale": true}));
833841

834842
let output = tool
@@ -853,6 +861,7 @@ mod tests {
853861
stored.context_snapshot.agent_tool_state_scope.as_deref(),
854862
Some("scope-a")
855863
);
864+
assert!(stored.context_snapshot.approvals.is_empty());
856865
assert!(stored.context_snapshot.tool_input.is_none());
857866

858867
drop_agent_tool_run_result("call-123", Some("scope-a".to_owned()));

crates/agents-core/src/internal/tool_execution.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,30 @@ pub(crate) async fn execute_local_function_tools(
108108
});
109109
break;
110110
}
111+
Some(approval)
112+
if approval.tool_name.as_deref() != Some(tool_call.name.as_str()) =>
113+
{
114+
provider.finish_span(&mut span, true);
115+
if let Some(recorder) = stream_recorder {
116+
recorder
117+
.push_lifecycle(
118+
"tool_approval_required",
119+
Some(serde_json::json!({
120+
"tool_name": tool_call.name.clone(),
121+
"call_id": tool_call.id.clone(),
122+
"namespace": tool_call.namespace.clone(),
123+
})),
124+
)
125+
.await;
126+
}
127+
interruptions.push(RunInterruption {
128+
kind: Some(RunInterruptionKind::ToolApproval),
129+
call_id: Some(tool_call.id.clone()),
130+
tool_name: Some(tool_call.name.clone()),
131+
reason: Some("tool approval required".to_owned()),
132+
});
133+
break;
134+
}
111135
Some(approval) if !approval.approved => {
112136
append_approval_error_output(
113137
&mut new_items,

crates/agents-core/src/run.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,15 @@ impl Runner {
10801080
let approval = state.approval(&call_id).cloned().ok_or_else(|| UserError {
10811081
message: format!("approval decision for `{call_id}` is missing"),
10821082
})?;
1083+
if approval.tool_name.as_deref() != Some(tool_call.name.as_str()) {
1084+
return Err(UserError {
1085+
message: format!(
1086+
"approval decision for `{call_id}` is not bound to tool `{}`",
1087+
tool_call.name
1088+
),
1089+
}
1090+
.into());
1091+
}
10831092

10841093
let context = state.restore_context::<crate::run_context::RunContext>()?;
10851094
let tool_outcome = internal_tool_execution::execute_local_function_tools(
@@ -2638,7 +2647,11 @@ mod tests {
26382647
.durable_state()
26392648
.cloned()
26402649
.expect("state should exist");
2641-
state.approve("call-1", Some("approved".to_owned()));
2650+
state.approve_for_tool(
2651+
"call-1",
2652+
Some("search".to_owned()),
2653+
Some("approved".to_owned()),
2654+
);
26422655

26432656
let resumed = Runner::new()
26442657
.with_model_provider(provider)
@@ -2659,6 +2672,44 @@ mod tests {
26592672
}));
26602673
}
26612674

2675+
#[tokio::test]
2676+
async fn runner_rejects_unbound_tool_approval_on_resume() {
2677+
let provider = Arc::new(FakeProvider {
2678+
model: Arc::new(FakeModel::default()),
2679+
});
2680+
let search_tool = function_tool(
2681+
"search",
2682+
"Search documents",
2683+
|_ctx, args: SearchArgs| async move {
2684+
Ok::<_, AgentsError>(format!("result:{}", args.query))
2685+
},
2686+
)
2687+
.expect("function tool should build")
2688+
.with_needs_approval(true);
2689+
let agent = Agent::builder("assistant")
2690+
.function_tool(search_tool)
2691+
.build();
2692+
2693+
let initial = Runner::new()
2694+
.with_model_provider(provider.clone())
2695+
.run(&agent, "hello")
2696+
.await
2697+
.expect("initial run should succeed");
2698+
2699+
let mut state = initial
2700+
.durable_state()
2701+
.cloned()
2702+
.expect("state should exist");
2703+
state.approve("call-1", Some("approved".to_owned()));
2704+
2705+
let resumed = Runner::new()
2706+
.with_model_provider(provider)
2707+
.resume_with_agent(&state, &agent)
2708+
.await;
2709+
2710+
assert!(matches!(resumed, Err(AgentsError::User(_))));
2711+
}
2712+
26622713
#[tokio::test]
26632714
async fn runner_follows_runtime_handoffs() {
26642715
let provider = Arc::new(FakeHandoffProvider {

crates/agents-core/src/run_context.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub struct RunContext {
1919
pub struct ApprovalRecord {
2020
pub approved: bool,
2121
pub reason: Option<String>,
22+
pub tool_name: Option<String>,
2223
}
2324

2425
/// Runtime context wrapper shared across callbacks.

crates/agents-core/src/run_state.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,21 +210,41 @@ impl RunState {
210210
}
211211

212212
pub fn approve(&mut self, id: impl Into<String>, reason: Option<String>) {
213+
self.approve_for_tool(id, None, reason);
214+
}
215+
216+
pub fn approve_for_tool(
217+
&mut self,
218+
id: impl Into<String>,
219+
tool_name: Option<String>,
220+
reason: Option<String>,
221+
) {
213222
self.context_snapshot.approvals.insert(
214223
id.into(),
215224
ApprovalRecord {
216225
approved: true,
217226
reason,
227+
tool_name,
218228
},
219229
);
220230
}
221231

222232
pub fn reject(&mut self, id: impl Into<String>, reason: Option<String>) {
233+
self.reject_for_tool(id, None, reason);
234+
}
235+
236+
pub fn reject_for_tool(
237+
&mut self,
238+
id: impl Into<String>,
239+
tool_name: Option<String>,
240+
reason: Option<String>,
241+
) {
223242
self.context_snapshot.approvals.insert(
224243
id.into(),
225244
ApprovalRecord {
226245
approved: false,
227246
reason,
247+
tool_name,
228248
},
229249
);
230250
}

0 commit comments

Comments
 (0)