copilot-hub 0.1.13 → 0.1.16
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/apps/agent-engine/dist/agent-worker.js +28 -20
- package/apps/agent-engine/dist/config.js +1 -2
- package/apps/agent-engine/dist/index.js +146 -41
- package/apps/control-plane/dist/agent-worker.js +28 -20
- package/apps/control-plane/dist/channels/channel-factory.js +1 -2
- package/apps/control-plane/dist/channels/hub-model-utils.js +280 -0
- package/apps/control-plane/dist/channels/hub-ops-commands.js +859 -143
- package/apps/control-plane/dist/channels/telegram-channel.js +197 -53
- package/apps/control-plane/dist/channels/whatsapp-channel.js +6 -1
- package/apps/control-plane/dist/config.js +1 -2
- package/apps/control-plane/dist/copilot-hub.js +2 -3
- package/apps/control-plane/dist/index.js +41 -23
- package/apps/control-plane/dist/kernel/admin-contract.js +1 -2
- package/apps/control-plane/dist/test/hub-model-utils.test.js +170 -0
- package/package.json +4 -2
- package/packages/core/dist/agent-supervisor.d.ts +109 -28
- package/packages/core/dist/agent-supervisor.js +99 -45
- package/packages/core/dist/agent-supervisor.js.map +1 -1
- package/packages/core/dist/bot-manager.d.ts +86 -45
- package/packages/core/dist/bot-manager.js +17 -3
- package/packages/core/dist/bot-manager.js.map +1 -1
- package/packages/core/dist/bot-runtime.d.ts +225 -125
- package/packages/core/dist/bot-runtime.js +240 -52
- package/packages/core/dist/bot-runtime.js.map +1 -1
- package/packages/core/dist/bridge-service.d.ts +158 -31
- package/packages/core/dist/bridge-service.js +33 -22
- package/packages/core/dist/bridge-service.js.map +1 -1
- package/packages/core/dist/capability-manager.d.ts +60 -12
- package/packages/core/dist/capability-manager.js +74 -37
- package/packages/core/dist/capability-manager.js.map +1 -1
- package/packages/core/dist/channel-factory.d.ts +9 -4
- package/packages/core/dist/channel-factory.js +1 -2
- package/packages/core/dist/channel-factory.js.map +1 -1
- package/packages/core/dist/codex-app-client.d.ts +110 -43
- package/packages/core/dist/codex-app-client.js +182 -333
- package/packages/core/dist/codex-app-client.js.map +1 -1
- package/packages/core/dist/codex-app-events.d.ts +30 -0
- package/packages/core/dist/codex-app-events.js +266 -0
- package/packages/core/dist/codex-app-events.js.map +1 -0
- package/packages/core/dist/codex-app-utils.d.ts +28 -0
- package/packages/core/dist/codex-app-utils.js +164 -0
- package/packages/core/dist/codex-app-utils.js.map +1 -0
- package/packages/core/dist/codex-provider.d.ts +36 -27
- package/packages/core/dist/codex-provider.js +12 -11
- package/packages/core/dist/codex-provider.js.map +1 -1
- package/packages/core/dist/codex-quota-display.d.ts +12 -0
- package/packages/core/dist/codex-quota-display.js +56 -0
- package/packages/core/dist/codex-quota-display.js.map +1 -0
- package/packages/core/dist/extension-contract.d.ts +1 -1
- package/packages/core/dist/extension-contract.js +0 -1
- package/packages/core/dist/extension-contract.js.map +1 -1
- package/packages/core/dist/kernel-control-plane.d.ts +52 -12
- package/packages/core/dist/kernel-control-plane.js +95 -32
- package/packages/core/dist/kernel-control-plane.js.map +1 -1
- package/packages/core/dist/provider-factory.d.ts +20 -6
- package/packages/core/dist/provider-factory.js +20 -8
- package/packages/core/dist/provider-factory.js.map +1 -1
- package/packages/core/dist/telegram-channel.d.ts +103 -16
- package/packages/core/dist/telegram-channel.js +195 -59
- package/packages/core/dist/telegram-channel.js.map +1 -1
- package/packages/core/dist/whatsapp-channel.d.ts +21 -20
- package/packages/core/dist/whatsapp-channel.js +6 -1
- package/packages/core/dist/whatsapp-channel.js.map +1 -1
- package/packages/core/package.json +4 -0
- package/scripts/dist/daemon.mjs +41 -2
- package/scripts/src/daemon.mts +45 -2
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
const BOT_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
4
|
+
let utilsPromise = null;
|
|
5
|
+
async function loadUtils() {
|
|
6
|
+
if (!utilsPromise) {
|
|
7
|
+
const specifier = ["..", "channels", "hub-model-utils.js"].join("/");
|
|
8
|
+
utilsPromise = import(specifier);
|
|
9
|
+
}
|
|
10
|
+
return utilsPromise;
|
|
11
|
+
}
|
|
12
|
+
test("parseSetModelCommand accepts explicit model and auto keyword", async () => {
|
|
13
|
+
const { parseSetModelCommand } = await loadUtils();
|
|
14
|
+
const modelResult = parseSetModelCommand("/set_model agent_a gpt-5.3-codex", BOT_ID_PATTERN);
|
|
15
|
+
assert.equal(modelResult.ok, true);
|
|
16
|
+
assert.equal(modelResult.botId, "agent_a");
|
|
17
|
+
assert.equal(modelResult.model, "gpt-5.3-codex");
|
|
18
|
+
const autoResult = parseSetModelCommand("/set_model agent_a auto", BOT_ID_PATTERN);
|
|
19
|
+
assert.equal(autoResult.ok, true);
|
|
20
|
+
assert.equal(autoResult.botId, "agent_a");
|
|
21
|
+
assert.equal(autoResult.model, null);
|
|
22
|
+
});
|
|
23
|
+
test("parseSetModelAllCommand accepts explicit model and auto keyword", async () => {
|
|
24
|
+
const { parseSetModelAllCommand } = await loadUtils();
|
|
25
|
+
const modelResult = parseSetModelAllCommand("/set_model_all gpt-5.3-codex");
|
|
26
|
+
assert.equal(modelResult.ok, true);
|
|
27
|
+
assert.equal(modelResult.model, "gpt-5.3-codex");
|
|
28
|
+
const autoResult = parseSetModelAllCommand("/set_model_all auto");
|
|
29
|
+
assert.equal(autoResult.ok, true);
|
|
30
|
+
assert.equal(autoResult.model, null);
|
|
31
|
+
});
|
|
32
|
+
test("parseSetModelCommand rejects invalid command input", async () => {
|
|
33
|
+
const { parseSetModelAllCommand, parseSetModelCommand } = await loadUtils();
|
|
34
|
+
const invalidId = parseSetModelCommand("/set_model bad*id gpt-5", BOT_ID_PATTERN);
|
|
35
|
+
assert.equal(invalidId.ok, false);
|
|
36
|
+
const invalidModel = parseSetModelCommand("/set_model agent_a bad/model", BOT_ID_PATTERN);
|
|
37
|
+
assert.equal(invalidModel.ok, false);
|
|
38
|
+
const missingModel = parseSetModelAllCommand("/set_model_all");
|
|
39
|
+
assert.equal(missingModel.ok, false);
|
|
40
|
+
const invalidGlobalModel = parseSetModelAllCommand("/set_model_all bad/model");
|
|
41
|
+
assert.equal(invalidGlobalModel.ok, false);
|
|
42
|
+
});
|
|
43
|
+
test("buildSessionModelOptions de-duplicates and marks selected model", async () => {
|
|
44
|
+
const { buildSessionModelOptions } = await loadUtils();
|
|
45
|
+
const options = buildSessionModelOptions({
|
|
46
|
+
currentModel: "gpt-5",
|
|
47
|
+
catalog: [
|
|
48
|
+
{ model: "gpt-4.1", displayName: "GPT-4.1" },
|
|
49
|
+
{ model: "gpt-5", displayName: "GPT-5" },
|
|
50
|
+
{ model: "gpt-5", displayName: "GPT-5 duplicate" },
|
|
51
|
+
],
|
|
52
|
+
inlineMax: 10,
|
|
53
|
+
});
|
|
54
|
+
assert.equal(options.length, 2);
|
|
55
|
+
assert.equal(options[0].model, "gpt-5");
|
|
56
|
+
assert.equal(options[0].selected, true);
|
|
57
|
+
assert.equal(options[1].model, "gpt-4.1");
|
|
58
|
+
assert.equal(options[1].selected, false);
|
|
59
|
+
});
|
|
60
|
+
test("resolveModelSelectionFromAction resolves auto and keyed entries", async () => {
|
|
61
|
+
const { resolveModelSelectionFromAction } = await loadUtils();
|
|
62
|
+
const session = {
|
|
63
|
+
modelOptions: [
|
|
64
|
+
{ key: "k0", model: "gpt-5", label: "GPT-5" },
|
|
65
|
+
{ key: "k1", model: "gpt-4.1", label: "GPT-4.1" },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
const autoSelection = resolveModelSelectionFromAction({ session, profileId: "auto" });
|
|
69
|
+
assert.equal(autoSelection.ok, true);
|
|
70
|
+
assert.equal(autoSelection.model, null);
|
|
71
|
+
const explicitSelection = resolveModelSelectionFromAction({ session, profileId: "k1" });
|
|
72
|
+
assert.equal(explicitSelection.ok, true);
|
|
73
|
+
assert.equal(explicitSelection.model, "gpt-4.1");
|
|
74
|
+
assert.equal(explicitSelection.label, "GPT-4.1");
|
|
75
|
+
});
|
|
76
|
+
test("fetchCodexModelOptions normalizes model catalog from API", async () => {
|
|
77
|
+
const { fetchCodexModelOptions } = await loadUtils();
|
|
78
|
+
const result = await fetchCodexModelOptions(async () => ({
|
|
79
|
+
models: [
|
|
80
|
+
{ id: "model-a", model: "gpt-5", displayName: "GPT-5", isDefault: true },
|
|
81
|
+
{ id: "model-b", model: "gpt-5", displayName: "Duplicate" },
|
|
82
|
+
{ id: "model-c", model: "gpt-4.1", displayName: "GPT-4.1" },
|
|
83
|
+
],
|
|
84
|
+
}));
|
|
85
|
+
assert.equal(result.available, true);
|
|
86
|
+
assert.equal(result.models.length, 2);
|
|
87
|
+
assert.deepEqual(result.models[0], {
|
|
88
|
+
model: "gpt-5",
|
|
89
|
+
displayName: "GPT-5",
|
|
90
|
+
isDefault: true,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
test("policy resolvers keep safe defaults", async () => {
|
|
94
|
+
const { applyBotModelPolicy, applyModelPolicyToBots, getBotPolicyState, resolveApprovalPolicy, resolveSandboxMode, } = await loadUtils();
|
|
95
|
+
assert.equal(resolveSandboxMode("read-only"), "read-only");
|
|
96
|
+
assert.equal(resolveSandboxMode("invalid"), "danger-full-access");
|
|
97
|
+
assert.equal(resolveApprovalPolicy("on-request"), "on-request");
|
|
98
|
+
assert.equal(resolveApprovalPolicy("invalid"), "never");
|
|
99
|
+
assert.deepEqual(getBotPolicyState({
|
|
100
|
+
provider: {
|
|
101
|
+
options: {
|
|
102
|
+
sandboxMode: "workspace-write",
|
|
103
|
+
approvalPolicy: "on-failure",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}), {
|
|
107
|
+
sandboxMode: "workspace-write",
|
|
108
|
+
approvalPolicy: "on-failure",
|
|
109
|
+
});
|
|
110
|
+
let capturedPath = "";
|
|
111
|
+
let capturedPayload = null;
|
|
112
|
+
await applyBotModelPolicy({
|
|
113
|
+
apiPost: async (path, payload) => {
|
|
114
|
+
capturedPath = path;
|
|
115
|
+
capturedPayload = payload;
|
|
116
|
+
return { ok: true };
|
|
117
|
+
},
|
|
118
|
+
botId: "worker-a",
|
|
119
|
+
botState: {
|
|
120
|
+
provider: {
|
|
121
|
+
options: {
|
|
122
|
+
sandboxMode: "read-only",
|
|
123
|
+
approvalPolicy: "on-request",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
model: "gpt-5",
|
|
128
|
+
});
|
|
129
|
+
assert.equal(capturedPath, "/api/bots/worker-a/policy");
|
|
130
|
+
assert.deepEqual(capturedPayload, {
|
|
131
|
+
sandboxMode: "read-only",
|
|
132
|
+
approvalPolicy: "on-request",
|
|
133
|
+
model: "gpt-5",
|
|
134
|
+
});
|
|
135
|
+
const posted = [];
|
|
136
|
+
const batchResult = await applyModelPolicyToBots({
|
|
137
|
+
apiPost: async (path, payload) => {
|
|
138
|
+
posted.push({ path, payload });
|
|
139
|
+
if (path.endsWith("/worker-b/policy")) {
|
|
140
|
+
throw new Error("restart failed");
|
|
141
|
+
}
|
|
142
|
+
return { ok: true };
|
|
143
|
+
},
|
|
144
|
+
bots: [
|
|
145
|
+
{
|
|
146
|
+
id: "worker-a",
|
|
147
|
+
provider: {
|
|
148
|
+
options: {
|
|
149
|
+
sandboxMode: "read-only",
|
|
150
|
+
approvalPolicy: "on-request",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "worker-b",
|
|
156
|
+
provider: {
|
|
157
|
+
options: {
|
|
158
|
+
sandboxMode: "workspace-write",
|
|
159
|
+
approvalPolicy: "on-failure",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
model: null,
|
|
165
|
+
});
|
|
166
|
+
assert.deepEqual(batchResult.updatedBotIds, ["worker-a"]);
|
|
167
|
+
assert.equal(batchResult.failures.length, 1);
|
|
168
|
+
assert.equal(batchResult.failures[0].botId, "worker-b");
|
|
169
|
+
assert.equal(posted.length, 2);
|
|
170
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Copilot Hub CLI and runtime bundle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"packages/*"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"build": "npm run build:scripts && npm run build
|
|
51
|
+
"build": "npm run build:scripts && npm run build:workspaces",
|
|
52
|
+
"build:workspaces": "npm run build --workspace @copilot-hub/contracts --if-present && npm run build --workspace @copilot-hub/core --if-present && npm run build --workspace @copilot-hub/agent-engine --if-present && npm run build --workspace @copilot-hub/control-plane --if-present",
|
|
52
53
|
"build:scripts": "tsc -p scripts/tsconfig.json",
|
|
53
54
|
"prepare:shared": "npm run build:scripts --silent && node scripts/dist/ensure-shared-build.mjs",
|
|
54
55
|
"start": "npm run build:scripts --silent && node scripts/dist/cli.mjs start",
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|
|
81
82
|
"@eslint/js": "^9.22.0",
|
|
83
|
+
"@types/express": "^5.0.6",
|
|
82
84
|
"@types/node": "^22.13.10",
|
|
83
85
|
"eslint": "^9.22.0",
|
|
84
86
|
"globals": "^16.0.0",
|
|
@@ -1,39 +1,120 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
type BotConfig = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
autoStart?: boolean;
|
|
7
|
+
threadMode?: string;
|
|
8
|
+
sharedThreadId?: string;
|
|
9
|
+
workspaceRoot: string;
|
|
10
|
+
dataDir: string;
|
|
11
|
+
provider?: {
|
|
12
|
+
kind?: string;
|
|
13
|
+
options?: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
kernelAccess?: {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
allowedActions?: unknown;
|
|
18
|
+
allowedChatIds?: unknown;
|
|
19
|
+
};
|
|
20
|
+
capabilities?: unknown[];
|
|
21
|
+
} & Record<string, unknown>;
|
|
22
|
+
type ProviderDefaults = Record<string, unknown>;
|
|
23
|
+
type KernelActionHandler = (payload: {
|
|
24
|
+
actorBotId: string;
|
|
25
|
+
action: unknown;
|
|
26
|
+
payload: Record<string, unknown>;
|
|
27
|
+
context: Record<string, unknown>;
|
|
28
|
+
}) => Promise<unknown> | unknown;
|
|
29
|
+
type WorkerStatusBase = {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
autoStart: boolean;
|
|
34
|
+
threadMode: string;
|
|
35
|
+
sharedThreadId: string;
|
|
36
|
+
providerKind: string;
|
|
37
|
+
kernelVersion: string | null;
|
|
38
|
+
webThreadId: string | null;
|
|
39
|
+
running: boolean;
|
|
40
|
+
telegramRunning: boolean;
|
|
41
|
+
telegramError: string | null;
|
|
42
|
+
workspaceRoot: string;
|
|
43
|
+
dataDir: string;
|
|
44
|
+
kernelAccess: {
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
allowedActions: string[];
|
|
47
|
+
allowedChatIds: string[];
|
|
48
|
+
};
|
|
49
|
+
capabilities: unknown[];
|
|
50
|
+
channels: unknown[];
|
|
51
|
+
};
|
|
52
|
+
type SupervisorStatus = WorkerStatusBase & {
|
|
53
|
+
lastHeartbeatAt: string | null;
|
|
54
|
+
lastHeartbeatError: string | null;
|
|
55
|
+
};
|
|
56
|
+
type PendingRequest = {
|
|
57
|
+
resolve: (value: unknown) => void;
|
|
58
|
+
reject: (error: Error) => void;
|
|
59
|
+
timer: NodeJS.Timeout;
|
|
60
|
+
};
|
|
1
61
|
export declare class AgentSupervisor {
|
|
2
62
|
#private;
|
|
63
|
+
botConfig: BotConfig;
|
|
64
|
+
providerDefaults: ProviderDefaults;
|
|
65
|
+
turnActivityTimeoutMs: number;
|
|
66
|
+
maxMessages: number;
|
|
67
|
+
webPublicBaseUrl: string;
|
|
68
|
+
workerScriptPath: string;
|
|
69
|
+
onKernelAction: KernelActionHandler | null;
|
|
70
|
+
child: ChildProcess | null;
|
|
71
|
+
startingPromise: Promise<void> | null;
|
|
72
|
+
shutdownRequested: boolean;
|
|
73
|
+
restartTimer: NodeJS.Timeout | null;
|
|
74
|
+
restartAttempt: number;
|
|
75
|
+
nextRequestId: number;
|
|
76
|
+
pendingRequests: Map<string, PendingRequest>;
|
|
77
|
+
desiredChannelsRunning: boolean;
|
|
78
|
+
heartbeatInFlight: Promise<SupervisorStatus> | null;
|
|
79
|
+
recoveryPromise: Promise<void> | null;
|
|
80
|
+
lastHeartbeatAt: string | null;
|
|
81
|
+
lastHeartbeatError: string | null;
|
|
82
|
+
statusCache: WorkerStatusBase;
|
|
3
83
|
constructor({ botConfig, providerDefaults, turnActivityTimeoutMs, maxMessages, webPublicBaseUrl, workerScriptPath, onKernelAction, }: {
|
|
4
|
-
botConfig:
|
|
5
|
-
providerDefaults:
|
|
6
|
-
turnActivityTimeoutMs:
|
|
7
|
-
maxMessages:
|
|
8
|
-
webPublicBaseUrl:
|
|
9
|
-
workerScriptPath:
|
|
10
|
-
onKernelAction?:
|
|
84
|
+
botConfig: BotConfig;
|
|
85
|
+
providerDefaults: ProviderDefaults;
|
|
86
|
+
turnActivityTimeoutMs: number;
|
|
87
|
+
maxMessages: number;
|
|
88
|
+
webPublicBaseUrl: string;
|
|
89
|
+
workerScriptPath: string;
|
|
90
|
+
onKernelAction?: KernelActionHandler | null;
|
|
11
91
|
});
|
|
12
92
|
get id(): string;
|
|
13
|
-
get config():
|
|
14
|
-
getStatus():
|
|
15
|
-
setWebPublicBaseUrl(value:
|
|
93
|
+
get config(): BotConfig;
|
|
94
|
+
getStatus(): SupervisorStatus;
|
|
95
|
+
setWebPublicBaseUrl(value: string): void;
|
|
16
96
|
boot(): Promise<void>;
|
|
17
97
|
ensureWorker(): Promise<void>;
|
|
18
|
-
startChannels(): Promise<
|
|
19
|
-
stopChannels(): Promise<
|
|
98
|
+
startChannels(): Promise<SupervisorStatus>;
|
|
99
|
+
stopChannels(): Promise<SupervisorStatus>;
|
|
20
100
|
resetWebThread(): Promise<unknown>;
|
|
21
|
-
listPendingApprovals(threadId
|
|
22
|
-
resolvePendingApproval({ threadId, approvalId, decision }: {
|
|
23
|
-
threadId:
|
|
24
|
-
approvalId:
|
|
25
|
-
decision:
|
|
101
|
+
listPendingApprovals(threadId?: string): Promise<unknown>;
|
|
102
|
+
resolvePendingApproval({ threadId, approvalId, decision, }: {
|
|
103
|
+
threadId: string;
|
|
104
|
+
approvalId: string;
|
|
105
|
+
decision: string;
|
|
26
106
|
}): Promise<unknown>;
|
|
27
|
-
listCapabilities(): Promise<
|
|
28
|
-
setCapabilities(nextCapabilities:
|
|
29
|
-
setProviderOptions(nextOptions:
|
|
30
|
-
reloadCapabilities(nextCapabilities?:
|
|
31
|
-
setProjectRoot(projectRoot:
|
|
32
|
-
refreshStatus(timeoutMs?: number): Promise<
|
|
33
|
-
heartbeat({ timeoutMs }?: {
|
|
34
|
-
timeoutMs?: number
|
|
35
|
-
}): Promise<
|
|
36
|
-
forceRestart(reason?: string): Promise<
|
|
107
|
+
listCapabilities(): Promise<unknown[]>;
|
|
108
|
+
setCapabilities(nextCapabilities: unknown): void;
|
|
109
|
+
setProviderOptions(nextOptions: Record<string, unknown>): Promise<SupervisorStatus>;
|
|
110
|
+
reloadCapabilities(nextCapabilities?: unknown): Promise<SupervisorStatus>;
|
|
111
|
+
setProjectRoot(projectRoot: string): Promise<SupervisorStatus>;
|
|
112
|
+
refreshStatus(timeoutMs?: number): Promise<SupervisorStatus>;
|
|
113
|
+
heartbeat({ timeoutMs, }?: {
|
|
114
|
+
timeoutMs?: number;
|
|
115
|
+
}): Promise<SupervisorStatus>;
|
|
116
|
+
forceRestart(reason?: string): Promise<SupervisorStatus>;
|
|
37
117
|
shutdown(): Promise<void>;
|
|
38
|
-
request(action:
|
|
118
|
+
request(action: string, payload?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
39
119
|
}
|
|
120
|
+
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import { fork } from "node:child_process";
|
|
3
2
|
const REQUEST_TIMEOUT_MS = 15000;
|
|
4
3
|
const HEARTBEAT_TIMEOUT_MS = 4000;
|
|
@@ -8,6 +7,26 @@ const WORKER_READY_POLL_INTERVAL_MS = 150;
|
|
|
8
7
|
const WORKER_RECOVERY_TIMEOUT_MS = 20000;
|
|
9
8
|
const WORKER_RECOVERY_POLL_INTERVAL_MS = 250;
|
|
10
9
|
export class AgentSupervisor {
|
|
10
|
+
botConfig;
|
|
11
|
+
providerDefaults;
|
|
12
|
+
turnActivityTimeoutMs;
|
|
13
|
+
maxMessages;
|
|
14
|
+
webPublicBaseUrl;
|
|
15
|
+
workerScriptPath;
|
|
16
|
+
onKernelAction;
|
|
17
|
+
child;
|
|
18
|
+
startingPromise;
|
|
19
|
+
shutdownRequested;
|
|
20
|
+
restartTimer;
|
|
21
|
+
restartAttempt;
|
|
22
|
+
nextRequestId;
|
|
23
|
+
pendingRequests;
|
|
24
|
+
desiredChannelsRunning;
|
|
25
|
+
heartbeatInFlight;
|
|
26
|
+
recoveryPromise;
|
|
27
|
+
lastHeartbeatAt;
|
|
28
|
+
lastHeartbeatError;
|
|
29
|
+
statusCache;
|
|
11
30
|
constructor({ botConfig, providerDefaults, turnActivityTimeoutMs, maxMessages, webPublicBaseUrl, workerScriptPath, onKernelAction = null, }) {
|
|
12
31
|
this.botConfig = botConfig;
|
|
13
32
|
this.providerDefaults = providerDefaults;
|
|
@@ -102,7 +121,7 @@ export class AgentSupervisor {
|
|
|
102
121
|
await this.ensureWorker();
|
|
103
122
|
return this.request("listPendingApprovals", { threadId });
|
|
104
123
|
}
|
|
105
|
-
async resolvePendingApproval({ threadId, approvalId, decision }) {
|
|
124
|
+
async resolvePendingApproval({ threadId, approvalId, decision, }) {
|
|
106
125
|
await this.ensureWorker();
|
|
107
126
|
return this.request("resolvePendingApproval", {
|
|
108
127
|
threadId,
|
|
@@ -136,6 +155,15 @@ export class AgentSupervisor {
|
|
|
136
155
|
if (typeof nextOptions?.approvalPolicy === "string" && nextOptions.approvalPolicy.trim()) {
|
|
137
156
|
mergedOptions.approvalPolicy = nextOptions.approvalPolicy.trim();
|
|
138
157
|
}
|
|
158
|
+
if (Object.prototype.hasOwnProperty.call(nextOptions ?? {}, "model")) {
|
|
159
|
+
const normalizedModel = String(nextOptions?.model ?? "").trim();
|
|
160
|
+
if (normalizedModel) {
|
|
161
|
+
mergedOptions.model = normalizedModel;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
delete mergedOptions.model;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
139
167
|
this.botConfig = {
|
|
140
168
|
...previousConfig,
|
|
141
169
|
provider: {
|
|
@@ -145,7 +173,7 @@ export class AgentSupervisor {
|
|
|
145
173
|
},
|
|
146
174
|
};
|
|
147
175
|
try {
|
|
148
|
-
const status = await this.forceRestart("provider
|
|
176
|
+
const status = await this.forceRestart("provider options updated");
|
|
149
177
|
this.#updateStatus(status);
|
|
150
178
|
return this.getStatus();
|
|
151
179
|
}
|
|
@@ -159,7 +187,7 @@ export class AgentSupervisor {
|
|
|
159
187
|
}
|
|
160
188
|
this.botConfig = previousConfig;
|
|
161
189
|
try {
|
|
162
|
-
await this.forceRestart("provider
|
|
190
|
+
await this.forceRestart("provider options rollback");
|
|
163
191
|
}
|
|
164
192
|
catch {
|
|
165
193
|
// Best effort rollback only.
|
|
@@ -172,9 +200,11 @@ export class AgentSupervisor {
|
|
|
172
200
|
this.setCapabilities(nextCapabilities);
|
|
173
201
|
}
|
|
174
202
|
await this.ensureWorker();
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
203
|
+
const payload = {};
|
|
204
|
+
if (Array.isArray(nextCapabilities)) {
|
|
205
|
+
payload.capabilityDefinitions = nextCapabilities;
|
|
206
|
+
}
|
|
207
|
+
const status = await this.request("reloadCapabilities", payload);
|
|
178
208
|
this.#updateStatus(status);
|
|
179
209
|
return this.getStatus();
|
|
180
210
|
}
|
|
@@ -190,7 +220,7 @@ export class AgentSupervisor {
|
|
|
190
220
|
this.#updateStatus(status);
|
|
191
221
|
return this.getStatus();
|
|
192
222
|
}
|
|
193
|
-
async heartbeat({ timeoutMs = HEARTBEAT_TIMEOUT_MS } = {}) {
|
|
223
|
+
async heartbeat({ timeoutMs = HEARTBEAT_TIMEOUT_MS, } = {}) {
|
|
194
224
|
if (this.shutdownRequested) {
|
|
195
225
|
return this.getStatus();
|
|
196
226
|
}
|
|
@@ -274,7 +304,7 @@ export class AgentSupervisor {
|
|
|
274
304
|
const child = this.child;
|
|
275
305
|
this.child = null;
|
|
276
306
|
try {
|
|
277
|
-
child
|
|
307
|
+
child?.kill("SIGTERM");
|
|
278
308
|
}
|
|
279
309
|
catch {
|
|
280
310
|
// Ignore.
|
|
@@ -282,7 +312,8 @@ export class AgentSupervisor {
|
|
|
282
312
|
this.#setOfflineStatus();
|
|
283
313
|
}
|
|
284
314
|
request(action, payload = null, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
285
|
-
|
|
315
|
+
const child = this.child;
|
|
316
|
+
if (!child || !child.connected) {
|
|
286
317
|
return Promise.reject(new Error(`Worker '${this.id}' is not running.`));
|
|
287
318
|
}
|
|
288
319
|
const requestId = `${this.id}_${Date.now()}_${this.nextRequestId++}`;
|
|
@@ -297,7 +328,7 @@ export class AgentSupervisor {
|
|
|
297
328
|
timer,
|
|
298
329
|
});
|
|
299
330
|
try {
|
|
300
|
-
|
|
331
|
+
child.send({
|
|
301
332
|
type: "request",
|
|
302
333
|
requestId,
|
|
303
334
|
action,
|
|
@@ -307,7 +338,7 @@ export class AgentSupervisor {
|
|
|
307
338
|
catch (error) {
|
|
308
339
|
clearTimeout(timer);
|
|
309
340
|
this.pendingRequests.delete(requestId);
|
|
310
|
-
reject(error);
|
|
341
|
+
reject(toError(error));
|
|
311
342
|
}
|
|
312
343
|
});
|
|
313
344
|
}
|
|
@@ -317,6 +348,7 @@ export class AgentSupervisor {
|
|
|
317
348
|
clearTimeout(this.restartTimer);
|
|
318
349
|
this.restartTimer = null;
|
|
319
350
|
}
|
|
351
|
+
const windowsForkOptions = process.platform === "win32" ? { windowsHide: true } : {};
|
|
320
352
|
const child = fork(this.workerScriptPath, [], {
|
|
321
353
|
cwd: process.cwd(),
|
|
322
354
|
env: {
|
|
@@ -328,8 +360,8 @@ export class AgentSupervisor {
|
|
|
328
360
|
AGENT_WEB_PUBLIC_BASE_URL: this.webPublicBaseUrl,
|
|
329
361
|
AGENT_KERNEL_REQUEST_TIMEOUT_MS: String(REQUEST_TIMEOUT_MS),
|
|
330
362
|
},
|
|
363
|
+
...windowsForkOptions,
|
|
331
364
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
332
|
-
windowsHide: true,
|
|
333
365
|
});
|
|
334
366
|
this.child = child;
|
|
335
367
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -356,32 +388,35 @@ export class AgentSupervisor {
|
|
|
356
388
|
await this.refreshStatus();
|
|
357
389
|
}
|
|
358
390
|
#handleWorkerMessage(message) {
|
|
359
|
-
|
|
391
|
+
const record = asRecord(message);
|
|
392
|
+
if (!record.type) {
|
|
360
393
|
return;
|
|
361
394
|
}
|
|
362
|
-
if (
|
|
363
|
-
const
|
|
395
|
+
if (record.type === "response") {
|
|
396
|
+
const response = record;
|
|
397
|
+
const requestId = String(response.requestId ?? "");
|
|
364
398
|
const pending = this.pendingRequests.get(requestId);
|
|
365
399
|
if (!pending) {
|
|
366
400
|
return;
|
|
367
401
|
}
|
|
368
402
|
this.pendingRequests.delete(requestId);
|
|
369
403
|
clearTimeout(pending.timer);
|
|
370
|
-
if (
|
|
371
|
-
pending.resolve(
|
|
404
|
+
if (response.ok) {
|
|
405
|
+
pending.resolve(response.result);
|
|
372
406
|
}
|
|
373
407
|
else {
|
|
374
|
-
pending.reject(new Error(String(
|
|
408
|
+
pending.reject(new Error(String(response.error ?? "Unknown worker error.")));
|
|
375
409
|
}
|
|
376
410
|
return;
|
|
377
411
|
}
|
|
378
|
-
if (
|
|
379
|
-
|
|
412
|
+
if (record.type === "event") {
|
|
413
|
+
const event = record;
|
|
414
|
+
if (event.event === "workerReady") {
|
|
380
415
|
return;
|
|
381
416
|
}
|
|
382
417
|
}
|
|
383
|
-
if (
|
|
384
|
-
void this.#handleKernelRequest(
|
|
418
|
+
if (record.type === "kernelRequest") {
|
|
419
|
+
void this.#handleKernelRequest(record);
|
|
385
420
|
}
|
|
386
421
|
}
|
|
387
422
|
async #handleKernelRequest(message) {
|
|
@@ -401,8 +436,8 @@ export class AgentSupervisor {
|
|
|
401
436
|
const result = await this.onKernelAction({
|
|
402
437
|
actorBotId: this.id,
|
|
403
438
|
action: message?.action,
|
|
404
|
-
payload: message?.payload
|
|
405
|
-
context: message?.context
|
|
439
|
+
payload: asRecord(message?.payload),
|
|
440
|
+
context: asRecord(message?.context),
|
|
406
441
|
});
|
|
407
442
|
this.#sendKernelResponse({
|
|
408
443
|
requestId,
|
|
@@ -418,7 +453,7 @@ export class AgentSupervisor {
|
|
|
418
453
|
});
|
|
419
454
|
}
|
|
420
455
|
}
|
|
421
|
-
#sendKernelResponse({ requestId, ok, result = null, error = null }) {
|
|
456
|
+
#sendKernelResponse({ requestId, ok, result = null, error = null, }) {
|
|
422
457
|
if (!this.child || !this.child.connected) {
|
|
423
458
|
return;
|
|
424
459
|
}
|
|
@@ -435,7 +470,7 @@ export class AgentSupervisor {
|
|
|
435
470
|
// Ignore response send errors.
|
|
436
471
|
}
|
|
437
472
|
}
|
|
438
|
-
#handleWorkerExit({ code, signal }) {
|
|
473
|
+
#handleWorkerExit({ code, signal, }) {
|
|
439
474
|
if (this.child) {
|
|
440
475
|
this.child = null;
|
|
441
476
|
}
|
|
@@ -454,20 +489,23 @@ export class AgentSupervisor {
|
|
|
454
489
|
}, delayMs);
|
|
455
490
|
}
|
|
456
491
|
#rejectAllPending(error) {
|
|
492
|
+
const normalized = toError(error);
|
|
457
493
|
for (const pending of this.pendingRequests.values()) {
|
|
458
494
|
clearTimeout(pending.timer);
|
|
459
|
-
pending.reject(
|
|
495
|
+
pending.reject(normalized);
|
|
460
496
|
}
|
|
461
497
|
this.pendingRequests.clear();
|
|
462
498
|
}
|
|
463
499
|
#updateStatus(status) {
|
|
464
|
-
|
|
500
|
+
const statusRecord = asRecord(status);
|
|
501
|
+
if (Object.keys(statusRecord).length === 0) {
|
|
465
502
|
return;
|
|
466
503
|
}
|
|
504
|
+
const running = typeof statusRecord.running === "boolean" ? statusRecord.running : this.statusCache.running;
|
|
467
505
|
this.statusCache = {
|
|
468
506
|
...this.statusCache,
|
|
469
|
-
...
|
|
470
|
-
running
|
|
507
|
+
...statusRecord,
|
|
508
|
+
running,
|
|
471
509
|
};
|
|
472
510
|
if (this.statusCache.running) {
|
|
473
511
|
this.restartAttempt = 0;
|
|
@@ -499,29 +537,27 @@ async function waitForWorkerReady(supervisor, child) {
|
|
|
499
537
|
throw new Error(`Worker '${supervisor.id}' did not become ready within ${timeoutMs}ms.`);
|
|
500
538
|
}
|
|
501
539
|
function createInitialStatus(botConfig) {
|
|
540
|
+
const provider = asRecord(botConfig.provider);
|
|
541
|
+
const kernelAccess = asRecord(botConfig.kernelAccess);
|
|
502
542
|
return {
|
|
503
|
-
id: botConfig.id,
|
|
504
|
-
name: botConfig.name,
|
|
543
|
+
id: String(botConfig.id),
|
|
544
|
+
name: String(botConfig.name),
|
|
505
545
|
enabled: botConfig.enabled !== false,
|
|
506
546
|
autoStart: Boolean(botConfig.autoStart),
|
|
507
|
-
threadMode: botConfig.threadMode,
|
|
508
|
-
sharedThreadId: botConfig.sharedThreadId,
|
|
509
|
-
providerKind:
|
|
547
|
+
threadMode: String(botConfig.threadMode ?? "single"),
|
|
548
|
+
sharedThreadId: String(botConfig.sharedThreadId ?? ""),
|
|
549
|
+
providerKind: String(provider.kind ?? "codex"),
|
|
510
550
|
kernelVersion: null,
|
|
511
551
|
webThreadId: null,
|
|
512
552
|
running: false,
|
|
513
553
|
telegramRunning: false,
|
|
514
554
|
telegramError: null,
|
|
515
|
-
workspaceRoot: botConfig.workspaceRoot,
|
|
516
|
-
dataDir: botConfig.dataDir,
|
|
555
|
+
workspaceRoot: String(botConfig.workspaceRoot),
|
|
556
|
+
dataDir: String(botConfig.dataDir),
|
|
517
557
|
kernelAccess: {
|
|
518
|
-
enabled:
|
|
519
|
-
allowedActions:
|
|
520
|
-
|
|
521
|
-
: [],
|
|
522
|
-
allowedChatIds: Array.isArray(botConfig.kernelAccess?.allowedChatIds)
|
|
523
|
-
? [...botConfig.kernelAccess.allowedChatIds]
|
|
524
|
-
: [],
|
|
558
|
+
enabled: kernelAccess.enabled === true,
|
|
559
|
+
allowedActions: normalizeStringList(kernelAccess.allowedActions),
|
|
560
|
+
allowedChatIds: normalizeStringList(kernelAccess.allowedChatIds),
|
|
525
561
|
},
|
|
526
562
|
capabilities: [],
|
|
527
563
|
channels: [],
|
|
@@ -584,4 +620,22 @@ function sanitizeError(error) {
|
|
|
584
620
|
const raw = error instanceof Error ? error.message : String(error);
|
|
585
621
|
return raw.split(/\r?\n/).slice(0, 12).join("\n");
|
|
586
622
|
}
|
|
623
|
+
function asRecord(value) {
|
|
624
|
+
return value && typeof value === "object" ? value : {};
|
|
625
|
+
}
|
|
626
|
+
function toError(error) {
|
|
627
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
628
|
+
}
|
|
629
|
+
function normalizeStringList(value) {
|
|
630
|
+
if (Array.isArray(value)) {
|
|
631
|
+
return value.map((entry) => String(entry ?? "").trim()).filter(Boolean);
|
|
632
|
+
}
|
|
633
|
+
if (typeof value === "string") {
|
|
634
|
+
return value
|
|
635
|
+
.split(",")
|
|
636
|
+
.map((entry) => entry.trim())
|
|
637
|
+
.filter(Boolean);
|
|
638
|
+
}
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
587
641
|
//# sourceMappingURL=agent-supervisor.js.map
|