@fickydev/pigent 0.1.5 → 0.1.7

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.
@@ -1,8 +1,9 @@
1
1
  import type { InboundMessage } from "../channels/types";
2
- import type { LoadedAgentConfig, LoadedConfig, ProfileConfig } from "../config/schemas";
3
2
  import type { Repositories } from "../db/repositories";
3
+ import type { AgentSessionRow } from "../db/schema";
4
4
  import { logger } from "../logging/logger";
5
5
  import { PiAgentRunner } from "../pi/PiAgentRunner";
6
+ import type { AgentRegistry } from "./AgentRegistry";
6
7
 
7
8
  export type AgentRunInput = {
8
9
  agentId: string;
@@ -10,15 +11,29 @@ export type AgentRunInput = {
10
11
  message: InboundMessage;
11
12
  };
12
13
 
14
+ type AgentRunResponse = {
15
+ text: string;
16
+ error?: string;
17
+ };
18
+
19
+ function sessionLockKey(input: AgentRunInput): string {
20
+ return [input.agentId, input.message.channel, input.message.chatId, input.message.threadId ?? ""].join(":");
21
+ }
22
+
13
23
  export class AgentRunner {
14
24
  private readonly piRunner = new PiAgentRunner();
25
+ private readonly sessionLocks = new Map<string, Promise<void>>();
15
26
 
16
27
  constructor(
17
- private readonly config: LoadedConfig,
28
+ private readonly registry: AgentRegistry,
18
29
  private readonly repositories: Repositories,
19
30
  ) {}
20
31
 
21
32
  async run(input: AgentRunInput): Promise<string> {
33
+ return this.withSessionLock(input, () => this.runUnlocked(input));
34
+ }
35
+
36
+ private async runUnlocked(input: AgentRunInput): Promise<string> {
22
37
  const session = await this.repositories.sessions.getOrCreate({
23
38
  agentId: input.agentId,
24
39
  channel: input.message.channel,
@@ -38,7 +53,20 @@ export class AgentRunner {
38
53
  rawJson: input.message.raw,
39
54
  });
40
55
 
41
- const response = await this.createResponse(input);
56
+ const chatInstructions = await this.chatInstructions(input);
57
+ const response = await this.createResponse(input, session, chatInstructions);
58
+
59
+ if (response.error) {
60
+ await this.repositories.messages.create({
61
+ agentId: input.agentId,
62
+ sessionId: session.id,
63
+ channel: input.message.channel,
64
+ direction: "internal",
65
+ chatId: input.message.chatId,
66
+ threadId: input.message.threadId,
67
+ content: response.error,
68
+ });
69
+ }
42
70
 
43
71
  await this.repositories.messages.create({
44
72
  agentId: input.agentId,
@@ -47,41 +75,70 @@ export class AgentRunner {
47
75
  direction: "outbound",
48
76
  chatId: input.message.chatId,
49
77
  threadId: input.message.threadId,
50
- content: response,
78
+ content: response.text,
51
79
  });
52
80
 
53
- return response;
81
+ return response.text;
54
82
  }
55
83
 
56
- private async createResponse(input: AgentRunInput): Promise<string> {
84
+ private async withSessionLock<T>(input: AgentRunInput, task: () => Promise<T>): Promise<T> {
85
+ const key = sessionLockKey(input);
86
+ const previous = this.sessionLocks.get(key) ?? Promise.resolve();
87
+ const run = previous.catch(() => undefined).then(task);
88
+ const tail = run.then(
89
+ () => undefined,
90
+ () => undefined,
91
+ );
92
+
93
+ this.sessionLocks.set(key, tail);
94
+
95
+ try {
96
+ return await run;
97
+ } finally {
98
+ if (this.sessionLocks.get(key) === tail) {
99
+ this.sessionLocks.delete(key);
100
+ }
101
+ }
102
+ }
103
+
104
+ private async createResponse(
105
+ input: AgentRunInput,
106
+ session: AgentSessionRow,
107
+ chatInstructions: string,
108
+ ): Promise<AgentRunResponse> {
57
109
  if (process.env.PIGENT_FAKE_AGENT === "1") {
58
- return this.fakeResponse(input.agentId, input.text);
110
+ return { text: this.fakeResponse(input.agentId, input.text) };
59
111
  }
60
112
 
61
- const agent = this.findAgent(input.agentId);
62
- if (!agent) return `Unknown agent: ${input.agentId}`;
113
+ const agent = this.registry.getAgent(input.agentId);
114
+ if (!agent) return { text: `Unknown agent: ${input.agentId}` };
63
115
 
64
116
  try {
65
- return await this.piRunner.run({
117
+ const text = await this.piRunner.run({
66
118
  agent,
67
- profile: this.findProfile(agent.profile),
68
- prompt: this.composePrompt(input),
119
+ profile: this.registry.getProfile(agent.profile),
120
+ session,
121
+ prompt: this.composePrompt(input, chatInstructions),
69
122
  });
123
+
124
+ return { text };
70
125
  } catch (error) {
126
+ const errorMessage = error instanceof Error ? error.message : String(error);
127
+
71
128
  logger.error("pi runner failed", {
72
129
  agentId: input.agentId,
73
- error: error instanceof Error ? error.message : String(error),
130
+ error: errorMessage,
74
131
  });
75
132
 
76
133
  if (process.env.PIGENT_FALLBACK_FAKE_AGENT === "1") {
77
- return this.fakeResponse(input.agentId, input.text);
134
+ return { text: this.fakeResponse(input.agentId, input.text), error: errorMessage };
78
135
  }
79
136
 
80
- return "Agent execution failed. Check daemon logs.";
137
+ return { text: "Agent failed to respond. Please try again later.", error: errorMessage };
81
138
  }
82
139
  }
83
140
 
84
- private composePrompt(input: AgentRunInput): string {
141
+ private composePrompt(input: AgentRunInput, chatInstructions: string): string {
85
142
  return [
86
143
  `[Channel]\n${input.message.channel}`,
87
144
  `[Chat]\n${input.message.chatId}`,
@@ -89,24 +146,24 @@ export class AgentRunner {
89
146
  input.message.senderName || input.message.senderId
90
147
  ? `[User]\n${input.message.senderName ?? input.message.senderId}`
91
148
  : null,
149
+ chatInstructions ? `[Chat Instructions]\n${chatInstructions}` : null,
92
150
  `[Message]\n${input.text}`,
93
151
  ]
94
152
  .filter(Boolean)
95
153
  .join("\n\n");
96
154
  }
97
155
 
98
- private fakeResponse(agentId: string, text: string): string {
99
- const agent = this.findAgent(agentId);
100
- const name = agent?.name ?? agentId;
156
+ private async chatInstructions(input: AgentRunInput): Promise<string> {
157
+ if (input.message.channel !== "telegram") return "";
101
158
 
102
- return `[${name}] fake runner received: ${text || "(empty message)"}`;
159
+ const chat = await this.repositories.telegram.findChat(input.message.chatId);
160
+ return chat?.instructions.trim() ?? "";
103
161
  }
104
162
 
105
- private findAgent(agentId: string): LoadedAgentConfig | null {
106
- return this.config.agents.find((candidate) => candidate.id === agentId) ?? null;
107
- }
163
+ private fakeResponse(agentId: string, text: string): string {
164
+ const agent = this.registry.getAgent(agentId);
165
+ const name = agent?.name ?? agentId;
108
166
 
109
- private findProfile(profileId: string): ProfileConfig | null {
110
- return this.config.profiles.find((candidate) => candidate.id === profileId) ?? null;
167
+ return `[${name}] fake runner received: ${text || "(empty message)"}`;
111
168
  }
112
169
  }
@@ -1,6 +1,7 @@
1
1
  import type { InboundMessage } from "../channels/types";
2
- import type { LoadedConfig } from "../config/schemas";
3
2
  import type { Repositories } from "../db/repositories";
3
+ import { isValidModelRef } from "../pi/PiModelResolver";
4
+ import type { AgentRegistry } from "./AgentRegistry";
4
5
 
5
6
  export type BotCommandResult =
6
7
  | {
@@ -11,9 +12,12 @@ export type BotCommandResult =
11
12
  handled: false;
12
13
  };
13
14
 
15
+ const THINKING_LEVELS = ["off", "low", "medium", "high"] as const;
16
+ type ThinkingLevel = (typeof THINKING_LEVELS)[number];
17
+
14
18
  export class BotCommandHandler {
15
19
  constructor(
16
- private readonly config: LoadedConfig,
20
+ private readonly registry: AgentRegistry,
17
21
  private readonly repositories: Repositories,
18
22
  ) {}
19
23
 
@@ -26,6 +30,10 @@ export class BotCommandHandler {
26
30
  return { handled: true, text: this.helpText() };
27
31
  case "/agents":
28
32
  return { handled: true, text: await this.agentsText(message) };
33
+ case "/model":
34
+ return { handled: true, text: await this.modelText(message) };
35
+ case "/thinking":
36
+ return { handled: true, text: await this.thinkingText(message) };
29
37
  default:
30
38
  return { handled: false };
31
39
  }
@@ -36,6 +44,12 @@ export class BotCommandHandler {
36
44
  "Pigent commands:",
37
45
  "/help - show this help",
38
46
  "/agents - list agents for this chat",
47
+ "/model - show model for this chat session",
48
+ "/model <provider/modelId> - set model for this chat session",
49
+ "/model default - clear model override for this chat session",
50
+ "/thinking - show thinking level for this chat session",
51
+ "/thinking <off|low|medium|high> - set thinking level for this chat session",
52
+ "/thinking default - clear thinking override for this chat session",
39
53
  "/agent <agentId> <message> - send message to specific agent",
40
54
  "@agentId <message> - send message to specific agent",
41
55
  ].join("\n");
@@ -48,7 +62,7 @@ export class BotCommandHandler {
48
62
  return "No agents configured for this chat.";
49
63
  }
50
64
 
51
- const allAgentIds = this.config.agents.map((agent) => agent.id);
65
+ const allAgentIds = this.registry.listAgents().map((agent) => agent.id);
52
66
  const allowedAgentIds = [];
53
67
 
54
68
  for (const agentId of allAgentIds) {
@@ -62,4 +76,102 @@ export class BotCommandHandler {
62
76
  `Allowed agents: ${allowedAgentIds.length > 0 ? allowedAgentIds.join(", ") : "none"}`,
63
77
  ].join("\n");
64
78
  }
79
+
80
+ private async modelText(message: InboundMessage): Promise<string> {
81
+ const sessionResult = await this.getDefaultSession(message);
82
+ if (!sessionResult.ok) return sessionResult.message;
83
+
84
+ const [, ...args] = message.text.trim().split(/\s+/);
85
+ const value = args.join(" ").trim();
86
+
87
+ if (!value) {
88
+ return [
89
+ `Agent: ${sessionResult.agentId}`,
90
+ `Session model: ${sessionResult.session.model ?? "default"}`,
91
+ "Use /model <provider/modelId> to set a session model.",
92
+ "Use /model default to clear the session model override.",
93
+ ].join("\n");
94
+ }
95
+
96
+ if (value === "default") {
97
+ const session = await this.repositories.sessions.updateModel(sessionResult.session.id, null);
98
+ return `Session model cleared for ${session.agentId}. Current: ${session.model ?? "default"}`;
99
+ }
100
+
101
+ if (!isValidModelRef(value)) {
102
+ return "Invalid model. Use provider/modelId, for example: anthropic/claude-opus-4-5";
103
+ }
104
+
105
+ const session = await this.repositories.sessions.updateModel(sessionResult.session.id, value);
106
+ return `Session model for ${session.agentId} set to ${session.model}.`;
107
+ }
108
+
109
+ private async thinkingText(message: InboundMessage): Promise<string> {
110
+ const sessionResult = await this.getDefaultSession(message);
111
+ if (!sessionResult.ok) return sessionResult.message;
112
+
113
+ const [, rawValue] = message.text.trim().split(/\s+/, 2);
114
+ const value = rawValue?.trim();
115
+
116
+ if (!value) {
117
+ return [
118
+ `Agent: ${sessionResult.agentId}`,
119
+ `Session thinking level: ${sessionResult.session.thinkingLevel ?? "default"}`,
120
+ "Use /thinking <off|low|medium|high> to set a session thinking level.",
121
+ "Use /thinking default to clear the session thinking override.",
122
+ ].join("\n");
123
+ }
124
+
125
+ if (value === "default") {
126
+ const session = await this.repositories.sessions.updateThinkingLevel(sessionResult.session.id, null);
127
+ return `Session thinking level cleared for ${session.agentId}. Current: ${session.thinkingLevel ?? "default"}`;
128
+ }
129
+
130
+ if (!isThinkingLevel(value)) {
131
+ return "Invalid thinking level. Use one of: off, low, medium, high, default";
132
+ }
133
+
134
+ const session = await this.repositories.sessions.updateThinkingLevel(sessionResult.session.id, value);
135
+ return `Session thinking level for ${session.agentId} set to ${session.thinkingLevel}.`;
136
+ }
137
+
138
+ private async getDefaultSession(message: InboundMessage) {
139
+ if (message.channel !== "telegram") {
140
+ return { ok: false as const, message: "Unsupported channel." };
141
+ }
142
+
143
+ const chat = await this.repositories.telegram.findChat(message.chatId);
144
+
145
+ if (!chat?.enabled) {
146
+ return { ok: false as const, message: "No enabled chat configuration found." };
147
+ }
148
+
149
+ const agentId = chat.defaultAgentId;
150
+
151
+ if (!agentId) {
152
+ return { ok: false as const, message: "No default agent configured for this chat." };
153
+ }
154
+
155
+ if (!this.registry.hasAgent(agentId)) {
156
+ return { ok: false as const, message: `Unknown agent: ${agentId}` };
157
+ }
158
+
159
+ if (!(await this.repositories.telegram.isAgentAllowed(message.chatId, agentId))) {
160
+ return { ok: false as const, message: `Agent is not allowed in this chat: ${agentId}` };
161
+ }
162
+
163
+ const session = await this.repositories.sessions.getOrCreate({
164
+ agentId,
165
+ channel: message.channel,
166
+ chatId: message.chatId,
167
+ threadId: message.threadId,
168
+ userId: message.senderId,
169
+ });
170
+
171
+ return { ok: true as const, agentId, session };
172
+ }
173
+ }
174
+
175
+ function isThinkingLevel(value: string): value is ThinkingLevel {
176
+ return THINKING_LEVELS.includes(value as ThinkingLevel);
65
177
  }
@@ -1,5 +1,6 @@
1
1
  import type { InboundMessage } from "../channels/types";
2
2
  import type { Repositories } from "../db/repositories";
3
+ import type { AgentRegistry } from "./AgentRegistry";
3
4
 
4
5
  export type RouteResult =
5
6
  | {
@@ -14,7 +15,10 @@ export type RouteResult =
14
15
  };
15
16
 
16
17
  export class MessageRouter {
17
- constructor(private readonly repositories: Repositories) {}
18
+ constructor(
19
+ private readonly repositories: Repositories,
20
+ private readonly registry: AgentRegistry,
21
+ ) {}
18
22
 
19
23
  async route(message: InboundMessage): Promise<RouteResult> {
20
24
  if (message.channel !== "telegram") {
@@ -54,8 +58,7 @@ export class MessageRouter {
54
58
  };
55
59
  }
56
60
 
57
- const agent = await this.repositories.agents.findById(agentId);
58
- if (!agent) {
61
+ if (!this.registry.hasAgent(agentId)) {
59
62
  return {
60
63
  ok: false,
61
64
  reason: "unknown_agent",
@@ -3,6 +3,8 @@ import type { TelegramGetMeResponse, TelegramGetUpdatesResponse, TelegramSendMes
3
3
  export type TelegramApiOptions = {
4
4
  token: string;
5
5
  baseUrl?: string;
6
+ maxAttempts?: number;
7
+ baseRetryDelayMs?: number;
6
8
  };
7
9
 
8
10
  export type GetUpdatesOptions = {
@@ -11,11 +13,24 @@ export type GetUpdatesOptions = {
11
13
  limit?: number;
12
14
  };
13
15
 
16
+ type TelegramApiResponse<T> = T & {
17
+ ok: boolean;
18
+ description?: string;
19
+ error_code?: number;
20
+ parameters?: {
21
+ retry_after?: number;
22
+ };
23
+ };
24
+
14
25
  export class TelegramApi {
15
26
  private readonly baseUrl: string;
27
+ private readonly maxAttempts: number;
28
+ private readonly baseRetryDelayMs: number;
16
29
 
17
30
  constructor(options: TelegramApiOptions) {
18
31
  this.baseUrl = options.baseUrl ?? `https://api.telegram.org/bot${options.token}`;
32
+ this.maxAttempts = options.maxAttempts ?? 3;
33
+ this.baseRetryDelayMs = options.baseRetryDelayMs ?? 500;
19
34
  }
20
35
 
21
36
  async getMe(): Promise<TelegramUser> {
@@ -44,24 +59,123 @@ export class TelegramApi {
44
59
  }
45
60
 
46
61
  private async request<T extends { ok: boolean; description?: string }>(method: string, body: unknown): Promise<T> {
47
- const response = await fetch(`${this.baseUrl}/${method}`, {
48
- method: "POST",
49
- headers: {
50
- "content-type": "application/json",
51
- },
52
- body: JSON.stringify(body),
53
- });
62
+ let lastError: unknown;
63
+
64
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt += 1) {
65
+ try {
66
+ return await this.requestOnce<T>(method, body);
67
+ } catch (error) {
68
+ lastError = error;
69
+
70
+ if (!isRetryableTelegramError(error) || attempt >= this.maxAttempts) {
71
+ throw error;
72
+ }
73
+
74
+ await sleep(retryDelayMs(error, attempt, this.baseRetryDelayMs));
75
+ }
76
+ }
77
+
78
+ throw lastError instanceof Error ? lastError : new Error(`telegram ${method} failed`);
79
+ }
80
+
81
+ private async requestOnce<T extends { ok: boolean; description?: string }>(method: string, body: unknown): Promise<T> {
82
+ let response: Response;
83
+
84
+ try {
85
+ response = await fetch(`${this.baseUrl}/${method}`, {
86
+ method: "POST",
87
+ headers: {
88
+ "content-type": "application/json",
89
+ },
90
+ body: JSON.stringify(body),
91
+ });
92
+ } catch (error) {
93
+ throw new TelegramApiError(method, `telegram ${method} failed: ${error instanceof Error ? error.message : String(error)}`, {
94
+ cause: error,
95
+ retryable: true,
96
+ });
97
+ }
98
+
99
+ const payload = await parseTelegramPayload<T>(response);
54
100
 
55
101
  if (!response.ok) {
56
- throw new Error(`telegram ${method} failed with HTTP ${response.status}`);
102
+ throw new TelegramApiError(method, `telegram ${method} failed with HTTP ${response.status}`, {
103
+ status: response.status,
104
+ description: payload?.description,
105
+ retryAfterSeconds: payload?.parameters?.retry_after,
106
+ retryable: isRetryableStatus(response.status),
107
+ });
57
108
  }
58
109
 
59
- const payload = (await response.json()) as T;
110
+ if (!payload?.ok) {
111
+ const status = payload?.error_code;
60
112
 
61
- if (!payload.ok) {
62
- throw new Error(`telegram ${method} failed: ${payload.description ?? "unknown error"}`);
113
+ throw new TelegramApiError(method, `telegram ${method} failed: ${payload?.description ?? "unknown error"}`, {
114
+ status,
115
+ description: payload?.description,
116
+ retryAfterSeconds: payload?.parameters?.retry_after,
117
+ retryable: status ? isRetryableStatus(status) : false,
118
+ });
63
119
  }
64
120
 
65
121
  return payload;
66
122
  }
67
123
  }
124
+
125
+ class TelegramApiError extends Error {
126
+ readonly method: string;
127
+ readonly status?: number;
128
+ readonly description?: string;
129
+ readonly retryAfterSeconds?: number;
130
+ readonly retryable: boolean;
131
+
132
+ constructor(
133
+ method: string,
134
+ message: string,
135
+ options: {
136
+ status?: number;
137
+ description?: string;
138
+ retryAfterSeconds?: number;
139
+ retryable: boolean;
140
+ cause?: unknown;
141
+ },
142
+ ) {
143
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
144
+ this.name = "TelegramApiError";
145
+ this.method = method;
146
+ this.status = options.status;
147
+ this.description = options.description;
148
+ this.retryAfterSeconds = options.retryAfterSeconds;
149
+ this.retryable = options.retryable;
150
+ }
151
+ }
152
+
153
+ async function parseTelegramPayload<T extends { ok: boolean; description?: string }>(
154
+ response: Response,
155
+ ): Promise<TelegramApiResponse<T> | null> {
156
+ try {
157
+ return (await response.json()) as TelegramApiResponse<T>;
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+
163
+ function isRetryableTelegramError(error: unknown): boolean {
164
+ return error instanceof TelegramApiError && error.retryable;
165
+ }
166
+
167
+ function isRetryableStatus(status: number): boolean {
168
+ return status === 429 || status >= 500;
169
+ }
170
+
171
+ function retryDelayMs(error: unknown, attempt: number, baseDelayMs: number): number {
172
+ if (error instanceof TelegramApiError && error.retryAfterSeconds) {
173
+ return error.retryAfterSeconds * 1000;
174
+ }
175
+
176
+ return baseDelayMs * 2 ** (attempt - 1);
177
+ }
178
+
179
+ function sleep(ms: number): Promise<void> {
180
+ return new Promise((resolve) => setTimeout(resolve, ms));
181
+ }
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  const relativeOrAbsolutePathSchema = z.string().min(1);
4
+ const modelReferenceSchema = z.string().min(1).regex(/^[^/\s]+\/.+$/);
5
+ const ThinkingLevelSchema = z.enum(["off", "low", "medium", "high"]);
4
6
 
5
7
  export const PermissionConfigSchema = z.object({
6
8
  canRunShell: z.boolean().default(false),
@@ -28,6 +30,8 @@ export const AgentConfigSchema = z.object({
28
30
  name: z.string().min(1),
29
31
  profile: z.string().min(1),
30
32
  workspace: relativeOrAbsolutePathSchema,
33
+ model: modelReferenceSchema.nullable().default(null),
34
+ thinkingLevel: ThinkingLevelSchema.nullable().default(null),
31
35
  systemPromptFile: relativeOrAbsolutePathSchema.optional(),
32
36
  skills: z.array(z.string()).default([]),
33
37
  extensions: z.array(z.string()).default([]),
@@ -48,7 +52,8 @@ export const AgentConfigSchema = z.object({
48
52
  export const ProfileConfigSchema = z.object({
49
53
  id: z.string().min(1).regex(/^[a-zA-Z0-9_-]+$/),
50
54
  name: z.string().min(1),
51
- model: z.string().min(1).nullable().default(null),
55
+ model: modelReferenceSchema.nullable().default(null),
56
+ thinkingLevel: ThinkingLevelSchema.nullable().default(null),
52
57
  instructions: z.string().default(""),
53
58
  defaultSkills: z.array(z.string()).default([]),
54
59
  defaultExtensions: z.array(z.string()).default([]),
@@ -1,3 +1,4 @@
1
+ import { AgentRegistry } from "../agents/AgentRegistry";
1
2
  import { AgentRunner } from "../agents/AgentRunner";
2
3
  import { BotCommandHandler } from "../agents/BotCommandHandler";
3
4
  import { MessageRouter } from "../agents/MessageRouter";
@@ -13,6 +14,7 @@ export class AgentDaemon {
13
14
  private running = false;
14
15
  private readonly adapters: ChannelAdapter[];
15
16
  private readonly commands: BotCommandHandler;
17
+ private readonly registry: AgentRegistry;
16
18
  private readonly router: MessageRouter;
17
19
  private readonly runner: AgentRunner;
18
20
 
@@ -21,9 +23,10 @@ export class AgentDaemon {
21
23
  private readonly repositories: Repositories,
22
24
  ) {
23
25
  this.adapters = createAdapters(repositories);
24
- this.commands = new BotCommandHandler(config, repositories);
25
- this.router = new MessageRouter(repositories);
26
- this.runner = new AgentRunner(config, repositories);
26
+ this.registry = new AgentRegistry(config, repositories);
27
+ this.commands = new BotCommandHandler(this.registry, repositories);
28
+ this.router = new MessageRouter(repositories, this.registry);
29
+ this.runner = new AgentRunner(this.registry, repositories);
27
30
  }
28
31
 
29
32
  static async create(): Promise<AgentDaemon> {
@@ -43,8 +46,8 @@ export class AgentDaemon {
43
46
  }
44
47
 
45
48
  logger.info("pigent daemon started", {
46
- agents: this.config.agents.length,
47
- profiles: this.config.profiles.length,
49
+ agents: this.registry.listAgents().length,
50
+ profiles: this.registry.listProfiles().length,
48
51
  telegramChats: this.config.telegramChats.length,
49
52
  adapters: this.adapters.map((adapter) => adapter.id),
50
53
  });
@@ -63,9 +66,7 @@ export class AgentDaemon {
63
66
  }
64
67
 
65
68
  private async syncConfiguredState(): Promise<void> {
66
- for (const agent of this.config.agents) {
67
- await this.repositories.agents.upsertLoadedAgent(agent);
68
- }
69
+ await this.registry.syncConfiguredAgents();
69
70
 
70
71
  for (const chat of this.config.telegramChats) {
71
72
  await this.repositories.telegram.upsertConfiguredChat(chat);
@@ -125,7 +126,7 @@ export class AgentDaemon {
125
126
  if (message.channel !== "telegram") return;
126
127
  if (process.env.PIGENT_AUTO_SETUP_CHATS !== "1") return;
127
128
 
128
- const defaultAgentId = process.env.PIGENT_AUTO_SETUP_DEFAULT_AGENT ?? this.config.agents[0]?.id;
129
+ const defaultAgentId = process.env.PIGENT_AUTO_SETUP_DEFAULT_AGENT ?? this.registry.defaultAgentId();
129
130
  if (!defaultAgentId) return;
130
131
 
131
132
  await this.repositories.telegram.autoConfigureChat(message, defaultAgentId);
@@ -149,7 +150,11 @@ function createAdapters(repositories: Repositories): ChannelAdapter[] {
149
150
  if (telegramToken) {
150
151
  adapters.push(
151
152
  new TelegramPollingAdapter({
152
- api: new TelegramApi({ token: telegramToken }),
153
+ api: new TelegramApi({
154
+ token: telegramToken,
155
+ maxAttempts: Number(process.env.TELEGRAM_API_MAX_ATTEMPTS ?? 3),
156
+ baseRetryDelayMs: Number(process.env.TELEGRAM_API_BASE_RETRY_DELAY_MS ?? 500),
157
+ }),
153
158
  runtimeKv: repositories.runtimeKv,
154
159
  pollTimeoutSeconds: Number(process.env.TELEGRAM_POLL_TIMEOUT_SECONDS ?? 30),
155
160
  pollIntervalMs: Number(process.env.TELEGRAM_POLL_INTERVAL_MS ?? 1000),