@tt-a1i/openpi 0.5.0 → 0.6.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/README.md +18 -10
- package/SETUP.md +8 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/openpi.js +25 -15
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +8 -1
- package/extensions/background-terminals/src/manager.ts +3 -5
- package/extensions/background-terminals/src/result-delivery.ts +43 -23
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +5 -1
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +45 -1
- package/extensions/plan-mode/index.ts +75 -4
- package/extensions/setup/index.ts +15 -3
- package/extensions/shared/child-session.ts +25 -5
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/setup-config.ts +10 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/subagents/index.ts +44 -4
- package/extensions/subagents/src/backends/pi.ts +76 -5
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +5 -0
- package/extensions/subagents/src/prompt.ts +17 -3
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- package/extensions/ui-customization/footer.ts +16 -5
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +25 -2
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/completion-projection.ts +3 -1
- package/extensions/workflows/dashboard.ts +8 -0
- package/extensions/workflows/index.ts +13 -0
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/prompt.ts +4 -10
- package/extensions/workflows/result-delivery.ts +96 -22
- package/extensions/workflows/retention.ts +6 -0
- package/extensions/workflows/runner.ts +6 -71
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +3 -2
- package/skills/subagents/SKILL.md +1 -0
- package/skills/workflows/REFERENCE.md +3 -3
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +3 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/web-host.ts +330 -50
- package/web/protocol/types.ts +5 -0
- package/web/runtime/pi-runtime.ts +240 -25
- package/web/runtime/types.ts +32 -1
- package/web/ui/app.js +343 -41
- package/web/ui/index.html +3 -0
- package/web/ui/styles.css +119 -37
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as http2 from "node:http2";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import * as tls from "node:tls";
|
|
4
|
+
|
|
5
|
+
export interface CursorHttp2ConnectOptions {
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isLocalOrMetadataHost(hostname: string): boolean {
|
|
11
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
12
|
+
if (
|
|
13
|
+
host === "localhost" ||
|
|
14
|
+
host.endsWith(".localhost") ||
|
|
15
|
+
host === "metadata.google.internal"
|
|
16
|
+
) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (host === "::" || host === "::1" || /^f[cd]/.test(host)) return true;
|
|
20
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\./.exec(host);
|
|
21
|
+
if (!ipv4) return false;
|
|
22
|
+
const first = Number(ipv4[1]);
|
|
23
|
+
const second = Number(ipv4[2]);
|
|
24
|
+
return (
|
|
25
|
+
first === 0 ||
|
|
26
|
+
first === 10 ||
|
|
27
|
+
first === 127 ||
|
|
28
|
+
(first === 169 && second === 254) ||
|
|
29
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
30
|
+
(first === 192 && second === 168)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shouldBypassProxy(target: URL): boolean {
|
|
35
|
+
if (isLocalOrMetadataHost(target.hostname)) return true;
|
|
36
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
|
37
|
+
if (!noProxy) return false;
|
|
38
|
+
const targetHost = target.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
39
|
+
const targetPort =
|
|
40
|
+
target.port || (target.protocol === "https:" ? "443" : "80");
|
|
41
|
+
for (const rawRule of noProxy.split(/[,\s]+/)) {
|
|
42
|
+
let rule = rawRule.trim().toLowerCase();
|
|
43
|
+
if (!rule) continue;
|
|
44
|
+
if (rule === "*") return true;
|
|
45
|
+
let rulePort: string | undefined;
|
|
46
|
+
const portMatch = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(rule);
|
|
47
|
+
if (portMatch) {
|
|
48
|
+
rule = portMatch[1]!;
|
|
49
|
+
rulePort = portMatch[2];
|
|
50
|
+
}
|
|
51
|
+
if (rulePort && rulePort !== targetPort) continue;
|
|
52
|
+
rule = rule.replace(/^\[|\]$/g, "").replace(/^\./, "");
|
|
53
|
+
if (targetHost === rule || targetHost.endsWith(`.${rule}`)) return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve Cursor's provider override first, then the standard proxy variables. */
|
|
59
|
+
export function resolveCursorProxy(target: URL): string | undefined {
|
|
60
|
+
if (shouldBypassProxy(target)) return undefined;
|
|
61
|
+
const protocolProxy =
|
|
62
|
+
target.protocol === "https:"
|
|
63
|
+
? process.env.HTTPS_PROXY || process.env.https_proxy
|
|
64
|
+
: process.env.HTTP_PROXY || process.env.http_proxy;
|
|
65
|
+
return [
|
|
66
|
+
process.env.PI_PROXY_CURSOR,
|
|
67
|
+
process.env.PI_PROXY,
|
|
68
|
+
protocolProxy,
|
|
69
|
+
process.env.ALL_PROXY || process.env.all_proxy,
|
|
70
|
+
]
|
|
71
|
+
.map((value) => value?.trim())
|
|
72
|
+
.find((value): value is string => Boolean(value));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function connectProxyTunnel(
|
|
76
|
+
proxyUrl: URL,
|
|
77
|
+
targetUrl: URL,
|
|
78
|
+
options: CursorHttp2ConnectOptions,
|
|
79
|
+
): Promise<net.Socket> {
|
|
80
|
+
if (!["http:", "https:"].includes(proxyUrl.protocol)) {
|
|
81
|
+
return Promise.reject(
|
|
82
|
+
new Error(`Unsupported Cursor proxy protocol: ${proxyUrl.protocol}`),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (options.signal?.aborted) {
|
|
86
|
+
return Promise.reject(new Error("Cursor proxy tunnel aborted"));
|
|
87
|
+
}
|
|
88
|
+
const proxyTls = proxyUrl.protocol === "https:";
|
|
89
|
+
const proxyPort = Number(proxyUrl.port || (proxyTls ? 443 : 80));
|
|
90
|
+
const targetPort = Number(
|
|
91
|
+
targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
92
|
+
);
|
|
93
|
+
const targetAuthority = `${targetUrl.hostname}:${targetPort}`;
|
|
94
|
+
let proxyAuthorization: string | undefined;
|
|
95
|
+
if (proxyUrl.username || proxyUrl.password) {
|
|
96
|
+
try {
|
|
97
|
+
const credentials = `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`;
|
|
98
|
+
proxyAuthorization = Buffer.from(credentials).toString("base64");
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
return Promise.reject(
|
|
101
|
+
new Error("Cursor proxy credentials contain invalid percent-encoding", {
|
|
102
|
+
cause,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const { promise, resolve, reject } = Promise.withResolvers<net.Socket>();
|
|
108
|
+
let rawSocket: net.Socket | undefined;
|
|
109
|
+
let targetSocket: net.Socket | undefined;
|
|
110
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
111
|
+
let response = Buffer.alloc(0);
|
|
112
|
+
let settled = false;
|
|
113
|
+
|
|
114
|
+
const cleanup = () => {
|
|
115
|
+
if (timer) clearTimeout(timer);
|
|
116
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
117
|
+
rawSocket?.removeListener("error", onError);
|
|
118
|
+
rawSocket?.removeListener(proxyTls ? "secureConnect" : "connect", onReady);
|
|
119
|
+
rawSocket?.removeListener("data", onData);
|
|
120
|
+
targetSocket?.removeListener("error", onError);
|
|
121
|
+
targetSocket?.removeListener("secureConnect", onTargetReady);
|
|
122
|
+
};
|
|
123
|
+
const fail = (error: Error) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
settled = true;
|
|
126
|
+
cleanup();
|
|
127
|
+
targetSocket?.destroy();
|
|
128
|
+
rawSocket?.destroy();
|
|
129
|
+
reject(error);
|
|
130
|
+
};
|
|
131
|
+
const succeed = (socket: net.Socket) => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
settled = true;
|
|
134
|
+
cleanup();
|
|
135
|
+
resolve(socket);
|
|
136
|
+
};
|
|
137
|
+
const onAbort = () => fail(new Error("Cursor proxy tunnel aborted"));
|
|
138
|
+
const onError = (error: Error) => fail(error);
|
|
139
|
+
const onTargetReady = () => {
|
|
140
|
+
if (targetSocket) succeed(targetSocket);
|
|
141
|
+
};
|
|
142
|
+
const onData = (chunk: Buffer) => {
|
|
143
|
+
if (!rawSocket) return;
|
|
144
|
+
response = Buffer.concat([response, chunk]);
|
|
145
|
+
if (response.length > 64 * 1024) {
|
|
146
|
+
fail(new Error("Cursor proxy response headers exceed 64 KiB"));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const headerEnd = response.indexOf("\r\n\r\n");
|
|
150
|
+
if (headerEnd === -1) return;
|
|
151
|
+
const statusLine = response
|
|
152
|
+
.subarray(0, headerEnd)
|
|
153
|
+
.toString("latin1")
|
|
154
|
+
.split("\r\n")[0];
|
|
155
|
+
if (!/^HTTP\/1\.[01] 200\b/.test(statusLine ?? "")) {
|
|
156
|
+
fail(
|
|
157
|
+
new Error(
|
|
158
|
+
`Cursor proxy tunnel failed: ${statusLine || "invalid response"}`,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
rawSocket.removeListener("data", onData);
|
|
164
|
+
if (targetUrl.protocol !== "https:") {
|
|
165
|
+
succeed(rawSocket);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
targetSocket = tls.connect({
|
|
169
|
+
socket: rawSocket,
|
|
170
|
+
servername: targetUrl.hostname,
|
|
171
|
+
ALPNProtocols: ["h2"],
|
|
172
|
+
});
|
|
173
|
+
targetSocket.once("error", onError);
|
|
174
|
+
targetSocket.once("secureConnect", onTargetReady);
|
|
175
|
+
};
|
|
176
|
+
const onReady = () => {
|
|
177
|
+
if (!rawSocket) return;
|
|
178
|
+
let request = `CONNECT ${targetAuthority} HTTP/1.1\r\nHost: ${targetAuthority}\r\n`;
|
|
179
|
+
if (proxyAuthorization) {
|
|
180
|
+
request += `Proxy-Authorization: Basic ${proxyAuthorization}\r\n`;
|
|
181
|
+
}
|
|
182
|
+
rawSocket.on("data", onData);
|
|
183
|
+
rawSocket.write(`${request}\r\n`);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
187
|
+
if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
|
|
188
|
+
const timeoutMs = Math.floor(options.timeoutMs);
|
|
189
|
+
timer = setTimeout(
|
|
190
|
+
() =>
|
|
191
|
+
fail(new Error(`Cursor proxy tunnel timed out after ${timeoutMs}ms`)),
|
|
192
|
+
timeoutMs,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
rawSocket = proxyTls
|
|
196
|
+
? tls.connect({ host: proxyUrl.hostname, port: proxyPort })
|
|
197
|
+
: net.connect({ host: proxyUrl.hostname, port: proxyPort });
|
|
198
|
+
rawSocket.once("error", onError);
|
|
199
|
+
rawSocket.once(proxyTls ? "secureConnect" : "connect", onReady);
|
|
200
|
+
return promise;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Open Cursor's HTTP/2 session directly or through an HTTP CONNECT proxy. */
|
|
204
|
+
export async function connectCursorHttp2(
|
|
205
|
+
baseUrl: string,
|
|
206
|
+
options: CursorHttp2ConnectOptions = {},
|
|
207
|
+
): Promise<http2.ClientHttp2Session> {
|
|
208
|
+
const target = new URL(baseUrl);
|
|
209
|
+
const proxy = resolveCursorProxy(target);
|
|
210
|
+
if (!proxy) return http2.connect(target);
|
|
211
|
+
const socket = await connectProxyTunnel(new URL(proxy), target, options);
|
|
212
|
+
return http2.connect(target, { createConnection: () => socket });
|
|
213
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Node >=22 provides Promise.withResolvers; the repo's ES2022 lib needs a shim. */
|
|
2
|
+
declare global {
|
|
3
|
+
interface PromiseConstructor {
|
|
4
|
+
withResolvers<T>(): {
|
|
5
|
+
promise: Promise<T>;
|
|
6
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
7
|
+
reject: (reason?: unknown) => void;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai-providers — OAuth-backed model providers for pi.
|
|
3
|
+
*
|
|
4
|
+
* Adds OAuth-backed Google Antigravity and Cursor model providers. Both are
|
|
5
|
+
* inert until the user logs in and selects one of their models. Cursor uses
|
|
6
|
+
* AgentService/Run in deliberately chat-only mode: Cursor-native coding tools
|
|
7
|
+
* are not exposed or executed by this extension.
|
|
8
|
+
*
|
|
9
|
+
* Wire protocol: Cloud Code Assist `v1internal:streamGenerateContent` over
|
|
10
|
+
* SSE (see antigravity/provider.ts). Reference implementation: oh-my-pi's
|
|
11
|
+
* google-gemini-cli provider (shared google-gemini-cli/google-antigravity).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createProvider, type ProviderStreams } from "@earendil-works/pi-ai";
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { createOAuthAuth } from "./oauth-adapter.ts";
|
|
17
|
+
import { encodeApiKey } from "./antigravity/credentials.ts";
|
|
18
|
+
import { fetchAntigravityModels } from "./antigravity/discovery.ts";
|
|
19
|
+
import {
|
|
20
|
+
ANTIGRAVITY_API_URL,
|
|
21
|
+
ANTIGRAVITY_MODELS,
|
|
22
|
+
} from "./antigravity/models.ts";
|
|
23
|
+
import {
|
|
24
|
+
loginAntigravity,
|
|
25
|
+
refreshAntigravityToken,
|
|
26
|
+
} from "./antigravity/oauth.ts";
|
|
27
|
+
import { streamAntigravity } from "./antigravity/provider.ts";
|
|
28
|
+
import { getCursorApiKey } from "./cursor/credentials.ts";
|
|
29
|
+
import { fetchCursorModels } from "./cursor/discovery.ts";
|
|
30
|
+
import { transformCursorImageInput } from "./cursor/input-images.ts";
|
|
31
|
+
import { CURSOR_MODELS } from "./cursor/models.ts";
|
|
32
|
+
import { loginCursor, refreshCursorToken } from "./cursor/oauth.ts";
|
|
33
|
+
import { streamCursor } from "./cursor/provider.ts";
|
|
34
|
+
|
|
35
|
+
function providerStreams(
|
|
36
|
+
streamSimple: ProviderStreams["streamSimple"],
|
|
37
|
+
): ProviderStreams {
|
|
38
|
+
return {
|
|
39
|
+
stream: (model, context, options) => streamSimple(model, context, options),
|
|
40
|
+
streamSimple,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function authProviders(pi: ExtensionAPI) {
|
|
45
|
+
pi.on("input", transformCursorImageInput);
|
|
46
|
+
|
|
47
|
+
pi.registerProvider(
|
|
48
|
+
createProvider({
|
|
49
|
+
id: "google-antigravity",
|
|
50
|
+
name: "Google Antigravity",
|
|
51
|
+
baseUrl: ANTIGRAVITY_API_URL,
|
|
52
|
+
api: providerStreams(streamAntigravity),
|
|
53
|
+
auth: {
|
|
54
|
+
oauth: createOAuthAuth({
|
|
55
|
+
name: "Google (Antigravity)",
|
|
56
|
+
isSubscription: true,
|
|
57
|
+
login: loginAntigravity,
|
|
58
|
+
refreshToken: refreshAntigravityToken,
|
|
59
|
+
getApiKey: encodeApiKey,
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
models: ANTIGRAVITY_MODELS,
|
|
63
|
+
fetchModels: fetchAntigravityModels,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
pi.registerProvider(
|
|
68
|
+
createProvider({
|
|
69
|
+
id: "cursor",
|
|
70
|
+
name: "Cursor",
|
|
71
|
+
baseUrl: "https://api2.cursor.sh",
|
|
72
|
+
api: providerStreams(streamCursor),
|
|
73
|
+
auth: {
|
|
74
|
+
oauth: createOAuthAuth({
|
|
75
|
+
name: "Cursor",
|
|
76
|
+
isSubscription: true,
|
|
77
|
+
login: loginCursor,
|
|
78
|
+
refreshToken: refreshCursorToken,
|
|
79
|
+
getApiKey: getCursorApiKey,
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
models: CURSOR_MODELS,
|
|
83
|
+
fetchModels: fetchCursorModels,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ModelAuth,
|
|
3
|
+
OAuthAuth,
|
|
4
|
+
OAuthCredential,
|
|
5
|
+
OAuthCredentials,
|
|
6
|
+
OAuthLoginCallbacks,
|
|
7
|
+
ProviderAuthInteraction,
|
|
8
|
+
} from "@earendil-works/pi-ai";
|
|
9
|
+
|
|
10
|
+
interface LegacyOAuthImplementation {
|
|
11
|
+
name: string;
|
|
12
|
+
isSubscription?: boolean;
|
|
13
|
+
login(callbacks: CancellableOAuthLoginCallbacks): Promise<OAuthCredentials>;
|
|
14
|
+
refreshToken(
|
|
15
|
+
credential: OAuthCredentials,
|
|
16
|
+
signal: AbortSignal,
|
|
17
|
+
): Promise<OAuthCredentials>;
|
|
18
|
+
getApiKey(credential: OAuthCredentials): string | Promise<string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CancellableOAuthLoginCallbacks = Omit<
|
|
22
|
+
OAuthLoginCallbacks,
|
|
23
|
+
"onManualCodeInput"
|
|
24
|
+
> & {
|
|
25
|
+
onManualCodeInput?(signal?: AbortSignal): Promise<string>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function legacyCallbacks(
|
|
29
|
+
interaction: ProviderAuthInteraction,
|
|
30
|
+
): CancellableOAuthLoginCallbacks {
|
|
31
|
+
return {
|
|
32
|
+
signal: interaction.signal,
|
|
33
|
+
onAuth: (info) => interaction.notify({ type: "auth_url", ...info }),
|
|
34
|
+
onDeviceCode: (info) =>
|
|
35
|
+
interaction.notify({ type: "device_code", ...info }),
|
|
36
|
+
onProgress: (message) => interaction.notify({ type: "progress", message }),
|
|
37
|
+
onPrompt: (prompt) =>
|
|
38
|
+
interaction.prompt({
|
|
39
|
+
type: "text",
|
|
40
|
+
message: prompt.message,
|
|
41
|
+
placeholder: prompt.placeholder,
|
|
42
|
+
}),
|
|
43
|
+
onManualCodeInput: (signal) =>
|
|
44
|
+
interaction.prompt({
|
|
45
|
+
type: "manual_code",
|
|
46
|
+
message: "Paste the authorization callback URL or code",
|
|
47
|
+
signal,
|
|
48
|
+
}),
|
|
49
|
+
onSelect: (prompt) =>
|
|
50
|
+
interaction.prompt({
|
|
51
|
+
type: "select",
|
|
52
|
+
message: prompt.message,
|
|
53
|
+
options: prompt.options,
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function canonicalCredential(credentials: OAuthCredentials): OAuthCredential {
|
|
59
|
+
return { ...credentials, type: "oauth" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Adapt pi's retained extension OAuth callbacks to the native Provider API. */
|
|
63
|
+
export function createOAuthAuth(
|
|
64
|
+
implementation: LegacyOAuthImplementation,
|
|
65
|
+
): OAuthAuth {
|
|
66
|
+
return {
|
|
67
|
+
name: implementation.name,
|
|
68
|
+
isSubscription: implementation.isSubscription,
|
|
69
|
+
login: async (interaction) =>
|
|
70
|
+
canonicalCredential(
|
|
71
|
+
await implementation.login(legacyCallbacks(interaction)),
|
|
72
|
+
),
|
|
73
|
+
refresh: async (credential, signal) =>
|
|
74
|
+
canonicalCredential(
|
|
75
|
+
await implementation.refreshToken(credential, signal),
|
|
76
|
+
),
|
|
77
|
+
toAuth: async (credential): Promise<ModelAuth> => ({
|
|
78
|
+
apiKey: await implementation.getApiKey(credential),
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
OPENPI_TOOL_SURFACE,
|
|
33
33
|
patchOwnedTools,
|
|
34
34
|
} from "../shared/tool-surface.ts";
|
|
35
|
+
import { completionOwnerFor } from "../shared/completion-inbox.ts";
|
|
35
36
|
import {
|
|
36
37
|
projectBackgroundTerminalCapability,
|
|
37
38
|
registerWebCapability,
|
|
@@ -105,7 +106,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
105
106
|
let ui: ExtensionUIContext | undefined;
|
|
106
107
|
let unsubStatus: (() => void) | undefined;
|
|
107
108
|
let startReservations = 0;
|
|
108
|
-
const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>(
|
|
109
|
+
const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>({
|
|
110
|
+
owner: () =>
|
|
111
|
+
sessionContext
|
|
112
|
+
? completionOwnerFor(sessionContext.sessionManager)
|
|
113
|
+
: undefined,
|
|
114
|
+
});
|
|
109
115
|
const hideLifecycleTools = () =>
|
|
110
116
|
patchOwnedTools(pi, "background", {
|
|
111
117
|
disable: OPENPI_TOOL_SURFACE.background.deferred,
|
|
@@ -245,6 +251,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
245
251
|
const flushResults = (wake: boolean) => {
|
|
246
252
|
const snaps = resultDelivery.drain(MAX_RUNNING);
|
|
247
253
|
if (!deliverResults(snaps, wake)) resultDelivery.restore(snaps);
|
|
254
|
+
else resultDelivery.acknowledge(snaps);
|
|
248
255
|
};
|
|
249
256
|
|
|
250
257
|
const idleResultBatcher = createIdleResultBatcher({
|
|
@@ -395,11 +395,9 @@ export async function signalWindowsProcessTree(
|
|
|
395
395
|
: attempt.outcome === "timed_out"
|
|
396
396
|
? `taskkill timed out after ${attempt.timeoutMs}ms; helper ${attempt.helperClosed ? "closed after SIGKILL" : `did not close within an additional ${attempt.helperCloseTimeoutMs}ms`}`
|
|
397
397
|
: `taskkill exited ${attempt.exitCode ?? "without a code"}${attempt.signal ? ` (${attempt.signal})` : ""}`;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
!targetExited()
|
|
402
|
-
) {
|
|
398
|
+
// Closing the helper or observing the shell exit does not prove that all
|
|
399
|
+
// descendants exited. Preserve uncertainty instead of killing only the shell.
|
|
400
|
+
if (attempt.outcome === "timed_out") {
|
|
403
401
|
return { outcome: "unresolved", detail };
|
|
404
402
|
}
|
|
405
403
|
// A failed graceful taskkill must leave the shell PID alive for the
|
|
@@ -1,44 +1,64 @@
|
|
|
1
1
|
import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
|
|
2
|
+
import {
|
|
3
|
+
type CompletionOwner,
|
|
4
|
+
createCompletionInbox,
|
|
5
|
+
} from "../../shared/completion-inbox.ts";
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
|
-
* Deferred one-shot delivery
|
|
5
|
-
* terminal's result is held
|
|
6
|
-
* message or consumed by a tool call (bg_kill /
|
|
7
|
-
* returned the settlement itself.
|
|
8
|
-
* structurally impossible — whoever
|
|
8
|
+
* Deferred one-shot delivery adapter (same semantics as subagents'): a
|
|
9
|
+
* settled terminal's result is held in the shared inbox until it is either
|
|
10
|
+
* drained into a follow-up message or consumed by a tool call (bg_kill /
|
|
11
|
+
* bg_status) that already returned the settlement itself. Stable ids make
|
|
12
|
+
* double delivery structurally impossible — whoever claims first wins.
|
|
9
13
|
*/
|
|
10
|
-
export function createDeferredResultDelivery<T extends { id: string }>(
|
|
11
|
-
|
|
14
|
+
export function createDeferredResultDelivery<T extends { id: string }>(
|
|
15
|
+
options: { readonly owner?: () => CompletionOwner | undefined } = {},
|
|
16
|
+
) {
|
|
17
|
+
const inbox = createCompletionInbox<T>();
|
|
18
|
+
const owner = options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
|
|
12
19
|
|
|
13
20
|
const queue = {
|
|
14
21
|
defer(result: T) {
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
const currentOwner = owner();
|
|
23
|
+
inbox.defer(
|
|
24
|
+
{
|
|
25
|
+
deliveryId: `background:${result.id}`,
|
|
26
|
+
owner: currentOwner ?? { sessionId: "unowned", epoch: 0 },
|
|
27
|
+
producer: "background",
|
|
28
|
+
producerId: result.id,
|
|
29
|
+
terminalRef: { kind: "terminal-snapshot", id: result.id },
|
|
30
|
+
wake: "producer-policy",
|
|
31
|
+
payload: result,
|
|
32
|
+
},
|
|
33
|
+
currentOwner,
|
|
34
|
+
);
|
|
35
|
+
return inbox.size();
|
|
17
36
|
},
|
|
18
37
|
consume(ids: Iterable<string>) {
|
|
19
|
-
|
|
38
|
+
inbox.consume("background", ids);
|
|
20
39
|
},
|
|
21
40
|
drain(maxResults = Number.POSITIVE_INFINITY) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
results.push(result);
|
|
26
|
-
pending.delete(id);
|
|
27
|
-
}
|
|
28
|
-
return results;
|
|
41
|
+
return inbox
|
|
42
|
+
.claim(owner(), maxResults)
|
|
43
|
+
.map((envelope) => envelope.payload);
|
|
29
44
|
},
|
|
30
45
|
restore(results: readonly T[]) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
inbox.retryClaimed(
|
|
47
|
+
"background",
|
|
48
|
+
results.map((result) => result.id),
|
|
49
|
+
owner(),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
acknowledge(results: readonly T[]) {
|
|
53
|
+
inbox.acknowledge(results.map((result) => `background:${result.id}`));
|
|
35
54
|
},
|
|
36
55
|
size() {
|
|
37
|
-
return
|
|
56
|
+
return inbox.size();
|
|
38
57
|
},
|
|
39
58
|
clear() {
|
|
40
|
-
|
|
59
|
+
inbox.clear();
|
|
41
60
|
},
|
|
61
|
+
inspectDeadLetters: inbox.inspectDeadLetters,
|
|
42
62
|
};
|
|
43
63
|
return queue satisfies ConsumableResultDeliveryQueue<T>;
|
|
44
64
|
}
|
package/extensions/cron/index.ts
CHANGED
|
@@ -17,6 +17,9 @@ import type {
|
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import {
|
|
19
19
|
advanceDeliveredJobs,
|
|
20
|
+
CRON_DELIVERY_MAX_BYTES,
|
|
21
|
+
CRON_DELIVERY_MAX_JOBS,
|
|
22
|
+
CRON_MAX_JOBS,
|
|
20
23
|
type CronJob,
|
|
21
24
|
dueJobs,
|
|
22
25
|
formatInterval,
|
|
@@ -41,6 +44,48 @@ const SYSTEM_RUNTIME: CronRuntime = {
|
|
|
41
44
|
},
|
|
42
45
|
};
|
|
43
46
|
|
|
47
|
+
function deliveryMessage(due: readonly CronJob[]) {
|
|
48
|
+
const jobs = due.map((job) => ({
|
|
49
|
+
id: job.id,
|
|
50
|
+
prompt: job.prompt,
|
|
51
|
+
recurring: job.intervalMs !== undefined,
|
|
52
|
+
}));
|
|
53
|
+
return due.length === 1
|
|
54
|
+
? {
|
|
55
|
+
customType: "cron-fire",
|
|
56
|
+
content: `[cron ${jobs[0]!.id} · ${jobs[0]!.recurring ? "recurring" : "once"}]\n${jobs[0]!.prompt}`,
|
|
57
|
+
display: true,
|
|
58
|
+
details: jobs[0]!,
|
|
59
|
+
}
|
|
60
|
+
: {
|
|
61
|
+
customType: "cron-fire",
|
|
62
|
+
content: `${due.length} scheduled prompts are due:\n\n${jobs
|
|
63
|
+
.map(
|
|
64
|
+
(job) =>
|
|
65
|
+
`[cron ${job.id} · ${job.recurring ? "recurring" : "once"}]\n${job.prompt}`,
|
|
66
|
+
)
|
|
67
|
+
.join("\n\n")}`,
|
|
68
|
+
display: true,
|
|
69
|
+
details: { count: jobs.length, jobs },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function dueDeliveryBatch(due: readonly CronJob[]) {
|
|
74
|
+
const selected: CronJob[] = [];
|
|
75
|
+
for (const job of due.slice(0, CRON_DELIVERY_MAX_JOBS)) {
|
|
76
|
+
const candidate = [...selected, job];
|
|
77
|
+
const message = deliveryMessage(candidate);
|
|
78
|
+
if (
|
|
79
|
+
new TextEncoder().encode(message.content).byteLength >
|
|
80
|
+
CRON_DELIVERY_MAX_BYTES
|
|
81
|
+
) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
selected.push(job);
|
|
85
|
+
}
|
|
86
|
+
return selected;
|
|
87
|
+
}
|
|
88
|
+
|
|
44
89
|
export default function cron(
|
|
45
90
|
pi: ExtensionAPI,
|
|
46
91
|
runtime: CronRuntime = SYSTEM_RUNTIME,
|
|
@@ -58,30 +103,7 @@ export default function cron(
|
|
|
58
103
|
const fire = (due: readonly CronJob[]) => {
|
|
59
104
|
if (due.length === 0) return true;
|
|
60
105
|
try {
|
|
61
|
-
const
|
|
62
|
-
id: job.id,
|
|
63
|
-
prompt: job.prompt,
|
|
64
|
-
recurring: job.intervalMs !== undefined,
|
|
65
|
-
}));
|
|
66
|
-
const message =
|
|
67
|
-
due.length === 1
|
|
68
|
-
? {
|
|
69
|
-
customType: "cron-fire",
|
|
70
|
-
content: `[cron ${jobs[0]!.id} · ${jobs[0]!.recurring ? "recurring" : "once"}]\n${jobs[0]!.prompt}`,
|
|
71
|
-
display: true,
|
|
72
|
-
details: jobs[0],
|
|
73
|
-
}
|
|
74
|
-
: {
|
|
75
|
-
customType: "cron-fire",
|
|
76
|
-
content: `${due.length} scheduled prompts are due:\n\n${jobs
|
|
77
|
-
.map(
|
|
78
|
-
(job) =>
|
|
79
|
-
`[cron ${job.id} · ${job.recurring ? "recurring" : "once"}]\n${job.prompt}`,
|
|
80
|
-
)
|
|
81
|
-
.join("\n\n")}`,
|
|
82
|
-
display: true,
|
|
83
|
-
details: { count: jobs.length, jobs },
|
|
84
|
-
};
|
|
106
|
+
const message = deliveryMessage(due);
|
|
85
107
|
pi.sendMessage<
|
|
86
108
|
| { id: number; prompt: string; recurring: boolean }
|
|
87
109
|
| {
|
|
@@ -109,9 +131,11 @@ export default function cron(
|
|
|
109
131
|
const now = runtime.now();
|
|
110
132
|
const due = dueJobs(jobs, now);
|
|
111
133
|
if (due.length === 0) return;
|
|
134
|
+
const batch = dueDeliveryBatch(due);
|
|
135
|
+
if (batch.length === 0) return;
|
|
112
136
|
const deliveredIds = new Set<number>();
|
|
113
|
-
if (fire(
|
|
114
|
-
for (const job of
|
|
137
|
+
if (fire(batch)) {
|
|
138
|
+
for (const job of batch) deliveredIds.add(job.id);
|
|
115
139
|
}
|
|
116
140
|
jobs = advanceDeliveredJobs(jobs, deliveredIds, runtime.now());
|
|
117
141
|
if (jobs.length === 0) stopTicker();
|
|
@@ -170,12 +194,29 @@ export default function cron(
|
|
|
170
194
|
return;
|
|
171
195
|
}
|
|
172
196
|
|
|
197
|
+
if (jobs.length >= CRON_MAX_JOBS) {
|
|
198
|
+
ctx.ui.notify(
|
|
199
|
+
`A session can have at most ${CRON_MAX_JOBS} scheduled prompts. Remove one before adding another.`,
|
|
200
|
+
"warning",
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
173
205
|
const intervalMs = parsed.intervalMs!;
|
|
206
|
+
const now = runtime.now();
|
|
207
|
+
const nextRunAt = now + intervalMs;
|
|
208
|
+
if (!Number.isSafeInteger(now) || !Number.isSafeInteger(nextRunAt)) {
|
|
209
|
+
ctx.ui.notify(
|
|
210
|
+
"Scheduled time is too far in the future. Use a shorter duration.",
|
|
211
|
+
"warning",
|
|
212
|
+
);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
174
215
|
const job: CronJob = {
|
|
175
216
|
id: nextId++,
|
|
176
217
|
prompt: parsed.prompt!,
|
|
177
218
|
...(parsed.oneShot ? {} : { intervalMs }),
|
|
178
|
-
nextRunAt
|
|
219
|
+
nextRunAt,
|
|
179
220
|
};
|
|
180
221
|
jobs.push(job);
|
|
181
222
|
startTicker();
|