@friendlyrobot/discord-pi-agent 0.16.0 → 0.17.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 CHANGED
@@ -132,7 +132,6 @@ Pretty console logs use:
132
132
 
133
133
  - `discordAllowedForumChannelIds` — string array of forum channel IDs to respond in
134
134
  - `discordAllowedUserIds` — string array of allowed user IDs (defaults to `[discordAllowedUserId]`)
135
- - `sessionIdleTimeoutMs` — auto-shutdown idle thread sessions (null = never)
136
135
 
137
136
  ## Env helpers
138
137
 
@@ -149,7 +148,6 @@ Pretty console logs use:
149
148
  - `DISCORD_STARTUP_MESSAGE`
150
149
  - `DISCORD_FORUM_CHANNEL_IDS` — comma-separated forum channel IDs
151
150
  - `DISCORD_ALLOWED_USER_IDS` — comma-separated allowed user IDs
152
- - `DISCORD_SESSION_IDLE_TIMEOUT_MS` — idle timeout in ms
153
151
 
154
152
  If `PI_AGENT_CWD` is missing it falls back to `process.cwd()`.
155
153
  Set `DISCORD_STARTUP_MESSAGE=false` to disable the startup DM.
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import { type AgentSession } from "@earendil-works/pi-coding-agent";
2
- import type { Model } from "@earendil-works/pi-ai";
3
- import type { AgentStatus, ResolvedDiscordGatewayConfig, ThinkingLevel } from "./types";
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,31 +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 applyConfiguredThinkingLevel;
37
- private applyConfiguredThinkingLevelForSession;
38
- listModels(session?: AgentSession | null): Promise<string>;
39
- switchModel(provider: string, modelId: string, session?: AgentSession | null): Promise<string>;
40
- getCurrentModelDisplay(session?: AgentSession | null): string;
41
- getThinkingLevel(): {
42
- current: ThinkingLevel;
43
- available: ThinkingLevel[];
44
- supported: boolean;
45
- };
46
- setThinkingLevel(level: ThinkingLevel): string;
47
33
  private getSessionDir;
48
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 logger2 = createModuleLogger("markdown-table-transformer");
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
- logger2.error({
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 logger3 = createModuleLogger("reply-buffer");
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
- logger3.debug({
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
- logger3.debug({
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
- logger3.debug("agent end");
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 logger4 = createModuleLogger("agent-service");
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
- logger4.info({
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
- logger4.info({
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
- logger4.debug({ sessionId: session.sessionId }, "temporary session created");
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,70 +462,18 @@ class AgentService {
267
462
  sessionManager: SessionManager.continueRecent(this.config.cwd, sessionDir),
268
463
  thinkingLevel: this.config.thinkingLevel
269
464
  });
270
- logger4.debug({
465
+ logger5.debug({
271
466
  sessionDir,
272
467
  sessionId: session.sessionId,
273
468
  sessionFile: session.sessionFile
274
469
  }, "scoped session created");
275
- await this.ensureModelForSession(session);
470
+ await this.models.ensureSessionHasConfiguredModel(session);
276
471
  return session;
277
472
  }
278
473
  async prompt(text) {
279
474
  const session = this.requireSession();
280
475
  const transformedPrompt = await this.config.promptTransform(text);
281
- return runPromptAndCollectReply(session, transformedPrompt, {
282
- logPrefix: `[agent:${session.sessionId}]`
283
- });
284
- }
285
- getSkillsSummary() {
286
- const result = this.resourceLoader.getSkills();
287
- const { skills } = result;
288
- if (skills.length === 0) {
289
- return "Skills: (none loaded)";
290
- }
291
- const names = skills.map((s) => s.name);
292
- return `Skills (${skills.length}): ${names.join(", ") || "(none)"}`;
293
- }
294
- async reloadResources() {
295
- await this.resourceLoader.reload();
296
- const extensions = this.resourceLoader.getExtensions().extensions.map((ext) => ext.path);
297
- const skills = this.resourceLoader.getSkills();
298
- const skillNames = skills.skills.map((s) => s.name);
299
- const agentsFiles = this.resourceLoader.getAgentsFiles().agentsFiles.map((f) => f.path);
300
- return [
301
- "Resources reloaded.",
302
- `Extensions (${extensions.length}): ${extensions.join(", ") || "(none)"}`,
303
- `Skills (${skills.skills.length}): ${skillNames.join(", ") || "(none)"}`,
304
- `AGENTS.md files (${agentsFiles.length}): ${agentsFiles.join(", ") || "(none)"}`
305
- ].join(`
306
- `);
307
- }
308
- getExtensionsSummary() {
309
- const result = this.resourceLoader.getExtensions();
310
- const { extensions, errors } = result;
311
- if (extensions.length === 0) {
312
- return "Extensions: (none loaded)";
313
- }
314
- const lines = extensions.map((ext) => {
315
- const toolCount = ext.tools.size;
316
- const commandCount = ext.commands.size;
317
- const parts = [];
318
- if (toolCount > 0) {
319
- parts.push(`${toolCount} tool${toolCount !== 1 ? "s" : ""}`);
320
- }
321
- if (commandCount > 0) {
322
- parts.push(`${commandCount} command${commandCount !== 1 ? "s" : ""}`);
323
- }
324
- const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
325
- return ` ${ext.path}${summary}`;
326
- });
327
- const header = `Extensions (${extensions.length}):`;
328
- const errorLines = errors.length > 0 ? [
329
- `Errors (${errors.length}):`,
330
- ...errors.map((e) => ` ${e.path}: ${e.error}`)
331
- ] : [];
332
- return [header, ...lines, ...errorLines].join(`
333
- `);
476
+ return runPromptAndCollectReply(session, transformedPrompt);
334
477
  }
335
478
  async compact() {
336
479
  const session = this.requireSession();
@@ -358,7 +501,7 @@ class AgentService {
358
501
  }
359
502
  getStatus() {
360
503
  const session = this.requireSession();
361
- const model = session.model ? `${session.model.provider}/${session.model.id}` : "(no model selected)";
504
+ const model = this.models.getCurrentModelDisplay(session);
362
505
  const contextUsage = session.getContextUsage();
363
506
  const thinkingInfo = session.supportsThinking() ? `thinking: ${session.thinkingLevel} (available: ${session.getAvailableThinkingLevels().join(", ")})` : "thinking: not supported";
364
507
  return {
@@ -390,38 +533,14 @@ class AgentService {
390
533
  thinkingLevel: this.config.thinkingLevel
391
534
  });
392
535
  this.session = session;
393
- logger4.info({
536
+ logger5.info({
394
537
  sessionId: session.sessionId,
395
538
  sessionFile: session.sessionFile,
396
539
  restoredModel: session.model ? `${session.model.provider}/${session.model.id}` : null
397
540
  }, "session ready");
398
541
  }
399
542
  async ensureConfiguredModel() {
400
- await this.ensureModelForSession(this.requireSession());
401
- }
402
- async ensureModelForSession(session) {
403
- if (session.model) {
404
- logger4.debug({
405
- model: `${session.model.provider}/${session.model.id}`
406
- }, "retaining existing session model");
407
- return;
408
- }
409
- const desiredModel = this.modelRegistry.find(this.config.modelProvider, this.config.modelId);
410
- const availableModels = await this.modelRegistry.getAvailable();
411
- logger4.debug({
412
- count: availableModels.length,
413
- matches: availableModels.filter((model) => {
414
- return model.provider === this.config.modelProvider;
415
- }).map((model) => `${model.provider}/${model.id}`)
416
- }, "available models");
417
- if (!desiredModel) {
418
- throw new Error(`Configured model not found: ${this.config.modelProvider}/${this.config.modelId}. Check your pi agent config and installed extensions.`);
419
- }
420
- logger4.info({
421
- to: `${desiredModel.provider}/${desiredModel.id}`
422
- }, "setting initial session model");
423
- await session.setModel(desiredModel);
424
- await this.applyConfiguredThinkingLevelForSession(session);
543
+ await this.models.ensureSessionHasConfiguredModel(this.requireSession());
425
544
  }
426
545
  requireSession() {
427
546
  if (!this.session) {
@@ -429,103 +548,10 @@ class AgentService {
429
548
  }
430
549
  return this.session;
431
550
  }
432
- async applyConfiguredThinkingLevel() {
433
- await this.applyConfiguredThinkingLevelForSession(this.requireSession());
434
- }
435
- async applyConfiguredThinkingLevelForSession(session) {
436
- if (session.supportsThinking()) {
437
- const available = session.getAvailableThinkingLevels();
438
- if (available.includes(this.config.thinkingLevel)) {
439
- session.setThinkingLevel(this.config.thinkingLevel);
440
- logger4.debug({
441
- level: this.config.thinkingLevel
442
- }, "thinking level applied");
443
- } else {
444
- logger4.debug({
445
- requested: this.config.thinkingLevel,
446
- available
447
- }, "thinking level not available for model");
448
- }
449
- }
450
- }
451
- async listModels(session) {
452
- const effectiveSession = session ?? this.session;
453
- const availableModels = await this.modelRegistry.getAvailable();
454
- const currentDisplay = effectiveSession?.model ? `${effectiveSession.model.provider}/${effectiveSession.model.id}` : null;
455
- const lines = availableModels.map((model) => {
456
- const display = `${model.provider}/${model.id}`;
457
- const marker = currentDisplay === display ? " (current)" : "";
458
- return ` ${display}${marker}`;
459
- });
460
- return [
461
- `Available models (${availableModels.length}):`,
462
- ...lines,
463
- `
464
- Usage: !model <provider/modelId> to switch.`
465
- ].join(`
466
- `);
467
- }
468
- async switchModel(provider, modelId, session) {
469
- const effectiveSession = session ?? this.requireSession();
470
- const model = this.modelRegistry.find(provider, modelId);
471
- if (!model) {
472
- const availableModels = await this.modelRegistry.getAvailable();
473
- const matches = availableModels.filter((m) => {
474
- return m.provider === provider;
475
- }).map((m) => `${m.provider}/${m.id}`);
476
- const hint = matches.length > 0 ? `
477
- Models from "${provider}": ${matches.join(", ")}` : `
478
- Use !model to see all available models.`;
479
- return `Model not found: ${provider}/${modelId}.${hint}`;
480
- }
481
- if (isSameModel(effectiveSession.model, model)) {
482
- return `Already using ${provider}/${modelId}.`;
483
- }
484
- await effectiveSession.setModel(model);
485
- await this.applyConfiguredThinkingLevelForSession(effectiveSession);
486
- const thinkingInfo = effectiveSession.supportsThinking() ? ` (thinking: ${effectiveSession.thinkingLevel})` : "";
487
- return `Switched to ${provider}/${modelId}${thinkingInfo}.`;
488
- }
489
- getCurrentModelDisplay(session) {
490
- const effectiveSession = session ?? this.session;
491
- if (!effectiveSession?.model) {
492
- return "(no model selected)";
493
- }
494
- return `${effectiveSession.model.provider}/${effectiveSession.model.id}`;
495
- }
496
- getThinkingLevel() {
497
- const session = this.requireSession();
498
- if (!session.supportsThinking()) {
499
- return { current: "off", available: [], supported: false };
500
- }
501
- return {
502
- current: session.thinkingLevel,
503
- available: session.getAvailableThinkingLevels(),
504
- supported: true
505
- };
506
- }
507
- setThinkingLevel(level) {
508
- const session = this.requireSession();
509
- if (!session.supportsThinking()) {
510
- return "Current model does not support reasoning/thinking.";
511
- }
512
- const available = session.getAvailableThinkingLevels();
513
- if (!available.includes(level)) {
514
- return `Invalid thinking level "${level}" for current model. Available: ${available.join(", ")}`;
515
- }
516
- session.setThinkingLevel(level);
517
- return `Thinking level set to "${level}".`;
518
- }
519
551
  getSessionDir() {
520
552
  return path.join(this.config.agentDir, "sessions");
521
553
  }
522
554
  }
523
- function isSameModel(currentModel, desiredModel) {
524
- if (!currentModel) {
525
- return false;
526
- }
527
- return currentModel.provider === desiredModel.provider && currentModel.id === desiredModel.id;
528
- }
529
555
 
530
556
  // src/config.ts
531
557
  import path2 from "node:path";
@@ -547,8 +573,7 @@ function resolveConfig(config) {
547
573
  shutdownOnSignals: config.shutdownOnSignals ?? true,
548
574
  visionModelId: config.visionModelId?.trim() || null,
549
575
  discordAllowedForumChannelIds: config.discordAllowedForumChannelIds ?? [],
550
- discordAllowedUserIds: config.discordAllowedUserIds ?? [discordAllowedUserId],
551
- sessionIdleTimeoutMs: config.sessionIdleTimeoutMs ?? null
576
+ discordAllowedUserIds: config.discordAllowedUserIds ?? [discordAllowedUserId]
552
577
  };
553
578
  }
554
579
  function loadDiscordGatewayConfigFromEnv(overrides = {}) {
@@ -568,8 +593,7 @@ function loadDiscordGatewayConfigFromEnv(overrides = {}) {
568
593
  shutdownOnSignals: overrides.shutdownOnSignals,
569
594
  visionModelId: overrides.visionModelId ?? process.env.PI_VISION_MODEL_ID,
570
595
  discordAllowedForumChannelIds: overrides.discordAllowedForumChannelIds ?? parseStringArrayFromEnv("DISCORD_FORUM_CHANNEL_IDS") ?? [],
571
- discordAllowedUserIds: overrides.discordAllowedUserIds ?? parseStringArrayFromEnv("DISCORD_ALLOWED_USER_IDS"),
572
- sessionIdleTimeoutMs: overrides.sessionIdleTimeoutMs ?? parseOptionalIntFromEnv("DISCORD_SESSION_IDLE_TIMEOUT_MS") ?? undefined
596
+ discordAllowedUserIds: overrides.discordAllowedUserIds ?? parseStringArrayFromEnv("DISCORD_ALLOWED_USER_IDS")
573
597
  });
574
598
  }
575
599
  function readRequiredValue(name, value) {
@@ -618,14 +642,6 @@ function parseStringArrayFromEnv(key) {
618
642
  }
619
643
  return value.split(",").map((id) => id.trim()).filter(Boolean);
620
644
  }
621
- function parseOptionalIntFromEnv(key) {
622
- const value = process.env[key];
623
- if (!value) {
624
- return;
625
- }
626
- const parsed = parseInt(value, 10);
627
- return Number.isNaN(parsed) ? undefined : parsed;
628
- }
629
645
 
630
646
  // src/discord-gateway-client.ts
631
647
  import {
@@ -665,16 +681,6 @@ function getSessionStatusText(session, promptQueue, extras) {
665
681
  return lines.join(`
666
682
  `);
667
683
  }
668
- function getThinkingInfo(session) {
669
- if (!session.supportsThinking()) {
670
- return { current: "off", available: [], supported: false };
671
- }
672
- return {
673
- current: session.thinkingLevel,
674
- available: session.getAvailableThinkingLevels(),
675
- supported: true
676
- };
677
- }
678
684
  function getEffectiveSession(context) {
679
685
  return context.session ?? context.agentService.getSession();
680
686
  }
@@ -736,8 +742,8 @@ async function handleStatusCommand(trimmedInput, context) {
736
742
  return effectiveSession;
737
743
  }
738
744
  const tools = effectiveSession.session.getAllTools();
739
- const extensionsSummary = context.agentService.getExtensionsSummary();
740
- const skillsSummary = context.agentService.getSkillsSummary();
745
+ const extensionsSummary = context.agentService.resources.getExtensionsSummary();
746
+ const skillsSummary = context.agentService.resources.getSkillsSummary();
741
747
  return {
742
748
  handled: true,
743
749
  response: getSessionStatusText(effectiveSession.session, context.promptQueue, {
@@ -757,7 +763,7 @@ async function handleThinkingCommand(trimmedInput, context) {
757
763
  }
758
764
  const parts = trimmedInput.split(" ");
759
765
  if (parts.length === 1) {
760
- const info = getThinkingInfo(effectiveSession.session);
766
+ const info = context.agentService.models.getThinkingLevel(effectiveSession.session);
761
767
  if (!info.supported) {
762
768
  return {
763
769
  handled: true,
@@ -775,23 +781,9 @@ async function handleThinkingCommand(trimmedInput, context) {
775
781
  };
776
782
  }
777
783
  const requestedLevel = parts[1];
778
- if (!effectiveSession.session.supportsThinking()) {
779
- return {
780
- handled: true,
781
- response: "Current model does not support reasoning/thinking."
782
- };
783
- }
784
- const available = effectiveSession.session.getAvailableThinkingLevels();
785
- if (!available.includes(requestedLevel)) {
786
- return {
787
- handled: true,
788
- response: `Invalid thinking level "${requestedLevel}" for current model. Available: ${available.join(", ")}`
789
- };
790
- }
791
- effectiveSession.session.setThinkingLevel(requestedLevel);
792
784
  return {
793
785
  handled: true,
794
- response: `Thinking level set to "${requestedLevel}".`
786
+ response: context.agentService.models.setThinkingLevel(effectiveSession.session, requestedLevel)
795
787
  };
796
788
  }
797
789
  async function handleModelCommand(trimmedInput, context) {
@@ -804,8 +796,8 @@ async function handleModelCommand(trimmedInput, context) {
804
796
  }
805
797
  const parts = trimmedInput.split(" ");
806
798
  if (parts.length === 1) {
807
- const current = context.agentService.getCurrentModelDisplay(effectiveSession.session);
808
- 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);
809
801
  return {
810
802
  handled: true,
811
803
  response: `Current model: ${current}
@@ -827,7 +819,7 @@ ${modelList}`
827
819
  const modelId = argument.substring(slashIndex + 1).trim();
828
820
  return {
829
821
  handled: true,
830
- response: await context.agentService.switchModel(provider, modelId, effectiveSession.session)
822
+ response: await context.agentService.models.switchModel(provider, modelId, effectiveSession.session)
831
823
  };
832
824
  }
833
825
  async function handleCompactCommand(trimmedInput, context) {
@@ -853,7 +845,7 @@ async function handleReloadCommand(trimmedInput, context) {
853
845
  return {
854
846
  handled: true,
855
847
  response: await context.promptQueue.enqueue(async () => {
856
- return context.agentService.reloadResources();
848
+ return context.agentService.resources.reloadResources();
857
849
  })
858
850
  };
859
851
  }
@@ -907,7 +899,7 @@ async function executeCommand(input, context) {
907
899
  }
908
900
 
909
901
  // src/discord-attachments.ts
910
- var logger5 = createModuleLogger("discord-attachments");
902
+ var logger6 = createModuleLogger("discord-attachments");
911
903
  var TEXT_ATTACHMENT_EXTENSIONS = [
912
904
  ".txt",
913
905
  ".md",
@@ -969,11 +961,11 @@ async function readTextAttachments(message) {
969
961
  const results = [];
970
962
  for (const [, attachment] of attachments) {
971
963
  if (!isSupportedTextAttachment(attachment)) {
972
- logger5.debug({ messageId: message.id, filename: attachment.name }, "skipping non-text attachment");
964
+ logger6.debug({ messageId: message.id, filename: attachment.name }, "skipping non-text attachment");
973
965
  continue;
974
966
  }
975
967
  if (attachment.size > MAX_TEXT_ATTACHMENT_SIZE_BYTES) {
976
- logger5.warn({
968
+ logger6.warn({
977
969
  messageId: message.id,
978
970
  filename: attachment.name,
979
971
  size: attachment.size
@@ -981,14 +973,14 @@ async function readTextAttachments(message) {
981
973
  continue;
982
974
  }
983
975
  try {
984
- logger5.info({
976
+ logger6.info({
985
977
  messageId: message.id,
986
978
  filename: attachment.name,
987
979
  size: attachment.size
988
980
  }, "fetching attachment");
989
981
  const response = await fetch(attachment.url);
990
982
  if (!response.ok) {
991
- logger5.warn({
983
+ logger6.warn({
992
984
  messageId: message.id,
993
985
  filename: attachment.name,
994
986
  status: response.status
@@ -998,7 +990,7 @@ async function readTextAttachments(message) {
998
990
  const content = await response.text();
999
991
  results.push({ filename: attachment.name, content });
1000
992
  } catch (error) {
1001
- logger5.error({ messageId: message.id, filename: attachment.name, error }, "error fetching attachment");
993
+ logger6.error({ messageId: message.id, filename: attachment.name, error }, "error fetching attachment");
1002
994
  }
1003
995
  }
1004
996
  return results;
@@ -1014,7 +1006,7 @@ async function readMediaAttachments(message) {
1014
1006
  continue;
1015
1007
  }
1016
1008
  if (attachment.size > MAX_MEDIA_ATTACHMENT_SIZE_BYTES) {
1017
- logger5.warn({
1009
+ logger6.warn({
1018
1010
  messageId: message.id,
1019
1011
  filename: attachment.name,
1020
1012
  size: attachment.size
@@ -1022,14 +1014,14 @@ async function readMediaAttachments(message) {
1022
1014
  continue;
1023
1015
  }
1024
1016
  try {
1025
- logger5.info({
1017
+ logger6.info({
1026
1018
  messageId: message.id,
1027
1019
  filename: attachment.name,
1028
1020
  size: attachment.size
1029
1021
  }, "fetching media attachment");
1030
1022
  const response = await fetch(attachment.url);
1031
1023
  if (!response.ok) {
1032
- logger5.warn({
1024
+ logger6.warn({
1033
1025
  messageId: message.id,
1034
1026
  filename: attachment.name,
1035
1027
  status: response.status
@@ -1043,7 +1035,7 @@ async function readMediaAttachments(message) {
1043
1035
  mimeType: attachment.contentType ?? "application/octet-stream"
1044
1036
  });
1045
1037
  } catch (error) {
1046
- logger5.error({ messageId: message.id, filename: attachment.name, error }, "error fetching media attachment");
1038
+ logger6.error({ messageId: message.id, filename: attachment.name, error }, "error fetching media attachment");
1047
1039
  }
1048
1040
  }
1049
1041
  return results;
@@ -1113,13 +1105,13 @@ function chunkMessage(text, maxChunkSize = SAFE_MESSAGE_LIMIT) {
1113
1105
  }
1114
1106
 
1115
1107
  // src/discord-replies.ts
1116
- var logger6 = createModuleLogger("discord-replies");
1108
+ var logger7 = createModuleLogger("discord-replies");
1117
1109
  var WORKING_EMOJI = "⚙️";
1118
1110
  async function addWorkingReaction(message) {
1119
1111
  try {
1120
1112
  await message.react(WORKING_EMOJI);
1121
1113
  } catch (error) {
1122
- logger6.debug({ messageId: message.id, error }, "failed to add working reaction");
1114
+ logger7.debug({ messageId: message.id, error }, "failed to add working reaction");
1123
1115
  }
1124
1116
  }
1125
1117
  async function removeWorkingReaction(message) {
@@ -1129,13 +1121,13 @@ async function removeWorkingReaction(message) {
1129
1121
  await reaction.users.remove(message.client.user);
1130
1122
  }
1131
1123
  } catch (error) {
1132
- logger6.debug({ messageId: message.id, error }, "failed to remove working reaction");
1124
+ logger7.debug({ messageId: message.id, error }, "failed to remove working reaction");
1133
1125
  }
1134
1126
  }
1135
1127
  async function sendReply(message, text) {
1136
1128
  const channel = message.channel;
1137
1129
  if (!channel.isSendable()) {
1138
- logger6.debug({
1130
+ logger7.debug({
1139
1131
  messageId: message.id
1140
1132
  }, "reply skipped, channel not sendable");
1141
1133
  return;
@@ -1151,7 +1143,7 @@ async function sendReply(message, text) {
1151
1143
  await channel.send(chunk);
1152
1144
  }
1153
1145
  } catch (error) {
1154
- logger6.error({
1146
+ logger7.error({
1155
1147
  messageId: message.id,
1156
1148
  error
1157
1149
  }, "send reply failed");
@@ -1159,7 +1151,7 @@ async function sendReply(message, text) {
1159
1151
  }
1160
1152
 
1161
1153
  // src/image-description.ts
1162
- var logger7 = createModuleLogger("image-description");
1154
+ var logger8 = createModuleLogger("image-description");
1163
1155
  async function describeMediaAttachment(agentService, imageData, mimeType, userText, visionModel) {
1164
1156
  const session = await agentService.createTemporarySession();
1165
1157
  await session.setModel(visionModel);
@@ -1180,7 +1172,7 @@ async function describeMediaAttachment(agentService, imageData, mimeType, userTe
1180
1172
  await session.prompt(promptText, { images: [imageContent] });
1181
1173
  text = extractLastAssistantText(session);
1182
1174
  } catch (error) {
1183
- logger7.error({ error, mimeType }, "vision model prompt failed");
1175
+ logger8.error({ error, mimeType }, "vision model prompt failed");
1184
1176
  text = "(Vision model failed to process the file.)";
1185
1177
  } finally {
1186
1178
  session.dispose();
@@ -1188,7 +1180,7 @@ async function describeMediaAttachment(agentService, imageData, mimeType, userTe
1188
1180
  if (!text) {
1189
1181
  return "(Vision model returned no description.)";
1190
1182
  }
1191
- logger7.debug({ textLength: text.length, mimeType }, "media described");
1183
+ logger8.debug({ textLength: text.length, mimeType }, "media described");
1192
1184
  return text;
1193
1185
  }
1194
1186
  function extractLastAssistantText(session) {
@@ -1224,7 +1216,7 @@ function isAssistantMessage(msg) {
1224
1216
  }
1225
1217
 
1226
1218
  // src/discord-media-resolution.ts
1227
- var logger8 = createModuleLogger("discord-media-resolution");
1219
+ var logger9 = createModuleLogger("discord-media-resolution");
1228
1220
  function parseProviderModelId(value) {
1229
1221
  const trimmed = value.trim();
1230
1222
  if (!trimmed) {
@@ -1258,7 +1250,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
1258
1250
  const modelSupportsVision = currentModel?.input.includes("image") ?? false;
1259
1251
  if (modelSupportsVision) {
1260
1252
  const names = mediaAttachments.map((media) => media.filename).join(", ");
1261
- logger8.info({
1253
+ logger9.info({
1262
1254
  count: mediaAttachments.length,
1263
1255
  filenames: names,
1264
1256
  model: currentModel ? `${currentModel.provider}/${currentModel.id}` : "none"
@@ -1274,7 +1266,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
1274
1266
  }
1275
1267
  if (!config.visionModelId) {
1276
1268
  const names = mediaAttachments.map((media) => media.filename).join(", ");
1277
- logger8.info({ filenames: names }, "media attachments received but vision model not configured");
1269
+ logger9.info({ filenames: names }, "media attachments received but vision model not configured");
1278
1270
  const note = `
1279
1271
 
1280
1272
  [User sent media attachment(s): ${names}]
@@ -1288,9 +1280,9 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
1288
1280
  if (!parsedVisionModelId) {
1289
1281
  return { content, images: [] };
1290
1282
  }
1291
- const visionModel = agentService.findModel(parsedVisionModelId.provider, parsedVisionModelId.modelId);
1283
+ const visionModel = agentService.models.findModel(parsedVisionModelId.provider, parsedVisionModelId.modelId);
1292
1284
  if (!visionModel) {
1293
- logger8.warn({ visionModelId: config.visionModelId }, "vision model not found in registry");
1285
+ logger9.warn({ visionModelId: config.visionModelId }, "vision model not found in registry");
1294
1286
  const names = mediaAttachments.map((media) => media.filename).join(", ");
1295
1287
  const note = `
1296
1288
 
@@ -1301,7 +1293,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
1301
1293
  images: []
1302
1294
  };
1303
1295
  }
1304
- logger8.info({
1296
+ logger9.info({
1305
1297
  count: mediaAttachments.length,
1306
1298
  visionModel: `${visionModel.provider}/${visionModel.id}`
1307
1299
  }, "describing media with vision model");
@@ -1328,7 +1320,7 @@ ${content}` : descriptionPrefix,
1328
1320
  }
1329
1321
 
1330
1322
  // src/discord-typing.ts
1331
- var logger9 = createModuleLogger("discord-typing");
1323
+ var logger10 = createModuleLogger("discord-typing");
1332
1324
  var TYPING_INTERVAL_MS = 9000;
1333
1325
  var typingIntervals = new Map;
1334
1326
  async function sendTypingSafe(channel, channelKey) {
@@ -1340,7 +1332,7 @@ async function sendTypingSafe(channel, channelKey) {
1340
1332
  headers: { Authorization: `Bot ${token}` }
1341
1333
  });
1342
1334
  if (response.ok) {
1343
- logger9.debug("[TYPING] STATUS UPDATED OK");
1335
+ logger10.debug("[TYPING] STATUS UPDATED OK");
1344
1336
  return;
1345
1337
  }
1346
1338
  if (response.status === 429) {
@@ -1352,28 +1344,28 @@ async function sendTypingSafe(channel, channelKey) {
1352
1344
  retryMs = parsed.retry_after * 1000 + 500;
1353
1345
  }
1354
1346
  } catch {}
1355
- logger9.warn({ channelKey, retryMs, response: body }, `[TYPING] 429, retrying after ${retryMs}ms delay`);
1347
+ logger10.warn({ channelKey, retryMs, response: body }, `[TYPING] 429, retrying after ${retryMs}ms delay`);
1356
1348
  await new Promise((resolve) => setTimeout(resolve, retryMs));
1357
1349
  await fetch(url, {
1358
1350
  method: "POST",
1359
1351
  headers: { Authorization: `Bot ${token}` }
1360
1352
  });
1361
- logger9.info({ channelKey }, "[TYPING] retry done");
1353
+ logger10.info({ channelKey }, "[TYPING] retry done");
1362
1354
  return;
1363
1355
  }
1364
- logger9.warn({ channelKey, status: response.status }, "[TYPING] unexpected status");
1356
+ logger10.warn({ channelKey, status: response.status }, "[TYPING] unexpected status");
1365
1357
  } catch (error) {
1366
- logger9.warn({ channelKey, error }, "[TYPING] FAILED");
1358
+ logger10.warn({ channelKey, error }, "[TYPING] FAILED");
1367
1359
  }
1368
1360
  }
1369
1361
  function startTypingForChannel(channel, channelKey) {
1370
1362
  const existing = typingIntervals.get(channelKey);
1371
1363
  if (existing) {
1372
1364
  existing.refs += 1;
1373
- logger9.debug({ channelKey, refs: existing.refs }, "[TYPING] ref++ (reusing existing interval)");
1365
+ logger10.debug({ channelKey, refs: existing.refs }, "[TYPING] ref++ (reusing existing interval)");
1374
1366
  return;
1375
1367
  }
1376
- logger9.debug("[TYPING] started new interval");
1368
+ logger10.debug("[TYPING] started new interval");
1377
1369
  sendTypingSafe(channel, channelKey);
1378
1370
  const interval = setInterval(() => {
1379
1371
  sendTypingSafe(channel, channelKey);
@@ -1383,17 +1375,17 @@ function startTypingForChannel(channel, channelKey) {
1383
1375
  function stopTypingForChannel(channelKey) {
1384
1376
  const entry = typingIntervals.get(channelKey);
1385
1377
  if (!entry) {
1386
- logger9.debug({ channelKey }, "[TYPING] stop called but no entry found");
1378
+ logger10.debug({ channelKey }, "[TYPING] stop called but no entry found");
1387
1379
  return;
1388
1380
  }
1389
1381
  entry.refs -= 1;
1390
1382
  if (entry.refs <= 0) {
1391
1383
  clearInterval(entry.interval);
1392
1384
  typingIntervals.delete(channelKey);
1393
- logger9.debug("[TYPING] interval cleared (refs hit 0)");
1385
+ logger10.debug("[TYPING] interval cleared (refs hit 0)");
1394
1386
  return;
1395
1387
  }
1396
- logger9.debug("[TYPING] ref-- (interval still active)");
1388
+ logger10.debug("[TYPING] ref-- (interval still active)");
1397
1389
  }
1398
1390
 
1399
1391
  // src/prompt-context.ts
@@ -1445,7 +1437,7 @@ function normalizeContextValue(value) {
1445
1437
  }
1446
1438
 
1447
1439
  // src/discord-message-handler.ts
1448
- var logger10 = createModuleLogger("discord-message-handler");
1440
+ var logger11 = createModuleLogger("discord-message-handler");
1449
1441
  function buildDiscordPromptContent(message, scope, content, config) {
1450
1442
  const isThread = scope.startsWith("thread:") && message.channel.isThread();
1451
1443
  return buildDiscordMessageContextPrompt(content, {
@@ -1465,23 +1457,23 @@ function buildDiscordPromptContent(message, scope, content, config) {
1465
1457
  }
1466
1458
  async function handleDiscordMessage(message, config, agentService, sessionRegistry, authConfig) {
1467
1459
  if (message.author.bot) {
1468
- logger10.debug("ignored bot message");
1460
+ logger11.debug("ignored bot message");
1469
1461
  return;
1470
1462
  }
1471
1463
  if (message.system) {
1472
- logger10.debug({ messageId: message.id }, "ignored system message");
1464
+ logger11.debug({ messageId: message.id }, "ignored system message");
1473
1465
  return;
1474
1466
  }
1475
1467
  const scope = resolveMessageScope(message);
1476
1468
  if (scope === null) {
1477
- logger10.debug({
1469
+ logger11.debug({
1478
1470
  messageId: message.id,
1479
1471
  channelType: message.channel.type
1480
1472
  }, "unsupported channel type, ignoring");
1481
1473
  return;
1482
1474
  }
1483
1475
  if (!isAuthorizedMessage(message, scope, authConfig)) {
1484
- logger10.debug({
1476
+ logger11.debug({
1485
1477
  messageId: message.id,
1486
1478
  authorId: message.author.id,
1487
1479
  scope
@@ -1501,10 +1493,10 @@ ${attachment.content}`;
1501
1493
  }
1502
1494
  const mediaAttachments = await readMediaAttachments(message);
1503
1495
  if (!content && mediaAttachments.length === 0) {
1504
- logger10.debug({ messageId: message.id }, "ignored empty message (no text or images)");
1496
+ logger11.debug({ messageId: message.id }, "ignored empty message (no text or images)");
1505
1497
  return;
1506
1498
  }
1507
- logger10.info({
1499
+ logger11.info({
1508
1500
  direction: "IN",
1509
1501
  scope,
1510
1502
  messageId: message.id,
@@ -1519,7 +1511,7 @@ ${attachment.content}`;
1519
1511
  const { entry, created } = await sessionRegistry.getOrCreate(scope);
1520
1512
  const { session, promptQueue } = entry;
1521
1513
  if (created && scope.startsWith("thread:") && message.channel.isThread()) {
1522
- logger10.info({
1514
+ logger11.info({
1523
1515
  scope,
1524
1516
  threadName: message.channel.name
1525
1517
  }, "new thread session");
@@ -1532,7 +1524,7 @@ ${attachment.content}`;
1532
1524
  if (commandResult.handled) {
1533
1525
  stopTypingForChannel(channelKey);
1534
1526
  if (commandResult.archive && scope.startsWith("thread:")) {
1535
- logger10.info({ scope }, "archiving thread");
1527
+ logger11.info({ scope }, "archiving thread");
1536
1528
  const archiveChannel = message.channel;
1537
1529
  if (archiveChannel.isSendable()) {
1538
1530
  await archiveChannel.send(commandResult.response ?? "Archiving...");
@@ -1542,12 +1534,12 @@ ${attachment.content}`;
1542
1534
  await archiveChannel.setArchived(true);
1543
1535
  }
1544
1536
  } catch (error) {
1545
- logger10.error({ error }, "failed to archive thread");
1537
+ logger11.error({ error }, "failed to archive thread");
1546
1538
  }
1547
1539
  await sessionRegistry.remove(scope);
1548
1540
  return;
1549
1541
  }
1550
- logger10.info({
1542
+ logger11.info({
1551
1543
  messageId: message.id,
1552
1544
  command: content,
1553
1545
  hasResponse: Boolean(commandResult.response)
@@ -1559,7 +1551,7 @@ ${attachment.content}`;
1559
1551
  }
1560
1552
  if (!message.channel.isSendable()) {
1561
1553
  stopTypingForChannel(channelKey);
1562
- logger10.debug({ messageId: message.id }, "channel not sendable");
1554
+ logger11.debug({ messageId: message.id }, "channel not sendable");
1563
1555
  return;
1564
1556
  }
1565
1557
  await addWorkingReaction(message);
@@ -1582,7 +1574,6 @@ ${attachment.content}`;
1582
1574
  const wrappedContent = buildDiscordPromptContent(message, scope, promptContent, config);
1583
1575
  const transformedPrompt = await config.promptTransform(wrappedContent);
1584
1576
  return runPromptAndCollectReply(session, transformedPrompt, {
1585
- logPrefix: `[agent:${session.sessionId}]`,
1586
1577
  images: promptImages
1587
1578
  });
1588
1579
  });
@@ -1594,7 +1585,7 @@ ${attachment.content}`;
1594
1585
  }
1595
1586
 
1596
1587
  // src/discord-gateway-client.ts
1597
- var logger11 = createModuleLogger("discord-gateway");
1588
+ var logger12 = createModuleLogger("discord-gateway");
1598
1589
  async function startGatewayClient(config, agentService, sessionRegistry, authConfig) {
1599
1590
  const client = new Client({
1600
1591
  intents: [
@@ -1606,7 +1597,7 @@ async function startGatewayClient(config, agentService, sessionRegistry, authCon
1606
1597
  partials: [Partials.Channel]
1607
1598
  });
1608
1599
  client.once(Events.ClientReady, async (readyClient) => {
1609
- logger11.info({ userTag: readyClient.user.tag }, "logged in");
1600
+ logger12.info({ userTag: readyClient.user.tag }, "logged in");
1610
1601
  if (!authConfig.startupMessage) {
1611
1602
  return;
1612
1603
  }
@@ -1614,24 +1605,24 @@ async function startGatewayClient(config, agentService, sessionRegistry, authCon
1614
1605
  const user = await readyClient.users.fetch(authConfig.discordAllowedUserId);
1615
1606
  const dmChannel = await user.createDM();
1616
1607
  await dmChannel.send(authConfig.startupMessage);
1617
- logger11.info({
1608
+ logger12.info({
1618
1609
  userId: authConfig.discordAllowedUserId
1619
1610
  }, "sent startup dm");
1620
1611
  } catch (error) {
1621
- logger11.error({ error }, "failed to send startup dm");
1612
+ logger12.error({ error }, "failed to send startup dm");
1622
1613
  }
1623
1614
  });
1624
1615
  client.on(Events.MessageCreate, async (message) => {
1625
1616
  try {
1626
1617
  await handleDiscordMessage(message, config, agentService, sessionRegistry, authConfig);
1627
1618
  } catch (error) {
1628
- logger11.error({ error, direction: "IN" }, "message handling failed");
1619
+ logger12.error({ error, direction: "IN" }, "message handling failed");
1629
1620
  await sendReply(message, "The bot hit an error while handling that message.");
1630
1621
  }
1631
1622
  });
1632
1623
  client.on(Events.ThreadDelete, async (thread) => {
1633
1624
  const scope = `thread:${thread.id}`;
1634
- logger11.info({ threadId: thread.id, scope }, "thread deleted");
1625
+ logger12.info({ threadId: thread.id, scope }, "thread deleted");
1635
1626
  await sessionRegistry.remove(scope);
1636
1627
  });
1637
1628
  await client.login(config.discordBotToken);
@@ -1690,7 +1681,7 @@ function sessionDirForScope(agentDir, scope) {
1690
1681
  }
1691
1682
  throw new Error(`Unknown session scope: ${scope}`);
1692
1683
  }
1693
- var logger12 = createModuleLogger("session-registry");
1684
+ var logger13 = createModuleLogger("session-registry");
1694
1685
 
1695
1686
  class SessionRegistry {
1696
1687
  scopes = new Map;
@@ -1712,7 +1703,7 @@ class SessionRegistry {
1712
1703
  createdAt: new Date
1713
1704
  };
1714
1705
  this.scopes.set(scope, entry);
1715
- logger12.debug({
1706
+ logger13.debug({
1716
1707
  scope,
1717
1708
  sessionDir,
1718
1709
  sessionId: session.sessionId
@@ -1724,7 +1715,7 @@ class SessionRegistry {
1724
1715
  if (!entry) {
1725
1716
  return;
1726
1717
  }
1727
- logger12.debug({ scope }, "removing scope");
1718
+ logger13.debug({ scope }, "removing scope");
1728
1719
  await entry.session.abort();
1729
1720
  entry.session.dispose();
1730
1721
  this.scopes.delete(scope);
@@ -1736,7 +1727,7 @@ class SessionRegistry {
1736
1727
  return Array.from(this.scopes.keys());
1737
1728
  }
1738
1729
  async shutdownAll() {
1739
- logger12.info({ count: this.scopes.size }, "shutting down all scopes");
1730
+ logger13.info({ count: this.scopes.size }, "shutting down all scopes");
1740
1731
  const scopes = Array.from(this.scopes.keys());
1741
1732
  for (const scope of scopes) {
1742
1733
  await this.remove(scope);
@@ -1745,13 +1736,13 @@ class SessionRegistry {
1745
1736
  }
1746
1737
 
1747
1738
  // src/index.ts
1748
- var logger13 = createModuleLogger("index");
1739
+ var logger14 = createModuleLogger("index");
1749
1740
  async function startDiscordGateway(config) {
1750
1741
  const resolvedConfig = resolveConfig(config);
1751
1742
  const agentService = new AgentService(resolvedConfig);
1752
- logger13.info("initializing agent service");
1743
+ logger14.info("initializing agent service");
1753
1744
  await agentService.initialize();
1754
- logger13.info(agentService.getStatus(), "agent ready");
1745
+ logger14.info(agentService.getStatus(), "agent ready");
1755
1746
  const authConfig = {
1756
1747
  discordAllowedUserId: resolvedConfig.discordAllowedUserId,
1757
1748
  discordAllowedForumChannelIds: resolvedConfig.discordAllowedForumChannelIds,
@@ -1779,7 +1770,7 @@ function createGatewayStopHandler(client, agentService, sessionRegistry, config)
1779
1770
  return;
1780
1771
  }
1781
1772
  stopped = true;
1782
- logger13.info({
1773
+ logger14.info({
1783
1774
  cwd: config.cwd,
1784
1775
  agentDir: config.agentDir
1785
1776
  }, "stopping discord gateway");
@@ -1790,9 +1781,9 @@ function createGatewayStopHandler(client, agentService, sessionRegistry, config)
1790
1781
  }
1791
1782
  function registerSignalHandlers(stop) {
1792
1783
  const handleSignal = (signal) => {
1793
- logger13.info({ signal }, "received signal");
1784
+ logger14.info({ signal }, "received signal");
1794
1785
  stop().finally(() => {
1795
- logger13.info("done");
1786
+ logger14.info("done");
1796
1787
  process.exit(0);
1797
1788
  });
1798
1789
  };
@@ -1,7 +1,6 @@
1
1
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
2
  import type { ImageContent } from "@earendil-works/pi-ai";
3
3
  type CollectReplyOptions = {
4
- logPrefix?: string;
5
4
  images?: ImageContent[];
6
5
  };
7
6
  export declare function runPromptAndCollectReply(session: AgentSession, prompt: string, options?: CollectReplyOptions): Promise<string>;
package/dist/types.d.ts CHANGED
@@ -25,8 +25,6 @@ export type DiscordGatewayConfig = {
25
25
  discordAllowedForumChannelIds?: string[];
26
26
  /** Which users can interact in forum threads (defaults to [discordAllowedUserId]). */
27
27
  discordAllowedUserIds?: string[];
28
- /** Auto-shutdown idle thread sessions after this many ms. */
29
- sessionIdleTimeoutMs?: number;
30
28
  };
31
29
  export type ResolvedDiscordGatewayConfig = {
32
30
  discordBotToken: string;
@@ -45,7 +43,6 @@ export type ResolvedDiscordGatewayConfig = {
45
43
  visionModelId: string | null;
46
44
  discordAllowedForumChannelIds: string[];
47
45
  discordAllowedUserIds: string[];
48
- sessionIdleTimeoutMs: number | null;
49
46
  };
50
47
  export type ContextUsageStatus = {
51
48
  tokens: number | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@friendlyrobot/discord-pi-agent",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Reusable Discord gateway for persistent pi agent sessions",
5
5
  "license": "MIT",
6
6
  "type": "module",