This directory contains a port of the pi-mono project from Mario Zechner to Swift, as I need to embed this into an iPad app:
https://github.com/badlogic/pi-mono
It is split up slightly differently.
PiSwiftAgent is the reusable low-level agent loop/proxy layer. The higher-level
coding-agent harness behavior from pi-mono/packages/agent lives in
PiSwiftCodingAgent instead of being exposed as a source-compatible generic
AgentHarness API. The macOS terminal UI and pi-coding-agent executable live
in the sibling ../PiSwiftTui package so this package stays suitable for mobile
applications. See
AGENT_HARNESS_API_BOUNDARY.md for the parity decision.
Hosts can set limits without changes to script source:
let codeMode = createCodemodeExtension(options: CodemodeExtensionOptions(
mode: .on,
inlineBudget: 0,
defaultTimeoutMs: 60_000,
maxTimeoutMs: 600_000,
validationErrorPrefix: "validation: ",
searchResultDetail: .summary
))CodemodeExtensionOptions.inlineBudget is a session extension override. It takes
precedence over codemode.inlineBudget in settings without writing a settings
file. Set it to 0 to omit all inline tool declarations. Tools with .codemode
or .deferred exposure remain callable and available through discovery. Use
.deferred when a tool must stay out of the inline catalog for every budget.
The default budget remains 3000 estimated tokens.
CodemodeSearchResultDetail has .full and .summary cases. Set the additive
searchResultDetail option on CodemodeExtensionOptions (or
CodemodeToolOptions) to .summary for rows with exactly name, namespace,
and description. The namespace is null when absent; the description is at
most 160 characters. Summary rows have no declarations or schemas. The default
.full keeps the existing { name, description } rows with full tool samples.
In summary mode, search for relevant tools, call describeTool(name) for each
selected tool, then execute calls and return selected results. describeTool
keeps the full description, guidance, input declaration, and output schema.
Ranking, namespace filters, and ALL_TOOLS keep their existing behavior.
The optional recordDiscoveryCall callback on each options type receives
searchTools, describeTool, or describeNamespace once per runtime dispatch.
Use it to measure actual discovery calls, including loops and invalid arguments.
The default is nil; the callback does not change results or native call records.
defaultTimeoutMs and maxTimeoutMs are optional. The same fields are available
in CodemodeSettings and the codemode settings object. Extension options take
precedence over settings. A script can request a timeout on its first line:
// @options: {"timeout_ms": 600000}. A request above the host maximum fails with
an error that gives the maximum. A script without timeout_ms uses the default,
and the maximum also limits that default. With only maxTimeoutMs set, the
maximum is the timeout. With both options unset, the existing timeout
behavior stays in use. A host timeout ends the wait. It cannot guarantee that
a JavaScriptCore thread or a native operation has stopped on iOS.
The host dispatches each call that a script makes before its result. When the script
ends, the host cancels the calls that are not complete. A call that did not start has
the status cancelled and the error "Execution ended before the tool call started".
After a script ends, later bridge calls are rejected before tool dispatch.
Their bounded call records use rejected_after_end. CodemodeRuntimeResult.rejectedCalls
remains available for attempts after the host returns. The built-in extension saves
late call details as codemode-late-call session entries, with the parent tool-call
ID and the call row. It retains at most 128 such rows per execution, with bounded
argument and error previews. A late bridge call also rejects its JavaScript promise.
When the host abandons a JavaScriptCore thread, it records rejected calls from that
thread for 60 seconds (CodemodeToolOptions.lateCallWindow). After that time, the
execution releases its call state, and rejectedCalls still holds the calls.
For a native operation whose result is uncertain, return AgentToolResult.details with {"outcome": "unknown"}.
Code mode records outcome_unknown and lists these calls separately in error and
abort summaries. The caller must inspect the operation before a retry. Keep an
existing error prefix in the result text. Code mode runs prepareArguments and
schema validation for each nested call before the host tool hook runs. The session
does not run prepareArguments again. It validates the arguments again against the
tool that it executes, because a live registration can replace that tool. The
optional validationErrorPrefix is added to the code mode argument failures.
Set CreateAgentSessionOptions.toolNameCollisionPolicy to .reject to reject
duplicate tool names and JavaScript identifiers such as edit-node and
edit_node. The default is .overwrite for compatibility. A rejected extension
registration returns HookAPIError and produces a diagnostic. An extension can
register its own tool again, for example to hide it. Only the built-in tools that the
session creates reserve their names. An extension that registers the tool name of a
replaceable built-in extension replaces that built-in extension, as with .overwrite.
Extension load failures are error diagnostics with each policy. Extension tools
retain registration order. ToolInfo.ownerPath identifies the owner of the
executable registration, including live replacements. ToolNamespace.version
is optional and appears as version in describeNamespace output.
PiMCPAdapter is an opt-in inline extension. Applications provide MCP
configuration in code. The adapter does not read or write project, home, or
third-party MCP config files.
import PiMCPAdapter
import PiSwiftCodingAgent
let adapter = McpAdapter.makeExtension(McpAdapterOptions(
config: McpConfig(mcpServers: [
"docs": ServerEntry(url: "https://mcp.example.com/mcp")
])
))
let result = await createAgentSession(CreateAgentSessionOptions(
inlineExtensions: [adapter.inlineExtension()]
))
let status = await adapter.runtime.status()The extension registers the mcp proxy tool. It also registers direct tools
when cached or live metadata enables them, and updates direct tools and prompt
commands during the session after a metadata refresh. Hosts can subscribe to
mcpStatusEvent and use the runtime status API to build native UI. The adapter
does not include terminal panels or a WebView renderer.
OAuth is closed by default. A host that needs OAuth must supply an
McpAuthorizationProvider, which can use its own secure credential store. The
adapter does not read token files from the user's home directory.
The runtime also provides connect, disconnect, reconnect, logout,
callTool, readResource, listTools, listResources, listPrompts, and
getPrompt. MCP prompts register namespaced slash commands. Set sampling or
elicitation and provide a
serverRequestHandler to enable server-initiated requests. The adapter does
not advertise those capabilities without that handler.
MCP text output has a guard by default. It limits inline text to 50 KiB or
2,000 lines and keeps images. Proxy-tool details limit raw MCP results to
16 KiB and replace larger values with a compact reference. Hosts can set
outputGuard to false or supply an McpOutputStore for complete-output
artifacts.
Tracing is disabled unless both settings.trace.enabled (or a server's
trace) and an McpTraceSink are supplied. Trace events contain only
redacted server and method metadata, direction, message type, and byte count.
They never contain MCP payloads, headers, URLs, or authorization values. The
host owns trace retention and storage.
PiSwift supports delegating work to specialized subagents without spawning a subprocess. Subagents are defined by user-editable Markdown files and run in-process with isolated context.
- User agents:
~/.pi/agent/agents - Project agents:
.pi/agents(nearest parent of the current working directory)
Each agent is a .md file with YAML-style frontmatter. The body becomes the agent’s system prompt.
Example:
---
name: worker
description: General-purpose subagent
model: gpt-5.2
tools: read,edit,write,bash
outputFormat: |
## Completed
## Files Changed
## Notes
---
You are a worker agent with full capabilities.
Supported frontmatter keys:
name(required): agent name used in tool calls.description: shown in listings and error messages.tools: comma-separated tool names. Useallto enable all built-in tools (excludingsubagent).model: model pattern (e.g.openai/gpt-5.2orgpt-5.2).outputFormat: appended to the system prompt as an “Output format” section.
The subagent tool supports three modes:
- Single:
{ "agent": "worker", "task": "Summarize the tests in Tests/." }
- Parallel:
{ "tasks": [
{ "agent": "worker", "task": "Scan Sources/ for concurrency violations." },
{ "agent": "reviewer", "task": "Review README for updates." }
] }
- Chain:
{ "chain": [
{ "agent": "planner", "task": "Plan the fix for X." },
{ "agent": "worker", "task": "Implement the plan:\n{previous}" }
] }
Optional parameters:
agentScope:user,project, orboth(default:user).cwd: per-task working directory (single/parallel/chain items).
- Subagents run in-process; no subprocess is spawned.
- If
toolsis omitted, the default coding toolset is used. - If
modelis omitted, the main agent’s selected model is used.
Prompt templates are Markdown files that expand when you type /name in the prompt.
Locations:
- User templates:
~/.pi/agent/prompts - Project templates:
.pi/prompts(nearest parent of the current working directory)
Template body supports $1, $2, $ARGUMENTS, and $@ substitutions.
When no model is selected, the default fallback order is:
- anthropic:
claude-sonnet-4-5 - openai:
gpt-5.2 - openai-codex:
gpt-5.2-codex - opencode:
claude-opus-4-5
This port builds with strict concurrency. Some lock-backed wrappers still use
@unchecked Sendable; see CONCURRENCY_FATALERROR_AUDIT.md for the current
inventory and cleanup plan. Errors use Swift enums that conform to
LocalizedError instead of NSError.
See examples/subagents/fetcher.md for a sample agent that uses curl via the bash tool to fetch files from the internet.