In-app agent¶
harness-core's AgentHarness runs an agent on the device: a model binding plus capabilities
(instructions and tools) becomes a ChatBackend. Capabilities mirror
Pydantic AI Harness, with the same tool names,
arguments and behaviour, so on-device and server agents render identically.
val workspace = File(context.filesDir, "workspace")
val harness = AgentHarness(
model = ProviderProfile("openai", "OpenAI", ProviderKind.OPENAI_RESPONSES, "https://api.openai.com/v1", "gpt-5.5")
.model(apiKey = key),
capabilities = {
listOf(
FileSystem(workspace), // workspace files
Shell(AlpineSandbox(context, workspace)), // Linux sandbox at /workspace
Memory(FileMemoryStore(File(context.filesDir, "memory"))),
Planning(),
Skills.from(SkillLibrary.assets(context.assets, "skills")),
mcpServers.toolset(), // McpServerStore
)
},
localSubAgents = { listOf(LocalSubAgent("researcher", "Researches a topic", "Be thorough; cite sources.")) },
)
val chat = ChatController(backend = harness::backend, scope = viewModel.viewModelScope)
Capabilities¶
| Capability | Artifact | Tools |
|---|---|---|
FileSystem |
harness-filesystem |
read_file, write_file, edit_file, list_directory, search_files, find_files, create_directory, file_info |
Shell + AlpineSandbox |
harness-shell, harness-sandbox-proot |
run_command, start_command, check_command, stop_command |
Memory |
harness-memory |
write_memory, read_memory, delete_memory, search_memory |
Planning |
harness-planning |
write_plan, read_plan, add_task, update_task_status(es), remove_task |
WebBrowser |
harness-browser |
navigate, snapshot, click, type_text, get_text, screenshot, … |
DeviceTools |
harness-device |
device info, clipboard, calendar, contacts, location, alarms, notifications |
Speech |
harness-speech |
speak, stop_speaking |
Scheduler |
harness-scheduler |
schedule_task, list_scheduled_tasks, cancel_scheduled_task |
Skills |
ai-elements-core |
load_capability |
AskUser |
ai-elements-core |
ask_user_question |
McpToolset |
ai-elements-core |
the tools of the user's MCP servers |
A Capability is instructions plus tools; write your own by implementing it, or pass plain
AgentTools.
Sub-agents¶
localSubAgents: agents with their own instructions (and optionally another model) that the model delegates to withdelegate_task(agent_name, task). The roster is a static instruction.remoteSubAgents: ready-made agents, for example A2A agents.
Delegations render as Subagent cards with the nested run.
Skills¶
Agent Skills are folders with a SKILL.md (YAML frontmatter plus
instructions). Skills lists them in the instructions and the model loads one with
load_capability(id), which returns # Skill: <name> and the body. Load them from assets, files or
a .zip; the demo bundles the skills in the repository's skills/ folder.
Files and folders¶
FileSystem works in the app's workspace and in folders the user shares through the Storage Access
Framework (SharedFolders), mounted at /mnt/<name>. Writes ask for approval.
The browser¶
WebBrowser gives the agent a browser on an off-screen WebView, with the tools of the Pydantic AI
Harness browser: navigate, snapshot (interactive elements with aria-ref handles), click,
type_text, press_key, select_option, hover, wait_for, get_text, scroll, go_back,
go_forward and screenshot. Only http(s) pages load.
The browser presents a desktop page by default (BrowserViewport.Desktop: 1280 × 720 CSS pixels,
Playwright's default viewport, which the Harness browser uses). Desktop pages give agents more per
screen, and sites serve their full layout. The browser asks for it the way Chrome's "Desktop site"
does, with Chrome's desktop user agent at the WebView's version. Pass BrowserViewport.Mobile for
the phone layout.
Screenshots are taken at CSS-pixel resolution, so an x,y the model reads off an image is where
click("x,y") lands. The step view opens a screenshot full screen to zoom on a phone.
Screenshots go to two places:
- To the model.
screenshot(full_page?)returns the viewport, or the whole page, as a PNG for vision models, andscreenshotOnNavigate = trueadds one to everynavigate. Both follow the Harness contract: the text saysScreenshot captured. URL: …and the image goes to the model as Pydantic AIToolReturn.content. On-device model loops send it in a user message after the tool results, which works with every model API. Images over 5 MB become a short error instead. - To the user. With
screenshots = true(the default), every call that changes the page also attaches a smaller JPEG of the viewport for the agent's computer view. The element the call acted on is outlined, the way agent computer views such as Manus mark each action. The model does not get these.
A tool of your own returns an image to the model the same way, with
ToolCallContext.current()?.content(mediaType, url).
The demo's offline agent shows a scripted browser-use run without a model. Tap Browse a web page:
it opens a page, reads it with snapshot, follows a link and takes a screenshot. Each step then
appears on the agent's computer.
The Linux sandbox¶
AlpineSandbox runs Alpine Linux through PRoot, with the workspace at /workspace. The agent can
install packages with apk add and run code. The root filesystem (about 4 MB, pinned and
checksum-verified) downloads on first use. Package native libraries extracted
(jniLibs.useLegacyPackaging = true).
Scheduled runs¶
Scheduler lets the agent schedule background runs on WorkManager. Implement ScheduledAgentHost
on your Application so a run can build its agent while the app is closed (runHeadless).
Model bindings¶
The model is a ModelBinding: ProviderProfile.model(apiKey = …) for the built-in loop, or
ai-elements-koog to run the loop on JetBrains Koog. See Model APIs.