pi-cursor-agent 0.1.0

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 (117) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +45 -0
  3. package/package.json +50 -0
  4. package/src/__generated__/agent/v1/agent_pb.ts +4642 -0
  5. package/src/__generated__/agent/v1/agent_service_connect.ts +71 -0
  6. package/src/__generated__/agent/v1/apply_agent_diff_tool_pb.ts +317 -0
  7. package/src/__generated__/agent/v1/ask_question_tool_pb.ts +588 -0
  8. package/src/__generated__/agent/v1/background_shell_exec_pb.ts +245 -0
  9. package/src/__generated__/agent/v1/computer_use_tool_pb.ts +959 -0
  10. package/src/__generated__/agent/v1/control_service_connect.ts +144 -0
  11. package/src/__generated__/agent/v1/control_service_pb.ts +1308 -0
  12. package/src/__generated__/agent/v1/create_plan_tool_pb.ts +366 -0
  13. package/src/__generated__/agent/v1/cursor_packages_pb.ts +278 -0
  14. package/src/__generated__/agent/v1/cursor_rules_pb.ts +301 -0
  15. package/src/__generated__/agent/v1/delete_exec_pb.ts +443 -0
  16. package/src/__generated__/agent/v1/delete_tool_pb.ts +52 -0
  17. package/src/__generated__/agent/v1/diagnostics_exec_pb.ts +399 -0
  18. package/src/__generated__/agent/v1/edit_tool_pb.ts +497 -0
  19. package/src/__generated__/agent/v1/exa_fetch_tool_pb.ts +472 -0
  20. package/src/__generated__/agent/v1/exa_search_tool_pb.ts +484 -0
  21. package/src/__generated__/agent/v1/exec_pb.ts +1271 -0
  22. package/src/__generated__/agent/v1/exec_service_connect.ts +14 -0
  23. package/src/__generated__/agent/v1/fetch_tool_pb.ts +242 -0
  24. package/src/__generated__/agent/v1/generate_image_tool_pb.ts +230 -0
  25. package/src/__generated__/agent/v1/glob_tool_pb.ts +248 -0
  26. package/src/__generated__/agent/v1/grep_exec_pb.ts +690 -0
  27. package/src/__generated__/agent/v1/grep_tool_pb.ts +52 -0
  28. package/src/__generated__/agent/v1/kv_pb.ts +281 -0
  29. package/src/__generated__/agent/v1/ls_exec_pb.ts +295 -0
  30. package/src/__generated__/agent/v1/ls_tool_pb.ts +52 -0
  31. package/src/__generated__/agent/v1/mcp_pb.ts +302 -0
  32. package/src/__generated__/agent/v1/mcp_resource_tool_pb.ts +688 -0
  33. package/src/__generated__/agent/v1/mcp_tool_pb.ts +630 -0
  34. package/src/__generated__/agent/v1/private_worker_bridge_external_connect.ts +26 -0
  35. package/src/__generated__/agent/v1/read_exec_pb.ts +412 -0
  36. package/src/__generated__/agent/v1/read_lints_tool_pb.ts +384 -0
  37. package/src/__generated__/agent/v1/read_tool_pb.ts +342 -0
  38. package/src/__generated__/agent/v1/record_screen_tool_pb.ts +376 -0
  39. package/src/__generated__/agent/v1/reflect_tool_pb.ts +236 -0
  40. package/src/__generated__/agent/v1/repo_pb.ts +154 -0
  41. package/src/__generated__/agent/v1/report_bugfix_results_tool_pb.ts +305 -0
  42. package/src/__generated__/agent/v1/request_context_exec_pb.ts +528 -0
  43. package/src/__generated__/agent/v1/sandbox_pb.ts +125 -0
  44. package/src/__generated__/agent/v1/selected_context_pb.ts +2272 -0
  45. package/src/__generated__/agent/v1/semsearch_tool_pb.ts +230 -0
  46. package/src/__generated__/agent/v1/setup_vm_environment_tool_pb.ts +168 -0
  47. package/src/__generated__/agent/v1/shell_exec_pb.ts +1195 -0
  48. package/src/__generated__/agent/v1/shell_tool_pb.ts +176 -0
  49. package/src/__generated__/agent/v1/start_grind_execution_tool_pb.ts +212 -0
  50. package/src/__generated__/agent/v1/start_grind_planning_tool_pb.ts +212 -0
  51. package/src/__generated__/agent/v1/subagents_pb.ts +1106 -0
  52. package/src/__generated__/agent/v1/switch_mode_tool_pb.ts +429 -0
  53. package/src/__generated__/agent/v1/todo_tool_pb.ts +551 -0
  54. package/src/__generated__/agent/v1/utils_pb.ts +348 -0
  55. package/src/__generated__/agent/v1/web_fetch_tool_pb.ts +429 -0
  56. package/src/__generated__/agent/v1/web_search_tool_pb.ts +466 -0
  57. package/src/__generated__/agent/v1/write_exec_pb.ts +379 -0
  58. package/src/__generated__/agent/v1/write_shell_stdin_tool_pb.ts +224 -0
  59. package/src/__generated__/aiserver/v1/aiserver_service_connect.ts +40 -0
  60. package/src/api/agent-service.ts +55 -0
  61. package/src/api/ai-service.ts +42 -0
  62. package/src/api/auth.ts +74 -0
  63. package/src/index.ts +101 -0
  64. package/src/lib/agent-store/disk.ts +139 -0
  65. package/src/lib/agent-store/index.ts +72 -0
  66. package/src/lib/agent-store/json-blob-store.ts +47 -0
  67. package/src/lib/auth.ts +135 -0
  68. package/src/lib/backoff.ts +32 -0
  69. package/src/lib/env.ts +3 -0
  70. package/src/lib/heartbeat.ts +21 -0
  71. package/src/pi/agent-store.ts +102 -0
  72. package/src/pi/env.ts +11 -0
  73. package/src/pi/executors/delete.ts +129 -0
  74. package/src/pi/executors/grep.ts +238 -0
  75. package/src/pi/executors/hook.ts +64 -0
  76. package/src/pi/executors/ls.ts +107 -0
  77. package/src/pi/executors/read.ts +73 -0
  78. package/src/pi/executors/request-context.ts +120 -0
  79. package/src/pi/executors/shell-stream.ts +136 -0
  80. package/src/pi/executors/shell.ts +157 -0
  81. package/src/pi/executors/stubs.ts +173 -0
  82. package/src/pi/executors/write.ts +189 -0
  83. package/src/pi/local-resource-provider/index.ts +10 -0
  84. package/src/pi/local-resource-provider/provider.ts +98 -0
  85. package/src/pi/local-resource-provider/types.ts +110 -0
  86. package/src/pi/model-mapping.ts +115 -0
  87. package/src/pi/model-override.ts +110 -0
  88. package/src/pi/model.ts +61 -0
  89. package/src/pi/request-builder.ts +279 -0
  90. package/src/pi/utils/tool-result.ts +35 -0
  91. package/src/stream.ts +386 -0
  92. package/src/tool-host.ts +44 -0
  93. package/src/vendor/agent-client/checkpoint-controller.ts +34 -0
  94. package/src/vendor/agent-client/connect.ts +348 -0
  95. package/src/vendor/agent-client/exec-controller.ts +102 -0
  96. package/src/vendor/agent-client/index.ts +25 -0
  97. package/src/vendor/agent-client/interaction-controller.ts +96 -0
  98. package/src/vendor/agent-client/split-stream.ts +143 -0
  99. package/src/vendor/agent-core/index.ts +9 -0
  100. package/src/vendor/agent-core/interaction-conversion.ts +558 -0
  101. package/src/vendor/agent-exec/controlled.ts +104 -0
  102. package/src/vendor/agent-exec/index.ts +45 -0
  103. package/src/vendor/agent-exec/registry-resource-accessor.ts +39 -0
  104. package/src/vendor/agent-exec/resources.ts +121 -0
  105. package/src/vendor/agent-exec/serialization.ts +22 -0
  106. package/src/vendor/agent-exec/simple-controlled-exec-manager.ts +161 -0
  107. package/src/vendor/agent-kv/agent-store.ts +115 -0
  108. package/src/vendor/agent-kv/blob-store.ts +36 -0
  109. package/src/vendor/agent-kv/controlled.ts +117 -0
  110. package/src/vendor/agent-kv/index.ts +15 -0
  111. package/src/vendor/agent-kv/serde.ts +44 -0
  112. package/src/vendor/local-exec/common.ts +19 -0
  113. package/src/vendor/local-exec/git-executor.ts +37 -0
  114. package/src/vendor/local-exec/git-helpers.ts +79 -0
  115. package/src/vendor/local-exec/index.ts +8 -0
  116. package/src/vendor/utils/index.ts +5 -0
  117. package/src/vendor/utils/map-writable.ts +34 -0
@@ -0,0 +1,44 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { SimpleControlledExecManager } from "./vendor/agent-exec";
3
+ import type { McpToolDefinition } from "./__generated__/agent/v1/mcp_pb";
4
+ import {
5
+ LocalResourceProvider,
6
+ type PiToolContext,
7
+ type ToolExecEvent,
8
+ } from "./pi/local-resource-provider";
9
+
10
+ export type {
11
+ ToolExecEvent,
12
+ ToolExecStartEvent,
13
+ ToolExecEndEvent,
14
+ } from "./pi/local-resource-provider";
15
+
16
+ export interface ToolHostOptions {
17
+ cwd: string;
18
+ signal?: AbortSignal;
19
+ getActiveTools: () => Set<string>;
20
+ getCtx: () => ExtensionContext | null;
21
+ requestContextTools?: McpToolDefinition[];
22
+ onToolExec?: (event: ToolExecEvent) => void;
23
+ }
24
+
25
+ export class ToolHost {
26
+ readonly execManager: SimpleControlledExecManager;
27
+
28
+ constructor(options: ToolHostOptions) {
29
+ const ctx: PiToolContext = {
30
+ cwd: options.cwd,
31
+ ...(options.signal ? { signal: options.signal } : {}),
32
+ getActiveTools: options.getActiveTools,
33
+ getCtx: options.getCtx,
34
+ onToolExec: (event) => options.onToolExec?.(event),
35
+ };
36
+
37
+ const provider = new LocalResourceProvider({
38
+ ctx,
39
+ requestContextTools: options.requestContextTools ?? [],
40
+ });
41
+
42
+ this.execManager = SimpleControlledExecManager.fromResources(provider);
43
+ }
44
+ }
@@ -0,0 +1,34 @@
1
+ import type { ConversationStateStructure } from "../../__generated__/agent/v1/agent_pb";
2
+
3
+ export interface CheckpointHandler {
4
+ handleCheckpoint(
5
+ ctx: unknown,
6
+ checkpoint: ConversationStateStructure,
7
+ ): Promise<void>;
8
+ getLatestCheckpoint?: () => ConversationStateStructure | undefined;
9
+ }
10
+
11
+ export class CheckpointController {
12
+ private readonly checkpointStream: AsyncIterable<ConversationStateStructure>;
13
+ private readonly checkpointHandler: CheckpointHandler;
14
+ private readonly ctx: unknown;
15
+
16
+ constructor(
17
+ checkpointStream: AsyncIterable<ConversationStateStructure>,
18
+ checkpointHandler: CheckpointHandler,
19
+ ctx: unknown,
20
+ ) {
21
+ this.checkpointStream = checkpointStream;
22
+ this.checkpointHandler = checkpointHandler;
23
+ this.ctx = ctx;
24
+ }
25
+
26
+ async run(): Promise<void> {
27
+ const ctx = this.ctx;
28
+ const promises: Promise<void>[] = [];
29
+ for await (const checkpoint of this.checkpointStream) {
30
+ promises.push(this.checkpointHandler.handleCheckpoint(ctx, checkpoint));
31
+ }
32
+ await Promise.all(promises);
33
+ }
34
+ }
@@ -0,0 +1,348 @@
1
+ import { createWritableIterable } from "@connectrpc/connect/protocol";
2
+ import {
3
+ AgentClientMessage,
4
+ AgentRunRequest,
5
+ type AgentServerMessage,
6
+ ClientHeartbeat,
7
+ ConversationAction,
8
+ type ConversationStateStructure,
9
+ type InteractionResponse,
10
+ type ModelDetails,
11
+ ResumeAction,
12
+ } from "../../__generated__/agent/v1/agent_pb";
13
+ import type { McpTools } from "../../__generated__/agent/v1/mcp_pb";
14
+ import {
15
+ ExecClientControlMessage,
16
+ ExecClientMessage,
17
+ } from "../../__generated__/agent/v1/exec_pb";
18
+ import type { KvClientMessage } from "../../__generated__/agent/v1/kv_pb";
19
+ import { SimpleControlledExecManager } from "../agent-exec/simple-controlled-exec-manager";
20
+ import type { ResourceAccessor } from "../agent-exec/registry-resource-accessor";
21
+ import { MapWritable } from "../utils";
22
+ import { ControlledKvManager, type BlobStore } from "../agent-kv";
23
+ import {
24
+ splitStream,
25
+ type StallDetector,
26
+ type SplitChannels,
27
+ } from "./split-stream";
28
+ import {
29
+ ClientExecController,
30
+ LostConnection,
31
+ } from "./exec-controller";
32
+ import {
33
+ ClientInteractionController,
34
+ type InteractionListener,
35
+ } from "./interaction-controller";
36
+ import {
37
+ CheckpointController,
38
+ type CheckpointHandler,
39
+ } from "./checkpoint-controller";
40
+
41
+ export interface AgentRpcClient {
42
+ run(
43
+ input: AsyncIterable<AgentClientMessage>,
44
+ options?: { signal?: AbortSignal; headers?: Record<string, string> },
45
+ ): AsyncIterable<AgentServerMessage>;
46
+ }
47
+
48
+ export interface AgentConnectRunOptions {
49
+ interactionListener: InteractionListener;
50
+ resources: ResourceAccessor;
51
+ blobStore: BlobStore;
52
+ checkpointHandler: CheckpointHandler;
53
+ signal?: AbortSignal;
54
+ headers?: Record<string, string>;
55
+ onConnectionStateChange?: (state: { state: "reconnecting" | "connected" }) => void;
56
+ }
57
+
58
+ const HEARTBEAT_INTERVAL_MS = 5_000;
59
+ const MAX_RETRY_ATTEMPTS = 5;
60
+
61
+ function createNoopStallDetector(): StallDetector {
62
+ return {
63
+ onServerSentHeartbeat() {},
64
+ reset() {},
65
+ onStreamEnded() {},
66
+ };
67
+ }
68
+
69
+ function isRetriableError(error: unknown): boolean {
70
+ if (error instanceof LostConnection) return true;
71
+ if (error instanceof Error && error.message.includes("NGHTTP2")) return true;
72
+ return false;
73
+ }
74
+
75
+ async function backoff(attempt: number, signal?: AbortSignal): Promise<void> {
76
+ const delay = Math.min(1_000 * 2 ** attempt, 30_000);
77
+ return new Promise<void>((resolve) => {
78
+ const timer = setTimeout(resolve, delay);
79
+ signal?.addEventListener("abort", () => {
80
+ clearTimeout(timer);
81
+ resolve();
82
+ }, { once: true });
83
+ });
84
+ }
85
+
86
+ export class AgentConnectClient {
87
+ private readonly client: AgentRpcClient;
88
+
89
+ constructor(client: AgentRpcClient) {
90
+ this.client = client;
91
+ }
92
+
93
+ /**
94
+ * Public entry point with centralized retry and resume logic.
95
+ *
96
+ * Retry behavior:
97
+ * - Transport/stall errors: retry indefinitely with exponential backoff
98
+ * - Server errors (high load): retry up to MAX_SERVER_ERROR_RETRIES times
99
+ * - Non-retriable errors: surface immediately
100
+ *
101
+ * Checkpoint behavior:
102
+ * - If a NEW checkpoint was received before failure, resume from checkpoint
103
+ * - If NO checkpoint was received, resend the original action (prevents message loss)
104
+ */
105
+ async run(
106
+ initialRequest: AgentClientMessage,
107
+ options: AgentConnectRunOptions,
108
+ ): Promise<void> {
109
+ const runRequest = initialRequest.message.value as AgentRunRequest;
110
+
111
+ // Retry state
112
+ let currentState = runRequest.conversationState;
113
+ let currentAction = runRequest.action!;
114
+ const modelDetails = runRequest.modelDetails;
115
+ const mcpTools = runRequest.mcpTools;
116
+ const conversationId = runRequest.conversationId;
117
+ let attempt = 0;
118
+ const receivedNewCheckpoint = { value: false };
119
+
120
+ // Helper: switch to ResumeAction if we received a checkpoint
121
+ const maybeResumeFromCheckpoint = () => {
122
+ if (!receivedNewCheckpoint.value) return;
123
+ const checkpoint = options.checkpointHandler.getLatestCheckpoint?.();
124
+ if (!checkpoint) return;
125
+ currentState = checkpoint;
126
+ currentAction = new ConversationAction({
127
+ action: { case: "resumeAction", value: new ResumeAction() },
128
+ });
129
+ };
130
+
131
+ // Wrap checkpoint handler to track when we receive new checkpoints
132
+ const trackingCheckpointHandler: CheckpointHandler = {
133
+ async handleCheckpoint(
134
+ ctx: unknown,
135
+ checkpoint: ConversationStateStructure,
136
+ ): Promise<void> {
137
+ receivedNewCheckpoint.value = true;
138
+ return options.checkpointHandler.handleCheckpoint(ctx, checkpoint);
139
+ },
140
+ getLatestCheckpoint: () =>
141
+ options.checkpointHandler.getLatestCheckpoint?.(),
142
+ };
143
+
144
+ // Main retry loop
145
+ while (true) {
146
+ if (options.signal?.aborted) {
147
+ throw new Error("Request cancelled");
148
+ }
149
+
150
+ // Reset per-attempt flags
151
+ receivedNewCheckpoint.value = false;
152
+
153
+ try {
154
+ const request = this.buildRequest(
155
+ currentState,
156
+ currentAction,
157
+ modelDetails,
158
+ mcpTools,
159
+ conversationId,
160
+ );
161
+
162
+ await this.runInternal(request, {
163
+ ...options,
164
+ checkpointHandler: trackingCheckpointHandler,
165
+ });
166
+ return;
167
+ } catch (error) {
168
+ if (!isRetriableError(error) || attempt >= MAX_RETRY_ATTEMPTS) {
169
+ throw error;
170
+ }
171
+
172
+ // Retry: notify UI, maybe resume from checkpoint, backoff
173
+ options.onConnectionStateChange?.({ state: "reconnecting" });
174
+ maybeResumeFromCheckpoint();
175
+
176
+ attempt++;
177
+ await backoff(attempt, options.signal);
178
+ }
179
+ }
180
+ }
181
+
182
+ private buildRequest(
183
+ conversationState: ConversationStateStructure | undefined,
184
+ action: ConversationAction,
185
+ modelDetails: ModelDetails | undefined,
186
+ mcpTools: McpTools | undefined,
187
+ conversationId: string | undefined,
188
+ ): AgentClientMessage {
189
+ return new AgentClientMessage({
190
+ message: {
191
+ case: "runRequest",
192
+ value: new AgentRunRequest({
193
+ ...(conversationState ? { conversationState } : {}),
194
+ action,
195
+ ...(modelDetails ? { modelDetails } : {}),
196
+ ...(mcpTools ? { mcpTools } : {}),
197
+ ...(conversationId ? { conversationId } : {}),
198
+ }),
199
+ },
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Internal implementation that may throw any error type.
205
+ * All errors are caught and converted at the public `run` boundary.
206
+ */
207
+ private async runInternal(
208
+ initialRequest: AgentClientMessage,
209
+ options: AgentConnectRunOptions,
210
+ ): Promise<void> {
211
+ const controlledExecManager = SimpleControlledExecManager.fromResources(
212
+ options.resources,
213
+ );
214
+
215
+ const stallDetector = createNoopStallDetector();
216
+
217
+ const baseRequestStream = createWritableIterable<AgentClientMessage>();
218
+
219
+ void baseRequestStream.write(initialRequest);
220
+
221
+ const runOptions: { signal?: AbortSignal; headers?: Record<string, string> } = {};
222
+ if (options.signal) runOptions.signal = options.signal;
223
+ if (options.headers) runOptions.headers = options.headers;
224
+
225
+ const response = this.client.run(baseRequestStream, runOptions);
226
+
227
+ const channels: SplitChannels = splitStream(
228
+ response,
229
+ stallDetector,
230
+ () => options.onConnectionStateChange?.({ state: "connected" }),
231
+ );
232
+
233
+ // Heartbeat sender using setTimeout (not setInterval)
234
+ let heartbeatTimeout: ReturnType<typeof setTimeout> | undefined;
235
+
236
+ const scheduleHeartbeat = () => {
237
+ heartbeatTimeout = setTimeout(() => {
238
+ baseRequestStream
239
+ .write(
240
+ new AgentClientMessage({
241
+ message: {
242
+ case: "clientHeartbeat",
243
+ value: new ClientHeartbeat(),
244
+ },
245
+ }),
246
+ )
247
+ .then(scheduleHeartbeat)
248
+ .catch(() => {});
249
+ }, HEARTBEAT_INTERVAL_MS);
250
+ };
251
+
252
+ const clearHeartbeat = () => {
253
+ if (heartbeatTimeout !== undefined) {
254
+ clearTimeout(heartbeatTimeout);
255
+ heartbeatTimeout = undefined;
256
+ }
257
+ };
258
+
259
+ scheduleHeartbeat();
260
+
261
+ try {
262
+ const execOutputStream = new MapWritable<
263
+ ExecClientMessage | ExecClientControlMessage,
264
+ AgentClientMessage
265
+ >(baseRequestStream, (message) => {
266
+ if (message instanceof ExecClientMessage) {
267
+ return new AgentClientMessage({
268
+ message: { case: "execClientMessage", value: message },
269
+ });
270
+ }
271
+ if (message instanceof ExecClientControlMessage) {
272
+ return new AgentClientMessage({
273
+ message: { case: "execClientControlMessage", value: message },
274
+ });
275
+ }
276
+ throw new Error("Unknown exec message type");
277
+ });
278
+
279
+ const kvOutputStream = new MapWritable<
280
+ KvClientMessage,
281
+ AgentClientMessage
282
+ >(
283
+ baseRequestStream,
284
+ (message) =>
285
+ new AgentClientMessage({
286
+ message: { case: "kvClientMessage", value: message },
287
+ }),
288
+ );
289
+
290
+ const queryResponseStream = new MapWritable<
291
+ InteractionResponse,
292
+ AgentClientMessage
293
+ >(
294
+ baseRequestStream,
295
+ (response) =>
296
+ new AgentClientMessage({
297
+ message: { case: "interactionResponse", value: response },
298
+ }),
299
+ );
300
+
301
+ const interactionController = new ClientInteractionController(
302
+ channels.interactionStream,
303
+ options.interactionListener,
304
+ queryResponseStream,
305
+ );
306
+
307
+ const execController = new ClientExecController(
308
+ channels.execStream,
309
+ execOutputStream,
310
+ controlledExecManager,
311
+ );
312
+
313
+ const kvManager = new ControlledKvManager(
314
+ channels.kvStream,
315
+ kvOutputStream,
316
+ options.blobStore,
317
+ );
318
+
319
+ const checkpointController = new CheckpointController(
320
+ channels.checkpointStream,
321
+ options.checkpointHandler,
322
+ null,
323
+ );
324
+
325
+ const ctx = null;
326
+
327
+ const results = await Promise.allSettled([
328
+ channels.done.finally(() => {
329
+ clearHeartbeat();
330
+ execOutputStream.close();
331
+ }),
332
+ execController.run(ctx),
333
+ interactionController.run(ctx),
334
+ checkpointController.run(),
335
+ kvManager.run(ctx),
336
+ ]);
337
+
338
+ for (const result of results) {
339
+ if (result.status === "rejected") {
340
+ throw result.reason;
341
+ }
342
+ }
343
+ } finally {
344
+ clearHeartbeat();
345
+ baseRequestStream.close();
346
+ }
347
+ }
348
+ }
@@ -0,0 +1,102 @@
1
+ import { ConnectError, Code } from "@connectrpc/connect";
2
+ import {
3
+ ExecServerControlMessage,
4
+ type ExecServerMessage,
5
+ type ExecClientMessage,
6
+ type ExecClientControlMessage,
7
+ } from "../../__generated__/agent/v1/exec_pb";
8
+ import { WriteIterableClosedError } from "../utils";
9
+
10
+ export interface Writable<T> {
11
+ write(value: T): Promise<void>;
12
+ }
13
+
14
+ export interface ControlledExecManager {
15
+ handle(
16
+ ctx: unknown,
17
+ message: ExecServerMessage,
18
+ ): AsyncIterable<ExecClientMessage | ExecClientControlMessage>;
19
+ handleControlMessage(message: ExecServerControlMessage): void;
20
+ }
21
+
22
+ export class LostConnection extends Error {
23
+ constructor(message: string) {
24
+ super(message);
25
+ this.name = "LostConnection";
26
+ }
27
+ }
28
+
29
+ export class ClientExecController {
30
+ private readonly serverStream: AsyncIterable<
31
+ ExecServerMessage | ExecServerControlMessage
32
+ >;
33
+ private readonly clientStream: Writable<
34
+ ExecClientMessage | ExecClientControlMessage
35
+ >;
36
+ private readonly controlledExecManager: ControlledExecManager;
37
+
38
+ constructor(
39
+ serverStream: AsyncIterable<ExecServerMessage | ExecServerControlMessage>,
40
+ clientStream: Writable<ExecClientMessage | ExecClientControlMessage>,
41
+ controlledExecManager: ControlledExecManager,
42
+ ) {
43
+ this.serverStream = serverStream;
44
+ this.clientStream = clientStream;
45
+ this.controlledExecManager = controlledExecManager;
46
+ }
47
+
48
+ async run(ctx: unknown): Promise<void> {
49
+ const pendingPromises: Promise<void>[] = [];
50
+ try {
51
+ for await (const message of this.serverStream) {
52
+ if (message instanceof ExecServerControlMessage) {
53
+ this.controlledExecManager.handleControlMessage(message);
54
+ continue;
55
+ }
56
+
57
+ const serverMessage = message as ExecServerMessage;
58
+
59
+ const promise = (async () => {
60
+ const stream = this.controlledExecManager.handle(ctx, serverMessage);
61
+ for await (const result of stream) {
62
+ await this.clientStream.write(result);
63
+ }
64
+ })();
65
+
66
+ pendingPromises.push(promise);
67
+ }
68
+
69
+ await Promise.all(pendingPromises);
70
+ } catch (error) {
71
+ if (
72
+ error instanceof ConnectError &&
73
+ error.rawMessage === "protocol error: missing EndStreamResponse"
74
+ ) {
75
+ throw new LostConnection(error.message);
76
+ } else if (error instanceof ConnectError && error.code === Code.Aborted) {
77
+ const cause = error.cause;
78
+ if (
79
+ cause instanceof Error &&
80
+ "code" in cause &&
81
+ typeof (cause as any).code === "string" &&
82
+ ((cause as any).code as string).includes("ERR_STREAM_WRITE_AFTER_END")
83
+ ) {
84
+ throw new LostConnection(error.message);
85
+ }
86
+ } else if (error instanceof WriteIterableClosedError) {
87
+ throw new LostConnection(error.message);
88
+ } else if (
89
+ error instanceof ConnectError &&
90
+ error.code === Code.Internal
91
+ ) {
92
+ const cause = error.cause;
93
+ if (
94
+ cause instanceof Error &&
95
+ cause.message.includes("NGHTTP2_PROTOCOL_ERROR")
96
+ ) {
97
+ throw new LostConnection(error.message);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,25 @@
1
+ export {
2
+ AgentConnectClient,
3
+ type AgentRpcClient,
4
+ type AgentConnectRunOptions,
5
+ } from "./connect";
6
+ export {
7
+ CheckpointController,
8
+ type CheckpointHandler,
9
+ } from "./checkpoint-controller";
10
+ export {
11
+ ClientExecController,
12
+ LostConnection,
13
+ type ControlledExecManager,
14
+ } from "./exec-controller";
15
+ export {
16
+ ClientInteractionController,
17
+ type InteractionListener,
18
+ } from "./interaction-controller";
19
+ export {
20
+ splitStream,
21
+ type ExecMessage,
22
+ type InteractionMessage,
23
+ type SplitChannels,
24
+ type StallDetector,
25
+ } from "./split-stream";
@@ -0,0 +1,96 @@
1
+ import type {
2
+ InteractionQuery,
3
+ InteractionResponse,
4
+ InteractionUpdate,
5
+ } from "../../__generated__/agent/v1/agent_pb";
6
+ import {
7
+ convertProtoToInteractionUpdate,
8
+ convertProtoToInteractionQuery,
9
+ convertInteractionResponseToProto,
10
+ type CoreInteractionUpdate,
11
+ type CoreInteractionQuery,
12
+ type CoreInteractionResponse,
13
+ } from "../agent-core/interaction-conversion";
14
+ import type { InteractionMessage } from "./split-stream";
15
+
16
+ export interface Writable<T> {
17
+ write(value: T): Promise<void>;
18
+ }
19
+
20
+ export interface InteractionListener {
21
+ sendUpdate(ctx: unknown, update: CoreInteractionUpdate): Promise<void>;
22
+ query(
23
+ ctx: unknown,
24
+ query: CoreInteractionQuery,
25
+ ): Promise<CoreInteractionResponse>;
26
+ }
27
+
28
+ export class ClientInteractionController {
29
+ private readonly interactionStream: AsyncIterable<InteractionMessage>;
30
+ private readonly interactionListener: InteractionListener;
31
+ private readonly queryResponseStream: Writable<InteractionResponse>;
32
+
33
+ constructor(
34
+ interactionStream: AsyncIterable<InteractionMessage>,
35
+ interactionListener: InteractionListener,
36
+ queryResponseStream: Writable<InteractionResponse>,
37
+ ) {
38
+ this.interactionStream = interactionStream;
39
+ this.interactionListener = interactionListener;
40
+ this.queryResponseStream = queryResponseStream;
41
+ }
42
+
43
+ async run(ctx: unknown): Promise<void> {
44
+ let promise = Promise.resolve();
45
+ let firstError: Error | undefined;
46
+
47
+ for await (const message of this.interactionStream) {
48
+ if (message.case === "interactionQuery") {
49
+ this.handleInteractionQuery(ctx, message.value);
50
+ } else if (message.case === "interactionUpdate") {
51
+ promise = promise
52
+ .then(() => this.handleInteractionUpdate(ctx, message.value))
53
+ .catch((error: unknown) => {
54
+ console.error("Error handling interaction update", error);
55
+ firstError ??=
56
+ error instanceof Error ? error : new Error(String(error));
57
+ });
58
+ }
59
+ }
60
+
61
+ await promise;
62
+ if (firstError !== undefined) {
63
+ throw firstError;
64
+ }
65
+ }
66
+
67
+ private async handleInteractionUpdate(
68
+ ctx: unknown,
69
+ update: InteractionUpdate,
70
+ ): Promise<void> {
71
+ const coreUpdate = convertProtoToInteractionUpdate(update);
72
+ if (coreUpdate) {
73
+ await this.interactionListener.sendUpdate(ctx, coreUpdate);
74
+ }
75
+ }
76
+
77
+ private handleInteractionQuery(
78
+ ctx: unknown,
79
+ queryProto: InteractionQuery,
80
+ ): void {
81
+ const coreQuery = convertProtoToInteractionQuery(queryProto);
82
+ void this.interactionListener
83
+ .query(ctx, coreQuery)
84
+ .then((response) => {
85
+ const responseProto = convertInteractionResponseToProto(
86
+ response,
87
+ queryProto.id,
88
+ coreQuery.type,
89
+ );
90
+ return this.queryResponseStream.write(responseProto);
91
+ })
92
+ .catch((error) => {
93
+ console.error("Error handling interaction query", error);
94
+ });
95
+ }
96
+ }