Category
Short deck for the post. Keep this aligned with the card summary on the tutorials landing page.
Introduction
Author: Eivind Holt, July 2026 Repository: github.com/eivholt/qai-nemotron Target: Qualcomm Dragonwing IQ-9075 EVK / QCS9075 / Hexagon v73. Hardware generously sponsored by Qualcomm ๐Model: mistralai/Ministral-3-3B-Instruct-2512 Q4_K_M GGUF running on device NPU Large language models that fit edge devices have passed the boundary where they can replace traditionally hardcoded controlflow logic with tool reasoning. They can inspect local state, choose an action, execute a tool, and react to the result. This tutorial builds that loop as a small console application on the Qualcomm Dragonwing IQ9075 EVK. The example is an outbound shipping coordinator. It reads one pending shipment, checks carriers and loading docks, and chooses one of three outcomes:
- schedule a compliant carrier and dock;
- hold a shipment when a temporary problem can clear; or
- escalate when no safe plan exists.

Related tutorials
Deploying NVIDIA Llama-3.1-Nemotron-Nano-8B-v1 on Dragonwing IQ9075 covers the EVK, WSL2 storage, Qualcomm AI Hub credentials, QAIRT setup, and Genie fundamentals in more detail. Agentic LLMs on Dragonwing IQ9075 compares the function-calling and logistics behavior of several models. That benchmark work led to the model and client design used here.What is in the example
The implementation lives in these repository paths:
The runtime includes four scenarios:
Why Ministral 3B
The example uses Mistral Ministral-3-3B-Instruct-2512 with its official Q4_K_M GGUF weights. In the preceding IQ9075 comparison, this model was the most stable accelerator-backed choice for bounded tool use. It scored 132/170 on the shared non-web BFCL selection and completed 9/14 strict custom hospital logistics cases. The IQ9075 Q4 package generated about 5 tokens/s in saved agent profiles. That is slower than some QAI Hub W4A16 models, but the model was more dependable at multi-turn tool use. Model selection should therefore consider completed work, not tokens/s alone. Mistralโs own usage instructions require the native Mistral tool-call parser when the source model is hosted with vLLM. Genie does not provide that OpenAI adapter itself. The repositoryโsmistral_tool renderer and parser perform the same translation around both the persistent C++ service and genie-t2t-run.
How the pieces fit
The running path is intentionally small: Both HTTP layers listen only on127.0.0.1. The Python adapter renders the complete request once with the native Mistral template, sends that text through an identity prompt in Qualcommโs C++ GenieAPIService, parses the returned model text, and gives Pydantic AI a normal OpenAI-compatible response. Pydantic AI then executes the selected tool directly or discovers and calls it through
MCP before including the result in the next model turn.
The C++ process loads the model and QNN contexts once. It resets the Genie dialog for each HTTP request because the Python adapter already supplies the complete conversation, but that reset does not reload the model. Keeping rendering in one
place also avoids applying a generic service template on top of the modelโs native Mistral prompt.
The repository retains a second, highly inspectable bridge around genie-t2t-run. It starts a new CLI process for each model turn and records prompts, raw responses, parser decisions, and Genie profiles. That makes it useful for model integration tests and parser debugging, while the persistent C++ path is the better live-demo backend.
Resource expectations
The validated export used QAIRT 2.47 and took about 25 minutes on the desktop PC.
Reserve at least 45 to 50 GB of fast Desktop storage for the complete build. The successful Desktop had 192 GB of RAM, but peak export RAM was not captured, so this work does not establish a smaller minimum. The generated bundle itself fits comfortably in the EVKโs system memory.
Prepare the Desktop export environment
Create an isolated environment:qairt-vm fetch if it is not 2.47.0.260601.
Keep the qairt-dev package in control of Python imports in this environment. Do not source the full SDK envsetup.sh; it can prepend a second qairt
Python tree and shadow the builder package. The two path exports above supply the Desktop binaries and shared libraries needed by the builder.
Download the official GGUF
The publisher providesMinistral-3-3B-Instruct-2512-Q4_K_M.gguf
in the official Mistral repository.
Compile and export for IQ9075
Run the repository helper from the activeqairt-dev-gguf environment:
dsp_arch:v73;soc_model:77;cores:1 and exports two QNN context binaries.
Successful output ends with an EXPORT_SUMMARY JSON line.
The deployable package is:
Install QAIRT 2.47 beside the EVK default
Keeping 2.47 beside/opt/qairt/current avoids changing every existing model on the board. Set the current EVK address, then create the target directories:
PATH,
LD_LIBRARY_PATH, and ADSP_LIBRARY_PATH only for this model process.
Copy the model and example
From the repository root on the Desktop:~/qairt-env.sh from the base EVK setup in the first tutorial. It supplies the normal device environment before the launcher adds the 2.47 paths.
Prepare the agent environment on the EVK
Open an SSH shell:genie_config.agent.json with seed 42, temperature 0, top-k=1, and top-p=1.0. It also writes the C++ serviceโs identity prompt.json and creates root-level relative links to the tokenizer, backend extension, and QNN context binaries under artifacts/. The links consume negligible storage and accommodate the sample serviceโs basename lookup. The original exported configuration and artifacts are not modified.
Build the persistent C++ service
The live demo uses Qualcommโs open-sourceGenieAPIService.
The validated build pins qai-appbuilder commit 86ce07a so that source and commands remain reproducible:
CLI/CLI.hpp. The small repository patch removes an invalid extra std:: qualification rejected by the EVKโs GCC 13 compiler and changes the serviceโs hard-coded bind address from 0.0.0.0 to 127.0.0.1. It does not add a model template or tool parser.
Configure and build on the EVK:
QNN_SDK_ROOT exported during both commands. The nested AppBuilder build reads it from the environment while compiling against the EVKโs installed QNN development headers. At launch, the script selects the side-by-side QAIRT 2.47 libraries needed by this model bundle.
A clean checkout with only this small source patch completed all four strict direct-tool scenarios on the EVK in 2m 20.6s. This confirms that the live demo does not depend on the earlier experimental C++ model-template or tool-parser patches used elsewhere in the model comparison work.
Start the local model server
Use the first EVK shell for the persistent live-demo backend:GenieAPIService on port 8911, loads the model once, waits for readiness, and starts the model-specific Python adapter on port 8001. The adapter uses the native Mistral tool template and exposes every parsed call. It does not silently keep only the first call when a model emits extras.
For inspectable model integration and parser regression tests, stop the C++ server first and start the CLI bridge instead:
genie-t2t-run separately for every model request. Do not run both backends at the same time because they share port 8001 and the HTP resources.
Test the Python tools without a model
Open a second EVK shell:Run the agent
Direct Pydantic function tools are the default. Start with the routine case:Weather-closure example
The following transcript came from the persistent C++ runner. Only whitespace and terminal labels have been rearranged to make the JSON easier to read. The vcomplete system instruction is shown in the next section. User input:Hazardous-material example
The hazardous-material case exercises the other non-scheduling outcome. It uses the same user input:The autonomous agent loop
Pydantic AI registers ordinary Python functions as model tools. The model never executes Python or shell code directly. All six tools remain available on every turn:get_pending_shipmentget_shipping_optionsschedule_shipmenthold_shipmentescalate_shipmentnotify_dispatch
sequential=True prevents concurrent state-changing calls. It does not hide tools or prescribe the next action. The model receives each tool result in the conversation and chooses the next step.
Return decision-ready observations
The first autonomous version returned one mixed carrier list. Ministral usually completed it, but strict repeated runs exposed two attention failures: it tried clearly rejected options in the weather case, and once held a routine shipment after focusing on an undersized van even though a usable truck was present. Adding more case-specific prompt text would have made the test easier to pass but less generic. Instead, the mock options service was improved as a real logistics service would be. It performs deterministic compatibility checks and groups records into:schedule, hold, or escalate. It supplies decision-ready facts and leaves coordination policy to the model. This is an
important edge-agent pattern: deterministic systems should calculate facts they can calculate exactly, while the LLM handles changing workflow and intent.
The tools operate on one current shipment, so downstream calls do not ask the model to repeat its shipment ID. Removing redundant identifier transcription
fixed a real MCP validation failure without reducing decision autonomy.
Run the same tools through MCP
The same console client can discover and execute all six tools through the Model Context Protocol:shipping_agent/mcp_server.py starts automatically as a separate stdio process for each scenario. It owns the mock state and persists every executed call for the strict evaluator. There is no MCP tool pruning.
One subtle difference mattered. Pydanticโs direct registration emitted concise JSON schemas, while raw MCP discovery added schema titles such as
get_shipping_optionsArguments and Shipment Id. An early MCP run copied SHP-1001 as SHP-1000, recovered, but correctly failed strict scoring because of the extra rejected call.
The client wraps the discovered tools with Pydantic AIโs PreparedToolset and normalizes only their model-facing JSON schemas:
A safer progressive-disclosure option
Some deployments should expose only actions that are valid in the current workflow stage. Pydantic AI can prepare the tool list dynamically:Keep hard validation in code
Autonomy does not require operational systems to accept impossible actions. The scheduling tool still enforces ordinary software rules:"dispatch_notified": false; only notify_dispatch returns true. This truthful state prevents the model from treating escalation or scheduling as if notification had happened. It is an API design improvement, not a hidden next
step or a scenario-specific hint.
Strict scoring uses the recorded action ledger, not an LLM judge. A run fails for a wrong disposition, wrong carrier or dock, rejected action, extra action, incorrect order, or missing dispatch notification. Pydantic tool retries are disabled, so a malformed call cannot be silently repaired outside that ledger.
Validated results
The final application was validated on the physical IQ9075 HTP with temperature 0, top-k 1, top-p 1, and the same four scenarios for both transports. Every row below contains two complete repetitions, exposes all model-emitted calls, uses zero automatic tool retries, and requires the exact action ledger.
The persistent service reduced total wall time by about 23% for direct tools and 22% for MCP. This is workflow time rather than raw decode throughput: each case needs four tool-producing requests plus a fifth request for its final summary. The C++ process keeps the model and QNN contexts loaded between those requests; the CLI path initializes a new dialog process each time.
Every case followed one of these exact four-call paths:
An earlier CLI setting kept only the first parsed call. That looked helpful until a raw weather response revealed one valid
hold_shipment call followed by an invented notify_shipment call. Silently dropping the second call made the run look better than the modelโs actual behavior. The final all policy exposes both, and every result above was rerun under that stricter rule.
Four general changes made the fully exposed runs repeatable:
- Model-facing empty assistant content is rendered as an empty string, never the literal word
null. - The prompt asks for exactly one tool call and a pause for its result.
- Hold and escalation descriptions distinguish temporary recovery from no compatible option.
- Disposition results explicitly report that dispatch is not yet notified.
Experiment with multiple tools in one model turn
The main suite deliberately asks for one tool call per model turn. This is reliable, but it leaves a useful question unanswered: can this model identify independent work, request several tools at once, and still wait before taking an action that depends on their results? The separate multi-tool case replaces the combined options tool with three read-only observations:get_pending_shipmentget_carrier_optionsget_dock_options
schedule_shipment, hold_shipment, escalate_shipment, and notify_dispatch. The cold-chain shipment can be scheduled only after the model combines the independent shipment, carrier, and dock results. Its extra instruction is about dependency handling rather than the answer:
sequential=True. The request also sets parallel_tool_calls=True. Run the case against the persistent C++ adapter:
REEFER-2 at COLD-1 in the second turn. Only after that action succeeds does it notify dispatch in the third turn. A fourth turn returns the final text summary without calling a tool.
The evaluator reports two separate results. task_passed requires the exact five tools, correct carrier and dock, successful final state, and dispatch notification. batching_passed additionally requires at least two independent read calls in one assistant response and rejects any response that mixes dependent state-changing calls. This means a fully sequential run can complete the shipping task but still fail the batching experiment.
All four runs emitted the same model-level tool batches, made exactly five calls, and reached the correct state. The execution order printed for the three Python read functions can vary because they run concurrently; the modelโs original call order remains available in
TOOL BATCHES and in the adapterโs raw request records.
This is a successful limited demonstration, not proof that every parallel plan is safe. The model did not emit the whole workflow up front, and it should not have: the schedule choice depends on observation results, while notification depends on successful scheduling. Useful multi-tool behavior parallelizes only calls that are genuinely independent and keeps dependent actions in later turns.
Generate and execute coordination code
Code generation is a different form of autonomy from tool calling. A tool-calling agent chooses one or more declared actions, receives their results, and decides what to do in later model turns; a code-generating agent writes a limited program that can perform loops, calculations, and several API calls inside a sandbox. That program can be tested, approved, cached, and reused without another model request, but it also introduces separate concerns around malicious code, validation, and cache selection. This approach is covered in TODO:Update link when published Build a Code-Generating Manufacturing Agent on Dragonwing IQ9075.What frontier agent platforms add
The successful multi-tool case does not mean that this small model matches a current frontier agent. It shows a useful but limited skill: the model can request independent information together, inspect the results, and then choose the next actions. Frontier platforms can handle more complex workflows than this step-by-step loop.In the following sections we outline the state of agentic capabilities in frontier models and APIs. While the gap between these and edge deployable models is huge, the latter seems to converge towards frontier model capabilities. Knowledge of those capabilities helps prepare for what lays ahead for the smaller models, and can give inspiration to clever agentic AI engineering.As of July 2026, the GPT-5.6 family combines stronger agent-trained models with orchestration features in the Responses API. The distinction matters: some capabilities live in the model, some in the hosted runtime, and the most advanced workflows depend on both.
Deferred tool discovery
A conventional function-calling request places every tool name, description, and parameter schema in the prompt. Large MCP deployments can spend tens of thousands of tokens describing tools that are never used. OpenAIโs tool search instead gives the model lightweight namespace or MCP-server descriptions. Functions markeddefer_loading: true are loaded only when the model searches for them. The selected definitions are then appended to the conversation. This is sometimes called deferred tool discovery. It reduces prompt size, preserves more of the prompt cache, and lets one agent navigate a much larger tool catalog.
This is not the same as simply hiding most tools from a small model. The model must understand a two-stage protocol: search for an appropriate capability, inspect the discovered schema, and then call it correctly. Only GPT-5.4 and later OpenAI models currently support that protocol. The IQ9075 demo therefore presents a small, stable tool set directly. A custom client could build a tool-search layer, but these edge models have not been trained or validated for its protocol.
Programmatic tool calling
Programmatic Tool Calling lets GPT-5.6 generate JavaScript that coordinates tools inside an isolated V8 runtime, Googleโs high-performance JavaScript engine. The program can run independent calls concurrently, use loops and conditions, pass one result into another call, and reduce large intermediate outputs before returning them to the model. It has no general filesystem, subprocess, package-installation, or direct network access; it can reach the world only through tools allowed by the application. This can replace many model round trips when the control flow is predictable. The shipping experiment still returns every tool result to the model for a fresh decision. Reproducing Programmatic Tool Calling locally would require both a sandbox runtime and a model that reliably generates orchestration code against typed tool inputs and outputs. The earlier edge experiments found direct, well-described function calls substantially more dependable than adding another generated-code protocol.Model-directed multi-agent work
GPT-5.6โsultra mode coordinates four agents by default. The Responses API also provides a multi-agent beta in which a root model can create subagents, give them bounded tasks, exchange messages, wait for results, and synthesize their findings. Separate contexts let several agents investigate independent parts of a codebase, compare hypotheses, or conduct research concurrently without crowding one conversation.
This is different from the multi-tool case above. That case has one model context and three Python functions executing concurrently. True multi-agent execution requires several active inference streams and context histories. Keeping multiple QNN model contexts resident, or repeatedly swapping them, would put much more pressure on EVK memory and HTP scheduling. Serializing the subagents is possible, but loses the main latency benefit and still leaves a 3B model responsible for delegation and synthesis.
Long-horizon state and a broader action surface
The GPT-5.6 Sol model has a 1.05-million-token context window, up to 128,000 output tokens, adjustable reasoning effort throughmax, structured outputs, and a Responses API tool surface that includes web and file search, code interpretation, hosted shell, patch application, computer use, skills, MCP, and tool search. Together these tools let an agent retrieve public or private information, operate graphical interfaces, execute sandboxed code, modify files, and load reusable procedures without defining every capability as a custom function. The platform also supports conversation state,
background execution, and context compaction for long-running work.
The Ministral bundle in this tutorial is compiled for a 4,096-token context. Increasing that compiled limit consumes more memory and makes export and runtime resource pressure worse. More importantly, a larger token limit alone would not teach the model to stay coherent across a long workflow, recover from partial failure, manage approvals, or use the additional hosted protocols.
Why the gap remains
The missing capabilities are not one single model-size limitation:- Training: frontier models are trained and evaluated on long tool workflows, deferred schemas, code orchestration, computer use, and recovery from failures.
- Model capacity: delegation, protocol selection, intermediate-state tracking, and final synthesis compete for limited attention in a 3B or 8B model.
- Runtime support: Genie and the tutorialโs adapter expose chat completions and function calls, not the Responses APIโs tool-search, program, subagent, compaction, and hosted-tool item types.
- Memory and compute: million-token contexts and concurrent model agents are far beyond the practical memory and latency budget of this exported EVK setup.
- Quantization: Q4 or W4A16 deployment makes these models practical on-device, but can further reduce reliability on exact, low-frequency protocol tokens and complex structured output.
Conclusion
A practical edge agent can be meaningfully autonomous without receiving arbitrary code or hardware access. This example gives a 3B NPU-hosted model all six workflow tools, lets it independently choose and sequence actions, and keeps hard operational rules and conditions in deterministic services. The final direct and MCP runs show that this level of local autonomy is already practical on IQ9075 when the model receives its native prompt format, concise tool schemas, and decision-ready observations. Progressive disclosure remains available when a deployment needs a narrower safety envelope.Troubleshooting
QNN context error 5000
If the raw response contains:openai_genie_server process.
The 2.47 paths must appear before /opt/qairt/current.
Device creation failure 14001
A QNN device creation error can be a stale runtime or resource conflict rather than a bad answer. Stop leftover Genie processes, verify no other test is using the HTP, and retry. Reboot the EVK if the device remains unavailable.Missing libatomic
If Genie reportslibatomic.so.1: cannot open shared object file, include the
QAIRT compiler-runtime library directory in LD_LIBRARY_PATH. The base
qairt-env.sh from the first tutorial should include both the QAIRT target
directory and lib/aarch64-oe-linux-gcc8.2 when that compatibility library is
required.
C++ service cannot find bundle files
The pinned service looks for context binaries at the bundle root and resolves some configuration paths by basename. A normal QAIRT GGUF export keeps those files underartifacts/. Rerun:
C++ /status returns an error
At the pinned service revision, /status can fail while serializing its
response even when the model is healthy. The launcher intentionally probes
http://127.0.0.1:8911/ for C++ readiness. After the adapter starts, use
http://127.0.0.1:8001/health. Do not treat the broken status response as an
inference failure.
Valid JSON but no Pydantic tool call
Genie returns text, not an OpenAI tool object. The bridge must use the native Mistral renderer andmistral_tool parser. A generic JSON parser or another
modelโs chat template can turn a valid-looking response into an unusable call.
Long or non-terminating output
Confirm the native Mistral[INST]...[/INST] wrapper and EOS token are in use.
A raw prompt without the native wrapper previously ran for more than 11 minutes;
the correctly wrapped smoke request completed in seconds.
Stop the demo
UseCtrl+C in the server shell. The mock state exists only for one console
run, so each scenario starts cleanly on the next invocation.
