Design Racing
Labs Knowledge 中文
  1. A Methodology for Porting an Agent Harness to Constrained Platforms — Android as a Case Study
  2. 1. Methodology at a Glance
  3. 2. Difference Inventory: Turn "Platform Assumptions" into a Table
  4. 3. Abstraction Layer: Slice Along the Axis of Change
  5. 3.1 The two axes must be kept separate
  6. 3.2 Four design disciplines
  7. 4. Contractualization: Nail Down the Invariants
  8. 5. Reliability Layering: State / Side Effects / Recovery
  9. 5.1 State layer: make every step recoverable
  10. 5.2 Side-effect layer: define the crash window, then plug it
  11. 5.3 Recovery layer: decision and mutual exclusion
  12. 6. Resource Governance: Tiered Budgets and Leases
  13. 7. Verifiability: The Decision Basis Comes Before the Implementation
  14. 8. Anti-Pattern List
  15. 9. Checklist for Migrating to the Next Platform
  16. Closing
Author: 小檬 XMeng · 墨迹 MojiPublished: Oct 14, 202618 min read

A Methodology for Porting an Agent Harness to Constrained Platforms — Android as a Case Study

Moving an Agent runtime that works well on a desktop or server to mobile is usually hard not because of the model, but because of the total collapse of platform assumptions:
processes no longer live forever, storage is no longer freely writable, the runtime is no longer complete, background execution is no longer free, and the tool surface is no longer provided by the operating system.

This article does not drill into any single product's implementation. Instead, it distills a real round of Android porting (extracting the ReAct core of a microkernel Agent framework into a cross-platform Harness) into a reusable methodology: how to inventory the differences, how to slice the abstraction layer, how to design reliability, how to verify it, and which anti-patterns are guaranteed to bite.


1. Methodology at a Glance

Platform porting tends to fail when done "discover-as-you-write": you find only at the very end that some assumption cannot hold on both platforms at once.
We converged on five steps, and the order itself is the method:

Step What to do Deliverable
① Difference inventory Write down the runtime's implicit assumptions one by one, instead of writing code first Platform capability matrix (which doubles as the abstraction layer's requirements list)
② Abstraction slicing Slice along the "axis of change": platform capability / domain form A set of interfaces with zero platform types + host adapters
③ Contractualization Write "what must always hold" as contracts and invariants Interface contract document + safe defaults
④ Reliability layering Design separately around "state / side effects / recovery" instead of vaguely "adding exception handling" Three-layer mechanisms (snapshots, pre-write records, tiered recovery)
⑤ Verifiability by design The decision basis comes before the implementation; gates are written together with their boundaries Pure-function decisions, contract-chain tests, build-time gates

One criterion runs through all five steps: when you switch hosts, do you change the adapter or the core?
If only the adapter needs changing, the abstraction layer is sliced correctly.


2. Difference Inventory: Turn "Platform Assumptions" into a Table

Don't start from code structure. List the implicit assumptions the runtime depends on and interrogate each one:
Does this assumption still hold? If not, what is the replacement? Which layer should own the replacement?

Taking Android relative to desktop (PC/JVM) as an example, the general dimensions are these nine:

Assumption dimension Desktop default Reality on a constrained platform Which layer the replacement lands in
Process lifetime Long-lived; a crash is an anomaly Being killed is routine (background reclaim, memory pressure, vendor power-saving policies) Reliability layer: recoverable snapshots and side-effect accounting
Storage semantics Any path is readable/writable and externally visible Private directories + scoped storage; a directory that is not externally visible equals "not delivered" Platform layer: logical path → physical path mapping and visibility policy
Runtime completeness Complete standard library Missing higher-version APIs, and release builds strip classes Platform layer: capability probing + degradation + truthful reporting
Background execution Essentially unrestricted Can be suspended, delayed, launch-restricted Host layer: task-level leases, watchdog, user authorization guidance
Capability surface The OS provides a full toolchain Capabilities are mostly app-defined (registry implementations or OS subsets) Domain layer: tool protocol + truthful reporting of capability gaps
Human interaction You can ask a person anytime Often no one is present (locked screen, background, unattended) Platform layer: a confirmation gate that must default to deny
Resource budget Relatively generous Battery, memory, and data are all tight All layers: tiered budgets and truncation strategy
Extension mechanism Cross-platform Dynamic loading is mostly platform-proprietary Domain layer: keep exactly one cross-platform extension point
Device consistency One system, one behavior The same APK may behave differently on different ROMs Host layer: real-device inspection and canary delivery

The point of this table is not to "reference" but to serve as a requirements list: each row's "where the replacement lands" becomes a capability in the abstraction layer.
The earlier the inventory is done, the less rework later — most of the problems we hit were, at bottom, some row of assumptions that was never written down.


3. Abstraction Layer: Slice Along the Axis of Change

3.1 The two axes must be kept separate

The most common mistake in platform porting is mixing two kinds of change into one abstraction:

The consequence of mixing axes is very concrete: change one tool form and you must change the platform implementation; change one platform and you must rewrite the tool layer.
The correct slice is inject capabilities as an aggregate value object, isolate form behind an execution protocol, with the core recognizing only interfaces.

3.2 Four design disciplines

  1. Inject capabilities; do not use global singletons. Global mutable state makes it impossible to run two independent instances safely in the same process
    (multi-tenancy and test isolation all break). Every new capability is added as a replaceable field on the aggregate root, with a default value.
  2. The core stays zero-platform-type, enforced by a build-time gate. But a gate always has boundaries: for example, one that only matches import statements
    will miss fully-qualified calls and indirect references. The boundary must be written into the documentation — otherwise the next person reads "gate is green"
    as "zero platform dependencies", which is exactly the most dangerous misunderstanding.
  3. New capabilities must come with safe defaults: confirmation-type capabilities default to deny, storage-type capabilities default to an in-memory implementation,
    dangerous capabilities default to off. The selection criterion for a default is "nothing goes wrong when it's misconfigured", not "convenient for running the demo".
  4. Keep the minimum usable surface as small as possible: push required items to the minimum (for example, requiring only "filesystem + path resolution"),
    leaving everything else with a safe default. The more required items, the more error-prone integration becomes.

4. Contractualization: Nail Down the Invariants

The abstraction layer answers "can it be connected"; contracts answer "will it go wrong once connected". Five kinds are worth writing as contracts:

① The directionality of safe defaults. Operations that require a human decision to authorize must be denied when no one is present,
and you must distinguish three outcomes — "the user explicitly denied", "no one to ask", "timed out undecided" — because their audit semantics and their wording to the model all differ.
Turning the confirmation gate into "always allow" makes the tests all green, but that is a security hole, not a shortcut.

② Consistency of the side-effect protocol. A tool executor does not express business failure by throwing (it uses a result object),
because an exception escalates "tool failure" into "loop failure"; but a cancellation exception must propagate upward as-is, otherwise the "stop" semantics break.
Rules like this belong in the interface contract, not left for the implementer to guess.

③ A minimal state machine. For example, "only in-progress tasks can be resumed, terminal states cannot",
and "the terminal state is written exactly once (closed out in finally)". The smaller the state machine, the smaller the surface for error.

④ Idempotency and repair. Duplicate writes under the same identifier must preserve the existing state (both concurrency and replay will trigger this);
before writing, an append-only log must confirm the integrity of the previous record (if it crashed mid-write, the new record sticks to the residual line and both are ruined together).

⑤ Order is semantics. Side-effect accounting must "write before execution, write after execution" — skip a step and it degrades into after-the-fact accounting,
leaving the crash window wide open; if a termination branch in a loop changes from "return" to "break", it will keep executing the wrap-up code after the loop
and silently overwrite a conclusion already reached — order problems like these can only be held by contracts and tests.

There is also a meta-contract: reliability-enhancing components must not become a new point of failure.
A failed snapshot write or a failed log write should not interrupt the main task, but it must leave a visible reporting channel (silence by default is acceptable;
silent and unobservable is not).


5. Reliability Layering: State / Side Effects / Recovery

The single fact that "the process can die at any time" must be handled by three layers separately. Missing any one leaves a real loss:

5.1 State layer: make every step recoverable

5.2 Side-effect layer: define the crash window, then plug it

Tool side effects cannot be rolled back, so "record after execution" is never enough:

5.3 Recovery layer: decision and mutual exclusion


6. Resource Governance: Tiered Budgets and Leases

Mobile resource governance has two general practices:

① Budgets are layered, and each layer's truncation behavior must be specified.
The same "output too large" problem has different caps and different handling at each layer (annotated truncation / silent truncation / spill to disk with a path).
Without that uniformity, the troubleshooter can never say clearly "which layer the output was lost at".
A practical default strategy is: keep recent content complete, compress old content into summaries, and store over-limit content off-screen with a searchable location.

② Keep-alive is not "hold the lock always" but "hold the lock only while active".
Build keep-alive as a task-state-driven lease, where every parameter has a reason rather than a copied rule of thumb:

Parameter How to choose the value
Idle release Covers the interval between task wrap-up and a user's follow-up question; a fixed long-held lock can differ in power draw by an order of magnitude
Single lease term Must be re-decided on expiry; never hold the lock indefinitely until the battery dies
Renewal threshold Leaves enough safety margin to guarantee the watchdog period is always smaller than it
Watchdog In step with the system's existing wakeups (adding no extra wakeups), and confirms "there is really work" on every wake
Exact timing Falls back to permission-free inexact timing without exact-alarm permission — the latter also penetrates power-saving mode, just with relaxed precision

Three more boundaries: no pre-held lock on cold start (zero power draw when there is no task); permission guidance is done once and keeps a manual entry;
all policies are extracted into pure functions with time passed in explicitly, so they can be tested without load.

③ Observability must answer "is it worth it", not "does it look fast".
The metrics must include the historical worst value and the most recent value — averages are diluted by "most steps being fast",
while real-device jank comes precisely from the worst step. Instrumentation itself obeys one discipline: only increment; never change control flow.


7. Verifiability: The Decision Basis Comes Before the Implementation

Platform porting has several verification requirements that differ from ordinary business development:


8. Anti-Pattern List

The most practical output of a methodology is a "don't do this" list. These are the ones actually encountered in this round of porting:

Anti-pattern Consequence Correct approach
Two copies of the same abstraction layer, relying on "remember to sync" Silent semantic divergence; tests don't go red Single source of truth; moving is moving, not copying
Dangerous capabilities on by default, or a "self-service enable" meta-tool The model can escalate its own privileges; the switch is meaningless Off by default + structured enablement guidance + explicit user enabling
Writing the confirmation gate as "always allow" or default-allow High-risk operations execute silently in unattended scenarios fail-closed, and distinguish denied / no one to ask / timed out
Reliability components themselves throwing A full disk or insufficient permission becomes a new crash source Snapshot/log failure does not interrupt the main task, but is observable
Recording side effects only "after execution" Duplicate execution inside the crash window (double writes, double sends) Account before execution + on recovery only prompt for verification, forbid automatic replay
Inferring identity from filesystem timestamps Lossy mapping causes crossed records / crossed sessions Content is authoritative; filenames are only for locating
Path safety done only with string prefix matching .. and relative paths can bypass it Real-path normalization + resolve against the working directory + no decision without a baseline
The same decision wired to only one path Another path bypasses the protection entirely A single decision entry point shared by all paths
Directly, statically referencing higher-version APIs Build-time stripping errors, missing classes at runtime Capability probing + degradation + truthful reporting
Evaluating performance with averages The worst case is concealed Record the historical worst and the most recent value
Claiming the chain works after unit tests alone Cross-component keys/reading conventions/defaults disagree Contract-chain integration tests
Incomplete gate coverage with no documented boundary A green gate ≠ no platform dependencies Cover all subpackages + document the blind spots
Global singletons carrying mutable state Multi-instance / test isolation breaks Capability aggregate-root injection
Treating "the process lives long-term" as a default premise A long task restarting from scratch the moment it breaks Every step recoverable + tiered resume

9. Checklist for Migrating to the Next Platform

Distilling the sections above into a checklist you can follow directly:

  1. List the runtime's implicit assumptions as a table, each row stating clearly "which layer the replacement lands in when it doesn't hold";
  2. Slice the abstraction layer along the two axes of "platform capability / domain form", keeping the core free of platform types;
  3. Give every platform capability a safe default; push required items to the minimum;
  4. Write the five kinds of invariants as contracts: confirmation, side effects, state machine, idempotency, order;
  5. Implement reliability in the three layers of state / side effects / recovery; missing one does not count as done;
  6. Set snapshot granularity by "worst-case loss" and do tiered truncation (explicitly exempt protocol-required fields);
  7. Make the recovery decision a pure function; do cross-process mutual exclusion with a "file + read-back + in-process monitor" combination;
  8. Justify every keep-alive parameter, and prepare a degradation path for exact timing;
  9. Set budgets per layer for output and context, and specify the truncation behavior (annotated / silent / off-screen);
  10. Observability must include the worst value; instrumentation must not change control flow;
  11. Make decision logic pure-functional and the clock explicit; test boundaries with a fake clock rather than real waiting;
  12. Add contract-chain tests across components, and do destructive verification of critical decisions;
  13. Have build-time gates cover all subpackages, and write the blind spots into the documentation;
  14. Bring text shown to the model (prompts / docs / index) and the implementation into static consistency scanning;
  15. Explicitly declare the boundaries of real-device verification and the substitutes (inspection + canary + user feedback).

Closing

Moving an Agent onto a new platform tests not the model's capability, but how much engineering respect you have for platform reality:
processes die, so every step must be recoverable; background execution is limited, so use leases instead of long-held locks;
permissions are requested at runtime, so "declared" does not equal "available"; the capability surface is app-defined, so the text shown to the model must be in sync with the implementation;
paths, libraries, and inter-process communication all differ from the desktop, so platform capabilities should go through injection from day one, rather than being hard-coded.

In the end there is only one criterion: when you switch hosts, are you changing the adapter, or the core?

AI