@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/bin/oats.mjs +678 -23
- package/capabilities/oats-okf/bin/oats-okf.mjs +47 -8
- package/capabilities/oats-okf/oats.json +18 -3
- package/docs/capabilities.md +7 -0
- package/docs/capability-manifest.schema.json +32 -0
- package/docs/design/2026-09-07-desktop-souls-capabilities.md +50 -0
- package/docs/design/operations-contract.md +123 -0
- package/docs/release-notes/v0.22.15.md +48 -0
- package/docs/release-notes/v0.22.16.md +53 -0
- package/docs/release-notes/v0.22.17.md +25 -0
- package/docs/schedules.md +9 -0
- package/lib/core.mjs +37 -2
- package/lib/schedule.mjs +32 -8
- package/lib/servers.mjs +32 -1
- package/package-catalog.json +29 -8
- package/package.json +1 -1
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
|
|
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
|
|
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)
|
|
359
|
-
//
|
|
360
|
-
|
|
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 === "
|
|
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
|
-
|
|
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
|
package/package-catalog.json
CHANGED
|
@@ -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
|
+
"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": {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"oas.
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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",
|