totemlayer 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -39
- 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 +67 -0
- package/bin/inbox-queue.mjs +23 -10
- package/bin/installation.mjs +57 -5
- package/bin/mission-infrastructure.mjs +0 -49
- package/bin/mission-worker.mjs +11 -129
- package/bin/model-adapter.mjs +35 -2
- package/bin/network-retry.mjs +26 -0
- package/bin/network-runtime.mjs +89 -19
- package/bin/platform-client.mjs +30 -2
- package/bin/runtime-diagnostics.mjs +38 -0
- package/bin/sdk/index.js +11 -40
- package/bin/totem.mjs +1 -1
- package/bin/worker-core/adapters.mjs +25 -3
- package/bin/worker-core/errors.mjs +5 -4
- package/package.json +1 -1
- package/scripts/prepare.mjs +20 -0
package/bin/network-runtime.mjs
CHANGED
|
@@ -5,7 +5,9 @@ import { join } from "node:path";
|
|
|
5
5
|
import { pause } from "./network.mjs";
|
|
6
6
|
import { localModelEndpoint } from "./local-model-provider.mjs";
|
|
7
7
|
import { runInboxQueue } from "./inbox-queue.mjs";
|
|
8
|
+
import { retryNetwork, transientNetworkError } from "./network-retry.mjs";
|
|
8
9
|
import { customerInspector, inspectNetworkMessage } from "./local-inspector.mjs";
|
|
10
|
+
import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
|
|
9
11
|
import {
|
|
10
12
|
externalModelReply,
|
|
11
13
|
isExternalModelProvider,
|
|
@@ -405,23 +407,38 @@ async function generate({
|
|
|
405
407
|
new Error(
|
|
406
408
|
"A previous provider turn has an uncertain outcome. Inspect its private journal before retrying.",
|
|
407
409
|
),
|
|
408
|
-
{
|
|
410
|
+
{
|
|
411
|
+
code: state.externalFailure ?? "AGENT_OUTCOME_UNCERTAIN",
|
|
412
|
+
externalOutcomeUncertain: true,
|
|
413
|
+
},
|
|
409
414
|
);
|
|
410
415
|
state.externalPending = true;
|
|
411
416
|
await journal.save(key, state);
|
|
412
417
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
418
|
+
let response;
|
|
419
|
+
try {
|
|
420
|
+
response = await modelReply({
|
|
421
|
+
origin: settings.origin,
|
|
422
|
+
provider: settings.provider,
|
|
423
|
+
qwenUrl: settings.qwenUrl,
|
|
424
|
+
nimUrl: settings.nimUrl,
|
|
425
|
+
model: settings.model,
|
|
426
|
+
messages: state.messages,
|
|
427
|
+
tools,
|
|
428
|
+
signal,
|
|
429
|
+
timeoutMs: settings.modelTimeoutMs,
|
|
430
|
+
adapterSettings: settings.adapterSettings,
|
|
431
|
+
});
|
|
432
|
+
} catch (error) {
|
|
433
|
+
// Preserve a fixed diagnostic instead of hiding the original failure after restart.
|
|
434
|
+
// Keep externalPending: arbitrary external providers may have uncertain side effects.
|
|
435
|
+
if (isExternalModelProvider(settings.provider)) {
|
|
436
|
+
error.externalOutcomeUncertain = true;
|
|
437
|
+
state.externalFailure = runtimeErrorCode(error);
|
|
438
|
+
await journal.save(key, state);
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
425
442
|
await settings.inspector.assertAllowed("output", response, { task: text, signal });
|
|
426
443
|
delete state.externalPending;
|
|
427
444
|
state.messages.push(response);
|
|
@@ -572,7 +589,17 @@ export async function runRuntime(settings) {
|
|
|
572
589
|
let inbox;
|
|
573
590
|
let humanDone = false;
|
|
574
591
|
let fatalError;
|
|
592
|
+
let diagnosticsSupported = false;
|
|
593
|
+
let diagnostics = { phase: "starting" };
|
|
594
|
+
const packageVersion = JSON.parse(
|
|
595
|
+
await readFile(new URL("../package.json", import.meta.url), "utf8"),
|
|
596
|
+
).version;
|
|
575
597
|
const report = (error, context = {}) => {
|
|
598
|
+
diagnostics = {
|
|
599
|
+
phase: "blocked",
|
|
600
|
+
errorCode: runtimeErrorCode(error),
|
|
601
|
+
...(typeof context.messageId === "string" ? { messageId: context.messageId } : {}),
|
|
602
|
+
};
|
|
576
603
|
if (settings.once) process.exitCode = 1;
|
|
577
604
|
emit({
|
|
578
605
|
type: "runtime_error",
|
|
@@ -591,11 +618,28 @@ export async function runRuntime(settings) {
|
|
|
591
618
|
});
|
|
592
619
|
};
|
|
593
620
|
try {
|
|
594
|
-
const beat = async () =>
|
|
595
|
-
client.runtime.heartbeat(
|
|
596
|
-
{
|
|
621
|
+
const beat = async () => {
|
|
622
|
+
const result = await client.runtime.heartbeat(
|
|
623
|
+
{
|
|
624
|
+
label: `${settings.provider}:${settings.model}`.slice(0, 100),
|
|
625
|
+
...(diagnosticsSupported
|
|
626
|
+
? {
|
|
627
|
+
diagnostics: {
|
|
628
|
+
...diagnostics,
|
|
629
|
+
packageVersion,
|
|
630
|
+
provider: settings.provider,
|
|
631
|
+
platform: ["darwin", "linux", "win32"].includes(process.platform)
|
|
632
|
+
? process.platform
|
|
633
|
+
: "other",
|
|
634
|
+
},
|
|
635
|
+
}
|
|
636
|
+
: {}),
|
|
637
|
+
},
|
|
597
638
|
{ signal },
|
|
598
639
|
);
|
|
640
|
+
diagnosticsSupported = result?.diagnosticsSupported === true;
|
|
641
|
+
return result;
|
|
642
|
+
};
|
|
599
643
|
await beat();
|
|
600
644
|
heartbeat = (async () => {
|
|
601
645
|
while (!signal.aborted) {
|
|
@@ -623,6 +667,7 @@ export async function runRuntime(settings) {
|
|
|
623
667
|
inspection: settings.inspector.mode,
|
|
624
668
|
},
|
|
625
669
|
});
|
|
670
|
+
diagnostics = { phase: "waiting" };
|
|
626
671
|
// The A2A queue must keep moving while a human command waits on another agent.
|
|
627
672
|
// ask_agent reads durable conversation history, so acknowledgments cannot steal a reply.
|
|
628
673
|
inbox = runInboxQueue({
|
|
@@ -630,16 +675,21 @@ export async function runRuntime(settings) {
|
|
|
630
675
|
pollMs: settings.pollMs,
|
|
631
676
|
concurrency: settings.messageConcurrency ?? 2,
|
|
632
677
|
once: settings.once,
|
|
678
|
+
onPollError: (error) => report(error),
|
|
633
679
|
shouldContinue: () => !settings.once || !humanDone,
|
|
634
680
|
messages: (signal) =>
|
|
635
681
|
client.messages.iterate({ direction: "inbound", unread: true, limit: 100 }, { signal }),
|
|
636
682
|
eligible: async (message) => !(await journal.read(`message-blocker:${message.id}`)),
|
|
637
683
|
handle: async (message, signal) => {
|
|
638
684
|
try {
|
|
685
|
+
diagnostics = { phase: "working", messageId: message.id };
|
|
686
|
+
emit({ type: "message_started", data: { messageId: message.id } });
|
|
639
687
|
await inspectNetworkMessage(settings.inspector, client, message, { signal });
|
|
640
688
|
const continuation = autonomousReplyMetadata(message, settings, identity.address);
|
|
641
689
|
if (message.inReplyToId && !continuation) {
|
|
642
690
|
await client.messages.ack(message.id, { signal });
|
|
691
|
+
diagnostics = { phase: "waiting" };
|
|
692
|
+
emit({ type: "message_handled", data: { messageId: message.id } });
|
|
643
693
|
return;
|
|
644
694
|
}
|
|
645
695
|
const thread = [];
|
|
@@ -696,6 +746,7 @@ export async function runRuntime(settings) {
|
|
|
696
746
|
{ signal },
|
|
697
747
|
);
|
|
698
748
|
await client.messages.ack(message.id, { signal });
|
|
749
|
+
diagnostics = { phase: "waiting" };
|
|
699
750
|
emit({
|
|
700
751
|
type: "message_handled",
|
|
701
752
|
data: { messageId: message.id, conversationId: message.conversationId },
|
|
@@ -703,10 +754,12 @@ export async function runRuntime(settings) {
|
|
|
703
754
|
} catch (error) {
|
|
704
755
|
if (!signal.aborted) report(error, { messageId: message.id });
|
|
705
756
|
if (error.status === 401) throw error;
|
|
706
|
-
|
|
757
|
+
// Retry this durable message on the next poll without stopping other turns.
|
|
758
|
+
if (transientNetworkError(error)) return;
|
|
707
759
|
if (
|
|
708
760
|
(error.status >= 400 && error.status < 500) ||
|
|
709
|
-
error.code === "AGENT_OUTCOME_UNCERTAIN"
|
|
761
|
+
error.code === "AGENT_OUTCOME_UNCERTAIN" ||
|
|
762
|
+
error.externalOutcomeUncertain
|
|
710
763
|
)
|
|
711
764
|
await journal.save(`message-blocker:${message.id}`, {
|
|
712
765
|
code: error.code ?? "MESSAGE_BLOCKED",
|
|
@@ -720,7 +773,20 @@ export async function runRuntime(settings) {
|
|
|
720
773
|
}
|
|
721
774
|
});
|
|
722
775
|
do {
|
|
723
|
-
|
|
776
|
+
let commands;
|
|
777
|
+
try {
|
|
778
|
+
commands = await retryNetwork(() => client.chatCommands.list({ limit: 20 }, { signal }), {
|
|
779
|
+
signal,
|
|
780
|
+
delayMs: settings.pollMs,
|
|
781
|
+
onRetry: report,
|
|
782
|
+
});
|
|
783
|
+
} catch (error) {
|
|
784
|
+
if (signal.aborted) break;
|
|
785
|
+
if (!transientNetworkError(error)) throw error;
|
|
786
|
+
report(error);
|
|
787
|
+
await pause(Math.max(settings.pollMs, 100), signal);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
724
790
|
for (const command of commands.data) {
|
|
725
791
|
if (signal.aborted) break;
|
|
726
792
|
let claim;
|
|
@@ -728,6 +794,10 @@ export async function runRuntime(settings) {
|
|
|
728
794
|
claim = await client.chatCommands.claim(command.id, { signal });
|
|
729
795
|
} catch (error) {
|
|
730
796
|
if (error.status === 409) continue;
|
|
797
|
+
if (transientNetworkError(error)) {
|
|
798
|
+
report(error);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
731
801
|
throw error;
|
|
732
802
|
}
|
|
733
803
|
const leaseStop = new AbortController();
|
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,38 @@
|
|
|
1
|
+
export const RUNTIME_ERROR_CODES = new Set([
|
|
2
|
+
"AGENT_START_FAILED",
|
|
3
|
+
"AGENT_EXIT_FAILED",
|
|
4
|
+
"AGENT_REPORTED_FAILURE",
|
|
5
|
+
"INVALID_AGENT_RESULT",
|
|
6
|
+
"AGENT_CLI_INCOMPATIBLE",
|
|
7
|
+
"AGENT_AUTH_REQUIRED",
|
|
8
|
+
"AGENT_QUOTA_EXCEEDED",
|
|
9
|
+
"AGENT_SANDBOX_UNAVAILABLE",
|
|
10
|
+
"AGENT_MODEL_UNAVAILABLE",
|
|
11
|
+
"AGENT_OUTCOME_UNCERTAIN",
|
|
12
|
+
"RUN_TIMEOUT",
|
|
13
|
+
"RUN_CANCELLED",
|
|
14
|
+
"OUTPUT_LIMIT_EXCEEDED",
|
|
15
|
+
"UNKNOWN_HTTP_OUTCOME",
|
|
16
|
+
"HTTP_AGENT_FAILED",
|
|
17
|
+
"INVALID_ADAPTER_CONFIG",
|
|
18
|
+
"INVALID_ADAPTER_ENV",
|
|
19
|
+
"INSPECTION_BLOCKED",
|
|
20
|
+
"INSPECTION_UNAVAILABLE",
|
|
21
|
+
"INSPECTION_UNSUPPORTED_FILE",
|
|
22
|
+
"AGENT_REPLY_TIMEOUT",
|
|
23
|
+
"INVALID_ASK",
|
|
24
|
+
"TOOL_NOT_ALLOWED",
|
|
25
|
+
"INVALID_PEER_REPLY",
|
|
26
|
+
"NETWORK_BUSY",
|
|
27
|
+
"NETWORK_ERROR",
|
|
28
|
+
"TIMEOUT",
|
|
29
|
+
"CANCELED",
|
|
30
|
+
"INVALID_NETWORK_CREDENTIAL",
|
|
31
|
+
"AGENT_IDENTITY_MISMATCH",
|
|
32
|
+
"MODEL_ERROR",
|
|
33
|
+
"MAX_STEPS",
|
|
34
|
+
"INTERNAL_ERROR",
|
|
35
|
+
]);
|
|
36
|
+
export function runtimeErrorCode(error) {
|
|
37
|
+
return RUNTIME_ERROR_CODES.has(error?.code) ? error.code : "INTERNAL_ERROR";
|
|
38
|
+
}
|
package/bin/sdk/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { TotemError } from "./errors.js";
|
|
2
|
-
import { createLocalInspector, InspectionError } from "./inspector.js";
|
|
3
2
|
import { idempotencyKey, sleep, Transport } from "./transport.js";
|
|
4
3
|
import { assertInputFields, assistantCheckOptions, signalOnlyOptions } from "./request-options.js";
|
|
5
4
|
export * from "./types.js";
|
|
@@ -113,11 +112,6 @@ export class Totem {
|
|
|
113
112
|
},
|
|
114
113
|
idempotencyKey: options.idempotencyKey ?? `sdk:reply:${id}`,
|
|
115
114
|
})).data,
|
|
116
|
-
replyContent: async (id, content, options = {}) => (await this.#transport.request("POST", `/messages/${encodeURIComponent(id)}/reply`, {
|
|
117
|
-
...options,
|
|
118
|
-
body: { content },
|
|
119
|
-
idempotencyKey: options.idempotencyKey ?? idempotencyKey(),
|
|
120
|
-
})).data,
|
|
121
115
|
ack: async (id, options) => (await this.#transport.request("POST", `/messages/${encodeURIComponent(id)}/ack`, {
|
|
122
116
|
...options,
|
|
123
117
|
body: {},
|
|
@@ -323,8 +317,6 @@ export class ConnectedAgent {
|
|
|
323
317
|
if (!Number.isFinite(interval) || interval < 10)
|
|
324
318
|
throw new TotemError("pollIntervalMs must be at least 10.", { code: "INVALID_CONFIG" });
|
|
325
319
|
const controller = new AbortController();
|
|
326
|
-
const inspector = options.inspector ?? createLocalInspector();
|
|
327
|
-
const blocked = new Set();
|
|
328
320
|
const report = (cause) => {
|
|
329
321
|
this.#lastError = cause instanceof Error ? cause : new Error(String(cause));
|
|
330
322
|
try {
|
|
@@ -340,42 +332,22 @@ export class ConnectedAgent {
|
|
|
340
332
|
for await (const message of this.client.messages.iterate({ direction: "inbound", unread: true, limit: 100 }, { signal: controller.signal })) {
|
|
341
333
|
if (controller.signal.aborted)
|
|
342
334
|
break;
|
|
343
|
-
|
|
344
|
-
|
|
335
|
+
const incoming = {
|
|
336
|
+
...message,
|
|
337
|
+
text: message.content.type === "text"
|
|
338
|
+
? message.content.text
|
|
339
|
+
: JSON.stringify(message.content.data),
|
|
340
|
+
reply: (text, writeOptions = {}) => this.client.messages.reply(message.id, text, {
|
|
341
|
+
...writeOptions,
|
|
342
|
+
idempotencyKey: writeOptions.idempotencyKey ?? `reply:${message.id}:0`,
|
|
343
|
+
}),
|
|
344
|
+
ack: () => this.client.messages.ack(message.id),
|
|
345
|
+
};
|
|
345
346
|
try {
|
|
346
|
-
if (message.content.type === "encrypted")
|
|
347
|
-
throw new TotemError("Use E2eeTotem and pinned endpoint keys to process encrypted messages.", { code: "E2EE_CLIENT_REQUIRED" });
|
|
348
|
-
await inspector.assertAllowed("input", { content: message.content, metadata: message.metadata ?? {} }, { signal: controller.signal });
|
|
349
|
-
const files = message.attachments ?? [];
|
|
350
|
-
const ids = new Set(files.map((file) => file.id));
|
|
351
|
-
if (message.content.type === "text" &&
|
|
352
|
-
message.content.attachments?.some((id) => !ids.has(id)))
|
|
353
|
-
throw new InspectionError("INSPECTION_UNSUPPORTED_FILE");
|
|
354
|
-
for (const file of files) {
|
|
355
|
-
const downloaded = await this.client.attachments.download(file.id, {
|
|
356
|
-
signal: controller.signal,
|
|
357
|
-
});
|
|
358
|
-
await inspector.inspectFile(file.name, file.mediaType, downloaded.bytes, {
|
|
359
|
-
signal: controller.signal,
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
const incoming = {
|
|
363
|
-
...message,
|
|
364
|
-
text: message.content.type === "text"
|
|
365
|
-
? message.content.text
|
|
366
|
-
: JSON.stringify(message.content.data),
|
|
367
|
-
reply: (text, writeOptions = {}) => inspector.runAction({ id: message.id, text }, (input) => this.client.messages.reply(input.id, input.text, {
|
|
368
|
-
...writeOptions,
|
|
369
|
-
idempotencyKey: writeOptions.idempotencyKey ?? `reply:${message.id}:0`,
|
|
370
|
-
}), { signal: controller.signal }),
|
|
371
|
-
ack: () => this.client.messages.ack(message.id),
|
|
372
|
-
};
|
|
373
347
|
await handler(incoming);
|
|
374
348
|
await incoming.ack();
|
|
375
349
|
}
|
|
376
350
|
catch (error) {
|
|
377
|
-
if (error instanceof InspectionError && error.code !== "INSPECTION_UNAVAILABLE")
|
|
378
|
-
blocked.add(message.id);
|
|
379
351
|
report(error);
|
|
380
352
|
}
|
|
381
353
|
}
|
|
@@ -413,4 +385,3 @@ export class ConnectedAgent {
|
|
|
413
385
|
/** Compatibility name for callers who prefer `new TotemAgent({ address, apiKey })`. */
|
|
414
386
|
export class TotemAgent extends Totem {
|
|
415
387
|
}
|
|
416
|
-
export { createLocalInspector, InspectionError };
|
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.13";
|
|
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,11 @@ const SAFE_CODES = new Set([
|
|
|
30
26
|
"INVALID_PROMPT",
|
|
31
27
|
"AGENT_START_FAILED",
|
|
32
28
|
"AGENT_EXIT_FAILED",
|
|
29
|
+
"AGENT_CLI_INCOMPATIBLE",
|
|
30
|
+
"AGENT_AUTH_REQUIRED",
|
|
31
|
+
"AGENT_QUOTA_EXCEEDED",
|
|
32
|
+
"AGENT_SANDBOX_UNAVAILABLE",
|
|
33
|
+
"AGENT_MODEL_UNAVAILABLE",
|
|
33
34
|
"AGENT_REPORTED_FAILURE",
|
|
34
35
|
"UNKNOWN_HTTP_OUTCOME",
|
|
35
36
|
"HTTP_AGENT_FAILED",
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdir, readdir, copyFile } 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
|
+
for (const name of await readdir(new URL("../../sdk/dist/", import.meta.url))) {
|
|
13
|
+
if (name.endsWith(".js"))
|
|
14
|
+
await copyFile(new URL(`../../sdk/dist/${name}`, import.meta.url), new URL(name, destination));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const workerCore = new URL("../bin/worker-core/", import.meta.url);
|
|
18
|
+
await mkdir(workerCore, { recursive: true });
|
|
19
|
+
for (const name of ["adapters.mjs", "errors.mjs"])
|
|
20
|
+
await copyFile(new URL(`../../agent-worker/${name}`, import.meta.url), new URL(name, workerCore));
|