totemlayer 0.1.21 → 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.
@@ -608,7 +608,25 @@ export async function runRuntime(settings) {
608
608
  const directory = join(settings.stateDir ?? join(homedir(), ".totem", "runtime"), scope);
609
609
  const release = await runtimeLock(directory);
610
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
+ }
611
628
  let heartbeat;
629
+ let probeFailed = false;
612
630
  let inbox;
613
631
  let humanDone = false;
614
632
  let fatalError;
@@ -618,14 +636,15 @@ export async function runRuntime(settings) {
618
636
  await readFile(new URL("../package.json", import.meta.url), "utf8"),
619
637
  ).version;
620
638
  const report = (error, context = {}) => {
639
+ const reconnecting = !context.messageId && transientNetworkError(error);
621
640
  diagnostics = {
622
- phase: "blocked",
641
+ phase: reconnecting ? "reconnecting" : "blocked",
623
642
  errorCode: runtimeErrorCode(error),
624
643
  ...(typeof context.messageId === "string" ? { messageId: context.messageId } : {}),
625
644
  };
626
645
  if (settings.once) process.exitCode = 1;
627
646
  emit({
628
- type: "runtime_error",
647
+ type: reconnecting ? "runtime_reconnecting" : "runtime_error",
629
648
  ...context,
630
649
  error: {
631
650
  code: error.code ?? "RUNTIME_FAILED",
@@ -641,10 +660,56 @@ export async function runRuntime(settings) {
641
660
  });
642
661
  };
643
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
+ }
644
708
  const beat = async () => {
645
709
  const result = await client.runtime.heartbeat(
646
710
  {
647
711
  label: `${settings.provider}:${settings.model}`.slice(0, 100),
712
+ ...(verify ? { runId, state: "online" } : {}),
648
713
  ...(diagnosticsSupported
649
714
  ? {
650
715
  diagnostics: {
@@ -672,7 +737,7 @@ export async function runRuntime(settings) {
672
737
  } catch (error) {
673
738
  if (signal.aborted) break;
674
739
  report(error);
675
- if (error.status === 401 || error.status === 403) {
740
+ if (error.status === 401 || error.status === 403 || error.code === "RUNTIME_SUPERSEDED") {
676
741
  fatalError = error;
677
742
  shutdown.abort();
678
743
  break;
@@ -688,6 +753,7 @@ export async function runRuntime(settings) {
688
753
  model: settings.model,
689
754
  allowedAgents: settings.allowAgents,
690
755
  inspection: settings.inspector.mode,
756
+ ready: verify,
691
757
  },
692
758
  });
693
759
  diagnostics = { phase: "waiting" };
@@ -700,8 +766,17 @@ export async function runRuntime(settings) {
700
766
  once: settings.once,
701
767
  onPollError: (error) => report(error),
702
768
  shouldContinue: () => !settings.once || !humanDone,
703
- messages: (signal) =>
704
- 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
+ },
705
780
  eligible: async (message) => {
706
781
  const blocker = await journal.read(`message-blocker:${message.id}`);
707
782
  // Older runtimes persisted inspector outages as permanent blockers.
@@ -713,13 +788,22 @@ export async function runRuntime(settings) {
713
788
  diagnostics = { phase: "working", messageId: message.id };
714
789
  emit({ type: "message_started", data: { messageId: message.id } });
715
790
  if (settings.codexThreadId) {
716
- const delivery = await queueCodexChatMessage({ settings, client, identity, journal, message, signal });
791
+ const delivery = await queueCodexChatMessage({
792
+ settings,
793
+ client,
794
+ identity,
795
+ journal,
796
+ message,
797
+ signal,
798
+ });
717
799
  if (delivery.state === "uncertain") {
718
800
  report({ code: "AGENT_OUTCOME_UNCERTAIN" }, { messageId: message.id });
719
801
  } else {
720
802
  diagnostics = { phase: "waiting" };
721
- emit({ type: delivery.state === "confirmed" ? "message_handled" : "chat_queued",
722
- data: { messageId: message.id, threadId: settings.codexThreadId } });
803
+ emit({
804
+ type: delivery.state === "confirmed" ? "message_handled" : "chat_queued",
805
+ data: { messageId: message.id, threadId: settings.codexThreadId },
806
+ });
723
807
  }
724
808
  return;
725
809
  }
@@ -871,7 +955,8 @@ export async function runRuntime(settings) {
871
955
  let leaseError;
872
956
  const renewal = (async () => {
873
957
  while (!commandSignal.aborted) {
874
- await pause(30000, commandSignal);
958
+ // Observe human interruption promptly without repeating provider execution.
959
+ await pause(5000, commandSignal);
875
960
  try {
876
961
  await client.chatCommands.renew(command.id, claim.leaseToken, {
877
962
  signal: commandSignal,
@@ -980,6 +1065,10 @@ export async function runRuntime(settings) {
980
1065
  shutdown.abort();
981
1066
  await heartbeat;
982
1067
  await inbox;
1068
+ if (verify && !probeFailed)
1069
+ await client.runtime
1070
+ .heartbeat({ runId, state: "offline" }, { signal: AbortSignal.timeout(2000) })
1071
+ .catch(() => {});
983
1072
  await release();
984
1073
  emit({ type: "runtime_stopped", data: { agent: identity.address } });
985
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
  }