镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(remote agents) hosted agent types and the owner's agent list - #870

Merged
baxen merged 3 commits into
mainfrom
baxen/remote-agents
Oct 12, 2026
Merged

baxen merged 3 commits into
mainfrom
baxen/remote-agents

Conversation

@baxen

@baxen baxen commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #874, the top of the Agents stack.

Agents could only make agents whose key lives on this device, and the agents page showed only those. This adds hosted agent types, whose provider holds the key and runs the agent, and a generic list of every agent the owner has, so each device can show agents that run elsewhere.

Hosted types

A type with remote steps is hosted. It has no run, models or timeoutMs, and its agents have no attention.

remote: {
  provision({ name, community, signal }) → { pubkey }  // provider makes the key
  activate({ pubkey, name, community, auth, signal })  // gets the owner's NIP-OA tag, publishes the profile, starts it
  profile({ pubkey, community, name, picture?, signal })
  remove({ pubkey, community, signal })                // idempotent
}
  • Create: the app runs provision, signs NIP-OA through prepareRemoteAgentAuthorization, runs activate, then lists the agent.
  • Failed setup: if anything after provision fails, the agent is kept as Setup unfinished, across restarts too. Finish setup continues it without making a second agent.
  • Rename and picture: these go through profile, then the listing is updated. The picture must be an HTTPS URL.
  • Settings: a hosted agent's settings are the type's own tabs calling its provider; save(config) rejects for it.
  • Delete: the provider removes the agent first, then it is archived, leaves its channels and its record is deleted. If the provider fails, the agent stays listed so Delete can be tried again.
  • Concurrency: setup and delete of one agent never run at the same time.

The owner's agent list (kind 30177)

  • Format: every agent, here or hosted, is listed in an owner-signed 30177 record with ["runtime", "<pluginId>/<typeId>"].
  • Compatibility: other desktops apply a 30177 whole, and their parser requires parallelism and respond_to (docs/relay-queries.md). So a record keeps every setting and tag of the record it replaces, and defaults those two to 1 / owner-only.
  • How each device shows it:
    • The key is on this device: here.
    • The runtime is a hosted type: hosted.
    • Anything else: On another device, read-only, never run here, and no Delete.
    • Records without a runtime belong to earlier systems and stay hidden.
  • Reads: records are read once per community connect, and a failed read is retried on the next sync. Listings written while a read is in flight survive it.

Teams stay local-only, so hosted agents can't join one yet.

Trying it

The example ack-bot plugin and its localhost provider live in a separate draft PR stacked on this one (#875), kept for reference and not for merging.

Testing

  • tsc and biome are clean.
  • New service tests cover: listing, the elsewhere view, hosted create, unfinished/finish/restart, rename, delete order and failure, unfinished delete, setup vs. delete, keeping a prior record, read/write race and retry, and HTTPS pictures.
  • New tests also cover publishAgentRecord and the page.
  • The full vitest suite passes on top of feat(agents2): set no deadline on an agent's work #874 (8263 tests).
  • baxen ran the hosted flow end to end in the desktop app with the ack-bot example: create, then an owner mention answered in its thread.

Not here yet

  • Builderlab on this contract.
  • The claude-code → claude-local rename, which changes the published runtime too; the next sync relists agents under the new key.
  • Live 30177 updates from other devices; for now they show on the next connect.

🤖 Generated with Claude Code

@baxen
baxen force-pushed the baxen/remote-agents branch from 3628fee to 4ba12f7 Compare October 12, 2026 01:18
@baxen baxen changed the title Agents2: hosted agent types and the owner's agent list Agents: hosted agent types and the owner's agent list Oct 12, 2026
@baxen
baxen changed the base branch from baxen/channel-templates to baxen/agent-no-time-limit October 12, 2026 01:18
@baxen baxen changed the title Agents: hosted agent types and the owner's agent list feat(remote agents) hosted agent types and the owner's agent list Oct 12, 2026
@baxen
baxen marked this pull request as ready for review October 12, 2026 04:49
@baxen
baxen requested a review from a team as a code owner October 12, 2026 04:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T04:53:36.682777Z 4ba12f7 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ba12f7d5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1069 to +1071
originOf(identity.relay) !== origin ||
this.listing.has(identity.pubkey)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile after an in-flight listing write

When a local agent is renamed or repictured while its previous 30177 publication is pending, the refresh reaches this guard and skips the newer state; after the old publication settles, the finally handler only clears this.listing and never calls list again. Until another refresh or reconnect happens, the relay record—and therefore other devices—continues showing the stale name or picture. Re-run reconciliation after clearing the in-flight marker.

AGENTS.md reference: AGENTS.md:L126-L129

Useful? React with 👍 / 👎.

Comment on lines +894 to +895
private async saveHosted(pubkey: string, change: AgentChange) {
const { agent, remote, origin, binding } = this.hosted(pubkey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize hosted profile saves with deletion

When a user edits the name and immediately clicks Delete, the input blur can start saveHosted while removeHosted runs because profile saves do not use the per-agent exclusion. If the profile call or subsequent 30177 publication completes after removeRelayAgent, writeListing recreates the just-deleted record and leaves a ghost hosted agent. Serialize profile saves through the same per-pubkey lifecycle guard as setup and deletion.

AGENTS.md reference: AGENTS.md:L126-L129

Useful? React with 👍 / 👎.

Comment on lines +740 to +743
if (
pubkey === binding.viewer ||
this.identities.some((identity) => identity.pubkey === pubkey)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject keys already present in the owner inventory

If a provider returns a pubkey already represented by a hosted/elsewhere 30177 record or an unfinished setup, this check accepts it because it examines only native identities. Setup then activates that key and overwrites the existing record's name and runtime, effectively replacing an existing agent instead of creating one. Validate the returned key against loaded and in-flight owner listings plus unfinished setups before persisting or activating it.

AGENTS.md reference: AGENTS.md:L126-L129

Useful? React with 👍 / 👎.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four additional changes needed: preserve listings across read failures, order replacement records, exclude hosted types from native import, and retain focus after setup. Details and smallest fixes are inline. The existing in-flight listing and profile/delete race reports also remain applicable.

Star Lord automated source review via Wes’s account. Head 4ba12f7d5f2b2c34e9516e11fa440c789e2637ec; base 7e0ffeb6730396dfc357547eb3c8f5526fc3146f. Source-only: no tests, app/provider execution, or CI validation. Inspected changed source, commit metadata and PR description; no attached images in the description.

Comment thread src/features/agents/service.ts Outdated
Comment on lines +1117 to +1119
this.edits.get(origin)?.set(pubkey, row);
const listings = this.listings.get(origin);
if (!listings) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep accepted writes when the initial listing read fails

After readListings fails, it removes edits without installing listings. A hosted create/Finish setup can still succeed at the provider and relay, but setRow then stores nothing; setUp deletes the unfinished entry and throws because find returns nothing. A write completed during a read is also lost if that read subsequently rejects and discards its edits. The running agent disappears from the local inventory until another successful sync. Preserve locally accepted rows independently of read success, and merge them into the next successful read; cover both failure orderings.

Comment on lines +103 to +104
const id = outbox.send(
{ kind: 30177, content: JSON.stringify(content), tags },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Make replacement agent records strictly newer

This send has no ordering information from the record it replaces. The outbox uses whole-second wall-clock timestamps for kind 30177; its monotonic replacement handling covers only 30175/30178 (catalog-envelope.ts). Two quick name/picture updates can therefore share a timestamp, and newer selects the lower event ID rather than the last edit. writeListing nevertheless caches the last write as current, so reconnect/another device can restore the older value without reconciliation correcting it. Retain the observed record timestamp and ensure each 30177 replacement advances beyond both observed and local writes; add same-second replacement coverage.

Comment on lines +826 to 828
const kind = this.types().find((entry) => entry.key === type);
if (!kind) throw new Error("That agent type is not available");
const settings = defaults("", kind);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Exclude hosted types from the native import path

BringOver still receives every registered type and offers each in its As selector. Selecting a new hosted type reaches this shared add path via port, which accepts it and imports the legacy key natively without calling the provider. The result is shown as here, but hosted types cannot have run, so the imported agent never answers; another device misclassifies its published runtime as provider-hosted even though the provider never received it. Filter hosted types out of Bring over and reject them at the native port/add boundary before saving the identity.

Comment thread src/bundled/agents/AgentsPage.tsx Outdated
Comment on lines +452 to +455
<AgentDelete agents={agents} agent={agent} onRemoved={onClose} />
</>
) : (
<Button variant="primary" onClick={onBuild}>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve keyboard focus when Finish setup succeeds

When agents.finish updates this agent to finished, this conditional replaces the fragment containing the focused Finish setup button with a different Open button. AgentPeek has no focus handoff, so keyboard activation loses its focused control on success instead of allowing the user to continue into the agent. Keep the action mounted across this transition, or explicitly transfer focus to Open only if it was still on Finish setup. This follows DESIGN.md’s States, feedback, and motion rule; the new page test mocks finish without changing the snapshot, so it never exercises the transition.

@baxen
baxen force-pushed the baxen/agent-no-time-limit branch from 7e0ffeb to 16bfdda Compare October 12, 2026 18:35
@baxen
baxen requested a review from a team as a code owner October 12, 2026 18:35
@baxen
baxen force-pushed the baxen/remote-agents branch from 4ba12f7 to 31cfcb6 Compare October 12, 2026 18:38
Base automatically changed from baxen/agent-no-time-limit to main October 12, 2026 18:40
baxen and others added 2 commits October 12, 2026 11:51
A type with `remote` steps (provision, activate, profile, remove) is
hosted: its provider holds the key and runs each agent. The app makes one
by asking the provider for its key, signing the owner's NIP-OA attestation,
handing that over, then listing it. A setup that fails partway is kept as
unfinished, across restarts, and Finish setup continues it without making
another. Rename and picture go through the provider; a hosted agent's
settings are its type's tabs calling its provider.

Every agent is listed in the owner's kind 30177 records with its type as a
`runtime` tag, so each of the owner's devices shows it here, hosted, or on
another device. A record keeps the settings and tags of the one it
replaces and defaults parallelism and respond_to, as other desktops parse
it whole. Records without a runtime are earlier systems' and stay hidden.
Agents on another device are read-only and never run here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
- Bring over offers only types that run here, and `add()` refuses a
  hosted type, whose key stays with its provider.
- A rename made while an agent's record is being written is listed once
  that write finishes.
- A hosted agent's profile save takes the same per-agent turn as setup
  and delete, so a late save cannot relist a deleted agent.
- A key from the provider that the owner already lists is refused.
- Records written before the community's records are first read are
  kept through a failed read and merged into the next one.
- Focus moves from Finish setup to Open when the setup finishes.

Co-authored-by: claude00 <fb8b76a01257932b8ca35060862c29bd09b2137638573731c2d5074fc02c5385@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
@baxen
baxen force-pushed the baxen/remote-agents branch from 31cfcb6 to 1796a8f Compare October 12, 2026 18:58

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two changes remain: the existing kind-30177 ordering finding is still applicable, and the new setup-focus handoff can steal focus after the user moves elsewhere (inline). The read-failure preservation, native-import exclusion, in-flight relisting and profile/delete serialization fixes address their earlier reports in source.

Star Lord automated source review via Wes’s account. Head 1796a8f6c8d4360c1e67e12ab41fb30352abb00c; base 8c151a238855802bde7be2ea65ea377c1b31002c. Source-only: no tests, app/provider execution or CI validation; rendered behavior remains unverified.

Comment on lines +426 to +429
useEffect(() => {
if (agent.unfinished || !handoff.current) return;
handoff.current = false;
openButton.current?.focus();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Cancel the setup focus handoff when the user moves away

handoff.current records focus only when Finish setup is activated. If the provider takes time and the user tabs or clicks to another surviving control (for example the peek’s Close button or a header action), it stays true; this effect then pulls focus back to Open on completion. This fixes lost focus but overrides the user’s newer focus choice. Keep the action mounted across the label change, or cancel the pending handoff when focus leaves it before completion. Add a deferred-setup test that moves focus away before resolving and verifies that it stays there, alongside the existing success case. This preserves the focus behavior required by DESIGN.md’s States, feedback, and motion guidance.

…list

The fixture relay only knew the profile panel's combined 30175/30177
read, so the new `{ authors: [viewer], kinds: [30177], limit: 500 }`
read failed the unexpected-request check in plugin-import and updates.

Co-authored-by: claude00 <fb8b76a01257932b8ca35060862c29bd09b2137638573731c2d5074fc02c5385@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two changes still needed: strict ordering for replacement agent records and cancelling the setup focus handoff when the user moves away. Both production paths are unchanged. No additional finding in the fixture-only follow-up: its exact filter assertion matches the owner-list query without relaxing the existing unexpected-request checks.

Star Lord automated source review via Wes’s account. Head d2ba7a65383a83bcb51ef6eb8aae26fa6285a86c; base 8c151a238855802bde7be2ea65ea377c1b31002c. Source-only: no tests, app/provider execution or CI validation; rendered behavior remains unverified.

@baxen
baxen merged commit 287a557 into main Oct 12, 2026
20 of 23 checks passed
@baxen
baxen deleted the baxen/remote-agents branch October 12, 2026 19:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants