tg-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +50 -0
- package/README.md +152 -0
- package/dist/auth.js +71 -0
- package/dist/cli.js +2 -0
- package/dist/codexAuth.js +93 -0
- package/dist/config.js +59 -0
- package/dist/customTools.js +386 -0
- package/dist/index.js +954 -0
- package/dist/mcp.js +427 -0
- package/dist/piAgentRunner.js +407 -0
- package/dist/piAiRunner.js +99 -0
- package/dist/proxy.js +19 -0
- package/dist/sessionStore.js +138 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +91 -0
- package/package.json +41 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { createAgentSession, discoverAuthStorage, discoverModels, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { config } from "./config.js";
|
|
3
|
+
import { readCodexOAuth } from "./codexAuth.js";
|
|
4
|
+
import { createCustomTools } from "./customTools.js";
|
|
5
|
+
import { ensureDir } from "./utils.js";
|
|
6
|
+
import { sessionFilePath } from "./sessionStore.js";
|
|
7
|
+
const CODEX_MODEL_ALIASES = {
|
|
8
|
+
"codex-mini-latest": "gpt-5.1-codex-mini",
|
|
9
|
+
"codex-max-latest": "gpt-5.1-codex-max",
|
|
10
|
+
"codex-latest": "gpt-5.2-codex",
|
|
11
|
+
};
|
|
12
|
+
function normalizeProviderId(provider) {
|
|
13
|
+
const trimmed = provider.trim().toLowerCase();
|
|
14
|
+
if (trimmed === "codex")
|
|
15
|
+
return "openai-codex";
|
|
16
|
+
return trimmed;
|
|
17
|
+
}
|
|
18
|
+
function splitModelRef(ref, fallbackProvider) {
|
|
19
|
+
const trimmed = ref.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
return { provider: fallbackProvider, modelId: "" };
|
|
22
|
+
}
|
|
23
|
+
if (trimmed.includes("/")) {
|
|
24
|
+
const [provider, modelId] = trimmed.split("/", 2);
|
|
25
|
+
return { provider: provider.trim() || fallbackProvider, modelId: modelId.trim() };
|
|
26
|
+
}
|
|
27
|
+
return { provider: fallbackProvider, modelId: trimmed };
|
|
28
|
+
}
|
|
29
|
+
function normalizeModelId(provider, modelId) {
|
|
30
|
+
if (provider !== "openai-codex") {
|
|
31
|
+
return modelId;
|
|
32
|
+
}
|
|
33
|
+
const mapped = CODEX_MODEL_ALIASES[modelId];
|
|
34
|
+
return mapped ?? modelId;
|
|
35
|
+
}
|
|
36
|
+
function pickDefaultModel(modelRegistry, provider) {
|
|
37
|
+
const available = modelRegistry.getAvailable().filter((model) => model.provider === provider);
|
|
38
|
+
if (available.length > 0) {
|
|
39
|
+
return available[0];
|
|
40
|
+
}
|
|
41
|
+
const allModels = modelRegistry.getAll().filter((model) => model.provider === provider);
|
|
42
|
+
return allModels[0];
|
|
43
|
+
}
|
|
44
|
+
function resolveModel(modelRegistry, overrides) {
|
|
45
|
+
const hasOverrideProvider = Boolean(overrides?.provider?.trim());
|
|
46
|
+
const hasOverrideModel = Boolean(overrides?.modelId?.trim());
|
|
47
|
+
const providerRaw = normalizeProviderId(overrides?.provider || config.modelProvider || "openai-codex");
|
|
48
|
+
const modelRef = hasOverrideProvider || hasOverrideModel ? "" : config.modelRef || "";
|
|
49
|
+
const { provider: refProvider, modelId: refModelId } = splitModelRef(modelRef, providerRaw);
|
|
50
|
+
const provider = overrides?.provider ? providerRaw : normalizeProviderId(refProvider);
|
|
51
|
+
let modelId = hasOverrideModel ? overrides?.modelId?.trim() || "" : refModelId;
|
|
52
|
+
if (!modelId) {
|
|
53
|
+
const pick = pickDefaultModel(modelRegistry, provider);
|
|
54
|
+
if (pick) {
|
|
55
|
+
return { model: pick, provider, modelId: pick.id };
|
|
56
|
+
}
|
|
57
|
+
return { model: undefined, provider, modelId: "" };
|
|
58
|
+
}
|
|
59
|
+
const normalizedModelId = normalizeModelId(provider, modelId);
|
|
60
|
+
const model = modelRegistry.find(provider, normalizedModelId);
|
|
61
|
+
return { model, provider, modelId: normalizedModelId };
|
|
62
|
+
}
|
|
63
|
+
function applyCodexOAuth(authStorage) {
|
|
64
|
+
const codex = readCodexOAuth();
|
|
65
|
+
if (!codex) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
authStorage.setRuntimeApiKey("openai-codex", codex.accessToken);
|
|
69
|
+
return { source: codex.source, expiresAt: codex.expiresAt };
|
|
70
|
+
}
|
|
71
|
+
function extractTextFromMessage(message) {
|
|
72
|
+
const role = message?.role;
|
|
73
|
+
if (role !== "assistant")
|
|
74
|
+
return "";
|
|
75
|
+
const content = message.content;
|
|
76
|
+
if (typeof content === "string") {
|
|
77
|
+
return content.trim();
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(content)) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
const textBlocks = content
|
|
83
|
+
.filter((block) => typeof block === "object" && block && block.type === "text")
|
|
84
|
+
.map((block) => block.text ?? "")
|
|
85
|
+
.map((text) => text.trim())
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
if (textBlocks.length > 0) {
|
|
88
|
+
return textBlocks.join("\n\n");
|
|
89
|
+
}
|
|
90
|
+
const thinkingBlocks = content
|
|
91
|
+
.filter((block) => typeof block === "object" && block && block.type === "thinking")
|
|
92
|
+
.map((block) => block.thinking ?? "")
|
|
93
|
+
.map((text) => text.trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
if (thinkingBlocks.length > 0) {
|
|
96
|
+
return thinkingBlocks.join("\n\n");
|
|
97
|
+
}
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
function summarizeMessages(messages) {
|
|
101
|
+
if (messages.length === 0) {
|
|
102
|
+
return "messages=0";
|
|
103
|
+
}
|
|
104
|
+
const tail = messages.slice(-6).map((message) => {
|
|
105
|
+
const role = message?.role ?? "unknown";
|
|
106
|
+
const content = message.content;
|
|
107
|
+
const stopReason = message?.stopReason;
|
|
108
|
+
const errorMessage = message?.errorMessage;
|
|
109
|
+
if (typeof content === "string") {
|
|
110
|
+
return `${role}(text:${content.length}${stopReason ? `,stop=${stopReason}` : ""})`;
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(content)) {
|
|
113
|
+
const types = content
|
|
114
|
+
.map((block) => {
|
|
115
|
+
if (typeof block === "object" && block && "type" in block) {
|
|
116
|
+
return String(block.type || "unknown");
|
|
117
|
+
}
|
|
118
|
+
return typeof block;
|
|
119
|
+
})
|
|
120
|
+
.join(",");
|
|
121
|
+
const meta = [
|
|
122
|
+
`blocks:${types || "none"}`,
|
|
123
|
+
stopReason ? `stop=${stopReason}` : null,
|
|
124
|
+
errorMessage ? "error=1" : null,
|
|
125
|
+
]
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join(",");
|
|
128
|
+
return `${role}(${meta})`;
|
|
129
|
+
}
|
|
130
|
+
if (content == null) {
|
|
131
|
+
const meta = [
|
|
132
|
+
"empty",
|
|
133
|
+
stopReason ? `stop=${stopReason}` : null,
|
|
134
|
+
errorMessage ? "error=1" : null,
|
|
135
|
+
]
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.join(",");
|
|
138
|
+
return `${role}(${meta})`;
|
|
139
|
+
}
|
|
140
|
+
return `${role}(${typeof content}${stopReason ? `,stop=${stopReason}` : ""})`;
|
|
141
|
+
});
|
|
142
|
+
return `messages=${messages.length} last=[${tail.join(" ")}]`;
|
|
143
|
+
}
|
|
144
|
+
function extractAssistantText(messages, session) {
|
|
145
|
+
const lastText = session?.getLastAssistantText?.();
|
|
146
|
+
if (lastText?.trim()) {
|
|
147
|
+
return lastText.trim();
|
|
148
|
+
}
|
|
149
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
150
|
+
const text = extractTextFromMessage(messages[i]);
|
|
151
|
+
if (text) {
|
|
152
|
+
return text;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
function findLastAssistantMessage(messages) {
|
|
158
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
159
|
+
const role = messages[i]?.role;
|
|
160
|
+
if (role === "assistant") {
|
|
161
|
+
return messages[i];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
export async function runPiAgentPrompt(params) {
|
|
167
|
+
await ensureDir(config.sessionDir);
|
|
168
|
+
await ensureDir(config.agentDir);
|
|
169
|
+
await ensureDir(config.workspaceDir);
|
|
170
|
+
const sessionFile = sessionFilePath(params.userId, params.sessionId);
|
|
171
|
+
const authStorage = discoverAuthStorage(config.agentDir);
|
|
172
|
+
const codexInfo = applyCodexOAuth(authStorage);
|
|
173
|
+
if (codexInfo) {
|
|
174
|
+
console.log(`[tg-agent] codex oauth source=${codexInfo.source} expiresAt=${new Date(codexInfo.expiresAt).toISOString()}`);
|
|
175
|
+
}
|
|
176
|
+
const modelRegistry = discoverModels(authStorage, config.agentDir);
|
|
177
|
+
const { model, provider, modelId } = resolveModel(modelRegistry, {
|
|
178
|
+
provider: params.modelProvider,
|
|
179
|
+
modelId: params.modelId,
|
|
180
|
+
});
|
|
181
|
+
if (modelId && !model) {
|
|
182
|
+
console.warn(`[tg-agent] model not found: ${provider}/${modelId}`);
|
|
183
|
+
}
|
|
184
|
+
const sessionManager = SessionManager.open(sessionFile);
|
|
185
|
+
const settingsManager = SettingsManager.create(config.workspaceDir, config.agentDir);
|
|
186
|
+
const { session, modelFallbackMessage } = await createAgentSession({
|
|
187
|
+
cwd: config.workspaceDir,
|
|
188
|
+
agentDir: config.agentDir,
|
|
189
|
+
authStorage,
|
|
190
|
+
modelRegistry,
|
|
191
|
+
model: model ?? undefined,
|
|
192
|
+
sessionManager,
|
|
193
|
+
settingsManager,
|
|
194
|
+
customTools: createCustomTools(),
|
|
195
|
+
systemPrompt: (defaultPrompt) => {
|
|
196
|
+
const extra = params.systemPrompt?.trim();
|
|
197
|
+
if (!extra)
|
|
198
|
+
return defaultPrompt;
|
|
199
|
+
return `${defaultPrompt}\n\n${extra}`;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
if (modelFallbackMessage) {
|
|
203
|
+
console.warn(`[tg-agent] modelFallback=${modelFallbackMessage}`);
|
|
204
|
+
}
|
|
205
|
+
let userAbortRequested = false;
|
|
206
|
+
const abortFn = () => {
|
|
207
|
+
userAbortRequested = true;
|
|
208
|
+
void session.abort();
|
|
209
|
+
};
|
|
210
|
+
params.onAbortReady?.(abortFn);
|
|
211
|
+
const logEvents = process.env.LOG_AGENT_EVENTS !== "0";
|
|
212
|
+
const logStream = process.env.LOG_AGENT_STREAM === "1";
|
|
213
|
+
const toolStartTimes = new Map();
|
|
214
|
+
const unsubscribe = session.subscribe((event) => {
|
|
215
|
+
switch (event.type) {
|
|
216
|
+
case "agent_start":
|
|
217
|
+
if (logEvents) {
|
|
218
|
+
console.log(`[tg-agent] agent start session=${params.sessionId}`);
|
|
219
|
+
}
|
|
220
|
+
lastProgressAt = Date.now();
|
|
221
|
+
params.onStatus?.({ type: "agent_start" });
|
|
222
|
+
break;
|
|
223
|
+
case "agent_end":
|
|
224
|
+
if (logEvents) {
|
|
225
|
+
console.log(`[tg-agent] agent end session=${params.sessionId}`);
|
|
226
|
+
}
|
|
227
|
+
params.onStatus?.({ type: "agent_end" });
|
|
228
|
+
break;
|
|
229
|
+
case "turn_start":
|
|
230
|
+
if (logEvents) {
|
|
231
|
+
console.log(`[tg-agent] turn start session=${params.sessionId}`);
|
|
232
|
+
}
|
|
233
|
+
lastProgressAt = Date.now();
|
|
234
|
+
params.onStatus?.({ type: "turn_start" });
|
|
235
|
+
break;
|
|
236
|
+
case "turn_end":
|
|
237
|
+
if (logEvents) {
|
|
238
|
+
console.log(`[tg-agent] turn end session=${params.sessionId} toolResults=${event.toolResults.length}`);
|
|
239
|
+
}
|
|
240
|
+
lastProgressAt = Date.now();
|
|
241
|
+
params.onStatus?.({ type: "turn_end", toolResults: event.toolResults.length });
|
|
242
|
+
break;
|
|
243
|
+
case "message_start": {
|
|
244
|
+
if (logEvents) {
|
|
245
|
+
const role = event.message.role ?? "unknown";
|
|
246
|
+
console.log(`[tg-agent] message start session=${params.sessionId} role=${role}`);
|
|
247
|
+
}
|
|
248
|
+
lastProgressAt = Date.now();
|
|
249
|
+
{
|
|
250
|
+
const role = event.message.role ?? "unknown";
|
|
251
|
+
params.onStatus?.({ type: "message_start", role });
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case "message_end": {
|
|
256
|
+
if (logEvents) {
|
|
257
|
+
const role = event.message.role ?? "unknown";
|
|
258
|
+
console.log(`[tg-agent] message end session=${params.sessionId} role=${role}`);
|
|
259
|
+
}
|
|
260
|
+
lastProgressAt = Date.now();
|
|
261
|
+
{
|
|
262
|
+
const role = event.message.role ?? "unknown";
|
|
263
|
+
params.onStatus?.({ type: "message_end", role });
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case "message_update": {
|
|
268
|
+
if (!logStream)
|
|
269
|
+
break;
|
|
270
|
+
if (event.assistantMessageEvent.type === "text_delta") {
|
|
271
|
+
const delta = event.assistantMessageEvent.delta ?? "";
|
|
272
|
+
const len = delta.length;
|
|
273
|
+
if (len > 0) {
|
|
274
|
+
console.log(`[tg-agent] stream delta session=${params.sessionId} chars=${len}`);
|
|
275
|
+
}
|
|
276
|
+
lastProgressAt = Date.now();
|
|
277
|
+
}
|
|
278
|
+
if (event.assistantMessageEvent.type === "thinking_delta") {
|
|
279
|
+
lastProgressAt = Date.now();
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "tool_execution_start": {
|
|
284
|
+
toolStartTimes.set(event.toolCallId, Date.now());
|
|
285
|
+
console.log(`[tg-agent] tool start name=${event.toolName} id=${event.toolCallId}`);
|
|
286
|
+
activeToolCount += 1;
|
|
287
|
+
lastProgressAt = Date.now();
|
|
288
|
+
params.onStatus?.({
|
|
289
|
+
type: "tool_start",
|
|
290
|
+
name: event.toolName,
|
|
291
|
+
id: event.toolCallId,
|
|
292
|
+
args: event.args,
|
|
293
|
+
});
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case "tool_execution_end": {
|
|
297
|
+
const startedAt = toolStartTimes.get(event.toolCallId);
|
|
298
|
+
const duration = startedAt ? Date.now() - startedAt : 0;
|
|
299
|
+
console.log(`[tg-agent] tool end name=${event.toolName} id=${event.toolCallId} ok=${!event.isError} durationMs=${duration}`);
|
|
300
|
+
toolStartTimes.delete(event.toolCallId);
|
|
301
|
+
activeToolCount = Math.max(0, activeToolCount - 1);
|
|
302
|
+
lastProgressAt = Date.now();
|
|
303
|
+
params.onStatus?.({
|
|
304
|
+
type: "tool_end",
|
|
305
|
+
name: event.toolName,
|
|
306
|
+
id: event.toolCallId,
|
|
307
|
+
ok: !event.isError,
|
|
308
|
+
durationMs: duration,
|
|
309
|
+
});
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
default:
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
let heartbeat = null;
|
|
317
|
+
let timeoutInterval = null;
|
|
318
|
+
let timedOut = false;
|
|
319
|
+
let timedOutAfterMs = 0;
|
|
320
|
+
let timedOutStreaming = false;
|
|
321
|
+
let lastProgressAt = Date.now();
|
|
322
|
+
let activeToolCount = 0;
|
|
323
|
+
try {
|
|
324
|
+
const promptStartedAt = Date.now();
|
|
325
|
+
heartbeat = setInterval(() => {
|
|
326
|
+
const elapsed = Date.now() - promptStartedAt;
|
|
327
|
+
console.log(`[tg-agent] prompt running session=${params.sessionId} elapsedMs=${elapsed} streaming=${session.isStreaming}`);
|
|
328
|
+
params.onStatus?.({
|
|
329
|
+
type: "heartbeat",
|
|
330
|
+
elapsedMs: elapsed,
|
|
331
|
+
streaming: session.isStreaming,
|
|
332
|
+
});
|
|
333
|
+
}, 15000);
|
|
334
|
+
timeoutInterval = setInterval(() => {
|
|
335
|
+
if (timedOut)
|
|
336
|
+
return;
|
|
337
|
+
if (activeToolCount > 0)
|
|
338
|
+
return;
|
|
339
|
+
const elapsed = Date.now() - lastProgressAt;
|
|
340
|
+
const baseTimeout = session.isStreaming
|
|
341
|
+
? config.modelTimeoutStreamingMs
|
|
342
|
+
: config.modelTimeoutMs;
|
|
343
|
+
const timeoutMs = Math.max(1_000, baseTimeout);
|
|
344
|
+
if (elapsed >= timeoutMs) {
|
|
345
|
+
timedOut = true;
|
|
346
|
+
timedOutAfterMs = timeoutMs;
|
|
347
|
+
timedOutStreaming = session.isStreaming;
|
|
348
|
+
console.warn(`[tg-agent] model timeout session=${params.sessionId} elapsedMs=${elapsed}`);
|
|
349
|
+
void session.abort();
|
|
350
|
+
}
|
|
351
|
+
}, 2000);
|
|
352
|
+
try {
|
|
353
|
+
await session.prompt(params.prompt);
|
|
354
|
+
if (timedOut) {
|
|
355
|
+
throw new Error(`Model request timed out after ${config.modelTimeoutMs}ms`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (userAbortRequested) {
|
|
360
|
+
throw new Error("Cancelled by user.");
|
|
361
|
+
}
|
|
362
|
+
if (timedOut) {
|
|
363
|
+
const suffix = timedOutStreaming ? " (streaming)" : "";
|
|
364
|
+
const ms = timedOutAfterMs || config.modelTimeoutMs;
|
|
365
|
+
throw new Error(`Model request timed out after ${ms}ms${suffix}`);
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
if (heartbeat)
|
|
370
|
+
clearInterval(heartbeat);
|
|
371
|
+
const lastAssistant = findLastAssistantMessage(session.messages);
|
|
372
|
+
if (lastAssistant) {
|
|
373
|
+
const stopReason = lastAssistant
|
|
374
|
+
?.stopReason;
|
|
375
|
+
const errorMessage = lastAssistant
|
|
376
|
+
?.errorMessage;
|
|
377
|
+
if (stopReason === "error") {
|
|
378
|
+
console.warn(`[tg-agent] model error session=${params.sessionId} error=${errorMessage ?? "unknown"}`);
|
|
379
|
+
throw new Error(errorMessage ?? "Model error without details.");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const text = extractAssistantText(session.messages, session);
|
|
383
|
+
if (!text) {
|
|
384
|
+
const summary = summarizeMessages(session.messages);
|
|
385
|
+
console.warn(`[tg-agent] empty response session=${params.sessionId} ${summary}`);
|
|
386
|
+
throw new Error("No assistant response.");
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
text,
|
|
390
|
+
sessionFile,
|
|
391
|
+
sessionId: session.sessionId,
|
|
392
|
+
modelProvider: session.model?.provider ?? provider,
|
|
393
|
+
modelId: session.model?.id ?? modelId,
|
|
394
|
+
modelFallbackMessage,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
if (heartbeat)
|
|
399
|
+
clearInterval(heartbeat);
|
|
400
|
+
if (timeoutInterval)
|
|
401
|
+
clearInterval(timeoutInterval);
|
|
402
|
+
unsubscribe();
|
|
403
|
+
const manager = sessionManager;
|
|
404
|
+
manager.flushPendingToolResults?.();
|
|
405
|
+
session.dispose();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { completeSimple, getModel, } from "@mariozechner/pi-ai";
|
|
2
|
+
import { config } from "./config.js";
|
|
3
|
+
import { resolveApiKeyForProvider } from "./auth.js";
|
|
4
|
+
const CODEX_MODEL_ALIASES = {
|
|
5
|
+
"codex-mini-latest": "gpt-5.1-codex-mini",
|
|
6
|
+
"codex-max-latest": "gpt-5.1-codex-max",
|
|
7
|
+
"codex-latest": "gpt-5.2-codex",
|
|
8
|
+
};
|
|
9
|
+
function splitModelRef(ref, fallbackProvider) {
|
|
10
|
+
const trimmed = ref.trim();
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
return { provider: fallbackProvider, modelId: "" };
|
|
13
|
+
}
|
|
14
|
+
if (trimmed.includes("/")) {
|
|
15
|
+
const [provider, modelId] = trimmed.split("/", 2);
|
|
16
|
+
return { provider: provider.trim() || fallbackProvider, modelId: modelId.trim() };
|
|
17
|
+
}
|
|
18
|
+
return { provider: fallbackProvider, modelId: trimmed };
|
|
19
|
+
}
|
|
20
|
+
function normalizeModelId(provider, modelId) {
|
|
21
|
+
if (provider !== "openai-codex") {
|
|
22
|
+
return modelId;
|
|
23
|
+
}
|
|
24
|
+
const mapped = CODEX_MODEL_ALIASES[modelId];
|
|
25
|
+
return mapped ?? modelId;
|
|
26
|
+
}
|
|
27
|
+
function toAssistantMessage(text, model, timestamp) {
|
|
28
|
+
return {
|
|
29
|
+
role: "assistant",
|
|
30
|
+
content: [{ type: "text", text }],
|
|
31
|
+
api: model.api,
|
|
32
|
+
provider: model.provider,
|
|
33
|
+
model: model.id,
|
|
34
|
+
usage: {
|
|
35
|
+
input: 0,
|
|
36
|
+
output: 0,
|
|
37
|
+
cacheRead: 0,
|
|
38
|
+
cacheWrite: 0,
|
|
39
|
+
totalTokens: 0,
|
|
40
|
+
cost: {
|
|
41
|
+
input: 0,
|
|
42
|
+
output: 0,
|
|
43
|
+
cacheRead: 0,
|
|
44
|
+
cacheWrite: 0,
|
|
45
|
+
total: 0,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
stopReason: "stop",
|
|
49
|
+
timestamp,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function buildContextMessages(localMessages, model) {
|
|
53
|
+
return localMessages.map((message) => {
|
|
54
|
+
if (message.role === "user") {
|
|
55
|
+
return {
|
|
56
|
+
role: "user",
|
|
57
|
+
content: message.content,
|
|
58
|
+
timestamp: message.ts,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return toAssistantMessage(message.content, model, message.ts);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function extractText(assistant) {
|
|
65
|
+
const textBlocks = assistant.content.filter((block) => block.type === "text");
|
|
66
|
+
const text = textBlocks.map((block) => block.text.trim()).filter(Boolean).join("\n\n");
|
|
67
|
+
if (text) {
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
const thinkingBlocks = assistant.content.filter((block) => block.type === "thinking");
|
|
71
|
+
const thinking = thinkingBlocks
|
|
72
|
+
.map((block) => (block.type === "thinking" ? block.thinking.trim() : ""))
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join("\n\n");
|
|
75
|
+
return thinking || "Empty response.";
|
|
76
|
+
}
|
|
77
|
+
export async function runPiCompletion(localMessages, systemPrompt, sessionId) {
|
|
78
|
+
const providerRaw = config.modelProvider || "openai-codex";
|
|
79
|
+
const modelRef = config.modelRef || "gpt-5.2";
|
|
80
|
+
const { provider, modelId: rawModelId } = splitModelRef(modelRef, providerRaw);
|
|
81
|
+
const modelId = normalizeModelId(provider, rawModelId || "gpt-5.2");
|
|
82
|
+
const model = getModel(provider, modelId);
|
|
83
|
+
const { apiKey } = resolveApiKeyForProvider(provider);
|
|
84
|
+
const context = {
|
|
85
|
+
systemPrompt,
|
|
86
|
+
messages: buildContextMessages(localMessages, model),
|
|
87
|
+
};
|
|
88
|
+
const assistant = await completeSimple(model, context, {
|
|
89
|
+
apiKey,
|
|
90
|
+
sessionId,
|
|
91
|
+
maxTokens: config.maxOutputTokens || undefined,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
text: extractText(assistant),
|
|
95
|
+
assistant,
|
|
96
|
+
provider,
|
|
97
|
+
modelId: model.id,
|
|
98
|
+
};
|
|
99
|
+
}
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ProxyAgent, setGlobalDispatcher } from "undici";
|
|
2
|
+
import { resolveFetchProxyInfo } from "./auth.js";
|
|
3
|
+
let applied = false;
|
|
4
|
+
let appliedInfo = null;
|
|
5
|
+
export function applyFetchProxy() {
|
|
6
|
+
if (applied) {
|
|
7
|
+
return appliedInfo;
|
|
8
|
+
}
|
|
9
|
+
applied = true;
|
|
10
|
+
const proxy = resolveFetchProxyInfo();
|
|
11
|
+
if (!proxy) {
|
|
12
|
+
appliedInfo = null;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const dispatcher = new ProxyAgent(proxy.url);
|
|
16
|
+
setGlobalDispatcher(dispatcher);
|
|
17
|
+
appliedInfo = proxy;
|
|
18
|
+
return proxy;
|
|
19
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { config } from "./config.js";
|
|
4
|
+
import { atomicWriteJson, ensureDir, nowMs, shortId } from "./utils.js";
|
|
5
|
+
const cache = new Map();
|
|
6
|
+
function userFilePath(userId) {
|
|
7
|
+
return path.join(config.sessionDir, `${userId}.json`);
|
|
8
|
+
}
|
|
9
|
+
function sanitizeSegment(value) {
|
|
10
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
11
|
+
}
|
|
12
|
+
export function sessionFilePath(userId, sessionId) {
|
|
13
|
+
const safeUser = sanitizeSegment(userId);
|
|
14
|
+
const safeSession = sanitizeSegment(sessionId);
|
|
15
|
+
return path.join(config.sessionDir, `${safeUser}-${safeSession}.jsonl`);
|
|
16
|
+
}
|
|
17
|
+
export async function deleteSessionFile(userId, sessionId) {
|
|
18
|
+
const filePath = sessionFilePath(userId, sessionId);
|
|
19
|
+
try {
|
|
20
|
+
await fs.unlink(filePath);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === "ENOENT") {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function normalizeState(userId, raw) {
|
|
30
|
+
const sessions = raw?.sessions ?? {};
|
|
31
|
+
const activeSessionId = raw?.activeSessionId ?? null;
|
|
32
|
+
const normalized = {
|
|
33
|
+
userId,
|
|
34
|
+
activeSessionId,
|
|
35
|
+
sessions,
|
|
36
|
+
};
|
|
37
|
+
if (!activeSessionId || !sessions[activeSessionId]) {
|
|
38
|
+
normalized.activeSessionId = null;
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
export async function loadUserState(userId) {
|
|
43
|
+
await ensureDir(config.sessionDir);
|
|
44
|
+
const cached = cache.get(userId);
|
|
45
|
+
if (cached) {
|
|
46
|
+
return cached;
|
|
47
|
+
}
|
|
48
|
+
const filePath = userFilePath(userId);
|
|
49
|
+
try {
|
|
50
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
const normalized = normalizeState(userId, parsed);
|
|
53
|
+
cache.set(userId, normalized);
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code === "ENOENT") {
|
|
58
|
+
const state = normalizeState(userId, {});
|
|
59
|
+
cache.set(userId, state);
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function saveUserState(state) {
|
|
66
|
+
await ensureDir(config.sessionDir);
|
|
67
|
+
const filePath = userFilePath(state.userId);
|
|
68
|
+
await atomicWriteJson(filePath, state);
|
|
69
|
+
cache.set(state.userId, state);
|
|
70
|
+
}
|
|
71
|
+
export function listSessions(state) {
|
|
72
|
+
return Object.values(state.sessions).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
73
|
+
}
|
|
74
|
+
export function pruneExpiredSessions(state, now = nowMs()) {
|
|
75
|
+
const removed = [];
|
|
76
|
+
for (const [id, session] of Object.entries(state.sessions)) {
|
|
77
|
+
if (now - session.updatedAt > config.sessionTtlMs) {
|
|
78
|
+
delete state.sessions[id];
|
|
79
|
+
removed.push(id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (state.activeSessionId && !state.sessions[state.activeSessionId]) {
|
|
83
|
+
state.activeSessionId = null;
|
|
84
|
+
}
|
|
85
|
+
return removed;
|
|
86
|
+
}
|
|
87
|
+
export function createSession(state, title) {
|
|
88
|
+
const count = Object.keys(state.sessions).length;
|
|
89
|
+
if (count >= config.maxSessions) {
|
|
90
|
+
throw new Error("Max sessions reached");
|
|
91
|
+
}
|
|
92
|
+
const id = shortId();
|
|
93
|
+
const now = nowMs();
|
|
94
|
+
const session = {
|
|
95
|
+
id,
|
|
96
|
+
title: title && title.length > 0 ? title : `session-${id}`,
|
|
97
|
+
messages: [],
|
|
98
|
+
createdAt: now,
|
|
99
|
+
updatedAt: now,
|
|
100
|
+
};
|
|
101
|
+
state.sessions[id] = session;
|
|
102
|
+
state.activeSessionId = id;
|
|
103
|
+
return session;
|
|
104
|
+
}
|
|
105
|
+
export function setActiveSession(state, sessionId) {
|
|
106
|
+
if (!state.sessions[sessionId]) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
state.activeSessionId = sessionId;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
export function getActiveSession(state) {
|
|
113
|
+
if (!state.activeSessionId) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return state.sessions[state.activeSessionId] ?? null;
|
|
117
|
+
}
|
|
118
|
+
export function closeSession(state, sessionId) {
|
|
119
|
+
if (!state.sessions[sessionId]) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
delete state.sessions[sessionId];
|
|
123
|
+
if (state.activeSessionId === sessionId) {
|
|
124
|
+
state.activeSessionId = null;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
export function resetSession(session) {
|
|
129
|
+
session.messages = [];
|
|
130
|
+
session.updatedAt = nowMs();
|
|
131
|
+
}
|
|
132
|
+
export function appendMessage(session, message, maxHistoryMessages) {
|
|
133
|
+
session.messages.push(message);
|
|
134
|
+
if (session.messages.length > maxHistoryMessages) {
|
|
135
|
+
session.messages = session.messages.slice(-maxHistoryMessages);
|
|
136
|
+
}
|
|
137
|
+
session.updatedAt = nowMs();
|
|
138
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|