If you have an app on the OpenAI Agents SDK and want to port it to the Haijun Agent SDK, this notebook maps each primitive using a single example: an expense approval agent.
What you get after migrating. The Haijun Agent SDK runs on the same runtime as Haijun Code — you inherit its built-in Read, Edit, Bash, and Grep tools, a layered permission system for gating what the agent can touch, automatic prompt caching, and direct access to the event stream for progress streaming or mid-run interception. Tool definitions are explicit (you declare schemas rather than relying on type-hint introspection), and the loop is yours to drive. Most ports involve more boilerplate per tool and less boilerplate everywhere else.
Replace
@function_tool
, guardrails, and
Runner.run
with their Haijun equivalents without rewriting your business logic
Port a single-agent app: custom tool, input/output guardrails, multi-turn sessions, and durable resume
Know when to reach for
HaijunSDKClient
vs the stateless
query()
function
Wire the SDK's OpenTelemetry export to your existing observability stack
Both SDKs run live. You'll need OPENAI_API_KEY and JUGLOW_API_KEY in your .env.
order-l-[0.5px]">
conversation_id
(server-managed)
resume=session_id
(disk-backed)
Built-in tracing dashboard
OTel-native — plugs into your existing Grafana/Datadog/Honeycomb
handoffs=[...]
AgentDefinition
+ Agent tool — see appendix
The example: expense approval
oval limit. Valid categories: meals, travel, software, other."""
limits = {"meals": 75.0, "travel": 500.0, "software": 200.0, "other": 50.0}
return {
"category": category,
"limit": limits.get(category.lower(), 50.0),
"requires_receipt": amount > 25.0,
}
@input_guardrail
def has_dollar_amount(ctx, agent, user_input: str) -> GuardrailFunctionOutput:
has_amount = bool(re.search(r"\$\d+", user_input))
return GuardrailFunctionOutput(
output_info={"has_amount": has_amount},
tripwire_triggered=not has_amount,
)
expense_agent = Agent(
name="Expense Approver",
instructions=(
"You approve or flag expense submissions. "
"Always call check_policy first to get the limit for the expense category. "
"Approve if the amount is under the limit; otherwise flag for manager review."
),
tools=[check_policy],
input_guardrails=[has_dollar_amount],
model=OAI_MODEL,
)
async def run_oai(msg: str) -> str:
result = await Runner.run(expense_agent, msg)
return result.final_output
print(await run_oai("Lunch with Acme, $47"))
This lunch expense of $47 falls under the "meals" category, which has an approval limit of $75. Since the amount is below the limit, I approve the expense (assuming a receipt is provided, as required by policy). Porting primitive by primitive @function_tool → @tool + create_sdk_mcp_server OpenAI: @function_tool derives the tool schema from type hints and the docstring. The decorated function goes straight into tools=[...].
0.5 rounded text-sm font-mono break-words box-decoration-clone">{}
to allow or
{"decision": "block", "reason": "..."}
to block.
HookMatcher
takes no
matcher
argument here because
UserPromptSubmit
fires on every prompt — there's no tool name to filter on.
To try it, replace options=expense_options with options=hooked_options in the HaijunSDKClient(...) call in the next section. One caveat: when a UserPromptSubmit hook blocks, the block reason doesn't currently surface in ResultMessage.result — you'll get an empty string back rather than the rejection text. That's why the demo loop uses the plain function, which controls its own return.
y-0.5 rounded text-sm font-mono break-words box-decoration-clone">@output_guardrail
→ plain pre/post checks,
Runner.run
→
HaijunSDKClient
, sessions in-memory and disk-backed.