totemlayer 0.1.20 → 0.1.24

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.
@@ -8,6 +8,7 @@ import { runInboxQueue } from "./inbox-queue.mjs";
8
8
  import { retryNetwork, transientNetworkError } from "./network-retry.mjs";
9
9
  import { customerInspector, inspectNetworkMessage } from "./local-inspector.mjs";
10
10
  import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
11
+ import { queueCodexChatMessage } from "./codex-chat-bridge.mjs";
11
12
  import {
12
13
  externalModelReply,
13
14
  isExternalModelProvider,
@@ -607,7 +608,25 @@ export async function runRuntime(settings) {
607
608
  const directory = join(settings.stateDir ?? join(homedir(), ".totem", "runtime"), scope);
608
609
  const release = await runtimeLock(directory);
609
610
  const journal = new Journal(directory);
611
+ const runId = settings.runId ?? randomUUID();
612
+ const verify = typeof client.runtime?.start === "function";
613
+ if (settings.verifyReadiness && !verify) {
614
+ await release();
615
+ throw Object.assign(new Error("Upgrade the Totem SDK to test and authorize this listener."), {
616
+ code: "READINESS_UNSUPPORTED",
617
+ });
618
+ }
619
+ if (verify && settings.codexThreadId) {
620
+ await release();
621
+ throw Object.assign(
622
+ new Error(
623
+ "Existing Codex chat queueing cannot verify a controlled receive/wake/reply round trip. Configure the isolated Codex adapter instead.",
624
+ ),
625
+ { code: "READINESS_UNSUPPORTED" },
626
+ );
627
+ }
610
628
  let heartbeat;
629
+ let probeFailed = false;
611
630
  let inbox;
612
631
  let humanDone = false;
613
632
  let fatalError;
@@ -617,14 +636,15 @@ export async function runRuntime(settings) {
617
636
  await readFile(new URL("../package.json", import.meta.url), "utf8"),
618
637
  ).version;
619
638
  const report = (error, context = {}) => {
639
+ const reconnecting = !context.messageId && transientNetworkError(error);
620
640
  diagnostics = {
621
- phase: "blocked",
641
+ phase: reconnecting ? "reconnecting" : "blocked",
622
642
  errorCode: runtimeErrorCode(error),
623
643
  ...(typeof context.messageId === "string" ? { messageId: context.messageId } : {}),
624
644
  };
625
645
  if (settings.once) process.exitCode = 1;
626
646
  emit({
627
- type: "runtime_error",
647
+ type: reconnecting ? "runtime_reconnecting" : "runtime_error",
628
648
  ...context,
629
649
  error: {
630
650
  code: error.code ?? "RUNTIME_FAILED",
@@ -640,10 +660,56 @@ export async function runRuntime(settings) {
640
660
  });
641
661
  };
642
662
  try {
663
+ if (verify) {
664
+ await client.runtime.start(
665
+ {
666
+ runId,
667
+ provider: settings.provider === "ollama" ? "ollama-local" : settings.provider,
668
+ hostKind: settings.hostKind ?? "local",
669
+ },
670
+ { signal },
671
+ );
672
+ try {
673
+ const probe = await client.runtime.probe(runId, { signal });
674
+ if (
675
+ probe.runId !== runId ||
676
+ probe.protocol !== "totem-readiness-v1" ||
677
+ !/^[a-f0-9]{64}$/.test(probe.challenge)
678
+ )
679
+ throw new Error("The readiness challenge is invalid.");
680
+ if (!probe.completed) {
681
+ const answer = await generate({
682
+ client,
683
+ identity,
684
+ journal,
685
+ key: `readiness:${runId}:${probe.challenge}`,
686
+ text: `Controlled readiness check. Reply with exactly TOTEM_READY:${probe.challenge} and no other text. No tools, files, external actions or business work are authorized.`,
687
+ history: [],
688
+ settings: { ...settings, system: "", allowAgents: [], modelTimeoutMs: 30000 },
689
+ signal,
690
+ allowTools: false,
691
+ });
692
+ await client.runtime.replyProbe({ runId, reply: String(answer).trim() }, { signal });
693
+ }
694
+ emit({ type: "runtime_ready", data: { runId, provider: settings.provider, ready: true } });
695
+ } catch (error) {
696
+ probeFailed = true;
697
+ await client.runtime
698
+ .heartbeat({ runId, state: "error", errorCode: "READINESS_PROBE_FAILED" }, { signal })
699
+ .catch(() => {});
700
+ throw Object.assign(
701
+ new Error(
702
+ "The bounded receive/wake/reply check failed. Check the selected adapter and its authorization, then restart this listener.",
703
+ ),
704
+ { code: "READINESS_PROBE_FAILED", cause: error },
705
+ );
706
+ }
707
+ }
643
708
  const beat = async () => {
644
709
  const result = await client.runtime.heartbeat(
645
710
  {
646
711
  label: `${settings.provider}:${settings.model}`.slice(0, 100),
712
+ ...(verify ? { runId, state: "online" } : {}),
647
713
  ...(diagnosticsSupported
648
714
  ? {
649
715
  diagnostics: {
@@ -671,7 +737,7 @@ export async function runRuntime(settings) {
671
737
  } catch (error) {
672
738
  if (signal.aborted) break;
673
739
  report(error);
674
- if (error.status === 401 || error.status === 403) {
740
+ if (error.status === 401 || error.status === 403 || error.code === "RUNTIME_SUPERSEDED") {
675
741
  fatalError = error;
676
742
  shutdown.abort();
677
743
  break;
@@ -687,6 +753,7 @@ export async function runRuntime(settings) {
687
753
  model: settings.model,
688
754
  allowedAgents: settings.allowAgents,
689
755
  inspection: settings.inspector.mode,
756
+ ready: verify,
690
757
  },
691
758
  });
692
759
  diagnostics = { phase: "waiting" };
@@ -699,8 +766,17 @@ export async function runRuntime(settings) {
699
766
  once: settings.once,
700
767
  onPollError: (error) => report(error),
701
768
  shouldContinue: () => !settings.once || !humanDone,
702
- messages: (signal) =>
703
- client.messages.iterate({ direction: "inbound", unread: true, limit: 100 }, { signal }),
769
+ messages: async function* (signal) {
770
+ for await (const message of client.messages.iterate(
771
+ { direction: "inbound", unread: true, limit: 100 },
772
+ { signal },
773
+ ))
774
+ yield message;
775
+ if (diagnostics.phase === "reconnecting") {
776
+ diagnostics = { phase: "waiting" };
777
+ emit({ type: "runtime_resumed" });
778
+ }
779
+ },
704
780
  eligible: async (message) => {
705
781
  const blocker = await journal.read(`message-blocker:${message.id}`);
706
782
  // Older runtimes persisted inspector outages as permanent blockers.
@@ -711,6 +787,26 @@ export async function runRuntime(settings) {
711
787
  try {
712
788
  diagnostics = { phase: "working", messageId: message.id };
713
789
  emit({ type: "message_started", data: { messageId: message.id } });
790
+ if (settings.codexThreadId) {
791
+ const delivery = await queueCodexChatMessage({
792
+ settings,
793
+ client,
794
+ identity,
795
+ journal,
796
+ message,
797
+ signal,
798
+ });
799
+ if (delivery.state === "uncertain") {
800
+ report({ code: "AGENT_OUTCOME_UNCERTAIN" }, { messageId: message.id });
801
+ } else {
802
+ diagnostics = { phase: "waiting" };
803
+ emit({
804
+ type: delivery.state === "confirmed" ? "message_handled" : "chat_queued",
805
+ data: { messageId: message.id, threadId: settings.codexThreadId },
806
+ });
807
+ }
808
+ return;
809
+ }
714
810
  await inspectNetworkMessage(settings.inspector, client, message, { signal });
715
811
  const continuation = autonomousReplyMetadata(message, settings, identity.address);
716
812
  if (message.inReplyToId && !continuation) {
@@ -818,6 +914,14 @@ export async function runRuntime(settings) {
818
914
  shutdown.abort();
819
915
  }
820
916
  });
917
+ // An owner-bound desktop chat consumes its own prompts. It does not need
918
+ // the independent Totem website-chat command queue or a fresh model turn.
919
+ if (settings.codexThreadId) {
920
+ humanDone = true;
921
+ await inbox;
922
+ if (fatalError) throw fatalError;
923
+ return;
924
+ }
821
925
  do {
822
926
  let commands;
823
927
  try {
@@ -851,7 +955,8 @@ export async function runRuntime(settings) {
851
955
  let leaseError;
852
956
  const renewal = (async () => {
853
957
  while (!commandSignal.aborted) {
854
- await pause(30000, commandSignal);
958
+ // Observe human interruption promptly without repeating provider execution.
959
+ await pause(5000, commandSignal);
855
960
  try {
856
961
  await client.chatCommands.renew(command.id, claim.leaseToken, {
857
962
  signal: commandSignal,
@@ -960,6 +1065,10 @@ export async function runRuntime(settings) {
960
1065
  shutdown.abort();
961
1066
  await heartbeat;
962
1067
  await inbox;
1068
+ if (verify && !probeFailed)
1069
+ await client.runtime
1070
+ .heartbeat({ runId, state: "offline" }, { signal: AbortSignal.timeout(2000) })
1071
+ .catch(() => {});
963
1072
  await release();
964
1073
  emit({ type: "runtime_stopped", data: { agent: identity.address } });
965
1074
  }
@@ -32,6 +32,10 @@ export const RUNTIME_ERROR_CODES = new Set([
32
32
  "AGENT_IDENTITY_MISMATCH",
33
33
  "MODEL_ERROR",
34
34
  "MAX_STEPS",
35
+ "READINESS_UNSUPPORTED",
36
+ "READINESS_PROBE_FAILED",
37
+ "RUNTIME_AUTHORIZATION_REJECTED",
38
+ "RUNTIME_SUPERSEDED",
35
39
  "INTERNAL_ERROR",
36
40
  ]);
37
41
  export function runtimeErrorCode(error) {
@@ -251,6 +251,7 @@ export async function installRuntimeService(runtime, env = process.env, options
251
251
  "TOTEM_INSPECTOR_MODEL",
252
252
  "TOTEM_INSPECTOR_TIMEOUT_MS",
253
253
  "TOTEM_PROVIDER",
254
+ "TOTEM_RUNTIME_HOST_KIND",
254
255
  "TOTEM_MODEL",
255
256
  "TOTEM_BASE_URL",
256
257
  "TOTEM_AGENT_ID",
@@ -0,0 +1,236 @@
1
+ import type { JsonValue, RequestOptions, TotemOptions } from "./types.js";
2
+ export interface TotemActionsOptions extends Omit<TotemOptions, "apiKey" | "address"> {
3
+ /** Explicitly separate scoped Action Checks credential; never a Network tn_ key. */
4
+ actionChecksApiKey: string;
5
+ }
6
+ export interface AssistantCheckOptions extends RequestOptions {
7
+ /** Same scoped credential used by the separate TotemActions client. */
8
+ actionChecksApiKey: string;
9
+ }
10
+ export interface AssistantActionCheck {
11
+ suite: ActionCheckSuite;
12
+ /** Use this exact returned action for grant issuance and execution. */
13
+ action: ActionProposal;
14
+ missionId: string;
15
+ workItemId: string;
16
+ executionAuthorized: false;
17
+ checksUrl: string;
18
+ }
19
+ export interface AssistantActionReceipt {
20
+ id: string;
21
+ outcome: ActionExecutionReceipt["outcome"];
22
+ reconciliationState: ActionExecutionReceipt["reconciliationStatus"];
23
+ executorAdapter: string;
24
+ completedAt: string | null;
25
+ }
26
+ export interface AssistantActionCheckList {
27
+ checks: Array<{
28
+ id: string;
29
+ /** Original private handoff suite, verified through immutable rerun lineage. */
30
+ handoffSuiteId?: string;
31
+ actionDigest: string;
32
+ status: ActionCheckSuite["status"];
33
+ conclusion: ActionSuiteConclusion | null;
34
+ expiresAt: string;
35
+ staleAt: string | null;
36
+ executionReceipts: AssistantActionReceipt[];
37
+ checksUrl: string;
38
+ }>;
39
+ }
40
+ export interface AssistantActionReceiptResult {
41
+ receipt: AssistantActionReceipt & {
42
+ receiptDigest: string;
43
+ };
44
+ missionId: string;
45
+ workItemId: string;
46
+ }
47
+ export type ActionSideEffectClass = "READ_ONLY" | "DATA_EGRESS" | "EXTERNAL_COMMUNICATION" | "FINANCIAL" | "DESTRUCTIVE" | "AUTHORITY_CHANGE" | "SECURITY_OPERATION";
48
+ export type ActionReversibility = "REVERSIBLE" | "COMPENSATABLE" | "IRREVERSIBLE" | "UNKNOWN";
49
+ export type ActionDataClassification = "PUBLIC" | "INTERNAL" | "CONFIDENTIAL" | "PERSONAL" | "FINANCIAL" | "AUTHENTICATION" | "SOURCE_CODE" | "SECRET";
50
+ export type ActionExecutorAdapter = "NOOP_READ" | "LOOPBACK_ECHO" | "FAKE_LEDGER" | "TEMP_FIXTURE_DELETE" | "SUPPLYCO_QUOTE" | "CREDENTIAL_ISSUER" | "RECEIVER_API_KEY" | "CREDENTIAL_VERIFY";
51
+ export interface ActionFinancialContext {
52
+ actionType: "PAYMENT" | "PURCHASE" | "TRANSFER" | "REFUND" | "WITHDRAWAL" | "DEPOSIT" | "TRADE" | "SUBSCRIPTION" | "INVOICE_PAYMENT" | "CRYPTO" | "QUOTE_REQUEST";
53
+ movesValue: boolean;
54
+ amount: number | null;
55
+ currency: string | null;
56
+ sourceAccount: string | null;
57
+ beneficiary: string | null;
58
+ destinationAccount: string | null;
59
+ beneficiaryIsNew: boolean | null;
60
+ reversible: boolean | null;
61
+ }
62
+ /** Exact proposed action. The server derives tenant authority from the scoped credential. */
63
+ export interface ActionProposal {
64
+ version: "1";
65
+ /** Totem identity-agent ID from assistant.context().agent.identityAgentId. */
66
+ agentId: string;
67
+ credentialId?: string | null;
68
+ automationRunId?: string | null;
69
+ toolName: string;
70
+ actionType: string;
71
+ sideEffectClass: ActionSideEffectClass;
72
+ executorAdapter: ActionExecutorAdapter;
73
+ target: {
74
+ resourceType: string;
75
+ resourceId?: string | null;
76
+ ownedTemporaryFixture?: boolean;
77
+ };
78
+ destination?: {
79
+ protocol: "local:" | "http:" | "https:";
80
+ host: string;
81
+ port?: number | null;
82
+ path: string;
83
+ tenant?: string | null;
84
+ redirects?: "DENY" | "SAME_ORIGIN";
85
+ } | null;
86
+ data: {
87
+ classifications?: ActionDataClassification[];
88
+ fields?: string[];
89
+ payload?: JsonValue;
90
+ minimized?: boolean;
91
+ byteLength?: number | null;
92
+ };
93
+ financial?: ActionFinancialContext | null;
94
+ reversibility: ActionReversibility;
95
+ justification: string;
96
+ provenance: Array<{
97
+ kind: "AUTHENTICATED_USER" | "TRUSTED_POLICY" | "INTERNAL_DATA" | "TOOL_OUTPUT" | "WEB_CONTENT" | "EMAIL" | "MESSAGE" | "OTHER_AGENT" | "UNTRUSTED_TEXT";
98
+ /** A caller-provided flag is advisory; the server derives source trust. */
99
+ trusted: boolean;
100
+ reference?: string | null;
101
+ content?: string | null;
102
+ }>;
103
+ idempotencyKey: string;
104
+ executionAttemptId: string;
105
+ expiresAt: string;
106
+ payload?: JsonValue;
107
+ }
108
+ export type ActionCheckConclusion = "PASS" | "WARN" | "REQUIRE_APPROVAL" | "BLOCK" | "ERROR" | "SKIPPED";
109
+ export type ActionSuiteConclusion = "ALLOW" | "ALLOW_WITH_WARNINGS" | "REQUIRES_APPROVAL" | "BLOCK" | "ERROR";
110
+ export type ActionRiskLevel = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
111
+ export interface ActionExecutionReceipt {
112
+ id: string;
113
+ outcome: "SUCCEEDED" | "FAILED" | "UNKNOWN" | "COMPENSATED";
114
+ executorAdapter: string;
115
+ externalResponseSummary: string | null;
116
+ idempotencyStatus: "RECORDED";
117
+ reconciliationStatus: "NOT_REQUIRED" | "PENDING" | "CONFIRMED" | "FAILED";
118
+ compensationStatus: "RECORDED" | "NOT_REQUIRED";
119
+ createdAt: string;
120
+ executedAt: string | null;
121
+ completedAt: string | null;
122
+ }
123
+ /** Redacted server view; do not reconstruct an executable action from this projection. */
124
+ export interface ActionCheckSuite {
125
+ id: string;
126
+ status: "QUEUED" | "RUNNING" | "COMPLETED" | "STALE";
127
+ conclusion: ActionSuiteConclusion | null;
128
+ riskLevel: ActionRiskLevel;
129
+ explanation: string | null;
130
+ policyVersion: string;
131
+ requiredProviders: string[];
132
+ actionDigest: string;
133
+ createdAt: string;
134
+ updatedAt: string;
135
+ startedAt: string | null;
136
+ completedAt: string | null;
137
+ expiresAt: string;
138
+ staleAt: string | null;
139
+ staleReason: string | null;
140
+ supersededBySuiteId: string | null;
141
+ action: {
142
+ id: string;
143
+ agentId: string;
144
+ agent: {
145
+ id: string;
146
+ name: string;
147
+ } | null;
148
+ toolName: string;
149
+ actionType: string;
150
+ sideEffectClass: ActionSideEffectClass;
151
+ target: {
152
+ resourceType: string;
153
+ resourceId: string | null;
154
+ };
155
+ destination: {
156
+ protocol: string;
157
+ host: string;
158
+ port: number | null;
159
+ path: string;
160
+ tenant: string | null;
161
+ } | null;
162
+ dataClassifications: JsonValue;
163
+ financial: {
164
+ actionType: string;
165
+ movesValue: boolean | null;
166
+ amount: number | null;
167
+ currency: string | null;
168
+ beneficiary: string | null;
169
+ destinationAccount: string | null;
170
+ } | null;
171
+ reversibility: ActionReversibility;
172
+ justification: string;
173
+ expiresAt: string;
174
+ };
175
+ runs: Array<{
176
+ id: string;
177
+ providerName: string;
178
+ providerVersion: string;
179
+ required: boolean;
180
+ status: "QUEUED" | "RUNNING" | "COMPLETED" | "TIMED_OUT";
181
+ conclusion: ActionCheckConclusion | null;
182
+ reasonCode: string | null;
183
+ summary: string | null;
184
+ findings: Array<{
185
+ id: string;
186
+ severity: "INFO" | ActionRiskLevel;
187
+ category: string;
188
+ reasonCode: string;
189
+ explanation: string;
190
+ fieldReference: string | null;
191
+ sensitiveFingerprint: string | null;
192
+ remediation: string | null;
193
+ evidenceSource: string;
194
+ confidence: number;
195
+ }>;
196
+ startedAt: string | null;
197
+ completedAt: string | null;
198
+ timedOutAt: string | null;
199
+ }>;
200
+ approval: {
201
+ id: string;
202
+ status: "APPROVED" | "REJECTED" | "REVOKED" | "EXPIRED";
203
+ reason: string | null;
204
+ approver: {
205
+ id: string;
206
+ name?: string | null;
207
+ email?: string;
208
+ };
209
+ createdAt: string;
210
+ expiresAt: string;
211
+ rejectedAt: string | null;
212
+ revokedAt: string | null;
213
+ } | null;
214
+ grant: {
215
+ id: string;
216
+ status: "ACTIVE" | "CONSUMED" | "REVOKED" | "EXPIRED";
217
+ createdAt: string;
218
+ expiresAt: string;
219
+ consumedAt: string | null;
220
+ invalidatedAt: string | null;
221
+ } | null;
222
+ receipt: ActionExecutionReceipt | null;
223
+ confirmationPhrase: string | null;
224
+ demoAction: ActionProposal | null;
225
+ }
226
+ export interface ActionExecutionGrant {
227
+ /** One-use secret, bound to the checked action and issuing principal. */
228
+ token: string;
229
+ expiresAt: string;
230
+ suite: ActionCheckSuite;
231
+ }
232
+ export interface CheckedExecutionResult {
233
+ suite: ActionCheckSuite;
234
+ receipt: ActionExecutionReceipt | null;
235
+ receiptId: string;
236
+ }
@@ -0,0 +1,19 @@
1
+ import type { RequestOptions } from "./types.js";
2
+ import type { ActionCheckSuite, ActionExecutionGrant, ActionProposal, CheckedExecutionResult, TotemActionsOptions } from "./action-types.js";
3
+ export * from "./action-types.js";
4
+ /**
5
+ * Separately authenticated Action Checks client. Human approval stays in Totem's UI.
6
+ * Writes are never automatically replayed, even when the action has an idempotency key.
7
+ * After an ambiguous execution failure, use get() to inspect the receipt before proceeding.
8
+ */
9
+ export declare class TotemActions {
10
+ #private;
11
+ constructor(options: TotemActionsOptions);
12
+ create(action: ActionProposal, options?: RequestOptions): Promise<ActionCheckSuite>;
13
+ get(suiteId: string, options?: RequestOptions): Promise<ActionCheckSuite>;
14
+ issueGrant(suiteId: string, action: ActionProposal, options?: RequestOptions): Promise<ActionExecutionGrant>;
15
+ execute(suiteId: string, input: {
16
+ action: ActionProposal;
17
+ grant: string;
18
+ }, options?: RequestOptions): Promise<CheckedExecutionResult>;
19
+ }
@@ -0,0 +1,30 @@
1
+ export declare class TotemError extends Error {
2
+ readonly code: string;
3
+ readonly status?: number;
4
+ readonly requestId?: string;
5
+ readonly retryAfterSeconds?: number;
6
+ constructor(message: string, options: {
7
+ code: string;
8
+ status?: number;
9
+ requestId?: string;
10
+ retryAfterSeconds?: number;
11
+ cause?: unknown;
12
+ });
13
+ }
14
+ export declare class TotemAuthenticationError extends TotemError {
15
+ }
16
+ export declare class TotemPermissionError extends TotemError {
17
+ }
18
+ export declare class TotemNotFoundError extends TotemError {
19
+ }
20
+ export declare class TotemConflictError extends TotemError {
21
+ }
22
+ export declare class TotemRateLimitError extends TotemError {
23
+ }
24
+ export declare class TotemTimeoutError extends TotemError {
25
+ }
26
+ export declare class TotemNetworkError extends TotemError {
27
+ }
28
+ export declare class TotemCanceledError extends TotemError {
29
+ }
30
+ export declare function apiError(status: number, body: unknown, requestId?: string): TotemError;
package/bin/sdk/errors.js CHANGED
@@ -2,12 +2,14 @@ export class TotemError extends Error {
2
2
  code;
3
3
  status;
4
4
  requestId;
5
+ retryAfterSeconds;
5
6
  constructor(message, options) {
6
7
  super(message, { cause: options.cause });
7
8
  this.name = new.target.name;
8
9
  this.code = options.code;
9
10
  this.status = options.status;
10
11
  this.requestId = options.requestId;
12
+ this.retryAfterSeconds = options.retryAfterSeconds;
11
13
  }
12
14
  }
13
15
  export class TotemAuthenticationError extends TotemError {
@@ -50,5 +52,16 @@ export function apiError(status, body, requestId) {
50
52
  : status === 429
51
53
  ? TotemRateLimitError
52
54
  : TotemError;
53
- return new Constructor(message, { code, status, requestId: id });
55
+ const details = "details" in error ? error.details : undefined;
56
+ const retryAfter = details && typeof details === "object" && "retryAfterSeconds" in details
57
+ ? details.retryAfterSeconds
58
+ : undefined;
59
+ const retryAfterSeconds = status === 429 &&
60
+ typeof retryAfter === "number" &&
61
+ Number.isSafeInteger(retryAfter) &&
62
+ retryAfter > 0 &&
63
+ retryAfter <= 86400
64
+ ? retryAfter
65
+ : undefined;
66
+ return new Constructor(message, { code, status, requestId: id, retryAfterSeconds });
54
67
  }