Learn › GPT-6 Astra in Practice › Function Calling, Async Tools, and Hosted Shell

Function Calling, Async Tools, and Hosted Shell

Intermediate 🕐 16 min Lesson 12 of 15
What you'll learn
  • Define and call a custom function via the Responses API
  • Implement async tool calling with call_id returns
  • Use hosted shell and apply patch for server-side code tasks

Defining a Custom Function

Function calling in the Responses API works by describing the tools your application can run, letting Astra decide when to use them, and executing the calls it makes. The model never runs the function itself — it tells you what to run and with what arguments. Your application executes the function, captures the result, and sends it back to the model to continue.

A function definition has three required fields: name, description, and parameters. The description is what Astra reads when deciding whether to call the function — write it as a clear statement of what the function does and when it applies, not what it returns. A vague description produces missed tool calls; a precise one routes correctly.

The parameters field uses JSON Schema to declare the arguments Astra may pass. A simple object schema with named properties covers most cases. Adding strict: true to the tool definition enforces schema adherence — Astra will not fabricate argument keys not declared. For production use, strict mode is the safer default.

from openai import OpenAI
import json

client = OpenAI()

tools = [
    {
        "type": "function",
        "name": "get_weather",
        "description": "Get the current weather for a city. Use when the user asks about weather conditions in a specific location.",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The city name, e.g. San Francisco"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit"
                }
            },
            "required": ["city"]
        },
        "strict": True
    }
]

response = client.responses.create(
    model="gpt-6-astra",
    tools=tools,
    input="What's the weather in Tokyo right now?"
)

for item in response.output:
    if item.type == "function_call":
        print(f"Tool: {item.name}")
        print(f"Arguments: {item.arguments}")
        print(f"Call ID: {item.call_id}")

When Astra calls a function, the response output contains a function_call item with three fields: name (the function requested), arguments (a JSON string your code parses), and call_id (the identifier you use when returning the result). The call_id ties the result to the specific invocation — it is required in the follow-up request.

To return the tool result, send a new request using previous_response_id and include a function_call_output item with the matching call_id:

def get_weather(city, unit="celsius"):
    # Your real implementation calls a weather API
    return {"city": city, "temperature": 22, "unit": unit, "condition": "partly cloudy"}

args = json.loads(item.arguments)
result = get_weather(**args)

response2 = client.responses.create(
    model="gpt-6-astra",
    previous_response_id=response.id,
    input=[
        {
            "type": "function_call_output",
            "call_id": item.call_id,
            "output": json.dumps(result)
        }
    ]
)

print(response2.output_text)

The tool_choice parameter controls how aggressively Astra invokes tools. The default "auto" lets Astra decide. "required" forces at least one tool call per turn. Passing a specific name as tool_choice={"type": "function", "name": "get_weather"} forces that exact function every time — useful for structured extraction workflows where you always want a parsed object rather than free-form text.

Synchronous vs Async Tool Calling

By default, tool calling is synchronous from Astra's perspective: the model emits a function call, waits for your application to execute it and return the result, and only then continues generating. This is correct for fast tools — in-process lookups, deterministic calculations, operations that complete in milliseconds. The pause is negligible when the tool is quick.

The problem arises with slow tools: a third-party API call that takes five seconds, a full-text database search, a web scrape. In a multi-tool workflow, the wait compounds — three slow tools called sequentially means three blocking round-trips before Astra can produce the final answer.

Async tool calling resolves this. Adding async: true to a function definition signals that Astra does not need to wait for the result before continuing. The model emits the function_call item and then continues reasoning — calling other tools, answering independent parts of the request, or working through context it already has. Your application runs the tool in the background and submits the result when ready, using the original call_id.

tools = [
    {
        "type": "function",
        "name": "fetch_market_data",
        "description": "Fetch current price and volume data for a stock ticker. This call typically takes 3-5 seconds.",
        "parameters": {
            "type": "object",
            "properties": {
                "ticker": {"type": "string", "description": "Stock symbol, e.g. AAPL"}
            },
            "required": ["ticker"]
        },
        "async": True,
        "strict": True
    },
    {
        "type": "function",
        "name": "get_company_profile",
        "description": "Get company name, sector, and description for a ticker.",
        "parameters": {
            "type": "object",
            "properties": {
                "ticker": {"type": "string"}
            },
            "required": ["ticker"]
        },
        "async": True,
        "strict": True
    }
]

OpenAI's benchmarks show async tool calling reduced mean execution time by roughly 19% on tasks where multiple tools run in parallel — from 23.4 seconds to 18.9 seconds on representative test sets. The gain compounds with the number of concurrent tools: a workflow requiring five async tool calls pays the latency of the slowest single call, not the sum of all five.

A practical heuristic: if a tool call takes longer than 500 milliseconds and other work could proceed in parallel, async: true is worth evaluating. For fast tools, the coordination overhead of async can exceed the saved wait time, and synchronous is faster.

Multi-Tool Orchestration

Astra can emit multiple tool calls in a single response turn. When a request touches several distinct data sources or requires independent operations, the model calls multiple tools at once. Your application receives a list of function_call items, executes each concurrently, and returns all results together in a single follow-up request.

import concurrent.futures
import json
from openai import OpenAI

client = OpenAI()

tools = [
    {
        "type": "function",
        "name": "query_database",
        "description": "Look up a customer's order history by email address.",
        "parameters": {
            "type": "object",
            "properties": {"email": {"type": "string"}},
            "required": ["email"]
        },
        "async": True,
        "strict": True
    },
    {
        "type": "function",
        "name": "web_lookup",
        "description": "Search the web for current information about a company.",
        "parameters": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"]
        },
        "async": True,
        "strict": True
    }
]

response = client.responses.create(
    model="gpt-6-astra",
    tools=tools,
    input="Look up orders for user@example.com and find their company's recent news."
)

tool_calls = [item for item in response.output if item.type == "function_call"]

def execute_tool(call):
    args = json.loads(call.arguments)
    if call.name == "query_database":
        result = {"orders": [{"id": "ORD-001", "total": 129.99, "status": "delivered"}]}
    elif call.name == "web_lookup":
        result = {"results": ["Company raised Series B in August 2026"]}
    else:
        result = {}
    return {"call_id": call.call_id, "result": result}

with concurrent.futures.ThreadPoolExecutor() as executor:
    results = list(executor.map(execute_tool, tool_calls))

# Submit ALL results in a single request — not one per tool call
response2 = client.responses.create(
    model="gpt-6-astra",
    previous_response_id=response.id,
    input=[
        {
            "type": "function_call_output",
            "call_id": r["call_id"],
            "output": json.dumps(r["result"])
        }
        for r in results
    ]
)

print(response2.output_text)

The critical implementation detail: all tool results for a turn are submitted in a single request, not one request per tool call. Submitting them individually produces undefined behavior — Astra expects a complete set of results for the turn before continuing. Build the full input list from all collected results before making the follow-up call.

For async tools, this extends naturally: launch all tool executions in parallel immediately after the first response, wait for all to complete, then submit the full results list. The combination of async: true on tool definitions and parallel execution in your application produces the largest latency savings.

Hosted Shell: A Server-Side Terminal

The hosted_shell tool gives Astra access to a sandboxed bash terminal running in OpenAI's infrastructure. When this tool is in the tools array, the model can execute shell commands without your application managing a subprocess, parsing output, or handling errors. Astra runs the command, reads the output, and decides what to do next — all within a single Responses API session.

This is the API-side capability that drives Astra's SRE-Bench performance. The 88% first-attempt completion rate on SRE-Bench is directly tied to this feedback loop: run code, observe real execution output, reason about what happened, and take the next step. Your application does not have to orchestrate the execution layer — Astra owns the loop.

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-6-astra",
    tools=[{"type": "hosted_shell"}],
    reasoning={"effort": "high"},
    input="Clone https://github.com/example/myproject, run the test suite, and report which tests are failing and the most likely root cause for each."
)

# Astra handles git clone, test runner invocation, and output parsing internally.
# The response contains Astra's analysis — not raw shell output.
print(response.output_text)

The hosted environment provides a standard Linux shell with Python, Node.js, and common build tools pre-installed. The environment is ephemeral: it resets between separate Responses API calls with no persistent file state. For multi-turn tasks that require a consistent workspace — debugging a codebase across several exchanges — use previous_response_id to continue within the same session, which maintains the environment state across turns.

Practical tasks where hosted_shell changes the workflow: running a test suite and receiving an analysis of why each failure occurred, not just their names; executing a linter across a codebase and getting prioritized findings; running a migration script against a staging database and having Astra confirm the results are correct. These previously required a separate execution layer — a container, a CI step, a subprocess — before the model could reason about the output.

One constraint to know: hosted_shell is gated behind OpenAI's critical cyber safety threshold. For the vast majority of developer use cases — debugging, testing, build automation — the tool is available in standard API access. Applications that could enable offensive security workflows require separate clearance.

Apply Patch: Code Diffs Without File Management

The apply_patch tool lets Astra generate and apply unified diffs in the hosted environment. Rather than asking the model to return a diff string and applying it yourself, you ask Astra to find and fix a problem — the diff is generated and applied internally, and the model can verify the fix by running tests before reporting back. The whole loop happens in a single turn.

from openai import OpenAI

client = OpenAI()

with open("buggy_module.py", "r") as f:
    source_code = f.read()

response = client.responses.create(
    model="gpt-6-astra",
    tools=[
        {"type": "hosted_shell"},
        {"type": "apply_patch"}
    ],
    reasoning={"effort": "high"},
    input=f"""Here is the source file buggy_module.py:

{source_code}

The function process_batch() raises a KeyError when the input dict is missing the 'status' key.
Find the bug, apply a fix using apply_patch, then run the existing tests to confirm the fix works."""
)

print(response.output_text)

The difference between apply_patch and returning a diff string matters in agentic workflows. When Astra returns a diff string, your application validates the patch, applies it to the correct file, handles conflicts, and confirms the result separately. When Astra uses apply_patch in a hosted environment, it applies the diff and immediately runs tests to verify the fix works — closing the loop in the same turn without any file management in your application code.

Use each approach according to what comes next in your workflow:

apply_patch in hosted env
Return diff to your code
Self-verifying: run tests after patching
Patch goes into your version-controlled repo
No file management in your application
Your CI/CD pipeline handles validation
Ephemeral: no persistent file state
Patch is auditable and reviewable in git
Best for: proof-of-fix in isolation
Best for: PR generation and code review

A common production pattern: use apply_patch in the hosted environment to verify a fix is correct first, then extract the confirmed diff and apply it to your codebase as a pull request. You get the self-verification of the hosted loop and the auditability of a versioned commit.

Key takeaways
  • async: true on a function lets Astra continue working while your code runs the tool
  • Hosted shell gives Astra a real server-side terminal — the basis of its SRE-level coding
  • apply_patch applies code diffs in a hosted environment without your code managing file state