open-multi-agent-kit 0.91.0 → 0.92.0

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +2 -2
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +8 -7
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/command-safety.d.ts.map +1 -1
  7. package/dist/core/command-safety.js +0 -50
  8. package/dist/core/command-safety.js.map +1 -1
  9. package/dist/core/compaction/transaction.d.ts.map +1 -1
  10. package/dist/core/compaction/transaction.js +7 -3
  11. package/dist/core/compaction/transaction.js.map +1 -1
  12. package/dist/core/context-budget-headroom-candidates.d.ts.map +1 -1
  13. package/dist/core/context-budget-headroom-candidates.js +21 -11
  14. package/dist/core/context-budget-headroom-candidates.js.map +1 -1
  15. package/dist/core/context-budget-system-prompt-items.d.ts +1 -0
  16. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  17. package/dist/core/context-budget-system-prompt-items.js +7 -8
  18. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  19. package/dist/core/loadouts.d.ts.map +1 -1
  20. package/dist/core/loadouts.js +27 -48
  21. package/dist/core/loadouts.js.map +1 -1
  22. package/dist/core/system-prompt.d.ts.map +1 -1
  23. package/dist/core/system-prompt.js +35 -30
  24. package/dist/core/system-prompt.js.map +1 -1
  25. package/dist/main.d.ts.map +1 -1
  26. package/dist/main.js +112 -11
  27. package/dist/main.js.map +1 -1
  28. package/dist/modes/print-mode.d.ts.map +1 -1
  29. package/dist/modes/print-mode.js +9 -0
  30. package/dist/modes/print-mode.js.map +1 -1
  31. package/docs/adaptorch-preview-spec.md +174 -0
  32. package/docs/adaptorch-preview.md +1 -1
  33. package/docs/compaction.md +13 -13
  34. package/docs/correctness-wall.md +13 -5
  35. package/docs/custom-provider.md +8 -8
  36. package/docs/development.md +2 -2
  37. package/docs/extensions.md +8 -8
  38. package/docs/json.md +4 -4
  39. package/docs/providers.md +1 -1
  40. package/docs/session-format.md +5 -5
  41. package/docs/skills.md +2 -0
  42. package/docs/themes.md +2 -2
  43. package/docs/tui.md +4 -4
  44. package/docs/usage.md +4 -0
  45. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  46. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  47. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  48. package/examples/extensions/gondolin/package-lock.json +2 -2
  49. package/examples/extensions/gondolin/package.json +1 -1
  50. package/examples/extensions/sandbox/package-lock.json +2 -2
  51. package/examples/extensions/sandbox/package.json +1 -1
  52. package/examples/extensions/subagent/README.md +35 -10
  53. package/examples/extensions/subagent/adaptive-agent-runtime.test.ts +234 -0
  54. package/examples/extensions/subagent/adaptive-agent-runtime.ts +240 -0
  55. package/examples/extensions/subagent/adaptive-result.ts +112 -0
  56. package/examples/extensions/subagent/checkpoint-runtime.test.ts +125 -0
  57. package/examples/extensions/subagent/checkpoint-runtime.ts +198 -0
  58. package/examples/extensions/subagent/deadline-budget.test.ts +135 -0
  59. package/examples/extensions/subagent/deadline-budget.ts +242 -0
  60. package/examples/extensions/subagent/deadline-profile-store.test.ts +72 -0
  61. package/examples/extensions/subagent/deadline-profile-store.ts +172 -0
  62. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +13 -0
  63. package/examples/extensions/subagent/fixtures/hanging-process-tree.mjs +40 -0
  64. package/examples/extensions/subagent/index.ts +253 -137
  65. package/examples/extensions/subagent/managed-process.test.ts +178 -0
  66. package/examples/extensions/subagent/managed-process.ts +236 -0
  67. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +112 -0
  68. package/examples/extensions/subagent/subagent-runtime-types.ts +76 -0
  69. package/examples/extensions/with-deps/package-lock.json +2 -2
  70. package/examples/extensions/with-deps/package.json +1 -1
  71. package/npm-shrinkwrap.json +21 -21
  72. package/package.json +5 -5
@@ -0,0 +1,72 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { DeadlineProfileStore } from "./deadline-profile-store.ts";
6
+
7
+ const cleanupPaths: string[] = [];
8
+
9
+ afterEach(async () => {
10
+ await Promise.all(cleanupPaths.splice(0).map((entry) => fs.promises.rm(entry, { force: true, recursive: true })));
11
+ });
12
+
13
+ describe("DeadlineProfileStore", () => {
14
+ it("persists provider/model cutoff history across dispatcher processes", async () => {
15
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-deadline-profile-"));
16
+ cleanupPaths.push(dir);
17
+ const filePath = path.join(dir, "profiles.json");
18
+ const first = new DeadlineProfileStore(filePath);
19
+ await first.record({
20
+ provider: "openai-codex",
21
+ model: "gpt-5.6",
22
+ outcome: "cutoff",
23
+ elapsedMs: 500,
24
+ estimatedTokens: 100,
25
+ workUnits: 2,
26
+ });
27
+
28
+ const reloaded = new DeadlineProfileStore(filePath);
29
+ expect(await reloaded.profileFor("openai-codex", "gpt-5.6")).toMatchObject({ samples: 1, cutoffs: 1 });
30
+ });
31
+
32
+ it("serializes concurrent samples without losing updates", async () => {
33
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-deadline-concurrent-"));
34
+ cleanupPaths.push(dir);
35
+ const filePath = path.join(dir, "profiles.json");
36
+ const stores = [new DeadlineProfileStore(filePath), new DeadlineProfileStore(filePath)];
37
+ await Promise.all(
38
+ Array.from({ length: 8 }, (_, index) =>
39
+ stores[index % stores.length].record({
40
+ provider: "anthropic",
41
+ model: "claude-sonnet",
42
+ outcome: "completed",
43
+ elapsedMs: 100 + index,
44
+ estimatedTokens: 10,
45
+ workUnits: 1,
46
+ }),
47
+ ),
48
+ );
49
+
50
+ const reloaded = new DeadlineProfileStore(filePath);
51
+ expect(await reloaded.profileFor("anthropic", "claude-sonnet")).toMatchObject({ samples: 8, completions: 8 });
52
+ });
53
+
54
+ it("recovers from malformed history without exposing untyped data", async () => {
55
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-deadline-corrupt-"));
56
+ cleanupPaths.push(dir);
57
+ const filePath = path.join(dir, "profiles.json");
58
+ await fs.promises.writeFile(filePath, "{not-json", "utf8");
59
+ const store = new DeadlineProfileStore(filePath);
60
+
61
+ expect(await store.profileFor("unknown", "default")).toBeUndefined();
62
+ await store.record({
63
+ provider: "unknown",
64
+ model: "default",
65
+ outcome: "failed",
66
+ elapsedMs: 20,
67
+ estimatedTokens: 5,
68
+ workUnits: 1,
69
+ });
70
+ expect(JSON.parse(await fs.promises.readFile(filePath, "utf8"))).toMatchObject({ version: 1 });
71
+ });
72
+ });
@@ -0,0 +1,172 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import {
4
+ type DeadlineProfile,
5
+ type DeadlineProfileDb,
6
+ type DeadlineProfileSample,
7
+ getDeadlineProfile,
8
+ updateDeadlineProfiles,
9
+ } from "./deadline-budget.ts";
10
+
11
+ const MAX_PROFILE_FILE_BYTES = 1024 * 1024;
12
+ const MAX_PROFILE_ENTRIES = 1_000;
13
+ const LOCK_RETRY_COUNT = 200;
14
+ const LOCK_RETRY_MS = 10;
15
+ const STALE_LOCK_MS = 30_000;
16
+
17
+ export class DeadlineProfileLockError extends Error {
18
+ constructor(filePath: string) {
19
+ super(`Timed out acquiring deadline profile lock for ${filePath}`);
20
+ this.name = "DeadlineProfileLockError";
21
+ }
22
+ }
23
+
24
+ export class DeadlineProfileStore {
25
+ private readonly filePath: string;
26
+ private db: DeadlineProfileDb = emptyDb();
27
+ private mutationTail: Promise<void> = Promise.resolve();
28
+
29
+ constructor(filePath: string) {
30
+ this.filePath = filePath;
31
+ }
32
+
33
+ async profileFor(provider: string, model: string): Promise<DeadlineProfile | undefined> {
34
+ await this.mutationTail;
35
+ this.db = await readDb(this.filePath);
36
+ return getDeadlineProfile(this.db, provider, model);
37
+ }
38
+
39
+ async record(sample: DeadlineProfileSample): Promise<DeadlineProfile> {
40
+ let recorded: DeadlineProfile | undefined;
41
+ const operation = this.mutationTail.then(async () => {
42
+ await withProfileLock(this.filePath, async () => {
43
+ this.db = updateDeadlineProfiles(await readDb(this.filePath), sample);
44
+ recorded = getDeadlineProfile(this.db, sample.provider, sample.model);
45
+ if (recorded === undefined) throw new Error("Deadline profile update did not produce a profile");
46
+ await writeAtomically(this.filePath, JSON.stringify(this.db));
47
+ });
48
+ });
49
+ this.mutationTail = operation.catch(() => undefined);
50
+ await operation;
51
+ if (recorded === undefined) throw new Error("Deadline profile update did not complete");
52
+ return recorded;
53
+ }
54
+ }
55
+
56
+ async function withProfileLock<T>(filePath: string, operation: () => Promise<T>): Promise<T> {
57
+ const lockPath = `${filePath}.lock`;
58
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
59
+ for (let attempt = 0; attempt < LOCK_RETRY_COUNT; attempt++) {
60
+ try {
61
+ await fs.promises.mkdir(lockPath);
62
+ try {
63
+ return await operation();
64
+ } finally {
65
+ await fs.promises.rm(lockPath, { recursive: true, force: true });
66
+ }
67
+ } catch (error) {
68
+ if (!isAlreadyExists(error)) throw error;
69
+ await removeStaleLock(lockPath);
70
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
71
+ }
72
+ }
73
+ throw new DeadlineProfileLockError(filePath);
74
+ }
75
+
76
+ async function removeStaleLock(lockPath: string): Promise<void> {
77
+ try {
78
+ const stats = await fs.promises.stat(lockPath);
79
+ if (Date.now() - stats.mtimeMs > STALE_LOCK_MS) {
80
+ await fs.promises.rm(lockPath, { recursive: true, force: true });
81
+ }
82
+ } catch {
83
+ return;
84
+ }
85
+ }
86
+
87
+ function isAlreadyExists(error: unknown): boolean {
88
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
89
+ }
90
+
91
+ async function readDb(filePath: string): Promise<DeadlineProfileDb> {
92
+ try {
93
+ const handle = await fs.promises.open(filePath, "r");
94
+ try {
95
+ const stats = await handle.stat();
96
+ if (stats.size > MAX_PROFILE_FILE_BYTES) return emptyDb();
97
+ return parseDb(JSON.parse(await handle.readFile("utf8")));
98
+ } finally {
99
+ await handle.close();
100
+ }
101
+ } catch {
102
+ return emptyDb();
103
+ }
104
+ }
105
+
106
+ async function writeAtomically(filePath: string, content: string): Promise<void> {
107
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
108
+ const tempPath = `${filePath}.${process.pid}.${process.hrtime.bigint()}.tmp`;
109
+ try {
110
+ await fs.promises.writeFile(tempPath, content, { encoding: "utf8", mode: 0o600 });
111
+ await fs.promises.rename(tempPath, filePath);
112
+ } finally {
113
+ await fs.promises.rm(tempPath, { force: true });
114
+ }
115
+ }
116
+
117
+ function parseDb(value: unknown): DeadlineProfileDb {
118
+ if (!isRecord(value) || value.version !== 1 || !isRecord(value.profiles)) return emptyDb();
119
+ const profiles: Record<string, DeadlineProfile> = Object.create(null);
120
+ for (const [key, raw] of Object.entries(value.profiles).slice(0, MAX_PROFILE_ENTRIES)) {
121
+ const parsed = parseProfile(raw);
122
+ if (parsed !== undefined) profiles[key] = parsed;
123
+ }
124
+ return { version: 1, profiles };
125
+ }
126
+
127
+ function parseProfile(value: unknown): DeadlineProfile | undefined {
128
+ if (!isRecord(value) || typeof value.provider !== "string" || typeof value.model !== "string") return undefined;
129
+ const samples = readCount(value.samples);
130
+ const completions = readCount(value.completions);
131
+ const cutoffs = readCount(value.cutoffs);
132
+ const aborts = readCount(value.aborts);
133
+ const failures = readCount(value.failures);
134
+ const ewmaElapsedMs = readCount(value.ewmaElapsedMs);
135
+ const ewmaMsPerWorkUnit = readCount(value.ewmaMsPerWorkUnit);
136
+ if ([samples, completions, cutoffs, aborts, failures, ewmaElapsedMs, ewmaMsPerWorkUnit].includes(undefined)) {
137
+ return undefined;
138
+ }
139
+ return {
140
+ provider: value.provider.slice(0, 256),
141
+ model: value.model.slice(0, 256),
142
+ samples: samples ?? 0,
143
+ completions: completions ?? 0,
144
+ cutoffs: cutoffs ?? 0,
145
+ aborts: aborts ?? 0,
146
+ failures: failures ?? 0,
147
+ ewmaElapsedMs: ewmaElapsedMs ?? 0,
148
+ ewmaMsPerWorkUnit: ewmaMsPerWorkUnit ?? 0,
149
+ completedDurationsMs: readDurations(value.completedDurationsMs),
150
+ cutoffDurationsMs: readDurations(value.cutoffDurationsMs),
151
+ };
152
+ }
153
+
154
+ function readCount(value: unknown): number | undefined {
155
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined;
156
+ }
157
+
158
+ function readDurations(value: unknown): number[] {
159
+ if (!Array.isArray(value)) return [];
160
+ return value
161
+ .map(readCount)
162
+ .filter((duration): duration is number => duration !== undefined && duration > 0)
163
+ .slice(-24);
164
+ }
165
+
166
+ function emptyDb(): DeadlineProfileDb {
167
+ return { version: 1, profiles: {} };
168
+ }
169
+
170
+ function isRecord(value: unknown): value is Record<string, unknown> {
171
+ return typeof value === "object" && value !== null && !Array.isArray(value);
172
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ process.stdout.write(
4
+ `${JSON.stringify({
5
+ type: "message_end",
6
+ message: {
7
+ role: "assistant",
8
+ content: [{ type: "text", text: "fixture subagent completed" }],
9
+ provider: "fixture-provider",
10
+ model: "fixture-model",
11
+ },
12
+ })}\n`,
13
+ );
@@ -0,0 +1,40 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const normalOrphan = process.argv.includes("--normal-orphan");
4
+
5
+ if (!process.argv.includes("--parent-exits") && !normalOrphan) {
6
+ process.on("SIGTERM", () => {
7
+ // Exercise the managed runner's SIGKILL escalation path.
8
+ });
9
+ }
10
+
11
+ // The grandchild ignores SIGTERM so the managed runner must escalate to
12
+ // SIGKILL. It writes "ready" on its stdout (piped back to this parent) only
13
+ // AFTER installing its SIGTERM handler. The parent waits for that signal
14
+ // before exiting or settling into its keep-alive loop, so the runner never
15
+ // signals the process group while the grandchild is still in its startup
16
+ // window (a fresh `node -e` process needs ~50-100ms before its first JS line
17
+ // runs). Without this handshake a SIGTERM can land before the handler is
18
+ // installed, killing the grandchild via the default handler and making the
19
+ // SIGKILL escalation (cleanup.killSent) a timing race.
20
+ const grandchild = spawn(
21
+ process.execPath,
22
+ ["-e", "process.on('SIGTERM', () => {}); process.stdout.write('ready'); setInterval(() => {}, 1000)"],
23
+ {
24
+ stdio: ["ignore", "pipe", "ignore"],
25
+ },
26
+ );
27
+
28
+ process.stdout.write(`${JSON.stringify({ grandchildPid: grandchild.pid })}\n`);
29
+
30
+ const startMain = () => {
31
+ if (normalOrphan) setTimeout(() => process.exit(0), 20);
32
+ else setInterval(() => {}, 1000);
33
+ };
34
+
35
+ let started = false;
36
+ grandchild.stdout.on("data", () => {
37
+ if (started) return;
38
+ started = true;
39
+ startMain();
40
+ });