@awebai/oats 0.22.14 → 0.22.17

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/lib/schedule.mjs CHANGED
@@ -40,11 +40,12 @@ export function scheduleError(code, message, extra) { return Object.assign(new E
40
40
  * is never observed or executed, and is reported invalid on that job only. */
41
41
  export function shapeError(def) {
42
42
  if (!def || typeof def !== "object" || Array.isArray(def)) return "definition is not an object";
43
- if (!["spawn", "command", "wake"].includes(def.kind)) return `kind ${JSON.stringify(def.kind)} is not spawn, command or wake`;
43
+ if (!["spawn", "command", "wake", "operation"].includes(def.kind)) return `kind ${JSON.stringify(def.kind)} is not spawn, command, wake or operation`;
44
44
  if (typeof def.cron !== "string" || typeof def.tz !== "string") return "cron and tz must be strings";
45
45
  if (def.kind === "spawn" && typeof def.agent !== "string") return "agent must be a string";
46
46
  if (def.kind === "command" && (typeof def.cwd !== "string" || !Array.isArray(def.argv))) return "cwd and argv are required";
47
47
  if (def.kind === "wake" && (typeof def.home !== "string" || typeof def.message !== "string")) return "home and message are required";
48
+ if (def.kind === "operation" && (typeof def.operation !== "string" || typeof def.home !== "string")) return "operation and home are required";
48
49
  return null;
49
50
  }
50
51
 
@@ -229,7 +230,14 @@ export function validateDefinition(ws, def, { checkAgent = true } = {}) {
229
230
  if (typeof def.home !== "string" || !isAbsolute(def.home) || !inside(ws, def.home) || !existsSync(join(def.home, "instance.json"))) throw scheduleError("E_SCHEDULE_INVALID", "home: an existing instance home inside the scope", { field: "home" });
230
231
  validateMessage(def.message, "message");
231
232
  out.home = resolve(def.home); out.message = def.message;
232
- } else throw scheduleError("E_SCHEDULE_INVALID", "kind: spawn, command or wake", { field: "kind" });
233
+ } else if (def.kind === "operation") {
234
+ // A provider operation in a running home, resolved when the job runs
235
+ // (oats operation run <layer>:<name> --home): the provider is whatever
236
+ // fills that layer for the home then; nothing about it is stored here.
237
+ if (typeof def.operation !== "string" || !/^(knowledge|messaging|tasks):[a-z][a-z0-9-]*$/.test(def.operation)) throw scheduleError("E_SCHEDULE_INVALID", "operation: <layer>:<name>, e.g. knowledge:harvest", { field: "operation" });
238
+ if (typeof def.home !== "string" || !isAbsolute(def.home) || !inside(ws, def.home) || !existsSync(join(def.home, "instance.json"))) throw scheduleError("E_SCHEDULE_INVALID", "home: an existing instance home inside the scope", { field: "home" });
239
+ out.operation = def.operation; out.home = resolve(def.home);
240
+ } else throw scheduleError("E_SCHEDULE_INVALID", "kind: spawn, command, wake or operation", { field: "kind" });
233
241
  return out;
234
242
  }
235
243
 
@@ -320,6 +328,13 @@ function launchSpawn(ws, def, minute, io) {
320
328
  }
321
329
  return run;
322
330
  }
331
+ /** The command job an operation job becomes when it runs: the kernel's own
332
+ * operation runner in the home, which resolves the provider at that moment
333
+ * and reports a launch receipt (instance/home) only for what the provider
334
+ * launched, never for the source home. */
335
+ export function operationAsCommand(def) {
336
+ return { ...def, kind: "command", cwd: def.home, argv: ["oats", "operation", "run", def.operation, "--home", def.home] };
337
+ }
323
338
  /** Parse a command's stdout as one JSON document (remote output can be
324
339
  * multi-line); fall back to the last {...} block; null when nothing parses. */
325
340
  export function parseEnvelopeText(text) {
@@ -355,9 +370,16 @@ function launchCommand(ws, def, io) {
355
370
  if (instance && !home) { const found = findHomesInScope(ws, instance); if (found.length === 1) home = found[0]; }
356
371
  if (envelope.ok && instance && !home) return { kind: "command", launched: true, instance, unconfirmed: true, error: `the envelope names instance ${instance} but no home for it is in this scope's roster; run oats schedule reconcile once it appears` };
357
372
  // A valid ok:false answer that reports an incomplete rollback (a harvest
358
- // spawn whose compensation could not stop or remove everything) is not a
359
- // confirmed failure either.
360
- if (!envelope.ok && reportsRetainedEffects(envelope.error?.message)) return { kind: "command", launched: false, unconfirmed: true, ...(instance ? { instance } : {}), error: `command failed with retained effects: ${envelope.error?.message}`, errorCode: envelope.error?.code };
373
+ // spawn whose compensation could not stop or remove everything), or an
374
+ // operation runner's unconfirmed outcome (timeout, no valid receipt, a
375
+ // receipt contradicted by the exit status), is not a confirmed failure.
376
+ // Whatever name the provider managed to answer travels in error.details
377
+ // so reconcile can adopt it.
378
+ if (!envelope.ok && (UNCONFIRMED_ERROR_CODES.has(envelope.error?.code) || envelope.error?.details?.unconfirmed === true || reportsRetainedEffects(envelope.error?.message))) {
379
+ const partial = envelope.error?.details?.envelope?.result;
380
+ const named = instance || (partial && typeof partial.instance === "string" ? partial.instance : undefined);
381
+ return { kind: "command", launched: false, unconfirmed: true, ...(named ? { instance: named } : {}), error: `command outcome unconfirmed: ${envelope.error?.message}`, errorCode: envelope.error?.code };
382
+ }
361
383
  return { kind: "command", launched: envelope.ok === true, ...(instance ? { instance } : {}), ...(home ? { home } : {}), ...(envelope.ok ? {} : { error: envelope.error?.message || "command failed", errorCode: envelope.error?.code }) };
362
384
  }
363
385
  /** One wake. `startIfStopped` false between due minutes (a harness that
@@ -367,6 +389,8 @@ function launchCommand(ws, def, io) {
367
389
  * confirm (spawn compensation's "rollback INCOMPLETE", a quarantined or
368
390
  * retained home): the launch's effects are unconfirmed, never a confirmed
369
391
  * failure. Shared by caught spawn errors and ok:false command envelopes. */
392
+ /** Error codes whose meaning is "the effects are unconfirmed" by contract. */
393
+ const UNCONFIRMED_ERROR_CODES = new Set(["E_OPERATION_TIMEOUT", "E_OPERATION_RESULT"]);
370
394
  export function reportsRetainedEffects(message) { return /INCOMPLETE|quarantin|retain|could not (?:be )?(?:verif|confirm)/i.test(String(message || "")); }
371
395
  function performWake(def, io, { startIfStopped = true, canStart = true, reserve } = {}) {
372
396
  const seen = observeHome(def.home, io);
@@ -498,7 +522,7 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
498
522
  js.lastLaunchedAt = now.toISOString();
499
523
  writeState(ws, st);
500
524
  let run;
501
- try { run = def.kind === "command" ? launchCommand(ws, def, io) : launchSpawn(ws, def, minute, io); }
525
+ try { run = def.kind === "spawn" ? launchSpawn(ws, def, minute, io) : launchCommand(ws, def.kind === "operation" ? operationAsCommand(def) : def, io); }
502
526
  catch (e) {
503
527
  // spawnInstance compensates its own failures, but its rollback can be
504
528
  // INCOMPLETE (a pane it could not stop, a quarantined home it kept):
@@ -595,7 +619,7 @@ export function reconcile(ws, id, { io, now = new Date(), clear = false } = {})
595
619
  const homes = findHomesInScope(ws, `${def.agent}-${purpose}`);
596
620
  if (homes.length === 1) adopted = { instance: `${def.agent}-${purpose}`, home: homes[0] };
597
621
  else if (homes.length > 1) ambiguous = `${homes.length} homes named ${def.agent}-${purpose}: ${homes.join(", ")}`;
598
- } else if (def.kind === "command") {
622
+ } else if (def.kind === "command" || def.kind === "operation") {
599
623
  const named = js.lastRun?.instance ? findHomesInScope(ws, js.lastRun.instance).map((home) => ({ instance: js.lastRun.instance, home })) : [];
600
624
  if (named.length === 1) adopted = named[0];
601
625
  else if (named.length > 1) ambiguous = `${named.length} homes named ${js.lastRun.instance}: ${named.map((c) => c.home).join(", ")}`;
@@ -651,7 +675,7 @@ export function updateSchedule(ws, id, spec, io) {
651
675
  return withHostLock(() => withScopeLock(ws, () => {
652
676
  const defs = readDefinitions(ws);
653
677
  if (!defs.jobs[id]) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
654
- const identity = (d) => JSON.stringify([d.kind, d.agent, d.agentsRoot, d.repo, d.purpose, d.home, d.cwd, d.argv]);
678
+ const identity = (d) => JSON.stringify([d.kind, d.agent, d.agentsRoot, d.repo, d.purpose, d.home, d.cwd, d.argv, d.operation]);
655
679
  const busy = !!jobLockInfo(ws, id) || !!readState(ws).jobs[id]?.attempt;
656
680
  if (busy && identity(defs.jobs[id]) !== identity(def)) throw scheduleError("E_SCHEDULE_RUNNING", `schedule ${id} is running or has an unresolved attempt; its kind, agent, agentsRoot, repo, purpose, home, cwd and argv cannot change until it ends (cron, tz, task, message, runtime, model and enabled can)`);
657
681
  defs.jobs[id] = { ...def, createdAt: defs.jobs[id].createdAt, updatedAt: new Date().toISOString() };
package/lib/servers.mjs CHANGED
@@ -232,6 +232,7 @@ export function checkRemote(target, io = {}) {
232
232
  launchOptions: list("launchOptions", []),
233
233
  remote: list("remote", []),
234
234
  features: list("features", []),
235
+ operationsApi: probe.operationsApi === 1 ? 1 : null,
235
236
  advertised: Array.isArray(probe.runtimes),
236
237
  };
237
238
  }
@@ -479,7 +480,34 @@ export function routeCommand(serverId, cmd, oatsArgs, io = {}) {
479
480
  const { envelope, stderr } = runRemote(target, json(withScope(["status", ...oatsArgs])), io);
480
481
  return { envelope: envelope.ok ? { ...envelope, result: { ...envelope.result, server: serverId, target, snapshots: listSnapshots(serverId) } } : envelope, stderr };
481
482
  }
482
- throw serverError("E_USAGE", `--server routes spawn, retire, status, session and okf harvest only (not ${cmd})`);
483
+ if (OPERATIONS_COMMANDS.has(cmd)) {
484
+ // The operations contract (inspect, operation run, use, soul set): the
485
+ // remote must advertise it before anything is sent. An explicit --dir is
486
+ // the exact member context the caller chose and travels as is; without
487
+ // one, a --home selection is left to the host (the home is its own
488
+ // context) and anything else runs in the registered workspace.
489
+ // An exact remote home (or a saved instance name) resolves through its
490
+ // FROZEN route exactly as session attach does: the snapshot's target,
491
+ // and a home/name disagreement is refused; the registration's target is
492
+ // used only for scope requests, which get its workspace as --dir.
493
+ let args = [...oatsArgs];
494
+ const valueOf = (name) => { const i = args.indexOf(name); return i >= 0 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : undefined; };
495
+ let route;
496
+ if (valueOf("--home") !== undefined || valueOf("--instance") !== undefined) {
497
+ route = resolveRoute(serverId, { instance: valueOf("--instance"), home: valueOf("--home") }, cmd);
498
+ target = route.target;
499
+ const ii = args.indexOf("--instance");
500
+ if (ii >= 0) { args.splice(ii, 2); if (valueOf("--home") === undefined) args.push("--home", route.home); }
501
+ }
502
+ const remote = checkRemote(target, io);
503
+ if (!Array.isArray(remote.features) || !remote.features.includes("operations") || remote.operationsApi !== 1) {
504
+ throw serverError("E_REMOTE_INCOMPATIBLE", `remote oats ${remote.version} at ${target.sshHost} does not advertise the operations contract (kernels from ${OPERATIONS_REMOTE_VERSION} do); upgrade it there; nothing was sent`);
505
+ }
506
+ const scoped = args.includes("--dir") || args.includes("--home") ? args : [...args, "--dir", target.workspace];
507
+ const { envelope, stderr } = runRemote(target, json([cmd, ...scoped]), io);
508
+ return { envelope: envelope.result && typeof envelope.result === "object" ? { ...envelope, result: { ...envelope.result, server: serverId, ...(route ? { route: { home: route.home, instance: route.snapshot?.instance || null, frozen: !!route.snapshot } } : {}) } } : envelope, stderr };
509
+ }
510
+ throw serverError("E_USAGE", `--server routes spawn, retire, status, session, okf harvest, schedule, inspect, operation, use and soul only (not ${cmd})`);
483
511
  }
484
512
 
485
513
  // ------------------------------------------------------------------- roster
@@ -621,6 +649,9 @@ export function inspectRemote(serverId, { instance, home } = {}, io = {}) {
621
649
  /** The kernel version whose probe first advertises the `session-start`
622
650
  * feature; the probe's features list is the actual check. */
623
651
  export const SESSION_START_REMOTE_VERSION = "0.22.9";
652
+ /** The kernel version whose probe first advertises the operations contract. */
653
+ export const OPERATIONS_REMOTE_VERSION = "0.22.16";
654
+ const OPERATIONS_COMMANDS = new Set(["inspect", "operation", "use", "soul"]);
624
655
 
625
656
  /** `session start` on the execution host for a remote instance: the same
626
657
  * route resolution as inspect, refused before any mutation when the remote
@@ -2,7 +2,7 @@
2
2
  "packages": {
3
3
  "oats.okf": {
4
4
  "url": "https://github.com/awebai/oats-okf.git",
5
- "ref": "v1.5.2",
5
+ "ref": "v1.6.1",
6
6
  "path": "oats-package"
7
7
  },
8
8
  "oats.aweb": {
@@ -33,12 +33,33 @@
33
33
  },
34
34
  "capabilities": {
35
35
  "oats.review": "oats.dev",
36
- "oas.okf": { "package": "oats.okf", "capability": "oats.okf" },
37
- "oas.aweb": { "package": "oats.aweb", "capability": "oats.aweb" },
38
- "oas.jira": { "package": "oats.jira", "capability": "oats.jira" },
39
- "oas.linear": { "package": "oats.linear", "capability": "oats.linear" },
40
- "oas.authoring": { "package": "oats.authoring", "capability": "oats.authoring" },
41
- "oas.dev": { "package": "oats.dev", "capability": "oats.dev" },
42
- "oas.review": { "package": "oats.dev", "capability": "oats.review" }
36
+ "oas.okf": {
37
+ "package": "oats.okf",
38
+ "capability": "oats.okf"
39
+ },
40
+ "oas.aweb": {
41
+ "package": "oats.aweb",
42
+ "capability": "oats.aweb"
43
+ },
44
+ "oas.jira": {
45
+ "package": "oats.jira",
46
+ "capability": "oats.jira"
47
+ },
48
+ "oas.linear": {
49
+ "package": "oats.linear",
50
+ "capability": "oats.linear"
51
+ },
52
+ "oas.authoring": {
53
+ "package": "oats.authoring",
54
+ "capability": "oats.authoring"
55
+ },
56
+ "oas.dev": {
57
+ "package": "oats.dev",
58
+ "capability": "oats.dev"
59
+ },
60
+ "oas.review": {
61
+ "package": "oats.dev",
62
+ "capability": "oats.review"
63
+ }
43
64
  }
44
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.22.14",
3
+ "version": "0.22.17",
4
4
  "description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
5
5
  "keywords": [
6
6
  "agents",