totemlayer 0.1.14 → 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/bin/agent-runner.mjs +5 -1
- package/bin/agent-setup.mjs +45 -11
- package/bin/connected-agent-service.mjs +8 -2
- package/bin/executor-readiness.mjs +37 -0
- package/bin/inbox-queue.mjs +71 -0
- package/bin/installation.mjs +30 -5
- package/bin/model-adapter.mjs +13 -2
- package/bin/network-runtime.mjs +106 -81
- package/bin/platform-client.mjs +30 -2
- package/bin/runtime-diagnostics.mjs +13 -0
- package/bin/totem.mjs +1 -1
- package/bin/worker-core/adapters.mjs +25 -3
- package/bin/worker-core/errors.mjs +1 -4
- package/package.json +1 -1
- package/scripts/prepare.mjs +0 -24
package/bin/agent-runner.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { parseArgs } from "node:util";
|
|
|
9
9
|
import { readAgentCredential, readAgentProfile, withAgentProfileLock } from "./agent-profile.mjs";
|
|
10
10
|
import { localClientEnvironment } from "./platform-client.mjs";
|
|
11
11
|
import { Totem } from "./sdk/index.js";
|
|
12
|
+
import { RUNTIME_ERROR_CODES, runtimeErrorCode } from "./runtime-diagnostics.mjs";
|
|
12
13
|
|
|
13
14
|
const CLI_PATH = fileURLToPath(new URL("./totem.mjs", import.meta.url));
|
|
14
15
|
const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
@@ -29,7 +30,7 @@ const LABELS = {
|
|
|
29
30
|
reconnecting: "Connection interrupted. Waiting to reconnect from saved progress.",
|
|
30
31
|
working: "Working on an assigned Mission task.",
|
|
31
32
|
waiting: "Connected and waiting for approved work.",
|
|
32
|
-
blocked: "
|
|
33
|
+
blocked: "Agent execution needs attention. Check status for the blocker.",
|
|
33
34
|
stopping: "Stopping the agent and saving its progress…",
|
|
34
35
|
stopped: "Agent stopped. Saved progress will be checked when it starts again.",
|
|
35
36
|
failed:
|
|
@@ -230,6 +231,7 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
230
231
|
: "interrupted",
|
|
231
232
|
runId: record.runId,
|
|
232
233
|
updatedAt: record.updatedAt,
|
|
234
|
+
...(RUNTIME_ERROR_CODES.has(record.blockerCode) ? { blockerCode: record.blockerCode } : {}),
|
|
233
235
|
...(UUID.test(record.missionId ?? "") ? { missionId: record.missionId } : {}),
|
|
234
236
|
...(UUID.test(record.workItemId ?? "") ? { workItemId: record.workItemId } : {}),
|
|
235
237
|
};
|
|
@@ -254,6 +256,7 @@ function eventState(event) {
|
|
|
254
256
|
if (!["connected", "working", "waiting", "blocked"].includes(phase)) return null;
|
|
255
257
|
return {
|
|
256
258
|
phase,
|
|
259
|
+
blockerCode: phase === "blocked" ? runtimeErrorCode({ code: event.code }) : undefined,
|
|
257
260
|
...(UUID.test(event.missionId ?? "") ? { missionId: event.missionId } : {}),
|
|
258
261
|
...(UUID.test(event.workItemId ?? "") ? { workItemId: event.workItemId } : {}),
|
|
259
262
|
};
|
|
@@ -311,6 +314,7 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
311
314
|
phase: status.phase,
|
|
312
315
|
message: LABELS[status.phase],
|
|
313
316
|
...(status.missionId ? { missionId: status.missionId } : {}),
|
|
317
|
+
...(status.blockerCode ? { blockerCode: status.blockerCode } : {}),
|
|
314
318
|
});
|
|
315
319
|
} catch {
|
|
316
320
|
persistenceFailed = true;
|
package/bin/agent-setup.mjs
CHANGED
|
@@ -12,10 +12,12 @@ import { installedClientInvocation, localClientEnvironment } from "./platform-cl
|
|
|
12
12
|
import { inspectLocalRepository } from "./repository.mjs";
|
|
13
13
|
import { Totem } from "./sdk/index.js";
|
|
14
14
|
import { detectInstalledProvider } from "./provider-detection.mjs";
|
|
15
|
+
import { RUNTIME_ERROR_CODES } from "./runtime-diagnostics.mjs";
|
|
15
16
|
|
|
16
17
|
const execute = promisify(execFile);
|
|
17
18
|
const ORIGIN = "https://totemlayer.com";
|
|
18
19
|
const KEY_LIMIT = 8192;
|
|
20
|
+
const IDENTITY_TIMEOUT_MS = 15000;
|
|
19
21
|
|
|
20
22
|
export const AGENT_SETUP_HELP = `Connect your agent once
|
|
21
23
|
|
|
@@ -195,7 +197,8 @@ function dependencies(options) {
|
|
|
195
197
|
stdin: process.stdin,
|
|
196
198
|
stdout: process.stdout,
|
|
197
199
|
stderr: process.stderr,
|
|
198
|
-
createClient: (config) =>
|
|
200
|
+
createClient: (config) =>
|
|
201
|
+
new Totem({ ...config, timeoutMs: IDENTITY_TIMEOUT_MS, maxRetries: 0 }),
|
|
199
202
|
fetch: globalThis.fetch,
|
|
200
203
|
inspectRepository: inspectLocalRepository,
|
|
201
204
|
installMcp: installTotemMcp,
|
|
@@ -224,20 +227,43 @@ function safeOutput(value, secret) {
|
|
|
224
227
|
}
|
|
225
228
|
|
|
226
229
|
function identityError(error) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
if (error?.status === 401 || error?.status === 403)
|
|
231
|
+
return [
|
|
232
|
+
"The connection key was rejected. For a saved profile, rerun setup with --replace to enter a new key, or use --replace --key-file with the replacement key.",
|
|
233
|
+
"CONNECTION_KEY_REJECTED",
|
|
234
|
+
];
|
|
235
|
+
if (["TIMEOUT", "CANCELED"].includes(error?.code))
|
|
236
|
+
return [
|
|
237
|
+
"Totem identity verification timed out. Check connectivity and retry status; this does not mean the key was rejected.",
|
|
238
|
+
"IDENTITY_TIMEOUT",
|
|
239
|
+
];
|
|
240
|
+
if (error?.code === "NETWORK_ERROR")
|
|
241
|
+
return [
|
|
242
|
+
"Totem identity verification could not reach the saved origin. Check connectivity, proxy and TLS settings.",
|
|
243
|
+
"IDENTITY_NETWORK_ERROR",
|
|
244
|
+
];
|
|
245
|
+
if (error?.status >= 500)
|
|
246
|
+
return [
|
|
247
|
+
"Totem identity verification encountered a server error. Retry status; this does not mean the key was rejected.",
|
|
248
|
+
"IDENTITY_SERVER_ERROR",
|
|
249
|
+
];
|
|
250
|
+
if (error?.code === "INVALID_RESPONSE")
|
|
251
|
+
return [
|
|
252
|
+
"The saved origin returned an invalid identity response. Check the origin and server availability.",
|
|
253
|
+
"IDENTITY_INVALID_RESPONSE",
|
|
254
|
+
];
|
|
255
|
+
return [
|
|
256
|
+
"Totem identity could not be verified. Check the saved origin and server availability.",
|
|
257
|
+
"IDENTITY_VERIFICATION_FAILED",
|
|
258
|
+
];
|
|
230
259
|
}
|
|
231
260
|
|
|
232
261
|
async function verifiedIdentity(client) {
|
|
233
262
|
let identity;
|
|
234
263
|
try {
|
|
235
|
-
identity = await client.agent({ signal: AbortSignal.timeout(
|
|
264
|
+
identity = await client.agent({ signal: AbortSignal.timeout(IDENTITY_TIMEOUT_MS) });
|
|
236
265
|
} catch (error) {
|
|
237
|
-
fail(
|
|
238
|
-
identityError(error),
|
|
239
|
-
error?.status === 401 || error?.status === 403 ? "CONNECTION_KEY_REJECTED" : undefined,
|
|
240
|
-
);
|
|
266
|
+
fail(...identityError(error));
|
|
241
267
|
}
|
|
242
268
|
if (
|
|
243
269
|
!identity ||
|
|
@@ -248,7 +274,10 @@ async function verifiedIdentity(client) {
|
|
|
248
274
|
!["SANDBOX", "PRODUCTION"].includes(identity.environment) ||
|
|
249
275
|
(identity.status && !["ACTIVE", "OFFLINE"].includes(identity.status))
|
|
250
276
|
)
|
|
251
|
-
fail(
|
|
277
|
+
fail(
|
|
278
|
+
"Totem did not return an active agent identity. Check this agent in Totem.",
|
|
279
|
+
"IDENTITY_INVALID_RESPONSE",
|
|
280
|
+
);
|
|
252
281
|
return identity;
|
|
253
282
|
}
|
|
254
283
|
|
|
@@ -600,6 +629,7 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
600
629
|
)
|
|
601
630
|
fail(
|
|
602
631
|
"The saved key no longer matches this profile's agent. Run setup with a separate profile or explicitly replace it.",
|
|
632
|
+
"AGENT_IDENTITY_MISMATCH",
|
|
603
633
|
);
|
|
604
634
|
checks.credential = {
|
|
605
635
|
state: "verified",
|
|
@@ -608,6 +638,9 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
608
638
|
} catch (error) {
|
|
609
639
|
checks.credential = {
|
|
610
640
|
state: "failed",
|
|
641
|
+
code: error?.setupSafe
|
|
642
|
+
? (error.code ?? "IDENTITY_VERIFICATION_FAILED")
|
|
643
|
+
: "IDENTITY_VERIFICATION_FAILED",
|
|
611
644
|
message: error?.setupSafe
|
|
612
645
|
? error.message
|
|
613
646
|
: "The saved agent identity could not be verified.",
|
|
@@ -688,6 +721,7 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
688
721
|
: "needs_attention";
|
|
689
722
|
checks.worker = {
|
|
690
723
|
state,
|
|
724
|
+
...(RUNTIME_ERROR_CODES.has(worker?.blockerCode) ? { code: worker.blockerCode } : {}),
|
|
691
725
|
...(phase ? { phase } : {}),
|
|
692
726
|
message:
|
|
693
727
|
state === "running"
|
|
@@ -697,7 +731,7 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
697
731
|
: phase === "stopping"
|
|
698
732
|
? "The worker is stopping and preserving progress. Wait for it to stop before changing setup or restarting."
|
|
699
733
|
: phase === "blocked"
|
|
700
|
-
? "The running worker has
|
|
734
|
+
? "The running worker has an execution blocker. Check its error code; starting another worker will not resolve an uncertain run."
|
|
701
735
|
: phase === "unresponsive"
|
|
702
736
|
? "The worker process is present but its status is stale. Inspect or stop it before starting another worker."
|
|
703
737
|
: "The local worker is not confirmed ready. Check it with totem start for this profile.",
|
|
@@ -11,13 +11,17 @@ export async function runConnectedAgentService(settings, dependencies = {}) {
|
|
|
11
11
|
: controller.signal;
|
|
12
12
|
const emit = settings.emit ?? (() => {});
|
|
13
13
|
const phases = { messages: "starting", missions: "starting" };
|
|
14
|
+
const blockers = {};
|
|
14
15
|
const report = (queue, event) => {
|
|
15
16
|
phases[queue] = event.type === "completed" ? "waiting" : event.type;
|
|
17
|
+
if (event.type === "blocked") blockers[queue] = { code: event.code, messageId: event.messageId };
|
|
18
|
+
else delete blockers[queue];
|
|
16
19
|
const values = Object.values(phases);
|
|
17
20
|
const type =
|
|
18
21
|
["reconnecting", "starting", "blocked", "working"].find((phase) => values.includes(phase)) ??
|
|
19
22
|
"waiting";
|
|
20
|
-
|
|
23
|
+
const blocked = type === "blocked" ? blockers.messages ?? blockers.missions : undefined;
|
|
24
|
+
emit({ ...event, type, ...(blocked ?? {}) });
|
|
21
25
|
};
|
|
22
26
|
const messages = async () => {
|
|
23
27
|
let attempts = 0;
|
|
@@ -36,7 +40,9 @@ export async function runConnectedAgentService(settings, dependencies = {}) {
|
|
|
36
40
|
attempts = 0;
|
|
37
41
|
report("messages", { type: "waiting" });
|
|
38
42
|
} else if (event.type === "runtime_error")
|
|
39
|
-
report("messages", { type: "blocked", code: event.error?.code });
|
|
43
|
+
report("messages", { type: "blocked", code: event.error?.code, messageId: event.messageId });
|
|
44
|
+
else if (event.type === "message_started")
|
|
45
|
+
report("messages", { type: "working", messageId: event.data?.messageId });
|
|
40
46
|
else if (["message_handled", "chat_completed"].includes(event.type))
|
|
41
47
|
report("messages", { type: "waiting" });
|
|
42
48
|
},
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { modelReply } from "./network-runtime.mjs";
|
|
4
|
+
import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
5
|
+
import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
|
|
6
|
+
|
|
7
|
+
export async function verifyExecutorReady(profile, env = process.env, options = {}) {
|
|
8
|
+
const version = options.version ?? JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
9
|
+
const file = join(profile.stateDirectory, "executor-readiness.json");
|
|
10
|
+
const binding = JSON.stringify([profile.agentId, profile.provider, profile.client, profile.model, profile.command, profile.commandArgs, profile.agentUrl, profile.qwenUrl, profile.ollamaUrl, profile.nimUrl]);
|
|
11
|
+
const previous = await privateRead(file).catch(error => {
|
|
12
|
+
if (error.code !== "ENOENT") throw error;
|
|
13
|
+
return null;
|
|
14
|
+
});
|
|
15
|
+
if (previous?.version === version && previous.binding === binding && previous.ready === true) return previous;
|
|
16
|
+
let response;
|
|
17
|
+
try { response = await (options.generate ?? modelReply)({
|
|
18
|
+
provider: profile.client ?? profile.provider,
|
|
19
|
+
model: profile.client && profile.provider === "qwen-local" ? "default" : profile.model,
|
|
20
|
+
origin: profile.ollamaUrl ?? "http://127.0.0.1:11434",
|
|
21
|
+
qwenUrl: profile.qwenUrl,
|
|
22
|
+
nimUrl: profile.nimUrl,
|
|
23
|
+
adapterSettings: { ...profile, environment: env },
|
|
24
|
+
messages: [{ role: "user", content: "This is a connection readiness check. Reply READY. No tools, file reads or external actions are authorized." }],
|
|
25
|
+
tools: [],
|
|
26
|
+
signal: options.signal ?? new AbortController().signal,
|
|
27
|
+
timeoutMs: 30000,
|
|
28
|
+
}); } catch (error) {
|
|
29
|
+
await privateWrite(file, { version, binding, ready: false, code: runtimeErrorCode(error), checkedAt: new Date().toISOString() });
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
if (typeof response?.content !== "string" || !response.content.trim())
|
|
33
|
+
throw Object.assign(new Error("The selected executor did not complete its readiness check."), { code: "INVALID_AGENT_RESULT" });
|
|
34
|
+
const record = { version, binding, ready: true, checkedAt: new Date().toISOString() };
|
|
35
|
+
await privateWrite(file, record);
|
|
36
|
+
return record;
|
|
37
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
|
|
3
|
+
// Polling is independent of generation. Unrelated conversations can make
|
|
4
|
+
// progress while one provider turn is slow; one conversation stays ordered.
|
|
5
|
+
export async function runInboxQueue({
|
|
6
|
+
messages,
|
|
7
|
+
handle,
|
|
8
|
+
eligible = async () => true,
|
|
9
|
+
signal: ownerSignal,
|
|
10
|
+
pollMs,
|
|
11
|
+
concurrency = 2,
|
|
12
|
+
shouldContinue = () => true,
|
|
13
|
+
once = false,
|
|
14
|
+
}) {
|
|
15
|
+
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 4)
|
|
16
|
+
throw new Error("Inbox concurrency must be between one and four.");
|
|
17
|
+
const stop = new AbortController();
|
|
18
|
+
const signal = AbortSignal.any([ownerSignal, stop.signal]);
|
|
19
|
+
const active = new Map();
|
|
20
|
+
const conversations = new Set();
|
|
21
|
+
const attempted = new Set();
|
|
22
|
+
let failure;
|
|
23
|
+
let pendingCount = 0;
|
|
24
|
+
try {
|
|
25
|
+
do {
|
|
26
|
+
const oldest = new Map();
|
|
27
|
+
for await (const message of messages(signal)) {
|
|
28
|
+
if (signal.aborted) break;
|
|
29
|
+
if (
|
|
30
|
+
active.has(message.id) ||
|
|
31
|
+
conversations.has(message.conversationId) ||
|
|
32
|
+
attempted.has(message.id)
|
|
33
|
+
)
|
|
34
|
+
continue;
|
|
35
|
+
if (!(await eligible(message))) continue;
|
|
36
|
+
// The server yields oldest first. Preserve that order within a chat.
|
|
37
|
+
if (!oldest.has(message.conversationId)) oldest.set(message.conversationId, message);
|
|
38
|
+
}
|
|
39
|
+
const pending = [...oldest.values()];
|
|
40
|
+
// Give both the backlog and recent conversations a slot, rather than
|
|
41
|
+
// draining yesterday's entire backlog before looking at today's inbox.
|
|
42
|
+
let recent = false;
|
|
43
|
+
while (pending.length && active.size < concurrency && !signal.aborted) {
|
|
44
|
+
const message = recent ? pending.pop() : pending.shift();
|
|
45
|
+
recent = !recent;
|
|
46
|
+
if (once) attempted.add(message.id);
|
|
47
|
+
conversations.add(message.conversationId);
|
|
48
|
+
const turn = Promise.resolve()
|
|
49
|
+
.then(() => handle(message, signal))
|
|
50
|
+
.catch((error) => {
|
|
51
|
+
failure ??= error;
|
|
52
|
+
stop.abort();
|
|
53
|
+
})
|
|
54
|
+
.finally(() => {
|
|
55
|
+
active.delete(message.id);
|
|
56
|
+
conversations.delete(message.conversationId);
|
|
57
|
+
});
|
|
58
|
+
active.set(message.id, turn);
|
|
59
|
+
}
|
|
60
|
+
pendingCount = pending.length;
|
|
61
|
+
if (!signal.aborted && (shouldContinue() || active.size || pendingCount))
|
|
62
|
+
await sleep(pollMs, undefined, { signal }).catch((error) => {
|
|
63
|
+
if (!signal.aborted) throw error;
|
|
64
|
+
});
|
|
65
|
+
} while (!signal.aborted && (shouldContinue() || active.size || pendingCount));
|
|
66
|
+
} finally {
|
|
67
|
+
stop.abort();
|
|
68
|
+
await Promise.allSettled(active.values());
|
|
69
|
+
}
|
|
70
|
+
if (failure) throw failure;
|
|
71
|
+
}
|
package/bin/installation.mjs
CHANGED
|
@@ -17,11 +17,25 @@ export async function activateConnection(profile, env = process.env, options = {
|
|
|
17
17
|
...(profile.agentTokenEnv ? { TOTEM_AGENT_TOKEN_ENV: profile.agentTokenEnv } : {}),
|
|
18
18
|
});
|
|
19
19
|
if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
|
|
20
|
+
const { verifyExecutorReady } = await import("./executor-readiness.mjs");
|
|
21
|
+
try {
|
|
22
|
+
await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return { state: "needs_attention", ...service, entry: runtime.cli,
|
|
25
|
+
code: error.code ?? "EXECUTOR_NOT_READY",
|
|
26
|
+
message: "Your connection is saved, but the selected agent could not complete its startup check. Run totem status to inspect the blocker." };
|
|
27
|
+
}
|
|
20
28
|
const { agentStartMain } = await import("./agent-runner.mjs");
|
|
21
|
-
await (options.start ?? agentStartMain)(
|
|
29
|
+
const started = await (options.start ?? agentStartMain)(
|
|
22
30
|
["--profile", profile.profileName, "--background", "--json"],
|
|
23
31
|
{ env, automatic: true, stdout: { write() {} } },
|
|
24
32
|
);
|
|
33
|
+
if (started !== 0)
|
|
34
|
+
return { state: "needs_attention", ...service, entry: runtime.cli, code: "WORKER_START_FAILED" };
|
|
35
|
+
const { readAgentRuntimeStatus } = await import("./agent-runner.mjs");
|
|
36
|
+
const state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
37
|
+
if (!state.running || !["connected", "working", "waiting"].includes(state.phase))
|
|
38
|
+
return { state: "needs_attention", ...service, entry: runtime.cli, code: state.blockerCode ?? "WORKER_NOT_READY", phase: state.phase };
|
|
25
39
|
return { state: "running", ...service, entry: runtime.cli };
|
|
26
40
|
}
|
|
27
41
|
|
|
@@ -108,25 +122,36 @@ export async function installationMain(args = [], options = {}) {
|
|
|
108
122
|
// An upgrade reuses private profiles and starts their existing connections.
|
|
109
123
|
const { opendir } = await import("node:fs/promises");
|
|
110
124
|
let saved = false;
|
|
125
|
+
const savedProfiles = [];
|
|
111
126
|
try {
|
|
112
127
|
const profiles = await opendir(join(runtime.root, "connections"));
|
|
113
128
|
for await (const entry of profiles)
|
|
114
129
|
if (
|
|
115
130
|
entry.isDirectory() &&
|
|
116
131
|
(!environment.TOTEM_AGENT_ID || entry.name === environment.TOTEM_AGENT_ID)
|
|
117
|
-
)
|
|
132
|
+
) {
|
|
118
133
|
saved = true;
|
|
134
|
+
savedProfiles.push(entry.name);
|
|
135
|
+
}
|
|
119
136
|
} catch (error) {
|
|
120
137
|
if (error.code !== "ENOENT") throw error;
|
|
121
138
|
}
|
|
122
139
|
if (saved && options.forceLogin !== true) {
|
|
123
|
-
const
|
|
140
|
+
const { readAgentProfile } = await import("./agent-profile.mjs");
|
|
141
|
+
const results = [];
|
|
142
|
+
for (const name of savedProfiles) {
|
|
143
|
+
const profile = await readAgentProfile(name, { environment });
|
|
144
|
+
results.push({ profile: name, ...(await (options.activateConnection ?? activateConnection)(profile, environment)) });
|
|
145
|
+
}
|
|
146
|
+
const ready = results.every(result => ["running", "paused"].includes(result.state));
|
|
147
|
+
const report = { installed: true, connection: "saved", automatic: true, ready, profiles: results, service };
|
|
124
148
|
stdout.write(
|
|
125
149
|
flags.json
|
|
126
150
|
? JSON.stringify(report) + "\n"
|
|
127
|
-
: "Totem's automatic listener
|
|
151
|
+
: ready ? "Totem's automatic listener reuses your saved connections; startup checks passed.\n"
|
|
152
|
+
: "Totem is installed, but an agent failed its startup check. Run totem status to inspect it.\n",
|
|
128
153
|
);
|
|
129
|
-
return 0;
|
|
154
|
+
return ready ? 0 : 1;
|
|
130
155
|
}
|
|
131
156
|
const connection = await (options.beginLogin ?? beginDeviceLogin)(runtime, environment, {
|
|
132
157
|
openBrowser: !flags["no-browser"] && environment.TOTEM_NO_BROWSER !== "1",
|
package/bin/model-adapter.mjs
CHANGED
|
@@ -4,6 +4,16 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { isAbsolute, join } from "node:path";
|
|
5
5
|
import { createCommandAdapter, createHttpAdapter } from "./worker-core/adapters.mjs";
|
|
6
6
|
import { installedClientInvocation, localClientEnvironment } from "./platform-client.mjs";
|
|
7
|
+
import { WorkerError } from "./worker-core/errors.mjs";
|
|
8
|
+
|
|
9
|
+
export function providerFailureCode(text) {
|
|
10
|
+
if (/unexpected argument|unknown option|unrecognized option|no such option|unknown flag/i.test(text)) return "AGENT_CLI_INCOMPATIBLE";
|
|
11
|
+
if (/not logged in|authentication required|invalid[_ ]api[_ ]key|unauthorized|refresh token.*(?:expired|reused)|sign in.*required/i.test(text)) return "AGENT_AUTH_REQUIRED";
|
|
12
|
+
if (/usage limit|rate limit|quota.*(?:exceeded|insufficient)|insufficient_quota|credit balance/i.test(text)) return "AGENT_QUOTA_EXCEEDED";
|
|
13
|
+
if (/sandbox.*(?:setup failed|not available|not configured|unsupported|error)|CreateProcessAsUser|error 1385/i.test(text)) return "AGENT_SANDBOX_UNAVAILABLE";
|
|
14
|
+
if (/model.*(?:not found|not supported|does not exist|not have access)|model_not_found/i.test(text)) return "AGENT_MODEL_UNAVAILABLE";
|
|
15
|
+
return "AGENT_EXIT_FAILED";
|
|
16
|
+
}
|
|
7
17
|
|
|
8
18
|
export const EXTERNAL_PROVIDERS = ["codex", "claude", "command", "http"];
|
|
9
19
|
export const isExternalModelProvider = (provider) => EXTERNAL_PROVIDERS.includes(provider);
|
|
@@ -88,7 +98,7 @@ export async function externalModelReply({
|
|
|
88
98
|
continue;
|
|
89
99
|
}
|
|
90
100
|
if (event.type === "turn.failed" || event.type === "error")
|
|
91
|
-
throw new
|
|
101
|
+
throw new WorkerError(providerFailureCode(event.error?.message ?? event.message ?? ""));
|
|
92
102
|
if (event.type === "item.completed" && event.item?.type === "agent_message")
|
|
93
103
|
final = event.item.text;
|
|
94
104
|
}
|
|
@@ -113,7 +123,7 @@ export async function externalModelReply({
|
|
|
113
123
|
];
|
|
114
124
|
parseResult = (output) => {
|
|
115
125
|
const result = JSON.parse(output);
|
|
116
|
-
if (result.is_error || result.subtype !== "success") throw new
|
|
126
|
+
if (result.is_error || result.subtype !== "success") throw new WorkerError(providerFailureCode(String(result.result ?? "")));
|
|
117
127
|
return {
|
|
118
128
|
status: "completed",
|
|
119
129
|
output: result.structured_output
|
|
@@ -153,6 +163,7 @@ export async function externalModelReply({
|
|
|
153
163
|
env,
|
|
154
164
|
timeoutMs,
|
|
155
165
|
parseResult,
|
|
166
|
+
...(["codex", "claude"].includes(provider) ? { classifyFailure: providerFailureCode } : {}),
|
|
156
167
|
});
|
|
157
168
|
}
|
|
158
169
|
const result = await adapter.run({ prompt, signal, runId: randomUUID() });
|
package/bin/network-runtime.mjs
CHANGED
|
@@ -4,7 +4,9 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { pause } from "./network.mjs";
|
|
6
6
|
import { localModelEndpoint } from "./local-model-provider.mjs";
|
|
7
|
+
import { runInboxQueue } from "./inbox-queue.mjs";
|
|
7
8
|
import { customerInspector, inspectNetworkMessage } from "./local-inspector.mjs";
|
|
9
|
+
import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
|
|
8
10
|
import {
|
|
9
11
|
externalModelReply,
|
|
10
12
|
isExternalModelProvider,
|
|
@@ -571,7 +573,12 @@ export async function runRuntime(settings) {
|
|
|
571
573
|
let inbox;
|
|
572
574
|
let humanDone = false;
|
|
573
575
|
let fatalError;
|
|
576
|
+
let diagnosticsSupported = false;
|
|
577
|
+
let diagnostics = { phase: "starting" };
|
|
578
|
+
const packageVersion = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
574
579
|
const report = (error, context = {}) => {
|
|
580
|
+
diagnostics = { phase: "blocked", errorCode: runtimeErrorCode(error),
|
|
581
|
+
...(typeof context.messageId === "string" ? { messageId: context.messageId } : {}) };
|
|
575
582
|
if (settings.once) process.exitCode = 1;
|
|
576
583
|
emit({
|
|
577
584
|
type: "runtime_error",
|
|
@@ -590,11 +597,16 @@ export async function runRuntime(settings) {
|
|
|
590
597
|
});
|
|
591
598
|
};
|
|
592
599
|
try {
|
|
593
|
-
const beat = async () =>
|
|
594
|
-
client.runtime.heartbeat(
|
|
595
|
-
{ label: `${settings.provider}:${settings.model}`.slice(0, 100)
|
|
600
|
+
const beat = async () => {
|
|
601
|
+
const result = await client.runtime.heartbeat(
|
|
602
|
+
{ label: `${settings.provider}:${settings.model}`.slice(0, 100),
|
|
603
|
+
...(diagnosticsSupported ? { diagnostics: { ...diagnostics, packageVersion, provider: settings.provider,
|
|
604
|
+
platform: ["darwin", "linux", "win32"].includes(process.platform) ? process.platform : "other" } } : {}) },
|
|
596
605
|
{ signal },
|
|
597
606
|
);
|
|
607
|
+
diagnosticsSupported = result?.diagnosticsSupported === true;
|
|
608
|
+
return result;
|
|
609
|
+
};
|
|
598
610
|
await beat();
|
|
599
611
|
heartbeat = (async () => {
|
|
600
612
|
while (!signal.aborted) {
|
|
@@ -622,90 +634,103 @@ export async function runRuntime(settings) {
|
|
|
622
634
|
inspection: settings.inspector.mode,
|
|
623
635
|
},
|
|
624
636
|
});
|
|
637
|
+
diagnostics = { phase: "waiting" };
|
|
625
638
|
// The A2A queue must keep moving while a human command waits on another agent.
|
|
626
639
|
// ask_agent reads durable conversation history, so acknowledgments cannot steal a reply.
|
|
627
|
-
inbox = (
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
for (const item of thread)
|
|
655
|
-
await inspectNetworkMessage(settings.inspector, client, item, { signal });
|
|
656
|
-
const history = boundedHistory(
|
|
657
|
-
thread.map((item) => ({
|
|
658
|
-
id: item.id,
|
|
659
|
-
actor: item.from === identity.address ? "agent" : "human",
|
|
660
|
-
text:
|
|
661
|
-
item.content.type === "text"
|
|
662
|
-
? item.content.text
|
|
663
|
-
: JSON.stringify(item.content.data),
|
|
664
|
-
})),
|
|
665
|
-
message.id,
|
|
666
|
-
);
|
|
667
|
-
const text = await generate({
|
|
668
|
-
client,
|
|
669
|
-
identity,
|
|
670
|
-
journal,
|
|
671
|
-
key: `message:${message.id}`,
|
|
672
|
-
text:
|
|
673
|
-
message.content.type === "text"
|
|
674
|
-
? message.content.text
|
|
675
|
-
: JSON.stringify(message.content.data),
|
|
676
|
-
history,
|
|
677
|
-
settings,
|
|
678
|
-
signal,
|
|
679
|
-
allowTools: false,
|
|
680
|
-
});
|
|
681
|
-
await settings.inspector.runAction(
|
|
682
|
-
{ id: message.id, text },
|
|
683
|
-
(inspected) =>
|
|
684
|
-
client.messages.reply(inspected.id, inspected.text, {
|
|
685
|
-
idempotencyKey: operationKey(identity.id, "reply", message.id),
|
|
686
|
-
...(continuation ? { metadata: continuation } : {}),
|
|
687
|
-
signal,
|
|
688
|
-
}),
|
|
640
|
+
inbox = runInboxQueue({
|
|
641
|
+
signal,
|
|
642
|
+
pollMs: settings.pollMs,
|
|
643
|
+
concurrency: settings.messageConcurrency ?? 2,
|
|
644
|
+
once: settings.once,
|
|
645
|
+
shouldContinue: () => !settings.once || !humanDone,
|
|
646
|
+
messages: (signal) =>
|
|
647
|
+
client.messages.iterate({ direction: "inbound", unread: true, limit: 100 }, { signal }),
|
|
648
|
+
eligible: async (message) => !(await journal.read(`message-blocker:${message.id}`)),
|
|
649
|
+
handle: async (message, signal) => {
|
|
650
|
+
try {
|
|
651
|
+
diagnostics = { phase: "working", messageId: message.id };
|
|
652
|
+
emit({ type: "message_started", data: { messageId: message.id } });
|
|
653
|
+
await inspectNetworkMessage(settings.inspector, client, message, { signal });
|
|
654
|
+
const continuation = autonomousReplyMetadata(message, settings, identity.address);
|
|
655
|
+
if (message.inReplyToId && !continuation) {
|
|
656
|
+
await client.messages.ack(message.id, { signal });
|
|
657
|
+
diagnostics = { phase: "waiting" };
|
|
658
|
+
emit({ type: "message_handled", data: { messageId: message.id } });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const thread = [];
|
|
662
|
+
let cursor;
|
|
663
|
+
for (let page = 0; page < 5; page++) {
|
|
664
|
+
const result = await client.conversations.messages(
|
|
665
|
+
message.conversationId,
|
|
666
|
+
{ limit: 100, ...(cursor ? { cursor } : {}) },
|
|
689
667
|
{ signal },
|
|
690
668
|
);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
data: { messageId: message.id, conversationId: message.conversationId },
|
|
695
|
-
});
|
|
696
|
-
} catch (error) {
|
|
697
|
-
if (!signal.aborted) report(error, { messageId: message.id });
|
|
698
|
-
if (error.status === 401) throw error;
|
|
699
|
-
if (error.status === 429 || error.status >= 500) throw error;
|
|
700
|
-
if (error.status >= 400 && error.status < 500)
|
|
701
|
-
await journal.save(`message-blocker:${message.id}`, {
|
|
702
|
-
code: error.code ?? "MESSAGE_BLOCKED",
|
|
703
|
-
});
|
|
669
|
+
thread.push(...result.data);
|
|
670
|
+
if (thread.some((item) => item.id === message.id) || !result.nextCursor) break;
|
|
671
|
+
cursor = result.nextCursor;
|
|
704
672
|
}
|
|
673
|
+
for (const item of thread)
|
|
674
|
+
await inspectNetworkMessage(settings.inspector, client, item, { signal });
|
|
675
|
+
const history = boundedHistory(
|
|
676
|
+
thread.map((item) => ({
|
|
677
|
+
id: item.id,
|
|
678
|
+
actor: item.from === identity.address ? "agent" : "human",
|
|
679
|
+
text:
|
|
680
|
+
item.content.type === "text"
|
|
681
|
+
? item.content.text
|
|
682
|
+
: JSON.stringify(item.content.data),
|
|
683
|
+
})),
|
|
684
|
+
message.id,
|
|
685
|
+
);
|
|
686
|
+
const text = await generate({
|
|
687
|
+
client,
|
|
688
|
+
identity,
|
|
689
|
+
journal,
|
|
690
|
+
key: `message:${message.id}`,
|
|
691
|
+
text:
|
|
692
|
+
message.content.type === "text"
|
|
693
|
+
? message.content.text
|
|
694
|
+
: JSON.stringify(message.content.data),
|
|
695
|
+
history,
|
|
696
|
+
settings: {
|
|
697
|
+
...settings,
|
|
698
|
+
modelTimeoutMs:
|
|
699
|
+
settings.messageTimeoutMs ?? Math.min(settings.modelTimeoutMs ?? 900000, 120000),
|
|
700
|
+
},
|
|
701
|
+
signal,
|
|
702
|
+
allowTools: false,
|
|
703
|
+
});
|
|
704
|
+
await settings.inspector.runAction(
|
|
705
|
+
{ id: message.id, text },
|
|
706
|
+
(inspected) =>
|
|
707
|
+
client.messages.reply(inspected.id, inspected.text, {
|
|
708
|
+
idempotencyKey: operationKey(identity.id, "reply", message.id),
|
|
709
|
+
...(continuation ? { metadata: continuation } : {}),
|
|
710
|
+
signal,
|
|
711
|
+
}),
|
|
712
|
+
{ signal },
|
|
713
|
+
);
|
|
714
|
+
await client.messages.ack(message.id, { signal });
|
|
715
|
+
diagnostics = { phase: "waiting" };
|
|
716
|
+
emit({
|
|
717
|
+
type: "message_handled",
|
|
718
|
+
data: { messageId: message.id, conversationId: message.conversationId },
|
|
719
|
+
});
|
|
720
|
+
} catch (error) {
|
|
721
|
+
if (!signal.aborted) report(error, { messageId: message.id });
|
|
722
|
+
if (error.status === 401) throw error;
|
|
723
|
+
if (error.status === 429 || error.status >= 500) throw error;
|
|
724
|
+
if (
|
|
725
|
+
(error.status >= 400 && error.status < 500) ||
|
|
726
|
+
error.code === "AGENT_OUTCOME_UNCERTAIN"
|
|
727
|
+
)
|
|
728
|
+
await journal.save(`message-blocker:${message.id}`, {
|
|
729
|
+
code: error.code ?? "MESSAGE_BLOCKED",
|
|
730
|
+
});
|
|
705
731
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
})().catch((error) => {
|
|
732
|
+
},
|
|
733
|
+
}).catch((error) => {
|
|
709
734
|
if (!signal.aborted) {
|
|
710
735
|
fatalError = error;
|
|
711
736
|
shutdown.abort();
|
package/bin/platform-client.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { join, dirname, isAbsolute, extname } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
|
|
3
4
|
const INHERITED_ENVIRONMENT = new Set(
|
|
4
5
|
[
|
|
@@ -71,9 +72,36 @@ export function powershellInvocation(script, environment = process.env) {
|
|
|
71
72
|
// base64-encoded JSON argument array is interpolated into the script, so a
|
|
72
73
|
// Windows path can never become shell syntax.
|
|
73
74
|
export function installedClientInvocation(command, args, environment = process.env) {
|
|
75
|
+
// Native executables inherit the pipe directly. For npm clients, launch the
|
|
76
|
+
// installed JavaScript entrypoint with Node rather than a .cmd/PowerShell
|
|
77
|
+
// pipeline, which can leave stdin open indefinitely on Windows.
|
|
78
|
+
const clientPackage = {
|
|
79
|
+
codex: ["@openai", "codex", "bin", "codex.js"],
|
|
80
|
+
claude: ["@anthropic-ai", "claude-code", "cli.js"],
|
|
81
|
+
}[command];
|
|
82
|
+
const pathValue =
|
|
83
|
+
environment.PATH ??
|
|
84
|
+
environment.Path ??
|
|
85
|
+
Object.entries(environment).find(([key]) => key.toUpperCase() === "PATH")?.[1] ??
|
|
86
|
+
"";
|
|
87
|
+
const candidates = isAbsolute(command)
|
|
88
|
+
? [command]
|
|
89
|
+
: pathValue
|
|
90
|
+
.split(";")
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.flatMap((dir) => [join(dir, `${command}.exe`), join(dir, `${command}.cmd`)]);
|
|
93
|
+
for (const path of candidates) {
|
|
94
|
+
if (!existsSync(path)) continue;
|
|
95
|
+
if (extname(path).toLowerCase() === ".exe") return { command: path, args };
|
|
96
|
+
if (clientPackage) {
|
|
97
|
+
const entry = join(dirname(path), "node_modules", ...clientPackage);
|
|
98
|
+
if (existsSync(entry)) return { command: process.execPath, args: [entry, ...args] };
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
74
102
|
const payload = Buffer.from(JSON.stringify([command, ...args]), "utf8").toString("base64");
|
|
75
103
|
return powershellInvocation(
|
|
76
|
-
`$ErrorActionPreference = 'Stop'\n$items = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payload}')) | ConvertFrom-Json\n$client = [string]$items[0]\n$clientArgs = @($items | Select-Object -Skip 1)\n& $client @clientArgs\nif ($null -eq $LASTEXITCODE) { exit 1 }\nexit $LASTEXITCODE`,
|
|
104
|
+
`$ErrorActionPreference = 'Stop'\n$items = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payload}')) | ConvertFrom-Json\n$client = [string]$items[0]\n$clientArgs = @($items | Select-Object -Skip 1)\n$OutputEncoding = [Text.UTF8Encoding]::new($false)\n[Console]::OutputEncoding = $OutputEncoding\n$prompt = [Console]::In.ReadToEnd()\n$prompt | & $client @clientArgs\nif ($null -eq $LASTEXITCODE) { exit 1 }\nexit $LASTEXITCODE`,
|
|
77
105
|
environment,
|
|
78
106
|
);
|
|
79
107
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const RUNTIME_ERROR_CODES = new Set([
|
|
2
|
+
"AGENT_START_FAILED", "AGENT_EXIT_FAILED", "AGENT_REPORTED_FAILURE", "INVALID_AGENT_RESULT",
|
|
3
|
+
"AGENT_CLI_INCOMPATIBLE", "AGENT_AUTH_REQUIRED", "AGENT_QUOTA_EXCEEDED", "AGENT_SANDBOX_UNAVAILABLE", "AGENT_MODEL_UNAVAILABLE",
|
|
4
|
+
"AGENT_OUTCOME_UNCERTAIN", "RUN_TIMEOUT", "RUN_CANCELLED", "OUTPUT_LIMIT_EXCEEDED",
|
|
5
|
+
"UNKNOWN_HTTP_OUTCOME", "HTTP_AGENT_FAILED", "INVALID_ADAPTER_CONFIG", "INVALID_ADAPTER_ENV",
|
|
6
|
+
"INSPECTION_BLOCKED", "INSPECTION_UNAVAILABLE", "INSPECTION_UNSUPPORTED_FILE",
|
|
7
|
+
"AGENT_REPLY_TIMEOUT", "INVALID_ASK", "TOOL_NOT_ALLOWED", "INVALID_PEER_REPLY",
|
|
8
|
+
"NETWORK_BUSY", "NETWORK_ERROR", "TIMEOUT", "CANCELED", "INVALID_NETWORK_CREDENTIAL",
|
|
9
|
+
"AGENT_IDENTITY_MISMATCH", "MODEL_ERROR", "MAX_STEPS", "INTERNAL_ERROR",
|
|
10
|
+
]);
|
|
11
|
+
export function runtimeErrorCode(error) {
|
|
12
|
+
return RUNTIME_ERROR_CODES.has(error?.code) ? error.code : "INTERNAL_ERROR";
|
|
13
|
+
}
|
package/bin/totem.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import { createReadStream } from "node:fs";
|
|
5
5
|
import { stripVTControlCharacters } from "node:util";
|
|
6
6
|
|
|
7
|
-
const CLI_VERSION = "0.1.
|
|
7
|
+
const CLI_VERSION = "0.1.15";
|
|
8
8
|
const DEFAULT_BASE_URL = "https://totemlayer.com";
|
|
9
9
|
const API_KEY_PATTERN = /^at_(?:live|test)_[A-Za-z0-9_-]{20,}$/;
|
|
10
10
|
const ACTION_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/;
|
|
@@ -51,7 +51,8 @@ export function createCommandAdapter(config) {
|
|
|
51
51
|
!Array.isArray(config.args ?? []) ||
|
|
52
52
|
!(config.args ?? []).every((arg) => typeof arg === "string" && !arg.includes("\0")) ||
|
|
53
53
|
typeof config.cwd !== "string" ||
|
|
54
|
-
!isAbsolute(config.cwd)
|
|
54
|
+
!isAbsolute(config.cwd) ||
|
|
55
|
+
(config.classifyFailure !== undefined && typeof config.classifyFailure !== "function")
|
|
55
56
|
) {
|
|
56
57
|
throw new WorkerError("INVALID_ADAPTER_CONFIG");
|
|
57
58
|
}
|
|
@@ -76,13 +77,25 @@ export function createCommandAdapter(config) {
|
|
|
76
77
|
let child;
|
|
77
78
|
let failure;
|
|
78
79
|
let output = "";
|
|
80
|
+
let diagnosticOutput = "";
|
|
79
81
|
let bytes = 0;
|
|
80
82
|
let killTimer;
|
|
81
83
|
// Also remove surviving descendants if the parent exits before escalation.
|
|
82
84
|
const kill = (kind) => {
|
|
83
85
|
try {
|
|
84
86
|
if (process.platform !== "win32" && child?.pid) process.kill(-child.pid, kind);
|
|
85
|
-
else child?.
|
|
87
|
+
else if (child?.pid) {
|
|
88
|
+
// A Windows shim can outlive its parent and keep stdio open.
|
|
89
|
+
const killer = spawn("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], {
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
stdio: "ignore",
|
|
92
|
+
});
|
|
93
|
+
killer.on("error", () => {
|
|
94
|
+
try {
|
|
95
|
+
child.kill(kind);
|
|
96
|
+
} catch {}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
86
99
|
} catch {
|
|
87
100
|
/* The process may already have exited. */
|
|
88
101
|
}
|
|
@@ -122,6 +135,7 @@ export function createCommandAdapter(config) {
|
|
|
122
135
|
child.stderr.on("data", (chunk) => {
|
|
123
136
|
bytes += chunk.length;
|
|
124
137
|
if (bytes > maxOutputBytes) stop("OUTPUT_LIMIT_EXCEEDED");
|
|
138
|
+
else if (config.classifyFailure) diagnosticOutput += chunk.toString("utf8");
|
|
125
139
|
});
|
|
126
140
|
child.stdin.on("error", () => {
|
|
127
141
|
/* A failed child can close stdin early. */
|
|
@@ -137,7 +151,15 @@ export function createCommandAdapter(config) {
|
|
|
137
151
|
kill("SIGKILL");
|
|
138
152
|
return reject(failure);
|
|
139
153
|
}
|
|
140
|
-
if (code !== 0)
|
|
154
|
+
if (code !== 0) {
|
|
155
|
+
try {
|
|
156
|
+
return reject(
|
|
157
|
+
new WorkerError(config.classifyFailure?.(diagnosticOutput) ?? "AGENT_EXIT_FAILED"),
|
|
158
|
+
);
|
|
159
|
+
} catch {
|
|
160
|
+
return reject(new WorkerError("AGENT_EXIT_FAILED"));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
141
163
|
try {
|
|
142
164
|
resolve(validateResult(parseResult(output)));
|
|
143
165
|
} catch (error) {
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
const SAFE_CODES = new Set([
|
|
2
|
-
"INSPECTION_BLOCKED",
|
|
3
|
-
"INSPECTION_UNAVAILABLE",
|
|
4
|
-
"INSPECTION_LIMIT",
|
|
5
|
-
"INSPECTION_UNSUPPORTED_FILE",
|
|
6
2
|
"INTERNAL_ERROR",
|
|
7
3
|
"INVALID_AGENT_RESULT",
|
|
8
4
|
"INVALID_ADAPTER_ENV",
|
|
@@ -30,6 +26,7 @@ const SAFE_CODES = new Set([
|
|
|
30
26
|
"INVALID_PROMPT",
|
|
31
27
|
"AGENT_START_FAILED",
|
|
32
28
|
"AGENT_EXIT_FAILED",
|
|
29
|
+
"AGENT_CLI_INCOMPATIBLE", "AGENT_AUTH_REQUIRED", "AGENT_QUOTA_EXCEEDED", "AGENT_SANDBOX_UNAVAILABLE", "AGENT_MODEL_UNAVAILABLE",
|
|
33
30
|
"AGENT_REPORTED_FAILURE",
|
|
34
31
|
"UNKNOWN_HTTP_OUTCOME",
|
|
35
32
|
"HTTP_AGENT_FAILED",
|
package/package.json
CHANGED
package/scripts/prepare.mjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { mkdir, readdir, copyFile, rm } from "node:fs/promises";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
// Build from the sibling source every time a distributable is packed.
|
|
6
|
-
const sdk = fileURLToPath(new URL("../../sdk/", import.meta.url));
|
|
7
|
-
const destination = new URL("../bin/sdk/", import.meta.url);
|
|
8
|
-
execFileSync(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build", "--prefix", sdk], {
|
|
9
|
-
stdio: "inherit",
|
|
10
|
-
});
|
|
11
|
-
await mkdir(destination, { recursive: true });
|
|
12
|
-
// The CLI has no encrypted endpoint/key-store configuration yet. Keep the optional
|
|
13
|
-
// HPKE entrypoints in the separately installed SDK, not a client without that dependency.
|
|
14
|
-
const encryptedOnly = new Set(["e2ee-wire.js", "e2ee.js", "encrypted-client.js", "encrypted.js"]);
|
|
15
|
-
for (const name of encryptedOnly) await rm(new URL(name, destination), { force: true });
|
|
16
|
-
for (const name of await readdir(new URL("../../sdk/dist/", import.meta.url))) {
|
|
17
|
-
if (name.endsWith(".js") && !encryptedOnly.has(name))
|
|
18
|
-
await copyFile(new URL(`../../sdk/dist/${name}`, import.meta.url), new URL(name, destination));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const workerCore = new URL("../bin/worker-core/", import.meta.url);
|
|
22
|
-
await mkdir(workerCore, { recursive: true });
|
|
23
|
-
for (const name of ["adapters.mjs", "errors.mjs"])
|
|
24
|
-
await copyFile(new URL(`../../agent-worker/${name}`, import.meta.url), new URL(name, workerCore));
|