totemlayer 0.1.14 → 0.1.15
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/inbox-queue.mjs +71 -0
- package/bin/network-runtime.mjs +86 -78
- package/bin/totem.mjs +1 -1
- package/package.json +1 -1
- package/scripts/prepare.mjs +0 -24
|
@@ -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/network-runtime.mjs
CHANGED
|
@@ -4,6 +4,7 @@ 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";
|
|
8
9
|
import {
|
|
9
10
|
externalModelReply,
|
|
@@ -624,88 +625,95 @@ export async function runRuntime(settings) {
|
|
|
624
625
|
});
|
|
625
626
|
// The A2A queue must keep moving while a human command waits on another agent.
|
|
626
627
|
// 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
|
-
thread.push(...result.data);
|
|
651
|
-
if (thread.some((item) => item.id === message.id) || !result.nextCursor) break;
|
|
652
|
-
cursor = result.nextCursor;
|
|
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
|
-
}),
|
|
628
|
+
inbox = runInboxQueue({
|
|
629
|
+
signal,
|
|
630
|
+
pollMs: settings.pollMs,
|
|
631
|
+
concurrency: settings.messageConcurrency ?? 2,
|
|
632
|
+
once: settings.once,
|
|
633
|
+
shouldContinue: () => !settings.once || !humanDone,
|
|
634
|
+
messages: (signal) =>
|
|
635
|
+
client.messages.iterate({ direction: "inbound", unread: true, limit: 100 }, { signal }),
|
|
636
|
+
eligible: async (message) => !(await journal.read(`message-blocker:${message.id}`)),
|
|
637
|
+
handle: async (message, signal) => {
|
|
638
|
+
try {
|
|
639
|
+
await inspectNetworkMessage(settings.inspector, client, message, { signal });
|
|
640
|
+
const continuation = autonomousReplyMetadata(message, settings, identity.address);
|
|
641
|
+
if (message.inReplyToId && !continuation) {
|
|
642
|
+
await client.messages.ack(message.id, { signal });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const thread = [];
|
|
646
|
+
let cursor;
|
|
647
|
+
for (let page = 0; page < 5; page++) {
|
|
648
|
+
const result = await client.conversations.messages(
|
|
649
|
+
message.conversationId,
|
|
650
|
+
{ limit: 100, ...(cursor ? { cursor } : {}) },
|
|
689
651
|
{ signal },
|
|
690
652
|
);
|
|
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
|
-
});
|
|
653
|
+
thread.push(...result.data);
|
|
654
|
+
if (thread.some((item) => item.id === message.id) || !result.nextCursor) break;
|
|
655
|
+
cursor = result.nextCursor;
|
|
704
656
|
}
|
|
657
|
+
for (const item of thread)
|
|
658
|
+
await inspectNetworkMessage(settings.inspector, client, item, { signal });
|
|
659
|
+
const history = boundedHistory(
|
|
660
|
+
thread.map((item) => ({
|
|
661
|
+
id: item.id,
|
|
662
|
+
actor: item.from === identity.address ? "agent" : "human",
|
|
663
|
+
text:
|
|
664
|
+
item.content.type === "text"
|
|
665
|
+
? item.content.text
|
|
666
|
+
: JSON.stringify(item.content.data),
|
|
667
|
+
})),
|
|
668
|
+
message.id,
|
|
669
|
+
);
|
|
670
|
+
const text = await generate({
|
|
671
|
+
client,
|
|
672
|
+
identity,
|
|
673
|
+
journal,
|
|
674
|
+
key: `message:${message.id}`,
|
|
675
|
+
text:
|
|
676
|
+
message.content.type === "text"
|
|
677
|
+
? message.content.text
|
|
678
|
+
: JSON.stringify(message.content.data),
|
|
679
|
+
history,
|
|
680
|
+
settings: {
|
|
681
|
+
...settings,
|
|
682
|
+
modelTimeoutMs:
|
|
683
|
+
settings.messageTimeoutMs ?? Math.min(settings.modelTimeoutMs ?? 900000, 120000),
|
|
684
|
+
},
|
|
685
|
+
signal,
|
|
686
|
+
allowTools: false,
|
|
687
|
+
});
|
|
688
|
+
await settings.inspector.runAction(
|
|
689
|
+
{ id: message.id, text },
|
|
690
|
+
(inspected) =>
|
|
691
|
+
client.messages.reply(inspected.id, inspected.text, {
|
|
692
|
+
idempotencyKey: operationKey(identity.id, "reply", message.id),
|
|
693
|
+
...(continuation ? { metadata: continuation } : {}),
|
|
694
|
+
signal,
|
|
695
|
+
}),
|
|
696
|
+
{ signal },
|
|
697
|
+
);
|
|
698
|
+
await client.messages.ack(message.id, { signal });
|
|
699
|
+
emit({
|
|
700
|
+
type: "message_handled",
|
|
701
|
+
data: { messageId: message.id, conversationId: message.conversationId },
|
|
702
|
+
});
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (!signal.aborted) report(error, { messageId: message.id });
|
|
705
|
+
if (error.status === 401) throw error;
|
|
706
|
+
if (error.status === 429 || error.status >= 500) throw error;
|
|
707
|
+
if (
|
|
708
|
+
(error.status >= 400 && error.status < 500) ||
|
|
709
|
+
error.code === "AGENT_OUTCOME_UNCERTAIN"
|
|
710
|
+
)
|
|
711
|
+
await journal.save(`message-blocker:${message.id}`, {
|
|
712
|
+
code: error.code ?? "MESSAGE_BLOCKED",
|
|
713
|
+
});
|
|
705
714
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
})().catch((error) => {
|
|
715
|
+
},
|
|
716
|
+
}).catch((error) => {
|
|
709
717
|
if (!signal.aborted) {
|
|
710
718
|
fatalError = error;
|
|
711
719
|
shutdown.abort();
|
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._:-]*$/;
|
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));
|