can1357/oh-my-pi · error

agent:// URL requires an output ID: agent://<id>

Error message

agent:// URL requires an output ID: agent://<id>

What it means

AgentProtocolHandler.resolve() resolves agent:// URLs to stored agent output artifacts. The output ID is taken from the URL host (url.rawHost || url.hostname); if the URL has no host component, there is nothing to look up, so the handler throws immediately. This is a malformed-URL guard, not a lookup failure.

Source

Thrown at packages/coding-agent/src/internal-urls/agent-protocol.ts:40

import { ensurePersistedRoster } from "../registry/persisted-agents";
import { applyQuery, pathToQuery } from "./json-query";
import { artifactsDirsFromRegistry } from "./registry-helpers";
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";

/**
 * Handler for agent:// URLs.
 *
 * Resolves output IDs like "reviewer_0" to their artifact files,
 * with optional JSON extraction.
 */
export class AgentProtocolHandler implements ProtocolHandler {
	readonly scheme = "agent";
	readonly immutable = true;

	async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
		const outputId = url.rawHost || url.hostname;
		if (!outputId) {
			throw new Error("agent:// URL requires an output ID: agent://<id>");
		}

		const urlPath = url.pathname;
		const queryParam = url.searchParams.get("q");
		const hasPathExtraction = urlPath && urlPath !== "/" && urlPath !== "";
		const hasQueryExtraction = queryParam !== null && queryParam !== "";

		if (hasPathExtraction && hasQueryExtraction) {
			throw new Error("agent:// URL cannot combine path extraction with ?q=");
		}

		const registry = AgentRegistry.global();
		const rootSessionFile = context?.sessionFile
			? await ensurePersistedRoster(registry, context.sessionFile)
			: undefined;
		// The caller root's canonical artifact directory (its session file minus
		// the `.jsonl` suffix) is scanned FIRST, ahead of every process-global
		// registry dir. The roster ref this refresh installs for the caller's

View on GitHub (pinned to 9690622007)

Solutions

  1. Put the output ID in the URL host position: agent://<id> (e.g. agent://reviewer_0)
  2. Use the path only for nested subagent ids or JSON extraction: agent://<id>/<child> or agent://<id>?q=<query>
  3. If building the URL in code, validate the id is non-empty before constructing it

Example fix

// before
await handler.resolve({ host: "", pathname: "/reviewer_0" } as InternalUrl, ctx);
// after
await handler.resolve({ host: "reviewer_0", pathname: "" } as InternalUrl, ctx);
Defensive patterns

Strategy: validation

Validate before calling

const outputId = url.rawHost || url.hostname;
if (!outputId) throw new Error(`agent:// URL requires an output ID: got '${url.href}'`);

Type guard

function hasOutputId(url: InternalUrl): boolean {
  return Boolean(url.rawHost || url.hostname);
}

Prevention

When it happens

Trigger: Calling resolve() with an InternalUrl whose rawHost and hostname are both empty — e.g. 'agent://', 'agent:///foo/bar', or a URL string that was parsed such that the id landed in the path instead of the host.

Common situations: Hand-writing an agent:// URL and putting the id in the path so the host ends up empty (e.g. 'agent:///reviewer_0'); constructing the InternalUrl programmatically with a missing host field; template strings that interpolate an undefined/empty id before the host.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/20973a84d1c86251. Report an issue: GitHub.