@agentorg/cli 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ AgentOrg as a dynamic system: Actors and widgets are modules in content-addresse
17
17
  - `@agentorg/kit`: the shell's own components, for the shell and for any view, drawn in the theme's tokens by the kit's styles (`KIT_CSS`, every class `kit-`, put first in the document's head when the kit is imported, so a view's own styles win). Accessible primitives on Radix: `Dialog` (focus held inside; Escape and a click outside close it), `Menu` with `MenuItem`, `Switch` and `Tabs`; plain elements for the rest: `Button` (`primary`, `quiet`), `IconButton` (its label is its name), `Field`, `Select` (the browser's own), `Card`, `List`, `Table`, `Meter` and `Actions`; layout pieces that fill and adapt: `Stack`, `Row`, `Fill`, `Scroll`, and `ResizableGroup`, `ResizablePanel` and `ResizeHandle` from react-resizable-panels. `Markdown` renders an agent's GitHub-flavored markdown with react-markdown, safe as it streams: remend first closes what's still open, and raw HTML is shown as text. `Chat` is a conversation on assistant-ui's primitives over an external store: the caller keeps the messages (`ChatEntry`, as `chat@1` has them) and runs the turns (`onSend`, `onCancel`, `onRetry`, `onEarlier`). The person's messages, each run of replies and tool messages as one reply in markdown, a tool message as a card (its name, how it ended, what went in and came back) when it says which tool ran and as its line otherwise; Enter sends, never mid-composition or during a turn, when Send is Stop; a refused send puts the text back; it fills its parent and goes compact below 460 px wide. Its tests run on happy-dom.
18
18
  - `@agentorg/wire`: the channel: yamux streams, length-prefixed JSON frames, stream headers and transports.
19
19
  - `installer`: a bundled host package that turns packages into working Actors, exporting `installer@1`. Bound to the Hub's management (`hub.packages`, `hub.actors`, `hub.grants`, `hub.exports` and `hub.bindings`, which makes it the person's delegated manager) and to `storage.admin@1`, it proposes and installs. `plan({package})` lists each slot the package doesn't wire itself, with the exports that could serve it now: exactly its interface and schema hash, on Actors that aren't stopped or closed. `install({package, name, connections, start?})` makes the Actor with exactly those connections, all or none. It grants nothing its caller couldn't: each connection's export side must be the caller's (the provider, bound to that export, or a manager), else the whole install is refused (`forbidden`, listing every such connection) and nothing is made. It seeds each store bound to `storage.kv` from the package's `seed` (an object of key to value per `module.slot`), starts the Actor if asked (a seed or start that fails closes it), and lists the slots left unbound and the seeds that couldn't land (`unseeded`). It never picks a connection itself. `install` records the new Actor as made for its caller (`installedFor`, which grants nothing), and takes `expose: [{export, to}]`: the new Actor's exports to expose, each to one Actor, all or none like the connections. A manager's are made at once (`granted`, with the slot); one to the caller itself is left for the caller to file, its result giving the exact `hub.grants.expose` call, which makes it the caller's own request (`request`); any other is refused `forbidden`, saying a manager could, and nothing is created. Only `rpc` exports of the package's host modules can be listed, and the Hub's own Actors take no slots. `plan({package, expose})` lists each entry with how it would settle, and refuses one `install` would refuse. Nothing is exposed that isn't listed. Bound to the person's `layout@1` too, it carries a system between homes as data: `exportSystem()`, for a manager only (it lists every binding and the person's layout; any other caller is refused `forbidden`), gives an `agentorg.system/1` composition (every package Actor that isn't closed, by a name of its own, with its package as `name@version#hash` and whether it runs; the connections among them and to the built-in Actors, as `{consumer, slot, provider, export}` by names; and the layout's placements), never credentials, tokens, stores or logs, and lists what it leaves out (external Actors and what connects to them). `importSystem({composition})` makes it again with the caller's authority (connecting the new Actors to each other takes a manager): it creates the Actors whose packages are here (a taken name gets a suffix), binds the connections, seeds, starts what ran and places the layout, all or nothing, and lists what it couldn't: a package that isn't here or is another version (whoever holds the composition's folder adds its archives first) and what depends on it, an Actor that needs a sign-in, a seed that can't land, and the placements unless the caller holds the person's layout. The exporting home's installer stays out: every home has its own. An import never grants management: a connection to `hub` or to `storage.admin` is recorded by an export but never made by an import, even a manager's (a composition may come from anyone); it comes back in `ungranted`, for the person to grant one by one.
20
- - `@agentorg/hub`: the core. Its state, in `node:sqlite` in a private home: Actors, a verified package store (`hub.packages.add` takes an archive by path; `hub.packages.build` takes a package's source as data, `package.json` and files under `src/` up to 768 KiB, builds it as `agentorg build` does, but reading no tsconfig, in a scratch folder of the home that is always removed: its files import only `@agentorg/sdk`, `@agentorg/interfaces`, `zod`, and for views `react`, `react-dom` and `@agentorg/kit` (JSX compiles to `react/jsx-runtime`), by name, each only within its own package, nothing outside the package's folder, and nothing by a pattern; a library's own imports are its own, and only the files resolved for it are read; a library's wrapped modules are named in the bundle by their library and version (`agentorg-library:react@19.3.1/cjs/react.production.js`), never by where it is installed, in `agentorg build` and Hub builds alike, so a package names no machine's folders and builds the same on any install; a Hub build resolves each allowed package's entry file from the Hub's own install, so in that file esbuild omits Node's ESM-to-CommonJS interop flag, which `agentorg build` keeps: no package allowed now behaves differently for it, but a Hub build's bytes can differ from `agentorg build`'s for packages that use them; at most 16 modules, 16 MiB built and 60 s; one build per caller at a time; and it adds the result. Reading each module's definition runs the source's code, so the Hub runs it in the same OS sandbox as host processes, and where those can't start, `build` is refused `unsupported` with the same reason. A failed build is `Refused("build")` with the build's message, which names no path outside the package), what each Actor takes and offers, bindings, binding requests and an audit trail. A binding is the grant, and it needs both ends: the export side (the provider, an Actor already bound to that export, or a manager) and the slot side (the consumer itself, or a manager). Through `hub.grants@1`, a caller with both ends binds; one with a single end approves the other end's open request or asks that end's approvers; one with neither is refused. `hub.grants.expose({provider, export, to, as?})` exposes an `rpc` export to one Actor: a slot made on `to` at run time, typed by the export's interface and bound to it in one step, named `as` (one word of at most 48 characters) or by default the provider's name camelCased, `_` and the export's last part (`Pocket Side Quests`' `logic.quests` is `pocketSideQuests_quests`), made unique on `to` with `_2`, `_3`…. Its authority is a binding's, `to` being the consumer: with both ends it is made; with one it is a request (`exposed: true` on the request, its `slot` the name it will have), which the other end approves by calling `expose` with the request's fields; with neither it is refused, naming who could. Exposing the same export to the same Actor again gives its slot back. A request reserves no name: the first exposure made under it has it, and the requests it settles close; one Actor has at most 32 exposure requests open toward another. An Actor that declares a slot of its own under an exposed slot's name takes the name, and the exposure goes. The slot goes with its binding: `unexpose({to, slot})`, `unbind`, the export withdrawn or either end closed; `hub.bindings` marks such a binding `exposed: true`. The Hub's own Actors take no slots. A manager creating an Actor for another (the installer, for its caller) passes `for`, shown as `installedFor` on the Actor: provenance only, from which the Hub grants nothing. A decision goes to the request's approvers and its requester, which can also read its requests' outcomes with `mine`. A manager is any Actor bound to `hub.bindings@1`. `hub.actors.create` can bind the new Actor's slots at once, with the creator's export side. The router serves it over WebSocket (`listen`), one yamux channel per Actor: a client's first stream is `hello` with its credential, then every `call`, `subscribe`, `open` and `http` stream is resolved through its binding, validated against the interface's JSON Schemas in both directions (ajv, 2020-12, in a pool of worker threads, each check under a time budget, since schemas come from Actors), forwarded to the provider with the caller named, and audited. A provider accepts or refuses an `open` or `http` stream with one body before any raw bytes. An Actor offers and withdraws exports at run time on its own channel (`offer`, `withdraw`). An Actor learns which of its slots are bound from hello's result, then from a `slot` notice on each change. A pipe the Hub makes for a package Actor's process says hello as that Actor without a credential (the pipe is the proof) and declares nothing (the manifest does). Every `serve` stream names the caller and the caller's slot, so a provider can keep state per consumer slot. `hub.actors.start` runs a package Actor's host modules in their own process: `node` under its permission model (reading only the runner and the package's folder, no child processes or workers), with an empty environment, a 256 MB heap and the channel on fd 3, inside the OS sandbox (`sandbox-exec` on macOS, a user, network and PID namespace with `unshare` on Linux), so it has no network, no DNS and no other process to signal. The Actor is running once its process says hello, within 30 s. `stop` sends SIGTERM, then SIGKILL after 5 s. A process that exits otherwise has crashed: it is restarted after a backoff from 1 s doubling to 60 s (starting over after 10 minutes up), and `hub.actors.get` counts `restarts` and keeps the `lastExit`. Every Actor's record in `hub.actors` says whether it is `connected`, holding a channel to the Hub now: an external Actor while its program is connected (the person while the app runs: its gateway holds the person's channel), a package Actor while its process is, so a running one isn't while its process starts or restarts; the built-in Actors are the Hub itself and have none. `hub.actors`' `changed` event names an Actor whose record may differ: made, started, stopped, closed, its process exited, or connected or gone; the audit trail records no connections. Actors running when the Hub stopped are started again when it opens. Where the sandbox can't run (stock Ubuntu restricts unprivileged user namespaces), `start` is refused with `unsupported` and every package Actor's `host` says why and what to change; the Hub probes again at each start. `hub.actors.logs` reads an Actor's log lines: its `ctx.log` entries and anything else its process writes (stdout as `info`, stderr as `error`). For the person's shell, `Hub.viewCall` and `Hub.viewSubscribe` run a package Actor's view module's calls and events in the Hub's process, as that Actor, through `<module>.<slot>`, and `Hub.viewSlots` tells the gateway a view module's slots: those bound now, then each change, and each bound again when it is usable again: its provider's channel comes back, or its Actor's own, since a view calls only while its Actor runs; `hub.actors.get` lists a package Actor's `views`. `Hub.viewLog` writes a view module's log line to its Actor's log, marked with the module; the Hub bounds each line itself (a known level, 4 KiB of text, 16 fields of 1 KiB) and rotates each Actor's log once at 1 MiB. The built-in `storage` Actor exports `kv@1`: one store per consumer slot (keyed by the consumer and slot the router names), in `<home>/storage.db`; values up to 256 KiB and 64 MiB per Actor, past which `Refused("quota")`; stores outlive unbinding and go when their Actor is closed. Its `admin` export (`storage.admin@1`, bound only to the installer) seeds a new Actor's store from its package's `seed`: all entries or none, into an empty store only, within the same limits, and audited as `storage.seed` with the consumer, the slot and how many entries. The built-in `net` Actor exports `egress` (`http.egress@1`): a consumer's request, made from the Hub's process to the public internet over HTTPS only. Its host is resolved and every address checked (loopback, private, shared, link-local including the cloud metadata address, multicast, reserved and documentation ranges, and their IPv4-mapped forms are refused), and the connection goes to the checked address with the name for SNI and the certificate, so a second resolution can't move it. Plain `http:`, a refused address and a name that resolves to one throw `Refused("forbidden")` at the consumer's `fetch`; a host that can't be reached before anything is sent throws `Refused("unreachable")`, and a failure after, or 5 minutes without a byte, throws `OutcomeUnknown`. Redirects come back as they are. Each request's method, host and status are audited as `net`'s; paths, queries and bodies never are. The `hub` Actor's six exports are served through the same path. A refusal that names an Actor names it as "Name (id)", and says what the caller can do instead: who could grant it, what to call, or what to do first.
20
+ - `@agentorg/hub`: the core. Its state, in `node:sqlite` in a private home: Actors, a verified package store (`hub.packages.add` takes an archive by path; `hub.packages.build` takes a package's source as data, `package.json` and files under `src/` up to 768 KiB, builds it as `agentorg build` does, but reading no tsconfig, in a scratch folder of the home that is always removed: its files import only `@agentorg/sdk`, `@agentorg/interfaces`, `zod`, and for views `react`, `react-dom` and `@agentorg/kit` (JSX compiles to `react/jsx-runtime`), by name, each only within its own package, nothing outside the package's folder, and nothing by a pattern; a library's own imports are its own, and only the files resolved for it are read; a library's wrapped modules are named in the bundle by their library and version (`agentorg-library:react@19.3.1/cjs/react.production.js`), never by where it is installed, in `agentorg build` and Hub builds alike, so a package names no machine's folders and builds the same on any install; a Hub build resolves each allowed package's entry file from the Hub's own install, so in that file esbuild omits Node's ESM-to-CommonJS interop flag, which `agentorg build` keeps: no package allowed now behaves differently for it, but a Hub build's bytes can differ from `agentorg build`'s for packages that use them; at most 16 modules, 16 MiB built and 60 s; one build per caller at a time; and it adds the result. Reading each module's definition runs the source's code, so the Hub runs it in the same OS sandbox as host processes, and where those can't start, `build` is refused `unsupported` with the same reason. A failed build is `Refused("build")` with the build's message, which names no path outside the package), what each Actor takes and offers, bindings, binding requests and an audit trail. A binding is the grant, and it needs both ends: the export side (the provider, an Actor already bound to that export, or a manager) and the slot side (the consumer itself, or a manager). Through `hub.grants@1`, a caller with both ends binds; one with a single end approves the other end's open request or asks that end's approvers; one with neither is refused. `hub.grants.expose({provider, export, to, as?})` exposes an `rpc` export to one Actor: a slot made on `to` at run time, typed by the export's interface and bound to it in one step, named `as` (one word of at most 48 characters) or by default the provider's name camelCased, `_` and the export's last part (`Pocket Side Quests`' `logic.quests` is `pocketSideQuests_quests`), made unique on `to` with `_2`, `_3`…. Its authority is a binding's, `to` being the consumer: with both ends it is made; with one it is a request (`exposed: true` on the request, its `slot` the name it will have), which the other end approves by calling `expose` with the request's fields; with neither it is refused, naming who could. Exposing the same export to the same Actor again gives its slot back. A request reserves no name: the first exposure made under it has it, and the requests it settles close; one Actor has at most 32 exposure requests open toward another. An Actor that declares a slot of its own under an exposed slot's name takes the name, and the exposure goes. The slot goes with its binding: `unexpose({to, slot})`, `unbind`, the export withdrawn or either end closed; `hub.bindings` marks such a binding `exposed: true`. The Hub's own Actors take no slots. A manager creating an Actor for another (the installer, for its caller) passes `for`, shown as `installedFor` on the Actor: provenance only, from which the Hub grants nothing. A decision goes to the request's approvers and its requester, which can also read its requests' outcomes with `mine`. A manager is any Actor bound to `hub.bindings@1`. `hub.actors.create` can bind the new Actor's slots at once, with the creator's export side. The router serves it over WebSocket (`listen`), one yamux channel per Actor: a client's first stream is `hello` with its credential, then every `call`, `subscribe`, `open` and `http` stream is resolved through its binding, validated against the interface's JSON Schemas in both directions (ajv, 2020-12, in a pool of worker threads, each check under a time budget, since schemas come from Actors), forwarded to the provider with the caller named, and audited. A provider accepts or refuses an `open` or `http` stream with one body before any raw bytes. An Actor offers and withdraws exports at run time on its own channel (`offer`, `withdraw`). An Actor learns which of its slots are bound from hello's result, then from a `slot` notice on each change. A pipe the Hub makes for a package Actor's process says hello as that Actor without a credential (the pipe is the proof) and declares nothing (the manifest does). Every `serve` stream names the caller and the caller's slot, so a provider can keep state per consumer slot. `hub.actors.start` runs a package Actor's host modules in their own process: `node` under its permission model (reading only the runner and the package's folder, no child processes or workers), with an empty environment, a 256 MB heap and the channel on fd 3, inside the OS sandbox (`sandbox-exec` on macOS, a user, network and PID namespace with `unshare` on Linux), so it has no network, no DNS and no other process to signal. The Actor is running once its process says hello, within 30 s. `stop` sends SIGTERM, then SIGKILL after 5 s. A process that exits otherwise has crashed: it is restarted after a backoff from 1 s doubling to 60 s (starting over after 10 minutes up), and `hub.actors.get` counts `restarts` and keeps the `lastExit`. Every Actor's record in `hub.actors` says whether it is `connected`, holding a channel to the Hub now: an external Actor while its program is connected (the person while the app runs: its gateway holds the person's channel), a package Actor while its process is, so a running one isn't while its process starts or restarts; the built-in Actors are the Hub itself and have none. `hub.actors`' `changed` event names an Actor whose record may differ: made, started, stopped, closed, its process exited, or connected or gone; the audit trail records no connections. Actors running when the Hub stopped are started again when it opens. `hub.actors.upgrade({actorId, package})` runs another package as the same package Actor, for a manager only, since the new package's code then holds everything the Actor was granted: its id, its stores and every binding whose interfaces still match stay, the new package's own wiring is made where its slot is unbound, and a running Actor restarts on it; a binding through a slot or export the new package drops or declares as another interface is dropped, audited and returned in `dropped`. When a redeclaration or an upgrade changes the interface of a bound name, the bindings whose other end doesn't declare the new one are dropped (audited as `binding.drop`) and the requests through it close: an external Actor's hello that redeclares a bound name as another is no longer refused for it. Dropping only takes access away. Where the sandbox can't run (stock Ubuntu restricts unprivileged user namespaces), `start` is refused with `unsupported` and every package Actor's `host` says why and what to change; the Hub probes again at each start. `hub.actors.logs` reads an Actor's log lines: its `ctx.log` entries and anything else its process writes (stdout as `info`, stderr as `error`). For the person's shell, `Hub.viewCall` and `Hub.viewSubscribe` run a package Actor's view module's calls and events in the Hub's process, as that Actor, through `<module>.<slot>`, and `Hub.viewSlots` tells the gateway a view module's slots: those bound now, then each change, and each bound again when it is usable again: its provider's channel comes back, or its Actor's own, since a view calls only while its Actor runs; `hub.actors.get` lists a package Actor's `views`. `Hub.viewLog` writes a view module's log line to its Actor's log, marked with the module; the Hub bounds each line itself (a known level, 4 KiB of text, 16 fields of 1 KiB) and rotates each Actor's log once at 1 MiB. The built-in `storage` Actor exports `kv@1`: one store per consumer slot (keyed by the consumer and slot the router names), in `<home>/storage.db`; values up to 256 KiB and 64 MiB per Actor, past which `Refused("quota")`; stores outlive unbinding and go when their Actor is closed. Its `admin` export (`storage.admin@1`, bound only to the installer) seeds a new Actor's store from its package's `seed`: all entries or none, into an empty store only, within the same limits, and audited as `storage.seed` with the consumer, the slot and how many entries. The built-in `net` Actor exports `egress` (`http.egress@1`): a consumer's request, made from the Hub's process to the public internet over HTTPS only. Its host is resolved and every address checked (loopback, private, shared, link-local including the cloud metadata address, multicast, reserved and documentation ranges, and their IPv4-mapped forms are refused), and the connection goes to the checked address with the name for SNI and the certificate, so a second resolution can't move it. Plain `http:`, a refused address and a name that resolves to one throw `Refused("forbidden")` at the consumer's `fetch`; a host that can't be reached before anything is sent throws `Refused("unreachable")`, and a failure after, or 5 minutes without a byte, throws `OutcomeUnknown`. Redirects come back as they are. Each request's method, host and status are audited as `net`'s; paths, queries and bodies never are. The `hub` Actor's six exports are served through the same path. A refusal that names an Actor names it as "Name (id)", and says what the caller can do instead: who could grant it, what to call, or what to do first.
21
21
  - `@agentorg/cli`: AgentOrg as one app. `agentorg` runs a Hub, the person's gateway and the shell on loopback; see "The app: the person's gateway".
22
22
  - `chatgpt`: the model Actor, a package with one host module. It signs in with ChatGPT for the person's plan (`account.login@1`: OpenAI's OAuth for open-source apps, with dynamic registration, PKCE and a verified id_token; refresh tokens rotate, one refresh at a time) and serves `llm.session@1` on that plan through the Responses API, streamed and stored nowhere. Its whole network is its `web: http.egress@1` slot and its sign-in lives in its `store: kv@1` slot; the tokens never leave it except to that store: never in a reply, an event, a log or a refusal. It never uses an API key or API billing: a usage limit is refused `usage_limit`, with where to manage it. Its tests run against an in-process fake of OpenAI, so none reaches OpenAI.
23
23
  - `assistant`: an agent, a package whose host module `logic` exports `chat@1` and runs the agent loop itself, with the model behind its `model: llm.session@1` slot. Its tools are its other bound slots (`layout`, `packages`, `actors`, `grants`, `exports` and `installer`), and every slot exposed to it at run time (`ctx.exposed()`): each method becomes a function tool named `<slot>__<method>` (a dot in the slot's name as `_`; a name past 64 characters is left out) with the method's input JSON Schema, so it can do exactly what it is connected to, and a call runs through the slot, checked and audited by the Hub. With `packages` and `installer: installer@1` bound, it can extend the system: its instructions then give the package format (views may use React and `@agentorg/kit`'s components), with a whole counter package and a view on the kit as the examples (`EXAMPLE` in `src/logic.ts`, built by its tests as the Hub builds), and the steps: `packages__build` with the files, fixing errors from the build's message; `installer__plan`; `installer__install` with explicit connections (a store to `storage.kv`); then `layout__place` with the new Actor's view. A turn makes at most 20 tool calls, and a function's output is cut at 16 KiB. Its chat is kept in its `store: kv@1` slot: each message once, its text and a call's arguments cut to fit a stored value, and the newest 2,000 messages (16 MiB) kept. The model is sent the newest whole exchanges that fit 400 KB of JSON (instructions and tool schemas aside), and always the latest one; if that alone is over, its oldest function outputs are left out, each call keeping an output. Its view module `chat` is the chat itself, the kit's `Chat` over its `chat: chat@1` slot: the record (the newest 100, earlier pages on request), the replies as they stream, in markdown, and a composer, with "Using ChatGPT plan" and "Manage usage" beside it, Retry under the last reply, and the action each refusal calls for (Manage usage, Continue with ChatGPT, Retry), which it asks of the shell through its `shell: ui.shell@1` slot.
@@ -137,7 +137,7 @@ From a clone, after `npm ci` and `npm run build`, with `agentorg` being `node pa
137
137
  - it makes the installer as the person, with its seven bindings (`hub.packages`, `hub.actors`, `hub.grants`, `hub.exports`, `hub.bindings`, `storage.admin` and `person.layout`), starts it, and binds the person's `installer` to it;
138
138
  - once the installer's process is connected, it installs ChatGPT through `installer.install`, with `model.web` to `net.egress` and `model.store` to `storage.kv`, started, and binds the person's `login` to its `model.login`.
139
139
 
140
- It records the Actors it made, and the bundled assistant's hash, in the person's store (`app/first-run`), and trusts only those, never an Actor because of the name its package gives itself. Each step reads what is there first, so a restart, or a first run that stopped half way, converges and makes nothing twice: a recorded Actor that lost one of its slots' bindings gets it again (of the slots its package declares), and one of an older build is kept. A recorded Actor that was closed is made again. A step that fails is logged and tried again at the next start.
140
+ It records the Actors it made, and the bundled assistant's hash, in the person's store (`app/first-run`), and trusts only those, never an Actor because of the name its package gives itself. Each step reads what is there first, so a restart, or a first run that stopped half way, converges and makes nothing twice: a recorded Actor that lost one of its slots' bindings gets it again (of the slots its package declares). A recorded Actor of an older build is upgraded in place to the bundled one (`hub.actors.upgrade`: the same id, store and matching bindings), and so is an agent on an assistant build an earlier run bundled, known by the hashes the first run records, never by a package's name; the bindings an upgrade drops are made again where their ends now match, and each upgrade is logged. A recorded Actor that was closed is made again. A step that fails is logged and tried again at the next start.
141
141
  - **`/oauth/chatgpt`:** where ChatGPT sends the browser back after sign-in. It takes `GET` only and hands the whole address, which holds the sign-in's code, to `login.complete`. Then it sends the tab on (303) to `/oauth/chatgpt?result=<outcome>`, so the code leaves the tab's address and history; that page says how it went and links back to the shell. The code is never logged or shown, and no referrer is sent.
142
142
  - **Named rules, `app.rules@1`:** conveniences the person turns on, served by the gateway on `/api` as `rules` (`list`, `set`, and `changed`). Each is named and off until the person turns it on, and the ones that are on are kept in the person's store under `app/rules`. The gateway acts on a rule that is on as the person, a manager, so only within what the person could grant: it hears each request through `grants.requested`, and goes through the open ones at start and whenever a rule is turned on. It approves a match the way an approver does, with the call the request asked for, and never declines; whatever no rule matches stays for the person.
143
143
  - **"Let an agent use what it installs"** (`agents-use-what-they-install`) approves a request to expose an Actor's export to the agent it was installed for (its `installedFor`), while the Actor isn't closed. `installedFor` is provenance only: nothing but a rule that is on acts on it.
@@ -1,6 +1,6 @@
1
1
  {
2
- "installer": "sha256-84165e3be5c904fc971d983dab31102e07cca725d8629a6fd7d118fa3a0a88b8.agentorg",
3
- "chatgpt": "sha256-f4330f5987f37d8cf3aeb44d2f831b573641a87186daa2f811f0c35ba04804f0.agentorg",
4
- "assistant": "sha256-ade9132b3f34b4c39db92adeadc7e540dc745e4a81b5b54f5d45bf9ec92bca75.agentorg",
5
- "board": "sha256-01d69bc3888c2a53963f4413d47de909d129ad27c78e9af6d830bb50c3478419.agentorg"
2
+ "installer": "sha256-4055f8fddf6b90f3d356543f554b2a9e9a2c4a8827a8774f8f87952dc1692331.agentorg",
3
+ "chatgpt": "sha256-e356ea3c1899138a8494baf521556831006f38534fa9e92ae873e7e6956c3a46.agentorg",
4
+ "assistant": "sha256-1f64ca62a0209d767d6aa6c4feb50b0653d60f10bebbc301f79bd3a37e0153c1.agentorg",
5
+ "board": "sha256-0e91cc3d0e9762b81abc2427b5496b20f383abcd6fb97d928a8d50e956b54d1f.agentorg"
6
6
  }
@@ -116,6 +116,39 @@ export class HubActor {
116
116
  await processes.stop(actorId);
117
117
  state.closeActor(caller, actorId);
118
118
  },
119
+ // A running Actor is stopped as `stop` does it, upgraded, and started again: on the new
120
+ // package, or on its own if the upgrade is refused. The machine's check comes first, so
121
+ // one that can't run it never has it stopped by an upgrade.
122
+ upgrade: async (caller, input) => {
123
+ const actorId = String(input.actorId);
124
+ const hash = String(input.package);
125
+ const actor = state.checkUpgrade(caller, actorId, hash);
126
+ if (actor.package === hash)
127
+ return { dropped: [] };
128
+ const running = actor.state === "running";
129
+ if (running) {
130
+ await processes.ready();
131
+ state.stop(caller, actorId);
132
+ await processes.stop(actorId);
133
+ }
134
+ const restart = () => {
135
+ // Started meanwhile, by another call: it runs already.
136
+ if (!running || state.actor(actorId).state === "running")
137
+ return;
138
+ state.start(caller, actorId);
139
+ processes.start(actorId);
140
+ };
141
+ let dropped;
142
+ try {
143
+ dropped = state.upgrade(caller, actorId, hash);
144
+ }
145
+ catch (error) {
146
+ restart();
147
+ throw error;
148
+ }
149
+ restart();
150
+ return { dropped };
151
+ },
119
152
  logs: (_, input) => {
120
153
  // The Actor must exist; its lines are its process's output and its views' lines.
121
154
  const { actorId } = state.actor(String(input.actorId));
@@ -115,6 +115,17 @@ declare const HUB_EXPORTS: {
115
115
  close: import("@agentorg/sdk").MethodSpec<import("zod").ZodObject<{
116
116
  actorId: import("zod").ZodString;
117
117
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{}, import("zod/v4/core").$strip>>;
118
+ upgrade: import("@agentorg/sdk").MethodSpec<import("zod").ZodObject<{
119
+ actorId: import("zod").ZodString;
120
+ package: import("zod").ZodString;
121
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
122
+ dropped: import("zod").ZodArray<import("zod").ZodObject<{
123
+ consumer: import("zod").ZodString;
124
+ slot: import("zod").ZodString;
125
+ provider: import("zod").ZodString;
126
+ export: import("zod").ZodString;
127
+ }, import("zod/v4/core").$strip>>;
128
+ }, import("zod/v4/core").$strip>>;
118
129
  logs: import("@agentorg/sdk").MethodSpec<import("zod").ZodObject<{
119
130
  actorId: import("zod").ZodString;
120
131
  limit: import("zod").ZodOptional<import("zod").ZodNumber>;
@@ -472,6 +483,16 @@ export declare class HubState {
472
483
  stop(caller: string, actorId: string): void;
473
484
  /** Close an Actor for good: its bindings both ways, its declarations and its open requests go. */
474
485
  closeActor(caller: string, actorId: string): void;
486
+ /** Refused unless the caller may upgrade the Actor to the package now; changes nothing. */
487
+ checkUpgrade(caller: string, actorId: string, hash: string): ActorRecord;
488
+ /**
489
+ * Run another package as the same Actor: its id, its stores and every binding whose interfaces
490
+ * still match stay; a binding through a slot or export the new package drops or declares as
491
+ * another interface is dropped, audited and returned. The new package's own wiring is applied
492
+ * where its slot is unbound. The process isn't touched here: a running Actor is stopped first
493
+ * and started after, by whoever runs processes.
494
+ */
495
+ upgrade(caller: string, actorId: string, hash: string): Wiring[];
475
496
  /** Add a `.agentorg` archive from a path on this machine, verified; adding it again is a no-op. */
476
497
  addPackage(caller: string, path: string): {
477
498
  hash: string;
@@ -493,8 +514,9 @@ export declare class HubState {
493
514
  declaration(actor: string, side: "slot" | "export", name: string): Declaration | undefined;
494
515
  /**
495
516
  * An external Actor's slots and static exports, as it declares them when it connects; they
496
- * replace what it declared before. A bound name keeps its interface: redeclaring it as another is
497
- * refused. A name it no longer declares is dropped, with its bindings.
517
+ * replace what it declared before. A name redeclared as another interface loses its bindings
518
+ * (audited as `binding.drop`), since a binding holds only while both ends declare the same one.
519
+ * A name it no longer declares is dropped, with its bindings.
498
520
  */
499
521
  declareExternal(actorId: string, declares: {
500
522
  slots: Record<string, DescribedInterface>;
@@ -143,6 +143,18 @@ const requestOf = (row) => ({
143
143
  ...(row.exposed ? { exposed: true } : {}),
144
144
  });
145
145
  const other = (side) => (side === "export" ? "slot" : "export");
146
+ /** What a package's Actor declares: each module's slots and exports, by `module.name`. */
147
+ function packageDeclarations(manifest) {
148
+ return Object.entries(manifest.modules).flatMap(([module, m]) => [
149
+ ["slot", m.slots],
150
+ ["export", m.exports],
151
+ ].flatMap(([side, members]) => Object.entries(members).map(([member, id]) => {
152
+ const entry = manifest.interfaces[id];
153
+ if (!entry)
154
+ throw new Refused("invalid", `The package doesn't record ${id}.`);
155
+ return { side, name: `${module}.${member}`, iface: { id, ...entry } };
156
+ })));
157
+ }
146
158
  /**
147
159
  * The core's state in its home folder: Actors, packages, declarations, bindings, binding requests
148
160
  * and the audit trail, in `node:sqlite`. Every method that changes something takes its caller, and
@@ -390,17 +402,8 @@ export class HubState {
390
402
  if (input.package) {
391
403
  const { manifest } = this.package(input.package);
392
404
  insert.run(actorId, input.name, input.package, null, now(), madeFor);
393
- for (const [module, m] of Object.entries(manifest.modules))
394
- for (const [side, members] of [
395
- ["slot", m.slots],
396
- ["export", m.exports],
397
- ])
398
- for (const [member, id] of Object.entries(members)) {
399
- const entry = manifest.interfaces[id];
400
- if (!entry)
401
- throw new Refused("invalid", `The package doesn't record ${id}.`);
402
- this.#declare(actorId, side, `${module}.${member}`, { id, ...entry }, false);
403
- }
405
+ for (const { side, name, iface } of packageDeclarations(manifest))
406
+ this.#declare(actorId, side, name, iface, false);
404
407
  // A package's own wiring is applied as it is installed.
405
408
  for (const [slot, exported] of manifest.connections)
406
409
  this.#insertBinding({ consumer: actorId, slot, provider: actorId, export: exported }, HUB);
@@ -494,6 +497,112 @@ export class HubState {
494
497
  this.#emit(() => this.#events.closed?.(actorId));
495
498
  });
496
499
  }
500
+ /**
501
+ * Drop the bindings through one of an Actor's slots or exports, its interface having changed,
502
+ * but those `keeps` says still match: each is audited as `binding.drop`. Dropping only takes
503
+ * access away. Returns what was dropped.
504
+ */
505
+ #dropBindingsOf(actorId, side, name, by, keeps = () => false) {
506
+ const dropped = this.bindings(actorId).filter((b) => (side === "slot"
507
+ ? b.consumer === actorId && b.slot === name
508
+ : b.provider === actorId && b.export === name) && !keeps(b));
509
+ for (const b of dropped)
510
+ this.#removeBindings("consumer = ? AND slot = ?", b.consumer, b.slot);
511
+ const wirings = dropped.map(({ consumer, slot, provider, export: exported }) => ({
512
+ consumer,
513
+ slot,
514
+ provider,
515
+ export: exported,
516
+ }));
517
+ for (const wiring of wirings)
518
+ this.#audit(by, "binding.drop", { ...wiring, why: "its interface changed" });
519
+ return wirings;
520
+ }
521
+ /** Close the open requests to bind through one of an Actor's slots or exports. */
522
+ #closeRequestsThrough(actorId, side, name) {
523
+ const where = side === "slot" ? "consumer = ? AND slot = ?" : "provider = ? AND export = ?";
524
+ for (const open of this.#pending(where, actorId, name))
525
+ this.#decide(open, "closed");
526
+ }
527
+ /** Refused unless the caller may upgrade the Actor to the package now; changes nothing. */
528
+ checkUpgrade(caller, actorId, hash) {
529
+ this.#require(caller, "actors");
530
+ // An upgrade runs new code as an Actor that keeps its bindings and its store: whatever it was
531
+ // granted, the new code holds. Only a manager, who could grant all of it anyway, may.
532
+ if (!this.isManager(caller))
533
+ throw new Refused("forbidden", `Only a manager may upgrade an Actor, since the new package keeps its bindings and its store; ${this.named(caller)} isn't one.`);
534
+ const actor = this.actor(actorId);
535
+ if (actorId === HUB || actorId === PERSON || actorId === STORAGE || actorId === NET)
536
+ throw new Refused("forbidden", `${this.named(actorId)} is built in and can't be upgraded.`);
537
+ if (actor.package === "external")
538
+ throw new Refused("state", `${this.named(actorId)} isn't a package Actor; it declares what it takes when it connects.`);
539
+ if (actor.state === "closed")
540
+ throw new Refused("state", `${this.named(actorId)} is closed for good; make another.`);
541
+ packageDeclarations(this.package(hash).manifest);
542
+ return actor;
543
+ }
544
+ /**
545
+ * Run another package as the same Actor: its id, its stores and every binding whose interfaces
546
+ * still match stay; a binding through a slot or export the new package drops or declares as
547
+ * another interface is dropped, audited and returned. The new package's own wiring is applied
548
+ * where its slot is unbound. The process isn't touched here: a running Actor is stopped first
549
+ * and started after, by whoever runs processes.
550
+ */
551
+ upgrade(caller, actorId, hash) {
552
+ const actor = this.checkUpgrade(caller, actorId, hash);
553
+ if (actor.package === hash)
554
+ return [];
555
+ const { manifest } = this.package(hash);
556
+ const next = packageDeclarations(manifest);
557
+ const declared = (side, name) => next.find((n) => n.side === side && n.name === name)?.iface;
558
+ const dropped = [];
559
+ this.#transaction(() => {
560
+ // What it offered at run time was its process's, which this restarts: it goes, with the
561
+ // bindings to it, which are returned with the rest.
562
+ for (const row of this.#db
563
+ .prepare("SELECT name FROM declarations WHERE actor = ? AND side = 'export' AND dynamic = 1")
564
+ .all(actorId)) {
565
+ dropped.push(...this.#dropBindingsOf(actorId, "export", String(row.name), caller));
566
+ this.#drop(actorId, "export", String(row.name));
567
+ }
568
+ // A binding stays while its two ends declare the same interface: this Actor's end as the
569
+ // new package declares it, the other end as it is declared now.
570
+ const keeps = (b) => {
571
+ const slot = b.consumer === actorId
572
+ ? declared("slot", b.slot)
573
+ : this.declaration(b.consumer, "slot", b.slot)?.iface;
574
+ const exported = b.provider === actorId
575
+ ? declared("export", b.export)
576
+ : this.declaration(b.provider, "export", b.export)?.iface;
577
+ return !!slot && !!exported && compareInterfaces(slot, exported) === "same";
578
+ };
579
+ const old = this.#db
580
+ .prepare("SELECT side, name, iface FROM declarations WHERE actor = ? AND dynamic = 0")
581
+ .all(actorId).map((row) => ({
582
+ side: row.side,
583
+ name: String(row.name),
584
+ iface: JSON.parse(String(row.iface)),
585
+ }));
586
+ for (const was of old) {
587
+ dropped.push(...this.#dropBindingsOf(actorId, was.side, was.name, caller, keeps));
588
+ // A request through a name the package no longer declares as it was can't be made.
589
+ const fresh = declared(was.side, was.name);
590
+ if (!fresh || compareInterfaces(was.iface, fresh) !== "same")
591
+ this.#closeRequestsThrough(actorId, was.side, was.name);
592
+ }
593
+ this.#db.prepare("DELETE FROM declarations WHERE actor = ? AND dynamic = 0").run(actorId);
594
+ for (const { side, name, iface } of next)
595
+ this.#declare(actorId, side, name, iface, false);
596
+ this.#db.prepare("UPDATE actors SET package = ? WHERE id = ?").run(hash, actorId);
597
+ for (const [slot, exported] of manifest.connections)
598
+ if (!this.binding(actorId, slot))
599
+ this.#insertBinding({ consumer: actorId, slot, provider: actorId, export: exported }, HUB);
600
+ this.#audit(caller, "actor.upgrade", { actorId, from: actor.package, to: hash, dropped });
601
+ this.#emit(() => this.#events.exportsChanged?.(actorId));
602
+ this.#emit(() => this.#events.actorChanged?.(actorId));
603
+ });
604
+ return dropped;
605
+ }
497
606
  // ── Packages ────────────────────────────────────────────────────────────────
498
607
  /** Add a `.agentorg` archive from a path on this machine, verified; adding it again is a no-op. */
499
608
  addPackage(caller, path) {
@@ -604,8 +713,9 @@ export class HubState {
604
713
  }
605
714
  /**
606
715
  * An external Actor's slots and static exports, as it declares them when it connects; they
607
- * replace what it declared before. A bound name keeps its interface: redeclaring it as another is
608
- * refused. A name it no longer declares is dropped, with its bindings.
716
+ * replace what it declared before. A name redeclared as another interface loses its bindings
717
+ * (audited as `binding.drop`), since a binding holds only while both ends declare the same one.
718
+ * A name it no longer declares is dropped, with its bindings.
609
719
  */
610
720
  declareExternal(actorId, declares) {
611
721
  const actor = this.#open(actorId);
@@ -628,11 +738,19 @@ export class HubState {
628
738
  for (const open of this.#pending("consumer = ? AND slot = ? AND exposed = 1", actorId, name))
629
739
  this.#decide(open, "closed");
630
740
  }
741
+ // A binding holds while both ends declare the same interface: redeclared as another,
742
+ // the name's bindings whose other end doesn't declare it are dropped, and its requests
743
+ // close.
631
744
  const known = this.declaration(actorId, side, name);
632
- if (known &&
633
- this.#isBound(actorId, side, name) &&
634
- compareInterfaces(known.iface, iface) !== "same")
635
- throw new Refused("conflict", `${name} is bound as ${known.iface.id}; it can't change while bound.`);
745
+ if (known && compareInterfaces(known.iface, iface) !== "same") {
746
+ this.#dropBindingsOf(actorId, side, name, actorId, (b) => {
747
+ const other = side === "slot"
748
+ ? this.declaration(b.provider, "export", b.export)?.iface
749
+ : this.declaration(b.consumer, "slot", b.slot)?.iface;
750
+ return !!other && compareInterfaces(iface, other) === "same";
751
+ });
752
+ this.#closeRequestsThrough(actorId, side, name);
753
+ }
636
754
  this.#declare(actorId, side, name, iface, false);
637
755
  }
638
756
  const kept = Object.keys(members);
@@ -648,13 +766,6 @@ export class HubState {
648
766
  this.#emit(() => this.#events.exportsChanged?.(actorId));
649
767
  });
650
768
  }
651
- #isBound(actor, side, name) {
652
- return !!this.#db
653
- .prepare(side === "slot"
654
- ? "SELECT 1 FROM bindings WHERE consumer = ? AND slot = ?"
655
- : "SELECT 1 FROM bindings WHERE provider = ? AND export = ?")
656
- .get(actor, name);
657
- }
658
769
  /** A declaration gone, and the bindings through it. */
659
770
  #drop(actor, side, name) {
660
771
  this.#db
@@ -172,6 +172,25 @@ export declare const hubActors: import("@agentorg/sdk").RpcInterface<"hub.actors
172
172
  close: import("@agentorg/sdk").MethodSpec<z.ZodObject<{
173
173
  actorId: z.ZodString;
174
174
  }, z.core.$strip>, z.ZodObject<{}, z.core.$strip>>;
175
+ /**
176
+ * Run another package as the same package Actor: its id, its stores and every binding whose
177
+ * interfaces still match stay. A binding through a slot or export the new package drops, or
178
+ * declares as another interface, is dropped and listed in `dropped`; the new package's own
179
+ * wiring is made where its slot is unbound. A running Actor restarts on the new package.
180
+ * Only a manager may upgrade: the new package runs as the Actor and keeps what it was granted
181
+ * (its bindings and its store), which only a manager could grant anyway.
182
+ */
183
+ upgrade: import("@agentorg/sdk").MethodSpec<z.ZodObject<{
184
+ actorId: z.ZodString;
185
+ package: z.ZodString;
186
+ }, z.core.$strip>, z.ZodObject<{
187
+ dropped: z.ZodArray<z.ZodObject<{
188
+ consumer: z.ZodString;
189
+ slot: z.ZodString;
190
+ provider: z.ZodString;
191
+ export: z.ZodString;
192
+ }, z.core.$strip>>;
193
+ }, z.core.$strip>>;
175
194
  /** Its newest log lines, oldest first. */
176
195
  logs: import("@agentorg/sdk").MethodSpec<z.ZodObject<{
177
196
  actorId: z.ZodString;
@@ -120,6 +120,20 @@ export const hubActors = defineInterface({
120
120
  stop: method({ input: z.object({ actorId: Id }), output: Empty }),
121
121
  /** Final: its bindings, both ways, are removed. */
122
122
  close: method({ input: z.object({ actorId: Id }), output: Empty }),
123
+ /**
124
+ * Run another package as the same package Actor: its id, its stores and every binding whose
125
+ * interfaces still match stay. A binding through a slot or export the new package drops, or
126
+ * declares as another interface, is dropped and listed in `dropped`; the new package's own
127
+ * wiring is made where its slot is unbound. A running Actor restarts on the new package.
128
+ * Only a manager may upgrade: the new package runs as the Actor and keeps what it was granted
129
+ * (its bindings and its store), which only a manager could grant anyway.
130
+ */
131
+ upgrade: method({
132
+ input: z.object({ actorId: Id, package: Hash }),
133
+ output: z.object({
134
+ dropped: z.array(z.object({ consumer: Id, slot: Name, provider: Id, export: Name })),
135
+ }),
136
+ }),
123
137
  /** Its newest log lines, oldest first. */
124
138
  logs: method({
125
139
  input: z.object({ actorId: Id, limit: z.number().int().min(1).max(1000).optional() }),
@@ -35,10 +35,13 @@ export interface FirstRun {
35
35
  * The first run, at every start after the person's store is bound: add the bundled packages,
36
36
  * ensure the installer (made by the person, with its bindings, started), bind the person's
37
37
  * `installer` to it, ensure the model Actor installed through it (`web` to `egress`, `store` to
38
- * storage's `kv`, started), and bind the person's `login` to the model's. What is there already is
39
- * kept: an installer or model Actor an earlier run made (recorded in the person's store) stays,
40
- * of whatever build. A step that fails is logged and the rest still runs where it can; the next
41
- * start tries again.
38
+ * storage's `kv`, started), and bind the person's `login` to the model's. What earlier runs made
39
+ * (recorded in the person's store) is kept and brought to this build: an installer or model Actor
40
+ * on another package is upgraded in place, with its id, its store and its bindings; so is an agent
41
+ * on an assistant build an earlier run bundled (by the hashes recorded, never by a package's
42
+ * name). A binding an upgrade drops, its interface having changed, is made again where its ends
43
+ * match now. A step that fails is logged and the rest still runs where it can; the next start
44
+ * tries again.
42
45
  */
43
46
  export declare function firstRun(person: PersonCalls, options?: FirstRunOptions): Promise<FirstRun>;
44
47
  /**
package/dist/src/model.js CHANGED
@@ -35,10 +35,13 @@ const INSTALLER_BINDINGS = [
35
35
  * The first run, at every start after the person's store is bound: add the bundled packages,
36
36
  * ensure the installer (made by the person, with its bindings, started), bind the person's
37
37
  * `installer` to it, ensure the model Actor installed through it (`web` to `egress`, `store` to
38
- * storage's `kv`, started), and bind the person's `login` to the model's. What is there already is
39
- * kept: an installer or model Actor an earlier run made (recorded in the person's store) stays,
40
- * of whatever build. A step that fails is logged and the rest still runs where it can; the next
41
- * start tries again.
38
+ * storage's `kv`, started), and bind the person's `login` to the model's. What earlier runs made
39
+ * (recorded in the person's store) is kept and brought to this build: an installer or model Actor
40
+ * on another package is upgraded in place, with its id, its store and its bindings; so is an agent
41
+ * on an assistant build an earlier run bundled (by the hashes recorded, never by a package's
42
+ * name). A binding an upgrade drops, its interface having changed, is made again where its ends
43
+ * match now. A step that fails is logged and the rest still runs where it can; the next start
44
+ * tries again.
42
45
  */
43
46
  export async function firstRun(person, options = {}) {
44
47
  const log = options.log ?? (() => { });
@@ -63,17 +66,48 @@ export async function firstRun(person, options = {}) {
63
66
  // What earlier runs made, by role, kept while it isn't closed. Unread, nothing is made: a run that
64
67
  // couldn't see its record would make a second installer.
65
68
  const read = (await call("store", "get", { key: RECORD })).value;
66
- const record = Object.fromEntries(Object.entries((read ?? {})).filter(([role, id]) => (role === "installer" || role === "chatgpt") && typeof id === "string"));
69
+ const kept = (read ?? {});
70
+ // Every assistant build a run has bundled, by hash: an agent on one of them is this app's to
71
+ // bring to this build. Never a package by its name, which an agent's own package could take.
72
+ const earlier = [
73
+ ...(Array.isArray(kept.assistants) ? kept.assistants : []),
74
+ kept.assistant,
75
+ ].filter((hash) => typeof hash === "string");
76
+ const isWiring = (wiring) => typeof wiring === "object" &&
77
+ wiring !== null &&
78
+ ["consumer", "slot", "provider", "export"].every((key) => typeof wiring[key] === "string");
79
+ const record = {
80
+ ...Object.fromEntries(Object.entries(kept).filter(([role, id]) => (role === "installer" || role === "chatgpt" || role === "assistant") &&
81
+ typeof id === "string")),
82
+ ...(earlier.length ? { assistants: [...new Set(earlier)] } : {}),
83
+ ...(Array.isArray(kept.rebind) && kept.rebind.some(isWiring)
84
+ ? { rebind: kept.rebind.filter(isWiring) }
85
+ : {}),
86
+ };
87
+ const save = () => call("store", "set", { key: RECORD, value: { ...record } });
67
88
  const remember = (role, actorId) => {
68
89
  record[role] = actorId;
69
- return call("store", "set", { key: RECORD, value: { ...record } });
90
+ return save();
70
91
  };
71
- // The assistant New agent installs is this build's, by hash: never a package by its name, which
72
- // an agent's own package could take.
92
+ // The assistant New agent installs is this build's, by hash.
73
93
  if (packages.assistant) {
74
94
  record.assistant = packages.assistant;
75
- await attempt("recording the assistant", () => call("store", "set", { key: RECORD, value: { ...record } }));
95
+ record.assistants = [...new Set([...earlier, packages.assistant])];
96
+ await attempt("recording the assistant", save);
76
97
  }
98
+ /** Run this build's package as an Actor an earlier run made, keeping its id and its store. */
99
+ const upgrade = async (actor, hash, what) => {
100
+ const { dropped: lost } = await call("actors", "upgrade", {
101
+ actorId: actor.actorId,
102
+ package: hash,
103
+ });
104
+ if (lost.length) {
105
+ record.rebind = [...(record.rebind ?? []), ...lost];
106
+ await attempt("recording the bindings to make again", save);
107
+ }
108
+ log(`First run: upgraded ${what} (${actor.actorId}) to this build${lost.length ? `; ${lost.length} of its bindings to make again` : ""}.`);
109
+ return { ...actor, package: hash };
110
+ };
77
111
  /** The Actor an earlier run made for `role`, unless it is gone or closed. */
78
112
  const madeBefore = async (role) => {
79
113
  const id = record[role];
@@ -129,9 +163,12 @@ export async function firstRun(person, options = {}) {
129
163
  actor = { actorId, name: "Installer", package: hash, state: "created" };
130
164
  }
131
165
  else {
166
+ const hash = packages[INSTALLER];
167
+ const before = actor;
168
+ if (hash && actor.package !== hash)
169
+ actor =
170
+ (await attempt("upgrading the installer", () => upgrade(before, hash, "the installer"))) ?? actor;
132
171
  await rebind(actor, INSTALLER_BINDINGS);
133
- if (actor.package !== packages[INSTALLER])
134
- log("First run: kept the installer; the bundled build is added, not used.");
135
172
  }
136
173
  await personSlot("installer", { actor: actor.actorId, export: "installer.installer" });
137
174
  await running(actor);
@@ -143,11 +180,14 @@ export async function firstRun(person, options = {}) {
143
180
  { slot: "model.store", provider: "storage", export: "kv" },
144
181
  ];
145
182
  const chatgpt = await attempt("ChatGPT", async () => {
146
- const actor = await madeBefore("chatgpt");
183
+ let actor = await madeBefore("chatgpt");
147
184
  if (actor) {
185
+ const hash = packages[CHATGPT];
186
+ const before = actor;
187
+ if (hash && actor.package !== hash)
188
+ actor =
189
+ (await attempt("upgrading ChatGPT", () => upgrade(before, hash, "ChatGPT"))) ?? actor;
148
190
  await rebind(actor, chatgptBindings);
149
- if (actor.package !== packages[CHATGPT])
150
- log("First run: kept ChatGPT; the bundled build is added, not used.");
151
191
  await running(actor);
152
192
  return actor.actorId;
153
193
  }
@@ -176,6 +216,35 @@ export async function firstRun(person, options = {}) {
176
216
  });
177
217
  if (chatgpt)
178
218
  await attempt("binding the login", () => personSlot("login", { actor: chatgpt, export: "model.login" }));
219
+ // Agents on an assistant build an earlier run bundled: brought to this build's assistant.
220
+ const assistant = packages.assistant;
221
+ if (assistant) {
222
+ const agents = await attempt("finding agents on earlier builds", async () => (await call("actors", "list")).actors.filter((a) => a.state !== "closed" && a.package !== assistant && earlier.includes(a.package)));
223
+ for (const agent of agents ?? [])
224
+ await attempt(`upgrading ${agent.name}`, () => upgrade(agent, assistant, `the agent ${agent.name}`));
225
+ }
226
+ // What upgrades dropped, this run or one that stopped before this, made again where its slot is
227
+ // still unbound: the ends now match, both on this build. One that can't be yet (an upgrade
228
+ // failed, or its ends still differ) is logged and kept for the next start.
229
+ if (record.rebind) {
230
+ const left = [];
231
+ for (const wiring of record.rebind) {
232
+ const made = await attempt(`binding ${wiring.slot} again`, async () => {
233
+ if ((await bindingsOf(wiring.consumer)).some((b) => b.slot === wiring.slot))
234
+ return true;
235
+ await call("grants", "bind", wiring);
236
+ log(`First run: bound ${wiring.consumer}'s ${wiring.slot} again.`);
237
+ return true;
238
+ });
239
+ if (!made)
240
+ left.push(wiring);
241
+ }
242
+ if (left.length)
243
+ record.rebind = left;
244
+ else
245
+ delete record.rebind;
246
+ await attempt("recording the bindings made again", save);
247
+ }
179
248
  return {
180
249
  packages,
181
250
  ...(installer ? { installer } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentorg/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "AgentOrg as one app: the Hub, the person's gateway and the shell, on loopback.",
5
5
  "license": "MIT",
6
6
  "repository": {