@friendlyrobot/discord-pi-agent 0.16.1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-model-service.d.ts +20 -0
- package/dist/agent-resource-service.d.ts +8 -0
- package/dist/agent-service.d.ts +5 -18
- package/dist/index.js +288 -281
- package/dist/session-registry.d.ts +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentSession, ModelRegistry as ModelRegistryType } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { ResolvedDiscordGatewayConfig, ThinkingLevel } from "./types";
|
|
4
|
+
export declare class AgentModelService {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly modelRegistry;
|
|
7
|
+
constructor(config: ResolvedDiscordGatewayConfig, modelRegistry: ModelRegistryType);
|
|
8
|
+
findModel(provider: string, modelId: string): Model<any> | undefined;
|
|
9
|
+
ensureSessionHasConfiguredModel(session: AgentSession): Promise<void>;
|
|
10
|
+
listModels(session?: AgentSession | null): Promise<string>;
|
|
11
|
+
switchModel(provider: string, modelId: string, session: AgentSession): Promise<string>;
|
|
12
|
+
getCurrentModelDisplay(session?: AgentSession | null): string;
|
|
13
|
+
getThinkingLevel(session: AgentSession): {
|
|
14
|
+
current: ThinkingLevel;
|
|
15
|
+
available: ThinkingLevel[];
|
|
16
|
+
supported: boolean;
|
|
17
|
+
};
|
|
18
|
+
setThinkingLevel(session: AgentSession, level: ThinkingLevel): string;
|
|
19
|
+
private applyConfiguredThinkingLevelForSession;
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export declare class AgentResourceService {
|
|
3
|
+
private readonly resourceLoader;
|
|
4
|
+
constructor(resourceLoader: DefaultResourceLoader);
|
|
5
|
+
getSkillsSummary(): string;
|
|
6
|
+
getExtensionsSummary(): string;
|
|
7
|
+
reloadResources(): Promise<string>;
|
|
8
|
+
}
|
package/dist/agent-service.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { AgentModelService } from "./agent-model-service";
|
|
3
|
+
import { AgentResourceService } from "./agent-resource-service";
|
|
4
|
+
import type { AgentStatus, ResolvedDiscordGatewayConfig } from "./types";
|
|
4
5
|
export declare class AgentService {
|
|
5
6
|
private readonly config;
|
|
6
7
|
private readonly authStorage;
|
|
@@ -8,6 +9,8 @@ export declare class AgentService {
|
|
|
8
9
|
private readonly settingsManager;
|
|
9
10
|
private readonly resourceLoader;
|
|
10
11
|
private session;
|
|
12
|
+
readonly models: AgentModelService;
|
|
13
|
+
readonly resources: AgentResourceService;
|
|
11
14
|
constructor(config: ResolvedDiscordGatewayConfig);
|
|
12
15
|
initialize(): Promise<void>;
|
|
13
16
|
getSession(): AgentSession | null;
|
|
@@ -18,30 +21,14 @@ export declare class AgentService {
|
|
|
18
21
|
* setModel() before prompting and dispose() when done.
|
|
19
22
|
*/
|
|
20
23
|
createTemporarySession(): Promise<AgentSession>;
|
|
21
|
-
/** Find a model by provider and ID. Returns undefined if not found. */
|
|
22
|
-
findModel(provider: string, modelId: string): Model<any> | undefined;
|
|
23
24
|
createSession(sessionDir: string): Promise<AgentSession>;
|
|
24
25
|
prompt(text: string): Promise<string>;
|
|
25
|
-
getSkillsSummary(): string;
|
|
26
|
-
reloadResources(): Promise<string>;
|
|
27
|
-
getExtensionsSummary(): string;
|
|
28
26
|
compact(): Promise<string>;
|
|
29
27
|
resetSession(): Promise<string>;
|
|
30
28
|
getStatus(): AgentStatus;
|
|
31
29
|
shutdown(): Promise<void>;
|
|
32
30
|
private createOrResumeSession;
|
|
33
31
|
private ensureConfiguredModel;
|
|
34
|
-
private ensureModelForSession;
|
|
35
32
|
private requireSession;
|
|
36
|
-
private applyConfiguredThinkingLevelForSession;
|
|
37
|
-
listModels(session?: AgentSession | null): Promise<string>;
|
|
38
|
-
switchModel(provider: string, modelId: string, session?: AgentSession | null): Promise<string>;
|
|
39
|
-
getCurrentModelDisplay(session?: AgentSession | null): string;
|
|
40
|
-
getThinkingLevel(): {
|
|
41
|
-
current: ThinkingLevel;
|
|
42
|
-
available: ThinkingLevel[];
|
|
43
|
-
supported: boolean;
|
|
44
|
-
};
|
|
45
|
-
setThinkingLevel(level: ThinkingLevel): string;
|
|
46
33
|
private getSessionDir;
|
|
47
34
|
}
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,200 @@ function createModuleLogger(moduleName) {
|
|
|
39
39
|
return logger.child({ module: moduleName });
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// src/agent-model-service.ts
|
|
43
|
+
var logger2 = createModuleLogger("agent-model-service");
|
|
44
|
+
|
|
45
|
+
class AgentModelService {
|
|
46
|
+
config;
|
|
47
|
+
modelRegistry;
|
|
48
|
+
constructor(config, modelRegistry) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
this.modelRegistry = modelRegistry;
|
|
51
|
+
}
|
|
52
|
+
findModel(provider, modelId) {
|
|
53
|
+
return this.modelRegistry.find(provider, modelId);
|
|
54
|
+
}
|
|
55
|
+
async ensureSessionHasConfiguredModel(session) {
|
|
56
|
+
if (session.model) {
|
|
57
|
+
logger2.debug({
|
|
58
|
+
model: `${session.model.provider}/${session.model.id}`
|
|
59
|
+
}, "retaining existing session model");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const desiredModel = this.modelRegistry.find(this.config.modelProvider, this.config.modelId);
|
|
63
|
+
const availableModels = await this.modelRegistry.getAvailable();
|
|
64
|
+
logger2.debug({
|
|
65
|
+
count: availableModels.length,
|
|
66
|
+
matches: availableModels.filter((model) => {
|
|
67
|
+
return model.provider === this.config.modelProvider;
|
|
68
|
+
}).map((model) => `${model.provider}/${model.id}`)
|
|
69
|
+
}, "available models");
|
|
70
|
+
if (!desiredModel) {
|
|
71
|
+
throw new Error(`Configured model not found: ${this.config.modelProvider}/${this.config.modelId}. Check your pi agent config and installed extensions.`);
|
|
72
|
+
}
|
|
73
|
+
logger2.info({
|
|
74
|
+
to: `${desiredModel.provider}/${desiredModel.id}`
|
|
75
|
+
}, "setting initial session model");
|
|
76
|
+
await session.setModel(desiredModel);
|
|
77
|
+
await this.applyConfiguredThinkingLevelForSession(session);
|
|
78
|
+
}
|
|
79
|
+
async listModels(session) {
|
|
80
|
+
const availableModels = await this.modelRegistry.getAvailable();
|
|
81
|
+
const currentDisplay = session?.model ? `${session.model.provider}/${session.model.id}` : null;
|
|
82
|
+
const lines = availableModels.map((model) => {
|
|
83
|
+
const display = `${model.provider}/${model.id}`;
|
|
84
|
+
const marker = currentDisplay === display ? " (current)" : "";
|
|
85
|
+
return ` ${display}${marker}`;
|
|
86
|
+
});
|
|
87
|
+
return [
|
|
88
|
+
`Available models (${availableModels.length}):`,
|
|
89
|
+
...lines,
|
|
90
|
+
`
|
|
91
|
+
Usage: !model <provider/modelId> to switch.`
|
|
92
|
+
].join(`
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
async switchModel(provider, modelId, session) {
|
|
96
|
+
const model = this.modelRegistry.find(provider, modelId);
|
|
97
|
+
if (!model) {
|
|
98
|
+
const availableModels = await this.modelRegistry.getAvailable();
|
|
99
|
+
const matches = availableModels.filter((availableModel) => {
|
|
100
|
+
return availableModel.provider === provider;
|
|
101
|
+
}).map((availableModel) => {
|
|
102
|
+
return `${availableModel.provider}/${availableModel.id}`;
|
|
103
|
+
});
|
|
104
|
+
const hint = matches.length > 0 ? `
|
|
105
|
+
Models from "${provider}": ${matches.join(", ")}` : `
|
|
106
|
+
Use !model to see all available models.`;
|
|
107
|
+
return `Model not found: ${provider}/${modelId}.${hint}`;
|
|
108
|
+
}
|
|
109
|
+
if (isSameModel(session.model, model)) {
|
|
110
|
+
return `Already using ${provider}/${modelId}.`;
|
|
111
|
+
}
|
|
112
|
+
await session.setModel(model);
|
|
113
|
+
await this.applyConfiguredThinkingLevelForSession(session);
|
|
114
|
+
const thinkingInfo = session.supportsThinking() ? ` (thinking: ${session.thinkingLevel})` : "";
|
|
115
|
+
return `Switched to ${provider}/${modelId}${thinkingInfo}.`;
|
|
116
|
+
}
|
|
117
|
+
getCurrentModelDisplay(session) {
|
|
118
|
+
if (!session?.model) {
|
|
119
|
+
return "(no model selected)";
|
|
120
|
+
}
|
|
121
|
+
return `${session.model.provider}/${session.model.id}`;
|
|
122
|
+
}
|
|
123
|
+
getThinkingLevel(session) {
|
|
124
|
+
if (!session.supportsThinking()) {
|
|
125
|
+
return { current: "off", available: [], supported: false };
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
current: session.thinkingLevel,
|
|
129
|
+
available: session.getAvailableThinkingLevels(),
|
|
130
|
+
supported: true
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
setThinkingLevel(session, level) {
|
|
134
|
+
if (!session.supportsThinking()) {
|
|
135
|
+
return "Current model does not support reasoning/thinking.";
|
|
136
|
+
}
|
|
137
|
+
const available = session.getAvailableThinkingLevels();
|
|
138
|
+
if (!available.includes(level)) {
|
|
139
|
+
return `Invalid thinking level "${level}" for current model. Available: ${available.join(", ")}`;
|
|
140
|
+
}
|
|
141
|
+
session.setThinkingLevel(level);
|
|
142
|
+
return `Thinking level set to "${level}".`;
|
|
143
|
+
}
|
|
144
|
+
async applyConfiguredThinkingLevelForSession(session) {
|
|
145
|
+
if (session.supportsThinking()) {
|
|
146
|
+
const available = session.getAvailableThinkingLevels();
|
|
147
|
+
if (available.includes(this.config.thinkingLevel)) {
|
|
148
|
+
session.setThinkingLevel(this.config.thinkingLevel);
|
|
149
|
+
logger2.debug({
|
|
150
|
+
level: this.config.thinkingLevel
|
|
151
|
+
}, "thinking level applied");
|
|
152
|
+
} else {
|
|
153
|
+
logger2.debug({
|
|
154
|
+
requested: this.config.thinkingLevel,
|
|
155
|
+
available
|
|
156
|
+
}, "thinking level not available for model");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function isSameModel(currentModel, desiredModel) {
|
|
162
|
+
if (!currentModel) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return currentModel.provider === desiredModel.provider && currentModel.id === desiredModel.id;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/agent-resource-service.ts
|
|
169
|
+
class AgentResourceService {
|
|
170
|
+
resourceLoader;
|
|
171
|
+
constructor(resourceLoader) {
|
|
172
|
+
this.resourceLoader = resourceLoader;
|
|
173
|
+
}
|
|
174
|
+
getSkillsSummary() {
|
|
175
|
+
const result = this.resourceLoader.getSkills();
|
|
176
|
+
const { skills } = result;
|
|
177
|
+
if (skills.length === 0) {
|
|
178
|
+
return "Skills: (none loaded)";
|
|
179
|
+
}
|
|
180
|
+
const names = skills.map((skill) => {
|
|
181
|
+
return skill.name;
|
|
182
|
+
});
|
|
183
|
+
return `Skills (${skills.length}): ${names.join(", ") || "(none)"}`;
|
|
184
|
+
}
|
|
185
|
+
getExtensionsSummary() {
|
|
186
|
+
const result = this.resourceLoader.getExtensions();
|
|
187
|
+
const { extensions, errors } = result;
|
|
188
|
+
if (extensions.length === 0) {
|
|
189
|
+
return "Extensions: (none loaded)";
|
|
190
|
+
}
|
|
191
|
+
const lines = extensions.map((extension) => {
|
|
192
|
+
const toolCount = extension.tools.size;
|
|
193
|
+
const commandCount = extension.commands.size;
|
|
194
|
+
const parts = [];
|
|
195
|
+
if (toolCount > 0) {
|
|
196
|
+
parts.push(`${toolCount} tool${toolCount !== 1 ? "s" : ""}`);
|
|
197
|
+
}
|
|
198
|
+
if (commandCount > 0) {
|
|
199
|
+
parts.push(`${commandCount} command${commandCount !== 1 ? "s" : ""}`);
|
|
200
|
+
}
|
|
201
|
+
const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
202
|
+
return ` ${extension.path}${summary}`;
|
|
203
|
+
});
|
|
204
|
+
const header = `Extensions (${extensions.length}):`;
|
|
205
|
+
const errorLines = errors.length > 0 ? [
|
|
206
|
+
`Errors (${errors.length}):`,
|
|
207
|
+
...errors.map((error) => {
|
|
208
|
+
return ` ${error.path}: ${error.error}`;
|
|
209
|
+
})
|
|
210
|
+
] : [];
|
|
211
|
+
return [header, ...lines, ...errorLines].join(`
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
async reloadResources() {
|
|
215
|
+
await this.resourceLoader.reload();
|
|
216
|
+
const extensions = this.resourceLoader.getExtensions().extensions.map((extension) => {
|
|
217
|
+
return extension.path;
|
|
218
|
+
});
|
|
219
|
+
const skills = this.resourceLoader.getSkills();
|
|
220
|
+
const skillNames = skills.skills.map((skill) => {
|
|
221
|
+
return skill.name;
|
|
222
|
+
});
|
|
223
|
+
const agentsFiles = this.resourceLoader.getAgentsFiles().agentsFiles.map((file) => {
|
|
224
|
+
return file.path;
|
|
225
|
+
});
|
|
226
|
+
return [
|
|
227
|
+
"Resources reloaded.",
|
|
228
|
+
`Extensions (${extensions.length}): ${extensions.join(", ") || "(none)"}`,
|
|
229
|
+
`Skills (${skills.skills.length}): ${skillNames.join(", ") || "(none)"}`,
|
|
230
|
+
`AGENTS.md files (${agentsFiles.length}): ${agentsFiles.join(", ") || "(none)"}`
|
|
231
|
+
].join(`
|
|
232
|
+
`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
42
236
|
// src/debug-print.ts
|
|
43
237
|
function debugPrint(body, title) {
|
|
44
238
|
const WIDTH = 80;
|
|
@@ -57,7 +251,7 @@ function debugPrint(body, title) {
|
|
|
57
251
|
|
|
58
252
|
// src/markdown-table-transformer.ts
|
|
59
253
|
import { Lexer } from "marked";
|
|
60
|
-
var
|
|
254
|
+
var logger3 = createModuleLogger("markdown-table-transformer");
|
|
61
255
|
var CODE_BLOCK_WRAPPER = "```\n{TABLE}\n```";
|
|
62
256
|
async function transformMarkdownTablesToCodeBlocks(text) {
|
|
63
257
|
const normalized = normalizeCodeFences(text);
|
|
@@ -101,7 +295,7 @@ async function formatWithPrettier(text) {
|
|
|
101
295
|
});
|
|
102
296
|
return formatted.trim();
|
|
103
297
|
} catch (error) {
|
|
104
|
-
|
|
298
|
+
logger3.error({
|
|
105
299
|
error
|
|
106
300
|
}, "Prettier formatting failed");
|
|
107
301
|
return text;
|
|
@@ -109,7 +303,7 @@ async function formatWithPrettier(text) {
|
|
|
109
303
|
}
|
|
110
304
|
|
|
111
305
|
// src/reply-buffer.ts
|
|
112
|
-
var
|
|
306
|
+
var logger4 = createModuleLogger("reply-buffer");
|
|
113
307
|
async function runPromptAndCollectReply(session, prompt, options = {}) {
|
|
114
308
|
let streamedText = "";
|
|
115
309
|
let eventCount = 0;
|
|
@@ -126,20 +320,20 @@ async function runPromptAndCollectReply(session, prompt, options = {}) {
|
|
|
126
320
|
}
|
|
127
321
|
if (event.type === "tool_execution_start") {
|
|
128
322
|
toolCount += 1;
|
|
129
|
-
|
|
323
|
+
logger4.debug({
|
|
130
324
|
toolName: event.toolName,
|
|
131
325
|
input: event.toolName === "bash" ? event.args.command : event.args
|
|
132
326
|
}, `tool start: [${event.toolName}] `);
|
|
133
327
|
}
|
|
134
328
|
if (event.type === "tool_execution_end") {
|
|
135
|
-
|
|
329
|
+
logger4.debug({
|
|
136
330
|
toolName: event.toolName,
|
|
137
331
|
isError: event.isError,
|
|
138
332
|
output: truncateForLog(extractToolOutput(event.result))
|
|
139
333
|
}, `tool end: [${event.toolName}]`);
|
|
140
334
|
}
|
|
141
335
|
if (event.type === "agent_end") {
|
|
142
|
-
|
|
336
|
+
logger4.debug("agent end");
|
|
143
337
|
}
|
|
144
338
|
});
|
|
145
339
|
try {
|
|
@@ -193,7 +387,7 @@ function getLatestAssistantText(messages) {
|
|
|
193
387
|
}
|
|
194
388
|
|
|
195
389
|
// src/agent-service.ts
|
|
196
|
-
var
|
|
390
|
+
var logger5 = createModuleLogger("agent-service");
|
|
197
391
|
|
|
198
392
|
class AgentService {
|
|
199
393
|
config;
|
|
@@ -202,6 +396,8 @@ class AgentService {
|
|
|
202
396
|
settingsManager;
|
|
203
397
|
resourceLoader;
|
|
204
398
|
session = null;
|
|
399
|
+
models;
|
|
400
|
+
resources;
|
|
205
401
|
constructor(config) {
|
|
206
402
|
this.config = config;
|
|
207
403
|
this.authStorage = AuthStorage.create(path.join(config.agentDir, "auth.json"));
|
|
@@ -212,11 +408,13 @@ class AgentService {
|
|
|
212
408
|
agentDir: config.agentDir,
|
|
213
409
|
settingsManager: this.settingsManager
|
|
214
410
|
});
|
|
411
|
+
this.models = new AgentModelService(config, this.modelRegistry);
|
|
412
|
+
this.resources = new AgentResourceService(this.resourceLoader);
|
|
215
413
|
}
|
|
216
414
|
async initialize() {
|
|
217
415
|
await fs.mkdir(this.config.agentDir, { recursive: true });
|
|
218
416
|
await fs.mkdir(this.getSessionDir(), { recursive: true });
|
|
219
|
-
|
|
417
|
+
logger5.info({
|
|
220
418
|
cwd: this.config.cwd,
|
|
221
419
|
agentDir: this.config.agentDir,
|
|
222
420
|
sessionDir: this.getSessionDir(),
|
|
@@ -225,7 +423,7 @@ class AgentService {
|
|
|
225
423
|
thinkingLevel: this.config.thinkingLevel
|
|
226
424
|
}, "config");
|
|
227
425
|
await this.resourceLoader.reload();
|
|
228
|
-
|
|
426
|
+
logger5.info({
|
|
229
427
|
extensions: this.resourceLoader.getExtensions().extensions.map((extension) => extension.path),
|
|
230
428
|
agentsFiles: this.resourceLoader.getAgentsFiles().agentsFiles.map((file) => file.path)
|
|
231
429
|
}, "resources loaded");
|
|
@@ -249,12 +447,9 @@ class AgentService {
|
|
|
249
447
|
sessionManager: SessionManager.inMemory(),
|
|
250
448
|
thinkingLevel: "off"
|
|
251
449
|
});
|
|
252
|
-
|
|
450
|
+
logger5.debug({ sessionId: session.sessionId }, "temporary session created");
|
|
253
451
|
return session;
|
|
254
452
|
}
|
|
255
|
-
findModel(provider, modelId) {
|
|
256
|
-
return this.modelRegistry.find(provider, modelId);
|
|
257
|
-
}
|
|
258
453
|
async createSession(sessionDir) {
|
|
259
454
|
await fs.mkdir(sessionDir, { recursive: true });
|
|
260
455
|
const { session } = await createAgentSession({
|
|
@@ -267,12 +462,12 @@ class AgentService {
|
|
|
267
462
|
sessionManager: SessionManager.continueRecent(this.config.cwd, sessionDir),
|
|
268
463
|
thinkingLevel: this.config.thinkingLevel
|
|
269
464
|
});
|
|
270
|
-
|
|
465
|
+
logger5.debug({
|
|
271
466
|
sessionDir,
|
|
272
467
|
sessionId: session.sessionId,
|
|
273
468
|
sessionFile: session.sessionFile
|
|
274
469
|
}, "scoped session created");
|
|
275
|
-
await this.
|
|
470
|
+
await this.models.ensureSessionHasConfiguredModel(session);
|
|
276
471
|
return session;
|
|
277
472
|
}
|
|
278
473
|
async prompt(text) {
|
|
@@ -280,56 +475,6 @@ class AgentService {
|
|
|
280
475
|
const transformedPrompt = await this.config.promptTransform(text);
|
|
281
476
|
return runPromptAndCollectReply(session, transformedPrompt);
|
|
282
477
|
}
|
|
283
|
-
getSkillsSummary() {
|
|
284
|
-
const result = this.resourceLoader.getSkills();
|
|
285
|
-
const { skills } = result;
|
|
286
|
-
if (skills.length === 0) {
|
|
287
|
-
return "Skills: (none loaded)";
|
|
288
|
-
}
|
|
289
|
-
const names = skills.map((s) => s.name);
|
|
290
|
-
return `Skills (${skills.length}): ${names.join(", ") || "(none)"}`;
|
|
291
|
-
}
|
|
292
|
-
async reloadResources() {
|
|
293
|
-
await this.resourceLoader.reload();
|
|
294
|
-
const extensions = this.resourceLoader.getExtensions().extensions.map((ext) => ext.path);
|
|
295
|
-
const skills = this.resourceLoader.getSkills();
|
|
296
|
-
const skillNames = skills.skills.map((s) => s.name);
|
|
297
|
-
const agentsFiles = this.resourceLoader.getAgentsFiles().agentsFiles.map((f) => f.path);
|
|
298
|
-
return [
|
|
299
|
-
"Resources reloaded.",
|
|
300
|
-
`Extensions (${extensions.length}): ${extensions.join(", ") || "(none)"}`,
|
|
301
|
-
`Skills (${skills.skills.length}): ${skillNames.join(", ") || "(none)"}`,
|
|
302
|
-
`AGENTS.md files (${agentsFiles.length}): ${agentsFiles.join(", ") || "(none)"}`
|
|
303
|
-
].join(`
|
|
304
|
-
`);
|
|
305
|
-
}
|
|
306
|
-
getExtensionsSummary() {
|
|
307
|
-
const result = this.resourceLoader.getExtensions();
|
|
308
|
-
const { extensions, errors } = result;
|
|
309
|
-
if (extensions.length === 0) {
|
|
310
|
-
return "Extensions: (none loaded)";
|
|
311
|
-
}
|
|
312
|
-
const lines = extensions.map((ext) => {
|
|
313
|
-
const toolCount = ext.tools.size;
|
|
314
|
-
const commandCount = ext.commands.size;
|
|
315
|
-
const parts = [];
|
|
316
|
-
if (toolCount > 0) {
|
|
317
|
-
parts.push(`${toolCount} tool${toolCount !== 1 ? "s" : ""}`);
|
|
318
|
-
}
|
|
319
|
-
if (commandCount > 0) {
|
|
320
|
-
parts.push(`${commandCount} command${commandCount !== 1 ? "s" : ""}`);
|
|
321
|
-
}
|
|
322
|
-
const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
323
|
-
return ` ${ext.path}${summary}`;
|
|
324
|
-
});
|
|
325
|
-
const header = `Extensions (${extensions.length}):`;
|
|
326
|
-
const errorLines = errors.length > 0 ? [
|
|
327
|
-
`Errors (${errors.length}):`,
|
|
328
|
-
...errors.map((e) => ` ${e.path}: ${e.error}`)
|
|
329
|
-
] : [];
|
|
330
|
-
return [header, ...lines, ...errorLines].join(`
|
|
331
|
-
`);
|
|
332
|
-
}
|
|
333
478
|
async compact() {
|
|
334
479
|
const session = this.requireSession();
|
|
335
480
|
await session.compact();
|
|
@@ -356,7 +501,7 @@ class AgentService {
|
|
|
356
501
|
}
|
|
357
502
|
getStatus() {
|
|
358
503
|
const session = this.requireSession();
|
|
359
|
-
const model =
|
|
504
|
+
const model = this.models.getCurrentModelDisplay(session);
|
|
360
505
|
const contextUsage = session.getContextUsage();
|
|
361
506
|
const thinkingInfo = session.supportsThinking() ? `thinking: ${session.thinkingLevel} (available: ${session.getAvailableThinkingLevels().join(", ")})` : "thinking: not supported";
|
|
362
507
|
return {
|
|
@@ -388,38 +533,14 @@ class AgentService {
|
|
|
388
533
|
thinkingLevel: this.config.thinkingLevel
|
|
389
534
|
});
|
|
390
535
|
this.session = session;
|
|
391
|
-
|
|
536
|
+
logger5.info({
|
|
392
537
|
sessionId: session.sessionId,
|
|
393
538
|
sessionFile: session.sessionFile,
|
|
394
539
|
restoredModel: session.model ? `${session.model.provider}/${session.model.id}` : null
|
|
395
540
|
}, "session ready");
|
|
396
541
|
}
|
|
397
542
|
async ensureConfiguredModel() {
|
|
398
|
-
await this.
|
|
399
|
-
}
|
|
400
|
-
async ensureModelForSession(session) {
|
|
401
|
-
if (session.model) {
|
|
402
|
-
logger4.debug({
|
|
403
|
-
model: `${session.model.provider}/${session.model.id}`
|
|
404
|
-
}, "retaining existing session model");
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const desiredModel = this.modelRegistry.find(this.config.modelProvider, this.config.modelId);
|
|
408
|
-
const availableModels = await this.modelRegistry.getAvailable();
|
|
409
|
-
logger4.debug({
|
|
410
|
-
count: availableModels.length,
|
|
411
|
-
matches: availableModels.filter((model) => {
|
|
412
|
-
return model.provider === this.config.modelProvider;
|
|
413
|
-
}).map((model) => `${model.provider}/${model.id}`)
|
|
414
|
-
}, "available models");
|
|
415
|
-
if (!desiredModel) {
|
|
416
|
-
throw new Error(`Configured model not found: ${this.config.modelProvider}/${this.config.modelId}. Check your pi agent config and installed extensions.`);
|
|
417
|
-
}
|
|
418
|
-
logger4.info({
|
|
419
|
-
to: `${desiredModel.provider}/${desiredModel.id}`
|
|
420
|
-
}, "setting initial session model");
|
|
421
|
-
await session.setModel(desiredModel);
|
|
422
|
-
await this.applyConfiguredThinkingLevelForSession(session);
|
|
543
|
+
await this.models.ensureSessionHasConfiguredModel(this.requireSession());
|
|
423
544
|
}
|
|
424
545
|
requireSession() {
|
|
425
546
|
if (!this.session) {
|
|
@@ -427,110 +548,20 @@ class AgentService {
|
|
|
427
548
|
}
|
|
428
549
|
return this.session;
|
|
429
550
|
}
|
|
430
|
-
async applyConfiguredThinkingLevelForSession(session) {
|
|
431
|
-
if (session.supportsThinking()) {
|
|
432
|
-
const available = session.getAvailableThinkingLevels();
|
|
433
|
-
if (available.includes(this.config.thinkingLevel)) {
|
|
434
|
-
session.setThinkingLevel(this.config.thinkingLevel);
|
|
435
|
-
logger4.debug({
|
|
436
|
-
level: this.config.thinkingLevel
|
|
437
|
-
}, "thinking level applied");
|
|
438
|
-
} else {
|
|
439
|
-
logger4.debug({
|
|
440
|
-
requested: this.config.thinkingLevel,
|
|
441
|
-
available
|
|
442
|
-
}, "thinking level not available for model");
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
async listModels(session) {
|
|
447
|
-
const effectiveSession = session ?? this.session;
|
|
448
|
-
const availableModels = await this.modelRegistry.getAvailable();
|
|
449
|
-
const currentDisplay = effectiveSession?.model ? `${effectiveSession.model.provider}/${effectiveSession.model.id}` : null;
|
|
450
|
-
const lines = availableModels.map((model) => {
|
|
451
|
-
const display = `${model.provider}/${model.id}`;
|
|
452
|
-
const marker = currentDisplay === display ? " (current)" : "";
|
|
453
|
-
return ` ${display}${marker}`;
|
|
454
|
-
});
|
|
455
|
-
return [
|
|
456
|
-
`Available models (${availableModels.length}):`,
|
|
457
|
-
...lines,
|
|
458
|
-
`
|
|
459
|
-
Usage: !model <provider/modelId> to switch.`
|
|
460
|
-
].join(`
|
|
461
|
-
`);
|
|
462
|
-
}
|
|
463
|
-
async switchModel(provider, modelId, session) {
|
|
464
|
-
const effectiveSession = session ?? this.requireSession();
|
|
465
|
-
const model = this.modelRegistry.find(provider, modelId);
|
|
466
|
-
if (!model) {
|
|
467
|
-
const availableModels = await this.modelRegistry.getAvailable();
|
|
468
|
-
const matches = availableModels.filter((m) => {
|
|
469
|
-
return m.provider === provider;
|
|
470
|
-
}).map((m) => `${m.provider}/${m.id}`);
|
|
471
|
-
const hint = matches.length > 0 ? `
|
|
472
|
-
Models from "${provider}": ${matches.join(", ")}` : `
|
|
473
|
-
Use !model to see all available models.`;
|
|
474
|
-
return `Model not found: ${provider}/${modelId}.${hint}`;
|
|
475
|
-
}
|
|
476
|
-
if (isSameModel(effectiveSession.model, model)) {
|
|
477
|
-
return `Already using ${provider}/${modelId}.`;
|
|
478
|
-
}
|
|
479
|
-
await effectiveSession.setModel(model);
|
|
480
|
-
await this.applyConfiguredThinkingLevelForSession(effectiveSession);
|
|
481
|
-
const thinkingInfo = effectiveSession.supportsThinking() ? ` (thinking: ${effectiveSession.thinkingLevel})` : "";
|
|
482
|
-
return `Switched to ${provider}/${modelId}${thinkingInfo}.`;
|
|
483
|
-
}
|
|
484
|
-
getCurrentModelDisplay(session) {
|
|
485
|
-
const effectiveSession = session ?? this.session;
|
|
486
|
-
if (!effectiveSession?.model) {
|
|
487
|
-
return "(no model selected)";
|
|
488
|
-
}
|
|
489
|
-
return `${effectiveSession.model.provider}/${effectiveSession.model.id}`;
|
|
490
|
-
}
|
|
491
|
-
getThinkingLevel() {
|
|
492
|
-
const session = this.requireSession();
|
|
493
|
-
if (!session.supportsThinking()) {
|
|
494
|
-
return { current: "off", available: [], supported: false };
|
|
495
|
-
}
|
|
496
|
-
return {
|
|
497
|
-
current: session.thinkingLevel,
|
|
498
|
-
available: session.getAvailableThinkingLevels(),
|
|
499
|
-
supported: true
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
setThinkingLevel(level) {
|
|
503
|
-
const session = this.requireSession();
|
|
504
|
-
if (!session.supportsThinking()) {
|
|
505
|
-
return "Current model does not support reasoning/thinking.";
|
|
506
|
-
}
|
|
507
|
-
const available = session.getAvailableThinkingLevels();
|
|
508
|
-
if (!available.includes(level)) {
|
|
509
|
-
return `Invalid thinking level "${level}" for current model. Available: ${available.join(", ")}`;
|
|
510
|
-
}
|
|
511
|
-
session.setThinkingLevel(level);
|
|
512
|
-
return `Thinking level set to "${level}".`;
|
|
513
|
-
}
|
|
514
551
|
getSessionDir() {
|
|
515
552
|
return path.join(this.config.agentDir, "sessions");
|
|
516
553
|
}
|
|
517
554
|
}
|
|
518
|
-
function isSameModel(currentModel, desiredModel) {
|
|
519
|
-
if (!currentModel) {
|
|
520
|
-
return false;
|
|
521
|
-
}
|
|
522
|
-
return currentModel.provider === desiredModel.provider && currentModel.id === desiredModel.id;
|
|
523
|
-
}
|
|
524
555
|
|
|
525
556
|
// src/config.ts
|
|
526
557
|
import path2 from "node:path";
|
|
527
558
|
import dotenv from "dotenv";
|
|
528
559
|
function resolveConfig(config) {
|
|
529
|
-
const discordAllowedUserId =
|
|
560
|
+
const discordAllowedUserId = requireNonEmptyConfigValue("discordAllowedUserId", config.discordAllowedUserId);
|
|
530
561
|
return {
|
|
531
|
-
discordBotToken:
|
|
562
|
+
discordBotToken: requireNonEmptyConfigValue("discordBotToken", config.discordBotToken),
|
|
532
563
|
discordAllowedUserId,
|
|
533
|
-
cwd:
|
|
564
|
+
cwd: requireNonEmptyConfigValue("cwd", config.cwd),
|
|
534
565
|
agentDir: config.agentDir?.trim() || path2.join(config.cwd, ".pi-agent"),
|
|
535
566
|
modelProvider: config.modelProvider?.trim() || "openrouter",
|
|
536
567
|
modelId: config.modelId?.trim() || "anthropic/claude-3.5-haiku",
|
|
@@ -565,7 +596,7 @@ function loadDiscordGatewayConfigFromEnv(overrides = {}) {
|
|
|
565
596
|
discordAllowedUserIds: overrides.discordAllowedUserIds ?? parseStringArrayFromEnv("DISCORD_ALLOWED_USER_IDS")
|
|
566
597
|
});
|
|
567
598
|
}
|
|
568
|
-
function
|
|
599
|
+
function requireNonEmptyConfigValue(name, value) {
|
|
569
600
|
const trimmedValue = value.trim();
|
|
570
601
|
if (!trimmedValue) {
|
|
571
602
|
throw new Error(`Missing required config value: ${name}`);
|
|
@@ -650,16 +681,6 @@ function getSessionStatusText(session, promptQueue, extras) {
|
|
|
650
681
|
return lines.join(`
|
|
651
682
|
`);
|
|
652
683
|
}
|
|
653
|
-
function getThinkingInfo(session) {
|
|
654
|
-
if (!session.supportsThinking()) {
|
|
655
|
-
return { current: "off", available: [], supported: false };
|
|
656
|
-
}
|
|
657
|
-
return {
|
|
658
|
-
current: session.thinkingLevel,
|
|
659
|
-
available: session.getAvailableThinkingLevels(),
|
|
660
|
-
supported: true
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
684
|
function getEffectiveSession(context) {
|
|
664
685
|
return context.session ?? context.agentService.getSession();
|
|
665
686
|
}
|
|
@@ -721,8 +742,8 @@ async function handleStatusCommand(trimmedInput, context) {
|
|
|
721
742
|
return effectiveSession;
|
|
722
743
|
}
|
|
723
744
|
const tools = effectiveSession.session.getAllTools();
|
|
724
|
-
const extensionsSummary = context.agentService.getExtensionsSummary();
|
|
725
|
-
const skillsSummary = context.agentService.getSkillsSummary();
|
|
745
|
+
const extensionsSummary = context.agentService.resources.getExtensionsSummary();
|
|
746
|
+
const skillsSummary = context.agentService.resources.getSkillsSummary();
|
|
726
747
|
return {
|
|
727
748
|
handled: true,
|
|
728
749
|
response: getSessionStatusText(effectiveSession.session, context.promptQueue, {
|
|
@@ -742,7 +763,7 @@ async function handleThinkingCommand(trimmedInput, context) {
|
|
|
742
763
|
}
|
|
743
764
|
const parts = trimmedInput.split(" ");
|
|
744
765
|
if (parts.length === 1) {
|
|
745
|
-
const info =
|
|
766
|
+
const info = context.agentService.models.getThinkingLevel(effectiveSession.session);
|
|
746
767
|
if (!info.supported) {
|
|
747
768
|
return {
|
|
748
769
|
handled: true,
|
|
@@ -760,23 +781,9 @@ async function handleThinkingCommand(trimmedInput, context) {
|
|
|
760
781
|
};
|
|
761
782
|
}
|
|
762
783
|
const requestedLevel = parts[1];
|
|
763
|
-
if (!effectiveSession.session.supportsThinking()) {
|
|
764
|
-
return {
|
|
765
|
-
handled: true,
|
|
766
|
-
response: "Current model does not support reasoning/thinking."
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
const available = effectiveSession.session.getAvailableThinkingLevels();
|
|
770
|
-
if (!available.includes(requestedLevel)) {
|
|
771
|
-
return {
|
|
772
|
-
handled: true,
|
|
773
|
-
response: `Invalid thinking level "${requestedLevel}" for current model. Available: ${available.join(", ")}`
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
effectiveSession.session.setThinkingLevel(requestedLevel);
|
|
777
784
|
return {
|
|
778
785
|
handled: true,
|
|
779
|
-
response:
|
|
786
|
+
response: context.agentService.models.setThinkingLevel(effectiveSession.session, requestedLevel)
|
|
780
787
|
};
|
|
781
788
|
}
|
|
782
789
|
async function handleModelCommand(trimmedInput, context) {
|
|
@@ -789,8 +796,8 @@ async function handleModelCommand(trimmedInput, context) {
|
|
|
789
796
|
}
|
|
790
797
|
const parts = trimmedInput.split(" ");
|
|
791
798
|
if (parts.length === 1) {
|
|
792
|
-
const current = context.agentService.getCurrentModelDisplay(effectiveSession.session);
|
|
793
|
-
const modelList = await context.agentService.listModels(effectiveSession.session);
|
|
799
|
+
const current = context.agentService.models.getCurrentModelDisplay(effectiveSession.session);
|
|
800
|
+
const modelList = await context.agentService.models.listModels(effectiveSession.session);
|
|
794
801
|
return {
|
|
795
802
|
handled: true,
|
|
796
803
|
response: `Current model: ${current}
|
|
@@ -812,7 +819,7 @@ ${modelList}`
|
|
|
812
819
|
const modelId = argument.substring(slashIndex + 1).trim();
|
|
813
820
|
return {
|
|
814
821
|
handled: true,
|
|
815
|
-
response: await context.agentService.switchModel(provider, modelId, effectiveSession.session)
|
|
822
|
+
response: await context.agentService.models.switchModel(provider, modelId, effectiveSession.session)
|
|
816
823
|
};
|
|
817
824
|
}
|
|
818
825
|
async function handleCompactCommand(trimmedInput, context) {
|
|
@@ -838,7 +845,7 @@ async function handleReloadCommand(trimmedInput, context) {
|
|
|
838
845
|
return {
|
|
839
846
|
handled: true,
|
|
840
847
|
response: await context.promptQueue.enqueue(async () => {
|
|
841
|
-
return context.agentService.reloadResources();
|
|
848
|
+
return context.agentService.resources.reloadResources();
|
|
842
849
|
})
|
|
843
850
|
};
|
|
844
851
|
}
|
|
@@ -892,7 +899,7 @@ async function executeCommand(input, context) {
|
|
|
892
899
|
}
|
|
893
900
|
|
|
894
901
|
// src/discord-attachments.ts
|
|
895
|
-
var
|
|
902
|
+
var logger6 = createModuleLogger("discord-attachments");
|
|
896
903
|
var TEXT_ATTACHMENT_EXTENSIONS = [
|
|
897
904
|
".txt",
|
|
898
905
|
".md",
|
|
@@ -954,11 +961,11 @@ async function readTextAttachments(message) {
|
|
|
954
961
|
const results = [];
|
|
955
962
|
for (const [, attachment] of attachments) {
|
|
956
963
|
if (!isSupportedTextAttachment(attachment)) {
|
|
957
|
-
|
|
964
|
+
logger6.debug({ messageId: message.id, filename: attachment.name }, "skipping non-text attachment");
|
|
958
965
|
continue;
|
|
959
966
|
}
|
|
960
967
|
if (attachment.size > MAX_TEXT_ATTACHMENT_SIZE_BYTES) {
|
|
961
|
-
|
|
968
|
+
logger6.warn({
|
|
962
969
|
messageId: message.id,
|
|
963
970
|
filename: attachment.name,
|
|
964
971
|
size: attachment.size
|
|
@@ -966,14 +973,14 @@ async function readTextAttachments(message) {
|
|
|
966
973
|
continue;
|
|
967
974
|
}
|
|
968
975
|
try {
|
|
969
|
-
|
|
976
|
+
logger6.info({
|
|
970
977
|
messageId: message.id,
|
|
971
978
|
filename: attachment.name,
|
|
972
979
|
size: attachment.size
|
|
973
980
|
}, "fetching attachment");
|
|
974
981
|
const response = await fetch(attachment.url);
|
|
975
982
|
if (!response.ok) {
|
|
976
|
-
|
|
983
|
+
logger6.warn({
|
|
977
984
|
messageId: message.id,
|
|
978
985
|
filename: attachment.name,
|
|
979
986
|
status: response.status
|
|
@@ -983,7 +990,7 @@ async function readTextAttachments(message) {
|
|
|
983
990
|
const content = await response.text();
|
|
984
991
|
results.push({ filename: attachment.name, content });
|
|
985
992
|
} catch (error) {
|
|
986
|
-
|
|
993
|
+
logger6.error({ messageId: message.id, filename: attachment.name, error }, "error fetching attachment");
|
|
987
994
|
}
|
|
988
995
|
}
|
|
989
996
|
return results;
|
|
@@ -999,7 +1006,7 @@ async function readMediaAttachments(message) {
|
|
|
999
1006
|
continue;
|
|
1000
1007
|
}
|
|
1001
1008
|
if (attachment.size > MAX_MEDIA_ATTACHMENT_SIZE_BYTES) {
|
|
1002
|
-
|
|
1009
|
+
logger6.warn({
|
|
1003
1010
|
messageId: message.id,
|
|
1004
1011
|
filename: attachment.name,
|
|
1005
1012
|
size: attachment.size
|
|
@@ -1007,14 +1014,14 @@ async function readMediaAttachments(message) {
|
|
|
1007
1014
|
continue;
|
|
1008
1015
|
}
|
|
1009
1016
|
try {
|
|
1010
|
-
|
|
1017
|
+
logger6.info({
|
|
1011
1018
|
messageId: message.id,
|
|
1012
1019
|
filename: attachment.name,
|
|
1013
1020
|
size: attachment.size
|
|
1014
1021
|
}, "fetching media attachment");
|
|
1015
1022
|
const response = await fetch(attachment.url);
|
|
1016
1023
|
if (!response.ok) {
|
|
1017
|
-
|
|
1024
|
+
logger6.warn({
|
|
1018
1025
|
messageId: message.id,
|
|
1019
1026
|
filename: attachment.name,
|
|
1020
1027
|
status: response.status
|
|
@@ -1028,7 +1035,7 @@ async function readMediaAttachments(message) {
|
|
|
1028
1035
|
mimeType: attachment.contentType ?? "application/octet-stream"
|
|
1029
1036
|
});
|
|
1030
1037
|
} catch (error) {
|
|
1031
|
-
|
|
1038
|
+
logger6.error({ messageId: message.id, filename: attachment.name, error }, "error fetching media attachment");
|
|
1032
1039
|
}
|
|
1033
1040
|
}
|
|
1034
1041
|
return results;
|
|
@@ -1098,13 +1105,13 @@ function chunkMessage(text, maxChunkSize = SAFE_MESSAGE_LIMIT) {
|
|
|
1098
1105
|
}
|
|
1099
1106
|
|
|
1100
1107
|
// src/discord-replies.ts
|
|
1101
|
-
var
|
|
1108
|
+
var logger7 = createModuleLogger("discord-replies");
|
|
1102
1109
|
var WORKING_EMOJI = "⚙️";
|
|
1103
1110
|
async function addWorkingReaction(message) {
|
|
1104
1111
|
try {
|
|
1105
1112
|
await message.react(WORKING_EMOJI);
|
|
1106
1113
|
} catch (error) {
|
|
1107
|
-
|
|
1114
|
+
logger7.debug({ messageId: message.id, error }, "failed to add working reaction");
|
|
1108
1115
|
}
|
|
1109
1116
|
}
|
|
1110
1117
|
async function removeWorkingReaction(message) {
|
|
@@ -1114,13 +1121,13 @@ async function removeWorkingReaction(message) {
|
|
|
1114
1121
|
await reaction.users.remove(message.client.user);
|
|
1115
1122
|
}
|
|
1116
1123
|
} catch (error) {
|
|
1117
|
-
|
|
1124
|
+
logger7.debug({ messageId: message.id, error }, "failed to remove working reaction");
|
|
1118
1125
|
}
|
|
1119
1126
|
}
|
|
1120
1127
|
async function sendReply(message, text) {
|
|
1121
1128
|
const channel = message.channel;
|
|
1122
1129
|
if (!channel.isSendable()) {
|
|
1123
|
-
|
|
1130
|
+
logger7.debug({
|
|
1124
1131
|
messageId: message.id
|
|
1125
1132
|
}, "reply skipped, channel not sendable");
|
|
1126
1133
|
return;
|
|
@@ -1136,7 +1143,7 @@ async function sendReply(message, text) {
|
|
|
1136
1143
|
await channel.send(chunk);
|
|
1137
1144
|
}
|
|
1138
1145
|
} catch (error) {
|
|
1139
|
-
|
|
1146
|
+
logger7.error({
|
|
1140
1147
|
messageId: message.id,
|
|
1141
1148
|
error
|
|
1142
1149
|
}, "send reply failed");
|
|
@@ -1144,7 +1151,7 @@ async function sendReply(message, text) {
|
|
|
1144
1151
|
}
|
|
1145
1152
|
|
|
1146
1153
|
// src/image-description.ts
|
|
1147
|
-
var
|
|
1154
|
+
var logger8 = createModuleLogger("image-description");
|
|
1148
1155
|
async function describeMediaAttachment(agentService, imageData, mimeType, userText, visionModel) {
|
|
1149
1156
|
const session = await agentService.createTemporarySession();
|
|
1150
1157
|
await session.setModel(visionModel);
|
|
@@ -1165,7 +1172,7 @@ async function describeMediaAttachment(agentService, imageData, mimeType, userTe
|
|
|
1165
1172
|
await session.prompt(promptText, { images: [imageContent] });
|
|
1166
1173
|
text = extractLastAssistantText(session);
|
|
1167
1174
|
} catch (error) {
|
|
1168
|
-
|
|
1175
|
+
logger8.error({ error, mimeType }, "vision model prompt failed");
|
|
1169
1176
|
text = "(Vision model failed to process the file.)";
|
|
1170
1177
|
} finally {
|
|
1171
1178
|
session.dispose();
|
|
@@ -1173,7 +1180,7 @@ async function describeMediaAttachment(agentService, imageData, mimeType, userTe
|
|
|
1173
1180
|
if (!text) {
|
|
1174
1181
|
return "(Vision model returned no description.)";
|
|
1175
1182
|
}
|
|
1176
|
-
|
|
1183
|
+
logger8.debug({ textLength: text.length, mimeType }, "media described");
|
|
1177
1184
|
return text;
|
|
1178
1185
|
}
|
|
1179
1186
|
function extractLastAssistantText(session) {
|
|
@@ -1209,7 +1216,7 @@ function isAssistantMessage(msg) {
|
|
|
1209
1216
|
}
|
|
1210
1217
|
|
|
1211
1218
|
// src/discord-media-resolution.ts
|
|
1212
|
-
var
|
|
1219
|
+
var logger9 = createModuleLogger("discord-media-resolution");
|
|
1213
1220
|
function parseProviderModelId(value) {
|
|
1214
1221
|
const trimmed = value.trim();
|
|
1215
1222
|
if (!trimmed) {
|
|
@@ -1243,7 +1250,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1243
1250
|
const modelSupportsVision = currentModel?.input.includes("image") ?? false;
|
|
1244
1251
|
if (modelSupportsVision) {
|
|
1245
1252
|
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1246
|
-
|
|
1253
|
+
logger9.info({
|
|
1247
1254
|
count: mediaAttachments.length,
|
|
1248
1255
|
filenames: names,
|
|
1249
1256
|
model: currentModel ? `${currentModel.provider}/${currentModel.id}` : "none"
|
|
@@ -1259,7 +1266,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1259
1266
|
}
|
|
1260
1267
|
if (!config.visionModelId) {
|
|
1261
1268
|
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1262
|
-
|
|
1269
|
+
logger9.info({ filenames: names }, "media attachments received but vision model not configured");
|
|
1263
1270
|
const note = `
|
|
1264
1271
|
|
|
1265
1272
|
[User sent media attachment(s): ${names}]
|
|
@@ -1273,9 +1280,9 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1273
1280
|
if (!parsedVisionModelId) {
|
|
1274
1281
|
return { content, images: [] };
|
|
1275
1282
|
}
|
|
1276
|
-
const visionModel = agentService.findModel(parsedVisionModelId.provider, parsedVisionModelId.modelId);
|
|
1283
|
+
const visionModel = agentService.models.findModel(parsedVisionModelId.provider, parsedVisionModelId.modelId);
|
|
1277
1284
|
if (!visionModel) {
|
|
1278
|
-
|
|
1285
|
+
logger9.warn({ visionModelId: config.visionModelId }, "vision model not found in registry");
|
|
1279
1286
|
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1280
1287
|
const note = `
|
|
1281
1288
|
|
|
@@ -1286,7 +1293,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1286
1293
|
images: []
|
|
1287
1294
|
};
|
|
1288
1295
|
}
|
|
1289
|
-
|
|
1296
|
+
logger9.info({
|
|
1290
1297
|
count: mediaAttachments.length,
|
|
1291
1298
|
visionModel: `${visionModel.provider}/${visionModel.id}`
|
|
1292
1299
|
}, "describing media with vision model");
|
|
@@ -1313,7 +1320,7 @@ ${content}` : descriptionPrefix,
|
|
|
1313
1320
|
}
|
|
1314
1321
|
|
|
1315
1322
|
// src/discord-typing.ts
|
|
1316
|
-
var
|
|
1323
|
+
var logger10 = createModuleLogger("discord-typing");
|
|
1317
1324
|
var TYPING_INTERVAL_MS = 9000;
|
|
1318
1325
|
var typingIntervals = new Map;
|
|
1319
1326
|
async function sendTypingSafe(channel, channelKey) {
|
|
@@ -1325,7 +1332,7 @@ async function sendTypingSafe(channel, channelKey) {
|
|
|
1325
1332
|
headers: { Authorization: `Bot ${token}` }
|
|
1326
1333
|
});
|
|
1327
1334
|
if (response.ok) {
|
|
1328
|
-
|
|
1335
|
+
logger10.debug("[TYPING] STATUS UPDATED OK");
|
|
1329
1336
|
return;
|
|
1330
1337
|
}
|
|
1331
1338
|
if (response.status === 429) {
|
|
@@ -1337,28 +1344,28 @@ async function sendTypingSafe(channel, channelKey) {
|
|
|
1337
1344
|
retryMs = parsed.retry_after * 1000 + 500;
|
|
1338
1345
|
}
|
|
1339
1346
|
} catch {}
|
|
1340
|
-
|
|
1347
|
+
logger10.warn({ channelKey, retryMs, response: body }, `[TYPING] 429, retrying after ${retryMs}ms delay`);
|
|
1341
1348
|
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
1342
1349
|
await fetch(url, {
|
|
1343
1350
|
method: "POST",
|
|
1344
1351
|
headers: { Authorization: `Bot ${token}` }
|
|
1345
1352
|
});
|
|
1346
|
-
|
|
1353
|
+
logger10.info({ channelKey }, "[TYPING] retry done");
|
|
1347
1354
|
return;
|
|
1348
1355
|
}
|
|
1349
|
-
|
|
1356
|
+
logger10.warn({ channelKey, status: response.status }, "[TYPING] unexpected status");
|
|
1350
1357
|
} catch (error) {
|
|
1351
|
-
|
|
1358
|
+
logger10.warn({ channelKey, error }, "[TYPING] FAILED");
|
|
1352
1359
|
}
|
|
1353
1360
|
}
|
|
1354
1361
|
function startTypingForChannel(channel, channelKey) {
|
|
1355
1362
|
const existing = typingIntervals.get(channelKey);
|
|
1356
1363
|
if (existing) {
|
|
1357
1364
|
existing.refs += 1;
|
|
1358
|
-
|
|
1365
|
+
logger10.debug({ channelKey, refs: existing.refs }, "[TYPING] ref++ (reusing existing interval)");
|
|
1359
1366
|
return;
|
|
1360
1367
|
}
|
|
1361
|
-
|
|
1368
|
+
logger10.debug("[TYPING] started new interval");
|
|
1362
1369
|
sendTypingSafe(channel, channelKey);
|
|
1363
1370
|
const interval = setInterval(() => {
|
|
1364
1371
|
sendTypingSafe(channel, channelKey);
|
|
@@ -1368,17 +1375,17 @@ function startTypingForChannel(channel, channelKey) {
|
|
|
1368
1375
|
function stopTypingForChannel(channelKey) {
|
|
1369
1376
|
const entry = typingIntervals.get(channelKey);
|
|
1370
1377
|
if (!entry) {
|
|
1371
|
-
|
|
1378
|
+
logger10.debug({ channelKey }, "[TYPING] stop called but no entry found");
|
|
1372
1379
|
return;
|
|
1373
1380
|
}
|
|
1374
1381
|
entry.refs -= 1;
|
|
1375
1382
|
if (entry.refs <= 0) {
|
|
1376
1383
|
clearInterval(entry.interval);
|
|
1377
1384
|
typingIntervals.delete(channelKey);
|
|
1378
|
-
|
|
1385
|
+
logger10.debug("[TYPING] interval cleared (refs hit 0)");
|
|
1379
1386
|
return;
|
|
1380
1387
|
}
|
|
1381
|
-
|
|
1388
|
+
logger10.debug("[TYPING] ref-- (interval still active)");
|
|
1382
1389
|
}
|
|
1383
1390
|
|
|
1384
1391
|
// src/prompt-context.ts
|
|
@@ -1430,7 +1437,7 @@ function normalizeContextValue(value) {
|
|
|
1430
1437
|
}
|
|
1431
1438
|
|
|
1432
1439
|
// src/discord-message-handler.ts
|
|
1433
|
-
var
|
|
1440
|
+
var logger11 = createModuleLogger("discord-message-handler");
|
|
1434
1441
|
function buildDiscordPromptContent(message, scope, content, config) {
|
|
1435
1442
|
const isThread = scope.startsWith("thread:") && message.channel.isThread();
|
|
1436
1443
|
return buildDiscordMessageContextPrompt(content, {
|
|
@@ -1450,23 +1457,23 @@ function buildDiscordPromptContent(message, scope, content, config) {
|
|
|
1450
1457
|
}
|
|
1451
1458
|
async function handleDiscordMessage(message, config, agentService, sessionRegistry, authConfig) {
|
|
1452
1459
|
if (message.author.bot) {
|
|
1453
|
-
|
|
1460
|
+
logger11.debug("ignored bot message");
|
|
1454
1461
|
return;
|
|
1455
1462
|
}
|
|
1456
1463
|
if (message.system) {
|
|
1457
|
-
|
|
1464
|
+
logger11.debug({ messageId: message.id }, "ignored system message");
|
|
1458
1465
|
return;
|
|
1459
1466
|
}
|
|
1460
1467
|
const scope = resolveMessageScope(message);
|
|
1461
1468
|
if (scope === null) {
|
|
1462
|
-
|
|
1469
|
+
logger11.debug({
|
|
1463
1470
|
messageId: message.id,
|
|
1464
1471
|
channelType: message.channel.type
|
|
1465
1472
|
}, "unsupported channel type, ignoring");
|
|
1466
1473
|
return;
|
|
1467
1474
|
}
|
|
1468
1475
|
if (!isAuthorizedMessage(message, scope, authConfig)) {
|
|
1469
|
-
|
|
1476
|
+
logger11.debug({
|
|
1470
1477
|
messageId: message.id,
|
|
1471
1478
|
authorId: message.author.id,
|
|
1472
1479
|
scope
|
|
@@ -1486,10 +1493,10 @@ ${attachment.content}`;
|
|
|
1486
1493
|
}
|
|
1487
1494
|
const mediaAttachments = await readMediaAttachments(message);
|
|
1488
1495
|
if (!content && mediaAttachments.length === 0) {
|
|
1489
|
-
|
|
1496
|
+
logger11.debug({ messageId: message.id }, "ignored empty message (no text or images)");
|
|
1490
1497
|
return;
|
|
1491
1498
|
}
|
|
1492
|
-
|
|
1499
|
+
logger11.info({
|
|
1493
1500
|
direction: "IN",
|
|
1494
1501
|
scope,
|
|
1495
1502
|
messageId: message.id,
|
|
@@ -1504,7 +1511,7 @@ ${attachment.content}`;
|
|
|
1504
1511
|
const { entry, created } = await sessionRegistry.getOrCreate(scope);
|
|
1505
1512
|
const { session, promptQueue } = entry;
|
|
1506
1513
|
if (created && scope.startsWith("thread:") && message.channel.isThread()) {
|
|
1507
|
-
|
|
1514
|
+
logger11.info({
|
|
1508
1515
|
scope,
|
|
1509
1516
|
threadName: message.channel.name
|
|
1510
1517
|
}, "new thread session");
|
|
@@ -1517,7 +1524,7 @@ ${attachment.content}`;
|
|
|
1517
1524
|
if (commandResult.handled) {
|
|
1518
1525
|
stopTypingForChannel(channelKey);
|
|
1519
1526
|
if (commandResult.archive && scope.startsWith("thread:")) {
|
|
1520
|
-
|
|
1527
|
+
logger11.info({ scope }, "archiving thread");
|
|
1521
1528
|
const archiveChannel = message.channel;
|
|
1522
1529
|
if (archiveChannel.isSendable()) {
|
|
1523
1530
|
await archiveChannel.send(commandResult.response ?? "Archiving...");
|
|
@@ -1527,12 +1534,12 @@ ${attachment.content}`;
|
|
|
1527
1534
|
await archiveChannel.setArchived(true);
|
|
1528
1535
|
}
|
|
1529
1536
|
} catch (error) {
|
|
1530
|
-
|
|
1537
|
+
logger11.error({ error }, "failed to archive thread");
|
|
1531
1538
|
}
|
|
1532
1539
|
await sessionRegistry.remove(scope);
|
|
1533
1540
|
return;
|
|
1534
1541
|
}
|
|
1535
|
-
|
|
1542
|
+
logger11.info({
|
|
1536
1543
|
messageId: message.id,
|
|
1537
1544
|
command: content,
|
|
1538
1545
|
hasResponse: Boolean(commandResult.response)
|
|
@@ -1544,7 +1551,7 @@ ${attachment.content}`;
|
|
|
1544
1551
|
}
|
|
1545
1552
|
if (!message.channel.isSendable()) {
|
|
1546
1553
|
stopTypingForChannel(channelKey);
|
|
1547
|
-
|
|
1554
|
+
logger11.debug({ messageId: message.id }, "channel not sendable");
|
|
1548
1555
|
return;
|
|
1549
1556
|
}
|
|
1550
1557
|
await addWorkingReaction(message);
|
|
@@ -1578,7 +1585,7 @@ ${attachment.content}`;
|
|
|
1578
1585
|
}
|
|
1579
1586
|
|
|
1580
1587
|
// src/discord-gateway-client.ts
|
|
1581
|
-
var
|
|
1588
|
+
var logger12 = createModuleLogger("discord-gateway");
|
|
1582
1589
|
async function startGatewayClient(config, agentService, sessionRegistry, authConfig) {
|
|
1583
1590
|
const client = new Client({
|
|
1584
1591
|
intents: [
|
|
@@ -1590,7 +1597,7 @@ async function startGatewayClient(config, agentService, sessionRegistry, authCon
|
|
|
1590
1597
|
partials: [Partials.Channel]
|
|
1591
1598
|
});
|
|
1592
1599
|
client.once(Events.ClientReady, async (readyClient) => {
|
|
1593
|
-
|
|
1600
|
+
logger12.info({ userTag: readyClient.user.tag }, "logged in");
|
|
1594
1601
|
if (!authConfig.startupMessage) {
|
|
1595
1602
|
return;
|
|
1596
1603
|
}
|
|
@@ -1598,24 +1605,24 @@ async function startGatewayClient(config, agentService, sessionRegistry, authCon
|
|
|
1598
1605
|
const user = await readyClient.users.fetch(authConfig.discordAllowedUserId);
|
|
1599
1606
|
const dmChannel = await user.createDM();
|
|
1600
1607
|
await dmChannel.send(authConfig.startupMessage);
|
|
1601
|
-
|
|
1608
|
+
logger12.info({
|
|
1602
1609
|
userId: authConfig.discordAllowedUserId
|
|
1603
1610
|
}, "sent startup dm");
|
|
1604
1611
|
} catch (error) {
|
|
1605
|
-
|
|
1612
|
+
logger12.error({ error }, "failed to send startup dm");
|
|
1606
1613
|
}
|
|
1607
1614
|
});
|
|
1608
1615
|
client.on(Events.MessageCreate, async (message) => {
|
|
1609
1616
|
try {
|
|
1610
1617
|
await handleDiscordMessage(message, config, agentService, sessionRegistry, authConfig);
|
|
1611
1618
|
} catch (error) {
|
|
1612
|
-
|
|
1619
|
+
logger12.error({ error, direction: "IN" }, "message handling failed");
|
|
1613
1620
|
await sendReply(message, "The bot hit an error while handling that message.");
|
|
1614
1621
|
}
|
|
1615
1622
|
});
|
|
1616
1623
|
client.on(Events.ThreadDelete, async (thread) => {
|
|
1617
1624
|
const scope = `thread:${thread.id}`;
|
|
1618
|
-
|
|
1625
|
+
logger12.info({ threadId: thread.id, scope }, "thread deleted");
|
|
1619
1626
|
await sessionRegistry.remove(scope);
|
|
1620
1627
|
});
|
|
1621
1628
|
await client.login(config.discordBotToken);
|
|
@@ -1674,7 +1681,7 @@ function sessionDirForScope(agentDir, scope) {
|
|
|
1674
1681
|
}
|
|
1675
1682
|
throw new Error(`Unknown session scope: ${scope}`);
|
|
1676
1683
|
}
|
|
1677
|
-
var
|
|
1684
|
+
var logger13 = createModuleLogger("session-registry");
|
|
1678
1685
|
|
|
1679
1686
|
class SessionRegistry {
|
|
1680
1687
|
scopes = new Map;
|
|
@@ -1696,7 +1703,7 @@ class SessionRegistry {
|
|
|
1696
1703
|
createdAt: new Date
|
|
1697
1704
|
};
|
|
1698
1705
|
this.scopes.set(scope, entry);
|
|
1699
|
-
|
|
1706
|
+
logger13.debug({
|
|
1700
1707
|
scope,
|
|
1701
1708
|
sessionDir,
|
|
1702
1709
|
sessionId: session.sessionId
|
|
@@ -1708,7 +1715,7 @@ class SessionRegistry {
|
|
|
1708
1715
|
if (!entry) {
|
|
1709
1716
|
return;
|
|
1710
1717
|
}
|
|
1711
|
-
|
|
1718
|
+
logger13.debug({ scope }, "removing scope");
|
|
1712
1719
|
await entry.session.abort();
|
|
1713
1720
|
entry.session.dispose();
|
|
1714
1721
|
this.scopes.delete(scope);
|
|
@@ -1720,7 +1727,7 @@ class SessionRegistry {
|
|
|
1720
1727
|
return Array.from(this.scopes.keys());
|
|
1721
1728
|
}
|
|
1722
1729
|
async shutdownAll() {
|
|
1723
|
-
|
|
1730
|
+
logger13.info({ count: this.scopes.size }, "shutting down all scopes");
|
|
1724
1731
|
const scopes = Array.from(this.scopes.keys());
|
|
1725
1732
|
for (const scope of scopes) {
|
|
1726
1733
|
await this.remove(scope);
|
|
@@ -1729,13 +1736,13 @@ class SessionRegistry {
|
|
|
1729
1736
|
}
|
|
1730
1737
|
|
|
1731
1738
|
// src/index.ts
|
|
1732
|
-
var
|
|
1739
|
+
var logger14 = createModuleLogger("index");
|
|
1733
1740
|
async function startDiscordGateway(config) {
|
|
1734
1741
|
const resolvedConfig = resolveConfig(config);
|
|
1735
1742
|
const agentService = new AgentService(resolvedConfig);
|
|
1736
|
-
|
|
1743
|
+
logger14.info("initializing agent service");
|
|
1737
1744
|
await agentService.initialize();
|
|
1738
|
-
|
|
1745
|
+
logger14.info(agentService.getStatus(), "agent ready");
|
|
1739
1746
|
const authConfig = {
|
|
1740
1747
|
discordAllowedUserId: resolvedConfig.discordAllowedUserId,
|
|
1741
1748
|
discordAllowedForumChannelIds: resolvedConfig.discordAllowedForumChannelIds,
|
|
@@ -1763,7 +1770,7 @@ function createGatewayStopHandler(client, agentService, sessionRegistry, config)
|
|
|
1763
1770
|
return;
|
|
1764
1771
|
}
|
|
1765
1772
|
stopped = true;
|
|
1766
|
-
|
|
1773
|
+
logger14.info({
|
|
1767
1774
|
cwd: config.cwd,
|
|
1768
1775
|
agentDir: config.agentDir
|
|
1769
1776
|
}, "stopping discord gateway");
|
|
@@ -1774,9 +1781,9 @@ function createGatewayStopHandler(client, agentService, sessionRegistry, config)
|
|
|
1774
1781
|
}
|
|
1775
1782
|
function registerSignalHandlers(stop) {
|
|
1776
1783
|
const handleSignal = (signal) => {
|
|
1777
|
-
|
|
1784
|
+
logger14.info({ signal }, "received signal");
|
|
1778
1785
|
stop().finally(() => {
|
|
1779
|
-
|
|
1786
|
+
logger14.info("done");
|
|
1780
1787
|
process.exit(0);
|
|
1781
1788
|
});
|
|
1782
1789
|
};
|
|
@@ -2,7 +2,7 @@ import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { AgentService } from "./agent-service";
|
|
3
3
|
import { PromptQueue } from "./prompt-queue";
|
|
4
4
|
export type SessionScope = string;
|
|
5
|
-
export type
|
|
5
|
+
export type ScopedSessionEntry = {
|
|
6
6
|
session: AgentSession;
|
|
7
7
|
promptQueue: PromptQueue;
|
|
8
8
|
createdAt: Date;
|
|
@@ -19,11 +19,11 @@ export declare class SessionRegistry {
|
|
|
19
19
|
private readonly agentService;
|
|
20
20
|
constructor(agentService: AgentService);
|
|
21
21
|
getOrCreate(scope: SessionScope): Promise<{
|
|
22
|
-
entry:
|
|
22
|
+
entry: ScopedSessionEntry;
|
|
23
23
|
created: boolean;
|
|
24
24
|
}>;
|
|
25
25
|
remove(scope: SessionScope): Promise<void>;
|
|
26
|
-
get(scope: SessionScope):
|
|
26
|
+
get(scope: SessionScope): ScopedSessionEntry | undefined;
|
|
27
27
|
getScopes(): SessionScope[];
|
|
28
28
|
shutdownAll(): Promise<void>;
|
|
29
29
|
}
|