doer-agent 0.9.0 → 0.9.2
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/agent-codex-cli.js +37 -1
- package/dist/agent-codex-cli.test.js +44 -0
- package/dist/agent-connectivity.test.js +42 -0
- package/dist/agent-jetstream.js +17 -1
- package/dist/agent-runtime-io.js +31 -8
- package/dist/agent-settings.js +36 -2
- package/dist/agent.js +3 -1
- package/package.json +1 -1
package/dist/agent-codex-cli.js
CHANGED
|
@@ -13,6 +13,11 @@ function toTomlStringLiteral(value) {
|
|
|
13
13
|
function toTomlStringArray(values) {
|
|
14
14
|
return `[${values.map((value) => toTomlStringLiteral(value)).join(", ")}]`;
|
|
15
15
|
}
|
|
16
|
+
function toTomlStringMap(values) {
|
|
17
|
+
return `{ ${Object.entries(values)
|
|
18
|
+
.map(([key, value]) => `${toTomlStringLiteral(key)} = ${toTomlStringLiteral(value)}`)
|
|
19
|
+
.join(", ")} }`;
|
|
20
|
+
}
|
|
16
21
|
function buildMcpServerConfigArgs(args) {
|
|
17
22
|
const serverName = args.serverName.trim();
|
|
18
23
|
const configArgs = [
|
|
@@ -28,6 +33,25 @@ function buildMcpServerConfigArgs(args) {
|
|
|
28
33
|
}
|
|
29
34
|
return configArgs;
|
|
30
35
|
}
|
|
36
|
+
function buildRemoteMcpServerConfigArgs(args) {
|
|
37
|
+
const prefix = `mcp_servers.${args.serverName.trim()}`;
|
|
38
|
+
const configArgs = [
|
|
39
|
+
"--config",
|
|
40
|
+
`${prefix}.url=${toTomlStringLiteral(args.url)}`,
|
|
41
|
+
"--config",
|
|
42
|
+
`${prefix}.enabled=${args.enabled === false ? "false" : "true"}`,
|
|
43
|
+
];
|
|
44
|
+
if (args.bearerTokenEnvVar?.trim()) {
|
|
45
|
+
configArgs.push("--config", `${prefix}.bearer_token_env_var=${toTomlStringLiteral(args.bearerTokenEnvVar.trim())}`);
|
|
46
|
+
}
|
|
47
|
+
if (Object.keys(args.httpHeaders ?? {}).length > 0) {
|
|
48
|
+
configArgs.push("--config", `${prefix}.http_headers=${toTomlStringMap(args.httpHeaders ?? {})}`);
|
|
49
|
+
}
|
|
50
|
+
if (Object.keys(args.envHttpHeaders ?? {}).length > 0) {
|
|
51
|
+
configArgs.push("--config", `${prefix}.env_http_headers=${toTomlStringMap(args.envHttpHeaders ?? {})}`);
|
|
52
|
+
}
|
|
53
|
+
return configArgs;
|
|
54
|
+
}
|
|
31
55
|
function hasDirectCodexBinary() {
|
|
32
56
|
const result = spawnSync("bash", ["-lc", "command -v codex >/dev/null 2>&1"], {
|
|
33
57
|
stdio: "ignore",
|
|
@@ -124,10 +148,22 @@ export function buildCustomMcpConfigArgs(servers) {
|
|
|
124
148
|
const seenNames = new Set();
|
|
125
149
|
for (const server of servers) {
|
|
126
150
|
const serverName = server.name.trim();
|
|
127
|
-
|
|
151
|
+
const hasEndpoint = server.transport === "streamable_http" ? server.url.trim() : server.command.trim();
|
|
152
|
+
if (!server.enabled || !serverName || !hasEndpoint || reservedNames.has(serverName) || seenNames.has(serverName)) {
|
|
128
153
|
continue;
|
|
129
154
|
}
|
|
130
155
|
seenNames.add(serverName);
|
|
156
|
+
if (server.transport === "streamable_http") {
|
|
157
|
+
configArgs.push(...buildRemoteMcpServerConfigArgs({
|
|
158
|
+
serverName,
|
|
159
|
+
url: server.url,
|
|
160
|
+
bearerTokenEnvVar: server.bearerTokenEnvVar,
|
|
161
|
+
httpHeaders: Object.fromEntries(server.httpHeaders.map((header) => [header.key, header.value])),
|
|
162
|
+
envHttpHeaders: Object.fromEntries(server.envHttpHeaders.map((header) => [header.key, header.value])),
|
|
163
|
+
enabled: true,
|
|
164
|
+
}));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
131
167
|
configArgs.push(...buildMcpServerConfigArgs({
|
|
132
168
|
serverName,
|
|
133
169
|
command: server.command,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildCustomMcpConfigArgs } from "./agent-codex-cli.js";
|
|
4
|
+
test("buildCustomMcpConfigArgs builds streamable HTTP MCP overrides", () => {
|
|
5
|
+
const args = buildCustomMcpConfigArgs([{
|
|
6
|
+
name: "remote_docs",
|
|
7
|
+
transport: "streamable_http",
|
|
8
|
+
command: "",
|
|
9
|
+
args: [],
|
|
10
|
+
env: [],
|
|
11
|
+
url: "https://example.com/mcp",
|
|
12
|
+
bearerTokenEnvVar: "MCP_TOKEN",
|
|
13
|
+
httpHeaders: [{ key: "X-Region", value: "seoul" }],
|
|
14
|
+
envHttpHeaders: [{ key: "X-API-Key", value: "MCP_API_KEY" }],
|
|
15
|
+
enabled: true,
|
|
16
|
+
}]);
|
|
17
|
+
assert.deepEqual(args, [
|
|
18
|
+
"--config", 'mcp_servers.remote_docs.url="https://example.com/mcp"',
|
|
19
|
+
"--config", "mcp_servers.remote_docs.enabled=true",
|
|
20
|
+
"--config", 'mcp_servers.remote_docs.bearer_token_env_var="MCP_TOKEN"',
|
|
21
|
+
"--config", 'mcp_servers.remote_docs.http_headers={ "X-Region" = "seoul" }',
|
|
22
|
+
"--config", 'mcp_servers.remote_docs.env_http_headers={ "X-API-Key" = "MCP_API_KEY" }',
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
test("buildCustomMcpConfigArgs keeps stdio MCP overrides", () => {
|
|
26
|
+
const args = buildCustomMcpConfigArgs([{
|
|
27
|
+
name: "local_tools",
|
|
28
|
+
transport: "stdio",
|
|
29
|
+
command: "npx",
|
|
30
|
+
args: ["-y", "local-mcp"],
|
|
31
|
+
env: [{ key: "LOCAL_TOKEN", value: "secret" }],
|
|
32
|
+
url: "",
|
|
33
|
+
bearerTokenEnvVar: "",
|
|
34
|
+
httpHeaders: [],
|
|
35
|
+
envHttpHeaders: [],
|
|
36
|
+
enabled: true,
|
|
37
|
+
}]);
|
|
38
|
+
assert.deepEqual(args, [
|
|
39
|
+
"--config", 'mcp_servers.local_tools.command="npx"',
|
|
40
|
+
"--config", 'mcp_servers.local_tools.args=["-y", "local-mcp"]',
|
|
41
|
+
"--config", "mcp_servers.local_tools.enabled=true",
|
|
42
|
+
"--config", 'mcp_servers.local_tools.env.LOCAL_TOKEN="secret"',
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildNatsConnectionOptions } from "./agent-jetstream.js";
|
|
4
|
+
import { heartbeatAgentSession } from "./agent-runtime-io.js";
|
|
5
|
+
test("NATS connection detects stale networks quickly and keeps reconnecting", () => {
|
|
6
|
+
const options = buildNatsConnectionOptions({
|
|
7
|
+
servers: ["tls://nats.example.com:4222"],
|
|
8
|
+
token: "secret",
|
|
9
|
+
});
|
|
10
|
+
assert.equal(options.pingInterval, 2_000);
|
|
11
|
+
assert.equal(options.maxPingOut, 2);
|
|
12
|
+
assert.equal(options.maxReconnectAttempts, -1);
|
|
13
|
+
assert.equal(options.reconnectTimeWait, 1_000);
|
|
14
|
+
assert.equal(options.token, "secret");
|
|
15
|
+
});
|
|
16
|
+
test("heartbeat stops waiting when a NATS flush stalls", async () => {
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
await assert.rejects(heartbeatAgentSession({
|
|
19
|
+
nc: { flush: () => new Promise(() => undefined) },
|
|
20
|
+
serverBaseUrl: "https://doer.example.com",
|
|
21
|
+
userId: "user-1",
|
|
22
|
+
agentToken: "token-1",
|
|
23
|
+
timeoutMs: 20,
|
|
24
|
+
postJson: async () => ({ ok: true }),
|
|
25
|
+
}), /nats flush timed out after 20ms/);
|
|
26
|
+
assert.ok(Date.now() - startedAt < 500);
|
|
27
|
+
});
|
|
28
|
+
test("heartbeat forwards its timeout to the HTTP probe", async () => {
|
|
29
|
+
let observedTimeout;
|
|
30
|
+
await heartbeatAgentSession({
|
|
31
|
+
nc: { flush: async () => undefined },
|
|
32
|
+
serverBaseUrl: "https://doer.example.com",
|
|
33
|
+
userId: "user-1",
|
|
34
|
+
agentToken: "token-1",
|
|
35
|
+
timeoutMs: 3_000,
|
|
36
|
+
postJson: async (_url, _body, options) => {
|
|
37
|
+
observedTimeout = options?.timeoutMs;
|
|
38
|
+
return { ok: true };
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
assert.equal(observedTimeout, 3_000);
|
|
42
|
+
});
|
package/dist/agent-jetstream.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { AckPolicy, connect, DeliverPolicy, JSONCodec, RetentionPolicy, StorageType, } from "nats";
|
|
2
|
+
const NATS_PING_INTERVAL_MS = 2_000;
|
|
3
|
+
const NATS_MAX_PING_OUT = 2;
|
|
4
|
+
const NATS_RECONNECT_TIME_WAIT_MS = 1_000;
|
|
5
|
+
const NATS_RECONNECT_JITTER_MS = 250;
|
|
6
|
+
export function buildNatsConnectionOptions(args) {
|
|
7
|
+
return {
|
|
8
|
+
servers: args.servers,
|
|
9
|
+
...(args.token ? { token: args.token } : {}),
|
|
10
|
+
pingInterval: NATS_PING_INTERVAL_MS,
|
|
11
|
+
maxPingOut: NATS_MAX_PING_OUT,
|
|
12
|
+
maxReconnectAttempts: -1,
|
|
13
|
+
reconnectTimeWait: NATS_RECONNECT_TIME_WAIT_MS,
|
|
14
|
+
reconnectJitter: NATS_RECONNECT_JITTER_MS,
|
|
15
|
+
reconnectJitterTLS: NATS_RECONNECT_JITTER_MS,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
2
18
|
export function normalizeNatsServers(value) {
|
|
3
19
|
if (!Array.isArray(value)) {
|
|
4
20
|
return [];
|
|
@@ -55,7 +71,7 @@ async function initJetStreamContext(args) {
|
|
|
55
71
|
const stream = `DOER_AGENT_EVENTS_${sanitized}`;
|
|
56
72
|
const subject = `doer.agent.events.${sanitized}`;
|
|
57
73
|
const durable = `doer-agent-uploader-${sanitized}`;
|
|
58
|
-
const nc = await connect(
|
|
74
|
+
const nc = await connect(buildNatsConnectionOptions({ servers: args.servers, token: args.token }));
|
|
59
75
|
const jsm = await nc.jetstreamManager();
|
|
60
76
|
await ensureJetStreamInfra({ jsm, stream, subject, durable });
|
|
61
77
|
void (async () => {
|
package/dist/agent-runtime-io.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
export async function postJson(url, body) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
export async function postJson(url, body, options = {}) {
|
|
2
|
+
let res;
|
|
3
|
+
try {
|
|
4
|
+
res = await fetch(url, {
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: { "Content-Type": "application/json" },
|
|
7
|
+
body: JSON.stringify(body),
|
|
8
|
+
...(options.timeoutMs ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (options.timeoutMs && error instanceof Error && error.name === "TimeoutError") {
|
|
13
|
+
throw new Error(`request timed out after ${options.timeoutMs}ms`);
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
7
17
|
const text = await res.text();
|
|
8
18
|
let data = {};
|
|
9
19
|
if (text) {
|
|
@@ -104,9 +114,22 @@ export function createEventPersistenceHelpers(args) {
|
|
|
104
114
|
};
|
|
105
115
|
}
|
|
106
116
|
export async function heartbeatAgentSession(args) {
|
|
107
|
-
|
|
117
|
+
let timeout;
|
|
118
|
+
try {
|
|
119
|
+
await Promise.race([
|
|
120
|
+
args.nc.flush(),
|
|
121
|
+
new Promise((_, reject) => {
|
|
122
|
+
timeout = setTimeout(() => reject(new Error(`nats flush timed out after ${args.timeoutMs}ms`)), args.timeoutMs);
|
|
123
|
+
}),
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
if (timeout) {
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
108
131
|
await args.postJson(`${args.serverBaseUrl}/api/agent/heartbeat`, {
|
|
109
132
|
userId: args.userId,
|
|
110
133
|
agentToken: args.agentToken,
|
|
111
|
-
});
|
|
134
|
+
}, { timeoutMs: args.timeoutMs });
|
|
112
135
|
}
|
package/dist/agent-settings.js
CHANGED
|
@@ -172,7 +172,7 @@ function normalizeMcpServerName(value) {
|
|
|
172
172
|
if (!trimmed || !/^[A-Za-z0-9_-]+$/.test(trimmed)) {
|
|
173
173
|
return null;
|
|
174
174
|
}
|
|
175
|
-
if (trimmed === "doer_daemon" || trimmed === "doer_mobile") {
|
|
175
|
+
if (trimmed === "doer_daemon" || trimmed === "doer_mobile" || trimmed === "doer_threads") {
|
|
176
176
|
return null;
|
|
177
177
|
}
|
|
178
178
|
return trimmed;
|
|
@@ -185,14 +185,38 @@ function normalizeStringArray(value) {
|
|
|
185
185
|
.map((item) => (typeof item === "string" ? item.replace(/\r/g, "") : null))
|
|
186
186
|
.filter((item) => item !== null && item.trim().length > 0);
|
|
187
187
|
}
|
|
188
|
+
function normalizeMcpHeaderEntries(value) {
|
|
189
|
+
if (!Array.isArray(value)) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const entries = [];
|
|
193
|
+
const seenKeys = new Set();
|
|
194
|
+
for (const item of value) {
|
|
195
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const raw = item;
|
|
199
|
+
const key = typeof raw.key === "string" ? raw.key.trim() : "";
|
|
200
|
+
if (!key || typeof raw.value !== "string" || seenKeys.has(key)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
seenKeys.add(key);
|
|
204
|
+
entries.push({ key, value: raw.value.replace(/\r/g, "") });
|
|
205
|
+
}
|
|
206
|
+
return entries;
|
|
207
|
+
}
|
|
188
208
|
function normalizeAgentMcpServer(value) {
|
|
189
209
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
190
210
|
return null;
|
|
191
211
|
}
|
|
192
212
|
const raw = value;
|
|
193
213
|
const name = normalizeMcpServerName(raw.name);
|
|
214
|
+
const transport = raw.transport === "streamable_http" || (typeof raw.url === "string" && raw.url.trim())
|
|
215
|
+
? "streamable_http"
|
|
216
|
+
: "stdio";
|
|
194
217
|
const command = typeof raw.command === "string" ? raw.command.trim() : "";
|
|
195
|
-
|
|
218
|
+
const url = typeof raw.url === "string" ? raw.url.trim() : "";
|
|
219
|
+
if (!name || (transport === "stdio" ? !command : !url)) {
|
|
196
220
|
return null;
|
|
197
221
|
}
|
|
198
222
|
const env = [];
|
|
@@ -208,9 +232,14 @@ function normalizeAgentMcpServer(value) {
|
|
|
208
232
|
}
|
|
209
233
|
return {
|
|
210
234
|
name,
|
|
235
|
+
transport,
|
|
211
236
|
command,
|
|
212
237
|
args: normalizeStringArray(raw.args),
|
|
213
238
|
env,
|
|
239
|
+
url,
|
|
240
|
+
bearerTokenEnvVar: typeof raw.bearerTokenEnvVar === "string" ? raw.bearerTokenEnvVar.trim() : "",
|
|
241
|
+
httpHeaders: normalizeMcpHeaderEntries(raw.httpHeaders),
|
|
242
|
+
envHttpHeaders: normalizeMcpHeaderEntries(raw.envHttpHeaders),
|
|
214
243
|
enabled: raw.enabled !== false,
|
|
215
244
|
};
|
|
216
245
|
}
|
|
@@ -386,12 +415,17 @@ export async function toAgentSettingsPublic(args) {
|
|
|
386
415
|
mcp: {
|
|
387
416
|
servers: args.config.mcp.servers.map((server) => ({
|
|
388
417
|
name: server.name,
|
|
418
|
+
transport: server.transport,
|
|
389
419
|
command: server.command,
|
|
390
420
|
args: [...server.args],
|
|
391
421
|
env: server.env.map((variable) => ({
|
|
392
422
|
key: variable.key,
|
|
393
423
|
value: variable.value,
|
|
394
424
|
})),
|
|
425
|
+
url: server.url,
|
|
426
|
+
bearerTokenEnvVar: server.bearerTokenEnvVar,
|
|
427
|
+
httpHeaders: server.httpHeaders.map((header) => ({ ...header })),
|
|
428
|
+
envHttpHeaders: server.envHttpHeaders.map((header) => ({ ...header })),
|
|
395
429
|
enabled: server.enabled,
|
|
396
430
|
})),
|
|
397
431
|
},
|
package/dist/agent.js
CHANGED
|
@@ -28,7 +28,8 @@ const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
|
|
|
28
28
|
const AGENT_PACKAGE_JSON_PATH = path.join(AGENT_PROJECT_DIR, "package.json");
|
|
29
29
|
const BUNDLED_SKILLS_ROOT = path.join(AGENT_PROJECT_DIR, "runtime", "skills");
|
|
30
30
|
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
31
|
-
const HEARTBEAT_FAILURE_THRESHOLD =
|
|
31
|
+
const HEARTBEAT_FAILURE_THRESHOLD = 2;
|
|
32
|
+
const HEARTBEAT_REQUEST_TIMEOUT_MS = 3_000;
|
|
32
33
|
const codexAppEventCodec = StringCodec();
|
|
33
34
|
let activeTaskLogContext = null;
|
|
34
35
|
let workspaceRootOverride = null;
|
|
@@ -187,6 +188,7 @@ const eventPersistenceHelpers = createEventPersistenceHelpers({
|
|
|
187
188
|
const heartbeatSession = async (args) => {
|
|
188
189
|
await heartbeatAgentSession({
|
|
189
190
|
...args,
|
|
191
|
+
timeoutMs: HEARTBEAT_REQUEST_TIMEOUT_MS,
|
|
190
192
|
postJson,
|
|
191
193
|
});
|
|
192
194
|
};
|