@agentconnect.md/daemon 1.0.0-rc.27 → 1.0.0-rc.29

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/dist/index.js CHANGED
@@ -17215,6 +17215,7 @@ const AgentWorkspace = discriminatedUnion("mode", [object({ mode: literal("scrat
17215
17215
  const AgentSpec = object({
17216
17216
  name: string(),
17217
17217
  description: string().optional(),
17218
+ runtime: string().optional(),
17218
17219
  model: string().optional(),
17219
17220
  reasoningEffort: string().optional(),
17220
17221
  executionMode: string().optional(),
@@ -23094,27 +23095,6 @@ function watch$1(paths, options = {}) {
23094
23095
  }
23095
23096
  //#endregion
23096
23097
  //#region src/agents/cp-overlay.ts
23097
- /** Apply a CP `AgentSpec` onto a local agent, returning a new overlaid agent. */
23098
- function overlayCpSpec(base, spec) {
23099
- const envMap = new Map((base.runtimeOverrides?.env ?? []).map((e) => [e.name, e.value]));
23100
- for (const [name, value] of Object.entries(spec.env ?? {})) envMap.set(name, value);
23101
- const env = [...envMap].map(([name, value]) => ({
23102
- name,
23103
- value
23104
- }));
23105
- return {
23106
- ...base,
23107
- name: spec.name,
23108
- ...spec.description !== void 0 ? { description: spec.description } : {},
23109
- ...spec.reasoningEffort !== void 0 ? { reasoningEffort: spec.reasoningEffort } : {},
23110
- ...spec.executionMode !== void 0 ? { executionMode: spec.executionMode } : {},
23111
- runtimeOverrides: {
23112
- ...base.runtimeOverrides,
23113
- ...spec.model !== void 0 ? { model: spec.model } : {},
23114
- env
23115
- }
23116
- };
23117
- }
23118
23098
  /**
23119
23099
  * Runtime config the daemon can only surface to an ACP child as environment
23120
23100
  * variables: ACP `session/new`/`initialize` carry no model or system-prompt
@@ -23188,10 +23168,6 @@ var LocalStore = class {
23188
23168
  id INTEGER PRIMARY KEY CHECK (id = 1),
23189
23169
  routingEpoch INTEGER, assignments TEXT, globalRules TEXT
23190
23170
  );
23191
- CREATE TABLE IF NOT EXISTS cp_agents (
23192
- id INTEGER PRIMARY KEY CHECK (id = 1),
23193
- specs TEXT
23194
- );
23195
23171
  `);
23196
23172
  }
23197
23173
  getSession(key) {
@@ -23225,13 +23201,6 @@ var LocalStore = class {
23225
23201
  globalRules
23226
23202
  });
23227
23203
  }
23228
- getCpAgents() {
23229
- return this.db.prepare("SELECT specs FROM cp_agents WHERE id = 1").get();
23230
- }
23231
- setCpAgents(specs) {
23232
- this.db.prepare(`INSERT INTO cp_agents (id, specs) VALUES (1, @specs)
23233
- ON CONFLICT(id) DO UPDATE SET specs=excluded.specs`).run({ specs });
23234
- }
23235
23204
  close() {
23236
23205
  this.db.close();
23237
23206
  }
@@ -81148,42 +81117,164 @@ var CpCronRegistry = class {
81148
81117
  }
81149
81118
  };
81150
81119
  //#endregion
81120
+ //#region src/agents/write-agent.ts
81121
+ /**
81122
+ * Persist CP-owned agent spec onto the daemon's on-disk `agent.json`, which is
81123
+ * the SINGLE SOURCE OF TRUTH for agent config. The CP pushes an `AgentSpec`
81124
+ * (agent/upsert + the register/ok roster); we map its CP-owned fields onto the
81125
+ * matching `<agentsDir>/<id>/agent.json`:
81126
+ *
81127
+ * - if one with that id exists → field-level MERGE the CP-owned keys into it;
81128
+ * - if none exists → CREATE a new `<agentsDir>/<id>/agent.json`.
81129
+ *
81130
+ * CRITICAL: we operate on the RAW JSON TEXT (readFileSync → JSON.parse → set
81131
+ * only CP-owned keys → writeFileSync). We never serialize the parsed/interpolated
81132
+ * `LoadedAgent` — that has `${VAR}` already substituted (secrets baked in) and
81133
+ * `workspace.path` rewritten to an absolute path. Editing the raw file preserves
81134
+ * `${VAR}` templates and the original relative `workspace.path`.
81135
+ *
81136
+ * Locally-owned keys (id, status, runtime, integrations, output, permissions,
81137
+ * crons, workspace.path / pullOnNewSession / skills) are preserved untouched.
81138
+ */
81139
+ /** Map a CP workspace mode onto the daemon's AgentSchema workspace mode. */
81140
+ function mapWorkspaceMode(mode) {
81141
+ return mode === "scratch" ? "from-scratch" : "git-repo";
81142
+ }
81143
+ /**
81144
+ * Locate the on-disk `agent.json` whose INTERNAL `id` equals `agentId`.
81145
+ * Agents live under arbitrary directory layouts (discoverAgents walks recursively
81146
+ * and the dir name need NOT equal the id), so we must match by parsed id — never
81147
+ * assume the file sits at `<agentsDir>/<id>/agent.json`. Returns the file path, or
81148
+ * undefined if no agent with that id exists. Unparseable files are skipped.
81149
+ */
81150
+ function findAgentFileById(agentsDir, agentId) {
81151
+ for (const file of findAgentFiles(agentsDir)) try {
81152
+ if (JSON.parse(readFileSync(file, "utf8")).id === agentId) return file;
81153
+ } catch {}
81154
+ }
81155
+ /**
81156
+ * Apply the CP-owned spec fields onto a parsed raw agent.json object IN PLACE.
81157
+ * `creating` controls whether workspace.path may be set (only on create — an
81158
+ * update never overwrites a path).
81159
+ */
81160
+ function applySpecFields(raw, spec, opts) {
81161
+ raw.name = spec.name;
81162
+ if (spec.description !== void 0) raw.description = spec.description;
81163
+ if (spec.reasoningEffort !== void 0) raw.reasoningEffort = spec.reasoningEffort;
81164
+ if (spec.executionMode !== void 0) raw.executionMode = spec.executionMode;
81165
+ if (spec.model !== void 0 || spec.env !== void 0) {
81166
+ const ro = typeof raw.runtimeOverrides === "object" && raw.runtimeOverrides !== null ? raw.runtimeOverrides : {};
81167
+ if (spec.model !== void 0) ro.model = spec.model;
81168
+ if (spec.env !== void 0) ro.env = Object.entries(spec.env).map(([name, value]) => ({
81169
+ name,
81170
+ value
81171
+ }));
81172
+ raw.runtimeOverrides = ro;
81173
+ }
81174
+ if (spec.workspace !== void 0) {
81175
+ const ws = spec.workspace;
81176
+ const existing = typeof raw.workspace === "object" && raw.workspace !== null ? raw.workspace : {};
81177
+ existing.mode = mapWorkspaceMode(ws.mode);
81178
+ if (ws.mode === "github") {
81179
+ existing.gitRepo = ws.gitRepo;
81180
+ existing.gitBranch = ws.branch;
81181
+ }
81182
+ if (opts.creating && existing.path === void 0) existing.path = join(opts.agentDir, "workspace");
81183
+ raw.workspace = existing;
81184
+ }
81185
+ }
81186
+ /**
81187
+ * Create-or-merge the CP spec onto `<agentsDir>/<agentId>/agent.json`.
81188
+ *
81189
+ * MERGE (file exists): re-read the raw JSON text, set only CP-owned keys,
81190
+ * write it back pretty-printed. Preserves ${VAR} templates, the relative
81191
+ * workspace.path, and all locally-owned keys.
81192
+ *
81193
+ * CREATE (no file): synthesize a minimal valid agent.json under a fresh
81194
+ * `<agentsDir>/<agentId>/` dir — runtime from `spec.runtime` (falling back to
81195
+ * the first known runtime + a warn), workspace translated or defaulted to
81196
+ * from-scratch, path daemon-generated.
81197
+ */
81198
+ function writeAgentSpec(agentsDir, agentId, spec, deps) {
81199
+ const existingFile = findAgentFileById(agentsDir, agentId);
81200
+ if (existingFile) {
81201
+ const raw = JSON.parse(readFileSync(existingFile, "utf8"));
81202
+ applySpecFields(raw, spec, {
81203
+ agentId,
81204
+ agentDir: dirname(existingFile),
81205
+ creating: false
81206
+ });
81207
+ writeFileSync(existingFile, JSON.stringify(raw, null, 2) + "\n");
81208
+ return;
81209
+ }
81210
+ const agentDir = join(agentsDir, agentId);
81211
+ const file = join(agentDir, "agent.json");
81212
+ let runtime = spec.runtime;
81213
+ if (!runtime) {
81214
+ runtime = deps.knownRuntimes[0];
81215
+ if (!runtime) throw new Error(`cannot create agent "${agentId}": spec has no runtime and no runtimes are known`);
81216
+ deps.warn?.(`cp: agent "${agentId}" spec has no runtime; defaulting to "${runtime}"`);
81217
+ }
81218
+ const raw = {
81219
+ id: agentId,
81220
+ name: spec.name,
81221
+ status: "active",
81222
+ runtime,
81223
+ workspace: {
81224
+ mode: "from-scratch",
81225
+ path: join(agentDir, "workspace")
81226
+ }
81227
+ };
81228
+ applySpecFields(raw, spec, {
81229
+ agentId,
81230
+ agentDir,
81231
+ creating: true
81232
+ });
81233
+ mkdirSync(agentDir, { recursive: true });
81234
+ writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
81235
+ }
81236
+ /**
81237
+ * Delete the agent whose internal `id` is `agentId`. Called ONLY from the
81238
+ * `agent/remove` handler — unconditional. Locates the file by id (any directory
81239
+ * layout) and removes its containing dir; falls back to `<agentsDir>/<id>` when
81240
+ * no such agent exists (no-op). `discoverAgents` treats an agent.json dir as a
81241
+ * leaf, so the containing dir is the agent's own dir.
81242
+ */
81243
+ function removeAgent(agentsDir, agentId) {
81244
+ const existingFile = findAgentFileById(agentsDir, agentId);
81245
+ rmSync(existingFile ? dirname(existingFile) : join(agentsDir, agentId), {
81246
+ recursive: true,
81247
+ force: true
81248
+ });
81249
+ }
81250
+ //#endregion
81151
81251
  //#region src/cp/cp-agent-registry.ts
81152
81252
  var CpAgentRegistry = class {
81153
- io;
81253
+ agentsDir;
81254
+ deps;
81154
81255
  onChange;
81155
- specs = /* @__PURE__ */ new Map();
81156
- constructor(io, onChange) {
81157
- this.io = io;
81256
+ constructor(agentsDir, deps, onChange) {
81257
+ this.agentsDir = agentsDir;
81258
+ this.deps = deps;
81158
81259
  this.onChange = onChange;
81159
- const s = io.load();
81160
- if (s) this.specs = new Map(Object.entries(s));
81161
81260
  }
81162
- /** Add or replace one agent's spec (agent/upsert EVT). */
81261
+ /** Add or merge one agent's spec onto disk (agent/upsert EVT). */
81163
81262
  upsert(agentId, spec) {
81164
- this.specs.set(agentId, spec);
81165
- this.changed();
81263
+ writeAgentSpec(this.agentsDir, agentId, spec, this.deps);
81264
+ this.onChange();
81166
81265
  }
81167
- /** Drop one agent's spec (agent/remove EVT). No-op if absent. */
81266
+ /** Delete one agent's on-disk dir (agent/remove EVT). Unconditional. */
81168
81267
  remove(agentId) {
81169
- if (this.specs.delete(agentId)) this.changed();
81268
+ removeAgent(this.agentsDir, agentId);
81269
+ this.onChange();
81170
81270
  }
81171
- /** Make the live set exactly `roster` (register/ok reconcile snapshot). */
81271
+ /**
81272
+ * Apply the register/ok reconcile roster: create-or-merge EACH entry and
81273
+ * NOTHING else. Never prunes agents absent from the roster (deletion is only
81274
+ * via agent/remove).
81275
+ */
81172
81276
  converge(roster) {
81173
- const next = /* @__PURE__ */ new Map();
81174
- for (const { agentId, ...spec } of roster) next.set(agentId, spec);
81175
- this.specs = next;
81176
- this.changed();
81177
- }
81178
- get(agentId) {
81179
- return this.specs.get(agentId);
81180
- }
81181
- /** agentIds the CP currently wants present. */
81182
- ids() {
81183
- return [...this.specs.keys()];
81184
- }
81185
- changed() {
81186
- this.io.save(Object.fromEntries(this.specs));
81277
+ for (const { agentId, ...spec } of roster) writeAgentSpec(this.agentsDir, agentId, spec, this.deps);
81187
81278
  this.onChange();
81188
81279
  }
81189
81280
  };
@@ -81339,12 +81430,9 @@ var Daemon = class {
81339
81430
  },
81340
81431
  save: (s) => this.store.setCpRouting(s.routingEpoch, JSON.stringify(s.assignments), JSON.stringify(s.globalRules))
81341
81432
  });
81342
- this.cpAgents = new CpAgentRegistry({
81343
- load: () => {
81344
- const row = this.store.getCpAgents();
81345
- return row ? JSON.parse(row.specs) : void 0;
81346
- },
81347
- save: (s) => this.store.setCpAgents(JSON.stringify(s))
81433
+ this.cpAgents = new CpAgentRegistry(this.agentsDir, {
81434
+ knownRuntimes: Object.keys(this.runtimes),
81435
+ warn: (m) => this.log.warn(m)
81348
81436
  }, () => void this.reconcile().catch((err) => this.log.error(`cp: agent reconcile failed: ${err.stack ?? err}`)));
81349
81437
  for (const a of this.effectiveAgents()) this.agents.set(a.id, a);
81350
81438
  this.mcp = new McpControlServer({
@@ -81435,16 +81523,13 @@ var Daemon = class {
81435
81523
  return this.opts.agentName ? [selectAgent(this.agentsDir, this.opts.agentName)] : loadAgents(this.agentsDir);
81436
81524
  }
81437
81525
  /**
81438
- * Effective agent set = on-disk `agent.json` base with the CP spec overlaid
81439
- * (joined by id; CP owns name/description/model/reasoning/execution/env, the
81440
- * file owns runtime/workspace). CP-only ids with no local base are skipped —
81441
- * not runnable and surfaced via cpDegradedScopes().
81526
+ * Effective agent set = the on-disk `agent.json` files verbatim. The CP spec is
81527
+ * no longer overlaid in memory — `agent/upsert` writes it straight to disk (see
81528
+ * cp/cp-agent-registry.ts agents/write-agent.ts), so the loaded file already
81529
+ * carries the CP-owned fields.
81442
81530
  */
81443
81531
  effectiveAgents() {
81444
- return [...this.fileAgents.values()].map((base) => {
81445
- const spec = this.cpAgents?.get(base.id);
81446
- return spec ? overlayCpSpec(base, spec) : base;
81447
- });
81532
+ return [...this.fileAgents.values()];
81448
81533
  }
81449
81534
  /**
81450
81535
  * Converge the running set to the desired effective agents. Driven by both the
@@ -81622,13 +81707,12 @@ var Daemon = class {
81622
81707
  }
81623
81708
  /**
81624
81709
  * agentIds the daemon can't fully serve: CP routing rules with no servable
81625
- * Slack integration, plus CP agent specs with no on-disk base (no runtime /
81626
- * workspace to materialize the spec alone can't be run).
81710
+ * Slack integration. (CP agent specs are now written to disk and create a
81711
+ * runnable agent.json, so they no longer contribute a "no base" degraded scope.)
81627
81712
  */
81628
81713
  cpDegradedScopes() {
81629
81714
  const out = /* @__PURE__ */ new Set();
81630
81715
  for (const cpRule of this.cpRouting?.effectiveRules() ?? []) if (!this.resolveCpAgent(cpRule.agentId)) out.add(cpRule.agentId);
81631
- for (const id of this.cpAgents?.ids() ?? []) if (!this.fileAgents.has(id)) out.add(id);
81632
81716
  return [...out];
81633
81717
  }
81634
81718
  async dispatch(agentId, msg, integrationId) {