@bastani/atomic 0.8.28 → 0.8.29-alpha.2
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/CHANGELOG.md +30 -0
- package/dist/builtin/cursor/CHANGELOG.md +27 -0
- package/dist/builtin/cursor/LICENSE +26 -0
- package/dist/builtin/cursor/README.md +22 -0
- package/dist/builtin/cursor/index.ts +9 -0
- package/dist/builtin/cursor/package.json +46 -0
- package/dist/builtin/cursor/src/auth.ts +352 -0
- package/dist/builtin/cursor/src/catalog-cache.ts +155 -0
- package/dist/builtin/cursor/src/config.ts +123 -0
- package/dist/builtin/cursor/src/conversation-state.ts +135 -0
- package/dist/builtin/cursor/src/cursor-models-raw.json +583 -0
- package/dist/builtin/cursor/src/model-mapper.ts +270 -0
- package/dist/builtin/cursor/src/models.ts +54 -0
- package/dist/builtin/cursor/src/native-loader.ts +71 -0
- package/dist/builtin/cursor/src/proto/README.md +34 -0
- package/dist/builtin/cursor/src/proto/agent_pb.ts +15294 -0
- package/dist/builtin/cursor/src/proto/protobuf-codec.ts +717 -0
- package/dist/builtin/cursor/src/provider.ts +301 -0
- package/dist/builtin/cursor/src/stream.ts +564 -0
- package/dist/builtin/cursor/src/transport.ts +791 -0
- package/dist/builtin/intercom/CHANGELOG.md +4 -0
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/intercom/skills/intercom/SKILL.md +5 -5
- package/dist/builtin/mcp/CHANGELOG.md +4 -0
- package/dist/builtin/mcp/package.json +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +12 -0
- package/dist/builtin/subagents/README.md +7 -3
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -24
- package/dist/builtin/subagents/agents/debugger.md +3 -5
- package/dist/builtin/subagents/package.json +4 -4
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +2 -1
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -1
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +19 -2
- package/dist/builtin/subagents/src/runs/shared/structured-output.ts +271 -10
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +12 -39
- package/dist/builtin/subagents/src/shared/types.ts +1 -0
- package/dist/builtin/subagents/src/shared/utils.ts +50 -10
- package/dist/builtin/subagents/src/slash/saved-chain-mapping.ts +77 -0
- package/dist/builtin/subagents/src/slash/slash-commands.ts +1 -55
- package/dist/builtin/web-access/CHANGELOG.md +5 -1
- package/dist/builtin/web-access/README.md +1 -1
- package/dist/builtin/web-access/github-extract.ts +1 -1
- package/dist/builtin/web-access/package.json +3 -3
- package/dist/builtin/workflows/CHANGELOG.md +18 -0
- package/dist/builtin/workflows/README.md +19 -1
- package/dist/builtin/workflows/package.json +2 -2
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +17 -3
- package/dist/builtin/workflows/src/extension/wiring.ts +17 -1
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +34 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +13 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +86 -14
- package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +11 -3
- package/dist/builtin/workflows/src/shared/types.ts +8 -4
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +64 -2
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
- package/dist/builtin/workflows/src/tui/workflow-status.ts +2 -0
- package/dist/core/builtin-packages.d.ts.map +1 -1
- package/dist/core/builtin-packages.js +6 -0
- package/dist/core/builtin-packages.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/types.d.ts +20 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +17 -8
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts +11 -9
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +55 -10
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/project-trust.d.ts +1 -0
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js +3 -3
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/resource-loader.d.ts +9 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +72 -9
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +5 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/structured-output.d.ts +39 -0
- package/dist/core/tools/structured-output.d.ts.map +1 -0
- package/dist/core/tools/structured-output.js +141 -0
- package/dist/core/tools/structured-output.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +36 -14
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +3 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +16 -0
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +11 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +158 -11
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +39 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/custom-provider.md +1 -0
- package/docs/extensions.md +2 -2
- package/docs/models.md +2 -0
- package/docs/packages.md +3 -1
- package/docs/providers.md +15 -0
- package/docs/sdk.md +61 -0
- package/docs/security.md +1 -1
- package/docs/subagents.md +21 -0
- package/docs/usage.md +2 -0
- package/docs/workflows.md +10 -7
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/structured-output.ts +22 -53
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +12 -9
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { createHash, randomUUID as nodeRandomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
type Api,
|
|
4
|
+
type AssistantMessage,
|
|
5
|
+
type AssistantMessageEventStream,
|
|
6
|
+
calculateCost,
|
|
7
|
+
type Context,
|
|
8
|
+
createAssistantMessageEventStream,
|
|
9
|
+
type Model,
|
|
10
|
+
type SimpleStreamOptions,
|
|
11
|
+
} from "@earendil-works/pi-ai";
|
|
12
|
+
import { parseJsonObject, sanitizeDiagnosticText } from "./config.js";
|
|
13
|
+
import { CursorConversationStateStore, type CursorConversationSnapshot } from "./conversation-state.js";
|
|
14
|
+
import { resolveCursorModelVariant } from "./model-mapper.js";
|
|
15
|
+
import type { CursorAgentTransport, CursorRunStream, CursorServerMessage, CursorToolCallMessage, CursorToolResultMessage } from "./transport.js";
|
|
16
|
+
|
|
17
|
+
export interface CursorStreamAdapterOptions {
|
|
18
|
+
readonly transport: CursorAgentTransport;
|
|
19
|
+
readonly conversationState?: CursorConversationStateStore;
|
|
20
|
+
readonly uuid?: () => string;
|
|
21
|
+
readonly pausedTurnIdleTimeoutMs?: number;
|
|
22
|
+
readonly streamReadTimeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CursorStreamRuntime {
|
|
26
|
+
readonly transport: CursorAgentTransport;
|
|
27
|
+
readonly conversationState: CursorConversationStateStore;
|
|
28
|
+
readonly uuid: () => string;
|
|
29
|
+
readonly pausedTurnIdleTimeoutMs: number;
|
|
30
|
+
readonly streamReadTimeoutMs: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_PAUSED_TURN_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
34
|
+
const DEFAULT_STREAM_READ_TIMEOUT_MS = 10 * 60 * 1000;
|
|
35
|
+
const TOOL_CALL_BATCH_IDLE_TIMEOUT_MS = 100;
|
|
36
|
+
|
|
37
|
+
type IteratorReadResult =
|
|
38
|
+
| { readonly kind: "message"; readonly result: IteratorResult<CursorServerMessage> }
|
|
39
|
+
| { readonly kind: "aborted" };
|
|
40
|
+
|
|
41
|
+
type CursorReadRaceResult =
|
|
42
|
+
| { readonly kind: "message"; readonly result: IteratorResult<CursorServerMessage>; readonly read: CursorMessageReadHandle }
|
|
43
|
+
| { readonly kind: "error"; readonly error: Error; readonly read: CursorMessageReadHandle }
|
|
44
|
+
| { readonly kind: "aborted" };
|
|
45
|
+
|
|
46
|
+
function defaultCursorUuid(): string {
|
|
47
|
+
return nodeRandomUUID();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class CursorStreamAdapter {
|
|
51
|
+
readonly #runtime: CursorStreamRuntime;
|
|
52
|
+
readonly #messageReaders = new WeakMap<CursorRunStream, CursorMessageReader>();
|
|
53
|
+
|
|
54
|
+
constructor(options: CursorStreamAdapterOptions) {
|
|
55
|
+
this.#runtime = {
|
|
56
|
+
transport: options.transport,
|
|
57
|
+
conversationState: options.conversationState ?? new CursorConversationStateStore(),
|
|
58
|
+
uuid: options.uuid ?? defaultCursorUuid,
|
|
59
|
+
pausedTurnIdleTimeoutMs: options.pausedTurnIdleTimeoutMs ?? DEFAULT_PAUSED_TURN_IDLE_TIMEOUT_MS,
|
|
60
|
+
streamReadTimeoutMs: options.streamReadTimeoutMs ?? DEFAULT_STREAM_READ_TIMEOUT_MS,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
streamSimple = (model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream => {
|
|
65
|
+
const stream = createAssistantMessageEventStream();
|
|
66
|
+
void this.#runStream(stream, model, context, options);
|
|
67
|
+
return stream;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
async dispose(): Promise<void> {
|
|
71
|
+
await this.#runtime.conversationState.dispose();
|
|
72
|
+
await this.#runtime.transport.dispose();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async cleanupSession(sessionId: string): Promise<void> {
|
|
76
|
+
await this.#runtime.conversationState.cancelTurn(deriveCursorBridgeKeyFromSessionId(sessionId));
|
|
77
|
+
this.#runtime.transport.discardConversation?.(deriveCursorWireConversationIdFromSessionId(sessionId));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getLifecycleSnapshot(): CursorConversationSnapshot {
|
|
81
|
+
return this.#runtime.conversationState.snapshot(this.#runtime.transport.getLifecycleSnapshot());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#messageReaderFor(runStream: CursorRunStream): CursorMessageReader {
|
|
85
|
+
const existing = this.#messageReaders.get(runStream);
|
|
86
|
+
if (existing) return existing;
|
|
87
|
+
const reader = new CursorMessageReader(runStream.messages);
|
|
88
|
+
this.#messageReaders.set(runStream, reader);
|
|
89
|
+
return reader;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async #runStream(
|
|
93
|
+
stream: AssistantMessageEventStream,
|
|
94
|
+
model: Model<Api>,
|
|
95
|
+
context: Context,
|
|
96
|
+
options?: SimpleStreamOptions,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const output = createOutputMessage(model);
|
|
99
|
+
stream.push({ type: "start", partial: output });
|
|
100
|
+
|
|
101
|
+
let runStream: CursorRunStream | undefined;
|
|
102
|
+
let activeConversationKey: string | undefined;
|
|
103
|
+
let textIndex: number | undefined;
|
|
104
|
+
let thinkingIndex: number | undefined;
|
|
105
|
+
let terminalEventSent = false;
|
|
106
|
+
let sawToolCall = false;
|
|
107
|
+
const pendingToolCalls: CursorToolCallMessage[] = [];
|
|
108
|
+
const effectiveTimeoutMs = options?.timeoutMs ?? this.#runtime.streamReadTimeoutMs;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
if (!options?.apiKey) {
|
|
112
|
+
throw new Error("Cursor OAuth credentials are required. Run /login and select Cursor.");
|
|
113
|
+
}
|
|
114
|
+
if (hasImageInput(context)) {
|
|
115
|
+
throw new Error("Cursor provider currently supports text input only; vision/image content is unsupported.");
|
|
116
|
+
}
|
|
117
|
+
if (options.signal?.aborted) {
|
|
118
|
+
throw new CursorStreamAbortError();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const requestId = this.#runtime.uuid();
|
|
122
|
+
const conversationIdentity = deriveCursorConversationIdentity(context, options.sessionId);
|
|
123
|
+
activeConversationKey = conversationIdentity.activeKey;
|
|
124
|
+
const resolvedModelId = resolveCursorModelVariant(model.id, model.thinkingLevelMap, options.reasoning);
|
|
125
|
+
const trailingToolResults = getTrailingToolResults(context);
|
|
126
|
+
if (trailingToolResults.length > 0) {
|
|
127
|
+
runStream = await this.#runtime.conversationState.resumeTurnWithToolResults(activeConversationKey, trailingToolResults, { signal: options.signal, timeoutMs: effectiveTimeoutMs });
|
|
128
|
+
} else {
|
|
129
|
+
runStream = await this.#runtime.transport.run({
|
|
130
|
+
accessToken: options.apiKey,
|
|
131
|
+
requestId,
|
|
132
|
+
conversationId: conversationIdentity.wireConversationId,
|
|
133
|
+
model,
|
|
134
|
+
resolvedModelId,
|
|
135
|
+
thinkingLevel: options.reasoning,
|
|
136
|
+
context,
|
|
137
|
+
signal: options.signal,
|
|
138
|
+
openTimeoutMs: effectiveTimeoutMs,
|
|
139
|
+
});
|
|
140
|
+
this.#runtime.conversationState.registerTurn(activeConversationKey, runStream);
|
|
141
|
+
}
|
|
142
|
+
const reader = this.#messageReaderFor(runStream);
|
|
143
|
+
while (true) {
|
|
144
|
+
const readTimeoutMs = pendingToolCalls.length > 0 ? Math.min(effectiveTimeoutMs, TOOL_CALL_BATCH_IDLE_TIMEOUT_MS) : effectiveTimeoutMs;
|
|
145
|
+
const next = await readNextCursorMessage(reader, options.signal, readTimeoutMs);
|
|
146
|
+
if (next.kind === "aborted") {
|
|
147
|
+
throw new CursorStreamAbortError();
|
|
148
|
+
}
|
|
149
|
+
if (next.result.done) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
const message = next.result.value;
|
|
153
|
+
if (pendingToolCalls.length > 0 && message.type !== "toolCall" && message.type !== "usage") {
|
|
154
|
+
closeOpenContent(stream, output, textIndex, thinkingIndex);
|
|
155
|
+
if (!(message.type === "done" && message.reason === "toolUse")) reader.unread(next.result);
|
|
156
|
+
this.#runtime.conversationState.pauseTurnForTools(activeConversationKey, runStream, pendingToolCalls, { signal: options?.signal, idleTimeoutMs: this.#runtime.pausedTurnIdleTimeoutMs });
|
|
157
|
+
output.stopReason = "toolUse";
|
|
158
|
+
stream.push({ type: "done", reason: "toolUse", message: output });
|
|
159
|
+
terminalEventSent = true;
|
|
160
|
+
runStream = undefined;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (message.type === "textDelta") {
|
|
164
|
+
textIndex = appendTextDelta(stream, output, textIndex, message.text);
|
|
165
|
+
} else if (message.type === "thinkingDelta") {
|
|
166
|
+
thinkingIndex = appendThinkingDelta(stream, output, thinkingIndex, message.text);
|
|
167
|
+
} else if (message.type === "toolCall") {
|
|
168
|
+
sawToolCall = true;
|
|
169
|
+
pendingToolCalls.push(message);
|
|
170
|
+
appendToolCall(stream, output, message.id, message.name, message.argumentsJson);
|
|
171
|
+
continue;
|
|
172
|
+
} else if (message.type === "usage") {
|
|
173
|
+
updateUsage(output, model, message);
|
|
174
|
+
} else if (message.type === "nonMcpExec") {
|
|
175
|
+
continue;
|
|
176
|
+
} else {
|
|
177
|
+
closeOpenContent(stream, output, textIndex, thinkingIndex);
|
|
178
|
+
if (pendingToolCalls.length > 0) {
|
|
179
|
+
this.#runtime.conversationState.pauseTurnForTools(activeConversationKey, runStream, pendingToolCalls, { signal: options?.signal, idleTimeoutMs: this.#runtime.pausedTurnIdleTimeoutMs });
|
|
180
|
+
output.stopReason = "toolUse";
|
|
181
|
+
stream.push({ type: "done", reason: "toolUse", message: output });
|
|
182
|
+
runStream = undefined;
|
|
183
|
+
} else {
|
|
184
|
+
output.stopReason = message.reason;
|
|
185
|
+
stream.push({ type: "done", reason: message.reason, message: output });
|
|
186
|
+
}
|
|
187
|
+
terminalEventSent = true;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!terminalEventSent) {
|
|
193
|
+
closeOpenContent(stream, output, textIndex, thinkingIndex);
|
|
194
|
+
if (pendingToolCalls.length > 0 && runStream) {
|
|
195
|
+
this.#runtime.conversationState.pauseTurnForTools(activeConversationKey, runStream, pendingToolCalls, { signal: options?.signal, idleTimeoutMs: this.#runtime.pausedTurnIdleTimeoutMs });
|
|
196
|
+
output.stopReason = "toolUse";
|
|
197
|
+
stream.push({ type: "done", reason: "toolUse", message: output });
|
|
198
|
+
runStream = undefined;
|
|
199
|
+
} else {
|
|
200
|
+
output.stopReason = sawToolCall ? "toolUse" : "stop";
|
|
201
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const aborted = error instanceof CursorStreamAbortError || options?.signal?.aborted;
|
|
206
|
+
const timedOut = error instanceof CursorStreamTimeoutError;
|
|
207
|
+
if (timedOut && pendingToolCalls.length > 0 && runStream && activeConversationKey) {
|
|
208
|
+
closeOpenContent(stream, output, textIndex, thinkingIndex);
|
|
209
|
+
this.#runtime.conversationState.pauseTurnForTools(activeConversationKey, runStream, pendingToolCalls, { signal: options?.signal, idleTimeoutMs: this.#runtime.pausedTurnIdleTimeoutMs });
|
|
210
|
+
output.stopReason = "toolUse";
|
|
211
|
+
stream.push({ type: "done", reason: "toolUse", message: output });
|
|
212
|
+
terminalEventSent = true;
|
|
213
|
+
runStream = undefined;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
output.stopReason = aborted ? "aborted" : "error";
|
|
217
|
+
output.errorMessage = aborted
|
|
218
|
+
? "Cursor stream aborted."
|
|
219
|
+
: timedOut
|
|
220
|
+
? "Cursor stream timed out while waiting for provider output."
|
|
221
|
+
: sanitizeDiagnosticText(error instanceof Error ? error.message : "Cursor stream failed.", [options?.apiKey ?? ""]);
|
|
222
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
223
|
+
if ((aborted || timedOut) && runStream && activeConversationKey) {
|
|
224
|
+
try {
|
|
225
|
+
await this.#runtime.conversationState.cancelTurn(activeConversationKey);
|
|
226
|
+
} catch {
|
|
227
|
+
// Terminal events must not be suppressed by best-effort cleanup failures.
|
|
228
|
+
} finally {
|
|
229
|
+
runStream = undefined;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} finally {
|
|
233
|
+
try {
|
|
234
|
+
if (runStream && !options?.signal?.aborted) {
|
|
235
|
+
await runStream.close();
|
|
236
|
+
if (activeConversationKey) this.#runtime.conversationState.completeTurn(activeConversationKey);
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
stream.end(output);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
class CursorStreamAbortError extends Error {
|
|
246
|
+
constructor() {
|
|
247
|
+
super("Cursor stream aborted.");
|
|
248
|
+
this.name = "CursorStreamAbortError";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
class CursorStreamTimeoutError extends Error {
|
|
253
|
+
constructor() {
|
|
254
|
+
super("Cursor stream timed out while waiting for provider output.");
|
|
255
|
+
this.name = "CursorStreamTimeoutError";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface CursorPendingMessageRead {
|
|
260
|
+
readonly promise: Promise<IteratorResult<CursorServerMessage>>;
|
|
261
|
+
consumed: boolean;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
interface CursorMessageReadHandle {
|
|
265
|
+
readonly promise: Promise<IteratorResult<CursorServerMessage>>;
|
|
266
|
+
consumeResult(result: IteratorResult<CursorServerMessage>): void;
|
|
267
|
+
consumeError(error: Error): void;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
type CursorBufferedMessageRead =
|
|
271
|
+
| { readonly kind: "result"; readonly result: IteratorResult<CursorServerMessage> }
|
|
272
|
+
| { readonly kind: "error"; readonly error: Error };
|
|
273
|
+
|
|
274
|
+
// A Cursor tool turn can pause by timing out while an iterator.next() is still
|
|
275
|
+
// waiting for the first post-tool provider message. Reuse and buffer that exact
|
|
276
|
+
// read across resume calls so the message is not consumed by an abandoned race.
|
|
277
|
+
class CursorMessageReader {
|
|
278
|
+
readonly #iterator: AsyncIterator<CursorServerMessage>;
|
|
279
|
+
#pending: CursorPendingMessageRead | undefined;
|
|
280
|
+
#buffered: CursorBufferedMessageRead | undefined;
|
|
281
|
+
|
|
282
|
+
constructor(messages: AsyncIterable<CursorServerMessage>) {
|
|
283
|
+
this.#iterator = messages[Symbol.asyncIterator]();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
unread(result: IteratorResult<CursorServerMessage>): void {
|
|
287
|
+
if (this.#buffered) return;
|
|
288
|
+
this.#buffered = { kind: "result", result };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
peek(): CursorMessageReadHandle {
|
|
292
|
+
if (this.#buffered) return this.peekBuffered(this.#buffered);
|
|
293
|
+
const pending = this.#pending ?? this.startRead();
|
|
294
|
+
return {
|
|
295
|
+
promise: pending.promise,
|
|
296
|
+
consumeResult: (result) => {
|
|
297
|
+
pending.consumed = true;
|
|
298
|
+
if (this.#pending === pending) this.#pending = undefined;
|
|
299
|
+
if (this.#buffered?.kind === "result" && this.#buffered.result === result) this.#buffered = undefined;
|
|
300
|
+
},
|
|
301
|
+
consumeError: (error) => {
|
|
302
|
+
pending.consumed = true;
|
|
303
|
+
if (this.#pending === pending) this.#pending = undefined;
|
|
304
|
+
if (this.#buffered?.kind === "error" && this.#buffered.error === error) this.#buffered = undefined;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private peekBuffered(buffered: CursorBufferedMessageRead): CursorMessageReadHandle {
|
|
310
|
+
return {
|
|
311
|
+
promise: buffered.kind === "result" ? Promise.resolve(buffered.result) : Promise.reject(buffered.error),
|
|
312
|
+
consumeResult: (result) => {
|
|
313
|
+
if (this.#buffered === buffered && buffered.kind === "result" && buffered.result === result) this.#buffered = undefined;
|
|
314
|
+
},
|
|
315
|
+
consumeError: (error) => {
|
|
316
|
+
if (this.#buffered === buffered && buffered.kind === "error" && buffered.error === error) this.#buffered = undefined;
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private startRead(): CursorPendingMessageRead {
|
|
322
|
+
const pending: CursorPendingMessageRead = {
|
|
323
|
+
promise: this.#iterator.next().catch((error: Error) => {
|
|
324
|
+
throw normalizeCursorReadError(error);
|
|
325
|
+
}),
|
|
326
|
+
consumed: false,
|
|
327
|
+
};
|
|
328
|
+
this.#pending = pending;
|
|
329
|
+
pending.promise.then(
|
|
330
|
+
(result) => {
|
|
331
|
+
if (this.#pending !== pending) return;
|
|
332
|
+
this.#pending = undefined;
|
|
333
|
+
if (!pending.consumed) this.#buffered = { kind: "result", result };
|
|
334
|
+
},
|
|
335
|
+
(error: Error) => {
|
|
336
|
+
if (this.#pending !== pending) return;
|
|
337
|
+
this.#pending = undefined;
|
|
338
|
+
if (!pending.consumed) this.#buffered = { kind: "error", error };
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
void pending.promise.catch(() => undefined);
|
|
342
|
+
return pending;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function normalizeCursorReadError(error: Error): Error {
|
|
347
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function createOutputMessage(model: Model<Api>): AssistantMessage {
|
|
351
|
+
return {
|
|
352
|
+
role: "assistant",
|
|
353
|
+
content: [],
|
|
354
|
+
api: model.api,
|
|
355
|
+
provider: model.provider,
|
|
356
|
+
model: model.id,
|
|
357
|
+
usage: {
|
|
358
|
+
input: 0,
|
|
359
|
+
output: 0,
|
|
360
|
+
cacheRead: 0,
|
|
361
|
+
cacheWrite: 0,
|
|
362
|
+
totalTokens: 0,
|
|
363
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
364
|
+
},
|
|
365
|
+
stopReason: "stop",
|
|
366
|
+
timestamp: Date.now(),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function getTrailingToolResults(context: Context): CursorToolResultMessage[] {
|
|
371
|
+
const results: CursorToolResultMessage[] = [];
|
|
372
|
+
for (let index = context.messages.length - 1; index >= 0; index--) {
|
|
373
|
+
const message = context.messages[index];
|
|
374
|
+
if (!message || message.role !== "toolResult") break;
|
|
375
|
+
results.unshift({ toolCallId: message.toolCallId, toolName: message.toolName, text: textFromToolResult(message), isError: message.isError });
|
|
376
|
+
}
|
|
377
|
+
return results;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function textFromToolResult(message: Extract<Context["messages"][number], { readonly role: "toolResult" }>): string {
|
|
381
|
+
return message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function textFromMessage(message: Context["messages"][number]): string {
|
|
385
|
+
if (message.role === "user") {
|
|
386
|
+
if (typeof message.content === "string") return message.content;
|
|
387
|
+
return message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
|
|
388
|
+
}
|
|
389
|
+
if (message.role === "assistant") {
|
|
390
|
+
return message.content.map((part) => {
|
|
391
|
+
if (part.type === "text") return part.text;
|
|
392
|
+
if (part.type === "thinking") return part.thinking;
|
|
393
|
+
return `toolCall:${part.id}:${part.name}:${JSON.stringify(part.arguments)}`;
|
|
394
|
+
}).join("\n");
|
|
395
|
+
}
|
|
396
|
+
return textFromToolResult(message);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
interface CursorConversationIdentity {
|
|
400
|
+
readonly activeKey: string;
|
|
401
|
+
readonly wireConversationId: string;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function deriveCursorConversationIdentity(context: Context, sessionId: string | undefined): CursorConversationIdentity {
|
|
405
|
+
const bridgeKey = deriveCursorConversationKey("bridge", context, sessionId);
|
|
406
|
+
const conversationKey = deriveCursorConversationKey("conv", context, sessionId);
|
|
407
|
+
return { activeKey: bridgeKey, wireConversationId: deterministicCursorConversationId(conversationKey) };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function deriveCursorBridgeKeyFromSessionId(sessionId: string): string {
|
|
411
|
+
return hashCursorKey("bridge", sessionId);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function deriveCursorWireConversationIdFromSessionId(sessionId: string): string {
|
|
415
|
+
return deterministicCursorConversationId(hashCursorKey("conv", sessionId));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function deriveCursorConversationKey(prefix: "bridge" | "conv", context: Context, sessionId: string | undefined): string {
|
|
419
|
+
const trimmedSessionId = sessionId?.trim();
|
|
420
|
+
if (trimmedSessionId) return hashCursorKey(prefix, trimmedSessionId);
|
|
421
|
+
const firstUserMessage = context.messages.find((message) => message.role === "user");
|
|
422
|
+
// No-session runs lack a durable session id, so this fallback is best-effort
|
|
423
|
+
// and can collide for conversations with the same leading user text.
|
|
424
|
+
const firstUserText = firstUserMessage ? textFromMessage(firstUserMessage).slice(0, 200) : "";
|
|
425
|
+
return hashCursorKey(prefix, firstUserText);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function hashCursorKey(prefix: "bridge" | "conv", value: string): string {
|
|
429
|
+
return createHash("sha256").update(`${prefix}:${value}`).digest("hex").slice(0, 16);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function deterministicCursorConversationId(conversationKey: string): string {
|
|
433
|
+
const hex = createHash("sha256").update(`cursor-conv-id:${conversationKey}`).digest("hex").slice(0, 32);
|
|
434
|
+
const variantNibble = (0x8 | (Number.parseInt(hex[16] ?? "0", 16) & 0x3)).toString(16);
|
|
435
|
+
return [
|
|
436
|
+
hex.slice(0, 8),
|
|
437
|
+
hex.slice(8, 12),
|
|
438
|
+
`4${hex.slice(13, 16)}`,
|
|
439
|
+
`${variantNibble}${hex.slice(17, 20)}`,
|
|
440
|
+
hex.slice(20, 32),
|
|
441
|
+
].join("-");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function hasImageInput(context: Context): boolean {
|
|
445
|
+
for (const message of context.messages) {
|
|
446
|
+
if (message.role === "user") {
|
|
447
|
+
if (typeof message.content !== "string" && message.content.some((content) => content.type === "image")) return true;
|
|
448
|
+
} else if (message.role === "toolResult") {
|
|
449
|
+
if (message.content.some((content) => content.type === "image")) return true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function appendTextDelta(stream: AssistantMessageEventStream, output: AssistantMessage, existingIndex: number | undefined, delta: string): number {
|
|
456
|
+
const contentIndex = existingIndex ?? output.content.length;
|
|
457
|
+
if (existingIndex === undefined) {
|
|
458
|
+
output.content.push({ type: "text", text: "" });
|
|
459
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
460
|
+
}
|
|
461
|
+
const block = output.content[contentIndex];
|
|
462
|
+
if (block?.type === "text") {
|
|
463
|
+
block.text += delta;
|
|
464
|
+
}
|
|
465
|
+
stream.push({ type: "text_delta", contentIndex, delta, partial: output });
|
|
466
|
+
return contentIndex;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function appendThinkingDelta(stream: AssistantMessageEventStream, output: AssistantMessage, existingIndex: number | undefined, delta: string): number {
|
|
470
|
+
const contentIndex = existingIndex ?? output.content.length;
|
|
471
|
+
if (existingIndex === undefined) {
|
|
472
|
+
output.content.push({ type: "thinking", thinking: "" });
|
|
473
|
+
stream.push({ type: "thinking_start", contentIndex, partial: output });
|
|
474
|
+
}
|
|
475
|
+
const block = output.content[contentIndex];
|
|
476
|
+
if (block?.type === "thinking") {
|
|
477
|
+
block.thinking += delta;
|
|
478
|
+
}
|
|
479
|
+
stream.push({ type: "thinking_delta", contentIndex, delta, partial: output });
|
|
480
|
+
return contentIndex;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function appendToolCall(stream: AssistantMessageEventStream, output: AssistantMessage, id: string, name: string, argumentsJson: string): void {
|
|
484
|
+
const contentIndex = output.content.length;
|
|
485
|
+
const parsedArguments = parseJsonObject(argumentsJson) ?? {};
|
|
486
|
+
output.content.push({ type: "toolCall", id, name, arguments: parsedArguments });
|
|
487
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
488
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: argumentsJson, partial: output });
|
|
489
|
+
stream.push({
|
|
490
|
+
type: "toolcall_end",
|
|
491
|
+
contentIndex,
|
|
492
|
+
toolCall: { type: "toolCall", id, name, arguments: parsedArguments },
|
|
493
|
+
partial: output,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function closeOpenContent(stream: AssistantMessageEventStream, output: AssistantMessage, textIndex: number | undefined, thinkingIndex: number | undefined): void {
|
|
498
|
+
if (textIndex !== undefined) {
|
|
499
|
+
const block = output.content[textIndex];
|
|
500
|
+
if (block?.type === "text") {
|
|
501
|
+
stream.push({ type: "text_end", contentIndex: textIndex, content: block.text, partial: output });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (thinkingIndex !== undefined) {
|
|
505
|
+
const block = output.content[thinkingIndex];
|
|
506
|
+
if (block?.type === "thinking") {
|
|
507
|
+
stream.push({ type: "thinking_end", contentIndex: thinkingIndex, content: block.thinking, partial: output });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function updateUsage(output: AssistantMessage, model: Model<Api>, message: Extract<CursorServerMessage, { readonly type: "usage" }>): void {
|
|
513
|
+
if (message.kind === "outputDelta") {
|
|
514
|
+
output.usage.output += message.outputTokens;
|
|
515
|
+
} else {
|
|
516
|
+
if (message.inputTokens !== undefined) output.usage.input = message.inputTokens;
|
|
517
|
+
// Cursor checkpoint `usedTokens` omits a dedicated input field on some
|
|
518
|
+
// frames, so estimate input from already-seen output/cache counters.
|
|
519
|
+
else if (message.usedTokens !== undefined) output.usage.input = Math.max(0, message.usedTokens - output.usage.output - output.usage.cacheRead - output.usage.cacheWrite);
|
|
520
|
+
if (message.outputTokens !== undefined) output.usage.output = message.outputTokens;
|
|
521
|
+
if (message.cacheReadTokens !== undefined) output.usage.cacheRead = message.cacheReadTokens;
|
|
522
|
+
if (message.cacheWriteTokens !== undefined) output.usage.cacheWrite = message.cacheWriteTokens;
|
|
523
|
+
}
|
|
524
|
+
output.usage.totalTokens = output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
525
|
+
output.usage.cost = calculateCost(model, output.usage);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function readNextCursorMessage(reader: CursorMessageReader, signal: AbortSignal | undefined, timeoutMs: number): Promise<IteratorReadResult> {
|
|
529
|
+
if (signal?.aborted) return { kind: "aborted" };
|
|
530
|
+
let abortListener: (() => void) | undefined;
|
|
531
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
532
|
+
const abortPromise = signal ? new Promise<CursorReadRaceResult>((resolve) => {
|
|
533
|
+
abortListener = () => resolve({ kind: "aborted" });
|
|
534
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
535
|
+
}) : undefined;
|
|
536
|
+
const timeoutPromise = timeoutMs > 0 ? new Promise<CursorReadRaceResult>((_resolve, reject) => {
|
|
537
|
+
timeout = setTimeout(() => reject(new CursorStreamTimeoutError()), timeoutMs);
|
|
538
|
+
timeout.unref?.();
|
|
539
|
+
}) : undefined;
|
|
540
|
+
const read = reader.peek();
|
|
541
|
+
const messagePromise = read.promise.then(
|
|
542
|
+
(result): CursorReadRaceResult => ({ kind: "message", result, read }),
|
|
543
|
+
(error: Error): CursorReadRaceResult => ({ kind: "error", error: normalizeCursorReadError(error), read }),
|
|
544
|
+
);
|
|
545
|
+
try {
|
|
546
|
+
const next = await Promise.race([messagePromise, ...(abortPromise ? [abortPromise] : []), ...(timeoutPromise ? [timeoutPromise] : [])]);
|
|
547
|
+
if (next.kind === "message") {
|
|
548
|
+
next.read.consumeResult(next.result);
|
|
549
|
+
return { kind: "message", result: next.result };
|
|
550
|
+
}
|
|
551
|
+
if (next.kind === "error") {
|
|
552
|
+
next.read.consumeError(next.error);
|
|
553
|
+
throw next.error;
|
|
554
|
+
}
|
|
555
|
+
return next;
|
|
556
|
+
} finally {
|
|
557
|
+
if (abortListener) signal?.removeEventListener("abort", abortListener);
|
|
558
|
+
if (timeout) clearTimeout(timeout);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function createCursorStreamAdapter(options: CursorStreamAdapterOptions): CursorStreamAdapter {
|
|
563
|
+
return new CursorStreamAdapter(options);
|
|
564
|
+
}
|