iTranslated by AI
Executing JavaScript using CodeAct in Microsoft Agent Framework
Introduction
By using CodeAct (Hyperlight) from the Microsoft Agent Framework, you can incorporate sandboxed code execution capabilities into AI agents.
I tested Microsoft.Agents.AI.Hyperlight in .NET with the following objectives:
- To utilize Hyperlight from a local LLM environment.
- To verify that the model can issue JavaScript code, receive the execution results, and return a final answer.
- To inspect the requests, responses, and tool execution results.
About CodeAct
CodeAct is an approach where agents write code, execute it via Tool Calling, and use the execution results to advance tasks. Instead of the model simply "choosing one tool at a time," it is characterized by aggregating control flow, data transformation, and tool orchestration into a short snippet of code, treating it as a single execution.
According to Microsoft Learn, CodeAct is particularly suitable for cases where "connecting many small tool calls increases model turns, latency, and token consumption," but I also find it well-suited for computational tasks. This sample also solves a simple calculation task via code execution.
About Hyperlight
Microsoft.Agents.AI.Hyperlight is a package designed to treat CodeAct as a feature within the Microsoft Agent Framework. Rather than simply adding a tool to run code, it serves as a thin integration layer for incorporating secure code execution into agents, based on Hyperlight's VM-isolated sandbox.
Its main roles are as follows:
- Hyperlight: A lightweight VM-based isolated sandbox. It provides the foundation for executing guest code separated from the host.
- Microsoft.Agents.AI.Hyperlight: A .NET integration package that makes the sandbox easy to use from the Microsoft Agent Framework.
The relationship is illustrated in the diagram below.
This package primarily offers two entry points:
-
HyperlightCodeActProvider: A method to register as anAIContextProviderfor an agent. It inserts theexecute_codetool and guidance for CodeAct into each call. -
HyperlightExecuteCodeFunction: A method to use as anAIFunctionon its own. It is suitable when you want manual wiring with a fixed sandbox configuration.
In this verification, I used HyperlightCodeActProvider.
This package allows for more than just simple JavaScript execution. The documentation explains that it provides the following capabilities:
- Calling provider-owned tools as
call_tool(...)from within the sandbox. - Adding file mounts only when necessary.
- Setting permission for outbound communication only when necessary.
- Switching approval policies via
CodeActApprovalMode. - Performing snapshot/restore per execution to start the guest from a clean state every time.
It also provides the following approval features:
-
NeverRequire: The default value. Usually proceeds without approval, but approvals for tools wrapped inApprovalRequiredAIFunctionpropagate. -
AlwaysRequire: Always requires approval.
I used NeverRequire for this verification to prioritize functionality, but AlwaysRequire should be considered for production environments.
Operational Workflow
Verification Environment
- Framework: net10.0
- Packages used:
- Microsoft.Agents.AI 1.6.1
- Microsoft.Agents.AI.Hyperlight 1.6.1-preview.260514.1
- Microsoft.Agents.AI.OpenAI 1.6.1
- Endpoint: LM Studio
- Model: openai/gpt-oss-20b (Context length 14000 in this LM Studio configuration)
Sample Overview
Key Code
Registering Hyperlight with an AI Agent
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hyperlight;
using Microsoft.Extensions.AI;
// Create HyperlightCodeActProviderOptions for JavaScript
var options = HyperlightCodeActProviderOptions.CreateForJavaScript();
// Set configuration to run without approval (used here for verification priority. Evaluate for production)
options.ApprovalMode = CodeActApprovalMode.NeverRequire;
// Create HyperlightCodeActProvider and register it as an AIContextProvider for the agent
using var hyperlightProvider = new HyperlightCodeActProvider(options);
AIAgent agent = loggingChatClient.AsAIAgent(
new ChatClientAgentOptions
{
Name = "HyperlightSampleAgent",
ChatOptions = new ChatOptions
{
Instructions = "You are a helpful coding assistant. When code execution is useful, use the Hyperlight CodeAct capability.",
},
AIContextProviders = [hyperlightProvider] // Register HyperlightCodeActProvider as AIContextProvider
});
The key here is CreateForJavaScript(). This allows Hyperlight to act as a CodeAct provider for JavaScript execution.
I have set the approval to NeverRequire for verification purposes.
Execution Results
In this section, I will verify the basic operation of CodeAct using a simple calculation scenario.
Calculation Scenario: Sum from 1 to 10
Input:
Use JavaScript code execution to calculate the sum from 1 to 10, and return only the result.
Expected flow:
- The initial response returns
finishReason: "tool_calls". -
execute_codeis invoked. - The tool execution result returns
stdout: "55". - After the re-query, the final response is
55.
Execution Log Trace
Below is the trace actually observed in each step.
(Showing only the contents of contents.)
Step 1. Initial Instruction
In the first ChatLog Request, the user instruction is included in the message with role: user and passed to the model.
[
{
"$type": "text",
"text": "Use JavaScript code execution to calculate the sum from 1 to 10, and return only the result."
}
]
Step 2. Code generated during this execution
In the first ChatLog Response, the model does not answer immediately but returns a function call for execute_code. Here, the agent receives finishReason: "tool_calls" and proceeds to sandbox execution. The following is an example of the code observed in this execution.
[
{
"$type": "functionCall",
"name": "execute_code",
"arguments": {
"code": "const sum = Array.from({length:10}, (_,i)=>i+1).reduce((a,b)=>a+b);\nconsole.log(sum);"
}
}
]
Step 3. Code execution result
When the Hyperlight sandbox executes the code, the result returns as a functionResult in the message with role: tool. This value is passed to the next invocation as a tool message.
[
{
"$type": "functionResult",
"result": {
"stdout": "55\n",
"stderr": "",
"exit_code": 0,
"success": true
}
}
]
Step 4. Final Answer
In the second ChatLog Response after receiving the tool execution result, the model returns the final text response. This time, it finished by returning only 55 with finishReason: "stop".
[
{
"$type": "text",
"text": "55"
}
]
Conclusion
By using Microsoft.Agents.AI.Hyperlight, I was able to easily build an AI agent with JavaScript execution capabilities. Furthermore, upon execution, I confirmed how the model and the sandbox work together, from the generation and execution of JavaScript code to the reception of the results.
Since context consumption is non-negligible, I think it is practical to use a multi-agent architecture in production, delegating only the parts that require code execution to the Hyperlight agent.
Source
Sample Code
using Microsoft.Agents.AI.Hyperlight;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;
using System.Text;
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Json.Nodes;
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
string baseUrl = Environment.GetEnvironmentVariable("OPENAI_BASE_URL") ?? "http://localhost:1234/v1";
string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? "sk-dummy";
string model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "openai/gpt-oss-20b";
OpenAIClient openAIClient = new(
new ApiKeyCredential(apiKey),
new OpenAIClientOptions
{
Endpoint = new Uri(baseUrl)
});
IChatClient baseChatClient = openAIClient
.GetChatClient(model)
.AsIChatClient();
var loggingChatClient = new ConsoleLoggingChatClient(baseChatClient);
var options = HyperlightCodeActProviderOptions.CreateForJavaScript();
options.ApprovalMode = CodeActApprovalMode.NeverRequire;
using var hyperlightProvider = new HyperlightCodeActProvider(options);
AIAgent agent = loggingChatClient.AsAIAgent(
new ChatClientAgentOptions
{
Name = "HyperlightSampleAgent",
ChatOptions = new ChatOptions
{
Instructions = "You are a helpful coding assistant. When code execution is useful, use the Hyperlight CodeAct capability.",
},
AIContextProviders = [hyperlightProvider]
});
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine("Microsoft.Agents.AI.Hyperlight agent sample");
Console.WriteLine($"Endpoint: {baseUrl}");
Console.WriteLine($"Model: {model}");
Console.WriteLine();
Console.Write("Prompt> ");
string prompt = Console.ReadLine() ?? "Calculate the sum from 1 to 10 in JavaScript and return only the result briefly.";
Console.WriteLine();
Console.WriteLine("[Execution] Starting agent run...");
var response = await agent.RunAsync(prompt, session);
Console.WriteLine();
Console.WriteLine("Response:");
Console.WriteLine(response);
sealed class ConsoleLoggingChatClient : DelegatingChatClient
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
public ConsoleLoggingChatClient(IChatClient innerClient)
: base(innerClient)
{
}
public override async Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
WritePayload("Request", messages);
var response = await base.GetResponseAsync(messages, options, cancellationToken);
WritePayload("Response", response);
return response;
}
private static void WritePayload(string title, object payload)
{
Console.WriteLine();
Console.WriteLine($"[ChatLog] {title}");
try
{
var normalizedPayload = NormalizePayload(payload);
WriteTraceSummary(normalizedPayload);
Console.WriteLine(normalizedPayload.ToJsonString(SerializerOptions));
}
catch (Exception exception)
{
Console.WriteLine($"<serialization failed: {exception.Message}>");
Console.WriteLine(payload);
}
}
private static void WriteTraceSummary(JsonNode payload)
{
var summaries = new List<string>();
foreach (var message in GetRelevantMessages(payload))
{
AddMessageSummaries(message, summaries);
}
if (summaries.Count == 0)
{
return;
}
foreach (var summary in summaries)
{
Console.WriteLine(summary);
}
Console.WriteLine();
}
private static IEnumerable<JsonObject> GetRelevantMessages(JsonNode payload)
{
if (payload is JsonArray requestMessages)
{
if (requestMessages.Count == 0 || requestMessages[requestMessages.Count - 1] is not JsonObject lastMessage)
{
yield break;
}
if (requestMessages.Count >= 2 &&
TryGetValue(lastMessage["role"], out string lastRole) &&
string.Equals(lastRole, "tool", StringComparison.OrdinalIgnoreCase) &&
requestMessages[requestMessages.Count - 2] is JsonObject previousMessage &&
TryGetValue(previousMessage["role"], out string previousRole) &&
string.Equals(previousRole, "assistant", StringComparison.OrdinalIgnoreCase))
{
yield return previousMessage;
}
yield return lastMessage;
yield break;
}
if (payload is JsonObject responseObject &&
responseObject.TryGetPropertyValue("messages", out var messagesNode) &&
messagesNode is JsonArray responseMessages)
{
foreach (var messageNode in responseMessages)
{
if (messageNode is JsonObject messageObject)
{
yield return messageObject;
}
}
}
}
private static void AddMessageSummaries(JsonObject message, ICollection<string> summaries)
{
if (!message.TryGetPropertyValue("contents", out var contentsNode) || contentsNode is not JsonArray contents)
{
return;
}
foreach (var contentNode in contents)
{
if (contentNode is not JsonObject contentObject ||
!TryGetValue(contentObject["$type"], out string contentType))
{
continue;
}
switch (contentType)
{
case "functionCall":
summaries.Add(FormatFunctionCall(contentObject));
break;
case "functionResult":
summaries.Add(FormatFunctionResult(contentObject));
break;
}
}
}
private static string FormatFunctionCall(JsonObject contentObject)
{
var callId = TryGetValue(contentObject["callId"], out string parsedCallId) ? parsedCallId : "?";
var name = TryGetValue(contentObject["name"], out string parsedName) ? parsedName : "<unknown>";
var codePreview = "<none>";
if (contentObject.TryGetPropertyValue("arguments", out var argumentsNode) &&
argumentsNode is JsonObject argumentsObject &&
TryGetValue(argumentsObject["code"], out string code))
{
codePreview = SummarizeText(code, 140);
}
return $"[Trace] ToolCall callId={callId} name={name} code={codePreview}";
}
private static string FormatFunctionResult(JsonObject contentObject)
{
var callId = TryGetValue(contentObject["callId"], out string parsedCallId) ? parsedCallId : "?";
if (!contentObject.TryGetPropertyValue("result", out var resultNode) || resultNode is not JsonObject resultObject)
{
return $"[Trace] ToolResult callId={callId} result={SummarizeText(resultNode?.ToJsonString(SerializerOptions), 160)}";
}
var success = TryGetValue(resultObject["success"], out bool parsedSuccess) && parsedSuccess;
var exitCode = TryGetValue(resultObject["exit_code"], out int parsedExitCode) ? parsedExitCode : -1;
var stdout = TryGetValue(resultObject["stdout"], out string parsedStdout) ? parsedStdout : string.Empty;
var stderr = TryGetValue(resultObject["stderr"], out string parsedStderr) ? parsedStderr : string.Empty;
if (!success)
{
return $"[Trace] ToolFailure callId={callId} exit_code={exitCode} stdout={SummarizeText(stdout, 120)} stderr={SummarizeText(stderr, 160)}";
}
if (string.IsNullOrWhiteSpace(stdout) && string.IsNullOrWhiteSpace(stderr))
{
return $"[Trace] ToolNoOutput callId={callId} exit_code={exitCode} stdout=<empty> stderr=<empty>";
}
return $"[Trace] ToolResult callId={callId} exit_code={exitCode} stdout={SummarizeText(stdout, 120)} stderr={SummarizeText(stderr, 120)}";
}
private static JsonNode NormalizePayload(object payload)
{
var node = JsonSerializer.SerializeToNode(payload, SerializerOptions)
?? throw new InvalidOperationException("The payload could not be converted to a JSON node.");
NormalizeNode(node);
return node;
}
private static void NormalizeNode(JsonNode? node)
{
switch (node)
{
case JsonObject jsonObject:
NormalizeFunctionResult(jsonObject);
foreach (var property in jsonObject)
{
NormalizeNode(property.Value);
}
break;
case JsonArray jsonArray:
foreach (var item in jsonArray)
{
NormalizeNode(item);
}
break;
}
}
private static void NormalizeFunctionResult(JsonObject jsonObject)
{
if (!jsonObject.TryGetPropertyValue("$type", out var typeNode) ||
typeNode?.GetValue<string>() is not "functionResult" ||
!jsonObject.TryGetPropertyValue("result", out var resultNode) ||
resultNode is null)
{
return;
}
if (!TryGetString(resultNode, out var resultText) || !TryParseJsonNode(resultText, out var parsedResult))
{
return;
}
NormalizeNode(parsedResult);
jsonObject["result"] = parsedResult;
}
private static bool TryGetString(JsonNode node, out string value)
{
try
{
value = node.GetValue<string>();
return true;
}
catch (InvalidOperationException)
{
value = string.Empty;
return false;
}
catch (FormatException)
{
value = string.Empty;
return false;
}
}
private static bool TryGetValue<T>(JsonNode? node, out T value)
{
try
{
if (node is null)
{
value = default!;
return false;
}
value = node.GetValue<T>();
return true;
}
catch (InvalidOperationException)
{
value = default!;
return false;
}
catch (FormatException)
{
value = default!;
return false;
}
}
private static string SummarizeText(string? text, int maxLength)
{
if (string.IsNullOrWhiteSpace(text))
{
return "<empty>";
}
var collapsed = text
.Replace("\r", " ", StringComparison.Ordinal)
.Replace("\n", " ", StringComparison.Ordinal)
.Replace("\t", " ", StringComparison.Ordinal)
.Trim();
while (collapsed.Contains(" ", StringComparison.Ordinal))
{
collapsed = collapsed.Replace(" ", " ", StringComparison.Ordinal);
}
if (collapsed.Length <= maxLength)
{
return collapsed;
}
return collapsed[..maxLength] + "...";
}
private static bool TryParseJsonNode(string json, out JsonNode? parsedNode)
{
var trimmed = json.Trim();
if (trimmed.Length == 0 || (trimmed[0] != '{' && trimmed[0] != '['))
{
parsedNode = null;
return false;
}
try
{
parsedNode = JsonNode.Parse(trimmed);
return parsedNode is not null;
}
catch (JsonException)
{
parsedNode = null;
return false;
}
}
}
Discussion