Skip to content

Commit b59944a

Browse files
committed
feat: add asynchronous tool registration and examples
Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent 511ec70 commit b59944a

6 files changed

Lines changed: 192 additions & 10 deletions

File tree

examples/agent-framework/DotnetAgent.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,18 @@
5454
_ => throw new ArgumentException($"Unknown operation: {args.Operation}"),
5555
});
5656

57-
codeTool.RegisterTool<FetchDataArgs, string>("fetch_data",
58-
args => args.Source switch
57+
// Async tool — simulates fetching from an external service.
58+
codeTool.RegisterToolAsync<FetchDataArgs, string>("fetch_data",
59+
async args =>
5960
{
60-
"weather" => """{"temperature": 22, "condition": "sunny"}""",
61-
"stock" => """{"symbol": "MSFT", "price": 425.50}""",
62-
_ => """{"error": "unknown source"}""",
61+
// In real system this would be an actual HTTP/DB call.
62+
await Task.Delay(1).ConfigureAwait(false);
63+
return args.Source switch
64+
{
65+
"weather" => """{"temperature": 22, "condition": "sunny"}""",
66+
"stock" => """{"symbol": "MSFT", "price": 425.50}""",
67+
_ => """{"error": "unknown source"}""",
68+
};
6369
});
6470

6571
// --- Create IChatClient with function invocation ---

examples/copilot-sdk/DotnetCopilotSdk.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,18 @@
4343
_ => throw new ArgumentException($"Unknown op: {args.Operation}"),
4444
});
4545

46-
codeTool.RegisterTool<FetchDataArgs, string>("fetch_data",
47-
args => args.Source switch
46+
// Async tool — simulates fetching from an external service.
47+
codeTool.RegisterToolAsync<FetchDataArgs, string>("fetch_data",
48+
async args =>
4849
{
49-
"weather" => """{"temperature": 22, "condition": "sunny"}""",
50-
"stock" => """{"symbol": "MSFT", "price": 425.50}""",
51-
_ => """{"error": "unknown source"}""",
50+
// In real system this would be an actual HTTP/DB call.
51+
await Task.Delay(1).ConfigureAwait(false);
52+
return args.Source switch
53+
{
54+
"weather" => """{"temperature": 22, "condition": "sunny"}""",
55+
"stock" => """{"symbol": "MSFT", "price": 425.50}""",
56+
_ => """{"error": "unknown source"}""",
57+
};
5258
});
5359

5460
codeTool.AllowDomain("https://httpbin.org", ["GET"]);

src/sdk/dotnet/core/Api/Sandbox.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,35 @@ public void RegisterTool<TArgs, TResult>(string name, Func<TArgs, TResult> handl
191191
} // lock
192192
}
193193

194+
/// <summary>
195+
/// Registers a typed tool whose handler is asynchronous.
196+
/// </summary>
197+
/// <typeparam name="TArgs">
198+
/// The argument type. Public properties define the tool's parameter schema.
199+
/// </typeparam>
200+
/// <typeparam name="TResult">
201+
/// The return type. Serialized to JSON for the guest.
202+
/// </typeparam>
203+
/// <param name="name">Tool name (must be unique).</param>
204+
/// <param name="handler">
205+
/// Async function invoked when the guest calls this tool. Receives
206+
/// deserialized arguments. The return value is serialized to JSON for
207+
/// the guest.
208+
/// </param>
209+
/// <remarks>
210+
/// <para>
211+
/// The underlying FFI callback is synchronous — the async handler is
212+
/// blocked on at the interop boundary via <c>GetAwaiter().GetResult()</c>.
213+
/// This is safe because FFI callbacks run on threads without a
214+
/// <see cref="System.Threading.SynchronizationContext"/>.
215+
/// </para>
216+
/// </remarks>
217+
public void RegisterToolAsync<TArgs, TResult>(string name, Func<TArgs, Task<TResult>> handler)
218+
{
219+
// Wrap the async handler into a sync handler that blocks at the FFI boundary.
220+
RegisterTool<TArgs, TResult>(name, args => handler(args).GetAwaiter().GetResult());
221+
}
222+
194223
/// <summary>
195224
/// Registers a tool with raw JSON input/output.
196225
/// </summary>
@@ -236,6 +265,28 @@ public void RegisterTool(string name, Func<string, string> handler)
236265
} // lock
237266
}
238267

268+
/// <summary>
269+
/// Registers a raw JSON tool whose handler is asynchronous.
270+
/// </summary>
271+
/// <param name="name">Tool name.</param>
272+
/// <param name="handler">
273+
/// Async function receiving a JSON string and returning a JSON string.
274+
/// Return <c>{"error": "message"}</c> to signal an error to the guest.
275+
/// </param>
276+
/// <remarks>
277+
/// <para>
278+
/// The underlying FFI callback is synchronous — the async handler is
279+
/// blocked on at the interop boundary via <c>GetAwaiter().GetResult()</c>.
280+
/// This is safe because FFI callbacks run on threads without a
281+
/// <see cref="System.Threading.SynchronizationContext"/>.
282+
/// </para>
283+
/// </remarks>
284+
public void RegisterToolAsync(string name, Func<string, Task<string>> handler)
285+
{
286+
// Wrap the async handler into a sync handler that blocks at the FFI boundary.
287+
RegisterTool(name, (string json) => handler(json).GetAwaiter().GetResult());
288+
}
289+
239290
// -----------------------------------------------------------------------
240291
// Code execution
241292
// -----------------------------------------------------------------------

src/sdk/dotnet/core/Examples/ToolRegistrationExample/Program.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
return """{"result": "not found"}""";
3131
});
3232

33+
// --- Register an async typed tool (e.g. simulating an external API call) ---
34+
sandbox.RegisterToolAsync<MathArgs, double>("add_async", async args =>
35+
{
36+
await Task.Delay(10).ConfigureAwait(false); // Simulate I/O latency
37+
return args.A + args.B;
38+
});
39+
3340
// --- Test 1: Typed tool dispatch ---
3441
Console.WriteLine("═══ Test 1: Typed tool dispatch ═══");
3542
var result = sandbox.Run("""
@@ -57,6 +64,16 @@
5764

5865
Console.WriteLine($"stdout:\n{result.Stdout}");
5966

67+
// --- Test 3: Async tool dispatch ---
68+
Console.WriteLine("═══ Test 3: Async tool dispatch ═══");
69+
70+
result = sandbox.Run("""
71+
async_sum = call_tool("add_async", a=100, b=200)
72+
print(f"Async 100 + 200 = {async_sum}")
73+
""");
74+
75+
Console.WriteLine($"stdout:\n{result.Stdout}");
76+
6077
Console.WriteLine("✅ Tool registration example finished successfully!");
6178
return 0;
6279

src/sdk/dotnet/core/Extensions.AI/CodeExecutionTool.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,31 @@ public void RegisterTool(string name, Func<string, string> handler)
8181
}
8282
}
8383

84+
/// <summary>
85+
/// Registers a typed tool whose handler is asynchronous.
86+
/// Must be called before the first <see cref="Execute"/>.
87+
/// </summary>
88+
public void RegisterToolAsync<TArgs, TResult>(string name, Func<TArgs, Task<TResult>> handler)
89+
{
90+
lock (_gate)
91+
{
92+
ObjectDisposedException.ThrowIf(_disposed, this);
93+
_sandbox.RegisterToolAsync(name, handler);
94+
}
95+
}
96+
97+
/// <summary>
98+
/// Registers a raw JSON tool whose handler is asynchronous.
99+
/// </summary>
100+
public void RegisterToolAsync(string name, Func<string, Task<string>> handler)
101+
{
102+
lock (_gate)
103+
{
104+
ObjectDisposedException.ThrowIf(_disposed, this);
105+
_sandbox.RegisterToolAsync(name, handler);
106+
}
107+
}
108+
84109
/// <summary>
85110
/// Adds a domain to the network allowlist.
86111
/// </summary>

src/sdk/dotnet/core/Tests/HyperlightSandbox.Tests/IntegrationTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,83 @@ public async Task Integration_RunAsync_WorksFromDifferentThread()
280280
Assert.Contains("async hello", result.Stdout, StringComparison.Ordinal);
281281
}
282282

283+
// -----------------------------------------------------------------------
284+
// Async tool dispatch
285+
// -----------------------------------------------------------------------
286+
287+
[Fact]
288+
public void Integration_ToolDispatch_AsyncTypedTool_Works()
289+
{
290+
using var sandbox = TryCreateSandbox();
291+
if (sandbox == null) return;
292+
293+
// Register a tool with an async handler (e.g. simulating a DB/HTTP call).
294+
sandbox.RegisterToolAsync<AddArgs, double>("add_async", async args =>
295+
{
296+
await Task.Delay(10).ConfigureAwait(false); // Simulate I/O
297+
return args.a + args.b;
298+
});
299+
300+
var result = sandbox.Run("""
301+
result = call_tool("add_async", a=50, b=25)
302+
print(f"result={result}")
303+
""");
304+
305+
Assert.True(result.Success);
306+
Assert.Contains("result=75", result.Stdout, StringComparison.Ordinal);
307+
}
308+
309+
[Fact]
310+
public void Integration_ToolDispatch_AsyncRawJsonTool_Works()
311+
{
312+
using var sandbox = TryCreateSandbox();
313+
if (sandbox == null) return;
314+
315+
// Register a raw JSON tool with an async handler.
316+
sandbox.RegisterToolAsync("fetch_async", async (string json) =>
317+
{
318+
await Task.Delay(10).ConfigureAwait(false); // Simulate I/O
319+
return json.Contains("weather", StringComparison.Ordinal)
320+
? """{"data": "sunny"}"""
321+
: """{"data": "unknown"}""";
322+
});
323+
324+
var result = sandbox.Run("""
325+
r = call_tool("fetch_async", key="weather")
326+
print(r)
327+
""");
328+
329+
Assert.True(result.Success);
330+
Assert.Contains("sunny", result.Stdout, StringComparison.Ordinal);
331+
}
332+
333+
[Fact]
334+
public void Integration_ToolDispatch_MixedSyncAndAsyncTools_Works()
335+
{
336+
using var sandbox = TryCreateSandbox();
337+
if (sandbox == null) return;
338+
339+
// Sync tool.
340+
sandbox.RegisterTool<AddArgs, double>("add", args => args.a + args.b);
341+
342+
// Async tool.
343+
sandbox.RegisterToolAsync<AddArgs, double>("multiply_async", async args =>
344+
{
345+
await Task.Delay(10).ConfigureAwait(false);
346+
return args.a * args.b;
347+
});
348+
349+
var result = sandbox.Run("""
350+
s = call_tool("add", a=3, b=4)
351+
p = call_tool("multiply_async", a=6, b=7)
352+
print(f"{s} {p}")
353+
""");
354+
355+
Assert.True(result.Success);
356+
Assert.Contains("7", result.Stdout, StringComparison.Ordinal);
357+
Assert.Contains("42", result.Stdout, StringComparison.Ordinal);
358+
}
359+
283360
// -----------------------------------------------------------------------
284361
// Helper types
285362
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)