Skip to main content

nemo_flow_ffi/api/
tool_lifecycle.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use super::{
5    Arc, FfiScopeHandle, FfiToolHandle, NemoFlowFreeFn, NemoFlowStatus, NemoFlowToolExecCb,
6    TASK_SCOPE_STACK, ToolAttributes, ToolExecutionNextFn, c_char, c_str_to_json,
7    c_str_to_opt_json, c_str_to_string, clear_last_error, core_tool_api, current_scope_stack,
8    json_to_c_string, set_last_error, status_from_error, tokio_runtime,
9    unix_micros_to_opt_timestamp, wrap_tool_exec_fn,
10};
11
12// ---------------------------------------------------------------------------
13// Tool lifecycle
14// ---------------------------------------------------------------------------
15
16/// Begin a manual tool call lifecycle span.
17///
18/// This emits a tool Start event after applying sanitize-request guardrails to
19/// the observability payload. Request and execution intercepts only run through
20/// `nemo_flow_tool_call_execute`.
21///
22/// # Parameters
23/// - `name`: Null-terminated tool name.
24/// - `args_json`: Tool arguments as a null-terminated JSON C string. These
25///   arguments become the start-event data after sanitize-request guardrails.
26/// - `parent`: Optional parent scope handle, or null to use the current top of
27///   stack.
28/// - `attributes`: Bitfield of tool attributes.
29/// - `data_json`: Optional null-terminated JSON string stored on the tool
30///   handle, or null.
31/// - `metadata_json`: Optional null-terminated JSON metadata string recorded
32///   on the start event, or null.
33/// - `tool_call_id`: Optional null-terminated external correlation ID recorded
34///   in the tool event category profile, or null.
35/// - `timestamp_unix_micros`: Optional Unix microseconds timestamp for the
36///   handle start time and start event, or null to use the current UTC time.
37/// - `out`: On success, receives a heap-allocated `FfiToolHandle` that must be
38///   freed with `nemo_flow_tool_handle_free`.
39///
40/// # Errors
41/// Returns `InvalidJson` for invalid JSON inputs and `InvalidArg` when
42/// `timestamp_unix_micros` is outside the supported timestamp range.
43///
44/// # Safety
45/// `name` and `args_json` must be valid C strings. `out` must be non-null.
46/// Optional pointer arguments may be null; when non-null, they must be valid
47/// for reads for the duration of the call.
48#[unsafe(no_mangle)]
49pub unsafe extern "C" fn nemo_flow_tool_call(
50    name: *const c_char,
51    args_json: *const c_char,
52    parent: *const FfiScopeHandle,
53    attributes: u32,
54    data_json: *const c_char,
55    metadata_json: *const c_char,
56    tool_call_id: *const c_char,
57    timestamp_unix_micros: *const i64,
58    out: *mut *mut FfiToolHandle,
59) -> NemoFlowStatus {
60    clear_last_error();
61    if out.is_null() {
62        set_last_error("out pointer is null");
63        return NemoFlowStatus::NullPointer;
64    }
65    let name = match c_str_to_string(name) {
66        Ok(s) => s,
67        Err(status) => return status,
68    };
69    let args = match c_str_to_json(args_json) {
70        Some(a) => a,
71        None => return NemoFlowStatus::InvalidJson,
72    };
73    let parent_ref = if parent.is_null() {
74        None
75    } else {
76        Some(&unsafe { &*parent }.0)
77    };
78    let attrs = ToolAttributes::from_bits_truncate(attributes);
79    let data = match c_str_to_opt_json(data_json) {
80        Some(d) => d,
81        None => return NemoFlowStatus::InvalidJson,
82    };
83    let metadata = match c_str_to_opt_json(metadata_json) {
84        Some(m) => m,
85        None => return NemoFlowStatus::InvalidJson,
86    };
87    let tool_call_id_opt = if tool_call_id.is_null() {
88        None
89    } else {
90        match c_str_to_string(tool_call_id) {
91            Ok(s) => Some(s),
92            Err(status) => return status,
93        }
94    };
95    let timestamp = match unix_micros_to_opt_timestamp(timestamp_unix_micros) {
96        Some(v) => v,
97        None => return NemoFlowStatus::InvalidArg,
98    };
99
100    match core_tool_api::tool_call(
101        core_tool_api::ToolCallParams::builder()
102            .name(name.as_str())
103            .args(args)
104            .parent_opt(parent_ref)
105            .attributes(attrs)
106            .data_opt(data)
107            .metadata_opt(metadata)
108            .tool_call_id_opt(tool_call_id_opt)
109            .timestamp_opt(timestamp)
110            .build(),
111    ) {
112        Ok(h) => {
113            unsafe { *out = Box::into_raw(Box::new(FfiToolHandle(h))) };
114            NemoFlowStatus::Ok
115        }
116        Err(e) => status_from_error(&e),
117    }
118}
119
120/// End a manual tool call lifecycle span.
121///
122/// This emits a tool End event after applying sanitize-response guardrails to
123/// the observability payload. Response intercepts only run through
124/// `nemo_flow_tool_call_execute`.
125///
126/// # Parameters
127/// - `handle`: The tool handle from `nemo_flow_tool_call`.
128/// - `result_json`: Tool result as a null-terminated JSON C string. This
129///   result becomes the end-event data after sanitize-response guardrails unless
130///   it sanitizes to JSON null.
131/// - `data_json`: Optional null-terminated JSON data used when the sanitized
132///   result is JSON null, or null.
133/// - `metadata_json`: Optional null-terminated JSON metadata recorded on the
134///   end event, or null.
135/// - `timestamp_unix_micros`: Optional Unix microseconds timestamp for the end
136///   event, or null to use the runtime default end timestamp.
137///
138/// # Errors
139/// Returns `InvalidJson` for invalid JSON inputs and `InvalidArg` when
140/// `timestamp_unix_micros` is outside the supported timestamp range.
141///
142/// # Safety
143/// `handle` and `result_json` must be valid, non-null pointers. Optional
144/// pointer arguments may be null; when non-null, they must be valid for reads
145/// for the duration of the call.
146#[unsafe(no_mangle)]
147pub unsafe extern "C" fn nemo_flow_tool_call_end(
148    handle: *const FfiToolHandle,
149    result_json: *const c_char,
150    data_json: *const c_char,
151    metadata_json: *const c_char,
152    timestamp_unix_micros: *const i64,
153) -> NemoFlowStatus {
154    clear_last_error();
155    if handle.is_null() {
156        set_last_error("handle is null");
157        return NemoFlowStatus::NullPointer;
158    }
159    let result = match c_str_to_json(result_json) {
160        Some(r) => r,
161        None => return NemoFlowStatus::InvalidJson,
162    };
163    let data = match c_str_to_opt_json(data_json) {
164        Some(d) => d,
165        None => return NemoFlowStatus::InvalidJson,
166    };
167    let metadata = match c_str_to_opt_json(metadata_json) {
168        Some(m) => m,
169        None => return NemoFlowStatus::InvalidJson,
170    };
171    let timestamp = match unix_micros_to_opt_timestamp(timestamp_unix_micros) {
172        Some(v) => v,
173        None => return NemoFlowStatus::InvalidArg,
174    };
175
176    match core_tool_api::tool_call_end(
177        core_tool_api::ToolCallEndParams::builder()
178            .handle(&unsafe { &*handle }.0)
179            .result(result)
180            .data_opt(data)
181            .metadata_opt(metadata)
182            .timestamp_opt(timestamp)
183            .build(),
184    ) {
185        Ok(()) => NemoFlowStatus::Ok,
186        Err(e) => status_from_error(&e),
187    }
188}
189
190/// Execute a tool call end-to-end: run conditional-execution guardrails (on raw
191/// args), then request intercepts, sanitize-request guardrails, execution
192/// intercepts, the callback, and sanitize-response
193/// guardrails. On rejection, only a standalone Mark event is emitted (no
194/// Start/End pair) and `GuardrailRejected` is returned. Blocks the calling
195/// thread until completion.
196///
197/// # Parameters
198/// - `name`: Null-terminated tool name.
199/// - `args_json`: Tool arguments as a JSON C string.
200/// - `func`: C callback that performs the actual tool execution.
201/// - `func_user_data`: Opaque pointer passed to `func`.
202/// - `func_free`: Optional destructor for `func_user_data`.
203/// - `parent`: Optional parent scope handle, or null.
204/// - `attributes`: Bitfield of tool attributes.
205/// - `data_json`: Optional JSON data, or null.
206/// - `metadata_json`: Optional JSON metadata, or null.
207/// - `out`: On success, receives the result as a JSON C string. Caller must free
208///   with `nemo_flow_string_free`.
209///
210/// # Safety
211/// `name`, `args_json`, and `out` must be valid, non-null pointers.
212#[unsafe(no_mangle)]
213pub unsafe extern "C" fn nemo_flow_tool_call_execute(
214    name: *const c_char,
215    args_json: *const c_char,
216    func: NemoFlowToolExecCb,
217    func_user_data: *mut libc::c_void,
218    func_free: NemoFlowFreeFn,
219    parent: *const FfiScopeHandle,
220    attributes: u32,
221    data_json: *const c_char,
222    metadata_json: *const c_char,
223    out: *mut *mut c_char,
224) -> NemoFlowStatus {
225    clear_last_error();
226    if out.is_null() {
227        set_last_error("out pointer is null");
228        return NemoFlowStatus::NullPointer;
229    }
230    let name = match c_str_to_string(name) {
231        Ok(s) => s,
232        Err(status) => return status,
233    };
234    let args = match c_str_to_json(args_json) {
235        Some(a) => a,
236        None => return NemoFlowStatus::InvalidJson,
237    };
238    let parent_handle = if parent.is_null() {
239        None
240    } else {
241        Some(unsafe { &*parent }.0.clone())
242    };
243    let attrs = ToolAttributes::from_bits_truncate(attributes);
244    let data = match c_str_to_opt_json(data_json) {
245        Some(d) => d,
246        None => return NemoFlowStatus::InvalidJson,
247    };
248    let metadata = match c_str_to_opt_json(metadata_json) {
249        Some(m) => m,
250        None => return NemoFlowStatus::InvalidJson,
251    };
252
253    let exec_fn = wrap_tool_exec_fn(func, func_user_data, func_free);
254    let default_fn: ToolExecutionNextFn = Arc::new(move |args| exec_fn(args));
255
256    let scope_stack = current_scope_stack();
257    let result = tokio_runtime().block_on(TASK_SCOPE_STACK.scope(scope_stack, async {
258        core_tool_api::tool_call_execute(
259            core_tool_api::ToolCallExecuteParams::builder()
260                .name(name)
261                .args(args)
262                .func(default_fn)
263                .parent_opt(parent_handle)
264                .attributes(attrs)
265                .data_opt(data)
266                .metadata_opt(metadata)
267                .build(),
268        )
269        .await
270    }));
271
272    match result {
273        Ok(json) => {
274            unsafe { *out = json_to_c_string(&json) };
275            NemoFlowStatus::Ok
276        }
277        Err(e) => status_from_error(&e),
278    }
279}