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.
- package/LICENSE +19 -0
- package/README.md +45 -0
- package/package.json +50 -0
- package/src/__generated__/agent/v1/agent_pb.ts +4642 -0
- package/src/__generated__/agent/v1/agent_service_connect.ts +71 -0
- package/src/__generated__/agent/v1/apply_agent_diff_tool_pb.ts +317 -0
- package/src/__generated__/agent/v1/ask_question_tool_pb.ts +588 -0
- package/src/__generated__/agent/v1/background_shell_exec_pb.ts +245 -0
- package/src/__generated__/agent/v1/computer_use_tool_pb.ts +959 -0
- package/src/__generated__/agent/v1/control_service_connect.ts +144 -0
- package/src/__generated__/agent/v1/control_service_pb.ts +1308 -0
- package/src/__generated__/agent/v1/create_plan_tool_pb.ts +366 -0
- package/src/__generated__/agent/v1/cursor_packages_pb.ts +278 -0
- package/src/__generated__/agent/v1/cursor_rules_pb.ts +301 -0
- package/src/__generated__/agent/v1/delete_exec_pb.ts +443 -0
- package/src/__generated__/agent/v1/delete_tool_pb.ts +52 -0
- package/src/__generated__/agent/v1/diagnostics_exec_pb.ts +399 -0
- package/src/__generated__/agent/v1/edit_tool_pb.ts +497 -0
- package/src/__generated__/agent/v1/exa_fetch_tool_pb.ts +472 -0
- package/src/__generated__/agent/v1/exa_search_tool_pb.ts +484 -0
- package/src/__generated__/agent/v1/exec_pb.ts +1271 -0
- package/src/__generated__/agent/v1/exec_service_connect.ts +14 -0
- package/src/__generated__/agent/v1/fetch_tool_pb.ts +242 -0
- package/src/__generated__/agent/v1/generate_image_tool_pb.ts +230 -0
- package/src/__generated__/agent/v1/glob_tool_pb.ts +248 -0
- package/src/__generated__/agent/v1/grep_exec_pb.ts +690 -0
- package/src/__generated__/agent/v1/grep_tool_pb.ts +52 -0
- package/src/__generated__/agent/v1/kv_pb.ts +281 -0
- package/src/__generated__/agent/v1/ls_exec_pb.ts +295 -0
- package/src/__generated__/agent/v1/ls_tool_pb.ts +52 -0
- package/src/__generated__/agent/v1/mcp_pb.ts +302 -0
- package/src/__generated__/agent/v1/mcp_resource_tool_pb.ts +688 -0
- package/src/__generated__/agent/v1/mcp_tool_pb.ts +630 -0
- package/src/__generated__/agent/v1/private_worker_bridge_external_connect.ts +26 -0
- package/src/__generated__/agent/v1/read_exec_pb.ts +412 -0
- package/src/__generated__/agent/v1/read_lints_tool_pb.ts +384 -0
- package/src/__generated__/agent/v1/read_tool_pb.ts +342 -0
- package/src/__generated__/agent/v1/record_screen_tool_pb.ts +376 -0
- package/src/__generated__/agent/v1/reflect_tool_pb.ts +236 -0
- package/src/__generated__/agent/v1/repo_pb.ts +154 -0
- package/src/__generated__/agent/v1/report_bugfix_results_tool_pb.ts +305 -0
- package/src/__generated__/agent/v1/request_context_exec_pb.ts +528 -0
- package/src/__generated__/agent/v1/sandbox_pb.ts +125 -0
- package/src/__generated__/agent/v1/selected_context_pb.ts +2272 -0
- package/src/__generated__/agent/v1/semsearch_tool_pb.ts +230 -0
- package/src/__generated__/agent/v1/setup_vm_environment_tool_pb.ts +168 -0
- package/src/__generated__/agent/v1/shell_exec_pb.ts +1195 -0
- package/src/__generated__/agent/v1/shell_tool_pb.ts +176 -0
- package/src/__generated__/agent/v1/start_grind_execution_tool_pb.ts +212 -0
- package/src/__generated__/agent/v1/start_grind_planning_tool_pb.ts +212 -0
- package/src/__generated__/agent/v1/subagents_pb.ts +1106 -0
- package/src/__generated__/agent/v1/switch_mode_tool_pb.ts +429 -0
- package/src/__generated__/agent/v1/todo_tool_pb.ts +551 -0
- package/src/__generated__/agent/v1/utils_pb.ts +348 -0
- package/src/__generated__/agent/v1/web_fetch_tool_pb.ts +429 -0
- package/src/__generated__/agent/v1/web_search_tool_pb.ts +466 -0
- package/src/__generated__/agent/v1/write_exec_pb.ts +379 -0
- package/src/__generated__/agent/v1/write_shell_stdin_tool_pb.ts +224 -0
- package/src/__generated__/aiserver/v1/aiserver_service_connect.ts +40 -0
- package/src/api/agent-service.ts +55 -0
- package/src/api/ai-service.ts +42 -0
- package/src/api/auth.ts +74 -0
- package/src/index.ts +101 -0
- package/src/lib/agent-store/disk.ts +139 -0
- package/src/lib/agent-store/index.ts +72 -0
- package/src/lib/agent-store/json-blob-store.ts +47 -0
- package/src/lib/auth.ts +135 -0
- package/src/lib/backoff.ts +32 -0
- package/src/lib/env.ts +3 -0
- package/src/lib/heartbeat.ts +21 -0
- package/src/pi/agent-store.ts +102 -0
- package/src/pi/env.ts +11 -0
- package/src/pi/executors/delete.ts +129 -0
- package/src/pi/executors/grep.ts +238 -0
- package/src/pi/executors/hook.ts +64 -0
- package/src/pi/executors/ls.ts +107 -0
- package/src/pi/executors/read.ts +73 -0
- package/src/pi/executors/request-context.ts +120 -0
- package/src/pi/executors/shell-stream.ts +136 -0
- package/src/pi/executors/shell.ts +157 -0
- package/src/pi/executors/stubs.ts +173 -0
- package/src/pi/executors/write.ts +189 -0
- package/src/pi/local-resource-provider/index.ts +10 -0
- package/src/pi/local-resource-provider/provider.ts +98 -0
- package/src/pi/local-resource-provider/types.ts +110 -0
- package/src/pi/model-mapping.ts +115 -0
- package/src/pi/model-override.ts +110 -0
- package/src/pi/model.ts +61 -0
- package/src/pi/request-builder.ts +279 -0
- package/src/pi/utils/tool-result.ts +35 -0
- package/src/stream.ts +386 -0
- package/src/tool-host.ts +44 -0
- package/src/vendor/agent-client/checkpoint-controller.ts +34 -0
- package/src/vendor/agent-client/connect.ts +348 -0
- package/src/vendor/agent-client/exec-controller.ts +102 -0
- package/src/vendor/agent-client/index.ts +25 -0
- package/src/vendor/agent-client/interaction-controller.ts +96 -0
- package/src/vendor/agent-client/split-stream.ts +143 -0
- package/src/vendor/agent-core/index.ts +9 -0
- package/src/vendor/agent-core/interaction-conversion.ts +558 -0
- package/src/vendor/agent-exec/controlled.ts +104 -0
- package/src/vendor/agent-exec/index.ts +45 -0
- package/src/vendor/agent-exec/registry-resource-accessor.ts +39 -0
- package/src/vendor/agent-exec/resources.ts +121 -0
- package/src/vendor/agent-exec/serialization.ts +22 -0
- package/src/vendor/agent-exec/simple-controlled-exec-manager.ts +161 -0
- package/src/vendor/agent-kv/agent-store.ts +115 -0
- package/src/vendor/agent-kv/blob-store.ts +36 -0
- package/src/vendor/agent-kv/controlled.ts +117 -0
- package/src/vendor/agent-kv/index.ts +15 -0
- package/src/vendor/agent-kv/serde.ts +44 -0
- package/src/vendor/local-exec/common.ts +19 -0
- package/src/vendor/local-exec/git-executor.ts +37 -0
- package/src/vendor/local-exec/git-helpers.ts +79 -0
- package/src/vendor/local-exec/index.ts +8 -0
- package/src/vendor/utils/index.ts +5 -0
- package/src/vendor/utils/map-writable.ts +34 -0
package/src/tool-host.ts
ADDED
|
@@ -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
|
+
}
|