totemlayer 0.1.44 → 0.1.59

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.
Files changed (41) hide show
  1. package/README.md +48 -11
  2. package/bin/agent-memory.mjs +244 -0
  3. package/bin/agent-profile.mjs +26 -4
  4. package/bin/agent-runner.mjs +43 -9
  5. package/bin/agent-setup.mjs +136 -50
  6. package/bin/automatic-runtime.mjs +26 -7
  7. package/bin/chat-prompt-limit.mjs +67 -0
  8. package/bin/codex-chat-bridge.mjs +36 -8
  9. package/bin/codex-chat-readiness.mjs +7 -0
  10. package/bin/codex-human-chat-bridge.mjs +209 -0
  11. package/bin/collaboration-listener.mjs +2 -1
  12. package/bin/connected-agent-service.mjs +4 -1
  13. package/bin/inbox-queue.mjs +2 -1
  14. package/bin/installation.mjs +92 -60
  15. package/bin/local-mcp.mjs +43 -4
  16. package/bin/mcp-config.d.mts +11 -0
  17. package/bin/mcp-config.mjs +56 -0
  18. package/bin/mcp-install.mjs +143 -12
  19. package/bin/mcp-listener.d.mts +22 -0
  20. package/bin/mcp-listener.mjs +4 -0
  21. package/bin/mcp-runtime.mjs +3 -1
  22. package/bin/mcp.mjs +331 -94
  23. package/bin/mission-agent-service.mjs +2 -1
  24. package/bin/mission-worker.mjs +13 -2
  25. package/bin/model-adapter.mjs +1 -4
  26. package/bin/network-contract.mjs +254 -0
  27. package/bin/network-runtime.mjs +160 -25
  28. package/bin/network.mjs +30 -5
  29. package/bin/platform-client.mjs +25 -3
  30. package/bin/provider-detection.mjs +1 -4
  31. package/bin/runtime-diagnostics.mjs +1 -0
  32. package/bin/sdk/index.d.ts +50 -6
  33. package/bin/sdk/index.js +1 -1
  34. package/bin/sdk/listener.d.ts +57 -0
  35. package/bin/sdk/listener.js +1 -0
  36. package/bin/sdk/types.d.ts +62 -0
  37. package/bin/startup-diagnostics.mjs +168 -0
  38. package/bin/totem.mjs +17 -3
  39. package/bin/worker-core/adapters.mjs +4 -4
  40. package/package.json +1 -1
  41. package/scripts/prepare.mjs +28 -8
@@ -1,5 +1,5 @@
1
- import { join, dirname, isAbsolute, extname } from "node:path";
2
- import { existsSync } from "node:fs";
1
+ import { delimiter, join, dirname, isAbsolute, extname } from "node:path";
2
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
3
3
 
4
4
  const INHERITED_ENVIRONMENT = new Set(
5
5
  [
@@ -71,7 +71,29 @@ export function powershellInvocation(script, environment = process.env) {
71
71
  // PowerShell resolves both native executables and npm's .cmd shims. Only a
72
72
  // base64-encoded JSON argument array is interpolated into the script, so a
73
73
  // Windows path can never become shell syntax.
74
- export function installedClientInvocation(command, args, environment = process.env) {
74
+ function executableFile(path) {
75
+ try {
76
+ accessSync(path, constants.X_OK);
77
+ return statSync(path).isFile();
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ export function installedClientInvocation(command, args, environment = process.env, options = {}) {
84
+ const platform = options.platform ?? process.platform;
85
+ if (platform !== "win32") {
86
+ const isExecutable = options.isExecutable ?? executableFile;
87
+ if (
88
+ platform === "darwin" && command === "codex" &&
89
+ !(environment.PATH ?? "/usr/bin:/bin").split(delimiter)
90
+ .some((directory) => isExecutable(join(directory || ".", command)))
91
+ ) {
92
+ const bundled = "/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex";
93
+ if (isExecutable(bundled)) return { command: bundled, args };
94
+ }
95
+ return { command, args };
96
+ }
75
97
  // Native executables inherit the pipe directly. For npm clients, launch the
76
98
  // installed JavaScript entrypoint with Node rather than a .cmd/PowerShell
77
99
  // pipeline, which can leave stdin open indefinitely on Windows.
@@ -21,10 +21,7 @@ export async function detectInstalledProvider(environment = process.env, options
21
21
  for (const client of ["codex", "claude"]) {
22
22
  try {
23
23
  const args = client === "codex" ? ["login", "status"] : ["auth", "status"];
24
- const invocation =
25
- process.platform === "win32"
26
- ? installedClientInvocation(client, args, environment)
27
- : { command: client, args };
24
+ const invocation = installedClientInvocation(client, args, environment, options);
28
25
  const result = await execute(invocation.command, invocation.args, {
29
26
  env: localClientEnvironment(environment),
30
27
  timeout: 5000,
@@ -53,6 +53,7 @@ export const RUNTIME_ERROR_CODES = new Set([
53
53
  "INVALID_COMMUNICATION_POLICY",
54
54
  "INBOX_CAPACITY_REACHED",
55
55
  "WORK_RATE_LIMITED",
56
+ "CHAT_PROMPT_RATE_LIMITED",
56
57
  "INTERNAL_ERROR",
57
58
  "HARNESS_SETUP_REQUIRED",
58
59
  "HARNESS_UNSUPPORTED",
@@ -1,6 +1,6 @@
1
1
  import { createLocalInspector, InspectionError } from "./inspector.js";
2
2
  import type { ActionProposal, AssistantActionCheck, AssistantActionCheckList, AssistantActionReceiptResult, AssistantCheckOptions } from "./action-types.js";
3
- import type { MissionCoordinationUpdate, MissionSessionReport, Agent, AssistantContext, AssistantMissionPermissions, Attachment, Chat, ChatCommand, ChatDetail, ChatEvent, ChatRun, RuntimeHeartbeat, Conversation, ListenOptions, ListMessages, ListMissions, ListTasks, Message, MessageContent, MessageHandler, Metadata, MissionArtifact, MissionDetail, MissionEvent, MissionEventInput, MissionSummary, MissionWorkItem, Page, Pagination, RequestOptions, RepoClaim, RepoClaimState, RepositoryCoordination, SearchAgents, SendMessage, Subscription, Task, TaskState, TotemOptions, WriteOptions, NetworkActivityPage, NetworkActionInput, NetworkActionRequest, NetworkProtectedAction } from "./types.js";
3
+ import type { MissionCoordinationUpdate, MissionSessionReport, Agent, AgentMemory, AgentMemoryHistory, AgentMemorySearch, AgentProfileInput, AssistantContext, AssistantMissionPermissions, Attachment, Chat, ChatCommand, ChatDetail, ChatEvent, ChatRun, RuntimeHeartbeat, Conversation, ListenOptions, ListMessages, ListMissions, ListTasks, Message, MessageContent, MessageHandler, Metadata, MissionArtifact, MissionDetail, MissionEvent, MissionEventInput, MissionSummary, MissionWorkItem, Page, Pagination, RequestOptions, RepoClaim, RepoClaimState, RepositoryCoordination, SearchAgents, SendMessage, Subscription, Task, TaskState, TotemOptions, WriteOptions, NetworkActivityPage, NetworkActionInput, NetworkActionRequest, NetworkProtectedAction } from "./types.js";
4
4
  export * from "./types.js";
5
5
  export * from "./errors.js";
6
6
  export * from "./actions.js";
@@ -11,8 +11,29 @@ export declare class Totem {
11
11
  static connect(options: TotemOptions): Promise<ConnectedAgent>;
12
12
  connect(): Promise<ConnectedAgent>;
13
13
  agent(options?: RequestOptions): Promise<Agent>;
14
+ profiles: {
15
+ create: (input: AgentProfileInput, options?: RequestOptions) => Promise<Agent & {
16
+ identityAgentId: string;
17
+ }>;
18
+ };
19
+ memory: {
20
+ search: (query?: AgentMemorySearch, options?: RequestOptions) => Promise<AgentMemoryHistory>;
21
+ get: (options?: RequestOptions) => Promise<AgentMemory>;
22
+ update: (input: {
23
+ notes: string;
24
+ workingMemory: string;
25
+ expectedVersion: number;
26
+ automatic?: boolean;
27
+ forgetIds?: string[];
28
+ reinforceIds?: string[];
29
+ }, options?: RequestOptions) => Promise<AgentMemory>;
30
+ };
31
+ /** Read durable approval and work updates from the authenticated agent's harness. */
14
32
  activity: {
15
- list: (query?: { cursor?: string; limit?: number }, options?: RequestOptions) => Promise<NetworkActivityPage>;
33
+ list: (query?: {
34
+ cursor?: string;
35
+ limit?: number;
36
+ }, options?: RequestOptions) => Promise<NetworkActivityPage>;
16
37
  };
17
38
  /** Native Network authorization. Human approval remains in the receiving owner's conversation. */
18
39
  actions: {
@@ -77,8 +98,7 @@ export declare class Totem {
77
98
  list: (query?: ListMessages, options?: RequestOptions) => Promise<Page<Message>>;
78
99
  iterate: (query?: ListMessages, options?: RequestOptions) => AsyncIterable<Message>;
79
100
  get: (id: string, options?: RequestOptions) => Promise<Message>;
80
- reply: (id: string, text: string, options?: WriteOptions & {
81
- metadata?: Metadata;
101
+ reply: (id: string, text: string, options?: WriteOptions & Pick<SendMessage, "metadata" | "authorization" | "ttlSeconds"> & {
82
102
  attachments?: string[];
83
103
  }) => Promise<Message>;
84
104
  replyContent: (id: string, content: MessageContent, options?: WriteOptions) => Promise<Message>;
@@ -227,11 +247,24 @@ export declare class Totem {
227
247
  }>;
228
248
  };
229
249
  runtime: {
230
- activations: (options?: RequestOptions) => Promise<{ data: Array<{ id: string; state: string; ready: boolean }> }>;
231
- reportActivation: (input: { id: string; state: "starting" | "ready" | "blocked" | "paused"; errorCode?: "READINESS_UNSUPPORTED" | "LISTENER_START_FAILED" | "AUTHORIZATION_REJECTED" }, options?: RequestOptions) => Promise<{ recorded: boolean }>;
250
+ activations: (options?: RequestOptions) => Promise<{
251
+ data: Array<{
252
+ id: string;
253
+ state: string;
254
+ ready: boolean;
255
+ }>;
256
+ }>;
257
+ reportActivation: (input: {
258
+ id: string;
259
+ state: "starting" | "ready" | "blocked" | "paused";
260
+ errorCode?: "READINESS_UNSUPPORTED" | "LISTENER_START_FAILED" | "AUTHORIZATION_REJECTED";
261
+ }, options?: RequestOptions) => Promise<{
262
+ recorded: boolean;
263
+ }>;
232
264
  start: (input: {
233
265
  runId: string;
234
266
  provider: string;
267
+ automatic?: boolean;
235
268
  hostKind?: "local" | "hosted";
236
269
  }, options?: RequestOptions) => Promise<{
237
270
  runId: string;
@@ -243,6 +276,7 @@ export declare class Totem {
243
276
  challenge: string;
244
277
  completed: boolean;
245
278
  protocol: string;
279
+ provider?: string | null;
246
280
  }>;
247
281
  replyProbe: (input: {
248
282
  runId: string;
@@ -255,6 +289,7 @@ export declare class Totem {
255
289
  heartbeat: (input?: {
256
290
  label?: string;
257
291
  runId?: string;
292
+ listenerKind?: "mcp";
258
293
  state?: "starting" | "online" | "offline" | "error" | "blocked" | "reconnecting";
259
294
  errorCode?: string;
260
295
  }, options?: RequestOptions) => Promise<RuntimeHeartbeat>;
@@ -316,6 +351,15 @@ export declare class ConnectedAgent {
316
351
  readonly address: string;
317
352
  readonly client: Totem;
318
353
  constructor(client: Totem, identity: Agent);
354
+ get listenerStatus(): {
355
+ state: string;
356
+ listenerReady: boolean;
357
+ executionAuthorized: false;
358
+ runId?: string;
359
+ code?: string;
360
+ automatic?: boolean;
361
+ hostKind?: "local" | "hosted";
362
+ };
319
363
  get lastError(): Error | undefined;
320
364
  send(input: SendMessage, options?: WriteOptions): Promise<Message>;
321
365
  send(to: string, text: string, options?: WriteOptions): Promise<Message>;
package/bin/sdk/index.js CHANGED
@@ -1 +1 @@
1
- import{TotemError}from"./errors.js";import{createLocalInspector,InspectionError}from"./inspector.js";import{idempotencyKey,sleep,Transport}from"./transport.js";import{assertInputFields,assistantCheckOptions,signalOnlyOptions}from"./request-options.js";export*from"./types.js";export*from"./errors.js";export*from"./actions.js";import{idempotencyKey as idempotencyKey2}from"./transport.js";function content(input){if("from"in input)throw new TotemError("Sender identity comes from the API key.",{code:"INVALID_MESSAGE"});return input.content??{type:"text",text:input.text??input.message}}async function*paginate(fetchPage,cursor){const seen=new Set;if(cursor)seen.add(cursor);do{const page=await fetchPage(cursor);for(const item of page.data)yield item;cursor=page.nextCursor??void 0;if(cursor&&seen.has(cursor))throw new TotemError("Totem returned a repeated pagination cursor.",{code:"INVALID_RESPONSE"});if(cursor)seen.add(cursor)}while(cursor)}class Totem{#transport;#address;constructor(options){this.#transport=new Transport(options);this.#address=options.address?.trim().toLowerCase()}static async connect(options){return new Totem(options).connect()}async connect(){const identity=await this.agent();if(this.#address&&this.#address!==identity.address.toLowerCase()){throw new TotemError("The configured address does not match the authenticated agent.",{code:"IDENTITY_MISMATCH"})}return new ConnectedAgent(this,identity)}async agent(options){return(await this.#transport.request("GET","/agents/me",options)).data}activity={list:(query={},options)=>this.#transport.request("GET","/activity",{...signalOnlyOptions(options),query})};actions={list:(query={},options)=>this.#transport.request("GET","/actions",{...signalOnlyOptions(options),query}),request:async(input,options={})=>{assertInputFields(input,["to","action","description","conversationId","taskId","ttlSeconds"]);const{idempotencyKey:key,...requestOptions}=options;return(await this.#transport.request("POST","/actions",{...signalOnlyOptions(requestOptions),body:input,idempotencyKey:key??idempotencyKey()})).data},get:async(id,options)=>(await this.#transport.request("GET",`/actions/${encodeURIComponent(id)}`,signalOnlyOptions(options))).data,dispatch:async(id,input,options)=>{assertInputFields(input,["digest","action"]);return(await this.#transport.request("POST",`/actions/${encodeURIComponent(id)}/dispatch`,{...signalOnlyOptions(options),body:input})).data},complete:async(id,input,options)=>{assertInputFields(input,["executionId","outcome","summary"]);return(await this.#transport.request("POST",`/actions/${encodeURIComponent(id)}/complete`,{...signalOnlyOptions(options),body:input,retrySafe:true})).data},cancel:async(id,options)=>(await this.#transport.request("POST",`/actions/${encodeURIComponent(id)}/cancel`,{...signalOnlyOptions(options),retrySafe:true})).data,run:async(id,action,execute,options)=>{const requestOptions=signalOnlyOptions(options);const current=await this.actions.get(id,requestOptions);const dispatched=await this.actions.dispatch(id,{action,digest:current.digest},requestOptions);if(!dispatched.executionAuthorized||!dispatched.executionId)throw new TotemError("No execution permission was granted.",{code:"NETWORK_ACTION_DISPATCH_INVALID"});let result;try{result=await execute(structuredClone(dispatched.action),dispatched.executionId)}catch(cause){try{await this.actions.complete(id,{executionId:dispatched.executionId,outcome:"uncertain",summary:"The local executor stopped without a confirmed outcome. Inspect before repeating any effect."})}catch{}throw new TotemError("Action outcome uncertain. Inspect it; do not repeat.",{code:"NETWORK_ACTION_OUTCOME_UNCERTAIN",cause})}try{const receipt=await this.actions.complete(id,{executionId:dispatched.executionId,outcome:"succeeded",summary:result.summary},requestOptions);return{value:result.value,receipt}}catch(cause){throw new TotemError("Action finished; receipt pending. Reconcile it without reexecuting.",{code:"NETWORK_ACTION_RECEIPT_PENDING",cause})}}};assistant={context:async options=>(await this.#transport.request("GET","/assistant/context",signalOnlyOptions(options))).data,checks:{create:async(missionId,input,options)=>{assertInputFields(input,["workItemId","leaseToken","action"]);return(await this.#transport.request("POST",`/assistant/missions/${encodeURIComponent(missionId)}/action-checks`,{...assistantCheckOptions(options),body:input})).data},list:async(missionId,workItemId,options)=>(await this.#transport.request("GET",`/assistant/missions/${encodeURIComponent(missionId)}/action-checks`,{...signalOnlyOptions(options),query:{workItemId}})).data,recordReceipt:async(missionId,suiteId,input,options)=>{assertInputFields(input,["workItemId"]);return(await this.#transport.request("POST",`/assistant/missions/${encodeURIComponent(missionId)}/action-checks/${encodeURIComponent(suiteId)}/receipt`,{...signalOnlyOptions(options),body:input})).data}},permissions:async(missionId,options)=>(await this.#transport.request("GET",`/assistant/missions/${encodeURIComponent(missionId)}/permissions`,signalOnlyOptions(options))).data};async resolve(address,options){return(await this.#transport.request("GET",`/resolve/${encodeURIComponent(address)}`,options)).data}agents={search:(query={},options)=>this.#transport.request("GET","/agents/search",{...options,query}),iterate:(query={},options)=>paginate(cursor=>this.agents.search({...query,cursor},options),query.cursor)};attachments={upload:async(name,mediaType,bytes,options={})=>{if(!bytes.byteLength||bytes.byteLength>2*1024*1024)throw new TotemError("Attachments must be nonempty and no larger than 2 MiB.",{code:"ATTACHMENT_SIZE_INVALID"});return(await this.#transport.request("POST","/attachments",{...options,binaryBody:bytes,headers:{"Content-Type":mediaType,"X-Totem-File-Name":encodeURIComponent(name)}})).data},download:(id,options)=>this.#transport.download(`/attachments/${encodeURIComponent(id)}`,options)};messages={send:async(input,options={})=>{const body={...input,content:content(input)};delete body.text;delete body.message;return(await this.#transport.request("POST","/messages",{...options,body,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data},list:(query={},options)=>this.#transport.request("GET","/messages",{...options,query}),iterate:(query={},options)=>paginate(cursor=>this.messages.list({...query,cursor},options),query.cursor),get:async(id,options)=>(await this.#transport.request("GET",`/messages/${encodeURIComponent(id)}`,options)).data,reply:async(id,text,options={})=>(await this.#transport.request("POST",`/messages/${encodeURIComponent(id)}/reply`,{...options,body:{content:{type:"text",text,...options.attachments?{attachments:options.attachments}:{}},...options.metadata?{metadata:options.metadata}:{}},idempotencyKey:options.idempotencyKey??`sdk:reply:${id}`})).data,replyContent:async(id,content2,options={})=>(await this.#transport.request("POST",`/messages/${encodeURIComponent(id)}/reply`,{...options,body:{content:content2},idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,ack:async(id,options)=>(await this.#transport.request("POST",`/messages/${encodeURIComponent(id)}/ack`,{...options,body:{},retrySafe:true})).data};send(input,textOrOptions,options){if(typeof input==="string")return this.messages.send({to:input,text:textOrOptions},options);return this.messages.send(input,textOrOptions)}conversations={create:async(input,options)=>{const result=await this.#transport.request("POST","/conversations",{...options,body:input});return new ConversationHandle(this,result.data,input.with)},list:(query={},options)=>this.#transport.request("GET","/conversations",{...options,query}),get:async(id,options)=>(await this.#transport.request("GET",`/conversations/${encodeURIComponent(id)}`,options)).data,messages:(id,query={},options)=>this.#transport.request("GET",`/conversations/${encodeURIComponent(id)}/messages`,{...options,query})};startConversation(input,options){return this.conversations.create(input,options)}missions={list:async options=>(await this.#transport.request("GET","/missions",options)).data.missions,listPage:async(query={},options)=>{const page=await this.#transport.request("GET","/missions",{...options,query});return{data:page.data.missions,nextCursor:page.nextCursor??null}},iterate:(query={},options)=>paginate(cursor=>this.missions.listPage({...query,cursor},options),query.cursor),get:async(id,options)=>{const result=await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}`,options);return{...result.data.mission,members:result.data.members,workItems:result.data.workItems,artifacts:result.data.artifacts,events:result.data.events,budget:result.data.budget,coordination:result.data.coordination}},coordination:async(id,version,options)=>(await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/coordination`,{...options,query:{version}})).data,sessions:{report:async(id,input,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/sessions`,{...options,body:input,retrySafe:true})).data},workItems:async(id,options)=>(await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/work-items`,options)).data.workItems,workItemsPage:async(id,query={},options)=>{const page=await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/work-items`,{...options,query});return{data:page.data.workItems,nextCursor:page.nextCursor??null}},iterateWorkItems:(id,query={},options)=>paginate(cursor=>this.missions.workItemsPage(id,{...query,cursor},options),query.cursor),assign:async(id,input,options={})=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/work-items`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,claim:async(id,workItemId,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/work-items/${encodeURIComponent(workItemId)}/claim`,{...options,body:{}})).data,renew:async(id,workItemId,leaseToken,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/work-items/${encodeURIComponent(workItemId)}/renew`,{...options,body:{leaseToken},retrySafe:true})).data,complete:async(id,workItemId,input,options={})=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/work-items/${encodeURIComponent(workItemId)}/complete`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,reserveEffect:async(id,input,options={})=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/effects`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,listEffects:async(id,workItemId,options)=>(await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/effects`,{...options,query:{workItemId}})).data.effects,dispatchEffect:async(id,effectId,leaseToken,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/effects/${encodeURIComponent(effectId)}/dispatch`,{...options,body:{leaseToken}})).data,settleEffect:async(id,effectId,input,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/effects/${encodeURIComponent(effectId)}/settle`,{...options,body:input,retrySafe:true})).data,events:{list:(id,query={},options)=>this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/events`,{...options,query}),iterate:(id,query={},options)=>paginate(cursor=>this.missions.events.list(id,{...query,cursor},options),query.cursor),post:async(id,input,options={})=>{if("from"in input||"sender"in input||"actor"in input)throw new TotemError("Mission event actors come from the agent credential.",{code:"INVALID_EVENT"});return(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/events`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data},ack:async(id,eventId,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/events/${encodeURIComponent(eventId)}/ack`,{...options,body:{},retrySafe:true})).data}};chats={list:(query={},options)=>this.#transport.request("GET","/chats",{...options,query}),get:async(id,options={})=>(await this.#transport.request("GET",`/chats/${encodeURIComponent(id)}`,{signal:options.signal,query:{cursor:options.cursor,before:options.before,limit:options.limit}})).data,updates:async(id,text,options={})=>(await this.#transport.request("POST",`/chats/${encodeURIComponent(id)}/updates`,{...options,body:{text},idempotencyKey:options.idempotencyKey??idempotencyKey()})).data};chatCommands={list:(query={},options)=>this.#transport.request("GET","/chat-commands",{...options,query}),claim:async(id,options)=>(await this.#transport.request("POST",`/chat-commands/${encodeURIComponent(id)}/claim`,{...options,body:{}})).data,renew:async(id,leaseToken,options)=>(await this.#transport.request("POST",`/chat-commands/${encodeURIComponent(id)}/renew`,{...options,body:{leaseToken},retrySafe:true})).data,complete:async(id,input,options={})=>(await this.#transport.request("POST",`/chat-commands/${encodeURIComponent(id)}/complete`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,fail:async(id,input,options)=>(await this.#transport.request("POST",`/chat-commands/${encodeURIComponent(id)}/fail`,{...options,body:input,retrySafe:true})).data};runtime={activations:async options=>(await this.#transport.request("GET","/runtime/activations",options)).data,reportActivation:async(input,options)=>(await this.#transport.request("POST","/runtime/activations",{...options,body:input,retrySafe:true})).data,start:async(input,options)=>(await this.#transport.request("POST","/runtime/start",{...options,body:input,retrySafe:true})).data,probe:async(runId,options)=>(await this.#transport.request("GET","/runtime/probe",{...options,query:{runId}})).data,replyProbe:async(input,options)=>(await this.#transport.request("POST","/runtime/probe",{...options,body:input,retrySafe:true})).data,heartbeat:async(input={},options)=>(await this.#transport.request("POST","/runtime/heartbeat",{...options,body:input,retrySafe:true})).data};tasks={create:async(input,options={})=>(await this.#transport.request("POST","/tasks",{...options,body:{to:input.to,content:content(input),conversationId:input.conversationId,metadata:input.metadata,ttlSeconds:input.ttlSeconds,authorization:input.authorization},idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,get:async(id,options)=>(await this.#transport.request("GET",`/tasks/${encodeURIComponent(id)}`,options)).data,list:(query={},options)=>this.#transport.request("GET","/tasks",{...options,query}),iterate:(query={},options)=>paginate(cursor=>this.tasks.list({...query,cursor},options),query.cursor),postUpdate:async(id,text,options={})=>(await this.#transport.request("POST",`/tasks/${encodeURIComponent(id)}/messages`,{...options,body:{content:{type:"text",text}},idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,update:async(id,input,options)=>(await this.#transport.request("PATCH",`/tasks/${encodeURIComponent(id)}`,{...options,body:input})).data,cancel:async(id,options)=>(await this.#transport.request("POST",`/tasks/${encodeURIComponent(id)}/cancel`,{...options,body:{}})).data};repositories={getTask:async(taskId,options)=>(await this.#transport.request("GET",`/tasks/${encodeURIComponent(taskId)}/repository`,options)).data,claim:async(taskId,input,options)=>(await this.#transport.request("POST",`/tasks/${encodeURIComponent(taskId)}/repository`,{...options,body:input})).data,update:async(taskId,input,options)=>(await this.#transport.request("PATCH",`/tasks/${encodeURIComponent(taskId)}/repository`,{...options,body:input})).data};github={repositories:async options=>(await this.#transport.request("GET","/github/repositories",options)).data,openPullRequest:async(taskId,input,options)=>(await this.#transport.request("POST",`/github/pull-requests?taskId=${encodeURIComponent(taskId)}`,{...options,body:input})).data}}class ConversationHandle{id;conversation;#totem;#recipient;constructor(totem,conversation,recipient){this.#totem=totem;this.conversation=conversation;this.id=conversation.id;this.#recipient=recipient}send(text,options){return this.#totem.messages.send({to:this.#recipient,text,conversationId:this.id},options)}messages(query={},options){return this.#totem.conversations.messages(this.id,query,options)}}class ConnectedAgent{identity;address;client;#subscription;#lastError;constructor(client,identity){this.client=client;this.identity=identity;this.address=identity.address}get lastError(){return this.#lastError}send(input,textOrOptions,options){return typeof input==="string"?this.client.send(input,textOrOptions,options):this.client.send(input,textOrOptions)}startConversation(input){return this.client.startConversation(input)}on(event,handler,options={}){if(event!=="message")throw new TotemError("Only message subscriptions are supported.",{code:"INVALID_EVENT"});if(this.#subscription)throw new TotemError("This agent already has a message listener.",{code:"ALREADY_LISTENING"});const interval=options.pollIntervalMs??1e3;if(!Number.isFinite(interval)||interval<10)throw new TotemError("pollIntervalMs must be at least 10.",{code:"INVALID_CONFIG"});const controller=new AbortController;const inspector=options.inspector??createLocalInspector();const blocked=new Set;const report=cause=>{this.#lastError=cause instanceof Error?cause:new Error(String(cause));try{options.onError?.(this.#lastError)}catch{}};const done=(async()=>{while(!controller.signal.aborted){try{for await(const message of this.client.messages.iterate({direction:"inbound",unread:true,limit:100},{signal:controller.signal})){if(controller.signal.aborted)break;if(blocked.has(message.id))continue;try{if(message.content.type==="encrypted")throw new TotemError("Use E2eeTotem and pinned endpoint keys to process encrypted messages.",{code:"E2EE_CLIENT_REQUIRED"});await inspector.assertAllowed("input",{content:message.content,metadata:message.metadata??{}},{signal:controller.signal});const files=message.attachments??[];const ids=new Set(files.map(file=>file.id));if(message.content.type==="text"&&message.content.attachments?.some(id=>!ids.has(id)))throw new InspectionError("INSPECTION_UNSUPPORTED_FILE");for(const file of files){const downloaded=await this.client.attachments.download(file.id,{signal:controller.signal});await inspector.inspectFile(file.name,file.mediaType,downloaded.bytes,{signal:controller.signal})}const incoming={...message,text:message.content.type==="text"?message.content.text:JSON.stringify(message.content.data),reply:(text,writeOptions={})=>inspector.runAction({id:message.id,text},input=>this.client.messages.reply(input.id,input.text,{...writeOptions,idempotencyKey:writeOptions.idempotencyKey??`reply:${message.id}:0`}),{signal:controller.signal}),ack:()=>this.client.messages.ack(message.id)};await handler(incoming);await incoming.ack()}catch(error){if(error instanceof InspectionError&&error.code!=="INSPECTION_UNAVAILABLE")blocked.add(message.id);report(error)}}}catch(error){if(!controller.signal.aborted)report(error)}if(!controller.signal.aborted){try{await sleep(interval,controller.signal)}catch{}}}})();const subscription={done,stop:async()=>{controller.abort();await done;if(this.#subscription===subscription)this.#subscription=void 0}};this.#subscription=subscription;return subscription}async stop(){await this.#subscription?.stop()}}class TotemAgent extends Totem{}export{ConnectedAgent,ConversationHandle,InspectionError,Totem,TotemAgent,createLocalInspector,idempotencyKey2 as idempotencyKey};
1
+ import{TotemError}from"./errors.js";import{createAutomaticNetworkListener,DEFAULT_LISTENER_POLL_MS}from"./listener.js";import{createLocalInspector,InspectionError}from"./inspector.js";import{idempotencyKey,sleep,Transport}from"./transport.js";import{assertInputFields,assistantCheckOptions,signalOnlyOptions}from"./request-options.js";export*from"./types.js";export*from"./errors.js";export*from"./actions.js";import{idempotencyKey as idempotencyKey2}from"./transport.js";function content(input){if("from"in input)throw new TotemError("Sender identity comes from the API key.",{code:"INVALID_MESSAGE"});return input.content??{type:"text",text:input.text??input.message}}async function*paginate(fetchPage,cursor){const seen=new Set;if(cursor)seen.add(cursor);do{const page=await fetchPage(cursor);for(const item of page.data)yield item;cursor=page.nextCursor??void 0;if(cursor&&seen.has(cursor))throw new TotemError("Totem returned a repeated pagination cursor.",{code:"INVALID_RESPONSE"});if(cursor)seen.add(cursor)}while(cursor)}class Totem{#transport;#address;constructor(options){this.#transport=new Transport(options);this.#address=options.address?.trim().toLowerCase()}static async connect(options){return new Totem(options).connect()}async connect(){const identity=await this.agent();if(this.#address&&this.#address!==identity.address.toLowerCase()){throw new TotemError("The configured address does not match the authenticated agent.",{code:"IDENTITY_MISMATCH"})}return new ConnectedAgent(this,identity)}async agent(options){return(await this.#transport.request("GET","/agents/me",options)).data}profiles={create:async(input,options)=>{assertInputFields(input,["name","localPart","description","creationContext"]);return(await this.#transport.request("POST","/agent-profiles",{...signalOnlyOptions(options),body:input})).data}};memory={search:async(query={},options)=>{assertInputFields(query,["query","includeHistory","before","limit"]);return(await this.#transport.request("GET","/memory/search",{...signalOnlyOptions(options),query})).data},get:async options=>(await this.#transport.request("GET","/memory",signalOnlyOptions(options))).data,update:async(input,options)=>{assertInputFields(input,["notes","workingMemory","expectedVersion","reinforceIds","automatic","forgetIds"]);return(await this.#transport.request("PATCH","/memory",{...signalOnlyOptions(options),body:input,retrySafe:true})).data}};activity={list:(query={},options)=>this.#transport.request("GET","/activity",{...signalOnlyOptions(options),query})};actions={list:(query={},options)=>this.#transport.request("GET","/actions",{...signalOnlyOptions(options),query}),request:async(input,options={})=>{assertInputFields(input,["to","action","description","conversationId","taskId","ttlSeconds"]);const{idempotencyKey:key,...requestOptions}=options;return(await this.#transport.request("POST","/actions",{...signalOnlyOptions(requestOptions),body:input,idempotencyKey:key??idempotencyKey()})).data},get:async(id,options)=>(await this.#transport.request("GET",`/actions/${encodeURIComponent(id)}`,signalOnlyOptions(options))).data,dispatch:async(id,input,options)=>{assertInputFields(input,["digest","action"]);return(await this.#transport.request("POST",`/actions/${encodeURIComponent(id)}/dispatch`,{...signalOnlyOptions(options),body:input})).data},complete:async(id,input,options)=>{assertInputFields(input,["executionId","outcome","summary"]);return(await this.#transport.request("POST",`/actions/${encodeURIComponent(id)}/complete`,{...signalOnlyOptions(options),body:input,retrySafe:true})).data},cancel:async(id,options)=>(await this.#transport.request("POST",`/actions/${encodeURIComponent(id)}/cancel`,{...signalOnlyOptions(options),retrySafe:true})).data,run:async(id,action,execute,options)=>{const requestOptions=signalOnlyOptions(options);const current=await this.actions.get(id,requestOptions);const dispatched=await this.actions.dispatch(id,{action,digest:current.digest},requestOptions);if(!dispatched.executionAuthorized||!dispatched.executionId)throw new TotemError("No execution permission was granted.",{code:"NETWORK_ACTION_DISPATCH_INVALID"});let result;try{result=await execute(structuredClone(dispatched.action),dispatched.executionId)}catch(cause){try{await this.actions.complete(id,{executionId:dispatched.executionId,outcome:"uncertain",summary:"The local executor stopped without a confirmed outcome. Inspect before repeating any effect."})}catch{}throw new TotemError("Action outcome uncertain. Inspect it; do not repeat.",{code:"NETWORK_ACTION_OUTCOME_UNCERTAIN",cause})}try{const receipt=await this.actions.complete(id,{executionId:dispatched.executionId,outcome:"succeeded",summary:result.summary},requestOptions);return{value:result.value,receipt}}catch(cause){throw new TotemError("Action finished; receipt pending. Reconcile it without reexecuting.",{code:"NETWORK_ACTION_RECEIPT_PENDING",cause})}}};assistant={context:async options=>(await this.#transport.request("GET","/assistant/context",signalOnlyOptions(options))).data,checks:{create:async(missionId,input,options)=>{assertInputFields(input,["workItemId","leaseToken","action"]);return(await this.#transport.request("POST",`/assistant/missions/${encodeURIComponent(missionId)}/action-checks`,{...assistantCheckOptions(options),body:input})).data},list:async(missionId,workItemId,options)=>(await this.#transport.request("GET",`/assistant/missions/${encodeURIComponent(missionId)}/action-checks`,{...signalOnlyOptions(options),query:{workItemId}})).data,recordReceipt:async(missionId,suiteId,input,options)=>{assertInputFields(input,["workItemId"]);return(await this.#transport.request("POST",`/assistant/missions/${encodeURIComponent(missionId)}/action-checks/${encodeURIComponent(suiteId)}/receipt`,{...signalOnlyOptions(options),body:input})).data}},permissions:async(missionId,options)=>(await this.#transport.request("GET",`/assistant/missions/${encodeURIComponent(missionId)}/permissions`,signalOnlyOptions(options))).data};async resolve(address,options){return(await this.#transport.request("GET",`/resolve/${encodeURIComponent(address)}`,options)).data}agents={search:(query={},options)=>this.#transport.request("GET","/agents/search",{...options,query}),iterate:(query={},options)=>paginate(cursor=>this.agents.search({...query,cursor},options),query.cursor)};attachments={upload:async(name,mediaType,bytes,options={})=>{if(!bytes.byteLength||bytes.byteLength>2*1024*1024)throw new TotemError("Attachments must be nonempty and no larger than 2 MiB.",{code:"ATTACHMENT_SIZE_INVALID"});return(await this.#transport.request("POST","/attachments",{...options,binaryBody:bytes,headers:{"Content-Type":mediaType,"X-Totem-File-Name":encodeURIComponent(name)}})).data},download:(id,options)=>this.#transport.download(`/attachments/${encodeURIComponent(id)}`,options)};messages={send:async(input,options={})=>{const body={...input,content:content(input)};delete body.text;delete body.message;return(await this.#transport.request("POST","/messages",{...options,body,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data},list:(query={},options)=>this.#transport.request("GET","/messages",{...options,query}),iterate:(query={},options)=>paginate(cursor=>this.messages.list({...query,cursor},options),query.cursor),get:async(id,options)=>(await this.#transport.request("GET",`/messages/${encodeURIComponent(id)}`,options)).data,reply:async(id,text,options={})=>(await this.#transport.request("POST",`/messages/${encodeURIComponent(id)}/reply`,{...options,body:{content:{type:"text",text,...options.attachments?{attachments:options.attachments}:{}},...options.metadata?{metadata:options.metadata}:{},...options.authorization?{authorization:options.authorization}:{},...options.ttlSeconds===void 0?{}:{ttlSeconds:options.ttlSeconds}},idempotencyKey:options.idempotencyKey??`sdk:reply:${id}`})).data,replyContent:async(id,content2,options={})=>(await this.#transport.request("POST",`/messages/${encodeURIComponent(id)}/reply`,{...options,body:{content:content2},idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,ack:async(id,options)=>(await this.#transport.request("POST",`/messages/${encodeURIComponent(id)}/ack`,{...options,body:{},retrySafe:true})).data};send(input,textOrOptions,options){if(typeof input==="string")return this.messages.send({to:input,text:textOrOptions},options);return this.messages.send(input,textOrOptions)}conversations={create:async(input,options)=>{const result=await this.#transport.request("POST","/conversations",{...options,body:input});return new ConversationHandle(this,result.data,input.with)},list:(query={},options)=>this.#transport.request("GET","/conversations",{...options,query}),get:async(id,options)=>(await this.#transport.request("GET",`/conversations/${encodeURIComponent(id)}`,options)).data,messages:(id,query={},options)=>this.#transport.request("GET",`/conversations/${encodeURIComponent(id)}/messages`,{...options,query})};startConversation(input,options){return this.conversations.create(input,options)}missions={list:async options=>(await this.#transport.request("GET","/missions",options)).data.missions,listPage:async(query={},options)=>{const page=await this.#transport.request("GET","/missions",{...options,query});return{data:page.data.missions,nextCursor:page.nextCursor??null}},iterate:(query={},options)=>paginate(cursor=>this.missions.listPage({...query,cursor},options),query.cursor),get:async(id,options)=>{const result=await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}`,options);return{...result.data.mission,members:result.data.members,workItems:result.data.workItems,artifacts:result.data.artifacts,events:result.data.events,budget:result.data.budget,coordination:result.data.coordination}},coordination:async(id,version,options)=>(await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/coordination`,{...options,query:{version}})).data,sessions:{report:async(id,input,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/sessions`,{...options,body:input,retrySafe:true})).data},workItems:async(id,options)=>(await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/work-items`,options)).data.workItems,workItemsPage:async(id,query={},options)=>{const page=await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/work-items`,{...options,query});return{data:page.data.workItems,nextCursor:page.nextCursor??null}},iterateWorkItems:(id,query={},options)=>paginate(cursor=>this.missions.workItemsPage(id,{...query,cursor},options),query.cursor),assign:async(id,input,options={})=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/work-items`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,claim:async(id,workItemId,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/work-items/${encodeURIComponent(workItemId)}/claim`,{...options,body:{}})).data,renew:async(id,workItemId,leaseToken,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/work-items/${encodeURIComponent(workItemId)}/renew`,{...options,body:{leaseToken},retrySafe:true})).data,complete:async(id,workItemId,input,options={})=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/work-items/${encodeURIComponent(workItemId)}/complete`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,reserveEffect:async(id,input,options={})=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/effects`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,listEffects:async(id,workItemId,options)=>(await this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/effects`,{...options,query:{workItemId}})).data.effects,dispatchEffect:async(id,effectId,leaseToken,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/effects/${encodeURIComponent(effectId)}/dispatch`,{...options,body:{leaseToken}})).data,settleEffect:async(id,effectId,input,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/effects/${encodeURIComponent(effectId)}/settle`,{...options,body:input,retrySafe:true})).data,events:{list:(id,query={},options)=>this.#transport.request("GET",`/missions/${encodeURIComponent(id)}/events`,{...options,query}),iterate:(id,query={},options)=>paginate(cursor=>this.missions.events.list(id,{...query,cursor},options),query.cursor),post:async(id,input,options={})=>{if("from"in input||"sender"in input||"actor"in input)throw new TotemError("Mission event actors come from the agent credential.",{code:"INVALID_EVENT"});return(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/events`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data},ack:async(id,eventId,options)=>(await this.#transport.request("POST",`/missions/${encodeURIComponent(id)}/events/${encodeURIComponent(eventId)}/ack`,{...options,body:{},retrySafe:true})).data}};chats={list:(query={},options)=>this.#transport.request("GET","/chats",{...options,query}),get:async(id,options={})=>(await this.#transport.request("GET",`/chats/${encodeURIComponent(id)}`,{signal:options.signal,query:{cursor:options.cursor,before:options.before,limit:options.limit}})).data,updates:async(id,text,options={})=>(await this.#transport.request("POST",`/chats/${encodeURIComponent(id)}/updates`,{...options,body:{text},idempotencyKey:options.idempotencyKey??idempotencyKey()})).data};chatCommands={list:(query={},options)=>this.#transport.request("GET","/chat-commands",{...options,query}),claim:async(id,options)=>(await this.#transport.request("POST",`/chat-commands/${encodeURIComponent(id)}/claim`,{...options,body:{}})).data,renew:async(id,leaseToken,options)=>(await this.#transport.request("POST",`/chat-commands/${encodeURIComponent(id)}/renew`,{...options,body:{leaseToken},retrySafe:true})).data,complete:async(id,input,options={})=>(await this.#transport.request("POST",`/chat-commands/${encodeURIComponent(id)}/complete`,{...options,body:input,idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,fail:async(id,input,options)=>(await this.#transport.request("POST",`/chat-commands/${encodeURIComponent(id)}/fail`,{...options,body:input,retrySafe:true})).data};runtime={activations:async options=>(await this.#transport.request("GET","/runtime/activations",options)).data,reportActivation:async(input,options)=>(await this.#transport.request("POST","/runtime/activations",{...options,body:input,retrySafe:true})).data,start:async(input,options)=>(await this.#transport.request("POST","/runtime/start",{...options,body:input,retrySafe:true})).data,probe:async(runId,options)=>(await this.#transport.request("GET","/runtime/probe",{...options,query:{runId}})).data,replyProbe:async(input,options)=>(await this.#transport.request("POST","/runtime/probe",{...options,body:input,retrySafe:true})).data,heartbeat:async(input={},options)=>(await this.#transport.request("POST","/runtime/heartbeat",{...options,body:input,retrySafe:true})).data};tasks={create:async(input,options={})=>(await this.#transport.request("POST","/tasks",{...options,body:{to:input.to,content:content(input),conversationId:input.conversationId,metadata:input.metadata,ttlSeconds:input.ttlSeconds,authorization:input.authorization},idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,get:async(id,options)=>(await this.#transport.request("GET",`/tasks/${encodeURIComponent(id)}`,options)).data,list:(query={},options)=>this.#transport.request("GET","/tasks",{...options,query}),iterate:(query={},options)=>paginate(cursor=>this.tasks.list({...query,cursor},options),query.cursor),postUpdate:async(id,text,options={})=>(await this.#transport.request("POST",`/tasks/${encodeURIComponent(id)}/messages`,{...options,body:{content:{type:"text",text}},idempotencyKey:options.idempotencyKey??idempotencyKey()})).data,update:async(id,input,options)=>(await this.#transport.request("PATCH",`/tasks/${encodeURIComponent(id)}`,{...options,body:input})).data,cancel:async(id,options)=>(await this.#transport.request("POST",`/tasks/${encodeURIComponent(id)}/cancel`,{...options,body:{}})).data};repositories={getTask:async(taskId,options)=>(await this.#transport.request("GET",`/tasks/${encodeURIComponent(taskId)}/repository`,options)).data,claim:async(taskId,input,options)=>(await this.#transport.request("POST",`/tasks/${encodeURIComponent(taskId)}/repository`,{...options,body:input})).data,update:async(taskId,input,options)=>(await this.#transport.request("PATCH",`/tasks/${encodeURIComponent(taskId)}/repository`,{...options,body:input})).data};github={repositories:async options=>(await this.#transport.request("GET","/github/repositories",options)).data,openPullRequest:async(taskId,input,options)=>(await this.#transport.request("POST",`/github/pull-requests?taskId=${encodeURIComponent(taskId)}`,{...options,body:input})).data}}class ConversationHandle{id;conversation;#totem;#recipient;constructor(totem,conversation,recipient){this.#totem=totem;this.conversation=conversation;this.id=conversation.id;this.#recipient=recipient}send(text,options){return this.#totem.messages.send({to:this.#recipient,text,conversationId:this.id},options)}messages(query={},options){return this.#totem.conversations.messages(this.id,query,options)}}class ConnectedAgent{identity;address;client;#subscription;#lastError;#receiver;constructor(client,identity){this.client=client;this.identity=identity;this.address=identity.address;this.#receiver=createAutomaticNetworkListener({client,expectedAgentId:identity.id,notify:(_logger,data)=>{const status=data;if(status.code)this.#lastError=new TotemError("The automatic listener needs attention.",{code:status.code})}});this.#receiver.start()}get listenerStatus(){return this.#receiver.status()}get lastError(){return this.#lastError}send(input,textOrOptions,options){return typeof input==="string"?this.client.send(input,textOrOptions,options):this.client.send(input,textOrOptions)}startConversation(input){return this.client.startConversation(input)}on(event,handler,options={}){if(event!=="message")throw new TotemError("Only message subscriptions are supported.",{code:"INVALID_EVENT"});if(this.#subscription)throw new TotemError("This agent already has a message listener.",{code:"ALREADY_LISTENING"});const interval=options.pollIntervalMs??DEFAULT_LISTENER_POLL_MS;if(!Number.isFinite(interval)||interval<10)throw new TotemError("pollIntervalMs must be at least 10.",{code:"INVALID_CONFIG"});const controller=new AbortController;const inspector=options.inspector??createLocalInspector();const blocked=new Set;const report=cause=>{this.#lastError=cause instanceof Error?cause:new Error(String(cause));try{options.onError?.(this.#lastError)}catch{}};const done=(async()=>{while(!controller.signal.aborted){try{for await(const message of this.client.messages.iterate({direction:"inbound",unread:true,limit:100},{signal:controller.signal})){if(controller.signal.aborted)break;if(blocked.has(message.id))continue;try{if(message.content.type==="encrypted")throw new TotemError("Use E2eeTotem and pinned endpoint keys to process encrypted messages.",{code:"E2EE_CLIENT_REQUIRED"});await inspector.assertAllowed("input",{content:message.content,metadata:message.metadata??{}},{signal:controller.signal});const files=message.attachments??[];const ids=new Set(files.map(file=>file.id));if(message.content.type==="text"&&message.content.attachments?.some(id=>!ids.has(id)))throw new InspectionError("INSPECTION_UNSUPPORTED_FILE");for(const file of files){const downloaded=await this.client.attachments.download(file.id,{signal:controller.signal});await inspector.inspectFile(file.name,file.mediaType,downloaded.bytes,{signal:controller.signal})}const incoming={...message,text:message.content.type==="text"?message.content.text:JSON.stringify(message.content.data),reply:(text,writeOptions={})=>inspector.runAction({id:message.id,text},input=>this.client.messages.reply(input.id,input.text,{...writeOptions,idempotencyKey:writeOptions.idempotencyKey??`reply:${message.id}:0`}),{signal:controller.signal}),ack:()=>this.client.messages.ack(message.id)};await handler(incoming);await incoming.ack()}catch(error){if(error instanceof InspectionError&&error.code!=="INSPECTION_UNAVAILABLE")blocked.add(message.id);report(error)}}}catch(error){if(!controller.signal.aborted)report(error)}if(!controller.signal.aborted){try{await sleep(interval,controller.signal)}catch{}}}})();const subscription={done,stop:async()=>{controller.abort();await done;if(this.#subscription===subscription)this.#subscription=void 0}};this.#subscription=subscription;return subscription}async stop(){await Promise.all([this.#subscription?.stop(),this.#receiver.stop()])}}class TotemAgent extends Totem{}export{ConnectedAgent,ConversationHandle,InspectionError,Totem,TotemAgent,createLocalInspector,idempotencyKey2 as idempotencyKey};
@@ -0,0 +1,57 @@
1
+ import type { Totem } from "./index.js";
2
+ export declare const DEFAULT_LISTENER_POLL_MS = 5000;
3
+ export type ListenerStatus = {
4
+ state: string;
5
+ listenerReady: boolean;
6
+ executionAuthorized: false;
7
+ runId?: string;
8
+ code?: string;
9
+ automatic?: boolean;
10
+ hostKind?: "local" | "hosted";
11
+ };
12
+ export type ListenerOptions = {
13
+ client: Totem;
14
+ authorize?: (signal: AbortSignal) => Promise<unknown>;
15
+ enabled?: () => Promise<boolean>;
16
+ notify: (logger: string, data: unknown, id?: string) => void | Promise<void>;
17
+ hostKind?: "local" | "hosted";
18
+ pollMs?: number;
19
+ cursors?: {
20
+ cursor?: string;
21
+ messageCursor?: string;
22
+ };
23
+ signal?: AbortSignal;
24
+ expectedAgentId?: string;
25
+ };
26
+ /** The same durable feed is used by tools and the connection-owned receiver. */
27
+ export declare function readTotemActivity(client: Totem, { cursor, messageCursor, limit, }: {
28
+ cursor?: string;
29
+ messageCursor?: string;
30
+ limit?: number;
31
+ }, signal: AbortSignal): Promise<{
32
+ activity: import("./types.js").NetworkActivityPage;
33
+ messages: {
34
+ cursor: string;
35
+ data: import("./types.js").Message[];
36
+ nextCursor?: string | null;
37
+ blocked?: Array<{
38
+ sequence: string;
39
+ }>;
40
+ };
41
+ executionAuthorized: boolean;
42
+ }>;
43
+ /** Lives with the active host connection. Never invokes a model or acknowledges work. */
44
+ export declare function createAutomaticNetworkListener({ client, authorize, enabled, notify, hostKind, pollMs, cursors, signal, expectedAgentId, }: ListenerOptions): {
45
+ start(): void;
46
+ stop(): Promise<void>;
47
+ status(): {
48
+ state: string;
49
+ listenerReady: boolean;
50
+ executionAuthorized: false;
51
+ runId?: string;
52
+ code?: string;
53
+ automatic?: boolean;
54
+ hostKind?: "local" | "hosted";
55
+ };
56
+ done(): Promise<void>;
57
+ };
@@ -0,0 +1 @@
1
+ import{sleep}from"./transport.js";const DEFAULT_LISTENER_POLL_MS=5e3;async function readTotemActivity(client,{cursor,messageCursor,limit=25},signal){const[activity,messagesPage]=await Promise.all([client.activity.list({cursor,limit},{signal}),client.messages.list({direction:"all",cursor:messageCursor,limit},{signal})]);const messages=messagesPage;const scanned=[...messages.data,...messages.blocked??[]];const last=scanned.reduce((value,row)=>BigInt(row.sequence)>BigInt(value)?row.sequence:value,messageCursor??"0");return{activity,messages:{...messages,cursor:last},executionAuthorized:false}}function createAutomaticNetworkListener({client,authorize=async()=>{},enabled=async()=>true,notify,hostKind="local",pollMs=DEFAULT_LISTENER_POLL_MS,cursors={},signal,expectedAgentId}){const abort=new AbortController;let pending;let runId;let state={state:"disconnected",listenerReady:false,executionAuthorized:false};let position={...cursors};const options={signal:abort.signal};signal?.addEventListener("abort",()=>abort.abort(),{once:true});if(signal?.aborted)abort.abort();async function status(next){if(state.state===next.state&&state.runId===next.runId)return;state={listenerReady:false,executionAuthorized:false,...next};await notify("totem.listener",state)}async function loop(){try{if(!await enabled()){await status({state:"paused"});return}while(!abort.signal.aborted){let delay=pollMs;try{if(!await enabled()){await status({state:"paused"});break}await authorize(abort.signal);if(!runId){const identity=await client.agent(options);if(expectedAgentId&&identity.id!==expectedAgentId||!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(identity.id)||!["PRODUCTION","SANDBOX"].includes(identity.environment))throw Object.assign(new Error("Unverified agent identity"),{code:"AGENT_IDENTITY_MISMATCH",status:403});const started=await client.runtime.start({runId:globalThis.crypto.randomUUID(),provider:"mcp",hostKind,automatic:true},options);runId=started.runId;const probe=await client.runtime.probe(runId,options);if(probe.provider!=="mcp")throw Object.assign(new Error("Preserve the supervised runtime"),{code:"LISTENER_ALREADY_ACTIVE"});if(!probe.completed)await client.runtime.replyProbe({runId,reply:`TOTEM_READY:${probe.challenge}`},options)}await client.runtime.heartbeat({runId,state:"online",listenerKind:"mcp"},options);await status({state:"listening",listenerReady:true,runId,automatic:true,hostKind});const page=await readTotemActivity(client,position,abort.signal);if(page.activity.data.length||page.messages.data.length||page.messages.blocked?.length){await notify("totem.activity",page,`${page.activity.cursor}:${page.messages.cursor}`)}position={cursor:page.activity.cursor,messageCursor:page.messages.cursor};if(page.activity.nextCursor||page.messages.nextCursor)delay=0}catch(caught){const error=caught;if(abort.signal.aborted)break;if([401,403].includes(error.status??0)||error.code==="AGENT_IDENTITY_MISMATCH"){await status({state:"authorization_rejected",code:error.code});break}runId=void 0;const paused=["LISTENER_PAUSED","LISTENER_STOPPED"].includes(error.code??"");await status({state:paused?"paused":error.code==="LISTENER_ALREADY_ACTIVE"?"managed":"reconnecting",code:error.code??"NETWORK_ERROR"});delay=Math.max(pollMs,Math.min(3e4,(error.retryAfterSeconds??5)*1e3))}await sleep(delay,abort.signal)}}catch(caught){const error=caught;if(!abort.signal.aborted)state={state:"disconnected",listenerReady:false,executionAuthorized:false,code:error.code??"MCP_TRANSPORT_CLOSED"}}finally{state={...state,state:["listening","disconnected"].includes(state.state)?"disconnected":state.state,listenerReady:false}}}return{start(){pending??=loop()},async stop(){abort.abort();await pending},status(){return{...state}},async done(){await pending}}}export{DEFAULT_LISTENER_POLL_MS,createAutomaticNetworkListener,readTotemActivity};
@@ -1,3 +1,39 @@
1
+ export interface AgentMemoryControls {
2
+ enabled: boolean;
3
+ knowledgeRetentionDays: number;
4
+ workingRetentionDays: number;
5
+ }
6
+ export interface AgentMemoryEntry {
7
+ id: string;
8
+ section: "notes" | "workingMemory";
9
+ text: string;
10
+ reinforcedAt: string;
11
+ pinned: boolean;
12
+ expiresAt: string | null;
13
+ sourceKey?: string;
14
+ sourceEventId?: string;
15
+ }
16
+
17
+ export interface AgentMemory {
18
+ agentId: string;
19
+ notes: string;
20
+ workingMemory: string;
21
+ version: number;
22
+ updatedAt: string | null;
23
+ updatedBy: string | null;
24
+ controls: AgentMemoryControls;
25
+ entries: AgentMemoryEntry[];
26
+ forgottenCount: number;
27
+ active: { pendingMessages: number; queuedChats: number; openTasks: number };
28
+ }
29
+
30
+ export interface AgentProfileInput {
31
+ name: string;
32
+ localPart: string;
33
+ description?: string;
34
+ creationContext: { background?: string; decisions?: string; nextSteps?: string };
35
+ }
36
+
1
37
  export type Metadata = Record<string, unknown>;
2
38
  export type JsonValue = null | boolean | number | string | JsonValue[] | {
3
39
  [key: string]: JsonValue;
@@ -22,6 +58,8 @@ export interface AgentRuntimeStatus {
22
58
  }
23
59
 
24
60
  export interface Agent {
61
+ /** Own authenticated identity only; automatically loaded at connection. */
62
+ memory?: AgentMemory;
25
63
  /** Absent from public discovery; a connected MCP client alone is not a ready listener. */
26
64
  runtime?: AgentRuntimeStatus;
27
65
  actionMode?: "ask_me" | "full_access";
@@ -652,6 +690,7 @@ export type MessageHandler = (message: IncomingMessage) => void | Promise<void>;
652
690
  export interface ListenOptions {
653
691
  /** Customer-side policy. Screens message/file plaintext before invoking the handler. */
654
692
  inspector?: import("./inspector.js").LocalInspector;
693
+ /** Healthy inbox checks default to 5000 ms; an explicit host override is preserved. */
655
694
  pollIntervalMs?: number;
656
695
  /** Receives handler and transport failures; polling continues afterward. */
657
696
  onError?: (error: Error) => void;
@@ -703,6 +742,8 @@ export interface ChatDetail {
703
742
  previousCursor?: string | null;
704
743
  }
705
744
  export interface RuntimeHeartbeat {
745
+ listenerReady?: boolean;
746
+ listenerKind?: "mcp" | "runtime";
706
747
  operationalStatesSupported?: boolean;
707
748
  ready?: boolean;
708
749
  state?: string;
@@ -726,3 +767,24 @@ export interface NetworkActivityPage extends Page<NetworkActivity> {
726
767
  actionMode: "ask_me" | "full_access";
727
768
  actionModeVersion: number;
728
769
  }
770
+
771
+ export interface AgentMemorySearch {
772
+ query?: string;
773
+ includeHistory?: boolean;
774
+ before?: number;
775
+ limit?: number;
776
+ }
777
+ export interface AgentMemoryHistory {
778
+ data: Array<{
779
+ id: string;
780
+ version: number;
781
+ notes: string;
782
+ workingMemory: string;
783
+ source: string;
784
+ sourceEventId: string | null;
785
+ createdAt: string;
786
+ current: boolean;
787
+ excerpt: string;
788
+ }>;
789
+ nextCursor: string | null;
790
+ }
@@ -0,0 +1,168 @@
1
+ // Public diagnostics contain fixed descriptions only. Never log exception messages,
2
+ // provider output, command arguments, paths, or credentials from a failed startup.
3
+ export const HOST_APPROVAL_RECOVERY =
4
+ "Stop retrying. Ask the owner to inspect this command in the host app's approval or automatic review controls. If no approval path is available, contact the host's support with the denial. Resume once after the owner resolves it; do not bypass the policy or change agents.";
5
+
6
+ const causes = {
7
+ HOST_APPROVAL_DENIED: [
8
+ "The host denied approval to execute the startup command.",
9
+ HOST_APPROVAL_RECOVERY,
10
+ false,
11
+ ],
12
+ EADDRINUSE: [
13
+ "The listener could not bind because its port or socket is already in use.",
14
+ "Inspect the process using the configured port or socket; stop a duplicate listener or choose an available endpoint before starting this profile again.",
15
+ ],
16
+ EACCES: [
17
+ "The operating system denied access to a startup resource.",
18
+ "Check the owner's access to the executable, private state directory, and configured port; repair the specific permission before restarting.",
19
+ ],
20
+ EPERM: [
21
+ "The operating system denied the startup operation.",
22
+ "Inspect the host's process and filesystem restrictions and have the owner resolve the denied operation before restarting.",
23
+ ],
24
+ ENOENT: [
25
+ "A required startup executable or file was not found.",
26
+ "Check the selected executable and private configuration paths; install or restore the missing resource before restarting.",
27
+ ],
28
+ ECONNREFUSED: [
29
+ "The configured service refused the connection.",
30
+ "Start the configured model or service and check its endpoint before restarting this profile.",
31
+ ],
32
+ ENOTFOUND: [
33
+ "The configured service hostname could not be resolved.",
34
+ "Check the configured hostname and network DNS, then restart this profile.",
35
+ ],
36
+ ETIMEDOUT: [
37
+ "The startup connection timed out.",
38
+ "Check the service endpoint and network availability before restarting this profile.",
39
+ ],
40
+ RUN_TIMEOUT: [
41
+ "The executor did not finish the controlled readiness reply before its deadline.",
42
+ "Check the selected executor and model availability before restarting this profile.",
43
+ ],
44
+ PROVIDER_AUTH_REQUIRED: [
45
+ "The selected inference provider credential is missing or unavailable.",
46
+ "Restore the selected provider credential in this host's secret environment before restarting. Never copy another agent's key.",
47
+ ],
48
+ RUNTIME_AUTHORIZATION_REJECTED: [
49
+ "Totem rejected the profile's credential or access.",
50
+ "Reauthorize this agent with totem login --reauthorize. Never copy another agent's key.",
51
+ ],
52
+ AGENT_IDENTITY_MISMATCH: [
53
+ "The authenticated agent does not match the saved profile.",
54
+ "Inspect this profile's agent and origin; reconnect the intended agent without replacing another agent's configuration.",
55
+ ],
56
+ ISOLATION_UNAVAILABLE: [
57
+ "The selected executor's required process isolation is unavailable.",
58
+ "Repair the supported isolation backend on this host before restarting; do not disable containment.",
59
+ ],
60
+ PRIVACY_BOUNDARY_UNAVAILABLE: [
61
+ "The executor's required privacy boundary is unavailable.",
62
+ "Repair the configured privacy boundary before restarting; do not bypass it.",
63
+ ],
64
+ AGENT_AUTH_REQUIRED: [
65
+ "The selected agent client is not authenticated.",
66
+ "Sign in to the selected client on this host, then explicitly start this profile once.",
67
+ ],
68
+ AGENT_QUOTA_EXCEEDED: [
69
+ "The selected agent provider rejected startup because its usage quota is exhausted.",
70
+ "Inspect the selected provider's usage limits and resume once capacity is available.",
71
+ ],
72
+ AGENT_CLI_INCOMPATIBLE: [
73
+ "The installed agent CLI does not support the required startup contract.",
74
+ "Install a supported version of the selected client, then start this profile once.",
75
+ ],
76
+ HARNESS_SETUP_REQUIRED: [
77
+ "The selected host has not completed its native receiver setup.",
78
+ "Complete the selected host's supported setup and inspect its approval controls before starting this profile.",
79
+ ],
80
+ READINESS_UNSUPPORTED: [
81
+ "The installed adapter cannot verify the controlled readiness check.",
82
+ "Update the selected adapter to a version that supports receive, wake, and reply verification before claiming readiness.",
83
+ ],
84
+ AGENT_NOT_INSTALLED: [
85
+ "The selected agent executable is not installed or cannot be located.",
86
+ "Install the selected agent client and check this host's executable path before restarting.",
87
+ ],
88
+ AGENT_START_FAILED: [
89
+ "The executor process could not be launched.",
90
+ "Check that the selected executable exists and the owner can run it on this host.",
91
+ ],
92
+ AGENT_EXIT_FAILED: [
93
+ "The executor exited with a nonzero status during startup.",
94
+ "Inspect the selected executor's local diagnostics and sign-in state before restarting; do not share credentials or raw provider output.",
95
+ ],
96
+ INVALID_AGENT_RESULT: [
97
+ "The executor returned an invalid readiness response.",
98
+ "Check the selected adapter's response format and configured model before restarting.",
99
+ ],
100
+ READINESS_CHALLENGE_FAILED: [
101
+ "Totem could not supply a valid controlled readiness challenge.",
102
+ "Check Totem connectivity and the installed receiver version before restarting this profile.",
103
+ ],
104
+ READINESS_GENERATION_FAILED: [
105
+ "The selected executor failed while generating the controlled readiness reply; it supplied no recognized cause.",
106
+ "Inspect the selected executor's local diagnostics and authorization before restarting this profile.",
107
+ ],
108
+ READINESS_REPLY_REJECTED: [
109
+ "Totem did not accept the controlled readiness reply.",
110
+ "Check that the selected executor returns the exact readiness challenge response and that the runtime session is current before restarting.",
111
+ ],
112
+ READINESS_REPLY_INVALID: [
113
+ "The executor did not return the exact controlled readiness response.",
114
+ "Check the selected adapter's output contract and repeat the readiness check once after repair.",
115
+ ],
116
+ READINESS_EXPIRED: [
117
+ "The controlled readiness challenge expired before it completed.",
118
+ "Check executor latency and start a fresh runtime session to obtain a new challenge.",
119
+ ],
120
+ RUNTIME_SUPERSEDED: [
121
+ "Another runtime session owns this connection credential.",
122
+ "Inspect the existing listener and use its session; stop it explicitly before replacing it. Do not start duplicate workers.",
123
+ ],
124
+ BACKGROUND_START_TIMEOUT: [
125
+ "The background worker did not confirm startup before its deadline.",
126
+ "Inspect totem status for this profile and the selected executor's local diagnostics before starting it again.",
127
+ ],
128
+ STARTUP_CAUSE_UNAVAILABLE: [
129
+ "Startup failed without a recognized underlying cause.",
130
+ "Inspect this profile with totem status and the selected executor's local diagnostics before restarting. Share error codes, not credentials or raw provider output.",
131
+ ],
132
+ };
133
+
134
+ const known = (code) => typeof code === "string" && Object.hasOwn(causes, code);
135
+
136
+ export function startupDiagnostic(error) {
137
+ const chain = [];
138
+ const seen = new Set();
139
+ for (
140
+ let item = error;
141
+ item && typeof item === "object" && chain.length < 8 && !seen.has(item);
142
+ item = item.cause
143
+ ) {
144
+ seen.add(item);
145
+ chain.push(item);
146
+ }
147
+ const denied = chain.some(
148
+ (item) =>
149
+ ["HOST_APPROVAL_DENIED", "APPROVAL_DENIED", "POLICY_DENIED"].includes(item.code) ||
150
+ /(?:rejected:\s*)?blocked by policy|automatic (?:approval )?review rejected|approval (?:was )?denied/i.test(
151
+ String(item.message ?? ""),
152
+ ),
153
+ );
154
+ const match = chain
155
+ .toReversed()
156
+ .find((item) => known(item.code) || known(item.causeCode) || [401, 403].includes(item.status));
157
+ const causeCode = denied
158
+ ? "HOST_APPROVAL_DENIED"
159
+ : match
160
+ ? known(match.code)
161
+ ? match.code
162
+ : known(match.causeCode)
163
+ ? match.causeCode
164
+ : "RUNTIME_AUTHORIZATION_REJECTED"
165
+ : "STARTUP_CAUSE_UNAVAILABLE";
166
+ const [cause, recovery, retryable = true] = causes[causeCode];
167
+ return { causeCode, cause, recovery, retryable };
168
+ }
package/bin/totem.mjs CHANGED
@@ -239,9 +239,9 @@ function compactDigest(value) {
239
239
  function help() {
240
240
  writeBrand();
241
241
  writeHelpBody(`Start here
242
- Install once npm install --global https://totemlayer.com/downloads/totemlayer-0.1.44.tgz
242
+ Install once npm install --global https://totemlayer.com/downloads/totemlayer-0.1.59.tgz
243
243
  Confirm installation totem --version
244
- Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer-0.1.44.tgz totem --help
244
+ Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer-0.1.59.tgz totem --help
245
245
 
246
246
  Connect once and receive messages and approved Mission work
247
247
  Select Codex, Claude, a local model, or an owner-configured CLI/HTTP agent.
@@ -255,7 +255,9 @@ Agent messaging and local runtime
255
255
  totem network --help
256
256
  totem network run --provider qwen-local --model qwen3.5-9b
257
257
 
258
- Connect an AI app through MCP
258
+ Connect any AI agent or personal assistant through MCP
259
+ totem mcp config
260
+ totem mcp config --transport stdio --key-file /absolute/path/to/agent.key
259
261
  totem mcp
260
262
 
261
263
  Authorization handshake
@@ -1781,6 +1783,18 @@ async function main() {
1781
1783
  return;
1782
1784
  }
1783
1785
  if (process.argv[2] === "mcp") {
1786
+ if (process.argv[3] === "config") {
1787
+ const { configureTotemMcp } = await import("./mcp-install.mjs");
1788
+ try {
1789
+ process.stdout.write(await configureTotemMcp(process.argv.slice(4)));
1790
+ } catch (error) {
1791
+ process.stderr.write(
1792
+ `${error instanceof Error ? error.message : "Could not export MCP settings."}\n`,
1793
+ );
1794
+ process.exitCode = 1;
1795
+ }
1796
+ return;
1797
+ }
1784
1798
  if (process.argv[3] === "install" || process.argv[3] === "--help" || process.argv[3] === "-h") {
1785
1799
  const { installTotemMcp } = await import("./mcp-install.mjs");
1786
1800
  try {