@tt-a1i/openpi 0.6.0 → 0.6.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.
- package/README.md +13 -11
- package/SETUP.md +2 -2
- package/extensions/ai-providers/README.md +12 -6
- package/extensions/ai-providers/cursor/input-images.ts +2 -3
- package/extensions/ai-providers/cursor/proto.ts +218 -11
- package/extensions/ai-providers/cursor/protobuf.ts +12 -2
- package/extensions/ai-providers/cursor/provider.ts +276 -20
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
- package/extensions/ai-providers/index.ts +3 -3
- package/extensions/shared/child-session.ts +14 -0
- package/extensions/subagents/index.ts +20 -3
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +70 -59
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/manager.ts +2 -71
- package/extensions/subagents/src/prompt.ts +2 -2
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/workflows/dashboard.ts +139 -21
- package/extensions/workflows/index.ts +62 -20
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/runner.ts +5 -162
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +1 -1
- package/skills/subagents/REFERENCE.md +6 -7
- package/skills/subagents/SKILL.md +1 -1
- package/skills/workflows/REFERENCE.md +3 -1
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
Model,
|
|
11
11
|
SimpleStreamOptions,
|
|
12
12
|
TextContent,
|
|
13
|
+
ToolCall,
|
|
13
14
|
} from "@earendil-works/pi-ai/compat";
|
|
14
15
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/compat";
|
|
15
16
|
import { emptyUsage } from "../usage.ts";
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
CursorRuleSchema,
|
|
36
37
|
CursorRuleTypeGlobalSchema,
|
|
37
38
|
CursorRuleTypeSchema,
|
|
39
|
+
CursorToolCallSchema,
|
|
38
40
|
ExecClientControlMessageSchema,
|
|
39
41
|
ExecClientMessageSchema,
|
|
40
42
|
ExecClientStreamCloseSchema,
|
|
@@ -44,6 +46,15 @@ import {
|
|
|
44
46
|
KvClientMessageSchema,
|
|
45
47
|
type KvServerMessage,
|
|
46
48
|
KvServerMessageSchema,
|
|
49
|
+
McpArgsSchema,
|
|
50
|
+
McpImageContentSchema,
|
|
51
|
+
McpRejectedSchema,
|
|
52
|
+
McpResultSchema,
|
|
53
|
+
McpSuccessSchema,
|
|
54
|
+
McpTextContentSchema,
|
|
55
|
+
McpToolCallSchema,
|
|
56
|
+
McpToolResultContentItemSchema,
|
|
57
|
+
McpToolResultSchema,
|
|
47
58
|
type ModelDetails,
|
|
48
59
|
ModelDetailsSchema,
|
|
49
60
|
RequestContextResultSchema,
|
|
@@ -59,8 +70,14 @@ import {
|
|
|
59
70
|
UserMessageActionSchema,
|
|
60
71
|
UserMessageSchema,
|
|
61
72
|
} from "./proto.ts";
|
|
62
|
-
import { create, fromBinary, toBinary } from "./protobuf.ts";
|
|
73
|
+
import { create, encodeJsonValue, fromBinary, toBinary } from "./protobuf.ts";
|
|
63
74
|
import { connectCursorHttp2 } from "./proxy.ts";
|
|
75
|
+
import {
|
|
76
|
+
buildCursorTools,
|
|
77
|
+
CURSOR_PI_PROVIDER,
|
|
78
|
+
CURSOR_PI_TOOLS_SYSTEM_PROMPT,
|
|
79
|
+
decodeCursorTool,
|
|
80
|
+
} from "./tool-bridge.ts";
|
|
64
81
|
|
|
65
82
|
const CONNECT_END_STREAM_FLAG = 0b00000010;
|
|
66
83
|
const CONNECT_COMPRESSED_FLAG = 0b00000001;
|
|
@@ -199,22 +216,51 @@ function rootPromptContent(
|
|
|
199
216
|
|
|
200
217
|
function assistantRootContent(
|
|
201
218
|
message: Extract<Message, { role: "assistant" }>,
|
|
219
|
+
results: Map<string, Extract<Message, { role: "toolResult" }>>,
|
|
202
220
|
) {
|
|
203
221
|
const content: Array<Record<string, unknown>> = [];
|
|
204
222
|
for (const item of message.content) {
|
|
205
223
|
if (item.type === "text" && item.text) {
|
|
206
224
|
content.push({ type: "text", text: item.text });
|
|
225
|
+
} else if (
|
|
226
|
+
item.type === "toolCall" &&
|
|
227
|
+
results.get(item.id)?.toolName === item.name
|
|
228
|
+
) {
|
|
229
|
+
content.push({
|
|
230
|
+
type: "tool-call",
|
|
231
|
+
toolCallId: item.id,
|
|
232
|
+
toolName: item.name,
|
|
233
|
+
args: item.arguments,
|
|
234
|
+
});
|
|
207
235
|
}
|
|
208
236
|
}
|
|
209
237
|
return content;
|
|
210
238
|
}
|
|
211
239
|
|
|
240
|
+
function pairedToolResults(messages: Message[], end: number) {
|
|
241
|
+
const calls = new Map<string, string>();
|
|
242
|
+
const results = new Map<string, Extract<Message, { role: "toolResult" }>>();
|
|
243
|
+
for (const message of messages.slice(0, end < 0 ? undefined : end)) {
|
|
244
|
+
if (message.role === "assistant") {
|
|
245
|
+
for (const part of message.content)
|
|
246
|
+
if (part.type === "toolCall") calls.set(part.id, part.name);
|
|
247
|
+
} else if (
|
|
248
|
+
message.role === "toolResult" &&
|
|
249
|
+
calls.get(message.toolCallId) === message.toolName
|
|
250
|
+
) {
|
|
251
|
+
results.set(message.toolCallId, message);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return results;
|
|
255
|
+
}
|
|
256
|
+
|
|
212
257
|
function buildHistoryRootPrompt(
|
|
213
258
|
messages: Message[],
|
|
214
259
|
store: CursorBlobStore,
|
|
215
260
|
activeUserIndex: number,
|
|
216
261
|
): Uint8Array[] {
|
|
217
262
|
const entries: Uint8Array[] = [];
|
|
263
|
+
const results = pairedToolResults(messages, activeUserIndex);
|
|
218
264
|
for (let index = 0; index < messages.length; index++) {
|
|
219
265
|
if (index === activeUserIndex) break;
|
|
220
266
|
const message = messages[index];
|
|
@@ -224,13 +270,29 @@ function buildHistoryRootPrompt(
|
|
|
224
270
|
if (content.length === 0) continue;
|
|
225
271
|
value = { role: "user", content };
|
|
226
272
|
} else if (message.role === "assistant") {
|
|
227
|
-
const content = assistantRootContent(message);
|
|
273
|
+
const content = assistantRootContent(message, results);
|
|
228
274
|
if (content.length === 0) continue;
|
|
229
275
|
value = { role: "assistant", content };
|
|
230
276
|
} else {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
277
|
+
if (results.get(message.toolCallId) !== message) continue;
|
|
278
|
+
value = {
|
|
279
|
+
role: "tool",
|
|
280
|
+
id: message.toolCallId,
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: "tool-result",
|
|
284
|
+
toolCallId: message.toolCallId,
|
|
285
|
+
toolName: message.toolName,
|
|
286
|
+
result: message.content.some((part) => part.type === "image")
|
|
287
|
+
? rootPromptContent(message.content)
|
|
288
|
+
: message.content
|
|
289
|
+
.filter((part) => part.type === "text")
|
|
290
|
+
.map((part) => part.text)
|
|
291
|
+
.join("\n"),
|
|
292
|
+
...(message.isError ? { isError: true } : {}),
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
234
296
|
}
|
|
235
297
|
entries.push(
|
|
236
298
|
storeBlob(store, new TextEncoder().encode(JSON.stringify(value))),
|
|
@@ -242,13 +304,17 @@ function buildHistoryRootPrompt(
|
|
|
242
304
|
function buildSystemPrompt(
|
|
243
305
|
systemPrompt: Context["systemPrompt"],
|
|
244
306
|
store: CursorBlobStore,
|
|
307
|
+
hasTools = false,
|
|
245
308
|
): Uint8Array[] {
|
|
246
309
|
const prompts = systemPrompt
|
|
247
310
|
? Array.isArray(systemPrompt)
|
|
248
311
|
? systemPrompt
|
|
249
312
|
: [systemPrompt]
|
|
250
313
|
: ["You are a helpful assistant."];
|
|
251
|
-
return [
|
|
314
|
+
return [
|
|
315
|
+
...prompts,
|
|
316
|
+
hasTools ? CURSOR_PI_TOOLS_SYSTEM_PROMPT : CURSOR_CHAT_ONLY_SYSTEM_PROMPT,
|
|
317
|
+
].map((prompt) =>
|
|
252
318
|
storeBlob(
|
|
253
319
|
store,
|
|
254
320
|
new TextEncoder().encode(
|
|
@@ -260,11 +326,12 @@ function buildSystemPrompt(
|
|
|
260
326
|
|
|
261
327
|
/**
|
|
262
328
|
* Cursor asks for these rules over the exec channel before generating text.
|
|
263
|
-
*
|
|
264
|
-
*
|
|
329
|
+
* These rules keep Cursor-native tools disabled. The request-context response
|
|
330
|
+
* advertises only the active Pi tools through the MCP protocol bridge.
|
|
265
331
|
*/
|
|
266
332
|
export function buildCursorRequestContextRules(
|
|
267
333
|
systemPrompt: Context["systemPrompt"],
|
|
334
|
+
hasTools = false,
|
|
268
335
|
): CursorRule[] {
|
|
269
336
|
const rules: CursorRule[] = systemPrompt?.trim()
|
|
270
337
|
? [
|
|
@@ -283,8 +350,10 @@ export function buildCursorRequestContextRules(
|
|
|
283
350
|
: [];
|
|
284
351
|
rules.push(
|
|
285
352
|
create(CursorRuleSchema, {
|
|
286
|
-
fullPath: "/pi/cursor-chat-only.mdc",
|
|
287
|
-
content:
|
|
353
|
+
fullPath: hasTools ? "/pi/cursor-tools.mdc" : "/pi/cursor-chat-only.mdc",
|
|
354
|
+
content: hasTools
|
|
355
|
+
? CURSOR_PI_TOOLS_SYSTEM_PROMPT
|
|
356
|
+
: CURSOR_CHAT_ONLY_SYSTEM_PROMPT,
|
|
288
357
|
source: 2,
|
|
289
358
|
type: create(CursorRuleTypeSchema, {
|
|
290
359
|
type: { case: "global", value: create(CursorRuleTypeGlobalSchema, {}) },
|
|
@@ -300,6 +369,7 @@ function buildHistoryTurns(
|
|
|
300
369
|
activeUserIndex: number,
|
|
301
370
|
): Uint8Array[] {
|
|
302
371
|
const turns: Uint8Array[] = [];
|
|
372
|
+
const results = pairedToolResults(messages, activeUserIndex);
|
|
303
373
|
const end = activeUserIndex >= 0 ? activeUserIndex : messages.length;
|
|
304
374
|
let index = 0;
|
|
305
375
|
while (index < end) {
|
|
@@ -335,6 +405,68 @@ function buildHistoryTurns(
|
|
|
335
405
|
),
|
|
336
406
|
),
|
|
337
407
|
);
|
|
408
|
+
} else if (item.type === "toolCall") {
|
|
409
|
+
const result = results.get(item.id);
|
|
410
|
+
if (!result || result.toolName !== item.name) continue;
|
|
411
|
+
const args = Object.fromEntries(
|
|
412
|
+
Object.entries(item.arguments).map(([key, value]) => [
|
|
413
|
+
key,
|
|
414
|
+
encodeJsonValue(JSON.parse(JSON.stringify(value))),
|
|
415
|
+
]),
|
|
416
|
+
);
|
|
417
|
+
const tool = create(CursorToolCallSchema, {
|
|
418
|
+
toolCallId: item.id,
|
|
419
|
+
tool: {
|
|
420
|
+
case: "mcpToolCall",
|
|
421
|
+
value: create(McpToolCallSchema, {
|
|
422
|
+
args: create(McpArgsSchema, {
|
|
423
|
+
name: item.name,
|
|
424
|
+
toolName: item.name,
|
|
425
|
+
providerIdentifier: CURSOR_PI_PROVIDER,
|
|
426
|
+
toolCallId: item.id,
|
|
427
|
+
args,
|
|
428
|
+
}),
|
|
429
|
+
result: create(McpToolResultSchema, {
|
|
430
|
+
result: {
|
|
431
|
+
case: "success",
|
|
432
|
+
value: create(McpSuccessSchema, {
|
|
433
|
+
isError: result.isError,
|
|
434
|
+
content: result.content.map((part) =>
|
|
435
|
+
create(McpToolResultContentItemSchema, {
|
|
436
|
+
content:
|
|
437
|
+
part.type === "text"
|
|
438
|
+
? {
|
|
439
|
+
case: "text",
|
|
440
|
+
value: create(McpTextContentSchema, {
|
|
441
|
+
text: part.text,
|
|
442
|
+
}),
|
|
443
|
+
}
|
|
444
|
+
: {
|
|
445
|
+
case: "image",
|
|
446
|
+
value: create(McpImageContentSchema, {
|
|
447
|
+
data: Buffer.from(part.data, "base64"),
|
|
448
|
+
mimeType: part.mimeType,
|
|
449
|
+
}),
|
|
450
|
+
},
|
|
451
|
+
}),
|
|
452
|
+
),
|
|
453
|
+
}),
|
|
454
|
+
},
|
|
455
|
+
}),
|
|
456
|
+
}),
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
steps.push(
|
|
460
|
+
storeBlob(
|
|
461
|
+
store,
|
|
462
|
+
toBinary(
|
|
463
|
+
ConversationStepSchema,
|
|
464
|
+
create(ConversationStepSchema, {
|
|
465
|
+
message: { case: "toolCall", value: tool },
|
|
466
|
+
}),
|
|
467
|
+
),
|
|
468
|
+
),
|
|
469
|
+
);
|
|
338
470
|
}
|
|
339
471
|
}
|
|
340
472
|
}
|
|
@@ -414,11 +546,14 @@ export async function buildCursorRequest(
|
|
|
414
546
|
options?: SimpleStreamOptions,
|
|
415
547
|
): Promise<CursorRequestBuild> {
|
|
416
548
|
const store: CursorBlobStore = new Map();
|
|
417
|
-
const activeIndex =
|
|
549
|
+
const activeIndex =
|
|
550
|
+
context.messages.at(-1)?.role === "user"
|
|
551
|
+
? lastUserIndex(context.messages)
|
|
552
|
+
: -1;
|
|
418
553
|
const active = activeIndex >= 0 ? context.messages[activeIndex] : undefined;
|
|
419
554
|
const activeContent = active?.role === "user" ? active.content : undefined;
|
|
420
555
|
const rootPromptMessagesJson = [
|
|
421
|
-
...buildSystemPrompt(context.systemPrompt, store),
|
|
556
|
+
...buildSystemPrompt(context.systemPrompt, store, !!context.tools?.length),
|
|
422
557
|
...buildHistoryRootPrompt(context.messages, store, activeIndex),
|
|
423
558
|
];
|
|
424
559
|
const state = create(ConversationStateStructureSchema, {
|
|
@@ -554,7 +689,7 @@ function isAbortError(
|
|
|
554
689
|
);
|
|
555
690
|
}
|
|
556
691
|
|
|
557
|
-
/** Cursor AgentService/Run
|
|
692
|
+
/** Cursor AgentService/Run with Pi-owned tool execution across provider turns. */
|
|
558
693
|
export function streamCursor(
|
|
559
694
|
model: Model<Api>,
|
|
560
695
|
context: Context,
|
|
@@ -586,6 +721,8 @@ export function streamCursor(
|
|
|
586
721
|
let turnEnded = false;
|
|
587
722
|
let terminalError: Error | undefined;
|
|
588
723
|
let finished = false;
|
|
724
|
+
const pendingCalls = new Map<string, ToolCall>();
|
|
725
|
+
let handingOffTools = false;
|
|
589
726
|
|
|
590
727
|
const closeBlocks = () => {
|
|
591
728
|
if (currentText) {
|
|
@@ -698,6 +835,7 @@ export function streamCursor(
|
|
|
698
835
|
};
|
|
699
836
|
let frameBuffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
700
837
|
const processFrame = (flags: number, bytes: Uint8Array) => {
|
|
838
|
+
if (handingOffTools) return;
|
|
701
839
|
if ((flags & CONNECT_COMPRESSED_FLAG) !== 0) {
|
|
702
840
|
throw new Error("Compressed Cursor Connect frames are unsupported");
|
|
703
841
|
}
|
|
@@ -715,8 +853,11 @@ export function streamCursor(
|
|
|
715
853
|
case: "success",
|
|
716
854
|
value: create(RequestContextSuccessSchema, {
|
|
717
855
|
requestContext: create(RequestContextSchema, {
|
|
718
|
-
rules: buildCursorRequestContextRules(
|
|
719
|
-
|
|
856
|
+
rules: buildCursorRequestContextRules(
|
|
857
|
+
context.systemPrompt,
|
|
858
|
+
!!context.tools?.length,
|
|
859
|
+
),
|
|
860
|
+
tools: buildCursorTools(context.tools),
|
|
720
861
|
}),
|
|
721
862
|
}),
|
|
722
863
|
},
|
|
@@ -734,6 +875,60 @@ export function streamCursor(
|
|
|
734
875
|
);
|
|
735
876
|
return;
|
|
736
877
|
}
|
|
878
|
+
if (exec.message.case === "mcpArgs") {
|
|
879
|
+
const args = exec.message.value;
|
|
880
|
+
if (args.smartModeApprovalOnly) {
|
|
881
|
+
// A probe must not become a tool call or preauthorize Pi execution.
|
|
882
|
+
const reply = create(AgentClientMessageSchema, {
|
|
883
|
+
message: {
|
|
884
|
+
case: "execClientMessage",
|
|
885
|
+
value: create(ExecClientMessageSchema, {
|
|
886
|
+
id: exec.id,
|
|
887
|
+
execId: exec.execId,
|
|
888
|
+
message: {
|
|
889
|
+
case: "mcpResult",
|
|
890
|
+
value: create(McpResultSchema, {
|
|
891
|
+
result: {
|
|
892
|
+
case: "rejected",
|
|
893
|
+
value: create(McpRejectedSchema, {
|
|
894
|
+
reason:
|
|
895
|
+
"Pi must evaluate permissions when executing the tool; approval-only probes cannot authorize execution.",
|
|
896
|
+
}),
|
|
897
|
+
},
|
|
898
|
+
}),
|
|
899
|
+
},
|
|
900
|
+
}),
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
h2Request?.write(
|
|
904
|
+
frameConnectMessage(toBinary(AgentClientMessageSchema, reply)),
|
|
905
|
+
);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const call = decodeCursorTool(args, context.tools);
|
|
909
|
+
if (
|
|
910
|
+
context.messages.some((message) =>
|
|
911
|
+
message.role === "toolResult"
|
|
912
|
+
? message.toolCallId === call.id
|
|
913
|
+
: message.role === "assistant" &&
|
|
914
|
+
message.content.some(
|
|
915
|
+
(part) => part.type === "toolCall" && part.id === call.id,
|
|
916
|
+
),
|
|
917
|
+
)
|
|
918
|
+
) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
"Cursor attempted to replay a tool call identity already present in Pi history",
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const previous = pendingCalls.get(call.id);
|
|
924
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(call)) {
|
|
925
|
+
throw new Error(
|
|
926
|
+
"Cursor repeated a tool call identity with different arguments",
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
pendingCalls.set(call.id, call);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
737
932
|
const throwReply = create(AgentClientMessageSchema, {
|
|
738
933
|
message: {
|
|
739
934
|
case: "execClientControlMessage",
|
|
@@ -742,8 +937,9 @@ export function streamCursor(
|
|
|
742
937
|
case: "throw",
|
|
743
938
|
value: create(ExecClientThrowSchema, {
|
|
744
939
|
id: exec.id,
|
|
745
|
-
error:
|
|
746
|
-
"Cursor
|
|
940
|
+
error: context.tools?.length
|
|
941
|
+
? "Cursor-native execution is unavailable; use advertised Pi MCP tools"
|
|
942
|
+
: "Cursor tools are not available in this chat-only provider",
|
|
747
943
|
errorCode: "UNIMPLEMENTED",
|
|
748
944
|
}),
|
|
749
945
|
},
|
|
@@ -762,7 +958,9 @@ export function streamCursor(
|
|
|
762
958
|
},
|
|
763
959
|
});
|
|
764
960
|
const error = new Error(
|
|
765
|
-
|
|
961
|
+
context.tools?.length
|
|
962
|
+
? "Cursor requested unsupported native execution outside Pi"
|
|
963
|
+
: "Cursor requested a tool that is unavailable in chat-only mode",
|
|
766
964
|
);
|
|
767
965
|
terminalError = error;
|
|
768
966
|
if (!h2Request) {
|
|
@@ -784,10 +982,32 @@ export function streamCursor(
|
|
|
784
982
|
}
|
|
785
983
|
if (message.message.case === "interactionQuery") {
|
|
786
984
|
throw new Error(
|
|
787
|
-
`Cursor interaction query ${message.message.value.query.case ?? "unknown"} is unavailable in chat-only mode`,
|
|
985
|
+
`Cursor interaction query ${message.message.value.query.case ?? "unknown"} is unavailable ${context.tools?.length ? "outside Pi's interaction lifecycle" : "in chat-only mode"}`,
|
|
788
986
|
);
|
|
789
987
|
}
|
|
790
988
|
if (message.message.case !== "interactionUpdate") return;
|
|
989
|
+
const update = message.message.value;
|
|
990
|
+
if (
|
|
991
|
+
context.tools?.length &&
|
|
992
|
+
(update.message.case === "partialToolCall" ||
|
|
993
|
+
update.message.case === "toolCallStarted" ||
|
|
994
|
+
update.message.case === "toolCallCompleted")
|
|
995
|
+
) {
|
|
996
|
+
const preview = update.message.value.toolCall;
|
|
997
|
+
if (preview && preview.tool.case !== "mcpToolCall") {
|
|
998
|
+
throw new Error(
|
|
999
|
+
"Cursor-native tools are unavailable; use the advertised Pi MCP tools",
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
// Only exec mcpArgs is an invocation. UI previews may be partial,
|
|
1003
|
+
// duplicated, or emitted for approval probes, and never execute.
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (context.tools?.length && update.message.case === "toolCallDelta") {
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
"Cursor-native tool deltas are unavailable; use the advertised Pi MCP tools",
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
791
1011
|
processInteraction(
|
|
792
1012
|
message.message.value,
|
|
793
1013
|
output,
|
|
@@ -830,6 +1050,37 @@ export function streamCursor(
|
|
|
830
1050
|
frameBuffer = frameBuffer.subarray(size + 5);
|
|
831
1051
|
processFrame(flags, data);
|
|
832
1052
|
}
|
|
1053
|
+
if (pendingCalls.size > 0 && !handingOffTools) {
|
|
1054
|
+
if (terminalError) throw terminalError;
|
|
1055
|
+
if (options?.signal?.aborted)
|
|
1056
|
+
throw new Error("Cursor request aborted");
|
|
1057
|
+
closeBlocks();
|
|
1058
|
+
for (const call of pendingCalls.values()) {
|
|
1059
|
+
const contentIndex = output.content.length;
|
|
1060
|
+
output.content.push(call);
|
|
1061
|
+
stream.push({
|
|
1062
|
+
type: "toolcall_start",
|
|
1063
|
+
contentIndex,
|
|
1064
|
+
partial: output,
|
|
1065
|
+
});
|
|
1066
|
+
stream.push({
|
|
1067
|
+
type: "toolcall_delta",
|
|
1068
|
+
contentIndex,
|
|
1069
|
+
delta: JSON.stringify(call.arguments),
|
|
1070
|
+
partial: output,
|
|
1071
|
+
});
|
|
1072
|
+
stream.push({
|
|
1073
|
+
type: "toolcall_end",
|
|
1074
|
+
contentIndex,
|
|
1075
|
+
toolCall: call,
|
|
1076
|
+
partial: output,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
output.stopReason = "toolUse";
|
|
1080
|
+
handingOffTools = true;
|
|
1081
|
+
turnEnded = true;
|
|
1082
|
+
settle();
|
|
1083
|
+
}
|
|
833
1084
|
};
|
|
834
1085
|
|
|
835
1086
|
h2Client = await connectCursorHttp2(baseUrl, {
|
|
@@ -967,7 +1218,12 @@ export function streamCursor(
|
|
|
967
1218
|
output.usage.totalTokens = output.usage.input + output.usage.output;
|
|
968
1219
|
stream.push({
|
|
969
1220
|
type: "done",
|
|
970
|
-
reason:
|
|
1221
|
+
reason:
|
|
1222
|
+
output.stopReason === "toolUse"
|
|
1223
|
+
? "toolUse"
|
|
1224
|
+
: output.stopReason === "length"
|
|
1225
|
+
? "length"
|
|
1226
|
+
: "stop",
|
|
971
1227
|
message: output,
|
|
972
1228
|
});
|
|
973
1229
|
stream.end();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Translate Cursor MCP requests into Pi calls; execution stays in Pi's loop. */
|
|
2
|
+
import type { Context, ToolCall } from "@earendil-works/pi-ai/compat";
|
|
3
|
+
import { type McpArgs, McpToolDefinitionSchema } from "./proto.ts";
|
|
4
|
+
import {
|
|
5
|
+
create,
|
|
6
|
+
decodeJsonValue,
|
|
7
|
+
encodeJsonValue,
|
|
8
|
+
type JsonValue,
|
|
9
|
+
} from "./protobuf.ts";
|
|
10
|
+
|
|
11
|
+
export const CURSOR_PI_PROVIDER = "openpi";
|
|
12
|
+
export const CURSOR_PI_TOOLS_SYSTEM_PROMPT =
|
|
13
|
+
"Use only the provided openpi MCP tools. These are the active Pi tools and Pi owns their execution and permissions. Do not use Cursor-native filesystem, shell, editing, web, task, or interaction tools. When tool results appear in conversation history, continue from those results. Do not repeat a completed tool call.";
|
|
14
|
+
|
|
15
|
+
export function buildCursorTools(tools: Context["tools"]) {
|
|
16
|
+
return (tools ?? []).map((tool) => {
|
|
17
|
+
const schema: JsonValue = JSON.parse(JSON.stringify(tool.parameters));
|
|
18
|
+
return create(McpToolDefinitionSchema, {
|
|
19
|
+
name: tool.name,
|
|
20
|
+
providerIdentifier: CURSOR_PI_PROVIDER,
|
|
21
|
+
toolName: tool.name,
|
|
22
|
+
description: tool.description,
|
|
23
|
+
inputSchema: encodeJsonValue(schema),
|
|
24
|
+
inputSchemaJson: JSON.stringify(schema),
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function decodeCursorTool(
|
|
30
|
+
args: McpArgs,
|
|
31
|
+
tools: Context["tools"],
|
|
32
|
+
): ToolCall {
|
|
33
|
+
const name = args.toolName || args.name;
|
|
34
|
+
if (
|
|
35
|
+
args.providerIdentifier !== CURSOR_PI_PROVIDER ||
|
|
36
|
+
(args.serverIdentifier && args.serverIdentifier !== CURSOR_PI_PROVIDER) ||
|
|
37
|
+
!name ||
|
|
38
|
+
(args.name && args.name !== name) ||
|
|
39
|
+
!tools?.some((tool) => tool.name === name)
|
|
40
|
+
) {
|
|
41
|
+
throw new Error("Cursor requested an unadvertised Pi tool identity");
|
|
42
|
+
}
|
|
43
|
+
if (!args.toolCallId.trim())
|
|
44
|
+
throw new Error("Cursor MCP tool call has no identity");
|
|
45
|
+
const values: Record<string, unknown> = {};
|
|
46
|
+
for (const [key, value] of Object.entries(args.args)) {
|
|
47
|
+
// google.protobuf.Value, not JSON text. Define own properties so keys such
|
|
48
|
+
// as __proto__ cannot alter the decoded argument object's prototype.
|
|
49
|
+
if (!value.length || ![8, 17, 26, 32, 42, 50].includes(value[0]!)) {
|
|
50
|
+
throw new Error("Cursor MCP argument is not a protobuf JSON value");
|
|
51
|
+
}
|
|
52
|
+
const decoded = decodeJsonValue(value);
|
|
53
|
+
const validateJson = (item: JsonValue): void => {
|
|
54
|
+
if (typeof item === "number" && !Number.isFinite(item))
|
|
55
|
+
throw new Error("Cursor MCP argument contains a non-finite number");
|
|
56
|
+
if (item && typeof item === "object")
|
|
57
|
+
for (const child of Object.values(item)) validateJson(child);
|
|
58
|
+
};
|
|
59
|
+
validateJson(decoded);
|
|
60
|
+
Object.defineProperty(values, key, {
|
|
61
|
+
value: decoded,
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { type: "toolCall", id: args.toolCallId, name, arguments: values };
|
|
68
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Adds OAuth-backed Google Antigravity and Cursor model providers. Both are
|
|
5
5
|
* inert until the user logs in and selects one of their models. Cursor uses
|
|
6
|
-
* AgentService/Run
|
|
7
|
-
* are not exposed or executed by this extension.
|
|
6
|
+
* AgentService/Run with an experimental bridge to normal Pi tool calls.
|
|
7
|
+
* Cursor-native coding tools are not exposed or executed by this extension.
|
|
8
8
|
*
|
|
9
9
|
* Wire protocol: Cloud Code Assist `v1internal:streamGenerateContent` over
|
|
10
10
|
* SSE (see antigravity/provider.ts). Reference implementation: oh-my-pi's
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
|
|
14
14
|
import { createProvider, type ProviderStreams } from "@earendil-works/pi-ai";
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { createOAuthAuth } from "./oauth-adapter.ts";
|
|
17
16
|
import { encodeApiKey } from "./antigravity/credentials.ts";
|
|
18
17
|
import { fetchAntigravityModels } from "./antigravity/discovery.ts";
|
|
19
18
|
import {
|
|
@@ -31,6 +30,7 @@ import { transformCursorImageInput } from "./cursor/input-images.ts";
|
|
|
31
30
|
import { CURSOR_MODELS } from "./cursor/models.ts";
|
|
32
31
|
import { loginCursor, refreshCursorToken } from "./cursor/oauth.ts";
|
|
33
32
|
import { streamCursor } from "./cursor/provider.ts";
|
|
33
|
+
import { createOAuthAuth } from "./oauth-adapter.ts";
|
|
34
34
|
|
|
35
35
|
function providerStreams(
|
|
36
36
|
streamSimple: ProviderStreams["streamSimple"],
|
|
@@ -550,6 +550,20 @@ export function effectiveChildToolAllowlist(tools?: readonly string[]) {
|
|
|
550
550
|
);
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
/** Project the parent's active surface into a child; a role can only narrow it.
|
|
554
|
+
* Active tools are a visibility choice, not a filesystem/network sandbox.
|
|
555
|
+
* Inactive tools are not implicitly activated by delegation.
|
|
556
|
+
*/
|
|
557
|
+
export function inheritedChildToolAllowlist(
|
|
558
|
+
parentTools: readonly string[],
|
|
559
|
+
roleTools?: readonly string[],
|
|
560
|
+
) {
|
|
561
|
+
const allowed = roleTools === undefined ? undefined : new Set(roleTools);
|
|
562
|
+
return effectiveChildToolAllowlist([...new Set(parentTools)])!.filter(
|
|
563
|
+
(name) => allowed === undefined || allowed.has(name),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
553
567
|
export function childToolPolicy(tools?: readonly string[]) {
|
|
554
568
|
const effectiveTools = effectiveChildToolAllowlist(tools);
|
|
555
569
|
return {
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
} from "../shared/below-editor-navigation.ts";
|
|
58
58
|
import {
|
|
59
59
|
effectiveChildToolAllowlist,
|
|
60
|
+
inheritedChildToolAllowlist,
|
|
60
61
|
resolveStandaloneChildProjectTrust,
|
|
61
62
|
} from "../shared/child-session.ts";
|
|
62
63
|
import { formatContextUtilization } from "../shared/context-utilization.ts";
|
|
@@ -144,6 +145,7 @@ import { createSubagentResultDelivery } from "./src/result-delivery.ts";
|
|
|
144
145
|
import {
|
|
145
146
|
createSubagentRuntime,
|
|
146
147
|
runTool,
|
|
148
|
+
SubagentToolInterruptedError,
|
|
147
149
|
type SubagentRuntime,
|
|
148
150
|
} from "./src/runtime.ts";
|
|
149
151
|
import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
|
|
@@ -886,6 +888,7 @@ export default function (
|
|
|
886
888
|
if (
|
|
887
889
|
planning &&
|
|
888
890
|
agentType &&
|
|
891
|
+
!agentType.planningCompatible &&
|
|
889
892
|
!planModeAllowsDeclaredTools(declaredChildTools)
|
|
890
893
|
) {
|
|
891
894
|
throw new Error(
|
|
@@ -934,7 +937,10 @@ export default function (
|
|
|
934
937
|
const requestedChildTools = planning
|
|
935
938
|
? planModeChildTools(declaredChildTools)
|
|
936
939
|
: declaredChildTools;
|
|
937
|
-
const childTools =
|
|
940
|
+
const childTools = inheritedChildToolAllowlist(
|
|
941
|
+
pi.getActiveTools(),
|
|
942
|
+
requestedChildTools,
|
|
943
|
+
);
|
|
938
944
|
// Read at spawn time so `/openpi-setup` changes affect the next child
|
|
939
945
|
// without reloading this extension. Undefined preserves parent-model
|
|
940
946
|
// inheritance in the backend.
|
|
@@ -980,11 +986,22 @@ export default function (
|
|
|
980
986
|
interruptMessage: "Subagent spawn aborted.",
|
|
981
987
|
});
|
|
982
988
|
} catch (error) {
|
|
983
|
-
//
|
|
984
|
-
//
|
|
989
|
+
// Known startup failures can reclaim their empty checkout. Interrupted
|
|
990
|
+
// startup must preserve it while asynchronous acquisition may continue.
|
|
985
991
|
if (worktree) {
|
|
986
992
|
const spawnError =
|
|
987
993
|
error instanceof Error ? error.message : String(error);
|
|
994
|
+
// Cancelling Effect acquisition does not prove an asynchronous
|
|
995
|
+
// factory or extension hook has quiesced. It may still use this cwd.
|
|
996
|
+
if (
|
|
997
|
+
signal?.aborted ||
|
|
998
|
+
error instanceof SubagentToolInterruptedError
|
|
999
|
+
) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`${spawnError}; startup quiescence is unknown; checkout preserved at ${worktree.path} (branch ${worktree.branch})`,
|
|
1002
|
+
{ cause: error },
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
988
1005
|
let cleanupWarning: string | undefined;
|
|
989
1006
|
let cleanupError: unknown;
|
|
990
1007
|
try {
|