@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.
@@ -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,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 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,12 +462,12 @@ 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) {
@@ -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 = session.model ? `${session.model.provider}/${session.model.id}` : "(no model selected)";
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
- logger4.info({
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.ensureModelForSession(this.requireSession());
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 = readRequiredValue("discordAllowedUserId", config.discordAllowedUserId);
560
+ const discordAllowedUserId = requireNonEmptyConfigValue("discordAllowedUserId", config.discordAllowedUserId);
530
561
  return {
531
- discordBotToken: readRequiredValue("discordBotToken", config.discordBotToken),
562
+ discordBotToken: requireNonEmptyConfigValue("discordBotToken", config.discordBotToken),
532
563
  discordAllowedUserId,
533
- cwd: readRequiredValue("cwd", config.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 readRequiredValue(name, value) {
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 = getThinkingInfo(effectiveSession.session);
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: `Thinking level set to "${requestedLevel}".`
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 logger5 = createModuleLogger("discord-attachments");
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
- 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");
958
965
  continue;
959
966
  }
960
967
  if (attachment.size > MAX_TEXT_ATTACHMENT_SIZE_BYTES) {
961
- logger5.warn({
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
- logger5.info({
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
- logger5.warn({
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
- 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");
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
- logger5.warn({
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
- logger5.info({
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
- logger5.warn({
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
- 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");
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 logger6 = createModuleLogger("discord-replies");
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
- logger6.debug({ messageId: message.id, error }, "failed to add working reaction");
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
- logger6.debug({ messageId: message.id, error }, "failed to remove working reaction");
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
- logger6.debug({
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
- logger6.error({
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 logger7 = createModuleLogger("image-description");
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
- logger7.error({ error, mimeType }, "vision model prompt failed");
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
- logger7.debug({ textLength: text.length, mimeType }, "media described");
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 logger8 = createModuleLogger("discord-media-resolution");
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
- logger8.info({
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
- 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");
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
- logger8.warn({ visionModelId: config.visionModelId }, "vision model not found in registry");
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
- logger8.info({
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 logger9 = createModuleLogger("discord-typing");
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
- logger9.debug("[TYPING] STATUS UPDATED OK");
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
- 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`);
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
- logger9.info({ channelKey }, "[TYPING] retry done");
1353
+ logger10.info({ channelKey }, "[TYPING] retry done");
1347
1354
  return;
1348
1355
  }
1349
- logger9.warn({ channelKey, status: response.status }, "[TYPING] unexpected status");
1356
+ logger10.warn({ channelKey, status: response.status }, "[TYPING] unexpected status");
1350
1357
  } catch (error) {
1351
- logger9.warn({ channelKey, error }, "[TYPING] FAILED");
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
- logger9.debug({ channelKey, refs: existing.refs }, "[TYPING] ref++ (reusing existing interval)");
1365
+ logger10.debug({ channelKey, refs: existing.refs }, "[TYPING] ref++ (reusing existing interval)");
1359
1366
  return;
1360
1367
  }
1361
- logger9.debug("[TYPING] started new interval");
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
- logger9.debug({ channelKey }, "[TYPING] stop called but no entry found");
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
- logger9.debug("[TYPING] interval cleared (refs hit 0)");
1385
+ logger10.debug("[TYPING] interval cleared (refs hit 0)");
1379
1386
  return;
1380
1387
  }
1381
- logger9.debug("[TYPING] ref-- (interval still active)");
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 logger10 = createModuleLogger("discord-message-handler");
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
- logger10.debug("ignored bot message");
1460
+ logger11.debug("ignored bot message");
1454
1461
  return;
1455
1462
  }
1456
1463
  if (message.system) {
1457
- logger10.debug({ messageId: message.id }, "ignored system message");
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
- logger10.debug({
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
- logger10.debug({
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
- 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)");
1490
1497
  return;
1491
1498
  }
1492
- logger10.info({
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
- logger10.info({
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
- logger10.info({ scope }, "archiving thread");
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
- logger10.error({ error }, "failed to archive thread");
1537
+ logger11.error({ error }, "failed to archive thread");
1531
1538
  }
1532
1539
  await sessionRegistry.remove(scope);
1533
1540
  return;
1534
1541
  }
1535
- logger10.info({
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
- logger10.debug({ messageId: message.id }, "channel not sendable");
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 logger11 = createModuleLogger("discord-gateway");
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
- logger11.info({ userTag: readyClient.user.tag }, "logged in");
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
- logger11.info({
1608
+ logger12.info({
1602
1609
  userId: authConfig.discordAllowedUserId
1603
1610
  }, "sent startup dm");
1604
1611
  } catch (error) {
1605
- logger11.error({ error }, "failed to send startup dm");
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
- logger11.error({ error, direction: "IN" }, "message handling failed");
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
- logger11.info({ threadId: thread.id, scope }, "thread deleted");
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 logger12 = createModuleLogger("session-registry");
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
- logger12.debug({
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
- logger12.debug({ scope }, "removing scope");
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
- logger12.info({ count: this.scopes.size }, "shutting down all scopes");
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 logger13 = createModuleLogger("index");
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
- logger13.info("initializing agent service");
1743
+ logger14.info("initializing agent service");
1737
1744
  await agentService.initialize();
1738
- logger13.info(agentService.getStatus(), "agent ready");
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
- logger13.info({
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
- logger13.info({ signal }, "received signal");
1784
+ logger14.info({ signal }, "received signal");
1778
1785
  stop().finally(() => {
1779
- logger13.info("done");
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 ScopeEntry = {
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: ScopeEntry;
22
+ entry: ScopedSessionEntry;
23
23
  created: boolean;
24
24
  }>;
25
25
  remove(scope: SessionScope): Promise<void>;
26
- get(scope: SessionScope): ScopeEntry | undefined;
26
+ get(scope: SessionScope): ScopedSessionEntry | undefined;
27
27
  getScopes(): SessionScope[];
28
28
  shutdownAll(): Promise<void>;
29
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@friendlyrobot/discord-pi-agent",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
4
4
  "description": "Reusable Discord gateway for persistent pi agent sessions",
5
5
  "license": "MIT",
6
6
  "type": "module",