hyperbrane 0.1.0 → 0.1.1

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 (137) hide show
  1. package/README.md +12 -2
  2. package/dist/agent/agentService.d.ts +1 -0
  3. package/dist/agent/agentService.js +16 -1
  4. package/dist/agent/agentStorage.d.ts +2 -0
  5. package/dist/agent/agentStorage.js +4 -2
  6. package/dist/agent/api.d.ts +51 -27
  7. package/dist/agent/client.d.ts +3 -0
  8. package/dist/agent/client.js +5 -0
  9. package/dist/agent/hostTranscript.d.ts +13 -0
  10. package/dist/agent/hostTranscript.js +190 -0
  11. package/dist/agent/session/backend.d.ts +80 -0
  12. package/dist/agent/session/backend.js +1 -0
  13. package/dist/agent/session/claude/home.d.ts +2 -0
  14. package/dist/agent/session/claude/home.js +4 -0
  15. package/dist/agent/session/claude/transcript.d.ts +13 -0
  16. package/dist/agent/session/claude/transcript.js +29 -0
  17. package/dist/agent/session/client.d.ts +5 -1
  18. package/dist/agent/session/codex/home.d.ts +2 -0
  19. package/dist/agent/session/codex/home.js +4 -0
  20. package/dist/agent/session/codex/messages.d.ts +30 -0
  21. package/dist/agent/session/codex/messages.js +135 -0
  22. package/dist/agent/session/codex/transcript.d.ts +12 -0
  23. package/dist/agent/session/codex/transcript.js +22 -0
  24. package/dist/agent/session/fileTranscript.d.ts +92 -0
  25. package/dist/agent/session/fileTranscript.js +454 -0
  26. package/dist/agent/session/sdkMessages.d.ts +15 -0
  27. package/dist/agent/session/sdkMessages.js +28 -0
  28. package/dist/agent/session/sessionStorage.d.ts +4 -0
  29. package/dist/agent/session/sessionStorage.js +13 -2
  30. package/dist/agent/session/sharedConnection.d.ts +1 -0
  31. package/dist/agent/session/storedTranscripts.d.ts +13 -0
  32. package/dist/agent/session/storedTranscripts.js +8 -0
  33. package/dist/agent/session/worker/protocol.d.ts +1 -0
  34. package/dist/agent/session/worker/workerParams.d.ts +11 -0
  35. package/dist/agent/session/worker/workerParams.js +162 -0
  36. package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.d.ts +10 -0
  37. package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.js +2 -0
  38. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.d.ts +2 -0
  39. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.js +2 -0
  40. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.d.ts +22 -0
  41. package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.js +2 -0
  42. package/dist/agent/vendor-types/codex/app-server/ImageDetail.d.ts +1 -0
  43. package/dist/agent/vendor-types/codex/app-server/ImageDetail.js +2 -0
  44. package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.d.ts +5 -0
  45. package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.js +2 -0
  46. package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.d.ts +11 -0
  47. package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.js +2 -0
  48. package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.d.ts +23 -0
  49. package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.js +2 -0
  50. package/dist/agent/vendor-types/codex/app-server/MessagePhase.d.ts +7 -0
  51. package/dist/agent/vendor-types/codex/app-server/MessagePhase.js +2 -0
  52. package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.d.ts +4 -0
  53. package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.js +2 -0
  54. package/dist/agent/vendor-types/codex/app-server/SleepItem.d.ts +7 -0
  55. package/dist/agent/vendor-types/codex/app-server/SleepItem.js +2 -0
  56. package/dist/agent/vendor-types/codex/app-server/WebSearchItem.d.ts +14 -0
  57. package/dist/agent/vendor-types/codex/app-server/WebSearchItem.js +2 -0
  58. package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.d.ts +3 -0
  59. package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.js +2 -0
  60. package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.d.ts +1 -0
  61. package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.js +2 -0
  62. package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.d.ts +4 -0
  63. package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.js +2 -0
  64. package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.d.ts +4 -0
  65. package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.js +2 -0
  66. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.d.ts +5 -0
  67. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.js +2 -0
  68. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.d.ts +1 -0
  69. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.js +2 -0
  70. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.d.ts +1 -0
  71. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.js +2 -0
  72. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.d.ts +1 -0
  73. package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.js +2 -0
  74. package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.d.ts +19 -0
  75. package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.js +2 -0
  76. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.d.ts +1 -0
  77. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.js +2 -0
  78. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.d.ts +1 -0
  79. package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.js +2 -0
  80. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.d.ts +10 -0
  81. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.js +2 -0
  82. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.d.ts +1 -0
  83. package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.js +2 -0
  84. package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.d.ts +6 -0
  85. package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.js +2 -0
  86. package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.d.ts +4 -0
  87. package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.js +2 -0
  88. package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.d.ts +1 -0
  89. package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.js +2 -0
  90. package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.d.ts +8 -0
  91. package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.js +2 -0
  92. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.d.ts +7 -0
  93. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.js +2 -0
  94. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.d.ts +3 -0
  95. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.js +2 -0
  96. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.d.ts +6 -0
  97. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.js +2 -0
  98. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.d.ts +1 -0
  99. package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.js +2 -0
  100. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.d.ts +5 -0
  101. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.js +2 -0
  102. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.d.ts +6 -0
  103. package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.js +2 -0
  104. package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.d.ts +1 -0
  105. package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.js +2 -0
  106. package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.d.ts +8 -0
  107. package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.js +2 -0
  108. package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.d.ts +1 -0
  109. package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.js +2 -0
  110. package/dist/agent/vendor-types/codex/app-server/v2/TextElement.d.ts +11 -0
  111. package/dist/agent/vendor-types/codex/app-server/v2/TextElement.js +2 -0
  112. package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.d.ts +215 -0
  113. package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.js +2 -0
  114. package/dist/agent/vendor-types/codex/app-server/v2/UserInput.d.ts +35 -0
  115. package/dist/agent/vendor-types/codex/app-server/v2/UserInput.js +2 -0
  116. package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.d.ts +14 -0
  117. package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.js +2 -0
  118. package/dist/agent/workerBundle.generated.js +1 -1
  119. package/dist/cli/agent.js +17 -4
  120. package/dist/cli/services.d.ts +6 -5
  121. package/dist/dimension/api.d.ts +5 -4
  122. package/dist/dimension/paths.d.ts +4 -0
  123. package/dist/dimension/paths.js +10 -2
  124. package/dist/dimension/service.d.ts +1 -0
  125. package/dist/dimension/storage.js +3 -2
  126. package/dist/index.d.ts +1 -3
  127. package/dist/index.js +4 -5
  128. package/dist/lib/docker.d.ts +1 -0
  129. package/dist/portal-gateway/api.d.ts +6 -5
  130. package/dist/portal-gateway/gatewayService.d.ts +1 -0
  131. package/dist/portal-gateway/portalService.d.ts +2 -0
  132. package/dist/sandbox/api.d.ts +49 -21
  133. package/dist/sandbox/boatService.js +11 -3
  134. package/dist/sandbox/containerService.js +10 -3
  135. package/dist/sandbox/localService.js +20 -2
  136. package/dist/sandbox/sshService.js +37 -4
  137. package/package.json +25 -8
package/README.md CHANGED
@@ -30,12 +30,22 @@ await agents.createAgent({ name: "dev", dimensionName: "work" });
30
30
  ```
31
31
 
32
32
  The root module exports the services, the sandboxes (`createLocalSandbox`, `createSshSandbox`,
33
- `createContainerSandbox`, `createBoatSandbox`), the endpoint auths and the agent client. Every module
34
- is also reachable by its path, e.g. `hyperbrane/sandbox/sshService`.
33
+ `createContainerSandbox`, `createBoatSandbox`), the endpoint auths and the agent client. For a
34
+ browser, which can't take the server side along, the package also exports each part's API
35
+ (`hyperbrane/sandbox/api`, `hyperbrane/dimension/api`, `hyperbrane/portal-gateway/api`,
36
+ `hyperbrane/agent/api`) and the agent's worker client (`hyperbrane/agent/client`). Nothing else is
37
+ importable.
38
+
39
+ What a client may rely on, as the types say: a `readonly` field is set by the package, for the client
40
+ to read; a type marked `@sealed` (the services, sandboxes, clients) is made by the package, for the
41
+ client to use, not implement. Both may gain members in any release; anything else gains only optional
42
+ ones, until a breaking release (`bun run api:diff --breaking` shows what would break).
35
43
 
36
44
  ## Building
37
45
 
38
46
  ```sh
39
47
  bun run build # dist/
40
48
  bun run check:package # packs it, installs the tarball elsewhere and uses it under Node and Bun
49
+ bun run api:diff [REF] # how the public interface changed since REF (HEAD): git diff of just that
50
+ bun run api:diff --breaking [REF] # only its breaking changes: removed, renamed, newly required, retyped
41
51
  ```
@@ -3,6 +3,7 @@ import type { Sandbox, SandboxAccessibleEndpoint } from "../sandbox/api.ts";
3
3
  import type { AgentStorage } from "./agentStorage.ts";
4
4
  import type { GatewaySignIns } from "./gatewaySignIns.ts";
5
5
  import { type AgentClient, type AgentEndpoint, type AgentProvision, type AgentSessionCapabilities } from "./api.ts";
6
+ /** @sealed */
6
7
  export interface AgentService extends AgentClient, AgentProvision, AgentSessionCapabilities, AgentEndpoint {
7
8
  /** Starts the workers that a restart took down (their dimension runs again); returns their agents' names. */
8
9
  restoreAgents(): Promise<string[]>;
@@ -1,3 +1,4 @@
1
+ import { dimensionStateDirectory } from "../dimension/paths.js";
1
2
  import { RUN_PATH, STATE_PATH, containerName as dimensionContainerName } from "../dimension/service.js";
2
3
  import { createContainerSandbox } from "../sandbox/containerService.js";
3
4
  import { InvalidNameError, checkName } from "../lib/jsonDirectory.js";
@@ -6,7 +7,9 @@ import { OS_CA_BUNDLE } from "../portal-gateway/portalService.js";
6
7
  import { AgentConflictError, AgentInvalidError, AgentNotFoundError, } from "./api.js";
7
8
  import { createWorkerClient } from "./session/client.js";
8
9
  import { workerSocketConnector } from "./session/workerConnection.js";
10
+ import { readHostTranscript } from "./hostTranscript.js";
9
11
  import { workerSocketPath } from "./session/sessionStorage.js";
12
+ import { WORKER_PARAMS } from "./session/worker/workerParams.js";
10
13
  import { workerBundle } from "./workerBundle.js";
11
14
  /** In the dimension: where the backends are installed (a package with its node_modules). Shared by agents. */
12
15
  export const WORKER_DIR = "/opt/hyperbrane/worker";
@@ -320,6 +323,18 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
320
323
  async function sessionClient({ agentName }) {
321
324
  return clientFor(agentName);
322
325
  }
326
+ /**
327
+ * Read on the host from the dimension's state directory (hostTranscript.ts), so nothing has to run,
328
+ * neither the worker nor the dimension; only a session whose backend's home is elsewhere asks its
329
+ * worker.
330
+ */
331
+ async function getTranscript(ref, request = {}) {
332
+ const { session, limit, before } = WORKER_PARAMS["session/transcript"]({ session: ref.sessionName, ...request });
333
+ const agent = await requireAgent(ref.agentName);
334
+ const stateDirectory = dimensionStateDirectory(await storage.dataDirectory(), agent.dimensionName);
335
+ const page = await readHostTranscript(host, { stateDirectory, agentName: agent.name, sessionName: session }, { limit, before });
336
+ return page ?? (await sessionClient(ref)).call("session/transcript", { session, limit, before });
337
+ }
323
338
  // Agents whose dimension or worker is down have no sessions to show right now.
324
339
  async function listSessions() {
325
340
  const perAgent = await Promise.all((await storage.listAgents()).map(async (agent) => {
@@ -359,7 +374,7 @@ export function createAgentService(storage, docker, host, gatewaySignIns) {
359
374
  createSession: async (agentName, session) => (await clientFor(agentName)).call("session/create", { session }),
360
375
  updateSession: async (ref, limits) => (await sessionClient(ref)).call("session/update", { session: ref.sessionName, limits }),
361
376
  stopSession: async (ref) => void (await (await sessionClient(ref)).call("session/stop", { session: ref.sessionName })),
362
- getTranscript: async (ref, request = {}) => (await sessionClient(ref)).call("session/transcript", { session: ref.sessionName, ...request }),
377
+ getTranscript,
363
378
  cancel: async (ref) => void (await (await sessionClient(ref)).call("session/interrupt", { session: ref.sessionName })),
364
379
  sendMessage: async (ref, message) => void (await (await sessionClient(ref)).call("session/send", { session: ref.sessionName, message })),
365
380
  getSessionStatus: async (ref) => (await sessionClient(ref)).call("session/status", { session: ref.sessionName }),
@@ -9,6 +9,8 @@ export interface AgentStorage {
9
9
  /** No-op if missing. */
10
10
  removeAgent(agentName: string): Promise<void>;
11
11
  listAgents(): Promise<Agent[]>;
12
+ /** Hyperbrane's data directory these are kept in, with the dimensions' (see dimension/paths.ts). */
13
+ dataDirectory(): Promise<string>;
12
14
  /**
13
15
  * The agent's accessible endpoints (AgentService.exposeAgent), credentials included, by the
14
16
  * accessible host each was exposed for: one per way the machine is reached.
@@ -4,9 +4,10 @@ import { createJsonDirectory, defaultDataDir } from "../lib/jsonDirectory.js";
4
4
  * agents/<agent>.json, agent-endpoints/<agent>.json.
5
5
  */
6
6
  export function createAgentStorage(sandbox, dir) {
7
- const agents = createJsonDirectory(sandbox, async () => `${dir ?? (await defaultDataDir(sandbox))}/agents`);
7
+ const dataDirectory = async () => dir ?? (await defaultDataDir(sandbox));
8
+ const agents = createJsonDirectory(sandbox, async () => `${await dataDirectory()}/agents`);
8
9
  // A record used to hold one endpoint; it is read as the entry for its URL's host:port.
9
- const endpoints = createJsonDirectory(sandbox, async () => `${dir ?? (await defaultDataDir(sandbox))}/agent-endpoints`);
10
+ const endpoints = createJsonDirectory(sandbox, async () => `${await dataDirectory()}/agent-endpoints`);
10
11
  async function readEndpoints(agentName) {
11
12
  const record = await endpoints.read(agentName);
12
13
  if (!record)
@@ -44,6 +45,7 @@ export function createAgentStorage(sandbox, dir) {
44
45
  readAgent,
45
46
  removeAgent,
46
47
  listAgents,
48
+ dataDirectory,
47
49
  readEndpoints,
48
50
  saveEndpoint,
49
51
  removeEndpoint,
@@ -30,18 +30,19 @@ export type AgentSessionLimits = Partial<Pick<AgentSession, "memoryLimit" | "cpu
30
30
  */
31
31
  export type AgentSessionState = "stopped" | "starting" | "idle" | "running" | "waiting" | "error";
32
32
  export type AgentSessionInfo = AgentSession & {
33
- agentName: string;
34
- status: AgentSessionState;
33
+ readonly agentName: string;
34
+ readonly status: AgentSessionState;
35
35
  /** The backend's own id for the conversation (claude's session id, codex's thread id), once known. */
36
- nativeId: string | null;
36
+ readonly nativeId: string | null;
37
37
  /** Chosen through AgentModels; null for the backend's default. */
38
- model: string | null;
38
+ readonly model: string | null;
39
39
  };
40
40
  /** Where a session lives: sessions are named per agent. */
41
41
  export type SessionRef = {
42
42
  agentName: string;
43
43
  sessionName: string;
44
44
  };
45
+ /** @sealed */
45
46
  export interface AgentClient {
46
47
  /** PUT /v1/agents/:agentName — starts (or updates) the worker in the dimension. */
47
48
  createAgent(agent: Agent): Promise<Agent>;
@@ -83,13 +84,23 @@ export type AgentTranscriptMessage = Extract<AgentStreamingMessage, {
83
84
  export type AgentTranscriptRequest = {
84
85
  /** How many messages a page holds, about (a tool call and its result share a page); default 50, at most 500. */
85
86
  limit?: number;
86
- /** A page's `nextCursor`, for the page before it; omitted for the newest page. */
87
+ /**
88
+ * An ISO 8601 time: the page holds the newest messages from before it (a page's `nextBefore`, for
89
+ * the page before that one); omitted for the newest page.
90
+ */
91
+ before?: string;
92
+ /** @deprecated `before` by its old name: a page's `nextCursor` is its `nextBefore`. */
87
93
  cursor?: string;
88
94
  };
89
- /** Newest pages first; each page's messages oldest first. */
95
+ /**
96
+ * Newest pages first; each page's messages oldest first. `nextBefore` is the time of its oldest
97
+ * message, when there are older ones: every message of that time is on this page.
98
+ */
90
99
  export type AgentTranscriptPage = {
91
- messages: AgentTranscriptMessage[];
92
- nextCursor: string | null;
100
+ readonly messages: AgentTranscriptMessage[];
101
+ readonly nextBefore: string | null;
102
+ /** @deprecated `nextBefore` by its old name, for requests that give it as `cursor`. */
103
+ readonly nextCursor: string | null;
93
104
  };
94
105
  export type UserMessage = SDKUserMessage;
95
106
  /**
@@ -97,24 +108,24 @@ export type UserMessage = SDKUserMessage;
97
108
  * `can_use_tool` control request) isn't exported, so this mirrors it.
98
109
  */
99
110
  export type AgentPermissionRequest = {
100
- id: string;
101
- tool_name: string;
102
- input: Record<string, unknown>;
103
- tool_use_id: string;
111
+ readonly id: string;
112
+ readonly tool_name: string;
113
+ readonly input: Record<string, unknown>;
114
+ readonly tool_use_id: string;
104
115
  /** Why the agent asks, when it says (e.g. codex's reason for wanting network access). */
105
- decision_reason?: string;
116
+ readonly decision_reason?: string;
106
117
  };
107
118
  export type AgentPermissionResult = PermissionResult;
108
119
  type AskUserQuestion = AskUserQuestionInput["questions"][number];
109
120
  /** A question of claude's AskUserQuestion tool; codex's may come with any number of options. */
110
121
  export type AgentQuestion = Omit<AskUserQuestion, "options"> & {
111
- options: AskUserQuestion["options"][number][];
122
+ readonly options: AskUserQuestion["options"][number][];
112
123
  };
113
124
  /** The agent asking the user something (claude's AskUserQuestion, codex's request_user_input). */
114
125
  export type AgentQuestionRequest = {
115
- id: string;
116
- tool_use_id: string;
117
- questions: AgentQuestion[];
126
+ readonly id: string;
127
+ readonly tool_use_id: string;
128
+ readonly questions: AgentQuestion[];
118
129
  };
119
130
  /**
120
131
  * AskUserQuestion's own answer: question text -> the chosen label (labels joined with ", " for a
@@ -138,20 +149,22 @@ export type AgentBackgroundTask = SDKBackgroundTasksChangedMessage["tasks"][numb
138
149
  * the code the page shows, when `needsCode`).
139
150
  */
140
151
  export type AgentAuthStatus = {
141
- state: "signedIn";
152
+ readonly state: "signedIn";
142
153
  } | {
143
- state: "signedOut"; /** Why the last sign-in failed, if one did. */
144
- error?: string;
154
+ readonly state: "signedOut"; /** Why the last sign-in failed, if one did. */
155
+ readonly error?: string;
145
156
  } | {
146
- state: "pending";
147
- url: string;
148
- userCode?: string;
149
- needsCode: boolean;
157
+ readonly state: "pending";
158
+ readonly url: string;
159
+ readonly userCode?: string;
160
+ readonly needsCode: boolean;
150
161
  };
162
+ /** @sealed */
151
163
  export interface AgentProvision {
152
164
  /** POST /v1/agents/:agentName/backends/:backend/install — installs the backend in the dimension, or updates it. */
153
165
  install(agentName: string, backend: AgentBackend): Promise<void>;
154
166
  }
167
+ /** @sealed */
155
168
  export interface AgentEndpoint {
156
169
  /**
157
170
  * POST /v1/agents/:agentName/endpoint — the agent's worker, reachable from the client over the
@@ -163,6 +176,7 @@ export interface AgentEndpoint {
163
176
  */
164
177
  exposeAgent(agentName: string, scheme: SandboxEndpointScheme, allowedOrigins?: string[]): Promise<SandboxEndpoint>;
165
178
  }
179
+ /** @sealed */
166
180
  export interface AgentStream {
167
181
  /**
168
182
  * GET /v1/agents/:agentName/sessions/:sessionName/stream?after_id= — the complete messages after
@@ -172,18 +186,21 @@ export interface AgentStream {
172
186
  */
173
187
  getStream(ref: SessionRef, req: AgentStreamingRequest): AsyncGenerator<AgentStreamingMessage>;
174
188
  }
189
+ /** @sealed */
175
190
  export interface AgentTranscript {
176
191
  /**
177
- * GET /v1/agents/:agentName/sessions/:sessionName/transcript?limit=&cursor= — a page of the
192
+ * GET /v1/agents/:agentName/sessions/:sessionName/transcript?limit=&before= (or &cursor=, its old name) — a page of the
178
193
  * conversation, read from the backend's own storage a piece at a time, so a conversation of any
179
194
  * length can be paged through.
180
195
  */
181
196
  getTranscript(ref: SessionRef, req?: AgentTranscriptRequest): Promise<AgentTranscriptPage>;
182
197
  }
198
+ /** @sealed */
183
199
  export interface AgentInterrupt {
184
200
  /** POST /v1/agents/:agentName/sessions/:sessionName/interrupt — ends the current turn; pending permission requests are denied and questions dropped. */
185
201
  cancel(ref: SessionRef): Promise<void>;
186
202
  }
203
+ /** @sealed */
187
204
  export interface AgentInput {
188
205
  /**
189
206
  * POST /v1/agents/:agentName/sessions/:sessionName/messages — resolves once the agent confirms it
@@ -192,10 +209,12 @@ export interface AgentInput {
192
209
  */
193
210
  sendMessage(ref: SessionRef, message: UserMessage): Promise<void>;
194
211
  }
212
+ /** @sealed */
195
213
  export interface AgentSessionStatus {
196
214
  /** GET /v1/agents/:agentName/sessions/:sessionName/status */
197
215
  getSessionStatus(ref: SessionRef): Promise<AgentSessionState>;
198
216
  }
217
+ /** @sealed */
199
218
  export interface AgentMessageQueue {
200
219
  /**
201
220
  * GET /v1/agents/:agentName/sessions/:sessionName/queue — from the agent (claude's command
@@ -205,24 +224,28 @@ export interface AgentMessageQueue {
205
224
  */
206
225
  getLastUserMessages(ref: SessionRef): Promise<AgentLastUserMessage[]>;
207
226
  }
227
+ /** @sealed */
208
228
  export interface AgentPermissions {
209
229
  /** GET /v1/agents/:agentName/sessions/:sessionName/permission-requests — the ones still waiting. */
210
230
  listPermissionRequests(ref: SessionRef): Promise<AgentPermissionRequest[]>;
211
231
  /** POST /v1/agents/:agentName/sessions/:sessionName/permission-requests/:requestId */
212
232
  respondPermissionRequest(ref: SessionRef, requestId: string, result: AgentPermissionResult): Promise<void>;
213
233
  }
234
+ /** @sealed */
214
235
  export interface AgentQuestions {
215
236
  /** GET /v1/agents/:agentName/sessions/:sessionName/question-requests — the ones still waiting. */
216
237
  listQuestionRequests(ref: SessionRef): Promise<AgentQuestionRequest[]>;
217
238
  /** POST /v1/agents/:agentName/sessions/:sessionName/question-requests/:requestId */
218
239
  respondQuestionRequest(ref: SessionRef, requestId: string, response: AgentQuestionResponse): Promise<void>;
219
240
  }
241
+ /** @sealed */
220
242
  export interface AgentBackgroundTasks {
221
243
  /** GET /v1/agents/:agentName/sessions/:sessionName/background-tasks — those still running; none while the CLI isn't. */
222
244
  listBackgroundTasks(ref: SessionRef): Promise<AgentBackgroundTask[]>;
223
245
  /** POST /v1/agents/:agentName/sessions/:sessionName/background-tasks/:taskId/stop — ends the task. */
224
246
  stopTask(ref: SessionRef, taskId: string): Promise<void>;
225
247
  }
248
+ /** @sealed */
226
249
  export interface AgentAuth {
227
250
  /** GET /v1/agents/:agentName/sessions/:sessionName/auth */
228
251
  getAuthStatus(ref: SessionRef): Promise<AgentAuthStatus>;
@@ -236,6 +259,7 @@ export interface AgentAuth {
236
259
  /** POST /v1/agents/:agentName/sessions/:sessionName/auth/code — finishes a pending sign-in that needs the page's code. */
237
260
  submitAuthCode(ref: SessionRef, code: string): Promise<AgentAuthStatus>;
238
261
  }
262
+ /** @sealed */
239
263
  export interface AgentModels {
240
264
  /** GET /v1/agents/:agentName/sessions/:sessionName/models — starts the CLI if it isn't running. */
241
265
  listModels(ref: SessionRef): Promise<AgentModelInfo[]>;
@@ -246,8 +270,8 @@ export interface AgentModels {
246
270
  export type AgentSessionCapabilities = AgentStream & AgentTranscript & AgentInterrupt & AgentInput & AgentSessionStatus & AgentMessageQueue & AgentPermissions & AgentQuestions & AgentBackgroundTasks & AgentAuth & AgentModels;
247
271
  /** A message sent to a session, and whether its agent still holds it queued (not yet taken into a turn). */
248
272
  export type AgentLastUserMessage = {
249
- message: UserMessage;
250
- queued: boolean;
273
+ readonly message: UserMessage;
274
+ readonly queued: boolean;
251
275
  };
252
276
  /** The request can't be satisfied as sent (bad name, unknown backend, unknown after_id, ...). */
253
277
  export declare class AgentInvalidError extends Error {
@@ -0,0 +1,3 @@
1
+ export { createWorkerClient, type WorkerClient } from "./session/client.ts";
2
+ export { sharedConnector, type SharedConnector } from "./session/sharedConnection.ts";
3
+ export { EndpointLoginRequiredError, exeDevLoginUrl, workerSseConnector, type WorkerEndpoint } from "./session/workerSseConnection.ts";
@@ -0,0 +1,5 @@
1
+ // An agent's worker, reached through its endpoint (AgentService.exposeAgent): what a browser can
2
+ // import (hyperbrane/agent/client), since the root module brings the server side with it.
3
+ export { createWorkerClient } from "./session/client.js";
4
+ export { sharedConnector } from "./session/sharedConnection.js";
5
+ export { EndpointLoginRequiredError, exeDevLoginUrl, workerSseConnector } from "./session/workerSseConnection.js";
@@ -0,0 +1,13 @@
1
+ import type { Sandbox } from "../sandbox/api.ts";
2
+ import { type AgentTranscriptPage, type AgentTranscriptRequest } from "./api.ts";
3
+ /**
4
+ * A page of the session's transcript, read on `host` from the dimension's state directory (its host
5
+ * side) by the backend's own layout and parsing, so nothing has to run: one command, plus a read
6
+ * for each further chunk the rare page that needs more takes. Null for a session whose env moves the backend's home
7
+ * elsewhere (into the container, where only its worker reaches it).
8
+ */
9
+ export declare function readHostTranscript(host: Sandbox, { stateDirectory, agentName, sessionName }: {
10
+ stateDirectory: string;
11
+ agentName: string;
12
+ sessionName: string;
13
+ }, { limit, before, cursor }: AgentTranscriptRequest): Promise<AgentTranscriptPage | null>;
@@ -0,0 +1,190 @@
1
+ import { AgentNotFoundError } from "./api.js";
2
+ import { CHUNK_BYTES, PROBE_BYTES, PROBE_LINES, TIME_SLACK_MS, createFileTranscript, prefetchedFiles, sandboxFiles, transcriptPage, transcriptTime, } from "./session/fileTranscript.js";
3
+ import { backendHomePath, sessionRecordPath } from "./session/sessionStorage.js";
4
+ import { STORED_TRANSCRIPTS } from "./session/storedTranscripts.js";
5
+ /**
6
+ * In one round trip: the session's record, its backend's file (found as the backend lays them out),
7
+ * the stretches written again in it, where the page starts (for a time, by bisecting the rest of the
8
+ * file as FileTranscript.page does) and what comes before that (PREFETCH_BYTES of it). The same
9
+ * searches as fileTranscript.ts, here where the file is. Args: a JSON object, see readHostTranscript.
10
+ */
11
+ const READ_SCRIPT = String.raw `
12
+ import base64, datetime, fnmatch, glob, json, mmap, os, sys
13
+ args = json.loads(sys.argv[1])
14
+ CHUNK, SLACK, PROBE_LINES, PROBE_BYTES = args["chunk"], args["slack"], args["probeLines"], args["probeBytes"]
15
+ try:
16
+ with open(args["record"], "rb") as f:
17
+ record = json.load(f)
18
+ except FileNotFoundError:
19
+ print("{}")
20
+ sys.exit(0)
21
+ out = {"record": record}
22
+ backend = record.get("spec", {}).get("backend")
23
+ native_id = record.get("nativeId")
24
+ layout = args["layouts"].get(backend)
25
+
26
+ def entries(path):
27
+ try:
28
+ return sorted(os.listdir(path), reverse=True)
29
+ except OSError:
30
+ return []
31
+
32
+ def line_time(raw):
33
+ try:
34
+ value = json.loads(raw).get(layout["time"])
35
+ return datetime.datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp() * 1000
36
+ except Exception:
37
+ return None
38
+
39
+ def kept(start, end, skips):
40
+ parts = []
41
+ for a, b in sorted(skips):
42
+ if b <= start or a >= end:
43
+ continue
44
+ if a > start:
45
+ parts.append((start, a))
46
+ start = max(start, b)
47
+ if start < end:
48
+ parts.append((start, end))
49
+ return parts
50
+
51
+ def forward(m, start, end, skips):
52
+ for a, b in kept(start, end, skips):
53
+ position = a
54
+ for raw in m[a:b].split(b"\n"):
55
+ if raw:
56
+ yield position, raw
57
+ position += len(raw) + 1
58
+
59
+ def probe(m, start, limit, skips):
60
+ first, times = None, []
61
+ for line_start, raw in forward(m, start, min(limit, start + PROBE_BYTES), skips):
62
+ if line_start == start and start > 0:
63
+ continue
64
+ if line_start >= limit:
65
+ break
66
+ if first is None:
67
+ first = line_start
68
+ found = line_time(raw)
69
+ if found is None:
70
+ continue
71
+ times.append(found)
72
+ if len(times) == PROBE_LINES:
73
+ break
74
+ return None if first is None or not times else (first, min(times))
75
+
76
+ def seek(m, low, high, target, skips):
77
+ return narrow(m, low, high, target, skips)[1]
78
+
79
+ def narrow(m, low, high, target, skips):
80
+ while high - low > CHUNK:
81
+ middle = (low + high) // 2
82
+ inside = next((s for s in skips if s[0] <= middle < s[1]), None)
83
+ if inside:
84
+ if inside[1] >= high:
85
+ high = max(low, inside[0])
86
+ continue
87
+ middle = inside[1]
88
+ found = probe(m, middle, high, skips)
89
+ if not found:
90
+ break
91
+ if found[1] >= target:
92
+ high = found[0]
93
+ else:
94
+ low = found[0]
95
+ return low, high
96
+
97
+ def copies_of(m, size, marker):
98
+ copies, found, position = [], [], 0
99
+ while True:
100
+ at = m.find(marker.encode(), position)
101
+ if at < 0:
102
+ break
103
+ found.append(at)
104
+ position = at + 1
105
+ for index, at in enumerate(found):
106
+ start = m.rfind(b"\n", 0, at) + 1
107
+ if any(a <= start < b for a, b in copies):
108
+ continue
109
+ earlier, end = None, start - 1
110
+ while end > 0 and earlier is None:
111
+ begin = m.rfind(b"\n", 0, end) + 1
112
+ earlier = line_time(m[begin:end])
113
+ end = begin - 1
114
+ line_end = m.find(b"\n", start)
115
+ time = line_time(m[start : line_end if line_end >= 0 else size])
116
+ if earlier is None or time is None or time >= earlier:
117
+ continue
118
+ high = m.rfind(b"\n", 0, found[index + 1]) + 1 if index + 1 < len(found) else size
119
+ low, high = narrow(m, start + 1, high, earlier + 1, [])
120
+ end = next((s for s, raw in forward(m, low, high, []) if s > start and (line_time(raw) or -1) > earlier), high)
121
+ copies.append((start, end))
122
+ return copies
123
+
124
+ if native_id and layout:
125
+ name = layout["name"].replace("{id}", glob.escape(native_id))
126
+ dirs = [os.path.join(args["homes"][backend], layout["dir"])]
127
+ for _ in range(layout["depth"] - 1):
128
+ dirs = [os.path.join(d, e) for d in dirs for e in entries(d) if os.path.isdir(os.path.join(d, e))]
129
+ path = next((os.path.join(d, e) for d in dirs for e in entries(d) if fnmatch.fnmatchcase(e, name)), None)
130
+ size = os.path.getsize(path) if path else 0
131
+ if path and size == 0:
132
+ out.update(path=path, size=0, end=0, after=None, copies=[], pieces=[])
133
+ elif path:
134
+ with open(path, "rb") as f:
135
+ m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
136
+ copies = copies_of(m, size, layout["copies"]) if layout.get("copies") else []
137
+ end = size if args["before"] is None else seek(m, 0, size, args["before"] + SLACK, copies)
138
+ after = next((t for t in (line_time(raw) for _, raw in forward(m, end, min(size, end + PROBE_BYTES), copies)) if t is not None), None)
139
+ pieces, left = [], args["prefetch"]
140
+ for a, b in reversed(kept(0, end, copies)):
141
+ if left <= 0:
142
+ break
143
+ start = max(a, b - left)
144
+ pieces.append({"start": start, "bytes": base64.b64encode(m[start:b]).decode()})
145
+ left -= b - start
146
+ out.update(path=path, size=size, end=end, after=after, copies=[[a, b] for a, b in copies], pieces=pieces)
147
+ print(json.dumps(out))
148
+ `;
149
+ /** What's sent of the file with the record: a page and the slack around it, mostly, so it takes no more reads. */
150
+ const PREFETCH_BYTES = 8 * CHUNK_BYTES;
151
+ /**
152
+ * A page of the session's transcript, read on `host` from the dimension's state directory (its host
153
+ * side) by the backend's own layout and parsing, so nothing has to run: one command, plus a read
154
+ * for each further chunk the rare page that needs more takes. Null for a session whose env moves the backend's home
155
+ * elsewhere (into the container, where only its worker reaches it).
156
+ */
157
+ export async function readHostTranscript(host, { stateDirectory, agentName, sessionName }, { limit, before, cursor }) {
158
+ before ??= cursor; // its old name
159
+ const args = {
160
+ record: sessionRecordPath(stateDirectory, agentName, sessionName),
161
+ homes: Object.fromEntries(Object.keys(STORED_TRANSCRIPTS).map((backend) => [backend, backendHomePath(stateDirectory, backend)])),
162
+ layouts: Object.fromEntries(Object.entries(STORED_TRANSCRIPTS).map(([backend, stored]) => [backend, stored.layout])),
163
+ before: transcriptTime(before),
164
+ chunk: CHUNK_BYTES,
165
+ prefetch: PREFETCH_BYTES,
166
+ slack: TIME_SLACK_MS,
167
+ probeLines: PROBE_LINES,
168
+ probeBytes: PROBE_BYTES,
169
+ };
170
+ const result = await host.runCommand(["python3", "-c", READ_SCRIPT, JSON.stringify(args)]);
171
+ if (result.exitCode !== 0)
172
+ throw new Error(`reading session ${sessionName}'s transcript failed: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
173
+ const { record, path, size, end, after, copies, pieces } = JSON.parse(result.stdout);
174
+ if (!record)
175
+ throw new AgentNotFoundError(`session ${sessionName} does not exist`);
176
+ const stored = STORED_TRANSCRIPTS[record.spec.backend];
177
+ if (Object.keys(stored.homeEnvironment("")).some((name) => record.spec.env?.[name] !== undefined))
178
+ return null;
179
+ if (!record.nativeId)
180
+ return transcriptPage(null, { limit, before });
181
+ let files = sandboxFiles(host);
182
+ let found;
183
+ if (path !== undefined && size !== undefined && end !== undefined) {
184
+ const fetched = (pieces ?? []).map((piece) => ({ start: piece.start, bytes: new Uint8Array(Buffer.from(piece.bytes, "base64")) }));
185
+ files = prefetchedFiles(files, path, size, fetched);
186
+ found = { end, after: after ?? null, copies: (copies ?? []).map(([start, stop]) => ({ start, end: stop })) };
187
+ }
188
+ const transcript = createFileTranscript(files, async () => path ?? null, stored.lineMessages(record.nativeId), stored.layout);
189
+ return transcriptPage(transcript, { limit, before }, found);
190
+ }
@@ -0,0 +1,80 @@
1
+ import type { AgentBackgroundTasks, AgentInput, AgentInterrupt, AgentMessageQueue, AgentModels, AgentPermissionRequest, AgentPermissionResult, AgentQuestionRequest, AgentQuestionResponse, AgentSession, AgentSessionStatus, AgentStreamingMessage, SessionRef } from "../api.ts";
2
+ import type { FileTranscript, TranscriptFiles } from "./fileTranscript.ts";
3
+ /** A capability's calls for one session: those of api.ts, with the session already chosen. */
4
+ export type ForSession<Capability> = {
5
+ [K in keyof Capability]: Capability[K] extends (ref: SessionRef, ...args: infer Args) => infer Result ? (...args: Args) => Result : never;
6
+ };
7
+ /** What one session's CLI runs with. */
8
+ export type SessionLaunch = {
9
+ spec: AgentSession;
10
+ /** The backend's id for the conversation, from an earlier run, to resume it; null for a new one. */
11
+ nativeId: string | null;
12
+ cwd: string;
13
+ /** The model chosen through AgentModels; null for the backend's default. */
14
+ model: string | null;
15
+ /** Where backends are installed (see SessionBackend.install). */
16
+ installDir: string;
17
+ /** The worker's own environment, the CLI pointed at its home, and the session's. */
18
+ env: Record<string, string>;
19
+ };
20
+ /** What the CLI reports to, and asks of, the worker. */
21
+ export type SessionCallbacks = {
22
+ /** Appends to the session's stream. */
23
+ emit(message: AgentStreamingMessage): void;
24
+ /** Records the backend's conversation id, so a later run can resume it. */
25
+ setNativeId(id: string): void;
26
+ /** The decision on a tool call: someone's answer, or a denial once the turn is interrupted, the session stops, or `signal` aborts. */
27
+ requestPermission(request: Omit<AgentPermissionRequest, "id">, signal?: AbortSignal): Promise<AgentPermissionResult>;
28
+ /** The user's answers, or null once the turn is interrupted, the session stops, or `signal` aborts. */
29
+ askQuestions(request: Omit<AgentQuestionRequest, "id">, signal?: AbortSignal): Promise<AgentQuestionResponse | null>;
30
+ /** The CLI is gone (crashed, or closed); the next message starts it again. */
31
+ exited(error?: Error): void;
32
+ };
33
+ export type SessionContext = SessionLaunch & SessionCallbacks;
34
+ /** One session's running CLI: the capabilities it serves itself. */
35
+ export type RunningSession = ForSession<AgentInput & AgentInterrupt & AgentSessionStatus & AgentMessageQueue & AgentModels & AgentBackgroundTasks> & {
36
+ close(): Promise<void>;
37
+ };
38
+ /** What a CLI's sign-in commands run with. */
39
+ export type AuthEnvironment = {
40
+ installDir: string;
41
+ env: Record<string, string>;
42
+ cwd: string;
43
+ };
44
+ /** A sign-in in progress: the CLI's own login command. */
45
+ export interface LoginProcess {
46
+ /** Resolves once the CLI has shown where to sign in. */
47
+ prompt: Promise<{
48
+ url: string;
49
+ userCode?: string;
50
+ needsCode: boolean;
51
+ }>;
52
+ /** Resolves when the CLI signed in; rejects with why it didn't. */
53
+ done: Promise<void>;
54
+ /** Gives the CLI the code the sign-in page showed. */
55
+ submitCode(code: string): void;
56
+ cancel(): void;
57
+ }
58
+ /** A CLI's sign-in, through its own commands. The credentials it saves are fakes when a gateway holds the real ones. */
59
+ export type BackendAuth = {
60
+ signedIn(environment: AuthEnvironment): Promise<boolean>;
61
+ /** With `gatewaySignedIn` (the dimension's gateway holds the upstream's credential) it completes by itself: the gateway answers. */
62
+ login(environment: AuthEnvironment, gatewaySignedIn: boolean): LoginProcess;
63
+ };
64
+ /** What finds a session's conversation in its backend's storage, read through `files`. */
65
+ export type TranscriptSource = {
66
+ nativeId: string;
67
+ env: Record<string, string>;
68
+ files: TranscriptFiles;
69
+ };
70
+ /** An agent CLI, wired in agent/session/<backend>/: one file per capability. */
71
+ export type SessionBackend = {
72
+ /** Installs the backend under `installDir`, or updates it to the pinned version. Runs in the dimension. */
73
+ install(installDir: string): Promise<void>;
74
+ start(context: SessionContext): Promise<RunningSession>;
75
+ auth: BackendAuth;
76
+ /** The conversation `nativeId` in the backend's own storage, which needs no CLI running. */
77
+ transcript(source: TranscriptSource): FileTranscript;
78
+ /** The environment that points the CLI at `home` for everything it keeps (see SessionStorage.backendHome). */
79
+ homeEnvironment(home: string): Record<string, string>;
80
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Points claude at `home` for everything it keeps: its .claude.json too, which otherwise sits in the home directory. */
2
+ export declare function claudeHomeEnvironment(home: string): Record<string, string>;
@@ -0,0 +1,4 @@
1
+ /** Points claude at `home` for everything it keeps: its .claude.json too, which otherwise sits in the home directory. */
2
+ export function claudeHomeEnvironment(home) {
3
+ return { CLAUDE_CONFIG_DIR: home };
4
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentTranscriptMessage } from "../../api.ts";
2
+ import type { TranscriptSource } from "../backend.ts";
3
+ import { type FileTranscript, type TranscriptLayout } from "../fileTranscript.ts";
4
+ /**
5
+ * The conversation from claude's own session file, <config>/projects/<cwd>/<session>.jsonl. Its user
6
+ * and assistant entries are the messages the stream carried, with their uuids (claude keeps the one
7
+ * a prompt was sent with); the rest (attachments, queue bookkeeping, a subagent's sidechain, meta
8
+ * notes) isn't the conversation.
9
+ */
10
+ export declare function claudeTranscript({ nativeId, env, files }: TranscriptSource): FileTranscript;
11
+ /** projects/<cwd>/<session>.jsonl in claude's config directory. */
12
+ export declare const CLAUDE_TRANSCRIPT_LAYOUT: TranscriptLayout;
13
+ export declare function claudeLineMessages(line: unknown): AgentTranscriptMessage[];