agents-relay
v1.0.112
Published
Durable async agent jobs coordinated through GitHub pull requests
Readme
Agents Relay
Agents Relay is a generic TypeScript runtime for durable asynchronous agent jobs coordinated through GitHub pull requests. The PR body stores the durable Job marker plus human-readable Job objective, while PR comments store durable Task and other append-friendly records; correlated terminal events are authoritative for model-backed task lifecycle transitions.
Quick start
For GitHub-backed work, create or adopt the PR through Agents Relay itself. This guarantees the durable job marker exists before any managed task is submitted.
cat > job.md <<'EOF'
# Goal
Standalone Job context. Put a Mermaid block first when a workflow/state/architecture diagram helps.
EOF
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main \
--id job-1 --title "Objective" --objective-file job.md --mode fixed
# The orchestrator decides the initial batch separately
cat > task.md <<'EOF'
# Work
Implement this phase using the Job objective as context.
EOF
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
--task-id initial --adapter codex --provider openai --model MODEL --output task-pr \
--input-file task.md
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
--task-id child --adapter codex --provider openai --model MODEL --output task-pr \
--input "Implement the described child task and publish a terminal event."
npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id job-1
npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1Task readiness is explicit: DRAFT means the durable task exists but is not executable yet; QUEUED means Relay may execute it now; RUNNING means worker execution has started. Tasks created with dependencies start DRAFT and become QUEUED when those dependencies succeed. A dependency-free task created with --draft stays held until task update --queue. Once a task has run, external waiting is represented as BLOCKED, never by returning to DRAFT.
Mutable draft/queued/retryable task fields can be changed in place without replacing the durable task identity. task update rejects RUNNING, SUCCEEDED, and CANCELLED tasks so active execution and terminal history cannot be rewritten. For example:
npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id child \
--adapter chatgpt --provider chatgpt --model gpt-5.6-sol --reasoning high \
--capabilities model,chatgpt,browser-harness
# Explicitly release a held DRAFT task
npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id child --queueJobs separate durable lifecycle from derived runtime status. Durable lifecycle is DRAFT | ACTIVE | COMPLETED | CANCELLED: a newly created unscheduled Job is DRAFT, creating executable work activates it, and scheduled Jobs are ACTIVE because the scheduler is allowed to advance them. Runtime status (IDLE, WAITING, QUEUED, RUNNING, BLOCKED, FAILED, REVIEWING, MERGE_PENDING, MERGED, or CANCELLED) is computed from Tasks, Review, PR, and schedule facts and is never persisted as lifecycle truth.
Lifecycle transition authority lives in the directly renderable canonical sources workflow/job-lifecycle.mmd and workflow/task-lifecycle.mmd; see docs/workflow-engine.md for the executable Mermaid profile and runtime boundary.
The canonical Mermaid Graph Engine is the single authoritative Job/Task lifecycle decision path. Lifecycle events, guards, deterministic mutations, and declarative effects are defined by the workflow graphs and executed through the shared graph runtime.
Job objectives and Task inputs are Markdown. Prefer --objective-file / --input-file (use - for stdin) for durable long-form context; inline --objective / --input remain for short/simple text and are mutually exclusive with the file variants. When a diagram materially clarifies the work, put Mermaid first and then Markdown detail. A newly created Job must capture enough context to finish independently of the originating chat. Its PR body contains exactly one hidden agents-relay:job:v1 metadata marker followed by the objective Markdown; there is no separate Job comment duplicating the objective. Objective and Job metadata updates rewrite that same PR body. Legacy Job-comment markers remain readable and are migrated into the PR body on the next Job save.
Jobs default to fixed. job create persists only the durable job and never
creates executable tasks. The top-level orchestrator decides whether the user's
request should start immediately and, if so, creates the first task batch
separately. Direct orchestrator-created tasks are persisted directly and are not wrapped in a synthetic Plan. A zero-task autonomous job remains idle;
reconciliation does not invent its first planner. After an autonomous job has
been explicitly started, later planning rounds may extend the work. Fixed jobs
never grow another planning round.
The durable status JSON and dashboard show the mode and planning provenance. An
autonomous runtime uses the bundled skills/agents-relay/agents/planner.agent.md by default; --planner-command is an optional override
snapshot as JSON on stdin and must return { "objective_status": "in_progress"|"satisfied", "assessment": "...", "next_tasks": [...] }.
Planner-created tasks use stable IDs and the existing parent/subtask tree, so
retries and daemon restarts do not create duplicate work.
Task producers must follow skills/agents-relay/contracts/task-job-contract.md; that shared skill contract defines story size, lifecycle events, review, and auto-merge semantics.
Worker/model routing is code-driven: Relay gathers authoritative worker/model/resource facts, applies hard gates, then can call the generic typed decide(request, contract) boundary for judgment among legal candidates. The optional Laya implementation is a transient CLI subprocess per decision (no resident model/service), with active Laya decisions and deterministic fallback on decision failure. See Worker Router, Model Router, and typed decisions.
All task producers use the same story-size contract: S≈5 minutes, M≈15 minutes, and L≈45 minutes of normal-reasoning execution, with M as the default. high reasoning doubles that overall window. ChatGPT Browser Worker start-ack timeout is derived as one third of the effective overall timeout, clamped between 1 and 5 minutes. Orchestrators and planners should choose storySize, not raw timeout milliseconds; --timeout remains only as an explicit compatibility/debug override.
Autonomous jobs can also be scheduled without a Relay database. The job marker
stores a five-field cron expression, IANA timezone, next run, and missed-run /
concurrency policy; GitHub issue labels provide the indexed discovery surface.
A repository heartbeat invokes tick at 10-minute resolution. See
Scheduled autonomous jobs for the exact
operational contract.
job create first resolves an existing open PR with the same head/base, then creates it only when needed. Re-running it is idempotent: it reuses the PR and maintains exactly one trusted agents-relay:job:v1 marker. Job creation is always create-only. Task execution options belong to task create, and the orchestrator may create zero, one, or multiple initial Tasks according to user intent.
To create a Job on an existing unmanaged PR, use the same job create action with --pr instead of --head:
npx agents-relay job create --repo OWNER/REPO --pr 6 --id job-6 --title "Existing objective"Use job update for partial mutable metadata changes on an already healthy Job marker/binding:
npx agents-relay job update --repo OWNER/REPO --pr 6 --id job-6 --priority P0 --no-auto-mergejob update validates the trusted Job marker first. Missing, malformed, duplicated, mismatched, or conflicting bindings fail with a precise diagnostic and are not repaired implicitly. The orchestrator handles exceptional repair explicitly according to the Job contract, then retries the update. job show reads one Job and job list lists managed Jobs in a repository.
Managed GitHub task create/update/show/list/retry/cancel and reconcile/serve commands also require a durable Job marker. Orchestrators do not need raw GitHub commands for normal Job creation or metadata updates: Agents Relay uses its authenticated GitHub client internally.
The original init command remains supported for backward compatibility:
npx agents-relay init --repo OWNER/REPO --pr 1 --id job-1 --title "Objective"npm publishing
Pushes to main run .github/workflows/publish.yml: install, test, choose the next npm version, pack-check, publish, and create the matching vX.Y.Z tag. Feature branches do not edit package.json versions. If agents-relay is not published yet the first release is 1.0.0; later releases increment only the patch version from the npm registry. The repository must provide an NPM_TOKEN Actions secret with permission to publish the public agents-relay package.
Normal consumers should invoke the CLI with npx agents-relay ... (or npx agents-relayd ...).
After a successful npm publish, the workflow records one idempotent agents-relay:package-published:v1 marker comment on the originating managed PR. The existing GitHub issue_comment webhook resolves that PR back to its durable Job and emits a correlated package.published events-bus event containing repository, package, version, registry, commit SHA, PR number, and webhook delivery ID. Its source identity is the GitHub delivery (github/<repository>/delivery/<delivery-id>), not a worker execution identity.
package.published is a release fact, not a service-management command. Agents Relay does not pull packages, restart PM2, or decide which local service consumes a package. An external deployment subscriber may listen for this event, verify that the exact version is pullable, update/restart the affected service, verify its running version and health, and publish its own deployment outcome event. This keeps publication, deployment, and healthy-running as separate facts and removes the need for an orchestrator thread to synchronously wait for npm readiness.
For explicit local demo/test mode:
npm install
npm run build
node dist/cli.js init --file .agents-relay.json --title "Dogfood" --id demo
node dist/cli.js task create --file .agents-relay.json --task-id hello --adapter codex --provider openai --model MODEL --output task-pr --input "Complete the described task"
node dist/cli.js reconcile --file .agents-relay.json
npx agents-relay serve --file .agents-relay.jsonOpt-in Temporary Chat quality checks
The normal npm test suite does not open a browser or send ChatGPT messages.
For an explicit live check of the one-shot Temporary Chat path, use an
authenticated browser session and run:
npm run test:chatgpt-temp-qualityThis small live suite covers two Temporary Chat submission gates: a normal text prompt and a prompt with an attachment. It does not exercise worker lifecycle events, durable-thread reuse, resume, or retry behavior.
Open the dashboard on localhost port 8787. The marker format is intentionally public and append-friendly; the CLI uses GitHubStore with the authenticated gh client in operational mode.
External product feedback uses the reusable src/feedback.ts contract. See docs/feedback-intake.md for the schema, attachment limits, privacy rules, materialization path, and provenance envelope for later Jobs/Tasks/PRs.
reconcile leases and launches ready tasks without waiting inside the runtime scheduler; the CLI waits only long enough for its launched workers to persist results. agents-relayd (or the equivalent npx agents-relay serve) is the watchdog/dashboard mode. Model-backed workers require --events nats; the event transport carries authoritative terminal worker events as well as wake/progress traffic. See docs/service.md. --file PATH is explicit local demo/test mode only.
agents-relayd without --pr/--id runs in workspace mode by default: it discovers nested Git repositories under ~/Workspace, keeps only GitHub origin remotes, and aggregates their managed PR jobs into one dashboard and worker pool. Pass --workspace PATH to use another root. --repo OWNER/REPO remains the single-repository pool mode.
For automation, Agents Relay can use GitHub App installation authentication via AGENTS_RELAY_GITHUB_APP_ID, AGENTS_RELAY_GITHUB_APP_INSTALLATION_ID, and AGENTS_RELAY_GITHUB_APP_PRIVATE_KEY_FILE (or matching CLI flags). Webhook mode is cache-first: targeted webhook reconciliation is primary, dashboard auto-refresh reads cached job state, and the broad watchdog drops to a 30-minute fallback. See docs/github-app-rate-limit.md.
V1 assumes one active runner/daemon per job. Use a distributed atomic lease before running multiple reconcilers. Do not put secrets, credentials, private prompts, or large private payloads in the PR-body Job marker or PR-comment Task markers; store only summaries and artifact references.
Codex/model-backed tasks require --provider and --model (or equivalent routing metadata) before launch. See docs/architecture.md and skills/agents-relay/SKILL.md.
XChat and other orchestrators can consume Relay's exact shared discovery implementation without duplicating filesystem search logic:
npx agents-relay context resolve --project-path ~/Workspace/agents-relay --role orchestratorThe command resolves concrete paths only for the current execution. Use --skills / --agents for explicit selections; no resolved absolute path is written into durable task state.
The minimal agent-network surface is machine-driven registration and discovery. agent-register persists an agent identity, responsibility boundary, claimed capabilities, endpoint/runtime, availability, and routing metadata in a trusted PR marker; agent-discover applies hard filters and returns evidence-backed candidates. Adapters remain runtimes, not agent identities. See docs/architecture.md for the constrained future remote submission contract.
Agent worker lifecycle
Managed tasks are descriptive work for agents, not shell commands. Use codex or chatgpt for worker tasks. Commands such as tests, build tools, Markad Vision, ffmpeg, and other CLIs are invoked by the orchestrator or by an executing agent; they are not worker adapters.
Every model-backed task declares a durable output with --output task-pr or --output file --output-path PATH. Agents Relay bootstraps workers with the managed PR URL, durable task-comment URL, job/task identity, project, output contract, and mandatory terminal-event contract; workers retrieve the task intent from GitHub rather than receiving a duplicate copy of the stored task prompt.
For Codex and ChatGPT, events are authoritative task state. Event publication uses the canonical Neo events-bus surface (events__publish for sandboxed/hosted workers, or NEO_EVENTS_EMIT for direct local workers); Agents Relay only subscribes and reconciles. Relay publishes task.process.launched only after successful adapter launch; task.started is worker-owned and must be a new, correlated start acknowledgement. Codex publishes task.process.exited when its synchronous runtime exits, while ChatGPT publishes task.process.async_exited after its submission runtime exits; neither substitutes for task.completed, task.failed, task.blocked, or task.cancelled. The ChatGPT worker-owned tab follows the configured close policy: after-start waits for the correlated start acknowledgement, never keeps it open for debug, and after-terminal waits for a new exact-task terminal event; start-ack failure closes only the owned tab after 60 seconds. The executing agent must publish exactly one correlated terminal event. Relay persists the matching durable state only from that terminal event. Model-backed tasks require an event bus and time out explicitly if no terminal event arrives.
The ChatGPT adapter uses the chatgpt-browser-worker path selected by the shared execution-context resolver; Relay does not bundle its own copy. The resolver applies project-local > Neo shared > global ~/.agents precedence at execution time, so durable tasks store names/intent rather than stale absolute skill paths. It opens a fresh Temporary Chat tab, uses the account defaults, submits the prompt, verifies acceptance, and applies --chatgpt-tab-close-policy (or AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY): after-start by default, never for test/debug, or after-terminal after a new exact-task terminal event. It never closes a user tab and does not poll for the assistant response, resume/reopen a thread, or use conversation text as the result channel. Any observed ChatGPT thread ID is diagnostic only.
npx agents-relay task create --repo OWNER/REPO --pr 5 --id job-1 \
--task-id research-ui --adapter chatgpt \
--capabilities model,chatgpt,browser-harness \
--provider openai --model MODEL \
--output file --output-path runs/research-ui.md \
--input "Research the dashboard UX and write the final report to the declared output file."GitHub PR state is authoritative for terminal lifecycle: a merged PR reconciles its managed job to COMPLETED; a closed, unmerged PR reconciles to CANCELLED and cannot launch queued work. The dashboard exposes /api/jobs and shows PR state, durable Job lifecycle, and derived runtime status separately. A merged PR reconciles lifecycle to COMPLETED; a closed, unmerged PR reconciles lifecycle to CANCELLED.
adapter: orchestrator is reserved for synchronous self-work by the exact orchestrator turn that creates the Task. Relay deliberately does not auto-start these Tasks during reconciliation. The creator must perform the work before ending the turn and terminalize the same Task with npx agents-relay task complete --task-id <id> --summary <summary> .... If work should continue asynchronously or in a later turn, use a real worker adapter instead.
For work that was completed outside the relay but must be represented truthfully in durable history, use npx agents-relay record --task-id <id> --summary <summary> --commit <sha>. Recorded tasks use the orchestrator adapter and terminal SUCCEEDED state; they do not pretend a shell/model worker executed the work. This is a repair/backfill mechanism—normal work should be created as a durable Task before execution. For managed jobs, the GitHub PR body is the canonical Job surface: hidden Job metadata marker plus the visible Markdown objective.
Automatic task troubleshooting
When an ACTIVE Job contains a current-run FAILED or BLOCKED task, reconciliation creates one P0-priority troubleshooter recovery task for that exact failed attempt. The recovery task is routed to a local Codex-capable worker with repository/tool access and runs under the bundled Troubleshooter agent contract. Troubleshooter diagnosis is event-first: it must inspect the failed task's task-scoped lifecycle event history (including ordering, duplicate/missing/late events, source and phase data) before classifying root cause or retrying.
The Troubleshooter diagnoses before retrying. A task-local or transient blocker is repaired narrowly; a shared worker/runtime/tool defect is repaired through the owning repository's P0/self-job workflow. Only after the blocker is removed does it retry the original task and verify durable progress. Historical failures from older scheduled autonomous runs do not spawn recovery work, and deterministic reconciliation does not duplicate a Troubleshooter task for the same failed attempt.
