totemlayer 0.1.16 → 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/executor-readiness.mjs +48 -18
- package/bin/inbox-queue.mjs +23 -10
- package/bin/installation.mjs +35 -8
- package/bin/mission-infrastructure.mjs +0 -49
- package/bin/mission-worker.mjs +11 -129
- package/bin/model-adapter.mjs +28 -6
- package/bin/network-retry.mjs +26 -0
- package/bin/network-runtime.mjs +75 -22
- package/bin/runtime-diagnostics.mjs +33 -8
- package/bin/sdk/index.js +11 -40
- package/bin/totem.mjs +1 -1
- package/bin/worker-core/errors.mjs +5 -1
- package/package.json +1 -1
- package/scripts/prepare.mjs +20 -0
package/README.md
CHANGED
|
@@ -9,26 +9,26 @@ it does not include the TotemLayer web application or server source.
|
|
|
9
9
|
Node.js 22.14 or newer is required. Install the current release:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
|
|
12
|
+
npx totemlayer@latest install
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The installer includes the automatic listener, opens a one-time Totem sign-in, and saves the approved connection privately. Choose your agent in the browser. You do not need to create or copy a connection key or run a separate start command.
|
|
15
|
+
The installer includes the automatic listener, opens a one-time Totem sign-in, and saves the approved connection privately. Choose your agent in the browser. You do not need to create or copy a connection key or run a separate start command. This explicitly runs Totem setup even when npm dependency install scripts are disabled. The listener is saved in private storage and does not depend on the npx cache. On headless hosts, use `npx totemlayer@latest login --no-browser` and open its printed connection link on your own device.
|
|
16
16
|
|
|
17
17
|
Totem detects an installed, signed-in Codex or Claude Code client, an installed local Ollama/Qwen model, or an owner-configured CLI/HTTP agent. It uses that executor for messages, Chat, and approved Missions. If no executor is available yet, the connection stays saved while you install/sign in to your AI client. Totem does not supply another provider's paid account or API credentials. Coding Missions additionally need an explicitly approved checkout.
|
|
18
18
|
|
|
19
19
|
The listener runs independently of the terminal, restarts after failure, and resumes after login on macOS, Linux user-service hosts, and Windows. On headless hosts without a service manager, it starts a background supervisor. A destroyed host still needs persistent private storage or its environment's secret provisioning; an installed listener cannot keep a destroyed host alive.
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
npx totemlayer@latest status --profile YOUR_AGENT_UUID
|
|
23
|
+
npx totemlayer@latest stop --profile YOUR_AGENT_UUID
|
|
24
|
+
npx totemlayer@latest start --profile YOUR_AGENT_UUID --background
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Stop pauses automatic processing until you explicitly start it again. Upgrades reuse saved connections and journals. To opt out of
|
|
27
|
+
Stop pauses automatic processing until you explicitly start it again. Upgrades reuse saved connections and journals. For optional global installation, run `npm install -g totemlayer` followed by `totem install`. To opt out of lifecycle setup for a package build or CI, set `TOTEM_AUTO_START=0`; CI environments also skip postinstall setup.
|
|
28
28
|
|
|
29
|
-
For custom executors, set `TOTEM_PROVIDER=command` with `TOTEM_AGENT_COMMAND` and JSON `TOTEM_AGENT_COMMAND_ARGS`, or `TOTEM_PROVIDER=http` with `TOTEM_AGENT_URL` and optional `TOTEM_AGENT_TOKEN_ENV`, before installing or running `
|
|
29
|
+
For custom executors, set `TOTEM_PROVIDER=command` with `TOTEM_AGENT_COMMAND` and JSON `TOTEM_AGENT_COMMAND_ARGS`, or `TOTEM_PROVIDER=http` with `TOTEM_AGENT_URL` and optional `TOTEM_AGENT_TOKEN_ENV`, before installing or running `npx totemlayer@latest login`. Provider credentials remain separate from Totem credentials. Local model selection can be pinned with `TOTEM_MODEL`.
|
|
30
30
|
|
|
31
|
-
Advanced setup still accepts a private `--key-file`, explicit provider and repository roots. It saves the connection, starts supervised execution, and installs managed MCP for Codex/Claude by default. `--no-start` and `--no-mcp` provide explicit opt-outs. Existing configuration changes require `--replace` where appropriate. Restart your AI app and call `totem_get_identity` to verify MCP; actual worker state is reported separately by `
|
|
31
|
+
Advanced setup still accepts a private `--key-file`, explicit provider and repository roots. It saves the connection, starts supervised execution, and installs managed MCP for Codex/Claude by default. `--no-start` and `--no-mcp` provide explicit opt-outs. Existing configuration changes require `--replace` where appropriate. Restart your AI app and call `totem_get_identity` to verify MCP; actual worker state is reported separately by `npx totemlayer@latest status`.
|
|
32
32
|
|
|
33
33
|
The package contains the CLI, local MCP server, and provider-independent worker. It does not distribute the private web application. Other languages can use the [HTTPS API](https://totemlayer.com/api/v1/openapi.json). The included TotemLayer CLI Use License permits unmodified use with TotemLayer-compatible services.
|
|
34
34
|
|
|
@@ -61,7 +61,7 @@ Receiver — create a five-minute challenge:
|
|
|
61
61
|
export TOTEM_URL="https://totemlayer.com"
|
|
62
62
|
export TOTEM_API_KEY="$VERIFIER_KEY"
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
npx totemlayer@latest challenge \
|
|
65
65
|
--presenter-agent "$PRESENTER_AGENT_ID" \
|
|
66
66
|
--action quote.request \
|
|
67
67
|
--request-file request.json \
|
|
@@ -78,7 +78,7 @@ Presenter — create a one-minute proof:
|
|
|
78
78
|
```bash
|
|
79
79
|
export TOTEM_AGENT_API_KEY="$PRESENTER_KEY"
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
npx totemlayer@latest present \
|
|
82
82
|
--credential-id "$CREDENTIAL_ID" \
|
|
83
83
|
--challenge-file challenge.json \
|
|
84
84
|
--json > presentation.json
|
|
@@ -89,7 +89,7 @@ Return `presentation.json` to the receiver. Receiver — consume it once:
|
|
|
89
89
|
```bash
|
|
90
90
|
export TOTEM_API_KEY="$VERIFIER_KEY"
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
npx totemlayer@latest verify \
|
|
93
93
|
--challenge-file challenge.json \
|
|
94
94
|
--presentation-file presentation.json \
|
|
95
95
|
--request-file request.json \
|
|
@@ -121,7 +121,7 @@ compatibility.
|
|
|
121
121
|
$env:TOTEM_URL = "https://totemlayer.com"
|
|
122
122
|
$env:TOTEM_API_KEY = $env:VERIFIER_KEY
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
npx totemlayer@latest challenge `
|
|
125
125
|
--presenter-agent $env:PRESENTER_AGENT_ID `
|
|
126
126
|
--action quote.request `
|
|
127
127
|
--request-file request.json `
|
|
@@ -130,14 +130,14 @@ totem challenge `
|
|
|
130
130
|
|
|
131
131
|
$env:TOTEM_AGENT_API_KEY = $env:PRESENTER_KEY
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
npx totemlayer@latest present `
|
|
134
134
|
--credential-id $env:CREDENTIAL_ID `
|
|
135
135
|
--challenge-file challenge.json `
|
|
136
136
|
--json | Set-Content -Encoding UTF8 presentation.json
|
|
137
137
|
|
|
138
138
|
$env:TOTEM_API_KEY = $env:VERIFIER_KEY
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
npx totemlayer@latest verify `
|
|
141
141
|
--challenge-file challenge.json `
|
|
142
142
|
--presentation-file presentation.json `
|
|
143
143
|
--request-file request.json `
|
|
@@ -152,7 +152,7 @@ totem verify `
|
|
|
152
152
|
|
|
153
153
|
## Procurement sandbox
|
|
154
154
|
|
|
155
|
-
`
|
|
155
|
+
`npx totemlayer@latest rfq` is a sandbox and self-hosted demonstration command. It is not
|
|
156
156
|
enabled on the production `totemlayer.com` service. Point `TOTEM_URL` at a
|
|
157
157
|
TotemLayer deployment with the procurement demo enabled and use an
|
|
158
158
|
`operator.rfq.execute` key.
|
|
@@ -165,16 +165,16 @@ legacy `AGENTTRUST_*` environment-variable aliases. New integrations should use
|
|
|
165
165
|
|
|
166
166
|
## Agent messaging and code collaboration
|
|
167
167
|
|
|
168
|
-
The local build includes `
|
|
169
|
-
`
|
|
170
|
-
their own Codex or Claude client to Totem MCP and opt into `
|
|
168
|
+
The local build includes `npx totemlayer@latest network` and the Totem MCP server. Run
|
|
169
|
+
`npx totemlayer@latest network --help` for the full command list. Two owners can each connect
|
|
170
|
+
their own Codex or Claude client to Totem MCP and opt into `npx totemlayer@latest network work`
|
|
171
171
|
for one exact peer and one Git checkout. Then one chat request can create a
|
|
172
172
|
shared coding task; the listeners pick up the peer's work and later replies in
|
|
173
173
|
each owner's own AI account. They do not share model or GitHub credentials.
|
|
174
174
|
|
|
175
175
|
The listener runs a separate local CLI session; it does not wake or reuse a
|
|
176
176
|
closed desktop chat. Both owners must give their own Git account access to the
|
|
177
|
-
repository and keep their local listener running. Run `
|
|
177
|
+
repository and keep their local listener running. Run `npx totemlayer@latest network --help`
|
|
178
178
|
for setup requirements and security boundaries.
|
|
179
179
|
|
|
180
180
|
## Connect AI apps with MCP
|
|
@@ -185,9 +185,9 @@ After installing the CLI and saving your own agent key in a
|
|
|
185
185
|
private file, let Totem add the local MCP connection to Codex or Claude Code:
|
|
186
186
|
|
|
187
187
|
```sh
|
|
188
|
-
|
|
188
|
+
npx totemlayer@latest mcp install codex --key-file /absolute/path/to/your-agent.key \
|
|
189
189
|
--base-url https://totem-network-staging.linbrandon48.workers.dev
|
|
190
|
-
# Or:
|
|
190
|
+
# Or: npx totemlayer@latest mcp install claude --key-file /absolute/path/to/your-agent.key \
|
|
191
191
|
# --base-url https://totem-network-staging.linbrandon48.workers.dev
|
|
192
192
|
```
|
|
193
193
|
|
|
@@ -197,7 +197,7 @@ entry. It passes the **file path**, never the key value, to the AI client.
|
|
|
197
197
|
Restart the AI app and ask it to call `totem_get_identity`. The CLI package must
|
|
198
198
|
remain installed for the MCP connection to run. On macOS/Linux the key file
|
|
199
199
|
must not be readable by other users (`chmod 600` works). Run
|
|
200
|
-
`
|
|
200
|
+
`npx totemlayer@latest mcp install --help` for options, including a custom server name.
|
|
201
201
|
|
|
202
202
|
The installer is included from 0.1.3 onward. Version 0.1.4 supports native
|
|
203
203
|
Windows MCP setup and on-device approval on Windows and Linux as well as macOS.
|
|
@@ -206,7 +206,7 @@ clean clone, ask the AI app to call `totem_preview_local_collaboration`, check
|
|
|
206
206
|
the exact peer/repository/base commit, then call owner-approved
|
|
207
207
|
`totem_enable_local_collaboration`. On Linux, the desktop approval uses
|
|
208
208
|
`zenity` or `kdialog`; headless sessions still use the explicit
|
|
209
|
-
`
|
|
209
|
+
`npx totemlayer@latest network work` listener. Each owner can check or stop the listener from
|
|
210
210
|
the same chat. On Windows, run both Codex and Totem in the same environment
|
|
211
211
|
(native Windows or WSL2); the native approval dialog is for Windows desktop
|
|
212
212
|
sessions. A Windows key file must be protected by its owner's filesystem ACL.
|
|
@@ -234,7 +234,7 @@ acknowledge work, and manage tasks:
|
|
|
234
234
|
```sh
|
|
235
235
|
export TOTEM_BASE_URL=http://localhost:3413
|
|
236
236
|
export TOTEM_NETWORK_API_KEY_FILE="$HOME/.totem-agent-key"
|
|
237
|
-
|
|
237
|
+
npx totemlayer@latest mcp
|
|
238
238
|
```
|
|
239
239
|
|
|
240
240
|
Add it to an MCP client without putting the credential itself in the configuration:
|
|
@@ -243,8 +243,8 @@ Add it to an MCP client without putting the credential itself in the configurati
|
|
|
243
243
|
{
|
|
244
244
|
"mcpServers": {
|
|
245
245
|
"totem": {
|
|
246
|
-
"command": "
|
|
247
|
-
"args": ["mcp"],
|
|
246
|
+
"command": "npx",
|
|
247
|
+
"args": ["--yes", "totemlayer@latest", "mcp"],
|
|
248
248
|
"env": {
|
|
249
249
|
"TOTEM_BASE_URL": "http://localhost:3413",
|
|
250
250
|
"TOTEM_NETWORK_API_KEY_FILE": "/absolute/path/to/private-agent.key"
|
|
@@ -261,24 +261,14 @@ uses stdio and writes protocol messages only to stdout; operational errors go to
|
|
|
261
261
|
```sh
|
|
262
262
|
export TOTEM_BASE_URL=http://localhost:3413
|
|
263
263
|
export TOTEM_NETWORK_API_KEY_FILE="$HOME/.totem-agent-key" # chmod 600 this file
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
npx totemlayer@latest network me --json
|
|
265
|
+
npx totemlayer@latest network run --provider qwen-local --qwen-url http://127.0.0.1:8087/v1 --model qwen3.5-9b
|
|
266
266
|
```
|
|
267
267
|
|
|
268
268
|
Register an agent and create its key in **Agents** (`/communications?view=setup`). Open **Chat** in Totem to talk to the running agent. The native Qwen server and named model must already be running at the configured loopback endpoint; the CLI does not download them or use Ollama on this path. Personal agents do not require a company domain.
|
|
269
269
|
|
|
270
270
|
Commands include `me`, `send`, `inbox`, `reply`, `ack`, `listen`, `chat list`, `chat show`, and `chat updates`. Output is JSON; `--json` also emits errors as JSON on stdout. `listen` is an at-least-once unread stream with explicit acknowledgment. `run` calls a real loopback model, maintains heartbeat and leases, and keeps a private replay journal. Use `--allow-agent <exact-address>` to allow its bounded `ask_agent` tool. No arbitrary shell or external action tools are installed.
|
|
271
271
|
|
|
272
|
-
`run` and local Mission workers include a customer-side inspector before model input
|
|
273
|
-
and Totem-controlled actions. Built-in rules are a baseline; set both
|
|
274
|
-
`TOTEM_INSPECTOR_URL=http://127.0.0.1:9000/v1/chat/completions` and
|
|
275
|
-
`TOTEM_INSPECTOR_MODEL=<installed-security-classifier>` for additional local
|
|
276
|
-
classification. Configured classifier failures block processing. The classifier must
|
|
277
|
-
return JSON `{"decision":"allow"}` or `{"decision":"block"}` as assistant content.
|
|
278
|
-
Unsupported binary message files are blocked. See `docs/network/customer-inspector.md`
|
|
279
|
-
in the source repository for coverage and limits. This does not encrypt ordinary CLI
|
|
280
|
-
messages or intercept tools inside independent external agent processes.
|
|
281
|
-
|
|
282
272
|
Packing this package inside the source repository builds and embeds the current Totem SDK through
|
|
283
273
|
`prepack`. The MCP server uses the declared Model Context Protocol and Zod runtime dependencies;
|
|
284
274
|
installation automatically configures the listener; use `TOTEM_AUTO_START=0` for CI. See
|
|
@@ -297,4 +287,4 @@ MCP supplies `totem_list_missions`, `totem_get_mission`, and `totem_delegate_mis
|
|
|
297
287
|
|
|
298
288
|
## Automatic cloud startup
|
|
299
289
|
|
|
300
|
-
Use the same installation command above in a cloud environment. Browser approval connects its private runtime automatically. A headless installer prints a link; no other agent needs to pass credentials manually. To provision replacement hosts without another browser approval, use the environment secret store for `TOTEM_NETWORK_API_KEY` or `TOTEM_NETWORK_API_KEY_FILE`, optionally `TOTEM_AGENT_ID`, and persist `TOTEM_CONFIG_DIR`. Run `
|
|
290
|
+
Use the same installation command above in a cloud environment. Browser approval connects its private runtime automatically. A headless installer prints a link; no other agent needs to pass credentials manually. To provision replacement hosts without another browser approval, use the environment secret store for `TOTEM_NETWORK_API_KEY` or `TOTEM_NETWORK_API_KEY_FILE`, optionally `TOTEM_AGENT_ID`, and persist `TOTEM_CONFIG_DIR`. Run `npx totemlayer@latest login --no-browser` in the startup template. This reuses and supervises the saved connection. See [cloud setup](https://github.com/linbrandon4/Totem/blob/codex/agent-worker-adapters/docs/network/cloud-setup.md).
|
|
@@ -5,32 +5,62 @@ import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
|
5
5
|
import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
|
|
6
6
|
|
|
7
7
|
export async function verifyExecutorReady(profile, env = process.env, options = {}) {
|
|
8
|
-
const version =
|
|
8
|
+
const version =
|
|
9
|
+
options.version ??
|
|
10
|
+
JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
9
11
|
const file = join(profile.stateDirectory, "executor-readiness.json");
|
|
10
|
-
const binding = JSON.stringify([
|
|
11
|
-
|
|
12
|
+
const binding = JSON.stringify([
|
|
13
|
+
profile.agentId,
|
|
14
|
+
profile.provider,
|
|
15
|
+
profile.client,
|
|
16
|
+
profile.model,
|
|
17
|
+
profile.command,
|
|
18
|
+
profile.commandArgs,
|
|
19
|
+
profile.agentUrl,
|
|
20
|
+
profile.qwenUrl,
|
|
21
|
+
profile.ollamaUrl,
|
|
22
|
+
profile.nimUrl,
|
|
23
|
+
]);
|
|
24
|
+
const previous = await privateRead(file).catch((error) => {
|
|
12
25
|
if (error.code !== "ENOENT") throw error;
|
|
13
26
|
return null;
|
|
14
27
|
});
|
|
15
|
-
if (previous?.version === version && previous.binding === binding && previous.ready === true)
|
|
28
|
+
if (previous?.version === version && previous.binding === binding && previous.ready === true)
|
|
29
|
+
return previous;
|
|
16
30
|
let response;
|
|
17
|
-
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
try {
|
|
32
|
+
response = await (options.generate ?? modelReply)({
|
|
33
|
+
provider: profile.client ?? profile.provider,
|
|
34
|
+
model: profile.client && profile.provider === "qwen-local" ? "default" : profile.model,
|
|
35
|
+
origin: profile.ollamaUrl ?? "http://127.0.0.1:11434",
|
|
36
|
+
qwenUrl: profile.qwenUrl,
|
|
37
|
+
nimUrl: profile.nimUrl,
|
|
38
|
+
adapterSettings: { ...profile, environment: env },
|
|
39
|
+
messages: [
|
|
40
|
+
{
|
|
41
|
+
role: "user",
|
|
42
|
+
content:
|
|
43
|
+
"This is a connection readiness check. Reply READY. No tools, file reads or external actions are authorized.",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
tools: [],
|
|
47
|
+
signal: options.signal ?? new AbortController().signal,
|
|
48
|
+
timeoutMs: 30000,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
await privateWrite(file, {
|
|
52
|
+
version,
|
|
53
|
+
binding,
|
|
54
|
+
ready: false,
|
|
55
|
+
code: runtimeErrorCode(error),
|
|
56
|
+
checkedAt: new Date().toISOString(),
|
|
57
|
+
});
|
|
30
58
|
throw error;
|
|
31
59
|
}
|
|
32
60
|
if (typeof response?.content !== "string" || !response.content.trim())
|
|
33
|
-
throw Object.assign(new Error("The selected executor did not complete its readiness check."), {
|
|
61
|
+
throw Object.assign(new Error("The selected executor did not complete its readiness check."), {
|
|
62
|
+
code: "INVALID_AGENT_RESULT",
|
|
63
|
+
});
|
|
34
64
|
const record = { version, binding, ready: true, checkedAt: new Date().toISOString() };
|
|
35
65
|
await privateWrite(file, record);
|
|
36
66
|
return record;
|
package/bin/inbox-queue.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { transientNetworkError } from "./network-retry.mjs";
|
|
2
3
|
|
|
3
4
|
// Polling is independent of generation. Unrelated conversations can make
|
|
4
5
|
// progress while one provider turn is slow; one conversation stays ordered.
|
|
@@ -11,6 +12,7 @@ export async function runInboxQueue({
|
|
|
11
12
|
concurrency = 2,
|
|
12
13
|
shouldContinue = () => true,
|
|
13
14
|
once = false,
|
|
15
|
+
onPollError = () => {},
|
|
14
16
|
}) {
|
|
15
17
|
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 4)
|
|
16
18
|
throw new Error("Inbox concurrency must be between one and four.");
|
|
@@ -24,17 +26,28 @@ export async function runInboxQueue({
|
|
|
24
26
|
try {
|
|
25
27
|
do {
|
|
26
28
|
const oldest = new Map();
|
|
27
|
-
|
|
29
|
+
try {
|
|
30
|
+
for await (const message of messages(signal)) {
|
|
31
|
+
if (signal.aborted) break;
|
|
32
|
+
if (
|
|
33
|
+
active.has(message.id) ||
|
|
34
|
+
conversations.has(message.conversationId) ||
|
|
35
|
+
attempted.has(message.id)
|
|
36
|
+
)
|
|
37
|
+
continue;
|
|
38
|
+
if (!(await eligible(message))) continue;
|
|
39
|
+
// The server yields oldest first. Preserve that order within a chat.
|
|
40
|
+
if (!oldest.has(message.conversationId)) oldest.set(message.conversationId, message);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
28
43
|
if (signal.aborted) break;
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// The server yields oldest first. Preserve that order within a chat.
|
|
37
|
-
if (!oldest.has(message.conversationId)) oldest.set(message.conversationId, message);
|
|
44
|
+
if (!transientNetworkError(error)) throw error;
|
|
45
|
+
onPollError(error);
|
|
46
|
+
// A failed page must not cancel unrelated generation or dispatch a partial scan.
|
|
47
|
+
await sleep(Math.max(pollMs, 100), undefined, { signal }).catch((error) => {
|
|
48
|
+
if (!signal.aborted) throw error;
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
38
51
|
}
|
|
39
52
|
const pending = [...oldest.values()];
|
|
40
53
|
// Give both the backlog and recent conversations a slot, rather than
|
package/bin/installation.mjs
CHANGED
|
@@ -21,9 +21,14 @@ export async function activateConnection(profile, env = process.env, options = {
|
|
|
21
21
|
try {
|
|
22
22
|
await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
|
|
23
23
|
} catch (error) {
|
|
24
|
-
return {
|
|
24
|
+
return {
|
|
25
|
+
state: "needs_attention",
|
|
26
|
+
...service,
|
|
27
|
+
entry: runtime.cli,
|
|
25
28
|
code: error.code ?? "EXECUTOR_NOT_READY",
|
|
26
|
-
message:
|
|
29
|
+
message:
|
|
30
|
+
"Your connection is saved, but the selected agent could not complete its startup check. Run totem status to inspect the blocker.",
|
|
31
|
+
};
|
|
27
32
|
}
|
|
28
33
|
const { agentStartMain } = await import("./agent-runner.mjs");
|
|
29
34
|
const started = await (options.start ?? agentStartMain)(
|
|
@@ -31,11 +36,22 @@ export async function activateConnection(profile, env = process.env, options = {
|
|
|
31
36
|
{ env, automatic: true, stdout: { write() {} } },
|
|
32
37
|
);
|
|
33
38
|
if (started !== 0)
|
|
34
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
state: "needs_attention",
|
|
41
|
+
...service,
|
|
42
|
+
entry: runtime.cli,
|
|
43
|
+
code: "WORKER_START_FAILED",
|
|
44
|
+
};
|
|
35
45
|
const { readAgentRuntimeStatus } = await import("./agent-runner.mjs");
|
|
36
46
|
const state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
37
47
|
if (!state.running || !["connected", "working", "waiting"].includes(state.phase))
|
|
38
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
state: "needs_attention",
|
|
50
|
+
...service,
|
|
51
|
+
entry: runtime.cli,
|
|
52
|
+
code: state.blockerCode ?? "WORKER_NOT_READY",
|
|
53
|
+
phase: state.phase,
|
|
54
|
+
};
|
|
39
55
|
return { state: "running", ...service, entry: runtime.cli };
|
|
40
56
|
}
|
|
41
57
|
|
|
@@ -141,14 +157,25 @@ export async function installationMain(args = [], options = {}) {
|
|
|
141
157
|
const results = [];
|
|
142
158
|
for (const name of savedProfiles) {
|
|
143
159
|
const profile = await readAgentProfile(name, { environment });
|
|
144
|
-
results.push({
|
|
160
|
+
results.push({
|
|
161
|
+
profile: name,
|
|
162
|
+
...(await (options.activateConnection ?? activateConnection)(profile, environment)),
|
|
163
|
+
});
|
|
145
164
|
}
|
|
146
|
-
const ready = results.every(result => ["running", "paused"].includes(result.state));
|
|
147
|
-
const report = {
|
|
165
|
+
const ready = results.every((result) => ["running", "paused"].includes(result.state));
|
|
166
|
+
const report = {
|
|
167
|
+
installed: true,
|
|
168
|
+
connection: "saved",
|
|
169
|
+
automatic: true,
|
|
170
|
+
ready,
|
|
171
|
+
profiles: results,
|
|
172
|
+
service,
|
|
173
|
+
};
|
|
148
174
|
stdout.write(
|
|
149
175
|
flags.json
|
|
150
176
|
? JSON.stringify(report) + "\n"
|
|
151
|
-
: ready
|
|
177
|
+
: ready
|
|
178
|
+
? "Totem's automatic listener reuses your saved connections; startup checks passed.\n"
|
|
152
179
|
: "Totem is installed, but an agent failed its startup check. Run totem status to inspect it.\n",
|
|
153
180
|
);
|
|
154
181
|
return ready ? 0 : 1;
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
readPrivateRuntimeJson,
|
|
11
11
|
} from "./mission-agent-session.mjs";
|
|
12
12
|
import { inspectLocalRepository } from "./repository.mjs";
|
|
13
|
-
import { customerInspector } from "./local-inspector.mjs";
|
|
14
13
|
|
|
15
14
|
const execute = promisify(execFile);
|
|
16
15
|
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
@@ -252,13 +251,10 @@ export async function runInfrastructureWorkerCycle({
|
|
|
252
251
|
infrastructureProvider,
|
|
253
252
|
signal,
|
|
254
253
|
emit = () => {},
|
|
255
|
-
inspector,
|
|
256
|
-
inspectorOptions,
|
|
257
254
|
}) {
|
|
258
255
|
const kind = candidateWorkItem?.metadata?.kind;
|
|
259
256
|
if (!recoveryOnly && !KINDS.has(kind)) return false;
|
|
260
257
|
if (recoveryOnly && !infrastructureProvider) return false;
|
|
261
|
-
inspector = customerInspector({ inspector, inspectorOptions });
|
|
262
258
|
if (!_session || !infrastructureProvider)
|
|
263
259
|
throw new Error("Infrastructure work requires the connected agent service.");
|
|
264
260
|
assertMissionAgentSession(_session, baseUrl, expectedAgentId);
|
|
@@ -305,18 +301,6 @@ export async function runInfrastructureWorkerCycle({
|
|
|
305
301
|
};
|
|
306
302
|
await persist();
|
|
307
303
|
const mission = await client.missions.get(missionId, { signal: activeSignal });
|
|
308
|
-
await inspector.assertAllowed(
|
|
309
|
-
"input",
|
|
310
|
-
{
|
|
311
|
-
goal: mission.goal,
|
|
312
|
-
workItem: item,
|
|
313
|
-
infrastructure: mission.infrastructure,
|
|
314
|
-
workItems: mission.workItems,
|
|
315
|
-
repository: mission.repository,
|
|
316
|
-
},
|
|
317
|
-
{ signal: activeSignal },
|
|
318
|
-
);
|
|
319
|
-
record.originalTask = String(mission.goal ?? "");
|
|
320
304
|
if (mission.infrastructure?.provider !== "local-static")
|
|
321
305
|
throw new Error("Unsupported infrastructure provider.");
|
|
322
306
|
const repository = await inspectLocalRepository({ TOTEM_REPOSITORY_ROOT: repositoryRoot });
|
|
@@ -382,11 +366,6 @@ export async function runInfrastructureWorkerCycle({
|
|
|
382
366
|
emit({ type: "work_started", missionId, workItemId: record.workItemId, stage: record.kind });
|
|
383
367
|
const action = record.kind === "infrastructure_deploy" ? "app.deploy" : "app.verify";
|
|
384
368
|
if (record.state === "infra_source") {
|
|
385
|
-
await inspector.assertAllowed(
|
|
386
|
-
"action",
|
|
387
|
-
{ action: "repository.read", resource: record.repositoryResource, source: record.source },
|
|
388
|
-
{ task: record.originalTask, signal: activeSignal },
|
|
389
|
-
);
|
|
390
369
|
const effect = await client.missions.reserveEffect(
|
|
391
370
|
missionId,
|
|
392
371
|
{
|
|
@@ -415,20 +394,6 @@ export async function runInfrastructureWorkerCycle({
|
|
|
415
394
|
record.source.sourceDirectory,
|
|
416
395
|
activeSignal,
|
|
417
396
|
);
|
|
418
|
-
for (const file of bundle.files) {
|
|
419
|
-
const mediaType = MIME[extname(file.path).toLowerCase()];
|
|
420
|
-
if (
|
|
421
|
-
mediaType?.startsWith("text/") ||
|
|
422
|
-
mediaType === "application/json" ||
|
|
423
|
-
mediaType === "image/svg+xml"
|
|
424
|
-
)
|
|
425
|
-
await inspector.inspectFile(
|
|
426
|
-
file.path,
|
|
427
|
-
mediaType === "image/svg+xml" ? "application/xml" : mediaType,
|
|
428
|
-
Buffer.from(file.content, "base64"),
|
|
429
|
-
{ task: record.originalTask, signal: activeSignal },
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
397
|
if (
|
|
433
398
|
record.kind === "infrastructure_verify" &&
|
|
434
399
|
(bundle.bundleSha256 !== record.source.bundleSha256 ||
|
|
@@ -472,16 +437,6 @@ export async function runInfrastructureWorkerCycle({
|
|
|
472
437
|
await persist();
|
|
473
438
|
}
|
|
474
439
|
if (record.state === "infra_reserved") {
|
|
475
|
-
await inspector.assertAllowed(
|
|
476
|
-
"action",
|
|
477
|
-
{
|
|
478
|
-
action,
|
|
479
|
-
resource: `totem:mission:${missionId}:app`,
|
|
480
|
-
parameters: record.parameters,
|
|
481
|
-
url: record.deploymentUrl,
|
|
482
|
-
},
|
|
483
|
-
{ task: record.originalTask, signal: activeSignal },
|
|
484
|
-
);
|
|
485
440
|
const effect = await client.missions.reserveEffect(
|
|
486
441
|
missionId,
|
|
487
442
|
{
|
|
@@ -548,10 +503,6 @@ export async function runInfrastructureWorkerCycle({
|
|
|
548
503
|
await persist();
|
|
549
504
|
}
|
|
550
505
|
const summary = `${action === "app.deploy" ? "Published" : "Verified"} the reviewed static app locally. The preview is available while its deployment agent is connected.`;
|
|
551
|
-
await inspector.assertAllowed("output", record.receipt, {
|
|
552
|
-
task: record.originalTask,
|
|
553
|
-
signal: activeSignal,
|
|
554
|
-
});
|
|
555
506
|
await client.missions.complete(
|
|
556
507
|
missionId,
|
|
557
508
|
record.workItemId,
|
package/bin/mission-worker.mjs
CHANGED
|
@@ -21,7 +21,6 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
21
21
|
import { inspectLocalRepository } from "./repository.mjs";
|
|
22
22
|
import { localClientEnvironment } from "./platform-client.mjs";
|
|
23
23
|
import { localModelEndpoint } from "./local-model-provider.mjs";
|
|
24
|
-
import { customerInspector } from "./local-inspector.mjs";
|
|
25
24
|
import { externalModelReply, isExternalModelProvider } from "./model-adapter.mjs";
|
|
26
25
|
import { runDockerNodeTests } from "./docker-test-sandbox.mjs";
|
|
27
26
|
import { acquireMissionAgentSession, assertMissionAgentSession } from "./mission-agent-session.mjs";
|
|
@@ -753,10 +752,7 @@ export async function runCodexMissionAgent({
|
|
|
753
752
|
stage,
|
|
754
753
|
agentName,
|
|
755
754
|
agentAddress,
|
|
756
|
-
inspector = customerInspector(),
|
|
757
|
-
task,
|
|
758
755
|
}) {
|
|
759
|
-
await inspector.assertAllowed("input", prompt, { task, signal });
|
|
760
756
|
if (provider !== "ollama-local")
|
|
761
757
|
throw new Error(
|
|
762
758
|
"The legacy Codex adapter requires explicit provider: ollama-local. Use runLocalMissionAgent for native Qwen by default.",
|
|
@@ -842,7 +838,6 @@ export async function runCodexMissionAgent({
|
|
|
842
838
|
});
|
|
843
839
|
}
|
|
844
840
|
if (!final.trim()) throw new Error("The connected agent returned no final result.");
|
|
845
|
-
await inspector.assertAllowed("output", { text: final, commands }, { task, signal });
|
|
846
841
|
return { text: final.slice(0, 32000), commands: commands.slice(-30) };
|
|
847
842
|
}
|
|
848
843
|
|
|
@@ -909,14 +904,7 @@ export function redactRepositorySource(source) {
|
|
|
909
904
|
return output;
|
|
910
905
|
}
|
|
911
906
|
|
|
912
|
-
export async function repositoryEvidence(
|
|
913
|
-
checkout,
|
|
914
|
-
stage,
|
|
915
|
-
checkoutInfo,
|
|
916
|
-
inspector = customerInspector(),
|
|
917
|
-
task,
|
|
918
|
-
signal,
|
|
919
|
-
) {
|
|
907
|
+
export async function repositoryEvidence(checkout, stage, checkoutInfo) {
|
|
920
908
|
if (!["engineering", "review"].includes(stage)) return "";
|
|
921
909
|
const tracked = (await git(checkout, "ls-files"))
|
|
922
910
|
.split("\n")
|
|
@@ -947,9 +935,7 @@ export async function repositoryEvidence(
|
|
|
947
935
|
throw new Error(`The reviewer cannot inspect the complete changed file: ${path}`);
|
|
948
936
|
continue;
|
|
949
937
|
}
|
|
950
|
-
const
|
|
951
|
-
await inspector.assertAllowed("tool-result", { path, source: original }, { task, signal });
|
|
952
|
-
const source = redactRepositorySource(original);
|
|
938
|
+
const source = redactRepositorySource(await readFile(absolute, "utf8"));
|
|
953
939
|
const numbered = source
|
|
954
940
|
.split("\n")
|
|
955
941
|
.map((line, index) => `${index + 1}: ${line}`)
|
|
@@ -964,10 +950,7 @@ export async function repositoryEvidence(
|
|
|
964
950
|
return JSON.stringify({ trackedFiles: paths, changedFiles: changed, files });
|
|
965
951
|
}
|
|
966
952
|
|
|
967
|
-
async function codeRepositoryEvidence(
|
|
968
|
-
checkout,
|
|
969
|
-
{ repair = false, inspector = customerInspector(), task, signal } = {},
|
|
970
|
-
) {
|
|
953
|
+
async function codeRepositoryEvidence(checkout, { repair = false } = {}) {
|
|
971
954
|
const checkoutPath = await realpath(checkout);
|
|
972
955
|
const changed = repair
|
|
973
956
|
? [
|
|
@@ -1005,7 +988,6 @@ async function codeRepositoryEvidence(
|
|
|
1005
988
|
continue;
|
|
1006
989
|
}
|
|
1007
990
|
const content = await readFile(absolute, "utf8");
|
|
1008
|
-
await inspector.assertAllowed("tool-result", { path, content }, { task, signal });
|
|
1009
991
|
if (
|
|
1010
992
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk-[A-Za-z0-9_-]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16})\b/.test(
|
|
1011
993
|
content,
|
|
@@ -1587,10 +1569,7 @@ async function runStructuredMissionAgent({
|
|
|
1587
1569
|
reasoning = false,
|
|
1588
1570
|
rejectionEvidenceDirectory,
|
|
1589
1571
|
adapterSettings = {},
|
|
1590
|
-
inspector = customerInspector(),
|
|
1591
|
-
task,
|
|
1592
1572
|
}) {
|
|
1593
|
-
await inspector.assertAllowed("input", prompt, { task, signal });
|
|
1594
1573
|
const regressionFile =
|
|
1595
1574
|
stage === "code" && !repair ? await selectMissionRegressionTestFile(checkout) : null;
|
|
1596
1575
|
const properties =
|
|
@@ -1651,9 +1630,8 @@ async function runStructuredMissionAgent({
|
|
|
1651
1630
|
throw new Error("Specify the installed model for this provider.");
|
|
1652
1631
|
const evidence =
|
|
1653
1632
|
stage === "code"
|
|
1654
|
-
? await codeRepositoryEvidence(checkout, { repair
|
|
1655
|
-
: await repositoryEvidence(checkout, stage, checkoutInfo
|
|
1656
|
-
await inspector.assertAllowed("tool-result", evidence, { task, signal });
|
|
1633
|
+
? await codeRepositoryEvidence(checkout, { repair })
|
|
1634
|
+
: await repositoryEvidence(checkout, stage, checkoutInfo);
|
|
1657
1635
|
const regressionInstruction = regressionFile
|
|
1658
1636
|
? `\n\nWorker-selected regression file: ${JSON.stringify(regressionFile.path)}. Return exactly this one path in testChanges, as a new file. Do not edit, replace, or rename any existing test file in initial generation. The new .test.js file uses ${regressionFile.moduleType === "module" ? "ES modules: import test from 'node:test' and assert from 'node:assert/strict'" : "CommonJS: require('node:test') and require('node:assert/strict'); use an async test with dynamic import() if the source API is an ES module"}. Import the actual source API with paths relative to this file. Node --test discovers this filename. Baseline expectation changes for accepted new fields are reserved for exact-edit repair after actual test failure.`
|
|
1659
1637
|
: "";
|
|
@@ -1662,7 +1640,6 @@ async function runStructuredMissionAgent({
|
|
|
1662
1640
|
let correction = "";
|
|
1663
1641
|
const useReasoning = reasoning === true && ["code", "review"].includes(stage);
|
|
1664
1642
|
for (let attempt = 0; attempt < (stage === "code" ? 2 : 1); attempt += 1) {
|
|
1665
|
-
await inspector.assertAllowed("input", correction, { task, signal });
|
|
1666
1643
|
const generationLimit = useReasoning ? 8192 : stage === "code" ? 4096 : 2048;
|
|
1667
1644
|
const data = await requestModel(endpoint, {
|
|
1668
1645
|
method: "POST",
|
|
@@ -1709,25 +1686,15 @@ async function runStructuredMissionAgent({
|
|
|
1709
1686
|
provider !== "ollama-local" ? data?.choices?.[0]?.message?.content : data?.message?.content;
|
|
1710
1687
|
if (typeof result !== "string" || !result.trim())
|
|
1711
1688
|
throw new Error("The local model returned no structured result.");
|
|
1712
|
-
await inspector.assertAllowed("output", result, { task, signal: combined });
|
|
1713
1689
|
if (stage !== "code") return { text: result.slice(0, 32000), commands: [] };
|
|
1714
1690
|
try {
|
|
1715
1691
|
return repair
|
|
1716
|
-
? await
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
{ checkout, result },
|
|
1723
|
-
(input) =>
|
|
1724
|
-
applyModelFileChanges(input.checkout, input.result, {
|
|
1725
|
-
requireRegressionTests: true,
|
|
1726
|
-
requireSeparateTestChanges: true,
|
|
1727
|
-
newRegressionTestPath: regressionFile.path,
|
|
1728
|
-
}),
|
|
1729
|
-
{ task, signal: combined },
|
|
1730
|
-
);
|
|
1692
|
+
? await applyModelFileEdits(checkout, result)
|
|
1693
|
+
: await applyModelFileChanges(checkout, result, {
|
|
1694
|
+
requireRegressionTests: true,
|
|
1695
|
+
requireSeparateTestChanges: true,
|
|
1696
|
+
newRegressionTestPath: regressionFile.path,
|
|
1697
|
+
});
|
|
1731
1698
|
} catch (error) {
|
|
1732
1699
|
if (rejectionEvidenceDirectory) {
|
|
1733
1700
|
await mkdir(rejectionEvidenceDirectory, {
|
|
@@ -2419,7 +2386,6 @@ async function settleCompletedResult(client, missionId, workItemId, path, record
|
|
|
2419
2386
|
stage: record.stage,
|
|
2420
2387
|
result: record.result,
|
|
2421
2388
|
leaseToken: record.leaseToken,
|
|
2422
|
-
originalTask: record.originalTask,
|
|
2423
2389
|
});
|
|
2424
2390
|
}
|
|
2425
2391
|
|
|
@@ -2431,7 +2397,6 @@ async function recoverReadyCompletions(
|
|
|
2431
2397
|
signal,
|
|
2432
2398
|
emit,
|
|
2433
2399
|
limit = Infinity,
|
|
2434
|
-
inspector = customerInspector(),
|
|
2435
2400
|
) {
|
|
2436
2401
|
let handled = 0;
|
|
2437
2402
|
for (const name of await readdir(directory)) {
|
|
@@ -2479,7 +2444,6 @@ async function recoverReadyCompletions(
|
|
|
2479
2444
|
await reportBlockedCompletion(record);
|
|
2480
2445
|
continue;
|
|
2481
2446
|
}
|
|
2482
|
-
await inspector.assertAllowed("output", record.result, { task: record.originalTask, signal });
|
|
2483
2447
|
if (record.state === "result_settling")
|
|
2484
2448
|
await settleCompletedResult(client, missionId, workItemId, path, record, signal);
|
|
2485
2449
|
await client.missions.complete(
|
|
@@ -2557,26 +2521,7 @@ export async function runMissionWorker({
|
|
|
2557
2521
|
emit = () => {},
|
|
2558
2522
|
runAgent = runLocalMissionAgent,
|
|
2559
2523
|
adapterSettings = {},
|
|
2560
|
-
inspector,
|
|
2561
|
-
inspectorOptions,
|
|
2562
2524
|
}) {
|
|
2563
|
-
inspector = customerInspector({ inspector, inspectorOptions });
|
|
2564
|
-
const uncheckedRunAgent = runAgent;
|
|
2565
|
-
let originalTask;
|
|
2566
|
-
runAgent = async (input) => {
|
|
2567
|
-
const result = await inspector.runAction(
|
|
2568
|
-
{
|
|
2569
|
-
prompt: input.prompt,
|
|
2570
|
-
checkout: input.checkout,
|
|
2571
|
-
write: input.write ?? false,
|
|
2572
|
-
stage: input.stage,
|
|
2573
|
-
},
|
|
2574
|
-
(snapshot) => uncheckedRunAgent({ ...input, ...snapshot, inspector, task: originalTask }),
|
|
2575
|
-
{ task: originalTask, signal: input.signal },
|
|
2576
|
-
);
|
|
2577
|
-
await inspector.assertAllowed("output", result, { task: originalTask, signal: input.signal });
|
|
2578
|
-
return result;
|
|
2579
|
-
};
|
|
2580
2525
|
if (!UUID.test(missionId)) throw new Error("Provide a valid mission ID.");
|
|
2581
2526
|
missionModelEndpoint(provider, { ollamaUrl, qwenUrl, nimUrl });
|
|
2582
2527
|
model ??=
|
|
@@ -2655,7 +2600,6 @@ export async function runMissionWorker({
|
|
|
2655
2600
|
signal,
|
|
2656
2601
|
emit,
|
|
2657
2602
|
_singleCycle ? 1 : Infinity,
|
|
2658
|
-
inspector,
|
|
2659
2603
|
);
|
|
2660
2604
|
if (_singleCycle && recovered) return { activity: "recovery", handled: recovered };
|
|
2661
2605
|
const reconciled = await recoverKnownLocalTestFailures(
|
|
@@ -2668,7 +2612,6 @@ export async function runMissionWorker({
|
|
|
2668
2612
|
);
|
|
2669
2613
|
if (_singleCycle && reconciled) return { activity: "recovery", handled: reconciled };
|
|
2670
2614
|
const mission = await client.missions.get(missionId, { signal });
|
|
2671
|
-
originalTask = String(mission.goal ?? "");
|
|
2672
2615
|
if (
|
|
2673
2616
|
mission.status === "canceled" ||
|
|
2674
2617
|
mission.status === "completed" ||
|
|
@@ -2713,32 +2656,6 @@ export async function runMissionWorker({
|
|
|
2713
2656
|
)
|
|
2714
2657
|
continue;
|
|
2715
2658
|
processed += 1;
|
|
2716
|
-
try {
|
|
2717
|
-
await inspector.assertAllowed(
|
|
2718
|
-
"input",
|
|
2719
|
-
{
|
|
2720
|
-
goal: mission.goal,
|
|
2721
|
-
desiredOutput: mission.desiredOutput,
|
|
2722
|
-
repository: mission.repository,
|
|
2723
|
-
workItem,
|
|
2724
|
-
events: mission.events ?? [],
|
|
2725
|
-
artifacts: mission.artifacts ?? [],
|
|
2726
|
-
},
|
|
2727
|
-
{ task: originalTask, signal },
|
|
2728
|
-
);
|
|
2729
|
-
} catch (error) {
|
|
2730
|
-
if (!error.code?.startsWith("INSPECTION_")) throw error;
|
|
2731
|
-
if (!reportedAttention.has(workItem.id)) {
|
|
2732
|
-
await recordRecoveryBlocker(workItem, error.code);
|
|
2733
|
-
emit({
|
|
2734
|
-
type: "needs_attention",
|
|
2735
|
-
workItemId: workItem.id,
|
|
2736
|
-
code: error.code,
|
|
2737
|
-
message: "The customer-side inspector blocked this assignment before launch.",
|
|
2738
|
-
});
|
|
2739
|
-
}
|
|
2740
|
-
continue;
|
|
2741
|
-
}
|
|
2742
2659
|
let stage;
|
|
2743
2660
|
try {
|
|
2744
2661
|
stage = stageOf(workItem);
|
|
@@ -3091,16 +3008,6 @@ export async function runMissionWorker({
|
|
|
3091
3008
|
? ["repository.read"]
|
|
3092
3009
|
: [];
|
|
3093
3010
|
const authorizeLocalLaunch = async () => {
|
|
3094
|
-
await inspector.assertAllowed(
|
|
3095
|
-
"action",
|
|
3096
|
-
{
|
|
3097
|
-
stage,
|
|
3098
|
-
actions: repositoryActions,
|
|
3099
|
-
resource: mission.repository?.url ?? null,
|
|
3100
|
-
objective: workItem.objective,
|
|
3101
|
-
},
|
|
3102
|
-
{ task: originalTask, signal: combined },
|
|
3103
|
-
);
|
|
3104
3011
|
// Renewal rechecks mission state, current credential, membership, and the lease.
|
|
3105
3012
|
await client.missions.renew(missionId, workItem.id, claim.leaseToken, {
|
|
3106
3013
|
signal: combined,
|
|
@@ -3149,15 +3056,6 @@ export async function runMissionWorker({
|
|
|
3149
3056
|
for (const repositoryAction of repositoryActions) {
|
|
3150
3057
|
if (!mission.repository)
|
|
3151
3058
|
throw new Error("This assignment needs a selected repository.");
|
|
3152
|
-
await inspector.assertAllowed(
|
|
3153
|
-
"action",
|
|
3154
|
-
{
|
|
3155
|
-
action: repositoryAction,
|
|
3156
|
-
resource: mission.repository.url,
|
|
3157
|
-
objective: workItem.objective,
|
|
3158
|
-
},
|
|
3159
|
-
{ task: originalTask, signal: combined },
|
|
3160
|
-
);
|
|
3161
3059
|
const reservation =
|
|
3162
3060
|
(prior?.reservedEffects ?? []).find(
|
|
3163
3061
|
(effect) =>
|
|
@@ -3199,16 +3097,6 @@ export async function runMissionWorker({
|
|
|
3199
3097
|
effectIds,
|
|
3200
3098
|
});
|
|
3201
3099
|
for (const effectId of effectIds) {
|
|
3202
|
-
await inspector.assertAllowed(
|
|
3203
|
-
"action",
|
|
3204
|
-
{
|
|
3205
|
-
effectId,
|
|
3206
|
-
actions: repositoryActions,
|
|
3207
|
-
resource: mission.repository.url,
|
|
3208
|
-
objective: workItem.objective,
|
|
3209
|
-
},
|
|
3210
|
-
{ task: originalTask, signal: combined },
|
|
3211
|
-
);
|
|
3212
3100
|
const dispatch = await client.missions.dispatchEffect(
|
|
3213
3101
|
missionId,
|
|
3214
3102
|
effectId,
|
|
@@ -3548,7 +3436,6 @@ export async function runMissionWorker({
|
|
|
3548
3436
|
state: "result_settling",
|
|
3549
3437
|
stage,
|
|
3550
3438
|
result,
|
|
3551
|
-
originalTask,
|
|
3552
3439
|
leaseToken: claim.leaseToken,
|
|
3553
3440
|
effectIds: [...effectIds],
|
|
3554
3441
|
repositoryUrl: mission.repository.url,
|
|
@@ -3569,15 +3456,10 @@ export async function runMissionWorker({
|
|
|
3569
3456
|
state: "ready",
|
|
3570
3457
|
stage,
|
|
3571
3458
|
result,
|
|
3572
|
-
originalTask,
|
|
3573
3459
|
leaseToken: claim.leaseToken,
|
|
3574
3460
|
});
|
|
3575
3461
|
}
|
|
3576
3462
|
}
|
|
3577
|
-
await inspector.assertAllowed("output", result, {
|
|
3578
|
-
task: originalTask,
|
|
3579
|
-
signal: combined,
|
|
3580
|
-
});
|
|
3581
3463
|
await client.missions.complete(
|
|
3582
3464
|
missionId,
|
|
3583
3465
|
workItem.id,
|
package/bin/model-adapter.mjs
CHANGED
|
@@ -7,11 +7,32 @@ import { installedClientInvocation, localClientEnvironment } from "./platform-cl
|
|
|
7
7
|
import { WorkerError } from "./worker-core/errors.mjs";
|
|
8
8
|
|
|
9
9
|
export function providerFailureCode(text) {
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
10
|
+
if (
|
|
11
|
+
/unexpected argument|unknown option|unrecognized option|no such option|unknown flag/i.test(text)
|
|
12
|
+
)
|
|
13
|
+
return "AGENT_CLI_INCOMPATIBLE";
|
|
14
|
+
if (
|
|
15
|
+
/not logged in|authentication required|invalid[_ ]api[_ ]key|unauthorized|refresh token.*(?:expired|reused)|sign in.*required/i.test(
|
|
16
|
+
text,
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
return "AGENT_AUTH_REQUIRED";
|
|
20
|
+
if (
|
|
21
|
+
/usage limit|rate limit|quota.*(?:exceeded|insufficient)|insufficient_quota|credit balance/i.test(
|
|
22
|
+
text,
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
return "AGENT_QUOTA_EXCEEDED";
|
|
26
|
+
if (
|
|
27
|
+
/sandbox.*(?:setup failed|not available|not configured|unsupported|error)|CreateProcessAsUser|error 1385/i.test(
|
|
28
|
+
text,
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
return "AGENT_SANDBOX_UNAVAILABLE";
|
|
32
|
+
if (
|
|
33
|
+
/model.*(?:not found|not supported|does not exist|not have access)|model_not_found/i.test(text)
|
|
34
|
+
)
|
|
35
|
+
return "AGENT_MODEL_UNAVAILABLE";
|
|
15
36
|
return "AGENT_EXIT_FAILED";
|
|
16
37
|
}
|
|
17
38
|
|
|
@@ -123,7 +144,8 @@ export async function externalModelReply({
|
|
|
123
144
|
];
|
|
124
145
|
parseResult = (output) => {
|
|
125
146
|
const result = JSON.parse(output);
|
|
126
|
-
if (result.is_error || result.subtype !== "success")
|
|
147
|
+
if (result.is_error || result.subtype !== "success")
|
|
148
|
+
throw new WorkerError(providerFailureCode(String(result.result ?? "")));
|
|
127
149
|
return {
|
|
128
150
|
status: "completed",
|
|
129
151
|
output: result.structured_output
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { pause } from "./network.mjs";
|
|
2
|
+
|
|
3
|
+
export function transientNetworkError(error) {
|
|
4
|
+
return (
|
|
5
|
+
error?.status === 429 ||
|
|
6
|
+
error?.status >= 500 ||
|
|
7
|
+
["NETWORK_BUSY", "NETWORK_ERROR", "TIMEOUT"].includes(error?.code)
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Only use for reads or mutations protected by a durable idempotency key.
|
|
12
|
+
export async function retryNetwork(
|
|
13
|
+
operation,
|
|
14
|
+
{ signal, onRetry = () => {}, delayMs = 1000, attempts = 4 } = {},
|
|
15
|
+
) {
|
|
16
|
+
for (let attempt = 0; ; attempt++) {
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
try {
|
|
19
|
+
return await operation();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (signal?.aborted || !transientNetworkError(error) || attempt + 1 >= attempts) throw error;
|
|
22
|
+
onRetry(error);
|
|
23
|
+
await pause(Math.min(delayMs * 2 ** attempt, 10000), signal);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/bin/network-runtime.mjs
CHANGED
|
@@ -5,6 +5,7 @@ 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";
|
|
9
10
|
import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
|
|
10
11
|
import {
|
|
@@ -406,23 +407,38 @@ async function generate({
|
|
|
406
407
|
new Error(
|
|
407
408
|
"A previous provider turn has an uncertain outcome. Inspect its private journal before retrying.",
|
|
408
409
|
),
|
|
409
|
-
{
|
|
410
|
+
{
|
|
411
|
+
code: state.externalFailure ?? "AGENT_OUTCOME_UNCERTAIN",
|
|
412
|
+
externalOutcomeUncertain: true,
|
|
413
|
+
},
|
|
410
414
|
);
|
|
411
415
|
state.externalPending = true;
|
|
412
416
|
await journal.save(key, state);
|
|
413
417
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
+
}
|
|
426
442
|
await settings.inspector.assertAllowed("output", response, { task: text, signal });
|
|
427
443
|
delete state.externalPending;
|
|
428
444
|
state.messages.push(response);
|
|
@@ -575,10 +591,15 @@ export async function runRuntime(settings) {
|
|
|
575
591
|
let fatalError;
|
|
576
592
|
let diagnosticsSupported = false;
|
|
577
593
|
let diagnostics = { phase: "starting" };
|
|
578
|
-
const packageVersion = JSON.parse(
|
|
594
|
+
const packageVersion = JSON.parse(
|
|
595
|
+
await readFile(new URL("../package.json", import.meta.url), "utf8"),
|
|
596
|
+
).version;
|
|
579
597
|
const report = (error, context = {}) => {
|
|
580
|
-
diagnostics = {
|
|
581
|
-
|
|
598
|
+
diagnostics = {
|
|
599
|
+
phase: "blocked",
|
|
600
|
+
errorCode: runtimeErrorCode(error),
|
|
601
|
+
...(typeof context.messageId === "string" ? { messageId: context.messageId } : {}),
|
|
602
|
+
};
|
|
582
603
|
if (settings.once) process.exitCode = 1;
|
|
583
604
|
emit({
|
|
584
605
|
type: "runtime_error",
|
|
@@ -599,9 +620,21 @@ export async function runRuntime(settings) {
|
|
|
599
620
|
try {
|
|
600
621
|
const beat = async () => {
|
|
601
622
|
const result = await client.runtime.heartbeat(
|
|
602
|
-
{
|
|
603
|
-
|
|
604
|
-
|
|
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
|
+
},
|
|
605
638
|
{ signal },
|
|
606
639
|
);
|
|
607
640
|
diagnosticsSupported = result?.diagnosticsSupported === true;
|
|
@@ -642,6 +675,7 @@ export async function runRuntime(settings) {
|
|
|
642
675
|
pollMs: settings.pollMs,
|
|
643
676
|
concurrency: settings.messageConcurrency ?? 2,
|
|
644
677
|
once: settings.once,
|
|
678
|
+
onPollError: (error) => report(error),
|
|
645
679
|
shouldContinue: () => !settings.once || !humanDone,
|
|
646
680
|
messages: (signal) =>
|
|
647
681
|
client.messages.iterate({ direction: "inbound", unread: true, limit: 100 }, { signal }),
|
|
@@ -720,10 +754,12 @@ export async function runRuntime(settings) {
|
|
|
720
754
|
} catch (error) {
|
|
721
755
|
if (!signal.aborted) report(error, { messageId: message.id });
|
|
722
756
|
if (error.status === 401) throw error;
|
|
723
|
-
|
|
757
|
+
// Retry this durable message on the next poll without stopping other turns.
|
|
758
|
+
if (transientNetworkError(error)) return;
|
|
724
759
|
if (
|
|
725
760
|
(error.status >= 400 && error.status < 500) ||
|
|
726
|
-
error.code === "AGENT_OUTCOME_UNCERTAIN"
|
|
761
|
+
error.code === "AGENT_OUTCOME_UNCERTAIN" ||
|
|
762
|
+
error.externalOutcomeUncertain
|
|
727
763
|
)
|
|
728
764
|
await journal.save(`message-blocker:${message.id}`, {
|
|
729
765
|
code: error.code ?? "MESSAGE_BLOCKED",
|
|
@@ -737,7 +773,20 @@ export async function runRuntime(settings) {
|
|
|
737
773
|
}
|
|
738
774
|
});
|
|
739
775
|
do {
|
|
740
|
-
|
|
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
|
+
}
|
|
741
790
|
for (const command of commands.data) {
|
|
742
791
|
if (signal.aborted) break;
|
|
743
792
|
let claim;
|
|
@@ -745,6 +794,10 @@ export async function runRuntime(settings) {
|
|
|
745
794
|
claim = await client.chatCommands.claim(command.id, { signal });
|
|
746
795
|
} catch (error) {
|
|
747
796
|
if (error.status === 409) continue;
|
|
797
|
+
if (transientNetworkError(error)) {
|
|
798
|
+
report(error);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
748
801
|
throw error;
|
|
749
802
|
}
|
|
750
803
|
const leaseStop = new AbortController();
|
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
export const RUNTIME_ERROR_CODES = new Set([
|
|
2
|
-
"AGENT_START_FAILED",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
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",
|
|
10
35
|
]);
|
|
11
36
|
export function runtimeErrorCode(error) {
|
|
12
37
|
return RUNTIME_ERROR_CODES.has(error?.code) ? error.code : "INTERNAL_ERROR";
|
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._:-]*$/;
|
|
@@ -26,7 +26,11 @@ const SAFE_CODES = new Set([
|
|
|
26
26
|
"INVALID_PROMPT",
|
|
27
27
|
"AGENT_START_FAILED",
|
|
28
28
|
"AGENT_EXIT_FAILED",
|
|
29
|
-
"AGENT_CLI_INCOMPATIBLE",
|
|
29
|
+
"AGENT_CLI_INCOMPATIBLE",
|
|
30
|
+
"AGENT_AUTH_REQUIRED",
|
|
31
|
+
"AGENT_QUOTA_EXCEEDED",
|
|
32
|
+
"AGENT_SANDBOX_UNAVAILABLE",
|
|
33
|
+
"AGENT_MODEL_UNAVAILABLE",
|
|
30
34
|
"AGENT_REPORTED_FAILURE",
|
|
31
35
|
"UNKNOWN_HTTP_OUTCOME",
|
|
32
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));
|