diff --git a/crates/agent-controller/README.md b/crates/agent-controller/README.md index 16cd15f54..1f57b7047 100644 --- a/crates/agent-controller/README.md +++ b/crates/agent-controller/README.md @@ -1,6 +1,6 @@ # Native agent identities -The desktop host uses this crate for Agents2's `app_agent_*` commands: identity +The desktop host uses this crate for Agents' `app_agent_*` commands: identity custody, signing, credentials and bringing over identities from earlier stores. Plugin runtimes own agent execution; this crate no longer contains a controller, supervisor, harness launcher, teams, provider defaults or bundled runtime loader. diff --git a/crates/agent-controller/src/app_agent.rs b/crates/agent-controller/src/app_agent.rs index f462807ef..2ffc1489a 100644 --- a/crates/agent-controller/src/app_agent.rs +++ b/crates/agent-controller/src/app_agent.rs @@ -1,4 +1,4 @@ -//! Identities for agents the app runs through a plugin (Agents2). They are not +//! Identities for agents the app runs through a plugin (Agents). They are not //! harness agents: there is no process, harness setting or controller record. //! Native keeps each key and its owner attestation and signs only bounded kinds, //! bounded requests to its community, and its memory. diff --git a/crates/agent-controller/src/app_agent/tests.rs b/crates/agent-controller/src/app_agent/tests.rs index 51648fddc..3be9c1b6d 100644 --- a/crates/agent-controller/src/app_agent/tests.rs +++ b/crates/agent-controller/src/app_agent/tests.rs @@ -56,7 +56,7 @@ fn owner() -> String { auth[1].clone() } fn created(dir: &std::path::Path, credentials: &Memory) -> (AppAgents, AppAgent) { - let agents = AppAgents::open(dir.join("agents2/identities.json")); + let agents = AppAgents::open(dir.join("agents/identities.json")); let prepared = AppAgents::prepare("https://relay.example/", &owner(), "p/t", "Ada").unwrap(); let auth: Vec = serde_json::from_str(&crate::secret::test_attestation(prepared.pubkey())).unwrap(); @@ -319,7 +319,7 @@ fn signs_uploads_and_memory_for_its_own_community_and_owner() { #[test] fn concurrent_writers_on_one_file_keep_every_identity() { let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("agents2/identities.json"); + let path = dir.path().join("agents/identities.json"); let credentials = std::sync::Arc::new(Memory::default()); let threads: Vec<_> = (0..8) .map(|_| { @@ -365,7 +365,7 @@ fn an_adopted_key_keeps_its_identity_picture_and_shared_saved_key() { credentials.add(&id, &key).unwrap(); set(&credentials.1.add, true); let dirs: Vec<_> = (0..3).map(|_| tempfile::tempdir().unwrap()).collect(); - let store = |n: usize| AppAgents::open(dirs[n].path().join("agents2/identities.json")); + let store = |n: usize| AppAgents::open(dirs[n].path().join("agents/identities.json")); let adopt = |picture: &str| { AppAgents::adopt( "https://relay.example/", @@ -430,7 +430,7 @@ fn an_adopted_key_keeps_its_identity_picture_and_shared_saved_key() { fn an_adopted_key_that_cannot_be_saved_withdraws_only_its_row() { let dir = tempfile::tempdir().unwrap(); let credentials = Memory::default(); - let agents = AppAgents::open(dir.path().join("agents2/identities.json")); + let agents = AppAgents::open(dir.path().join("agents/identities.json")); let key = Secret::generate().unwrap(); let pubkey = key.pubkey().to_owned(); let auth: Vec = diff --git a/crates/agent-controller/src/port.rs b/crates/agent-controller/src/port.rs index 114c8f96f..c655ecbca 100644 --- a/crates/agent-controller/src/port.rs +++ b/crates/agent-controller/src/port.rs @@ -1,4 +1,4 @@ -//! Agent identities an earlier agent system saved on this machine, which Agents2 +//! Agent identities an earlier agent system saved on this machine, which Agents //! can take over: the harness controller's library and old Buzz's. Only the //! identity and its public name, picture and description move; instructions, //! harness settings and memberships stay behind. Neither store is ever written. diff --git a/crates/agent-controller/src/port/tests.rs b/crates/agent-controller/src/port/tests.rs index 174add3b3..51bb36d26 100644 --- a/crates/agent-controller/src/port/tests.rs +++ b/crates/agent-controller/src/port/tests.rs @@ -184,7 +184,7 @@ fn takes_the_key_from_the_store_it_was_listed_in() { agent.about, ) .unwrap(); - let destination = crate::AppAgents::open(dir.path().join("agents2/identities.json")); + let destination = crate::AppAgents::open(dir.path().join("agents/identities.json")); let (agent, key) = destination.commit(prepared, &auth, &credentials).unwrap(); let reopened = destination.get(&agent.pubkey).unwrap(); let profile = reopened.profile_event(&key).unwrap(); @@ -271,7 +271,7 @@ fn imports_authored_descriptions_from_the_saved_profile_or_exact_definition() { } #[test] -fn retained_harness_artwork_survives_agents2_storage_and_signed_publication() { +fn retained_harness_artwork_survives_agents_storage_and_signed_publication() { let retained = json!({ "record": {"persona_id": "reviewer", "avatar_url": "https://old.example/identity.png"}, "definition": {"slug": "reviewer", "avatar_url": "https://old.example/definition.png"} @@ -350,7 +350,7 @@ fn retained_harness_artwork_survives_agents2_storage_and_signed_publication() { agent.about, ) .unwrap(); - let path = dir.path().join("agents2/identities.json"); + let path = dir.path().join("agents/identities.json"); let (agent, _) = crate::AppAgents::open(path.clone()) .commit(prepared, &auth, &credentials) .unwrap(); diff --git a/crates/plugin-manager/src/lib.rs b/crates/plugin-manager/src/lib.rs index 6b4ee0e3d..45b22f743 100644 --- a/crates/plugin-manager/src/lib.rs +++ b/crates/plugin-manager/src/lib.rs @@ -13,6 +13,7 @@ use std::{ }; pub mod imports; +mod migrate_agent_flag; pub type Result = std::result::Result; const LIMIT: u64 = 8 * 1024 * 1024; @@ -212,8 +213,8 @@ pub fn bundled_manifests() -> Vec { .expect("bestie manifest"), serde_json::from_str(include_str!("../../../src/bundled/projects/manifest.json")) .expect("projects manifest"), - serde_json::from_str(include_str!("../../../src/bundled/agents2/manifest.json")) - .expect("agents2 manifest"), + serde_json::from_str(include_str!("../../../src/bundled/agents/manifest.json")) + .expect("agents manifest"), serde_json::from_str(include_str!( "../../../src/bundled/claude-code/manifest.json" )) @@ -258,7 +259,7 @@ fn enabled_by_default(id: &str) -> bool { | "buzz.inbox" | "buzz.reminders" | "buzz.projects" - | "buzz.agents2" + | "buzz.agents" | "buzz.claude-code" | "buzz.codex" | "buzz.workflows" @@ -317,7 +318,7 @@ struct Registry { impl Default for Registry { fn default() -> Self { Self { - version: 1, + version: 2, bundled_enabled: true, bundled_overrides: BTreeMap::new(), installed: BTreeMap::new(), @@ -512,7 +513,7 @@ impl Manager { }; let text = read_file_limited(file)?; let mut r: Registry = serde_json::from_str(&text).map_err(err)?; - if r.version != 1 { + if r.version != 1 && r.version != 2 { return Err( "Unsupported settings version; use a compatible Buzz or recover settings".into(), ); @@ -536,6 +537,9 @@ impl Manager { valid_hash(h)?; } } + if migrate_agent_flag::migrate(&mut r) { + self.save(&r)?; + } Ok(r) } fn save(&self, r: &Registry) -> Result<()> { @@ -1560,6 +1564,54 @@ mod tests { assert!(!enabled(&manager), "{id} exposes persisted disable"); } + #[test] + fn agents_migrates_its_switch_and_ignores_v1s() { + let open = |overrides: serde_json::Value| { + let temp = tempfile::tempdir().unwrap(); + let profile = temp.path().join("profiles").join("agents"); + fs::create_dir_all(&profile).unwrap(); + let registry = serde_json::json!({ + "version": 1, + "bundledEnabled": true, + "bundledOverrides": overrides, + "installed": {}, + }); + fs::write(profile.join("registry.json"), registry.to_string()).unwrap(); + let manager = Manager::open(Some(temp.path().into()), "agents", false).unwrap(); + (temp, manager) + }; + let enabled = |manager: &Manager| { + manager + .catalog() + .unwrap() + .plugins + .into_iter() + .find(|plugin| plugin.manifest.id == "buzz.agents") + .unwrap() + .enabled + }; + let (_v1, manager) = open(serde_json::json!({ "buzz.agents": false })); + assert!(enabled(&manager), "v1's switch does not turn Agents off"); + let (_temp, manager) = open(serde_json::json!({ "buzz.agents2": false })); + assert!(!enabled(&manager), "the Agents2 switch still applies"); + manager.change("enable", "buzz.agents").unwrap(); + assert!(enabled(&manager)); + let reopened = Manager::open(Some(_temp.path().into()), "agents", false).unwrap(); + assert!(enabled(&reopened), "reopening keeps the new preference"); + reopened.change("disable", "buzz.agents").unwrap(); + assert!(!enabled(&manager), "migration does not reset later changes"); + reopened.change("enable", "buzz.agents").unwrap(); + let saved: serde_json::Value = serde_json::from_slice( + &fs::read(manager.storage_root().join("registry.json")).unwrap(), + ) + .unwrap(); + assert_eq!( + saved["bundledOverrides"], + serde_json::json!({ "buzz.agents": true }), + "the switch now uses the current plugin id" + ); + } + #[test] fn formerly_installed_agent_plugins_upgrade_to_the_bundled_copy() { let manifest: serde_json::Value = serde_json::from_str(include_str!( diff --git a/crates/plugin-manager/src/migrate_agent_flag.rs b/crates/plugin-manager/src/migrate_agent_flag.rs new file mode 100644 index 000000000..22e233295 --- /dev/null +++ b/crates/plugin-manager/src/migrate_agent_flag.rs @@ -0,0 +1,17 @@ +//! TEMPORARY agents rename: remove after 2026-10-18 with the version-1 reader. +//! Version 2 distinguishes new Agents preferences from the retired v1 plugin. +use super::Registry; + +pub(super) fn migrate(registry: &mut Registry) -> bool { + if registry.version != 1 { + return false; + } + registry.bundled_overrides.remove("buzz.agents"); + if let Some(enabled) = registry.bundled_overrides.remove("buzz.agents2") { + registry + .bundled_overrides + .insert("buzz.agents".into(), enabled); + } + registry.version = 2; + true +} diff --git a/docs/agents.md b/docs/agents.md index fd68ba284..9ee832197 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -2,7 +2,7 @@ ## Scope -Agents2 (`src/bundled/agents2`, `src/features/agents2`) is the app's only agent +Agents (`src/bundled/agents`, `src/features/agents`) is the app's only agent system: it creates, runs and deletes the viewer's agents. This document covers what the rest of the app knows about agents: shared selection, mentions, archive display, activity, names, inventory and usage. The v1 Agents page, Profiles plugin, @@ -10,6 +10,27 @@ Channel templates plugin, agent snapshots, team bundles, community catalog and agent-management request popups are gone, as are the native `agent_control_*`, model-discovery and harness-install commands behind them. +Agents was called Agents2 while v1 still shipped. Startup now moves its identity +folder to `agents/identities.json` and its browser settings to `buzz.agents.v1` +and `buzz.agents.teams.v1`. Identity bytes and keychain entries are unchanged. +The Agents2 plugin switch moves to `buzz.agents`; the retired v1 switch is +removed. Plugin registry versions distinguish these preferences (native v2, +browser v3), so a later restart preserves changes to the new switch. + +The temporary migrators are `src/features/agents/migrate-storage.ts`, +`src/plugins/migrate-agent-flag.ts`, +`crates/plugin-manager/src/migrate_agent_flag.rs`, and +`src-tauri/src/sign_out/migrate_agents.rs`. Remove them and their call sites after +2026-10-18 once development users have upgraded; remove the old registry-version +readers and the legacy sign-out keep/key-deletion entries at the same time. +This is a one-way development migration: close older Buzz copies before the +first upgraded launch and use the upgraded build afterward. Native migration +requires the existing exclusive instance lock; conflicts stop startup without +replacing either folder. A pending erase handles both folder names before the +migration runs. Browser moves write the destination before removing the source +and resume identical interrupted copies; differing destinations are left intact +and reported as migration errors. + Agent identity is evidence, never authority. Names, library rows, local agent rows, archive state and presence do not prove ownership, membership or runtime state. @@ -18,13 +39,13 @@ archive state and presence do not prove ownership, membership or runtime state. `LocalAgents` (`src/features/agents/local.ts`) is a read-only snapshot of the viewer's live agents on this machine, in any community: `{ status, agents: [{ pubkey, name, relayUrl, owner }] }`, with `status` one of -`unavailable`, `loading`, `ready` or `error`. `Agents2Service.local()` produces it -from its saved identities (deleted ones omitted), so it carries Agents2's own -loading and error state. Agents2's `snapshot()` is different: it holds only the +`unavailable`, `loading`, `ready` or `error`. `AgentsService.local()` produces it +from its saved identities (deleted ones omitted), so it carries Agents' own +loading and error state. Agents' `snapshot()` is different: it holds only the selected community's agents, with their settings. -`app/services.ts` composes consumers before Agents2 exists, through -`createLocalAgents()`. Until `bind` connects `agents2.local`, the snapshot is +`app/services.ts` composes consumers before Agents exists, through +`createLocalAgents()`. Until `bind` connects `agents.local`, the snapshot is `unavailable`; unbinding returns it there. Two consumers read it directly: - the identity-name service, which lets a local agent's name win in its own @@ -65,7 +86,7 @@ retirement revokes its candidates. `templates` serves saved channel-kit templates and section defaults (session setup resolves lineups through it with `templateAgentChoices`). Its identities are the -local agents once Agents2 is bound, and the library only while the local source is +local agents once Agents is bound, and the library only while the local source is `unavailable`. It never falls back to the library after a local error. `templateAgentChoices` returns nothing until archive evidence is ready, and library-only choices additionally need visible community membership. Saved keys @@ -314,8 +335,8 @@ the relay re-verifies consent. An owner request copies the target's single live by that re-read, and only a definitive relay rejection skips it. No profile pane offers Archive or Unarchive now; the only caller is agent deletion. -**Delete agent** on Agents2 forgets the agent's key on this device, then -(`Agents2Service.cleanup`, shared helpers in `features/agents/relay-removal.ts`) +**Delete agent** on Agents forgets the agent's key on this device, then +(`AgentsService.cleanup`, shared helpers in `features/agents/relay-removal.ts`) runs whenever its community is open: 1. An exact one-member 9001 (`h`, `p`, `client-id`) goes through the outbox for every diff --git a/docs/channels.md b/docs/channels.md index 0279a2e8f..484c0fc9e 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -260,7 +260,7 @@ The **+** action in the DMs sidebar header opens **New message**, a routed empty conversation in Messages. The header remains available before the first DM exists. Its inline **To:** field owns a paginated people picker and up to eight recipient chips, excluding this viewer. Agent profiles are offered only when their exact public key is one of the viewer's -local agents (Agents2) for this community, the `managed` choices of +local agents (Agents) for this community, the `managed` choices of `session.agentChoices`. Public profile hints and the inventory library do not qualify; this filter also applies to searches and cached results. Selected agent chips are revalidated against the current local agents before a fresh open and diff --git a/docs/codex-binding-readiness.md b/docs/codex-binding-readiness.md index 6ce8cde85..3c9024355 100644 --- a/docs/codex-binding-readiness.md +++ b/docs/codex-binding-readiness.md @@ -1,9 +1,9 @@ # Codex binding readiness -> **Status: superseded by Agents2.** This describes the v1 native Codex binding +> **Status: superseded by Agents.** This describes the v1 native Codex binding > (Settings rows, Create/Edit form, native model discovery and harness install), > which was removed with the v1 Agents system. Codex now reaches the app as an -> Agents2 agent type registered by the bundled Codex plugin. +> The agent type registered by the bundled Codex plugin. Codex is a native integration. New selection requires installed tools; Create and Save require no inference check. Settings shows tool presence and, once diff --git a/docs/codex-create-edit-ui.md b/docs/codex-create-edit-ui.md index 557647984..3d1bcbecb 100644 --- a/docs/codex-create-edit-ui.md +++ b/docs/codex-create-edit-ui.md @@ -1,9 +1,9 @@ # Codex Create and Edit acceptance -> **Status: superseded by Agents2.** This describes the v1 native Codex binding +> **Status: superseded by Agents.** This describes the v1 native Codex binding > (Settings rows, Create/Edit form, native model discovery and harness install), > which was removed with the v1 Agents system. Codex now reaches the app as an -> Agents2 agent type registered by the bundled Codex plugin. +> The agent type registered by the bundled Codex plugin. This is the current contract after the October 8, 2026 simplification. Historical validation/proof test results in earlier stack revisions do not establish this diff --git a/docs/codex-model-discovery.md b/docs/codex-model-discovery.md index 6ebb4f455..80d052da0 100644 --- a/docs/codex-model-discovery.md +++ b/docs/codex-model-discovery.md @@ -1,9 +1,9 @@ # Codex model and effort discovery -> **Status: superseded by Agents2.** This describes the v1 native Codex binding +> **Status: superseded by Agents.** This describes the v1 native Codex binding > (Settings rows, Create/Edit form, native model discovery and harness install), > which was removed with the v1 Agents system. Codex now reaches the app as an -> Agents2 agent type registered by the bundled Codex plugin. +> The agent type registered by the bundled Codex plugin. Create and Edit use headless model and effort discovery for the native Codex binding described in [Codex binding readiness](codex-binding-readiness.md). diff --git a/docs/codex-validation-execution.md b/docs/codex-validation-execution.md index 14219177e..06ac342f0 100644 --- a/docs/codex-validation-execution.md +++ b/docs/codex-validation-execution.md @@ -1,9 +1,9 @@ # Codex persistence and execution -> **Status: superseded by Agents2.** This describes the v1 native Codex binding +> **Status: superseded by Agents.** This describes the v1 native Codex binding > (Settings rows, Create/Edit form, native model discovery and harness install), > which was removed with the v1 Agents system. Codex now reaches the app as an -> Agents2 agent type registered by the bundled Codex plugin. +> The agent type registered by the bundled Codex plugin. The October 8, 2026 simplification replaces the earlier mandatory connection validation contract. Create and Save follow the existing Claude lifecycle: diff --git a/docs/communities.md b/docs/communities.md index ebcc50509..9cf670946 100644 --- a/docs/communities.md +++ b/docs/communities.md @@ -115,7 +115,7 @@ community origin and viewer; channel IDs alone are not sufficient keys. Selecting a saved membership lazily acquires its session. Sessions survive page navigation and switching; startup opens only the selected one. Once its agent -list loads, Agents2 also opens each joined community where the viewer has an +list loads, Agents also opens each joined community where the viewer has an agent, through `open`, which connects without selecting so those agents keep running, and retries one whose connection failed while it is not selected. Opened communities retain their existing per-session query/cache budgets. Arbitrary destinations mean diff --git a/docs/configuration.md b/docs/configuration.md index d003e7588..3be4bc4f4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -16,9 +16,9 @@ not in source. Build defaults are readable binary data, **never secret storage** | `BUZZ_UPDATER_PUBLIC_KEY`, `BUZZ_UPDATER_ENDPOINT` | Native release build, process environment only: two non-empty values register the updater plugin. The same public key and endpoint must reach `tauri build --config` as `plugins.updater`, and update archives must be signed by the matching private key. The macOS prerelease workflow supplies both (see [releases](releases.md)); other builds report automatic updates as unavailable. | The retired v1 inputs `BUZZ_BUILD_AGENT_ENV`, `BUZZ_BUILD_BUZZ_AGENT_PROVIDER` -and `BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY` are no longer read or compiled. Agents2 +and `BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY` are no longer read or compiled. Agents uses each plugin agent type's settings. Remove these old defaults from local/CI -configuration; they do not configure Agents2. Restart Vite to change broker or +configuration; they do not configure Agents. Restart Vite to change broker or routing configuration. Production frontend builds neither load the dev broker nor expose the auto-open seed. @@ -88,7 +88,7 @@ copied here. | Updater helper fallback aliases | Only the two canonical updater names above are read. Private signing keys never belong in app defaults. | | `--features mesh-llm` | No mesh/provider integration is supplied by the native identity crate. Import carries identities, not legacy runtimes. | | `BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT`, `BUZZ_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT` | Already no-ops in inspected OG: its archive capability checks return true for all builds. This app does not claim equivalent archive collection by accepting inert flags. | -| Build-command `BUZZ_ACP_ALLOWED_RESPOND_TO`, `BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES` | OG consumers are runtime-only; setting these around compilation does not bake them. The retired v1 listener no longer consumes these inputs; Agents2 uses its attention settings. | +| Build-command `BUZZ_ACP_ALLOWED_RESPOND_TO`, `BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES` | OG consumers are runtime-only; setting these around compilation does not bake them. The retired v1 listener no longer consumes these inputs; Agents uses its attention settings. | | Mobile `BUZZ_AGE_GATING_ENABLED`, Android `BUZZ_ANDROID_RELEASE_SIGNING=external` | No corresponding Flutter/Android target in this app. | | CI version/tag metadata, artifact paths/upload controls, signing teams/profiles, Apple/App Store/Artifactory/Play credentials, Android signer roles/sockets, SDK/JDK/toolchain settings, Tauri bundle/config arguments | Release infrastructure and packaging controls, not product defaults. No new build-variable forwarding API. | diff --git a/docs/design-system-adoption.md b/docs/design-system-adoption.md index 144b77892..a9aab15ac 100644 --- a/docs/design-system-adoption.md +++ b/docs/design-system-adoption.md @@ -73,7 +73,7 @@ Use [DESIGN.md's whole-surface recipes](../src/shared/design-system/DESIGN.md#co as the target; remove resolved gaps from this table when their migrations are verified. During PR preparation, source references were refreshed against `a6ea532d6`, including the new Me workspace. The browser observations below remain tied to -`2bea18769`; the newer Me workspace and Agents2 page were not browser-retested. +`2bea18769`; the newer Me workspace and Agents page were not browser-retested. Browser inspection covered Messages, Inbox, Reminders, Projects, Agents, Sessions, Workflows, Me, the account menu, Channel members, a profile companion panel, and diff --git a/docs/hermes-agent-research.md b/docs/hermes-agent-research.md index 98db4969d..81d1920c5 100644 --- a/docs/hermes-agent-research.md +++ b/docs/hermes-agent-research.md @@ -1,8 +1,8 @@ # Hermes Agent integration research -> **Status: superseded by Agents2.** The v1 create/edit form, Settings harness rows, +> **Status: superseded by Agents.** The v1 create/edit form, Settings harness rows, > model picker and device-defaults card this research targeted were removed with the -> v1 Agents system. A harness now reaches the app as an Agents2 agent type that a +> v1 Agents system. A harness now reaches the app as an agent type that a > plugin registers, as the bundled Claude Code and Codex plugins do. The sections > below record the research and the slice built against the removed UI; table rows > whose owners no longer exist are dropped. diff --git a/docs/identity.md b/docs/identity.md index 33cdd7ade..5fac8405a 100644 --- a/docs/identity.md +++ b/docs/identity.md @@ -32,7 +32,7 @@ isolation from other programs running as the same OS user. There is no file/environment fallback, automatic legacy migration or human key replacement. The human key is deleted from this device only by [Sign out of Buzz](#sign-out-of-buzz); no other command or plugin service can -delete it. Agents2's bring-over (`app_agent_port`) may read only the selected +delete it. Agents' bring-over (`app_agent_port`) may read only the selected old Buzz service/account and copy the selected agent key into this app's separate agent namespace; it never writes the old blob. No user key belongs in release configuration. diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md index 923f630ce..8dd2c1907 100644 --- a/docs/plugin-architecture.md +++ b/docs/plugin-architecture.md @@ -17,7 +17,7 @@ broader supported external reuse remains an [author-contract gate](status.md#ope **Rich conversation content.** An integration plugin recognizes a link to a GitHub pull request or a native Buzz object and supplies a panel showing that object. The Channels page decides where the panel appears. Another page can display the same content in a different arrangement. -Both experiences use real session capabilities. As AI integrations become available, authors should be able to consume those capabilities without rebuilding authentication, execution, or state handling. Agents2 is the app's only agent system; [shared agent selection](agents.md) gives other plugins the viewer's local agents and the known inventory through `session.agentChoices`, and mentions use current channel membership. Execution and credential handling stay blocked in the disposable editor. +Both experiences use real session capabilities. As AI integrations become available, authors should be able to consume those capabilities without rebuilding authentication, execution, or state handling. Agents is the app's only agent system; [shared agent selection](agents.md) gives other plugins the viewer's local agents and the known inventory through `session.agentChoices`, and mentions use current channel membership. Execution and credential handling stay blocked in the disposable editor. ## Ownership @@ -67,8 +67,8 @@ bundled/channels/ conversation navigation, page layout and panel placement bundled/channel-usage/ optional archive-backed Usage panel and Channels menu launcher bundled/projects/ repository/project pages, issue/PR details and Git views features/projects/ entity route/data contracts and bounded Git read bridge -bundled/agents2/ Agents2 page and agent types (harness plugins register theirs) -features/agents2/ Agents2 service: saved agents, creation, deletion +bundled/agents/ Agents page and agent types (harness plugins register theirs) +features/agents/ Agents service: saved agents, creation, deletion features/agents/ session-owned agent inventory, choices, local agents and activity bundled/github/ builtin GitHub panel plugin bundled/bestie/ builtin Bestie page @@ -215,7 +215,7 @@ contracts with their own layout and local navigation. **Channels is the only required bundled plugin.** Bestie and Todos are off by default. Feedback, Diff viewer, Identity Naming, Agent Activity, Channel Usage, Terminal, Links, Mentions, Emoji, GitHub, Me, Inbox, -Projects, Agents2, Claude Code, Codex, Workflows, Sessions, Builderlab, Hosted communities, and Community admin are +Projects, Agents, Claude Code, Codex, Workflows, Sessions, Builderlab, Hosted communities, and Community admin are on by default, but optional. Both browser and native catalogs declare that policy. Me is an empty, optional default-enabled page. Me and Channels request topbar @@ -270,7 +270,7 @@ exact registration identity and mounted lifetime revoke callbacks on removal. The channel template extension point (`ctx.channelTemplates`) was removed with the v1 Agents system. The host sidebar owns personal groups and the existing + creation buttons. Channel templates are now device-local (`buzz.channel-templates.v1`, -`features/channel-templates/local.ts`): a name, Agents2 team ids and a starting +`features/channel-templates/local.ts`): a name, Agents team ids and a starting Canvas, managed under Settings → Channel templates. Picking one in Create resolves its teams to their members that are the owner's agents in this community and sends them with the Canvas as ordinary channel setup, which the session applies and @@ -548,7 +548,7 @@ environment (`null` removes a variable); variables starting `BUZZ_` or `NOSTR_` are never inherited. The program is found on the same search path as commands, which on macOS also includes `~/.local/bin`. -`agent` names an Agents2 agent whose type the calling plugin registered. The +`agent` names an agent whose type the calling plugin registered. The process then runs as that agent, as harness agents do: native puts its key in `BUZZ_PRIVATE_KEY` (and `NOSTR_PRIVATE_KEY`), its community in `BUZZ_RELAY_URL` and its owner attestation in `BUZZ_AUTH_TAG`, and puts the bundled agent tools, diff --git a/docs/profiles.md b/docs/profiles.md index 23f627f47..40a786094 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -5,9 +5,9 @@ public identity, human or agent. It uses the current session's shared profile directory. It is read-only for people and for other users' agents; profile metadata and agent hints grant no authority. -For an Agents2 agent this app keeps, the panel also shows its type's tabs and -the app's Delete action, from `features/agents2` (`useAgent2`, `agentSections`). -Its settings, attention and identity are edited there or on the Agents2 page; +For an agent this app keeps, the panel also shows its type's tabs and +the app's Delete action, from `features/agents` (`useAgent`, `agentSections`). +Its settings, attention and identity are edited there or on the Agents page; Profiles adds no second editor, runtime view or process control. ## Agent activity @@ -124,7 +124,7 @@ Avatar, name, about, self-declared NIP-05 (not DNS-verified), exact copyable npub, and, for known agents, an optional compact activity preview/action. **Message** is offered for a foreign profile when the session can start direct messages; as in New message, a known agent qualifies only while it is one of this machine's agents in this -community (the Agents2 `LocalAgents` snapshot), rechecked on click. It opens (or reopens) the one-to-one DM through the session's direct-message operation and +community (the Agents `LocalAgents` snapshot), rechecked on click. It opens (or reopens) the one-to-one DM through the session's direct-message operation and navigates to the relay-confirmed channel in the scope captured at click time. A confirmed open also clears that DM's local sidebar hide. The person's self-published NIP-38 `general` status (kind 30315, emoji tag plus text) uses the @@ -140,7 +140,7 @@ Agent hints change avatar shape, not authority: - Profiles, mentions, participants and membership avatars use squircles for self-declared `is_agent`/`isAgent` metadata or exact keys of this machine's - agents; otherwise they use circles. Agents2 tiles always use squircles. + agents; otherwise they use circles. Agents tiles always use squircles. - Message authors also use these hints. An original kind-40002 envelope is enough on its own and keeps that treatment through edits. - Avatars never fetch an agent list or scan telemetry for these hints. diff --git a/src-tauri/src/app_agents.rs b/src-tauri/src/app_agents.rs index 12506ec6b..9aff71a60 100644 --- a/src-tauri/src/app_agents.rs +++ b/src-tauri/src/app_agents.rs @@ -1,4 +1,4 @@ -//! Native custody for agents the app runs through a plugin (Agents2). The key +//! Native custody for agents the app runs through a plugin (Agents). The key //! and owner attestation never enter the WebView; it asks for one bounded //! event, read, upload or memory write at a time, and native signs and sends it //! to the agent's community. @@ -27,7 +27,7 @@ const BUSY_WAITS: u32 = 50; const BUSY_WAIT: std::time::Duration = std::time::Duration::from_millis(100); /// The identity file (which locks its own writes across app instances) and its -/// own credential handle, so Agents2 never waits on, or fails with, the harness +/// own credential handle, so Agents never waits on, or fails with, the harness /// agent controller (whose storage lock another app may hold). #[derive(Clone)] pub(crate) struct AppAgentHost { @@ -292,7 +292,7 @@ pub(crate) async fn app_agent_create( .await } -/// Identities earlier agent systems saved on this machine, as Agents2 could take +/// Identities earlier agent systems saved on this machine, as Agents could take /// them over; the caller leaves out its own. #[tauri::command] pub(crate) async fn app_agent_portable( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cd2cf47f0..cd67abaa7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -521,10 +521,14 @@ pub fn run() { { buzz_agent_controller::delete_local_agent_keys(old, &credentials)?; } - buzz_agent_controller::AppAgents::open( - app_data.join(sign_out::AGENTS).join("identities.json"), - ) - .delete_keys(&credentials) + // The pending erase runs before the temporary startup migration. + for folder in [sign_out::AGENTS, sign_out::migrate_agents::LEGACY] { + buzz_agent_controller::AppAgents::open( + app_data.join(folder).join("identities.json"), + ) + .delete_keys(&credentials)?; + } + Ok(()) }, identity::remove_saved_key, ) @@ -600,7 +604,10 @@ pub fn run() { app.manage(app_agents::AppAgentHost::new( paths .as_ref() - .map(|(root, _)| root.with_file_name("agents2").join("identities.json")) + .map(|(root, _)| { + root.with_file_name(sign_out::AGENTS) + .join("identities.json") + }) .map_err(Clone::clone), paths .as_ref() diff --git a/src-tauri/src/sign_out.rs b/src-tauri/src/sign_out.rs index a497be4a6..16d82273d 100644 --- a/src-tauri/src/sign_out.rs +++ b/src-tauri/src/sign_out.rs @@ -32,19 +32,21 @@ const AGENT_STOPPING: &str = const ALREADY: &str = "Buzz is already signing out."; const LINKED: &str = "Erasing is unavailable because a Buzz storage folder is a link or couldn't be checked; nothing was removed"; const DEV_WIPE: &str = "Erasing is unavailable in development builds because they share agent keys and plugin storage with the installed Buzz"; -/// App data folders of the Agents2 list and of the earlier agent system, -/// whose agents Agents2 can still port. -pub(crate) const AGENTS: &str = "agents2"; +// TEMPORARY agents rename: remove after 2026-10-18; see the migration module. +pub(crate) mod migrate_agents; +/// App data folder holding the current Agents identities. +pub(crate) const AGENTS: &str = "agents"; pub(crate) const OLD_AGENTS: &str = "agent-controller"; /// One lock per running agent, held by its supervisor until the agent has /// stopped, even after Buzz exits. Shared by every Buzz, under the user data folder. pub(crate) const AGENT_OWNERSHIP: &str = "dev.local.buzz.agent-ownership"; /// What kept agents need to be identified and start again, by app data folder: -/// the Agents2 list, and the earlier system's list and defaults to port from. +/// the Agents list, and the earlier system's list and defaults to port from. /// Their keys live in the keychain. Anything else in these folders (hosting /// locks, saved logins, logs, run folders, and whatever is added later) is wiped. const KEPT: Kept = &[ (AGENTS, &["identities.json"]), + (migrate_agents::LEGACY, &["identities.json"]), (OLD_AGENTS, &["agents.json", "defaults.json"]), ]; /// The children a wiped folder keeps, each with the files it keeps. @@ -380,6 +382,7 @@ pub(crate) fn boot( locks.key.unlock().map_err(failed)?; handoff(); } + migrate_agents::migrate(paths, &locks)?; Ok(Instance { locks: Mutex::new(locks), marker: paths.marker.clone(), @@ -784,7 +787,7 @@ pub(crate) async fn sign_out( wipe, remove_agents, }; - // Agents2 agents run as plugin host processes, which stop when Buzz exits. + // Agents run as plugin host processes, which stop when Buzz exits. let shutdown = || async { Ok(()) }; let identity = app.state::().inner().clone(); let pairing = app.state::().inner().clone(); diff --git a/src-tauri/src/sign_out/migrate_agents.rs b/src-tauri/src/sign_out/migrate_agents.rs new file mode 100644 index 000000000..1339f8f62 --- /dev/null +++ b/src-tauri/src/sign_out/migrate_agents.rs @@ -0,0 +1,35 @@ +//! TEMPORARY agents rename: remove after 2026-10-18, once development users +//! have upgraded, together with the boot call and legacy erase/keep entries. +use super::{real_dir, wait_for, Locks, Paths, AGENTS}; + +pub(crate) const LEGACY: &str = "agents2"; + +#[allow(clippy::incompatible_msrv)] // File locking, like the owning sign_out module. +pub(super) fn migrate(paths: &Paths, locks: &Locks) -> Result<(), String> { + let old = paths.app_data.join(LEGACY); + let new = paths.app_data.join(AGENTS); + let failed = |error: std::io::Error| format!("Could not migrate agent storage: {error}"); + if !real_dir(&old).map_err(failed)? { + return Ok(()); + } + // Another running build may still write the old path. Reuse Buzz's shared + // instance lock rather than moving files out from under it. Our key lock + // stays shared so sign-out cannot start during the migration. + locks.all.unlock().map_err(failed)?; + if !wait_for(|| locks.all.try_lock()) { + return Err("Close other Buzz copies, then reopen Buzz to migrate agent storage. Nothing was moved.".into()); + } + // Recheck after acquiring the lock: another upgrading process may have + // completed the move while we waited. Never replace an existing directory. + if real_dir(&old).map_err(failed)? { + if real_dir(&new).map_err(failed)? { + return Err("Both agents and agents2 storage exist. Resolve the conflicting folders before reopening Buzz; nothing was moved.".into()); + } + std::fs::rename(&old, &new).map_err(failed)?; + } + locks.all.unlock().map_err(failed)?; + if !wait_for(|| locks.all.try_lock_shared()) { + return Err(super::SIGNING_OUT.into()); + } + Ok(()) +} diff --git a/src-tauri/src/sign_out/tests.rs b/src-tauri/src/sign_out/tests.rs index c18c12e74..7d936291d 100644 --- a/src-tauri/src/sign_out/tests.rs +++ b/src-tauri/src/sign_out/tests.rs @@ -137,8 +137,8 @@ fn wipe_keeps_agents_unless_they_are_removed_too() { "app/", "app/agent-controller/", "app/agent-controller/agents.json", - "app/agents2/", - "app/agents2/identities.json", + "app/agents/", + "app/agents/identities.json", ] }; assert_eq!(listing(dir.path()), expected); @@ -179,8 +179,8 @@ fn a_wipe_interrupted_part_way_resumes_on_the_next_launch() { "app/", "app/agent-controller/", "app/agent-controller/agents.json", - "app/agents2/", - "app/agents2/identities.json" + "app/agents/", + "app/agents/identities.json" ] ); } @@ -245,8 +245,8 @@ fn a_retry_clears_storage_recreated_after_the_key_was_removed() { "app/", "app/agent-controller/", "app/agent-controller/agents.json", - "app/agents2/", - "app/agents2/identities.json" + "app/agents/", + "app/agents/identities.json" ] ); } @@ -471,8 +471,8 @@ fn across_processes_a_running_instance_blocks_sign_out_and_a_later_launch_finish "app/", "app/agent-controller/", "app/agent-controller/agents.json", - "app/agents2/", - "app/agents2/identities.json" + "app/agents/", + "app/agents/identities.json" ] ); } @@ -563,8 +563,8 @@ fn an_uninspectable_kept_registry_stops_the_wipe_and_the_retry_keeps_it() { "app/", "app/agent-controller/", "app/agent-controller/agents.json", - "app/agents2/", - "app/agents2/identities.json" + "app/agents/", + "app/agents/identities.json" ] ); } @@ -677,8 +677,8 @@ fn a_sign_out_escalated_to_a_wipe_during_recovery_waits_for_every_copy() { "app/", "app/agent-controller/", "app/agent-controller/agents.json", - "app/agents2/", - "app/agents2/identities.json" + "app/agents/", + "app/agents/identities.json" ] ); } @@ -781,8 +781,8 @@ fn kept_agents_keep_only_their_list_settings_and_keys() { "app/agent-controller/", "app/agent-controller/agents.json", "app/agent-controller/defaults.json", - "app/agents2/", - "app/agents2/identities.json" + "app/agents/", + "app/agents/identities.json" ] ); assert_eq!(fs::read(controller.join("agents.json")).unwrap(), b"agents"); @@ -1255,3 +1255,78 @@ fn a_sign_out_finished_before_launch_does_not_fence_a_later_refusal() { assert_eq!(instance.begin(PLAIN), Err(refuse(OTHERS))); running.finish(); } + +#[test] +fn agents_rename_moves_once_and_retains_exact_identity_bytes() { + let (_dir, paths) = fixture(); + let old = paths.app_data.join(migrate_agents::LEGACY); + let new = paths.app_data.join(AGENTS); + fs::rename(&new, &old).unwrap(); + let instance = boot(&paths, no_agents, || panic!("no sign-out")).unwrap(); + assert!(!old.exists()); + assert_eq!( + fs::read(new.join("identities.json")).unwrap(), + b"identities" + ); + drop(instance); + fs::write(new.join("identities.json"), "updated").unwrap(); + let _reopened = boot(&paths, no_agents, || panic!("no sign-out")).unwrap(); + assert_eq!(fs::read(new.join("identities.json")).unwrap(), b"updated"); +} + +#[test] +fn agents_rename_refuses_conflicts_and_a_running_old_copy() { + let (_dir, paths) = fixture(); + let old = paths.app_data.join(migrate_agents::LEGACY); + let new = paths.app_data.join(AGENTS); + fs::create_dir(&old).unwrap(); + fs::write(old.join("identities.json"), "old").unwrap(); + assert!(boot(&paths, no_agents, || Ok(())).is_err()); + assert_eq!( + fs::read(new.join("identities.json")).unwrap(), + b"identities" + ); + assert_eq!(fs::read(old.join("identities.json")).unwrap(), b"old"); + fs::remove_dir_all(&old).unwrap(); + let running = boot(&paths, no_agents, || Ok(())).unwrap(); + fs::rename(&new, &old).unwrap(); + assert!(boot(&paths, no_agents, || Ok(())).is_err()); + assert!(!new.exists()); + assert_eq!( + fs::read(old.join("identities.json")).unwrap(), + b"identities" + ); + drop(running); + assert!(boot(&paths, no_agents, || Ok(())).is_ok()); +} + +#[cfg(unix)] +#[test] +fn agents_rename_rejects_symlinks_without_touching_the_target() { + let (dir, paths) = fixture(); + let outside = dir.path().join("outside"); + fs::create_dir(&outside).unwrap(); + fs::write(outside.join("identities.json"), "outside").unwrap(); + std::os::unix::fs::symlink(&outside, paths.app_data.join(migrate_agents::LEGACY)).unwrap(); + assert!(boot(&paths, no_agents, || Ok(())).is_err()); + assert_eq!( + fs::read(outside.join("identities.json")).unwrap(), + b"outside" + ); +} + +#[test] +fn agents_rename_runs_after_pending_erase_keeps_legacy_identities() { + let (_dir, paths) = fixture(); + let old = paths.app_data.join(migrate_agents::LEGACY); + let new = paths.app_data.join(AGENTS); + fs::rename(&new, &old).unwrap(); + mark(&paths, true, false); + let _instance = boot(&paths, no_agents, || Ok(())).unwrap(); + assert!(!old.exists()); + assert_eq!( + fs::read(new.join("identities.json")).unwrap(), + b"identities" + ); + assert!(!paths.app_data.join("archive.sqlite3").exists()); +} diff --git a/src/app/App.tsx b/src/app/App.tsx index 92f1d9961..be6a27b6e 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -3,7 +3,7 @@ import { IdentitySetup } from "../features/identity/IdentitySetup"; import { MeSidebar } from "../bundled/me/MeSidebar"; import { ChannelSidebar } from "../features/channel-navigation/ChannelSidebar"; import { ChannelNavigationProvider } from "../features/channel-navigation/ChannelNavigationState"; -import { Agents2Context } from "../features/agents2/react"; +import { AgentsContext } from "../features/agents/react"; import { ToastProvider } from "../shared/design-system/ui/Toast"; import { Button } from "../shared/design-system/ui/Button"; import { UpdateNotice } from "../features/updates/UpdateNotice"; @@ -146,7 +146,7 @@ function ConnectedApp({ services }: { services: AppServices }) { portalContainer={document.getElementById("buzz-toast-root") ?? undefined} > - + settings ? ( @@ -186,7 +186,7 @@ function ConnectedApp({ services }: { services: AppServices }) { page.key === "buzz.me/me", @@ -309,7 +309,7 @@ function ConnectedApp({ services }: { services: AppServices }) { /> ) : null} - + ); diff --git a/src/app/entity-navigation.test.tsx b/src/app/entity-navigation.test.tsx index d52baf49d..c9e58625b 100644 --- a/src/app/entity-navigation.test.tsx +++ b/src/app/entity-navigation.test.tsx @@ -34,8 +34,8 @@ vi.mock("../bundled", async () => ({ module: await import("../bundled/projects"), }, { - manifest: { id: "buzz.agents2", name: "Agents2", apiVersion: 1 }, - module: await import("../bundled/agents2"), + manifest: { id: "buzz.agents", name: "Agents", apiVersion: 1 }, + module: await import("../bundled/agents"), }, { manifest: { id: "buzz.inbox", name: "Inbox", apiVersion: 1 }, @@ -215,7 +215,7 @@ it("lists only active primary pages in the channel sidebar", async () => { within(pages) .getAllByRole("button") .map((row) => row.textContent), - ).toEqual(["Inbox", "Bestie", "Projects", "Agents2"]), + ).toEqual(["Inbox", "Bestie", "Projects", "Agents"]), ); // The fixture page is active and searchable but never a sidebar row. @@ -223,7 +223,7 @@ it("lists only active primary pages in the channel sidebar", async () => { "fixture.notes/notes", ); - await act(() => current.plugins.change("disable", "buzz.agents2")); + await act(() => current.plugins.change("disable", "buzz.agents")); await waitFor(() => expect( within(screen.getByRole("navigation", { name: "Pages" })) @@ -238,7 +238,7 @@ it("lists only active primary pages in the channel sidebar", async () => { within(search).getByRole("option", { name: "Fixture notes" }), ).toBeInTheDocument(); expect( - within(search).queryByRole("option", { name: "Agents2" }), + within(search).queryByRole("option", { name: "Agents" }), ).not.toBeInTheDocument(); await userEvent.keyboard("{Escape}"); diff --git a/src/app/navigation.ts b/src/app/navigation.ts index 58dfd09f5..d2e344d60 100644 --- a/src/app/navigation.ts +++ b/src/app/navigation.ts @@ -304,7 +304,7 @@ export function useAppNavigation(services: AppServices) { if ( (pageKey === channelsKey || pageKey === "buzz.projects/projects" || - pageKey === "buzz.agents2/agents2" || + pageKey === "buzz.agents/agents" || settings) && !state.ingress && (!failure || diff --git a/src/app/pages.integration.test.mjs b/src/app/pages.integration.test.mjs index 93c25b2f0..6bce7095b 100644 --- a/src/app/pages.integration.test.mjs +++ b/src/app/pages.integration.test.mjs @@ -470,7 +470,7 @@ test("the app runtime exposes ready bundled pages and removes them on disable", [ "buzz.channels", "buzz.reminders", - "buzz.agents2", + "buzz.agents", "buzz.inbox", "buzz.bestie", ], diff --git a/src/app/services.ts b/src/app/services.ts index a124aa1b8..53e73e626 100644 --- a/src/app/services.ts +++ b/src/app/services.ts @@ -19,7 +19,7 @@ import { } from "../features/notifications/messages"; import { AccountActionsService } from "../features/account-actions/service"; import { ShortcutsService } from "../features/shortcuts/service"; -import { Agents2Service } from "../features/agents2/service"; +import { AgentsService } from "../features/agents/service"; import { createShortcutBindings } from "../features/shortcuts/preferences"; import { ConversationService } from "../features/conversation/service"; import { createAppearance } from "../shared/theme/service"; @@ -70,9 +70,9 @@ export function createServices() { subscribe: communities.subscribe, }); const relay = communities.relay; - const agents2 = new Agents2Service(ctx, communities); + const agents = new AgentsService(ctx, communities); ctx.effect(() => - localAgents.bind({ snapshot: agents2.local, subscribe: agents2.subscribe }), + localAgents.bind({ snapshot: agents.local, subscribe: agents.subscribe }), ); const notifications = new NotificationsService( ctx, @@ -109,7 +109,7 @@ export function createServices() { navigation, navigationHost, shortcuts, - agents2, + agents, accountActions, shortcutBindings, conversation, diff --git a/src/app/shell/AppShell.test.tsx b/src/app/shell/AppShell.test.tsx index d12aaf588..2ac323069 100644 --- a/src/app/shell/AppShell.test.tsx +++ b/src/app/shell/AppShell.test.tsx @@ -107,7 +107,7 @@ it("hides and shows the channel sidebar without re-rendering its content", async expect(renders).toHaveBeenCalledTimes(rendered); // A selection change still reaches the page rows the sidebar renders. - rerender(shell("buzz.agents2/agents2")); + rerender(shell("buzz.agents/agents")); expect(renders.mock.calls.length).toBeGreaterThan(rendered); expect(row()).not.toHaveAttribute("aria-current"); }); diff --git a/src/app/shell/AppShell.tsx b/src/app/shell/AppShell.tsx index 7e01f0fd3..d7112a4ed 100644 --- a/src/app/shell/AppShell.tsx +++ b/src/app/shell/AppShell.tsx @@ -83,8 +83,7 @@ export function AppShell({ }) { const fillsWorkspace = workspace || selected === "settings"; const channelsNavigation = - selected === "buzz.channels/channels" || - selected === "buzz.agents2/agents2"; + selected === "buzz.channels/channels" || selected === "buzz.agents/agents"; const [narrow, setNarrow] = useState( () => window.matchMedia("(max-width: 650px)").matches, ); diff --git a/src/app/shell/presentation.test.ts b/src/app/shell/presentation.test.ts index ddd07fd2a..b79450e07 100644 --- a/src/app/shell/presentation.test.ts +++ b/src/app/shell/presentation.test.ts @@ -77,7 +77,7 @@ test("bundled pages get their own icons and Bestie's row shows its artwork", () expect( renderToStaticMarkup( createElement( - pagePresentation(page("buzz.agents2/agents2", "Agents2")).icon, + pagePresentation(page("buzz.agents/agents", "Agents")).icon, ), ), ).toContain("tabler-icon-robot-face"); diff --git a/src/app/shell/presentation.ts b/src/app/shell/presentation.ts index b1a76675d..9df6e71c8 100644 --- a/src/app/shell/presentation.ts +++ b/src/app/shell/presentation.ts @@ -49,7 +49,7 @@ const bundledIcons = new Map([ ["buzz.reminders/reminders", AlarmIcon], ["buzz.bestie/bestie", BestieIcon], ["buzz.projects/projects", FolderSimpleIcon], - ["buzz.agents2/agents2", RobotIcon], + ["buzz.agents/agents", RobotIcon], ["buzz.workflows/workflows", LightningIcon], ]); type PagePresentation = Readonly<{ diff --git a/src/bundled/agents2/AgentTabs.tsx b/src/bundled/agents/AgentTabs.tsx similarity index 82% rename from src/bundled/agents2/AgentTabs.tsx rename to src/bundled/agents/AgentTabs.tsx index be99e0d89..5268e8657 100644 --- a/src/bundled/agents2/AgentTabs.tsx +++ b/src/bundled/agents/AgentTabs.tsx @@ -1,16 +1,16 @@ import { useState } from "react"; -import type { ChannelChoice } from "../../features/agents2/attention"; +import type { ChannelChoice } from "../../features/agents/attention"; import type { Agent, - Agents2, + Agents, RegisteredAgentType, -} from "../../features/agents2/service"; -import { agentSections } from "../../features/agents2/tabs"; +} from "../../features/agents/service"; +import { agentSections } from "../../features/agents/tabs"; import { Tabs } from "../../shared/design-system/ui/Tabs"; /** An agent's sections as tabs, opening on the first. */ export function AgentTabs(props: { - agents2: Agents2; + agents: Agents; agent: Agent; type: RegisteredAgentType | undefined; channels: readonly ChannelChoice[]; diff --git a/src/bundled/agents2/Agents2Page.module.css b/src/bundled/agents/AgentsPage.module.css similarity index 100% rename from src/bundled/agents2/Agents2Page.module.css rename to src/bundled/agents/AgentsPage.module.css diff --git a/src/bundled/agents2/Agents2Page.test.tsx b/src/bundled/agents/AgentsPage.test.tsx similarity index 88% rename from src/bundled/agents2/Agents2Page.test.tsx rename to src/bundled/agents/AgentsPage.test.tsx index 6d0957de2..cc8924d0d 100644 --- a/src/bundled/agents2/Agents2Page.test.tsx +++ b/src/bundled/agents/AgentsPage.test.tsx @@ -7,14 +7,14 @@ import userEvent from "@testing-library/user-event"; import { afterEach, expect, it, vi } from "vitest"; import type { Agent, - Agents2, + Agents, AgentsSnapshot, RegisteredAgentType, TeamChange, -} from "../../features/agents2/service"; -import type { PortableAgent } from "../../features/agents2/native"; +} from "../../features/agents/service"; +import type { PortableAgent } from "../../features/agents/native"; import type { RelayData } from "../../features/relay/service"; -import { Agents2Page } from "./Agents2Page"; +import { AgentsPage } from "./AgentsPage"; afterEach(cleanup); @@ -66,11 +66,11 @@ const portable: PortableAgent[] = [ }, ]; -function setup(agents: Agent[], { shown = true } = {}) { +function setup(saved: Agent[], { shown = true } = {}) { let snapshot: AgentsSnapshot = { status: "ready", - agents, - running: agents, + agents: saved, + running: saved, teams: [], }; const listeners = new Set<() => void>(); @@ -88,7 +88,7 @@ function setup(agents: Agent[], { shown = true } = {}) { } as unknown as RegisteredAgentType; const created = agent("Nova", 3); const types = [type]; - const agents2 = { + const agents = { types: () => types, snapshot: () => snapshot, subscribe: (listener: () => void) => { @@ -125,14 +125,14 @@ function setup(agents: Agent[], { shown = true } = {}) { }; for (const listener of listeners) listener(); }), - } satisfies Agents2; + } satisfies Agents; const disconnected = { status: "disconnected", generation: 0 }; const relay = { snapshot: () => disconnected, subscribe: () => () => {}, } as unknown as RelayData; - render(); - return { agents2 }; + render(); + return { agents }; } it("peeks at a selected agent beside the grid and follows the arrow keys", async () => { @@ -196,26 +196,26 @@ it("shows an agent's picture in the grid, peek and build view", async () => { it("edits the picture from the build view and shows why a save failed", async () => { const user = userEvent.setup(); const picture = "https://old.example/media/a.png"; - const { agents2 } = setup([{ ...agent("Ada", 1), picture }]); + const { agents } = setup([{ ...agent("Ada", 1), picture }]); await user.click(screen.getByRole("option", { name: /Ada/ })); await user.keyboard("{Enter}"); - agents2.save.mockRejectedValueOnce(new Error("Could not save")); + agents.save.mockRejectedValueOnce(new Error("Could not save")); await user.click(screen.getByRole("button", { name: "Edit avatar" })); await user.click( await screen.findByRole("button", { name: "Remove avatar" }), ); - expect(agents2.save).toHaveBeenCalledWith("1".repeat(64), { picture: null }); + expect(agents.save).toHaveBeenCalledWith("1".repeat(64), { picture: null }); expect(await screen.findByRole("alert")).toHaveTextContent("Could not save"); }); it("makes a new agent in the side panel and goes straight to building it", async () => { const user = userEvent.setup(); - const { agents2 } = setup([agent("Ada", 1)]); + const { agents } = setup([agent("Ada", 1)]); await user.click(screen.getByRole("button", { name: "New agent" })); const form = screen.getByRole("form", { name: "New agent" }); expect(within(form).getByText("Says its line.")).toBeInTheDocument(); await user.type(within(form).getByLabelText("Name"), "Nova{Enter}"); - expect(agents2.create).toHaveBeenCalledWith({ + expect(agents.create).toHaveBeenCalledWith({ type: "test/echo", name: "Nova", }); @@ -225,11 +225,11 @@ it("makes a new agent in the side panel and goes straight to building it", async it("stays on the grid when the new agent belongs to a community no longer shown", async () => { const user = userEvent.setup(); - const { agents2 } = setup([agent("Ada", 1)], { shown: false }); + const { agents } = setup([agent("Ada", 1)], { shown: false }); await user.click(screen.getByRole("button", { name: "New agent" })); const form = screen.getByRole("form", { name: "New agent" }); await user.type(within(form).getByLabelText("Name"), "Nova{Enter}"); - await vi.waitFor(() => expect(agents2.create).toHaveBeenCalled()); + await vi.waitFor(() => expect(agents.create).toHaveBeenCalled()); expect( await screen.findByRole("listbox", { name: "Agents" }), ).toBeInTheDocument(); @@ -239,7 +239,7 @@ it("stays on the grid when the new agent belongs to a community no longer shown" it("saves a rename on Enter and discards it on Escape", async () => { const user = userEvent.setup(); - const { agents2 } = setup([agent("Ada", 1)]); + const { agents } = setup([agent("Ada", 1)]); await user.click(screen.getByRole("option", { name: /Ada/ })); await user.keyboard("{Enter}"); const field = within( @@ -247,18 +247,18 @@ it("saves a rename on Enter and discards it on Escape", async () => { ).getByLabelText("Name"); await user.clear(field); await user.type(field, "Discarded{Escape}"); - expect(agents2.save).not.toHaveBeenCalled(); + expect(agents.save).not.toHaveBeenCalled(); expect(field).toHaveValue("Ada"); await user.clear(field); await user.type(field, "Kept{Enter}"); - expect(agents2.save).toHaveBeenCalledExactlyOnceWith("1".repeat(64), { + expect(agents.save).toHaveBeenCalledExactlyOnceWith("1".repeat(64), { name: "Kept", }); }); it("brings over an earlier agent as the chosen type and drops it from the list", async () => { const user = userEvent.setup(); - const { agents2 } = setup([agent("Ada", 1)]); + const { agents } = setup([agent("Ada", 1)]); await user.click(screen.getByRole("button", { name: "Bring over" })); const list = await screen.findByRole("list", { name: "Agents to bring over", @@ -268,7 +268,7 @@ it("brings over an earlier agent as the chosen type and drops it from the list", ).toBeInTheDocument(); expect(within(list).getByText(/Buzz 0\.X · npub/)).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Bring over Bea" })); - expect(agents2.port).toHaveBeenCalledWith({ + expect(agents.port).toHaveBeenCalledWith({ source: "installed", pubkey: "b".repeat(64), type: "test/echo", @@ -279,8 +279,8 @@ it("brings over an earlier agent as the chosen type and drops it from the list", it("shows why bringing an agent over failed and keeps it listed", async () => { const user = userEvent.setup(); - const { agents2 } = setup([]); - agents2.port.mockRejectedValueOnce(new Error("Keychain access was denied")); + const { agents } = setup([]); + agents.port.mockRejectedValueOnce(new Error("Keychain access was denied")); await user.click(screen.getByRole("button", { name: "Bring over" })); await user.click( await screen.findByRole("button", { name: "Bring over Ada" }), @@ -294,7 +294,7 @@ it("shows why bringing an agent over failed and keeps it listed", async () => { it("makes a team of chosen agents and lists it with its members", async () => { const user = userEvent.setup(); const ada = agent("Ada", 1); - const { agents2 } = setup([ada, agent("Bo", 2), agent("Cy", 3)]); + const { agents } = setup([ada, agent("Bo", 2), agent("Cy", 3)]); await user.click(screen.getByRole("button", { name: "Teams" })); expect(screen.getByText("No teams yet.")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "New team" })); @@ -307,7 +307,7 @@ it("makes a team of chosen agents and lists it with its members", async () => { await user.click(screen.getByRole("checkbox", { name: "Cy" })); await user.click(screen.getByRole("button", { name: "Create team" })); - expect(agents2.saveTeam).toHaveBeenCalledWith({ + expect(agents.saveTeam).toHaveBeenCalledWith({ name: "Crew", instructions: "Ada leads.", members: [ada.pubkey, agent("Cy", 3).pubkey], @@ -318,6 +318,6 @@ it("makes a team of chosen agents and lists it with its members", async () => { await user.click(screen.getByRole("button", { name: "Edit Crew" })); await user.click(screen.getByRole("button", { name: "Delete team" })); - expect(agents2.removeTeam).toHaveBeenCalledWith("team-1"); + expect(agents.removeTeam).toHaveBeenCalledWith("team-1"); expect(screen.getByText("No teams yet.")).toBeInTheDocument(); }); diff --git a/src/bundled/agents2/Agents2Page.tsx b/src/bundled/agents/AgentsPage.tsx similarity index 94% rename from src/bundled/agents2/Agents2Page.tsx rename to src/bundled/agents/AgentsPage.tsx index e76ce42d4..1efe40d14 100644 --- a/src/bundled/agents2/Agents2Page.tsx +++ b/src/bundled/agents/AgentsPage.tsx @@ -1,4 +1,4 @@ -// The Agents2 page. Three frames, one at a time: +// The Agents page. Three frames, one at a time: // - Inventory: every agent as a tile. Selecting one (click, or arrow keys) opens // its peek beside the grid without leaving it; Enter or a double-click builds. // - Peek: identity, the type's own read-only glance, and what wakes it. @@ -13,25 +13,25 @@ import { type KeyboardEvent, type ReactNode, } from "react"; -import styles from "./Agents2Page.module.css"; +import styles from "./AgentsPage.module.css"; import { AgentTabs } from "./AgentTabs"; -import type { ChannelChoice } from "../../features/agents2/attention"; -import { AgentDelete } from "../../features/agents2/AgentManage"; -import { AttentionSummary } from "../../features/agents2/AttentionPanel"; +import type { ChannelChoice } from "../../features/agents/attention"; +import { AgentDelete } from "../../features/agents/AgentManage"; +import { AttentionSummary } from "../../features/agents/AttentionPanel"; import { AvatarEditor } from "../../features/profiles/AvatarEditor"; import { useAvatarPreview } from "../../features/profiles/use-avatar-preview"; -import { useAgents2, useChannelChoices } from "../../features/agents2/react"; +import { useAgents, useChannelChoices } from "../../features/agents/react"; import type { PortableAgent, PortableAgents, PortSource, -} from "../../features/agents2/native"; +} from "../../features/agents/native"; import type { Agent, - Agents2, + Agents, RegisteredAgentType, Team, -} from "../../features/agents2/service"; +} from "../../features/agents/service"; import { useRelayConnection } from "../../features/relay/react"; import type { RelayData } from "../../features/relay/service"; import { @@ -77,14 +77,14 @@ function summaryOf(agent: Agent, type: RegisteredAgentType | undefined) { } } -export function Agents2Page({ - agents2, +export function AgentsPage({ + agents, relay, }: { - agents2: Agents2; + agents: Agents; relay: RelayData; }) { - const { snapshot, types } = useAgents2(agents2); + const { snapshot, types } = useAgents(agents); const connection = useRelayConnection(relay); const channels = useChannelChoices( connection.status === "ready" ? connection.session.channels : undefined, @@ -110,7 +110,7 @@ export function Agents2Page({ return ( ); @@ -126,7 +126,7 @@ export function Agents2Page({ return ( browse()} onCreated={(agent) => build(agent.pubkey)} /> ) : side === "porting" ? ( - browse()} /> + browse()} /> ) : side === "teams" ? ( browse()} /> @@ -198,7 +198,7 @@ export function Agents2Page({ return (
- +
0 && snapshot.status === "ready" ? (
@@ -431,12 +431,12 @@ function AgentPeek({ } function NewAgent({ - agents2, + agents, types, onClose, onCreated, }: { - agents2: Agents2; + agents: Agents; types: readonly RegisteredAgentType[]; onClose(): void; onCreated(agent: Agent): void; @@ -451,7 +451,7 @@ function NewAgent({ setPending(true); setError(""); try { - onCreated(await agents2.create({ type: chosen.key, name: name.trim() })); + onCreated(await agents.create({ type: chosen.key, name: name.trim() })); } catch (reason) { setError(message(reason)); setPending(false); @@ -528,11 +528,11 @@ const host = (relay: string) => relay.replace(/^wss:\/\//, ""); /** Identities earlier agent systems saved on this machine, each brought over * with its key, name and picture as an agent of the chosen type. */ function BringOver({ - agents2, + agents, types, onClose, }: { - agents2: Agents2; + agents: Agents; types: readonly RegisteredAgentType[]; onClose(): void; }) { @@ -542,14 +542,14 @@ function BringOver({ const [error, setError] = useState(""); useEffect(() => { let live = true; - agents2.portable().then( + agents.portable().then( (next) => live && setFound(next), (reason) => live && setError(message(reason)), ); return () => { live = false; }; - }, [agents2]); + }, [agents]); const chosen = types.find((item) => item.key === type) ?? types[0]; const labels = publicKeyLabels(found?.agents.map((a) => a.pubkey) ?? []); const port = async (agent: PortableAgent) => { @@ -557,7 +557,7 @@ function BringOver({ setPending(agent.pubkey); setError(""); try { - await agents2.port({ + await agents.port({ source: agent.source, pubkey: agent.pubkey, type: chosen.key, @@ -654,13 +654,13 @@ function BringOver({ /** The owner's teams: each a name, instructions and members, given to every * member's prompt with a roster. Agents record nothing about their teams. */ function Teams({ - agents2, agents, + saved, teams, onClose, }: { - agents2: Agents2; - agents: readonly Agent[]; + agents: Agents; + saved: readonly Agent[]; teams: readonly Team[]; onClose(): void; }) { @@ -671,13 +671,13 @@ function Teams({ return ( setEditing(undefined)} /> ); - const names = new Map(agents.map((agent) => [agent.pubkey, agent.name])); + const names = new Map(saved.map((agent) => [agent.pubkey, agent.name])); return (
act(() => - agents2.saveTeam({ + agents.saveTeam({ ...(team ? { id: team.id } : {}), name: name.trim(), instructions, @@ -800,9 +800,9 @@ function TeamEditor({ />
- {agents.length ? ( + {saved.length ? (
- {agents.map((agent) => ( + {saved.map((agent) => ( void act(() => agents2.removeTeam(team.id))} + onClick={() => void act(() => agents.removeTeam(team.id))} > Delete team @@ -851,13 +851,13 @@ function TeamEditor({ } function AgentBuild({ - agents2, + agents, agent, type, channels, onRemoved, }: { - agents2: Agents2; + agents: Agents; agent: Agent; type: RegisteredAgentType | undefined; channels: readonly ChannelChoice[]; @@ -882,7 +882,7 @@ function AgentBuild({ community={agent.relay} onChange={(picture) => { setPictureError(""); - agents2 + agents .save(agent.pubkey, { picture: picture || null }) .catch((error: unknown) => setPictureError(message(error))); }} @@ -895,7 +895,7 @@ function AgentBuild({
agents2.save(agent.pubkey, { name })} + onSave={(name) => agents.save(agent.pubkey, { name })} />
Type
@@ -916,7 +916,7 @@ function AgentBuild({
- +
@@ -927,7 +927,7 @@ function AgentBuild({

)} { + const { agents, relay } = ctx; + ctx.pages.register({ + id: "agents", + title: "Agents", + layout: "workspace", + primary: true, + component: () => , + }); +}; diff --git a/src/bundled/agents/manifest.json b/src/bundled/agents/manifest.json new file mode 100644 index 000000000..6fe14d876 --- /dev/null +++ b/src/bundled/agents/manifest.json @@ -0,0 +1 @@ +{ "id": "buzz.agents", "name": "Agents", "apiVersion": 1 } diff --git a/src/bundled/agents2/index.tsx b/src/bundled/agents2/index.tsx deleted file mode 100644 index 2f2657315..000000000 --- a/src/bundled/agents2/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { PluginModule } from "../../plugins/api"; -import { Agents2Page } from "./Agents2Page"; - -export const inject = ["pages", "agents2", "relay"]; -export const apply: PluginModule["apply"] = (ctx) => { - const { agents2, relay } = ctx; - ctx.pages.register({ - id: "agents2", - title: "Agents2", - layout: "workspace", - primary: true, - component: () => , - }); -}; diff --git a/src/bundled/agents2/manifest.json b/src/bundled/agents2/manifest.json deleted file mode 100644 index f700ad48f..000000000 --- a/src/bundled/agents2/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{ "id": "buzz.agents2", "name": "Agents2", "apiVersion": 1 } diff --git a/src/bundled/builderlab/README.md b/src/bundled/builderlab/README.md index f69c16ea1..72b623a53 100644 --- a/src/bundled/builderlab/README.md +++ b/src/bundled/builderlab/README.md @@ -2,7 +2,7 @@ The bundled `block.builderlab` plugin adds Settings → Integrations → Builderlab. With a valid `BUZZ_BUILDERLAB_URL` build input, it also registers -`block.builderlab/builderlab` in Agents2's new-agent type picker. +`block.builderlab/builderlab` in Agents' new-agent type picker. This type has empty configuration and no execution handler yet; existing remote agents remain in Settings until the later migration. diff --git a/src/bundled/builderlab/index.test.tsx b/src/bundled/builderlab/index.test.tsx index c617d5361..fe68d277f 100644 --- a/src/bundled/builderlab/index.test.tsx +++ b/src/bundled/builderlab/index.test.tsx @@ -7,11 +7,11 @@ import userEvent from "@testing-library/user-event"; import { HostService } from "../../features/host/service"; import { SettingsCardsService } from "../../features/settings/service"; import { - Agents2Service, + AgentsService, type AgentCommunities, -} from "../../features/agents2/service"; +} from "../../features/agents/service"; import type { RelayData } from "../../features/relay/service"; -import { Agents2Page } from "../agents2/Agents2Page"; +import { AgentsPage } from "../agents/AgentsPage"; import * as builderlab from "./index"; import { PluginRuntime } from "../../plugins/runtime"; import type { PluginInfo } from "../../plugins/types"; @@ -44,13 +44,13 @@ afterEach(() => { vi.unstubAllGlobals(); }); -function provideAgents2(root: Context, relay: RelayData) { +function provideAgents(root: Context, relay: RelayData) { const communities = { snapshot: () => ({ memberships: [], selected: null }), subscribe: () => () => {}, open: () => relay, } as unknown as AgentCommunities; - return new Agents2Service(root, communities, { + return new AgentsService(root, communities, { list: async () => [], create: vi.fn(), portable: vi.fn(), @@ -70,7 +70,7 @@ function provideAgents2(root: Context, relay: RelayData) { } it.each([undefined, "", "http://builderlab.example"])( - "an unavailable desktop build (%s) shows setup guidance without login or an Agents2 type", + "an unavailable desktop build (%s) shows setup guidance without login or an agent type", async (url) => { vi.stubEnv("VITE_BUZZ_BUILDERLAB_URL", url); const root = new Context(); @@ -85,7 +85,7 @@ it.each([undefined, "", "http://builderlab.example"])( ); root.provide("relay", community.relay); root.provide("communityReader", community.reader); - const agents2 = provideAgents2(root, community.relay); + const agents = provideAgents(root, community.relay); try { runtime.reconcile([ { @@ -108,9 +108,9 @@ it.each([undefined, "", "http://builderlab.example"])( ); expect(screen.queryByRole("button")).not.toBeInTheDocument(); expect(native.invoke).not.toHaveBeenCalled(); - expect(agents2.types()).toHaveLength(0); + expect(agents.types()).toHaveLength(0); cleanup(); - render(); + render(); expect( await screen.findByText("Enable an agent type plugin to make one."), ).toBeInTheDocument(); @@ -171,7 +171,7 @@ it("binds login, list and creation to the plugin host and clears the session on ); root.provide("relay", community.relay); root.provide("communityReader", community.reader); - const agents2 = provideAgents2(root, community.relay); + const agents = provideAgents(root, community.relay); const plugin: PluginInfo = { manifest: { ...manifest, apiVersion: 1 }, source: "bundled", @@ -184,12 +184,12 @@ it("binds login, list and creation to the plugin host and clears the session on try { runtime.reconcile([plugin]); await waitFor(() => expect(cards.snapshot()).toHaveLength(1)); - expect(agents2.types()).toMatchObject([ + expect(agents.types()).toMatchObject([ { key: "block.builderlab/builderlab", title: "Builderlab" }, ]); - expect(agents2.types()[0]?.defaults()).toEqual({ config: {} }); + expect(agents.types()[0]?.defaults()).toEqual({ config: {} }); const picker = render( - , + , ); const [newAgent] = await screen.findAllByRole("button", { name: "New agent", @@ -298,14 +298,14 @@ it("binds login, list and creation to the plugin host and clears the session on runtime.reconcile([]); }); expect(cards.snapshot()).toHaveLength(0); - expect(agents2.types()).toHaveLength(0); + expect(agents.types()).toHaveLength(0); expect( screen.getByRole("button", { name: "Sign in with Builderlab" }), ).toBeEnabled(); mounted.unmount(); runtime.reconcile([plugin]); await waitFor(() => expect(cards.snapshot()).toHaveLength(1)); - expect(agents2.types()).toMatchObject([ + expect(agents.types()).toMatchObject([ { key: "block.builderlab/builderlab", title: "Builderlab" }, ]); const fresh = cards.snapshot()[0]; diff --git a/src/bundled/builderlab/index.tsx b/src/bundled/builderlab/index.tsx index 7f5ae341f..cb06c0c7b 100644 --- a/src/bundled/builderlab/index.tsx +++ b/src/bundled/builderlab/index.tsx @@ -16,7 +16,7 @@ export const inject = [ "settingsCards", "relay", "communityReader", - "agents2", + "agents", ]; export const apply: PluginModule["apply"] = (ctx) => { let unavailable = ""; @@ -29,7 +29,7 @@ export const apply: PluginModule["apply"] = (ctx) => { : "Builderlab sign-in is unavailable."; } if (!unavailable) - ctx.agents2.register({ + ctx.agents.register({ id: "builderlab", title: "Builderlab", description: "TBD", diff --git a/src/bundled/channels/ChannelMembersDialog.test.tsx b/src/bundled/channels/ChannelMembersDialog.test.tsx index e46c4a155..94c82fd7a 100644 --- a/src/bundled/channels/ChannelMembersDialog.test.tsx +++ b/src/bundled/channels/ChannelMembersDialog.test.tsx @@ -1,5 +1,5 @@ // @vitest-environment jsdom -import type { Agents2 } from "../../features/agents2/service"; +import type { Agents } from "../../features/agents/service"; import "@testing-library/jest-dom/vitest"; import { act, cleanup, render, screen, within } from "@testing-library/react"; import { createHash } from "node:crypto"; @@ -67,7 +67,7 @@ async function setup( canOpenLink(target: string): boolean; onOpenLink(target: string, returnFocus?: HTMLElement): boolean; }, - agents2?: Agents2, + agents?: Agents, ) { const viewer = keypair(), relay = keypair(), @@ -165,7 +165,7 @@ async function setup( session={owner.session} channelId={id} {...navigation} - {...(agents2 ? { agents2 } : {})} + {...(agents ? { agents } : {})} /> , ); @@ -1249,11 +1249,11 @@ it("adds a team's members not in the channel with one button", async () => { }, ], }; - const agents2 = { + const agents = { subscribe: () => () => {}, snapshot: () => snapshot, - } as unknown as Agents2; - const t = await setup("stream", false, false, undefined, agents2); + } as unknown as Agents; + const t = await setup("stream", false, false, undefined, agents); await t.user.type(screen.getByRole("searchbox"), "Cr"); const teams = await screen.findByRole("list", { name: "Teams" }); expect(within(teams).getByText("Ada, Cy")).toBeInTheDocument(); diff --git a/src/bundled/channels/ChannelMembersDialog.tsx b/src/bundled/channels/ChannelMembersDialog.tsx index a9350170b..ed35ed5c2 100644 --- a/src/bundled/channels/ChannelMembersDialog.tsx +++ b/src/bundled/channels/ChannelMembersDialog.tsx @@ -18,7 +18,7 @@ import { type PointerEvent, } from "react"; import type { RelaySession } from "../../features/relay/session"; -import type { Agents2, Team } from "../../features/agents2/service"; +import type { Agents, Team } from "../../features/agents/service"; import { useAgentChoices } from "../../features/agents/use-choices"; import { useIdentityNames } from "../../features/identity-names/react"; import { usePresenceStatus } from "../../features/presence/react"; @@ -339,11 +339,11 @@ export function ChannelMembersButton({ onOpenLink, onOpenConversation, presentation, - agents2, + agents, }: { session: RelaySession; channelId: string; - agents2?: Agents2 | undefined; + agents?: Agents | undefined; presentation?: | { open: boolean; onOpenChange(open: boolean): void } | undefined; @@ -372,7 +372,7 @@ export function ChannelMembersButton({ canOpenLink={canOpenLink} onOpenLink={onOpenLink} onOpenConversation={onOpenConversation} - {...(agents2 ? { agents2 } : {})} + {...(agents ? { agents } : {})} close={() => setOpen(false)} trigger={trigger} /> @@ -441,13 +441,13 @@ export function ChannelMembersDialog({ canOpenLink, onOpenLink, onOpenConversation, - agents2, + agents, }: { session: RelaySession; channelId: string; close(): void; trigger: React.RefObject; - agents2?: Agents2; + agents?: Agents; } & MemberNavigation) { const reducedMotion = useReducedMotion(); const presenceId = useId(); @@ -529,11 +529,11 @@ export function ChannelMembersDialog({ session.archives.snapshot, session.archives.snapshot, ); - const agents = useAgentChoices(session); - const agents2Snapshot = useSyncExternalStore( - agents2?.subscribe ?? noTeams, - agents2?.snapshot ?? noSnapshot, - agents2?.snapshot ?? noSnapshot, + const agentChoices = useAgentChoices(session); + const agentsSnapshot = useSyncExternalStore( + agents?.subscribe ?? noTeams, + agents?.snapshot ?? noSnapshot, + agents?.snapshot ?? noSnapshot, ); const resolveName = useIdentityNames(session.names); const channel = @@ -654,8 +654,9 @@ export function ChannelMembersDialog({ }, [session, channelId, rosterBusy, namesBusy, rolesSettled]); const initialLoading = revealedFor !== session; const known = useMemo( - () => new Map(agents.identities.map((agent) => [agent.pubkey, agent])), - [agents.identities], + () => + new Map(agentChoices.identities.map((agent) => [agent.pubkey, agent])), + [agentChoices.identities], ); const members = new Set(channel?.members ?? []); const archived = new Set(archives.archived); @@ -725,7 +726,7 @@ export function ChannelMembersDialog({ search.people.map((person) => [person.pubkey, person]), ); if (query.trim()) - for (const agent of agents.identities) { + for (const agent of agentChoices.identities) { if (addable(agent.pubkey, label(agent.pubkey, agent.name), agent.name)) candidates.set(agent.pubkey, { pubkey: agent.pubkey, @@ -800,7 +801,7 @@ export function ChannelMembersDialog({ administration.status === "loading" || namesBusy || search.loading || - agents.pending || + agentChoices.pending || agentsBusy || archives.status === "loading"; const mutationPending = @@ -831,9 +832,9 @@ export function ChannelMembersDialog({ }; // Teams match by name; each adds its members in this community not here yet. const teamNames = new Map( - (agents2Snapshot?.agents ?? []).map((agent) => [agent.pubkey, agent.name]), + (agentsSnapshot?.agents ?? []).map((agent) => [agent.pubkey, agent.name]), ); - const teams = (agents2Snapshot?.teams ?? []).flatMap((team) => { + const teams = (agentsSnapshot?.teams ?? []).flatMap((team) => { const missing = team.members.filter( (key) => teamNames.has(key) && !members.has(key), ); @@ -1413,7 +1414,7 @@ export function ChannelMembersDialog({ Some agent managers or their names could not load. Try again.

)} - {agents.error && ( + {agentChoices.error && (

Some agents could not load.

diff --git a/src/bundled/channels/ChannelTemplateSettings.test.tsx b/src/bundled/channels/ChannelTemplateSettings.test.tsx index b919c6939..8db30fb87 100644 --- a/src/bundled/channels/ChannelTemplateSettings.test.tsx +++ b/src/bundled/channels/ChannelTemplateSettings.test.tsx @@ -3,7 +3,7 @@ import "@testing-library/jest-dom/vitest"; import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, expect, it } from "vitest"; -import type { Agents2 } from "../../features/agents2/service"; +import type { Agents } from "../../features/agents/service"; import { channelTemplates } from "../../features/channel-templates/local"; import { ChannelTemplateSettings } from "./ChannelTemplateSettings"; @@ -21,14 +21,14 @@ const snapshot = { { id: "t2", name: "Docs", instructions: "", members: [] }, ], }; -const agents2 = { +const agents = { snapshot: () => snapshot, subscribe: () => () => {}, -} as unknown as Agents2; +} as unknown as Agents; it("makes, edits and deletes a template of teams and a Canvas", async () => { const user = userEvent.setup(); - render( true} />); + render( true} />); await user.click(screen.getByRole("button", { name: "New template" })); await user.type(screen.getByRole("textbox", { name: "Name" }), "Launch"); await user.click(screen.getByRole("checkbox", { name: "Docs" })); diff --git a/src/bundled/channels/ChannelTemplateSettings.tsx b/src/bundled/channels/ChannelTemplateSettings.tsx index 8239dbbd1..35522fd80 100644 --- a/src/bundled/channels/ChannelTemplateSettings.tsx +++ b/src/bundled/channels/ChannelTemplateSettings.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { useTeams } from "../../features/agents2/react"; -import type { Agents2, Team } from "../../features/agents2/service"; +import { useTeams } from "../../features/agents/react"; +import type { Agents, Team } from "../../features/agents/service"; import { channelTemplates, useChannelTemplates, @@ -21,14 +21,14 @@ import styles from "./ChannelTemplates.module.css"; /** Templates a new channel can start from: its teams and its Canvas. */ export function ChannelTemplateSettings({ - agents2, + agents, active, }: { - agents2?: Agents2 | undefined; + agents?: Agents | undefined; active(): boolean; }) { const templates = useChannelTemplates(); - const teams = useTeams(agents2); + const teams = useTeams(agents); // A template's id while it is edited, or "new" for one being made. const [editing, setEditing] = useState(); const template = templates.find((item) => item.id === editing); diff --git a/src/bundled/channels/ChannelsPage.tsx b/src/bundled/channels/ChannelsPage.tsx index 5925f63a2..36c50a023 100644 --- a/src/bundled/channels/ChannelsPage.tsx +++ b/src/bundled/channels/ChannelsPage.tsx @@ -1,5 +1,5 @@ import { pendingSessionDraft } from "../../features/sessions/pending-start"; -import type { Agents2 } from "../../features/agents2/service"; +import type { Agents } from "../../features/agents/service"; import { NewSessionComposer } from "../../features/sessions/NewSessionComposer"; import { activityTarget } from "../../features/agents/activity-target"; import { useChannelNavigation } from "../../features/channel-navigation/ChannelNavigationState"; @@ -91,11 +91,11 @@ export function ChannelsPage({ companion, navigation, navigator, - agents2, + agents, }: { extensions?: ConversationExtensions | undefined; /** For adding a whole team of the owner's agents from the members dialog. */ - agents2?: Agents2 | undefined; + agents?: Agents | undefined; relay: RelayData; navigation?: PageNavigation | undefined; navigator?: Navigation | undefined; @@ -150,7 +150,7 @@ export function ChannelsPage({ panels={panels} sessionsEnabled={sessionsEnabled} companion={companion} - agents2={agents2} + agents={agents} /> )}
@@ -168,10 +168,10 @@ function ChannelWorkspace({ navigation, navigator, viewer, - agents2, + agents, }: { extensions?: ConversationExtensions | undefined; - agents2?: Agents2 | undefined; + agents?: Agents | undefined; companion?: ReactNode; scope: string; navigation?: PageNavigation | undefined; @@ -1184,7 +1184,7 @@ function ChannelWorkspace({ : membersHeaderTrigger } canOpenLink={canOpenLink} - {...(agents2 ? { agents2 } : {})} + {...(agents ? { agents } : {})} onOpenLink={(url, returnFocus) => openLink(url, false, returnFocus)} onOpenConversation={ navigator && viewer diff --git a/src/bundled/channels/CreateChannelDialog.test.tsx b/src/bundled/channels/CreateChannelDialog.test.tsx index 12e808129..98c6cdeeb 100644 --- a/src/bundled/channels/CreateChannelDialog.test.tsx +++ b/src/bundled/channels/CreateChannelDialog.test.tsx @@ -10,7 +10,7 @@ import { import userEvent from "@testing-library/user-event"; import { afterEach, expect, it, vi } from "vitest"; import { createRelaySession } from "../../features/relay/session"; -import type { Agents2 } from "../../features/agents2/service"; +import type { Agents } from "../../features/agents/service"; import { channelTemplates } from "../../features/channel-templates/local"; import { CreateChannelDialog } from "./CreateChannelDialog"; @@ -859,10 +859,10 @@ it("starts a channel from a template with its teams' agents here and its Canvas" { id: "t2", name: "Docs", instructions: "", members: [bo, ada] }, ], }; - const agents2 = { + const agents = { snapshot: () => snapshot, subscribe: () => () => {}, - } as unknown as Agents2; + } as unknown as Agents; channelTemplates.save({ name: "Launch", teams: ["t1", "t2", "gone"], @@ -872,7 +872,7 @@ it("starts a channel from a template with its teams' agents here and its Canvas" render( {}} onCreate={onCreate} diff --git a/src/bundled/channels/CreateChannelDialog.tsx b/src/bundled/channels/CreateChannelDialog.tsx index 3441411f8..9affc4198 100644 --- a/src/bundled/channels/CreateChannelDialog.tsx +++ b/src/bundled/channels/CreateChannelDialog.tsx @@ -1,13 +1,13 @@ import type { Groups, Group } from "../../features/channel-templates/model"; import type { ChannelCreationInput } from "../../features/channel-templates/setup"; import type { RelaySession } from "../../features/relay/session"; -import type { Agents2 } from "../../features/agents2/service"; +import type { Agents } from "../../features/agents/service"; import { templateAgents, useChannelTemplates, } from "../../features/channel-templates/local"; import { Select } from "../../shared/design-system/ui/Select"; -import { useAgents2Snapshot } from "../../features/agents2/react"; +import { useAgentsSnapshot } from "../../features/agents/react"; import { useEffect, useId, @@ -44,7 +44,7 @@ type Props = { finalFocus?: RefObject | undefined; session: RelaySession; /** Supplies the teams a template adds. */ - agents2?: Agents2 | undefined; + agents?: Agents | undefined; groups: Groups | undefined; destinations?: readonly (Pick & { icon?: string })[]; groupSource?: "legacy"; @@ -62,7 +62,7 @@ function OpenCreateChannelDialog({ pending, finalFocus, session, - agents2, + agents, groups, destinations = groups?.groups ?? [], groupSource, @@ -103,7 +103,7 @@ function OpenCreateChannelDialog({ const [error, setError] = useState(""); const [groupId, setGroupId] = useState(""); const templates = useChannelTemplates(); - const agentsSnapshot = useAgents2Snapshot(agents2); + const agentsSnapshot = useAgentsSnapshot(agents); const [chosenTemplate, setTemplateId] = useState(""); // A template deleted meanwhile, here or in another window, is no choice. const template = templates.find((item) => item.id === chosenTemplate); diff --git a/src/bundled/channels/index.tsx b/src/bundled/channels/index.tsx index aec5abe32..28cd336b1 100644 --- a/src/bundled/channels/index.tsx +++ b/src/bundled/channels/index.tsx @@ -13,7 +13,7 @@ export const inject = [ "conversation", "navigation", "settingsCards", - "agents2", + "agents", ]; export const apply: PluginModule["apply"] = (ctx) => { ctx.conversation.registerCompletion({ @@ -27,7 +27,7 @@ export const apply: PluginModule["apply"] = (ctx) => { const panels = ctx.panels; const pages = ctx.pages; const navigator = ctx.navigation; - const agents2 = ctx.agents2; + const agents = ctx.agents; ctx.settingsCards.register({ id: "groups", title: "Personal groups", @@ -41,7 +41,7 @@ export const apply: PluginModule["apply"] = (ctx) => { title: "Channel templates", icon: CopyIcon, component: ({ active }) => ( - + ), }); ctx.pages.register({ @@ -62,7 +62,7 @@ export const apply: PluginModule["apply"] = (ctx) => { panels={panels} pages={pages} companion={companion} - agents2={agents2} + agents={agents} /> ), }); diff --git a/src/bundled/claude-code/index.ts b/src/bundled/claude-code/index.ts index 87a16c73e..8658814aa 100644 --- a/src/bundled/claude-code/index.ts +++ b/src/bundled/claude-code/index.ts @@ -1,4 +1,4 @@ -// Claude Code agents: an Agents2 type whose agents are Claude Code sessions on +// Claude Code agents: an agent type whose agents are Claude Code sessions on // this computer. Each conversation the agent is in is one continuing session; // Claude answers with the in-process Buzz tools. import { isTauri } from "@tauri-apps/api/core"; @@ -11,18 +11,18 @@ export const TYPE_ID = "claude-code"; /** A turn's run returns once Claude has it, so this bounds only the hand-over. */ const HANDOVER_MS = 2 * 60_000; -export const inject = ["agents2", "host"]; +export const inject = ["agents", "host"]; export const apply: PluginModule["apply"] = (ctx) => { // Only the desktop app can start `claude`, and setup and file uploads run // `bash` and `base64`, which stock Windows lacks; no unusable type elsewhere. if (!isTauri() || !/Mac|Linux/i.test(navigator.platform)) return; - const { agents2, host } = ctx; + const { agents, host } = ctx; const typeKey = `${ctx.pluginOwner?.id ?? "buzz.claude-code"}/${TYPE_ID}`; const spawn = host.spawn?.bind(host); const setup = new ClaudeSetup(spawn); - const runtime = new ClaudeRuntime(host, agents2.relay); + const runtime = new ClaudeRuntime(host, agents.relay); const sync = () => { - const snapshot = agents2.snapshot(); + const snapshot = agents.snapshot(); // Agents that stop running, the app signing out included, stop their // processes; switching communities does not. if (snapshot.status === "loading") return; @@ -30,7 +30,7 @@ export const apply: PluginModule["apply"] = (ctx) => { }; // One spare session waits for whichever running agent needs it next. if (spawn) { - const unsubscribe = agents2.subscribe(sync); + const unsubscribe = agents.subscribe(sync); sync(); ctx.effect(() => () => { unsubscribe(); @@ -40,7 +40,7 @@ export const apply: PluginModule["apply"] = (ctx) => { } const { ClaudeTab, SettingsTab } = createTabs({ setup, runtime }); - agents2.register({ + agents.register({ id: TYPE_ID, title: "Claude Code", description: diff --git a/src/bundled/claude-code/runtime.test.ts b/src/bundled/claude-code/runtime.test.ts index 09d97dcce..f96f3abe8 100644 --- a/src/bundled/claude-code/runtime.test.ts +++ b/src/bundled/claude-code/runtime.test.ts @@ -2,7 +2,7 @@ import { createHash } from "node:crypto"; import { schnorr } from "@noble/curves/secp256k1.js"; import { bytesToHex } from "nostr-tools/utils"; import { afterEach, expect, it, vi } from "vitest"; -import type { Agent, Delivery } from "../../features/agents2/service"; +import type { Agent, Delivery } from "../../features/agents/service"; import type { Host } from "../../features/host/service"; import type { EventData, RelayEvent } from "../../features/relay/events"; import type { RelayData } from "../../features/relay/service"; diff --git a/src/bundled/claude-code/runtime.ts b/src/bundled/claude-code/runtime.ts index 432ea0408..93d8f0bbe 100644 --- a/src/bundled/claude-code/runtime.ts +++ b/src/bundled/claude-code/runtime.ts @@ -1,4 +1,4 @@ -// Turns Agents2 deliveries into Claude turns. Each agent of this type has its +// Turns Agents deliveries into Claude turns. Each agent of this type has its // own sessions; a delivery becomes a prompt in the harness format, sent to the // session for its conversation. The run returns once the turn is handed over, // so one agent works in several conversations at once. Claude acts in Buzz with @@ -13,7 +13,7 @@ import { type Delivery, type Effort, type Models, -} from "../../features/agents2/service"; +} from "../../features/agents/service"; import type { Host } from "../../features/host/service"; import type { EventData } from "../../features/relay/events"; import type { RelayData } from "../../features/relay/service"; diff --git a/src/bundled/claude-code/tabs.test.tsx b/src/bundled/claude-code/tabs.test.tsx index c4857a34a..a71161923 100644 --- a/src/bundled/claude-code/tabs.test.tsx +++ b/src/bundled/claude-code/tabs.test.tsx @@ -3,7 +3,7 @@ import "@testing-library/jest-dom/vitest"; import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; -import type { Agent, AgentType } from "../../features/agents2/service"; +import type { Agent, AgentType } from "../../features/agents/service"; import type { HostProcessOptions } from "../../features/host/service"; import { apply, inject } from "./index"; import { type Config, DEFAULT_CONFIG } from "./runtime"; @@ -61,7 +61,7 @@ function applied({ loggedIn = true } = {}) { const ctx = { pluginOwner: { id: "buzz.claude-code", revision: "r1" }, host: { spawn, request: vi.fn() }, - agents2: { + agents: { register: (registered: AgentType) => { type = registered; }, @@ -91,7 +91,7 @@ async function choose(label: string, option: string) { } it("is an agent type on the desktop app", () => { - expect(inject).toEqual(["agents2", "host"]); + expect(inject).toEqual(["agents", "host"]); const { type } = installed(); expect(type).toMatchObject({ id: "claude-code", title: "Claude Code" }); expect(type.defaults()).toEqual({ config: DEFAULT_CONFIG }); diff --git a/src/bundled/claude-code/tabs.tsx b/src/bundled/claude-code/tabs.tsx index 9e4ffc785..4a2df3cdb 100644 --- a/src/bundled/claude-code/tabs.tsx +++ b/src/bundled/claude-code/tabs.tsx @@ -1,7 +1,7 @@ // The type's tabs: Claude Code's setup on this computer, and the agent's // settings. import { useEffect, useState, useSyncExternalStore } from "react"; -import type { AgentViewProps } from "../../features/agents2/service"; +import type { AgentViewProps } from "../../features/agents/service"; import { Button } from "../../shared/design-system/ui/Button"; import { Field } from "../../shared/design-system/ui/Field"; import { Input } from "../../shared/design-system/ui/Input"; diff --git a/src/bundled/codex/README.md b/src/bundled/codex/README.md index 8eed47317..b86a181d8 100644 --- a/src/bundled/codex/README.md +++ b/src/bundled/codex/README.md @@ -1,7 +1,7 @@ -# Codex for Agents2 +# Codex for Agents A bundled desktop plugin, on by default on macOS and Linux, using the native process host introduced in #731. -Agents2 owns the identity, admission, configuration, and signed Buzz actions. This plugin +Agents owns the identity, admission, configuration, and signed Buzz actions. This plugin owns Codex work after delivery handover. It starts one `codex app-server` for all agents and uses an independent Codex thread for each agent's Buzz conversation. Each thread carries its agent's workspace, instructions and tools, so one crash @@ -13,13 +13,13 @@ From the feature worktree: ```sh source bin/activate-hermit -BUZZODZ_PROFILE=codex-agents2 BUZZ_DEV_VIEWER= bin/just desktop +BUZZODZ_PROFILE=codex-agents BUZZ_DEV_VIEWER= bin/just desktop ``` 1. Codex is on by default; **Settings → Plugins** turns it off or on. An older profile that installed Codex from a folder gets the bundled copy instead, and keeps it off if the installed copy was off. -2. Open **Agents2 → New agent**, choose **Codex**, and create an agent. The Codex +2. Open **Agents → New agent**, choose **Codex**, and create an agent. The Codex tab checks the installed CLI and existing account. If needed, install Codex and run `codex login` in a terminal, then click **Check again**. 3. The **Workspace** defaults to `~/.buzz`, like Claude Code. In the agent's @@ -101,14 +101,14 @@ and cleanup. - Only the owner's events start work. Event watches support those events; timers are unsupported and report an error. -- Replies use Agents2's host signer. Codex receives no Buzz agent key and does not +- Replies use Agents' host signer. Codex receives no Buzz agent key and does not publish through the Buzz CLI. Short Buzz guidance and custom instructions are supplied as developer instructions, preserving Codex’s native coding prompt. - Codex runs with sandbox `danger-full-access` and approval policy `never`, matching Claude Code's `bypassPermissions`: it can write outside its workspace, reach the network, and use git and `gh`. Web search, inherited MCP servers, Apps, and plugins are disabled. -- Shared Agent Activity integration is deferred because this Agents2 delivery +- Shared Agent Activity integration is deferred because this Agents delivery contract has no live-activity API. The Codex tab shows conversation status and the latest command/output. Messages and uploads use the shared Buzz tools. Codex assistant text is not automatically posted. After `buzz.send` with `final: true`, Codex finishes normally and consumes pending steering. Final sends preserve background processes, such as a dev server it started. - Turns have a 29-minute deadline; diff --git a/src/bundled/codex/index.test.ts b/src/bundled/codex/index.test.ts index 8ca97b59a..5d13d2561 100644 --- a/src/bundled/codex/index.test.ts +++ b/src/bundled/codex/index.test.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom import * as React from "react"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; -import type { AgentType } from "../../features/agents2/service"; +import type { AgentType } from "../../features/agents/service"; import { CodexRuntime } from "./runtime"; import { apply } from "./index"; @@ -24,7 +24,7 @@ function applied() { react: React, pluginOwner: { id: "buzz.codex", revision: "r1" }, host: { spawn: vi.fn(), request: vi.fn() }, - agents2: { + agents: { register: (registered: AgentType) => { type = registered; }, @@ -58,7 +58,7 @@ it("stops agents that stop running even when the agent list could not be read", react: React, pluginOwner: { id: "buzz.codex" }, host: { spawn: vi.fn(), request: vi.fn() }, - agents2: { + agents: { register: () => undefined, snapshot: () => state, relay: () => undefined, diff --git a/src/bundled/codex/index.ts b/src/bundled/codex/index.ts index 1c836296f..e6ace2ea3 100644 --- a/src/bundled/codex/index.ts +++ b/src/bundled/codex/index.ts @@ -3,7 +3,7 @@ import type { PluginModule } from "../../plugins/api"; import { defaults, config } from "./config"; import { CodexRuntime } from "./runtime"; import { createTabs } from "./tabs"; -export const inject = ["react", "agents2", "host"]; +export const inject = ["react", "agents", "host"]; export const apply: PluginModule["apply"] = (ctx) => { // Only the desktop app can start `codex`, and file uploads run `base64`, // which stock Windows lacks; no unusable type elsewhere. @@ -14,23 +14,23 @@ export const apply: PluginModule["apply"] = (ctx) => { (async () => { throw new Error("Codex agents run only in the desktop app"); }); - const runtime = new CodexRuntime(start, ctx.agents2.relay); + const runtime = new CodexRuntime(start, ctx.agents.relay); const typeKey = `${ctx.pluginOwner?.id ?? "buzz.codex"}/codex`; // Agents that stop running stop their turns, and the last one stops the // server; switching communities or a reconnect does not. const sync = () => { - const snapshot = ctx.agents2.snapshot(); + const snapshot = ctx.agents.snapshot(); if (snapshot.status === "loading") return; runtime.sync(snapshot.running.filter((agent) => agent.type === typeKey)); }; - const offAgents = ctx.agents2.subscribe(sync); + const offAgents = ctx.agents.subscribe(sync); sync(); ctx.effect(() => () => { offAgents(); runtime.dispose(); }); const { CodexTab, SettingsTab } = createTabs(ctx.react, start, runtime); - ctx.agents2.register({ + ctx.agents.register({ id: "codex", title: "Codex", description: diff --git a/src/bundled/codex/live.mjs b/src/bundled/codex/live.mjs index dceae90ae..892e18584 100644 --- a/src/bundled/codex/live.mjs +++ b/src/bundled/codex/live.mjs @@ -8,7 +8,7 @@ import { build } from "rolldown"; import { JSDOM } from "jsdom"; globalThis.document = new JSDOM("").window.document; -const workspace = await mkdtemp(join(tmpdir(), "buzz-codex-agents2-")); +const workspace = await mkdtemp(join(tmpdir(), "buzz-codex-agents-")); await build({ input: { runtime: new URL("./runtime.ts", import.meta.url).pathname, diff --git a/src/bundled/codex/prompt.ts b/src/bundled/codex/prompt.ts index faaf7e8cc..29e87e218 100644 --- a/src/bundled/codex/prompt.ts +++ b/src/bundled/codex/prompt.ts @@ -1,5 +1,5 @@ import { BUZZ_PROMPT } from "../../buzz-mcp/prompt"; -import type { AgentHandle } from "../../features/agents2/service"; +import type { AgentHandle } from "../../features/agents/service"; import type { EventData } from "../../features/relay/events"; import { threadReference } from "../../features/relay/thread-reference"; import type { Config } from "./config"; diff --git a/src/bundled/codex/runtime.test.ts b/src/bundled/codex/runtime.test.ts index b7e943a63..55c5b82fa 100644 --- a/src/bundled/codex/runtime.test.ts +++ b/src/bundled/codex/runtime.test.ts @@ -4,7 +4,7 @@ import { bytesToHex } from "nostr-tools/utils"; import * as React from "react"; import { apply } from "./index"; import { afterEach, expect, it, vi } from "vitest"; -import type { Agent, Delivery } from "../../features/agents2/service"; +import type { Agent, Delivery } from "../../features/agents/service"; import type { RelayData } from "../../features/relay/service"; import type { EventData } from "../../features/relay/events"; import type { HostProcessOptions } from "../../features/host/service"; @@ -277,7 +277,7 @@ function fixture( events.push(result.trigger.event as EventData); return result; }; - /** Agents2's running agents, whichever community is selected. */ + /** Agents' running agents, whichever community is selected. */ const running = [{ ...agent, type: "buzz.codex/codex" }]; const listeners: (() => void)[] = []; let runPlugin: (delivery: Delivery) => Promise = (delivery) => @@ -289,7 +289,7 @@ function fixture( apply({ react: React, host: { spawn }, - agents2: { + agents: { snapshot: () => ({ status: "ready", agents: [], running }), relay: () => relay, subscribe: (listener: () => void) => { diff --git a/src/bundled/codex/runtime.ts b/src/bundled/codex/runtime.ts index 5c5879d5a..89d0f8828 100644 --- a/src/bundled/codex/runtime.ts +++ b/src/bundled/codex/runtime.ts @@ -4,7 +4,7 @@ import { type AgentHandle, type Delivery, type Models, -} from "../../features/agents2/service"; +} from "../../features/agents/service"; import type { RelayData } from "../../features/relay/service"; import type { EventData } from "../../features/relay/events"; import { appClient } from "../../buzz-mcp/app-client"; @@ -96,7 +96,7 @@ function textOf(event: EventData, name: string) { return text; } -/** Agents2 owns admission/identity. This plugin owns work after hand-over, and +/** Agents owns admission/identity. This plugin owns work after hand-over, and * uses one app-server for every agent. Each conversation is its own Codex * thread carrying its agent's workspace, instructions and tools, and tool * calls find their agent by thread and turn. */ @@ -386,7 +386,7 @@ export class CodexRuntime { } if (lane.active) { const active = lane.active; - // Startup and history reads stay ordered without holding Agents2 delivery. + // Startup and history reads stay ordered without holding Agents delivery. const accepted = active.steering.then(async () => { try { await active.ready; @@ -454,7 +454,7 @@ export class CodexRuntime { }); lane.tail = job; this.status(request.agent.pubkey, key, "Starting"); - // Agents2 can deliver follow-ups and other conversations immediately. + // Agents can deliver follow-ups and other conversations immediately. void job .catch((error) => console.error("Codex turn failed", error)) .finally(() => { diff --git a/src/bundled/codex/tabs.test.tsx b/src/bundled/codex/tabs.test.tsx index 0034f5c92..32e7ad9fb 100644 --- a/src/bundled/codex/tabs.test.tsx +++ b/src/bundled/codex/tabs.test.tsx @@ -9,7 +9,7 @@ import { waitFor, } from "@testing-library/react"; import { afterEach, expect, it, vi } from "vitest"; -import type { AgentViewProps } from "../../features/agents2/service"; +import type { AgentViewProps } from "../../features/agents/service"; import type { HostProcessOptions } from "../../features/host/service"; import { defaults, type Config } from "./config"; import { createTabs } from "./tabs"; diff --git a/src/bundled/codex/tabs.ts b/src/bundled/codex/tabs.ts index aa776c4cc..eb5d4eb6d 100644 --- a/src/bundled/codex/tabs.ts +++ b/src/bundled/codex/tabs.ts @@ -1,6 +1,6 @@ import type * as ReactModule from "react"; import type { ReactNode } from "react"; -import type { AgentViewProps } from "../../features/agents2/service"; +import type { AgentViewProps } from "../../features/agents/service"; import { config, effortName, diff --git a/src/bundled/identity-naming/README.md b/src/bundled/identity-naming/README.md index 920a22806..5d5d61304 100644 --- a/src/bundled/identity-naming/README.md +++ b/src/bundled/identity-naming/README.md @@ -248,7 +248,7 @@ bin/pnpm exec vitest run src/features/identity-names/policy.test.ts At the extraction baseline, [directory.ts](../../features/identity-names/directory.ts) constructs facts from public profiles, ready agent-library records, the -viewer's local Agents2 agents in the current community, and view-local display +viewer's local agents in the current community, and view-local display facts. Local non-empty names take precedence over library names in that community. The first library record per key uses its trimmed name, then the profile name, then diff --git a/src/bundled/index.ts b/src/bundled/index.ts index 9cb1c512c..7a3e4e610 100644 --- a/src/bundled/index.ts +++ b/src/bundled/index.ts @@ -22,8 +22,8 @@ import mentionsManifest from "./mentions/manifest.json"; import * as mentions from "./mentions"; import emojiManifest from "./emoji/manifest.json"; import * as emoji from "./emoji"; -import agents2Manifest from "./agents2/manifest.json"; -import * as agents2 from "./agents2"; +import agentsManifest from "./agents/manifest.json"; +import * as agents from "./agents"; import claudeCodeManifest from "./claude-code/manifest.json"; import * as claudeCode from "./claude-code"; import codexManifest from "./codex/manifest.json"; @@ -163,8 +163,8 @@ export const bundledPlugins: readonly BundledPlugin[] = [ enabledByDefault: true, }, { - manifest: { ...agents2Manifest, apiVersion: 1 }, - module: agents2, + manifest: { ...agentsManifest, apiVersion: 1 }, + module: agents, enabledByDefault: true, }, { diff --git a/src/bundled/profiles/ProfilePanel.fallback.test.tsx b/src/bundled/profiles/ProfilePanel.fallback.test.tsx index ab9e361cb..707dfb2f1 100644 --- a/src/bundled/profiles/ProfilePanel.fallback.test.tsx +++ b/src/bundled/profiles/ProfilePanel.fallback.test.tsx @@ -28,7 +28,7 @@ function fixture(configuredNames = false) { relayUrl: "wss://relay.example.test", owner: viewer, }; - // Agents2 lists this machine's agents once it loads. + // Agents lists this machine's agents once it loads. const control = localAgentsFixture(configuredNames ? [agent] : []); const provider = createAgentDirectory(control); const query = vi.fn(async () => []); @@ -73,7 +73,7 @@ function fixture(configuredNames = false) { agent, person, control, - /** Agents2 lists the person as one of this machine's agents. */ + /** Agents lists the person as one of this machine's agents. */ known: (relayUrl = agent.relayUrl) => act(() => control.set({ agents: [{ ...agent, relayUrl }] })), query, diff --git a/src/bundled/profiles/ProfilePanel.tsx b/src/bundled/profiles/ProfilePanel.tsx index d844f4411..1f5e5e345 100644 --- a/src/bundled/profiles/ProfilePanel.tsx +++ b/src/bundled/profiles/ProfilePanel.tsx @@ -37,23 +37,23 @@ import type { RelayData } from "../../features/relay/service"; import type { RelaySession } from "../../features/relay/session"; import { ProfileAgentIdentity } from "./ProfileAgentIdentity"; import styles from "./Profiles.module.css"; -import { AgentDelete } from "../../features/agents2/AgentManage"; -import { useAgent2, useChannelChoices } from "../../features/agents2/react"; -import type { Agents2 } from "../../features/agents2/service"; -import { agentSections } from "../../features/agents2/tabs"; +import { AgentDelete } from "../../features/agents/AgentManage"; +import { useAgent, useChannelChoices } from "../../features/agents/react"; +import type { Agents } from "../../features/agents/service"; +import { agentSections } from "../../features/agents/tabs"; export function ProfilePanel({ relay, target, context, navigation, - agents2, + agents, close, }: PanelProps & { relay: RelayData; navigation?: Navigation; - /** Agents2 agents show their type's tabs. */ - agents2?: Agents2; + /** Agents show their type's tabs. */ + agents?: Agents; }) { const connection = useRelayConnection(relay); const pubkey = profilePanelKey(target); @@ -68,7 +68,7 @@ export function ProfilePanel({ agentHint={profileAgentHint(target)} context={context} navigation={navigation} - agents2={agents2} + agents={agents} scope={connection.scope} viewer={connection.viewer} close={close} @@ -81,12 +81,12 @@ function ProfileDetails({ pubkey, context, navigation, - agents2, + agents, scope, viewer, close, }: { - agents2: Agents2 | undefined; + agents: Agents | undefined; agentHint: boolean; session: RelaySession; pubkey: string; @@ -114,14 +114,14 @@ function ProfileDetails({ const [tab, setTab] = useState<"info" | "channels" | `agent:${string}`>( "info", ); - // An Agents2 agent's sections come from its type and the app, not the harness. - const plugin = useAgent2(agents2, pubkey); + // An agent's sections come from its type and the app, not the harness. + const plugin = useAgent(agents, pubkey); const channelChoices = useChannelChoices( plugin ? session.channels : undefined, ); const sections = - plugin && agents2 - ? agentSections({ agents2, ...plugin, channels: channelChoices }) + plugin && agents + ? agentSections({ agents, ...plugin, channels: channelChoices }) : []; const section = sections.find((item) => item.value === tab); const tabHost = usePanelTabHost(); @@ -313,10 +313,10 @@ function ProfileDetails({ {profile?.about && (

{profile.about}

)} - {plugin && agents2 && ( + {plugin && agents && (
diff --git a/src/bundled/profiles/index.tsx b/src/bundled/profiles/index.tsx index 78e442cdb..d59449e28 100644 --- a/src/bundled/profiles/index.tsx +++ b/src/bundled/profiles/index.tsx @@ -2,7 +2,7 @@ import type { PluginModule } from "../../plugins/api"; import { profilePanelKey } from "../../features/profiles/target"; import { ProfilePanel } from "./ProfilePanel"; -export const inject = ["panels", "relay", "navigation", "agents2"]; +export const inject = ["panels", "relay", "navigation", "agents"]; export const apply: PluginModule["apply"] = (ctx) => { ctx.panels.register({ id: "profile", @@ -13,7 +13,7 @@ export const apply: PluginModule["apply"] = (ctx) => { {...props} relay={ctx.relay} navigation={ctx.navigation} - agents2={ctx.agents2} + agents={ctx.agents} /> ), }); diff --git a/src/buzz-mcp/app-client.ts b/src/buzz-mcp/app-client.ts index c469ff9bd..ab65e9c6c 100644 --- a/src/buzz-mcp/app-client.ts +++ b/src/buzz-mcp/app-client.ts @@ -1,9 +1,9 @@ // The Buzz tools' client inside the app: the agent's native-held key signs and -// reads through its Agents2 handle, files are read by a declared process, and +// reads through its Agents handle, files are read by a declared process, and // memory is read back through the owner's own session, to whom it is // encrypted. import type { BuzzClient, Memory } from "./client"; -import type { AgentHandle } from "../features/agents2/service"; +import type { AgentHandle } from "../features/agents/service"; import type { HostProcess, HostProcessOptions } from "../features/host/service"; type Spawn = (id: string, options?: HostProcessOptions) => Promise; diff --git a/src/features/agents2/AgentManage.test.tsx b/src/features/agents/AgentManage.test.tsx similarity index 91% rename from src/features/agents2/AgentManage.test.tsx rename to src/features/agents/AgentManage.test.tsx index 27728a232..59065414e 100644 --- a/src/features/agents2/AgentManage.test.tsx +++ b/src/features/agents/AgentManage.test.tsx @@ -4,7 +4,7 @@ import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, expect, it, vi } from "vitest"; import { AgentDelete } from "./AgentManage"; -import type { Agent, Agents2 } from "./service"; +import type { Agent, Agents } from "./service"; afterEach(cleanup); @@ -23,13 +23,13 @@ const agent = { it("deletes after confirming, and keeps the dialog open with the error when Delete fails", async () => { const user = userEvent.setup(); const remove = vi - .fn() + .fn() .mockRejectedValueOnce(new Error("Credential store refused")) .mockResolvedValueOnce(); const onRemoved = vi.fn(); render( , diff --git a/src/features/agents2/AgentManage.tsx b/src/features/agents/AgentManage.tsx similarity index 88% rename from src/features/agents2/AgentManage.tsx rename to src/features/agents/AgentManage.tsx index d881a4602..df751097f 100644 --- a/src/features/agents2/AgentManage.tsx +++ b/src/features/agents/AgentManage.tsx @@ -1,21 +1,21 @@ -// Owner actions on an Agents2 agent that every surface showing it offers: the -// Agents2 Build view and the profile panel. +// Owner actions on an agent that every surface showing it offers: the +// Agents Build view and the profile panel. import { useState } from "react"; import { TrashIcon } from "../../shared/design-system/icons"; import { Button } from "../../shared/design-system/ui/Button"; import { Dialog } from "../../shared/design-system/ui/Dialog"; -import type { Agent, Agents2 } from "./service"; +import type { Agent, Agents } from "./service"; const message = (error: unknown) => error instanceof Error ? error.message : String(error); /** Delete, after a confirmation that says what it does. */ export function AgentDelete({ - agents2, + agents, agent, onRemoved, }: { - agents2: Agents2; + agents: Agents; agent: Agent; onRemoved(): void; }) { @@ -26,7 +26,7 @@ export function AgentDelete({ setRemoving(true); setError(""); try { - await agents2.remove(agent.pubkey); + await agents.remove(agent.pubkey); setConfirming(false); onRemoved(); } catch (reason) { diff --git a/src/features/agents2/AttentionPanel.test.tsx b/src/features/agents/AttentionPanel.test.tsx similarity index 100% rename from src/features/agents2/AttentionPanel.test.tsx rename to src/features/agents/AttentionPanel.test.tsx diff --git a/src/features/agents2/AttentionPanel.tsx b/src/features/agents/AttentionPanel.tsx similarity index 100% rename from src/features/agents2/AttentionPanel.tsx rename to src/features/agents/AttentionPanel.tsx diff --git a/src/features/agents2/attention.test.ts b/src/features/agents/attention.test.ts similarity index 100% rename from src/features/agents2/attention.test.ts rename to src/features/agents/attention.test.ts diff --git a/src/features/agents2/attention.ts b/src/features/agents/attention.ts similarity index 100% rename from src/features/agents2/attention.ts rename to src/features/agents/attention.ts diff --git a/src/features/agents2/local-typing.test.ts b/src/features/agents/local-typing.test.ts similarity index 100% rename from src/features/agents2/local-typing.test.ts rename to src/features/agents/local-typing.test.ts diff --git a/src/features/agents2/local-typing.ts b/src/features/agents/local-typing.ts similarity index 98% rename from src/features/agents2/local-typing.ts rename to src/features/agents/local-typing.ts index f7c0604a9..7f348582d 100644 --- a/src/features/agents2/local-typing.ts +++ b/src/features/agents/local-typing.ts @@ -2,7 +2,7 @@ // relay. Every piece is tagged STOPGAP(agent-local-typing): grep for it and // delete this file and its test, its import and use in service.ts, and // `typing.local` in relay/typing.ts with its test and the fake in -// agents2/service.test.ts. +// agents/service.test.ts. // // While an agent this app runs works on a mention, it shows as typing there, // on this device only. diff --git a/src/features/agents/local.ts b/src/features/agents/local.ts index ef629fd4a..83f4d04aa 100644 --- a/src/features/agents/local.ts +++ b/src/features/agents/local.ts @@ -1,5 +1,5 @@ /** One agent this machine keeps, in any community. Names never grant control; - * Agents2 owns its key and runtime. */ + * Agents owns its key and runtime. */ export type LocalAgent = Readonly<{ pubkey: string; name: string; @@ -22,7 +22,7 @@ const unavailable: LocalAgentsSnapshot = Object.freeze({ agents: Object.freeze([]), }); -/** Names and mentions are composed before Agents2 exists; this lets them +/** Names and mentions are composed before Agents exists; this lets them * follow its list once `bind` connects it. */ export function createLocalAgents() { let source: LocalAgents | undefined; diff --git a/src/features/agents/migrate-storage.test.ts b/src/features/agents/migrate-storage.test.ts new file mode 100644 index 000000000..5b9289e6d --- /dev/null +++ b/src/features/agents/migrate-storage.test.ts @@ -0,0 +1,57 @@ +import { expect, it } from "vitest"; +import { migrateAgentStorage } from "./migrate-storage"; +import { readRecords, readTeams, STORAGE_KEY, TEAMS_KEY } from "./store"; +import { memoryStorage } from "./test-fakes"; + +it("moves settings and teams byte for byte, then restarts without resurrecting old data", () => { + const storage = memoryStorage(); + const pubkey = "a".repeat(64); + const record = { pubkey, attention: {}, config: { draft: "kept" } }; + const team = { + id: "one", + name: "One", + members: [pubkey], + instructions: "hello", + }; + const records = JSON.stringify({ version: 1, agents: { [pubkey]: record } }); + const teams = JSON.stringify({ version: 1, teams: [team] }); + storage.setItem("buzz.agents2.v1", records); + storage.setItem("buzz.agents2.teams.v1", teams); + migrateAgentStorage(storage); + expect(storage.getItem(STORAGE_KEY)).toBe(records); + expect(storage.getItem(TEAMS_KEY)).toBe(teams); + expect(readRecords(storage)[pubkey]).toMatchObject(record); + expect(readTeams(storage)).toEqual([team]); + expect(storage.getItem("buzz.agents2.v1")).toBeNull(); + expect(storage.getItem("buzz.agents2.teams.v1")).toBeNull(); + storage.setItem(STORAGE_KEY, "{}"); + migrateAgentStorage(storage); + expect(storage.getItem(STORAGE_KEY)).toBe("{}"); +}); + +it("preserves the source on write failure and resumes an interrupted copy", () => { + const storage = memoryStorage(); + storage.setItem("buzz.agents2.v1", "exact bytes"); + expect(() => + migrateAgentStorage({ + ...storage, + setItem: () => { + throw new Error("full"); + }, + }), + ).toThrow("full"); + expect(storage.getItem("buzz.agents2.v1")).toBe("exact bytes"); + storage.setItem(STORAGE_KEY, "exact bytes"); + migrateAgentStorage(storage); + expect(storage.getItem("buzz.agents2.v1")).toBeNull(); + expect(storage.getItem(STORAGE_KEY)).toBe("exact bytes"); +}); + +it("refuses to overwrite conflicting settings", () => { + const storage = memoryStorage(); + storage.setItem("buzz.agents2.v1", "old"); + storage.setItem(STORAGE_KEY, "new"); + expect(() => migrateAgentStorage(storage)).toThrow("different data"); + expect(storage.getItem("buzz.agents2.v1")).toBe("old"); + expect(storage.getItem(STORAGE_KEY)).toBe("new"); +}); diff --git a/src/features/agents/migrate-storage.ts b/src/features/agents/migrate-storage.ts new file mode 100644 index 000000000..e7cc0c23e --- /dev/null +++ b/src/features/agents/migrate-storage.ts @@ -0,0 +1,22 @@ +// TEMPORARY agents rename: remove this module and its startup call after +// 2026-10-18, once the development users have upgraded. No runtime aliases. +import { STORAGE_KEY, TEAMS_KEY } from "./store"; + +export function migrateAgentStorage(storage: Storage) { + for (const [oldKey, key] of [ + ["buzz.agents2.v1", STORAGE_KEY], + ["buzz.agents2.teams.v1", TEAMS_KEY], + ] as const) { + const old = storage.getItem(oldKey); + if (old === null) continue; + const current = storage.getItem(key); + // An interrupted copy can resume; conflicting data needs recovery, never + // an implicit overwrite or merge of independently edited settings. + if (current !== null && current !== old) + throw new Error( + `Cannot migrate ${oldKey}: ${key} already contains different data`, + ); + if (current === null) storage.setItem(key, old); + storage.removeItem(oldKey); + } +} diff --git a/src/features/agents2/native.test.ts b/src/features/agents/native.test.ts similarity index 100% rename from src/features/agents2/native.test.ts rename to src/features/agents/native.test.ts diff --git a/src/features/agents2/native.ts b/src/features/agents/native.ts similarity index 98% rename from src/features/agents2/native.ts rename to src/features/agents/native.ts index 943e0aea5..2a66ac85b 100644 --- a/src/features/agents2/native.ts +++ b/src/features/agents/native.ts @@ -20,7 +20,7 @@ export type AgentIdentity = Readonly<{ /** Where an earlier agent system saved an identity on this machine: this app's * harness agents, or the installed or development build of old Buzz. */ export type PortSource = "harness" | "installed" | "development"; -/** An identity an earlier agent system saved, which Agents2 can take over. */ +/** An identity an earlier agent system saved, which Agents can take over. */ export type PortableAgent = Readonly<{ source: PortSource; pubkey: string; diff --git a/src/features/agents2/react.test.tsx b/src/features/agents/react.test.tsx similarity index 84% rename from src/features/agents2/react.test.tsx rename to src/features/agents/react.test.tsx index 7dbdc721a..cd292f265 100644 --- a/src/features/agents2/react.test.tsx +++ b/src/features/agents/react.test.tsx @@ -2,8 +2,8 @@ import type { ReactNode } from "react"; import { act, waitFor, renderHook } from "@testing-library/react"; import { expect, it } from "vitest"; -import { useAgent2, useMentionModels, Agents2Context } from "./react"; -import type { Agent, Agents2 } from "./service"; +import { useAgent, useMentionModels, AgentsContext } from "./react"; +import type { Agent, Agents } from "./service"; function deferred() { let resolve!: (value: T) => void; @@ -15,14 +15,14 @@ function deferred() { it("still finds a saved agent whose type plugin is not registered", () => { const agent = { pubkey: "a".repeat(64), type: "gone/echo" } as Agent; - const agents2 = { + const agents = { subscribe: () => () => {}, find: (pubkey: string) => (pubkey === agent.pubkey ? agent : undefined), types: () => [], - } as unknown as Agents2; - const { result } = renderHook(() => useAgent2(agents2, agent.pubkey)); + } as unknown as Agents; + const { result } = renderHook(() => useAgent(agents, agent.pubkey)); expect(result.current).toEqual({ agent, type: undefined }); - const other = renderHook(() => useAgent2(agents2, "b".repeat(64))); + const other = renderHook(() => useAgent(agents, "b".repeat(64))); expect(other.result.current).toBeUndefined(); }); @@ -41,11 +41,9 @@ it("loads models and their effort levels asynchronously before an agent runs", a subscribe: () => () => {}, snapshot: () => snapshot, types: () => types, - } as unknown as Agents2; + } as unknown as Agents; const wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - + {children} ); const { result } = renderHook(() => useMentionModels(["agent"]), { wrapper, @@ -80,11 +78,9 @@ it("offers nothing for a single model without effort levels", async () => { subscribe: () => () => {}, snapshot: () => snapshot, types: () => types, - } as unknown as Agents2; + } as unknown as Agents; const wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - + {children} ); const { result } = renderHook(() => useMentionModels(["fixed", "tunable"]), { wrapper, @@ -112,11 +108,9 @@ it("discards pending metadata after the mentioned agent changes", async () => { subscribe: () => () => {}, snapshot: () => snapshot, types: () => types, - } as unknown as Agents2; + } as unknown as Agents; const wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - + {children} ); const { result, rerender } = renderHook( ({ pubkey }) => useMentionModels([pubkey]), diff --git a/src/features/agents2/react.ts b/src/features/agents/react.ts similarity index 72% rename from src/features/agents2/react.ts rename to src/features/agents/react.ts index f081e7d18..2de6ef3f8 100644 --- a/src/features/agents2/react.ts +++ b/src/features/agents/react.ts @@ -8,58 +8,58 @@ import { } from "react"; import type { ChannelQueries } from "../relay/contracts"; import type { ChannelChoice } from "./attention"; -import type { Agents2, Models } from "./service"; +import type { Agents, Models } from "./service"; const noSubscribe = () => () => {}; const none = () => undefined; -/** The selected community's Agents2 agents and the registered types. */ -export function useAgents2(agents2: Agents2) { +/** The selected community's agents and the registered types. */ +export function useAgents(agents: Agents) { const snapshot = useSyncExternalStore( - agents2.subscribe, - agents2.snapshot, - agents2.snapshot, + agents.subscribe, + agents.snapshot, + agents.snapshot, ); const types = useSyncExternalStore( - agents2.subscribe, - agents2.types, - agents2.types, + agents.subscribe, + agents.types, + agents.types, ); return { snapshot, types }; } -/** Agents2's snapshot, or undefined without it. */ -export function useAgents2Snapshot(agents2: Agents2 | undefined) { - const subscribe = agents2?.subscribe ?? noSubscribe; - const snapshot = agents2?.snapshot ?? none; +/** Agents' snapshot, or undefined without it. */ +export function useAgentsSnapshot(agents: Agents | undefined) { + const subscribe = agents?.subscribe ?? noSubscribe; + const snapshot = agents?.snapshot ?? none; return useSyncExternalStore(subscribe, snapshot, snapshot); } const noTeams: readonly never[] = Object.freeze([]); -/** The owner's teams on this device, or none without Agents2. */ -export const useTeams = (agents2: Agents2 | undefined) => - useAgents2Snapshot(agents2)?.teams ?? noTeams; +/** The owner's teams on this device, or none without Agents. */ +export const useTeams = (agents: Agents | undefined) => + useAgentsSnapshot(agents)?.teams ?? noTeams; -/** The Agents2 agent at `pubkey`, with its type when that is registered. A saved - * agent whose type plugin is disabled or reloading is still an Agents2 agent. */ -export function useAgent2(agents2: Agents2 | undefined, pubkey: string) { - const subscribe = agents2?.subscribe ?? noSubscribe; +/** The agent at `pubkey`, with its type when that is registered. A saved + * agent whose type plugin is disabled or reloading is still an agent. */ +export function useAgent(agents: Agents | undefined, pubkey: string) { + const subscribe = agents?.subscribe ?? noSubscribe; const agent = useSyncExternalStore( subscribe, - agents2 ? () => agents2.find(pubkey) : none, - agents2 ? () => agents2.find(pubkey) : none, + agents ? () => agents.find(pubkey) : none, + agents ? () => agents.find(pubkey) : none, ); const type = useSyncExternalStore( subscribe, - agents2 && agent - ? () => agents2.types().find((type) => type.key === agent.type) + agents && agent + ? () => agents.types().find((type) => type.key === agent.type) : none, none, ); return agent ? { agent, type } : undefined; } -/** The app's Agents2, for surfaces such as the composer that plugins do not own. */ -export const Agents2Context = createContext(undefined); +/** The app's Agents, for surfaces such as the composer that plugins do not own. */ +export const AgentsContext = createContext(undefined); export type MentionModelChoice = Models & Readonly<{ pubkey: string; name: string; loading?: boolean }>; @@ -68,16 +68,16 @@ export type MentionModelChoice = Models & export function useMentionModels( pubkeys: readonly string[], ): readonly MentionModelChoice[] { - const agents2 = useContext(Agents2Context); - const subscribe = agents2?.subscribe ?? noSubscribe; + const agents = useContext(AgentsContext); + const subscribe = agents?.subscribe ?? noSubscribe; const snapshot = useSyncExternalStore( subscribe, - agents2 ? agents2.snapshot : none, + agents ? agents.snapshot : none, none, ); const types = useSyncExternalStore( subscribe, - agents2 ? agents2.types : none, + agents ? agents.types : none, none, ); const key = [...new Set(pubkeys)].join(":"); diff --git a/src/features/agents2/service.test.ts b/src/features/agents/service.test.ts similarity index 98% rename from src/features/agents2/service.test.ts rename to src/features/agents/service.test.ts index eeaeb3040..a3996117e 100644 --- a/src/features/agents2/service.test.ts +++ b/src/features/agents/service.test.ts @@ -10,7 +10,7 @@ import type { RelayData, RelaySnapshot } from "../relay/service"; import type { RelaySession } from "../relay/session"; import type { AgentEventTemplate, AgentIdentity, AgentsNative } from "./native"; import { - Agents2Service, + AgentsService, type AgentCommunities, type AgentType, type Delivery, @@ -23,7 +23,7 @@ import { memoryStorage } from "./test-fakes"; // place in the order matters. const steps: string[] = []; const leaveChannels = vi.hoisted(() => vi.fn()); -vi.mock("../agents/relay-removal", () => ({ +vi.mock("./relay-removal", () => ({ removeAgentFromChannels: leaveChannels, })); @@ -273,7 +273,7 @@ async function setup({ const second = fakeRelay("https://other.example.test"); const client = fakeCommunities(fake, second); const native = fakeNative(identities); - const service = new Agents2Service(ctx, client.communities, native, storage); + const service = new AgentsService(ctx, client.communities, native, storage); const plugin = ctx.extend({ pluginOwner: Object.freeze({ id: "example", revision: "one" }), }); @@ -312,7 +312,7 @@ async function setup({ }), run, }; - plugin.agents2.register(type); + plugin.agents.register(type); await vi.waitFor(() => expect(service.snapshot().status).toBe("ready")); return { service, @@ -483,7 +483,7 @@ it("gives each member its teams' roster and instructions, and drops a deleted ag [], ]), ); - expect(storage.getItem("buzz.agents2.teams.v1")).not.toContain(sibling); + expect(storage.getItem("buzz.agents.teams.v1")).not.toContain(sibling); await service.removeTeam(team.id); expect(service.snapshot().teams.map((saved) => saved.name)).toEqual(["Solo"]); }); @@ -808,7 +808,7 @@ it("opens no community for an agent list that arrives after it stopped", async ( list = resolve; }), ); - const service = new Agents2Service( + const service = new AgentsService( ctx, client.communities, native, @@ -1045,7 +1045,7 @@ it("shows an agent whose settings could not be saved with its type's defaults", const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); await service.create({ type: "example/echo", name: "Echo" }); warn.mockRestore(); - expect(storage.getItem("buzz.agents2.v1") ?? "").not.toContain(bot); + expect(storage.getItem("buzz.agents.v1") ?? "").not.toContain(bot); expect(service.find(bot)).toMatchObject({ config: { reply: "ok" } }); expect(Object.keys(service.find(bot)?.attention ?? {})).toEqual([ "interest/default", @@ -1053,7 +1053,7 @@ it("shows an agent whose settings could not be saved with its type's defaults", ]); // The next save stores them. await service.save(bot, { config: { reply: "saved" } }); - expect(storage.getItem("buzz.agents2.v1")).toContain(bot); + expect(storage.getItem("buzz.agents.v1")).toContain(bot); }); it("saves nothing when native create fails", async () => { @@ -1064,7 +1064,7 @@ it("saves nothing when native create fails", async () => { service.create({ type: "example/echo", name: "Echo" }), ).rejects.toThrow("Credential store refused"); expect(service.snapshot().agents).toEqual([]); - expect(storage.getItem("buzz.agents2.v1")).toBeNull(); + expect(storage.getItem("buzz.agents.v1")).toBeNull(); }); it("returns the created agent after the community changes mid-create", async () => { @@ -1083,7 +1083,7 @@ it("returns the created agent after the community changes mid-create", async () it("forgets records whose key is gone when identities load", async () => { const storage = memoryStorage(); storage.setItem( - "buzz.agents2.v1", + "buzz.agents.v1", JSON.stringify({ version: 1, agents: { @@ -1108,7 +1108,7 @@ it("forgets records whose key is gone when identities load", async () => { expect(service.snapshot().agents.map((agent) => agent.name)).toEqual([ "Echo", ]); - expect(storage.getItem("buzz.agents2.v1")).not.toContain(sibling); + expect(storage.getItem("buzz.agents.v1")).not.toContain(sibling); }); it("keeps an agent created while identities are still loading", async () => { @@ -1123,7 +1123,7 @@ it("keeps an agent created while identities are still loading", async () => { native.list.mockImplementationOnce( () => new Promise((resolve) => (release = resolve)), ); - const service = new Agents2Service( + const service = new AgentsService( ctx, fakeCommunities(fakeRelay()).communities, native, @@ -1131,7 +1131,7 @@ it("keeps an agent created while identities are still loading", async () => { ); ctx .extend({ pluginOwner: Object.freeze({ id: "example", revision: "one" }) }) - .agents2.register({ + .agents.register({ id: "echo", title: "Echo", defaults: () => ({ config: { reply: "ok" } }), @@ -1141,7 +1141,7 @@ it("keeps an agent created while identities are still loading", async () => { release([]); await vi.waitFor(() => expect(service.snapshot().status).toBe("ready")); expect(service.find(bot)?.name).toBe("Echo"); - expect(storage.getItem("buzz.agents2.v1")).toContain(bot); + expect(storage.getItem("buzz.agents.v1")).toContain(bot); }); it("fires a due timer once, then again an interval after it ran", async () => { @@ -1191,7 +1191,7 @@ it("fires a due timer once, then again an interval after it ran", async () => { expect(run).toHaveBeenCalledTimes(2); // Its run state lives in the agent's record and goes when the timer does. const stored = () => - JSON.parse(storage.getItem("buzz.agents2.v1") ?? "{}").agents[bot]; + JSON.parse(storage.getItem("buzz.agents.v1") ?? "{}").agents[bot]; expect(stored().timers["watch/tick"]).toMatchObject({ used: 2 }); expect(stored().timers["watch/stale"]).toMatchObject({ used: 16 }); await service.save(bot, { attention: { "watch/tick": null } }); @@ -1307,7 +1307,7 @@ it("deletes the key at once, then leaves its channels and archives it before for await service.create({ type: "example/echo", name: "Echo" }); await service.remove(bot); expect(service.snapshot().agents).toEqual([]); - expect(storage.getItem("buzz.agents2.v1")).not.toContain(bot); + expect(storage.getItem("buzz.agents.v1")).not.toContain(bot); await vi.waitFor(() => expect(steps).toEqual(["key", "channels", "archive", "forget"]), ); diff --git a/src/features/agents2/service.ts b/src/features/agents/service.ts similarity index 98% rename from src/features/agents2/service.ts rename to src/features/agents/service.ts index 545efb5d3..4fd73a126 100644 --- a/src/features/agents2/service.ts +++ b/src/features/agents/service.ts @@ -1,9 +1,10 @@ -// Agents2: agents that are plugins from the start. A plugin registers an agent +// Agents: agents that are plugins from the start. A plugin registers an agent // type (its summary line, peek view, settings tabs and `run`); each agent made // from it has its own native-held key, an `agent-attention/v1` configuration the // app owns and edits, and a config blob the type owns. The app delivers matching live events to // `run` from the stream the owner already receives in the agent's community, // selected or not, so no plugin opens a socket or a REQ. +import { migrateAgentStorage } from "./migrate-storage"; import { Service, type Context } from "@deepseek-ai/cordis"; import { npubEncode } from "nostr-tools/nip19"; import type { ComponentType } from "react"; @@ -11,9 +12,9 @@ import { createContributions, type Contribution, } from "../../plugins/contributions"; -import type { LocalAgentsSnapshot } from "../agents/local"; -import { attestedOwner } from "../agents/owner-attestation"; -import { removeAgentFromChannels } from "../agents/relay-removal"; +import type { LocalAgentsSnapshot } from "./local"; +import { attestedOwner } from "./owner-attestation"; +import { removeAgentFromChannels } from "./relay-removal"; import { communityDestination, relayOrigin } from "../communities/destination"; import type { Communities } from "../communities/service"; import type { RelayEvent } from "../relay/events"; @@ -209,19 +210,19 @@ export type AgentsSnapshot = Readonly<{ }>; export type { Team }; export type TeamChange = Omit & Readonly<{ id?: string }>; -export type Agents2 = { +export type Agents = { register(type: AgentType): void; types(): readonly RegisteredAgentType[]; snapshot(): AgentsSnapshot; subscribe(listener: () => void): () => void; - /** The agent of this pubkey in the selected community, if it is an Agents2 agent. */ + /** The agent of this pubkey in the selected community, if it is an agent. */ find(pubkey: string): Agent | undefined; /** The owner's connection to a running agent's community, which need not be * the selected one: read the agent's conversations through this. */ relay(pubkey: string): RelayData | undefined; create(input: Readonly<{ type: string; name: string }>): Promise; /** Identities earlier agent systems saved on this machine that are not - * Agents2 agents yet. */ + * Agents yet. */ portable(): Promise; /** Takes one over as an agent of `type` in the selected community, keeping its * key, name and picture. Its instructions and settings stay behind. */ @@ -240,7 +241,7 @@ export type Agents2 = { }; declare module "@deepseek-ai/cordis" { interface Context { - agents2: Agents2; + agents: Agents; } } @@ -355,7 +356,7 @@ async function pickFor( } } -/** What Agents2 needs of the communities: the viewer, the selected one, and +/** What Agents needs of the communities: the viewer, the selected one, and * a connection to any joined one without selecting it. */ export type AgentCommunities = Pick< Communities, @@ -416,7 +417,7 @@ type Runner = { }; }; -export class Agents2Service extends Service implements Agents2 { +export class AgentsService extends Service implements Agents { private readonly contributions; private readonly listeners = new Set<() => void>(); /** Every saved identity, deleted ones included: native's list is the agent @@ -442,8 +443,9 @@ export class Agents2Service extends Service implements Agents2 { private readonly native: AgentsNative | undefined = nativeAgents(), private readonly storage: Storage = globalThis.localStorage, ) { - super(ctx, "agents2"); + super(ctx, "agents"); this.contributions = createContributions(ctx); + migrateAgentStorage(storage); this.records = readRecords(storage); this.teamList = Object.freeze(readTeams(storage)); this.state = Object.freeze({ diff --git a/src/features/agents2/store.test.ts b/src/features/agents/store.test.ts similarity index 100% rename from src/features/agents2/store.test.ts rename to src/features/agents/store.test.ts diff --git a/src/features/agents2/store.ts b/src/features/agents/store.ts similarity index 97% rename from src/features/agents2/store.ts rename to src/features/agents/store.ts index 04e085685..0b203f1d3 100644 --- a/src/features/agents2/store.ts +++ b/src/features/agents/store.ts @@ -1,4 +1,4 @@ -// Device-local settings for Agents2 agents: each agent's attention objects in the +// Device-local settings for agents: each agent's attention objects in the // `agent-attention/v1` shape, one JSON blob its plugin type owns, and its timers' // run state. Which agents exist, their type and name, and their keys live in // native custody, never here; a record without an identity does nothing. A @@ -35,7 +35,7 @@ export type AgentRecord = Readonly<{ }>; type Stored = { version: 1; agents: Record }; -export const STORAGE_KEY = "buzz.agents2.v1"; +export const STORAGE_KEY = "buzz.agents.v1"; const PUBKEY = /^[0-9a-f]{64}$/; const group = (value: AttentionValue) => value.type === "interest" ? "Interests" : "watches and timers"; @@ -181,7 +181,7 @@ export type Team = Readonly<{ /** Agent pubkeys, in the order shown. */ members: readonly string[]; }>; -export const TEAMS_KEY = "buzz.agents2.teams.v1"; +export const TEAMS_KEY = "buzz.agents.teams.v1"; /** Reads every team, skipping any that is not one. */ export function readTeams(storage: Storage): readonly Team[] { diff --git a/src/features/agents2/tabs.tsx b/src/features/agents/tabs.tsx similarity index 77% rename from src/features/agents2/tabs.tsx rename to src/features/agents/tabs.tsx index 3db4c2d12..c2c0ebd2b 100644 --- a/src/features/agents2/tabs.tsx +++ b/src/features/agents/tabs.tsx @@ -1,10 +1,10 @@ -// An Agents2 agent's sections, wherever they are shown (its Build view and its +// An agent's sections, wherever they are shown (its Build view and its // profile panel): the type's own tabs, then the app's Attention tab, which every // type shares because the app is what wakes the agent. import type { ReactNode } from "react"; import type { ChannelChoice } from "./attention"; import { AttentionPanel } from "./AttentionPanel"; -import type { Agent, Agents2, RegisteredAgentType } from "./service"; +import type { Agent, Agents, RegisteredAgentType } from "./service"; export type AgentSection = Readonly<{ /** `agent:` or `agent:attention`; never collides with a host tab. */ @@ -14,12 +14,12 @@ export type AgentSection = Readonly<{ }>; export function agentSections({ - agents2, + agents, agent, type, channels, }: { - agents2: Agents2; + agents: Agents; agent: Agent; type: RegisteredAgentType | undefined; channels: readonly ChannelChoice[]; @@ -31,7 +31,7 @@ export function agentSections({ render: () => ( agents2.save(agent.pubkey, { config })} + save={(config) => agents.save(agent.pubkey, { config })} /> ), })), @@ -41,7 +41,7 @@ export function agentSections({ render: () => ( agents2.save(agent.pubkey, change)} + save={(change) => agents.save(agent.pubkey, change)} channels={channels} /> ), diff --git a/src/features/agents2/test-fakes.ts b/src/features/agents/test-fakes.ts similarity index 100% rename from src/features/agents2/test-fakes.ts rename to src/features/agents/test-fakes.ts diff --git a/src/features/agents2/working-reactions.ts b/src/features/agents/working-reactions.ts similarity index 95% rename from src/features/agents2/working-reactions.ts rename to src/features/agents/working-reactions.ts index 193c4181c..f551abc4b 100644 --- a/src/features/agents2/working-reactions.ts +++ b/src/features/agents/working-reactions.ts @@ -1,4 +1,4 @@ -// STOPGAP(agent-working-reactions): DELETE once Agents2 agents show as typing +// STOPGAP(agent-working-reactions): DELETE once agents show as typing // through the relay. Every piece is tagged STOPGAP(agent-working-reactions): // grep for it and delete this file, its import and use in service.ts, and its // test. Keep `Delivery.workingUntil`, which typing indicators need too. diff --git a/src/features/channel-navigation/ChannelSidebar.tsx b/src/features/channel-navigation/ChannelSidebar.tsx index 0946e5fb4..ad21475b2 100644 --- a/src/features/channel-navigation/ChannelSidebar.tsx +++ b/src/features/channel-navigation/ChannelSidebar.tsx @@ -15,7 +15,7 @@ import type { ChannelLifecycleAction } from "../relay/channel-lifecycle-protocol import { useMePlacement } from "../sessions/personal"; import { personalGroups } from "../channel-templates/setup"; import type { RelayData } from "../relay/service"; -import type { Agents2 } from "../agents2/service"; +import type { Agents } from "../agents/service"; import type { RelaySession } from "../relay/session"; import { useChannelList, useRelayConnection } from "../relay/react"; import type { Navigation } from "../navigation/controller"; @@ -91,7 +91,7 @@ type Props = { relay: RelayData; navigator: Navigation; /** For channel templates, which start a channel with the owner's teams. */ - agents2?: Agents2 | undefined; + agents?: Agents | undefined; target: OpenTarget; sessionsEnabled: boolean; meEnabled?: boolean; @@ -189,7 +189,7 @@ class SidebarBoundary extends Component< function ReadySidebar({ relay, navigator, - agents2, + agents, target, children, queries, @@ -1376,7 +1376,7 @@ function ReadySidebar({ pending={pendingChannelCreation} finalFocus={createChannelTrigger} session={queries} - agents2={agents2} + agents={agents} groups={groups} destinations={displayedPreferences?.sections ?? []} {...(preferences.data?.groupSource !== "personal" diff --git a/src/features/channel-templates/local.ts b/src/features/channel-templates/local.ts index 8fddd2017..83e4c94d7 100644 --- a/src/features/channel-templates/local.ts +++ b/src/features/channel-templates/local.ts @@ -1,4 +1,4 @@ -// The owner's channel templates on this device: a name, the Agents2 teams a +// The owner's channel templates on this device: a name, the Agents teams a // new channel starts with, and its starting Canvas. Creating a channel from // one sends ordinary channel setup, so the session owns applying it. import { useSyncExternalStore } from "react"; @@ -7,7 +7,7 @@ import { CANVAS_BYTES } from "./model"; export type ChannelTemplate = Readonly<{ id: string; name: string; - /** Agents2 team ids. A team deleted since is skipped. */ + /** Agents team ids. A team deleted since is skipped. */ teams: readonly string[]; canvas: string; }>; diff --git a/src/features/messages/ComposerModels.tsx b/src/features/messages/ComposerModels.tsx index 6b80dcbe7..40bc8490a 100644 --- a/src/features/messages/ComposerModels.tsx +++ b/src/features/messages/ComposerModels.tsx @@ -7,9 +7,9 @@ import { type ReactNode, type Ref, } from "react"; -import { useMentionModels } from "../agents2/react"; -import type { MentionModelChoice } from "../agents2/react"; -import type { Effort } from "../agents2/service"; +import { useMentionModels } from "../agents/react"; +import type { MentionModelChoice } from "../agents/react"; +import type { Effort } from "../agents/service"; import type { MentionPicks, RunPick } from "../relay/messages"; import { CaretDownIcon, diff --git a/src/features/messages/MessageComposer.test.tsx b/src/features/messages/MessageComposer.test.tsx index b3c7004e1..abb782b0b 100644 --- a/src/features/messages/MessageComposer.test.tsx +++ b/src/features/messages/MessageComposer.test.tsx @@ -43,8 +43,8 @@ import { UploadError, UPLOAD_FAILURES } from "../relay/attachments"; import { ToastProvider } from "../../shared/design-system/ui/Toast"; import type { OutgoingEvent } from "../relay/outbox"; import { ConversationPresentation } from "../conversation/ConversationPresentation"; -import { Agents2Context } from "../agents2/react"; -import type { Agents2 } from "../agents2/service"; +import { AgentsContext } from "../agents/react"; +import type { Agents } from "../agents/service"; import { MessageComposer, type MessageComposerProps } from "./MessageComposer"; import { createRelaySession, type RelaySession } from "../relay/session"; import { keypair, metadata, roster, signed } from "../relay/testing"; @@ -114,7 +114,7 @@ function mount( options: Partial = {}, control?: LocalAgents, viewer?: string, - agents2?: Agents2, + agents?: Agents, ) { let commands: ComposerToolProps; const completionRequests: ComposerCompletionProps["publish"][] = []; @@ -340,13 +340,13 @@ function mount( bindChoices(); let presented = true; const tree = () => ( - + - + ); const view = render(tree(), { reactStrictMode: true, @@ -2076,12 +2076,12 @@ it("opens the model picker while discovery is pending, then fills its controls", agents: [{ pubkey: first.pubkey, name: "Honey", type: "test/agent" }], }; const types = [{ key: "test/agent", models: () => pending.promise }]; - const agents2 = { + const agents = { subscribe: () => () => {}, snapshot: () => snapshot, types: () => types, - } as unknown as Agents2; - const h = mount({}, undefined, undefined, agents2); + } as unknown as Agents; + const h = mount({}, undefined, undefined, agents); await h.user.click(screen.getByRole("button", { name: "First Honey" })); await h.user.click( screen.getByRole("button", { name: "Model and effort for the next run" }), @@ -2128,12 +2128,12 @@ it("asks a mentioned agent of the viewer's for a model and effort on the next ru ], }; const types = [{ key: "test/agent", models: () => models }]; - const agents2 = { + const agents = { subscribe: () => () => {}, snapshot: () => snapshot, types: () => types, - } as unknown as Agents2; - const h = mount({}, undefined, undefined, agents2); + } as unknown as Agents; + const h = mount({}, undefined, undefined, agents); const picker = () => screen.queryByRole("button", { name: "Model and effort for the next run", @@ -2247,12 +2247,12 @@ it("opens the effort picker at the effort the agent's next run uses", async () = ], }; const types = [{ key: "test/agent", models: () => models }]; - const agents2 = { + const agents = { subscribe: () => () => {}, snapshot: () => snapshot, types: () => types, - } as unknown as Agents2; - const h = mount({}, undefined, undefined, agents2); + } as unknown as Agents; + const h = mount({}, undefined, undefined, agents); const open = async () => { await h.user.click( await screen.findByRole("button", { diff --git a/src/features/panels/service.test.tsx b/src/features/panels/service.test.tsx index 66b4ff961..35c58e9e9 100644 --- a/src/features/panels/service.test.tsx +++ b/src/features/panels/service.test.tsx @@ -31,7 +31,7 @@ it("independently unloads panels without removing the page or shared data", asyn new ConversationService(root); new SettingsCardsService(root); // Channels adds teams of agents from the members dialog. - root.provide("agents2", { + root.provide("agents", { subscribe: () => () => {}, snapshot: () => undefined, }); diff --git a/src/features/relay/typing.test.ts b/src/features/relay/typing.test.ts index a6e59e68c..32834d09e 100644 --- a/src/features/relay/typing.test.ts +++ b/src/features/relay/typing.test.ts @@ -309,7 +309,7 @@ describe("completion capacity", () => { expect(vi.getTimerCount()).toBe(0); }); }); -// STOPGAP(agent-local-typing): delete with agents2/local-typing.ts. +// STOPGAP(agent-local-typing): delete with agents/local-typing.ts. it("shows a local pulse like a live one, through the agent's posts until a local end", () => { const { owner, snapshot } = setup(); const local = { diff --git a/src/features/relay/typing.ts b/src/features/relay/typing.ts index 131dd534c..76cd3c5d7 100644 --- a/src/features/relay/typing.ts +++ b/src/features/relay/typing.ts @@ -15,7 +15,7 @@ type ParticipantActivity = { lastMessageAt: number; visibleUntil: number; quietUntil: number; - // STOPGAP(agent-local-typing): delete with agents2/local-typing.ts. + // STOPGAP(agent-local-typing): delete with agents/local-typing.ts. /** Driven by this app's own pulses, which end only with a local end. */ local?: boolean; }; @@ -179,7 +179,7 @@ export function createTyping( return { capability: Object.freeze({ snapshot: () => snapshot, - // STOPGAP(agent-local-typing): delete with agents2/local-typing.ts. + // STOPGAP(agent-local-typing): delete with agents/local-typing.ts. /** Takes a pulse (20002) this app made for an agent it runs, or the * end of its turn (9, as if the agent had replied), on this device only, * as if it came live, until agents' pulses reach the relay. Until that diff --git a/src/plugins/migrate-agent-flag.ts b/src/plugins/migrate-agent-flag.ts new file mode 100644 index 000000000..e2762e9c9 --- /dev/null +++ b/src/plugins/migrate-agent-flag.ts @@ -0,0 +1,8 @@ +// TEMPORARY agents rename: remove after 2026-10-18 with the version-2 reader. +// Version 3 distinguishes new Agents preferences from the retired v1 plugin. +export function migrateAgentFlag(flags: Record) { + delete flags["buzz.agents"]; + const enabled = flags["buzz.agents2"]; + if (enabled !== undefined) flags["buzz.agents"] = enabled; + delete flags["buzz.agents2"]; +} diff --git a/src/plugins/storage.test.ts b/src/plugins/storage.test.ts index 713ee7e4c..ff461a658 100644 --- a/src/plugins/storage.test.ts +++ b/src/plugins/storage.test.ts @@ -46,7 +46,7 @@ it("ignores the retired Welcome setting and changes each bundled plugin independ it("restores required Channels from saved disabled settings and rejects disabling it", async () => { const saved = JSON.stringify({ - version: 2, + version: 3, enabled: { "buzz.channels": false, "buzz.github": false, @@ -80,7 +80,7 @@ it.each([true, undefined])( "preserves saved Bestie=%s over a changed default without migrating storage", async (saved) => { const flags = saved === undefined ? {} : { "buzz.bestie": saved }; - const raw = JSON.stringify({ version: 2, enabled: flags }); + const raw = JSON.stringify({ version: 3, enabled: flags }); const values = new Map([["buzzodz.plugins.v1", raw]]); vi.stubGlobal("localStorage", { getItem: (key: string) => values.get(key) ?? null, @@ -116,3 +116,72 @@ it.each([true, undefined])( }); }, ); + +it("migrates the Agents switch once and ignores v1's", async () => { + const agents = (): PluginInfo[] => + catalog() + .slice(0, 1) + .map((plugin) => ({ + ...plugin, + manifest: { id: "buzz.agents", name: "Agents", apiVersion: 1 }, + })); + const values = new Map([ + [ + "buzzodz.plugins.v1", + JSON.stringify({ version: 2, enabled: { "buzz.agents": false } }), + ], + ]); + vi.stubGlobal("localStorage", { + getItem: (key: string) => values.get(key) ?? null, + setItem: (key: string, value: string) => values.set(key, value), + }); + const installation = createPluginStorage(agents); + expect(await installation.getCatalog()).toMatchObject({ + catalog: { plugins: [{ enabled: true }] }, + }); + expect( + await installation.changePlugin("disable", "buzz.agents"), + ).toMatchObject({ catalog: { plugins: [{ enabled: false }] } }); + expect(await createPluginStorage(agents).getCatalog()).toMatchObject({ + catalog: { plugins: [{ enabled: false }] }, + }); + expect(JSON.parse(values.get("buzzodz.plugins.v1") ?? "").version).toBe(3); + expect(JSON.parse(values.get("buzzodz.plugins.v1") ?? "").enabled).toEqual({ + "buzz.agents": false, + }); +}); + +it.each([true, false])( + "migrates Agents2=%s over the retired v1 override", + async (enabled) => { + const values = new Map([ + [ + "buzzodz.plugins.v1", + JSON.stringify({ + version: 2, + enabled: { "buzz.agents": !enabled, "buzz.agents2": enabled }, + }), + ], + ]); + vi.stubGlobal("localStorage", { + getItem: (key: string) => values.get(key) ?? null, + setItem: (key: string, value: string) => values.set(key, value), + }); + const agents = (): PluginInfo[] => + catalog() + .slice(0, 1) + .map((plugin) => ({ + ...plugin, + manifest: { id: "buzz.agents", name: "Agents", apiVersion: 1 }, + })); + for (let n = 0; n < 2; n++) { + expect(await createPluginStorage(agents).getCatalog()).toMatchObject({ + catalog: { plugins: [{ enabled }] }, + }); + } + expect(JSON.parse(values.get("buzzodz.plugins.v1") ?? "")).toEqual({ + version: 3, + enabled: { "buzz.agents": enabled }, + }); + }, +); diff --git a/src/plugins/storage.ts b/src/plugins/storage.ts index 991492f61..e5b613b43 100644 --- a/src/plugins/storage.ts +++ b/src/plugins/storage.ts @@ -1,3 +1,4 @@ +import { migrateAgentFlag } from "./migrate-agent-flag"; import { invoke, isTauri } from "@tauri-apps/api/core"; import type { Catalog, @@ -29,7 +30,7 @@ export function createPluginStorage(bundledCatalog: () => Catalog["plugins"]) { ) { // This retired format controlled only the removed Welcome demo. } else if ( - value.version === 2 && + (value.version === 2 || value.version === 3) && "enabled" in value && value.enabled && typeof value.enabled === "object" && @@ -39,6 +40,13 @@ export function createPluginStorage(bundledCatalog: () => Catalog["plugins"]) { ) ) { const flags = value.enabled as Record; + if (value.version === 2) { + migrateAgentFlag(flags); + localStorage.setItem( + storageKey, + JSON.stringify({ version: 3, enabled: flags }), + ); + } for (const plugin of catalog.plugins) plugin.enabled = flags[plugin.manifest.id] ?? plugin.enabled; } else throw new Error("Invalid browser plugin settings"); @@ -73,7 +81,7 @@ export function createPluginStorage(bundledCatalog: () => Catalog["plugins"]) { localStorage.setItem( storageKey, JSON.stringify({ - version: 2, + version: 3, enabled: Object.fromEntries( catalog.plugins.map((plugin) => [ plugin.manifest.id, diff --git a/tests/browser/layout.spec.mjs b/tests/browser/layout.spec.mjs index 082ac3565..a4e124eaf 100644 --- a/tests/browser/layout.spec.mjs +++ b/tests/browser/layout.spec.mjs @@ -60,7 +60,7 @@ const destinationTitles = [ "Reminders", "Bestie", "Projects", - "Agents2", + "Agents", "Workflows", ]; const sidebarDestinations = (page, options = {}) => @@ -1080,7 +1080,7 @@ test("Projects directory fits the workspace and page navigation survives plugin "Reminders", "Bestie", "Projects", - "Agents2", + "Agents", "Sessions (now in Me)", "Workflows", ]; @@ -1162,7 +1162,7 @@ test("Projects directory fits the workspace and page navigation survives plugin "Inbox", "Reminders", "Bestie", - "Agents2", + "Agents", "Sessions (now in Me)", "Workflows", ]); diff --git a/tests/browser/navigation-repairs.spec.mjs b/tests/browser/navigation-repairs.spec.mjs index 1a2f8c016..1db1c202e 100644 --- a/tests/browser/navigation-repairs.spec.mjs +++ b/tests/browser/navigation-repairs.spec.mjs @@ -130,7 +130,7 @@ launchTest( await expect(failed).toBeVisible(); await openPage(page, "Projects"); await expect(failed).toBeHidden(); - await button(page, "Agents2").first().click(); + await button(page, "Agents").first().click(); await page.evaluate(() => { location.hash = "#buzz=%7B"; }); @@ -140,7 +140,7 @@ launchTest( await expect(failed).toBeVisible(); await button(page, "Go back").click(); await expect( - page.getByRole("heading", { name: "Agents2", exact: true }), + page.getByRole("heading", { name: "Agents", exact: true }), ).toBeVisible(); await button(page, "Go forward").click(); await expect(failed).toBeVisible(); diff --git a/tests/browser/navigation-sidebar.spec.mjs b/tests/browser/navigation-sidebar.spec.mjs index ec285f8c7..9b1a1ea2a 100644 --- a/tests/browser/navigation-sidebar.spec.mjs +++ b/tests/browser/navigation-sidebar.spec.mjs @@ -520,7 +520,7 @@ test("a reveal while the narrow drawer is closed runs when the drawer opens", as for (const destination of [ "Projects", - "Agents2", + "Agents", "Workflows", "Settings", "Back/Forward", diff --git a/tests/browser/navigation-sorting.spec.mjs b/tests/browser/navigation-sorting.spec.mjs index 0426388cf..85cfa2efb 100644 --- a/tests/browser/navigation-sorting.spec.mjs +++ b/tests/browser/navigation-sorting.spec.mjs @@ -97,7 +97,7 @@ test("section sort applies immediately, rolls back on failure, and persists retr await page.unroute("**/sidebar-sort"); // Error ownership is session-scoped, not coupled to the dismissed menu/page. await page - .getByRole("button", { name: "Agents2", exact: true }) + .getByRole("button", { name: "Agents", exact: true }) .first() .click(); await expect(sidebar).toBeVisible(); diff --git a/tests/browser/sidenav-polish.spec.mjs b/tests/browser/sidenav-polish.spec.mjs index 2ba13062a..4880973af 100644 --- a/tests/browser/sidenav-polish.spec.mjs +++ b/tests/browser/sidenav-polish.spec.mjs @@ -74,7 +74,7 @@ test("compact sidenav keeps its geometry across persistent page navigation", asy "Inbox", "Bestie", "Projects", - "Agents2", + "Agents", "Workflows", ].map((name) => pages.getByRole("button", { name, exact: true })); const assertDestinationFillParity = async () => { @@ -200,11 +200,11 @@ test("compact sidenav keeps its geometry across persistent page navigation", asy await expect(alpha).toBeFocused(); const node = await sidebar.elementHandle(); await page - .getByRole("button", { name: "Agents2", exact: true }) + .getByRole("button", { name: "Agents", exact: true }) .first() .click(); await expect( - page.getByRole("heading", { name: "Agents2", exact: true }), + page.getByRole("heading", { name: "Agents", exact: true }), ).toBeVisible(); await expect(sidebar).toBeVisible(); expect(await node.evaluate((element) => element.isConnected)).toBe(true); @@ -653,15 +653,15 @@ test("shell toggle restores the shared sidebar for Channels and Agents", async ( ) .toBe(expandedWidth); - await sidebar.getByRole("button", { name: "Agents2", exact: true }).click(); + await sidebar.getByRole("button", { name: "Agents", exact: true }).click(); await expect( - page.getByRole("heading", { name: "Agents2", exact: true }), + page.getByRole("heading", { name: "Agents", exact: true }), ).toBeVisible(); await page.getByRole("button", { name: "Hide Channel sidebar" }).click(); await expect(sidebar).not.toBeVisible(); await expect(rail).toBeVisible(); await expect( - page.getByRole("heading", { name: "Agents2", exact: true }), + page.getByRole("heading", { name: "Agents", exact: true }), ).toBeVisible(); await page.getByRole("button", { name: "Show Channel sidebar" }).click(); @@ -678,7 +678,7 @@ test("shell toggle restores the shared sidebar for Channels and Agents", async ( ) .toBe(expandedWidth); await expect( - sidebar.getByRole("button", { name: "Agents2", exact: true }), + sidebar.getByRole("button", { name: "Agents", exact: true }), ).toHaveAttribute("aria-current", "page"); }); diff --git a/tests/fixtures/conversation.tsx b/tests/fixtures/conversation.tsx index 163f2f819..882addb24 100644 --- a/tests/fixtures/conversation.tsx +++ b/tests/fixtures/conversation.tsx @@ -289,7 +289,7 @@ conversation.tools.subscribe(() => { ctx.provide("relay", relay); // Channels needs Agents for its members/templates; this fixture has no agents. const noAgents = { status: "unavailable", agents: [], running: [], teams: [] }; -ctx.provide("agents2", { +ctx.provide("agents", { snapshot: () => noAgents, subscribe: () => () => {}, }); diff --git a/tests/fixtures/mentions.tsx b/tests/fixtures/mentions.tsx index 132072592..f3ac92026 100644 --- a/tests/fixtures/mentions.tsx +++ b/tests/fixtures/mentions.tsx @@ -336,7 +336,7 @@ if (channels) { new SettingsCardsService(context); provideNavigation(context); // Channels requires Agents for its page/settings, neither mounted here. - context.provide("agents2", {}); + context.provide("agents", {}); // Only the composer is mounted; the Channels page never consumes this service. context.provide("relay", { snapshot: () => ({ session: namedSession }), diff --git a/tests/fixtures/profiles.tsx b/tests/fixtures/profiles.tsx index a5702b287..d85c12931 100644 --- a/tests/fixtures/profiles.tsx +++ b/tests/fixtures/profiles.tsx @@ -16,7 +16,7 @@ import { PagesService } from "../../src/features/pages/service"; import { ChannelWorkspaceFixture } from "./channel-workspace"; import { createRelaySession } from "../../src/features/relay/session"; import { localAgentsFixture } from "../../src/features/agents/local-testing"; -import { Agents2Service } from "../../src/features/agents2/service"; +import { AgentsService } from "../../src/features/agents/service"; import { provideNavigation } from "../../src/features/navigation/service"; import type { RelayData, @@ -317,8 +317,8 @@ const manager = createPluginManager(context, { : bundledPlugins.filter(({ manifest }) => manifest.id === "buzz.profiles"), }); const panels = new PanelsService(context); -// Profiles reads Agents2 to choose agent tabs; this fixture has no Agents2 agents. -new Agents2Service(context, relay, undefined); +// Profiles reads Agents to choose agent tabs; this fixture has no agents. +new AgentsService(context, relay, undefined); const pages = new PagesService(context); Object.assign(window, { profilesFixture: {