How Task‑Space Hand‑Off and Take‑Over Work Between Agent and User in ego‑lite
In ego‑lite, control over a browsing context is transferred between agent and user via three declarative helpers—handOffTaskSpace, takeOverTaskSpace, and completeTaskSpace—that manage overlay visibility, ownership state, and cleanup.
A task space in citrolabs/ego‑lite represents an isolated browsing session that can be owned either by the autonomous agent (with overlay UI visible) or by the human user (overlay hidden, full manual control). The runtime exposes a clean API for switching this ownership without dropping into low‑level Chrome DevTools Protocol (CDP) calls.
Understanding Task‑Space Ownership
Before diving into hand‑off mechanics, you need to understand how ego‑lite tracks who controls a space.
According to the source code in package/ego-browser/src/helpers.ts (lines 18‑20), ownership is classified as follows:
"agent"or"agentDelegatedToUser"→ agent‑owned (agent retains authority)"user"→ user‑owned (user has full control)
// helpers.ts#L18-L20
const isAgentOwned = (space: TaskSpace) =>
space.ownership === "agent" || space.ownership === "agentDelegatedToUser";
This distinction determines whether hand‑off operations proceed or short‑circuit as no‑ops.
handOffTaskSpace: Returning Control to the User
The handOffTaskSpace([nameOrId]) helper transfers the current (or specified) task space to the user. When invoked:
- If a
nameOrIdis provided, the helper first selects that space viaselectTaskSpaceIfProvided. - It checks ownership—if already user‑owned, it resolves immediately with
{ done: false, skipped: "user-owned" }. - Otherwise, it delegates to the low‑level runtime
ego.handOffTaskSpace()to hide the overlay and grant user control.
// helpers.ts#L26-L39
export async function handOffTaskSpace(nameOrId?: string): Promise<HandoffResult> {
const space = await selectTaskSpaceIfProvided(nameOrId);
if (!isAgentOwned(space)) {
return { done: false, skipped: "user-owned" };
}
await globalThis.ego.handOffTaskSpace(space.id);
return { done: true };
}
Use case: Pause automation so the user can complete a CAPTCHA, verify a transaction, or inspect intermediate results.
// Hand current space to user; overlay disappears
const result = await taskSpaces.handOff();
console.log(result); // → { done: true }
takeOverTaskSpace: Reclaiming Agent Control
The takeOverTaskSpace([nameOrId]) helper reverses the transfer, restoring the agent overlay and command authority:
- Optionally switches to the specified space.
- Invokes
ego.takeOverTaskSpace()regardless of current ownership—user‑owned spaces are automatically reclaimed.
// helpers.ts#L47-L53
export async function takeOverTaskSpace(nameOrId?: string): Promise<void> {
const space = await selectTaskSpaceIfProvided(nameOrId);
await globalThis.ego.takeOverTaskSpace(space.id);
}
Unlike handOffTaskSpace, this helper has no conditional skip logic. It forcefully regains control, making it suitable for resuming automation after human intervention.
// Resume agent work; overlay reappears
await taskSpaces.takeOver();
Detecting Control Loss: probeAgentControl and waitForAgentControl
Agents occasionally need to probe whether they still hold control without attempting a take‑over. The internal probeAgentControl function (helpers.ts#L57‑71) provides this capability:
async function probeAgentControl(): Promise<boolean> {
try {
await globalThis.ego.snapshot(); // succeeds only when overlay active
return true;
} catch (err) {
if (err instanceof EgoUserControlError) return false;
throw err;
}
}
When the user has control, ego.snapshot() throws EgoUserControlError (defined in src/ego-errors.ts), which the probe converts to false.
Building on this, waitForAgentControl (helpers.ts#L77‑88) polls until the agent naturally regains control—useful when the user signals completion through external means rather than explicit API calls.
// Poll until user returns control (without calling takeOver)
await taskSpaces.waitForAgentControl({ timeout: 60000 });
completeTaskSpace: Finishing Work and Cleanup
The completeTaskSpace(nameOrId, { keep }) helper terminates agent interaction with a space. Its behavior branches on the keep option and current ownership:
| Scenario | Behavior |
|---|---|
keep: true + user‑owned |
No‑op; resolves { done: false, skipped: "user-owned" } (user already has page) |
keep: true + agent‑owned |
Hands off to user, keeps page open |
keep: false |
Claims space if user‑owned, then closes via ego.closeTaskSpace() |
// helpers.ts#L63-L70, L104-L115
export async function completeTaskSpace(
nameOrId: string,
options: { keep?: boolean } = {}
): Promise<CompletionResult> {
const space = await getTaskSpace(nameOrId);
if (options.keep) {
if (!isAgentOwned(space)) {
return { done: false, skipped: "user-owned" };
}
await handOffTaskSpace(nameOrId); // Transfer before completing
return { done: true };
}
// keep: false — close unconditionally
if (!isAgentOwned(space)) {
await takeOverTaskSpace(nameOrId); // reclaim first
}
await globalThis.ego.closeTaskSpace(space.id);
return { done: true };
}
// Close space entirely (user cannot see result)
await taskSpaces.complete("checkout-flow", { keep: false });
// Keep page open for user review
await taskSpaces.complete("checkout-flow", { keep: true });
API Surface and Public Exposure
These helpers are exposed through the CLI API defined in package/ego-browser/src/format.ts (lines 735‑762):
// format.ts#L735-L762
taskSpaces: {
handOff: (nameOrId?: string) => Promise<HandoffResult>;
takeOver: (nameOrId?: string) => Promise<void>;
complete: (nameOrId: string, options?: { keep?: boolean }) => Promise<CompletionResult>;
waitForAgentControl: (options?: { timeout?: number }) => Promise<void>;
}
The low‑level runtime bindings in src/driver/* implement the actual CDP calls (ego.handOffTaskSpace, ego.takeOverTaskSpace, ego.closeTaskSpace), which the helpers abstract away.
Summary
-
Task‑space ownership in ego‑lite tracks whether the agent overlay is active (
"agent"/"agentDelegatedToUser") or hidden ("user"). -
handOffTaskSpacereturns control to the user, idempotently skipping if already user‑owned. -
takeOverTaskSpaceunconditionally reclaims control for the agent. -
completeTaskSpacefinalizes work with flexible cleanup—preserving pages for user review or closing entirely. -
Control detection via
probeAgentControlandwaitForAgentControllets agents gracefully handle asynchronous human‑in‑the‑loop workflows.
Frequently Asked Questions
What happens if I call handOffTaskSpace on a space the user already controls?
The call resolves immediately with { done: false, skipped: "user-owned" }. No state change occurs, and no error is thrown. This idempotent design prevents race conditions when multiple automation steps attempt to hand off control.
Can the agent forcefully take over a space without the user's permission?
Yes—takeOverTaskSpace has no conditional checks and will reclaim any space it can access. However, this is typically used after explicit user signaling (external message, webhook, or waitForAgentControl detection) rather than as a surprise interruption.
How does ego‑lite detect when the user has finished their manual task?
The runtime throws EgoUserControlError from ego.snapshot() when the agent overlay is inactive. The probeAgentControl internal helper catches this and returns false, enabling waitForAgentControl to poll until the user completes their work. Alternatively, workflows can use external coordination (webhooks, shared state, or explicit takeOver calls) to resume automation.
Is there a way to close a space without first taking it back from the user?
No—completeTaskSpace with keep: false automatically claims user‑owned spaces before closing. This ensures clean teardown without leaving dangling resources. If you want the page to remain visible to the user, use keep: true instead.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →