@fickydev/pigent 0.1.6 → 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.
- package/CHANGELOG.md +4 -0
- package/PLAN.md +44 -1
- package/TODO.md +22 -1
- package/agents/assistant/agent.yaml +2 -0
- package/drizzle/migrations/0001_session_model_overrides.sql +2 -0
- package/drizzle/migrations/meta/0001_snapshot.json +519 -0
- package/drizzle/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/profiles/assistant.yaml +1 -0
- package/src/agents/AgentRunner.ts +8 -2
- package/src/agents/BotCommandHandler.ts +112 -0
- package/src/channels/telegram/TelegramApi.ts +125 -11
- package/src/config/schemas.ts +6 -1
- package/src/daemon/AgentDaemon.ts +5 -1
- package/src/db/repositories/SessionRepository.ts +33 -0
- package/src/db/schema.ts +2 -0
- package/src/pi/PiAgentRunner.ts +6 -0
- package/src/pi/PiModelResolver.ts +76 -0
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
- Added safe Pi runner failure replies with internal error persistence.
|
|
40
40
|
- Added per-session locking to serialize Pi runs for the same agent/channel/chat/thread.
|
|
41
41
|
- Added Telegram chat instructions to Pi prompt composition.
|
|
42
|
+
- Added Telegram API retry/backoff for transient network, rate limit, and server errors.
|
|
43
|
+
- Planned model selection support across profile, agent, and Telegram chat override levels.
|
|
44
|
+
- Added profile and agent config-based Pi model and thinking level selection.
|
|
45
|
+
- Added session-scoped model and thinking level overrides with `/model` and `/thinking` Telegram commands.
|
|
42
46
|
- Kept daemon process alive after startup so CLI runs do not exit after `pigent ready`.
|
|
43
47
|
|
|
44
48
|
### Changed
|
package/PLAN.md
CHANGED
|
@@ -61,7 +61,8 @@ Profile defines reusable behavior shared by agents.
|
|
|
61
61
|
|
|
62
62
|
Profile may include:
|
|
63
63
|
|
|
64
|
-
- model preference
|
|
64
|
+
- model preference using `provider/modelId`
|
|
65
|
+
- thinking level preference (`off`, `low`, `medium`, `high`)
|
|
65
66
|
- base system prompt
|
|
66
67
|
- response style
|
|
67
68
|
- default skills
|
|
@@ -152,6 +153,8 @@ Each Telegram group/private chat can define:
|
|
|
152
153
|
- default agent
|
|
153
154
|
- allowed agents
|
|
154
155
|
- custom instructions
|
|
156
|
+
- per-agent model override via Telegram command
|
|
157
|
+
- per-agent thinking level override via Telegram command
|
|
155
158
|
- heartbeat settings
|
|
156
159
|
- enabled/disabled status
|
|
157
160
|
|
|
@@ -170,6 +173,41 @@ telegramChats:
|
|
|
170
173
|
This is engineering group. Be concise. Prefer TypeScript.
|
|
171
174
|
```
|
|
172
175
|
|
|
176
|
+
## Model Selection
|
|
177
|
+
|
|
178
|
+
Pigent should support model selection at multiple levels without exposing provider secrets to channel prompts.
|
|
179
|
+
|
|
180
|
+
Model identifiers use Pi SDK `provider/modelId` format, for example:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
anthropic/claude-opus-4-5
|
|
184
|
+
openai/gpt-4.1
|
|
185
|
+
ollama/qwen2.5-coder:7b
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Selection priority:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
session override > agent config > profile config > Pi default
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
A session is keyed by `agentId + channel + chatId + threadId`, so the same agent can use different models in private chats, group chats, and Telegram forum topics.
|
|
195
|
+
|
|
196
|
+
Config support should come first:
|
|
197
|
+
|
|
198
|
+
- `profiles/*.yaml` may define `model` and `thinkingLevel`
|
|
199
|
+
- `agents/*/agent.yaml` may override `model` and `thinkingLevel`
|
|
200
|
+
|
|
201
|
+
Telegram command support should persist session-scoped overrides after config support exists:
|
|
202
|
+
|
|
203
|
+
- `/model` shows current model for the default agent in the chat
|
|
204
|
+
- `/model <provider/modelId>` sets model for the default agent session in the current chat/thread
|
|
205
|
+
- `/model default` clears model override for the default agent session in the current chat/thread
|
|
206
|
+
- `/thinking <level>` sets thinking level for the default agent session in the current chat/thread
|
|
207
|
+
- `/thinking default` clears thinking override for the default agent session in the current chat/thread
|
|
208
|
+
|
|
209
|
+
The command handler should validate agent access and model availability through a Pi model resolver. It must not reveal API keys, auth values, raw environment variables, or provider secrets.
|
|
210
|
+
|
|
173
211
|
## Prompt Composition
|
|
174
212
|
|
|
175
213
|
For each request, compose context from:
|
|
@@ -195,6 +233,7 @@ Initial entities:
|
|
|
195
233
|
- telegram chat agents
|
|
196
234
|
- messages
|
|
197
235
|
- heartbeats
|
|
236
|
+
- session model/thinking overrides
|
|
198
237
|
- tasks/events later
|
|
199
238
|
|
|
200
239
|
SQLite first. Keep schema types portable so PostgreSQL migration is straightforward.
|
|
@@ -319,6 +358,8 @@ drizzle/
|
|
|
319
358
|
### Milestone 6: Pi Runner
|
|
320
359
|
|
|
321
360
|
- create Pi SDK sessions
|
|
361
|
+
- resolve profile/agent model preferences
|
|
362
|
+
- resolve chat model overrides
|
|
322
363
|
- send prompts
|
|
323
364
|
- return responses
|
|
324
365
|
- persist inbound/outbound messages
|
|
@@ -362,6 +403,8 @@ Add Hono only after daemon works. Use it for:
|
|
|
362
403
|
|
|
363
404
|
## Open Questions
|
|
364
405
|
|
|
406
|
+
- Should `/model <agentId> ...` support non-default agent sessions in the command itself?
|
|
407
|
+
- Should model availability errors block `/model` saves or save with a warning?
|
|
365
408
|
- Which Pi SDK session manager should be used for persistent sessions?
|
|
366
409
|
- How exactly should per-agent skills/extensions be loaded into Pi runtime?
|
|
367
410
|
- Should group instructions live in YAML, DB, or both?
|
package/TODO.md
CHANGED
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
|
|
40
40
|
## Config
|
|
41
41
|
|
|
42
|
+
- [x] Add profile-level `thinkingLevel` config
|
|
43
|
+
- [x] Add agent-level `model` override config
|
|
44
|
+
- [x] Add agent-level `thinkingLevel` override config
|
|
42
45
|
- [x] Define root config format
|
|
43
46
|
- [x] Define `AgentConfig`
|
|
44
47
|
- [x] Define `ProfileConfig`
|
|
@@ -66,6 +69,8 @@
|
|
|
66
69
|
- [x] Define `messages` table
|
|
67
70
|
- [x] Define `heartbeats` table
|
|
68
71
|
- [x] Define `runtime_kv` table for offsets and daemon state
|
|
72
|
+
- [x] Add `agent_sessions.model` for session model override
|
|
73
|
+
- [x] Add `agent_sessions.thinking_level` for session thinking override
|
|
69
74
|
- [x] Implement repositories
|
|
70
75
|
- [x] `AgentRepository`
|
|
71
76
|
- [x] `SessionRepository`
|
|
@@ -83,7 +88,7 @@
|
|
|
83
88
|
- [x] `getUpdates`
|
|
84
89
|
- [x] `sendMessage`
|
|
85
90
|
- [x] error handling
|
|
86
|
-
- [
|
|
91
|
+
- [x] retry/backoff
|
|
87
92
|
- [x] Implement `TelegramPollingAdapter`
|
|
88
93
|
- [x] polling loop
|
|
89
94
|
- [x] offset tracking
|
|
@@ -121,6 +126,12 @@
|
|
|
121
126
|
|
|
122
127
|
## Pi Integration
|
|
123
128
|
|
|
129
|
+
- [x] Confirm SDK model selection supports `createAgentSession({ model, thinkingLevel })`
|
|
130
|
+
- [x] Implement Pi model resolver for `provider/modelId`
|
|
131
|
+
- [x] Apply profile-level model selection
|
|
132
|
+
- [x] Apply agent-level model override
|
|
133
|
+
- [x] Apply session model override
|
|
134
|
+
- [x] Apply thinking level selection
|
|
124
135
|
- [x] Confirm SDK APIs needed for session creation
|
|
125
136
|
- [ ] Confirm persistent session manager approach
|
|
126
137
|
- [ ] Confirm per-agent system prompt injection
|
|
@@ -162,6 +173,14 @@
|
|
|
162
173
|
- [ ] `/sessions` list active sessions for chat
|
|
163
174
|
- [ ] `/reset-session <agentId>` clear chat session
|
|
164
175
|
- [ ] `/heartbeat status` show heartbeat state
|
|
176
|
+
- [x] `/model` show current model for default chat agent session
|
|
177
|
+
- [x] `/model <provider/modelId>` set model for default chat agent session
|
|
178
|
+
- [x] `/model default` clear model for default chat agent session
|
|
179
|
+
- [x] `/thinking` show thinking level for default chat agent session
|
|
180
|
+
- [x] `/thinking <level>` set thinking level for default chat agent session
|
|
181
|
+
- [x] `/thinking default` clear thinking level for default chat agent session
|
|
182
|
+
- [ ] `/model <agentId> <provider/modelId>` set model for an explicit agent session
|
|
183
|
+
- [ ] `/thinking <agentId> <level>` set thinking level for an explicit agent session
|
|
165
184
|
- [x] `/help` show bot commands
|
|
166
185
|
|
|
167
186
|
## Tests
|
|
@@ -171,6 +190,8 @@
|
|
|
171
190
|
- [ ] Unit test routing rules
|
|
172
191
|
- [ ] Unit test session key generation
|
|
173
192
|
- [ ] Unit test heartbeat `NOOP` behavior
|
|
193
|
+
- [ ] Unit test model selection priority
|
|
194
|
+
- [ ] Unit test Telegram `/model` command parsing
|
|
174
195
|
- [ ] Repository tests against temp SQLite DB
|
|
175
196
|
- [ ] Integration test fake Telegram update to fake Pi runner
|
|
176
197
|
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "6",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "a63274e2-bbd7-4003-864b-df9dc906e6c1",
|
|
5
|
+
"prevId": "e22a939b-7de8-4f8f-97d8-f01f58bd0f25",
|
|
6
|
+
"tables": {
|
|
7
|
+
"agent_sessions": {
|
|
8
|
+
"name": "agent_sessions",
|
|
9
|
+
"columns": {
|
|
10
|
+
"id": {
|
|
11
|
+
"name": "id",
|
|
12
|
+
"type": "text",
|
|
13
|
+
"primaryKey": true,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": false
|
|
16
|
+
},
|
|
17
|
+
"agent_id": {
|
|
18
|
+
"name": "agent_id",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"channel": {
|
|
25
|
+
"name": "channel",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true,
|
|
29
|
+
"autoincrement": false
|
|
30
|
+
},
|
|
31
|
+
"chat_id": {
|
|
32
|
+
"name": "chat_id",
|
|
33
|
+
"type": "text",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": true,
|
|
36
|
+
"autoincrement": false
|
|
37
|
+
},
|
|
38
|
+
"thread_id": {
|
|
39
|
+
"name": "thread_id",
|
|
40
|
+
"type": "text",
|
|
41
|
+
"primaryKey": false,
|
|
42
|
+
"notNull": false,
|
|
43
|
+
"autoincrement": false
|
|
44
|
+
},
|
|
45
|
+
"user_id": {
|
|
46
|
+
"name": "user_id",
|
|
47
|
+
"type": "text",
|
|
48
|
+
"primaryKey": false,
|
|
49
|
+
"notNull": false,
|
|
50
|
+
"autoincrement": false
|
|
51
|
+
},
|
|
52
|
+
"pi_session_id": {
|
|
53
|
+
"name": "pi_session_id",
|
|
54
|
+
"type": "text",
|
|
55
|
+
"primaryKey": false,
|
|
56
|
+
"notNull": false,
|
|
57
|
+
"autoincrement": false
|
|
58
|
+
},
|
|
59
|
+
"instructions_hash": {
|
|
60
|
+
"name": "instructions_hash",
|
|
61
|
+
"type": "text",
|
|
62
|
+
"primaryKey": false,
|
|
63
|
+
"notNull": false,
|
|
64
|
+
"autoincrement": false
|
|
65
|
+
},
|
|
66
|
+
"model": {
|
|
67
|
+
"name": "model",
|
|
68
|
+
"type": "text",
|
|
69
|
+
"primaryKey": false,
|
|
70
|
+
"notNull": false,
|
|
71
|
+
"autoincrement": false
|
|
72
|
+
},
|
|
73
|
+
"thinking_level": {
|
|
74
|
+
"name": "thinking_level",
|
|
75
|
+
"type": "text",
|
|
76
|
+
"primaryKey": false,
|
|
77
|
+
"notNull": false,
|
|
78
|
+
"autoincrement": false
|
|
79
|
+
},
|
|
80
|
+
"created_at": {
|
|
81
|
+
"name": "created_at",
|
|
82
|
+
"type": "integer",
|
|
83
|
+
"primaryKey": false,
|
|
84
|
+
"notNull": true,
|
|
85
|
+
"autoincrement": false
|
|
86
|
+
},
|
|
87
|
+
"updated_at": {
|
|
88
|
+
"name": "updated_at",
|
|
89
|
+
"type": "integer",
|
|
90
|
+
"primaryKey": false,
|
|
91
|
+
"notNull": true,
|
|
92
|
+
"autoincrement": false
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"indexes": {
|
|
96
|
+
"agent_sessions_key_unique": {
|
|
97
|
+
"name": "agent_sessions_key_unique",
|
|
98
|
+
"columns": [
|
|
99
|
+
"agent_id",
|
|
100
|
+
"channel",
|
|
101
|
+
"chat_id",
|
|
102
|
+
"thread_id"
|
|
103
|
+
],
|
|
104
|
+
"isUnique": true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"foreignKeys": {},
|
|
108
|
+
"compositePrimaryKeys": {},
|
|
109
|
+
"uniqueConstraints": {},
|
|
110
|
+
"checkConstraints": {}
|
|
111
|
+
},
|
|
112
|
+
"agents": {
|
|
113
|
+
"name": "agents",
|
|
114
|
+
"columns": {
|
|
115
|
+
"id": {
|
|
116
|
+
"name": "id",
|
|
117
|
+
"type": "text",
|
|
118
|
+
"primaryKey": true,
|
|
119
|
+
"notNull": true,
|
|
120
|
+
"autoincrement": false
|
|
121
|
+
},
|
|
122
|
+
"name": {
|
|
123
|
+
"name": "name",
|
|
124
|
+
"type": "text",
|
|
125
|
+
"primaryKey": false,
|
|
126
|
+
"notNull": true,
|
|
127
|
+
"autoincrement": false
|
|
128
|
+
},
|
|
129
|
+
"profile": {
|
|
130
|
+
"name": "profile",
|
|
131
|
+
"type": "text",
|
|
132
|
+
"primaryKey": false,
|
|
133
|
+
"notNull": true,
|
|
134
|
+
"autoincrement": false
|
|
135
|
+
},
|
|
136
|
+
"workspace": {
|
|
137
|
+
"name": "workspace",
|
|
138
|
+
"type": "text",
|
|
139
|
+
"primaryKey": false,
|
|
140
|
+
"notNull": true,
|
|
141
|
+
"autoincrement": false
|
|
142
|
+
},
|
|
143
|
+
"config_json": {
|
|
144
|
+
"name": "config_json",
|
|
145
|
+
"type": "text",
|
|
146
|
+
"primaryKey": false,
|
|
147
|
+
"notNull": true,
|
|
148
|
+
"autoincrement": false
|
|
149
|
+
},
|
|
150
|
+
"system_prompt": {
|
|
151
|
+
"name": "system_prompt",
|
|
152
|
+
"type": "text",
|
|
153
|
+
"primaryKey": false,
|
|
154
|
+
"notNull": true,
|
|
155
|
+
"autoincrement": false,
|
|
156
|
+
"default": "''"
|
|
157
|
+
},
|
|
158
|
+
"created_at": {
|
|
159
|
+
"name": "created_at",
|
|
160
|
+
"type": "integer",
|
|
161
|
+
"primaryKey": false,
|
|
162
|
+
"notNull": true,
|
|
163
|
+
"autoincrement": false
|
|
164
|
+
},
|
|
165
|
+
"updated_at": {
|
|
166
|
+
"name": "updated_at",
|
|
167
|
+
"type": "integer",
|
|
168
|
+
"primaryKey": false,
|
|
169
|
+
"notNull": true,
|
|
170
|
+
"autoincrement": false
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"indexes": {},
|
|
174
|
+
"foreignKeys": {},
|
|
175
|
+
"compositePrimaryKeys": {},
|
|
176
|
+
"uniqueConstraints": {},
|
|
177
|
+
"checkConstraints": {}
|
|
178
|
+
},
|
|
179
|
+
"heartbeats": {
|
|
180
|
+
"name": "heartbeats",
|
|
181
|
+
"columns": {
|
|
182
|
+
"id": {
|
|
183
|
+
"name": "id",
|
|
184
|
+
"type": "text",
|
|
185
|
+
"primaryKey": true,
|
|
186
|
+
"notNull": true,
|
|
187
|
+
"autoincrement": false
|
|
188
|
+
},
|
|
189
|
+
"agent_id": {
|
|
190
|
+
"name": "agent_id",
|
|
191
|
+
"type": "text",
|
|
192
|
+
"primaryKey": false,
|
|
193
|
+
"notNull": true,
|
|
194
|
+
"autoincrement": false
|
|
195
|
+
},
|
|
196
|
+
"session_id": {
|
|
197
|
+
"name": "session_id",
|
|
198
|
+
"type": "text",
|
|
199
|
+
"primaryKey": false,
|
|
200
|
+
"notNull": false,
|
|
201
|
+
"autoincrement": false
|
|
202
|
+
},
|
|
203
|
+
"status": {
|
|
204
|
+
"name": "status",
|
|
205
|
+
"type": "text",
|
|
206
|
+
"primaryKey": false,
|
|
207
|
+
"notNull": true,
|
|
208
|
+
"autoincrement": false
|
|
209
|
+
},
|
|
210
|
+
"prompt": {
|
|
211
|
+
"name": "prompt",
|
|
212
|
+
"type": "text",
|
|
213
|
+
"primaryKey": false,
|
|
214
|
+
"notNull": true,
|
|
215
|
+
"autoincrement": false
|
|
216
|
+
},
|
|
217
|
+
"result": {
|
|
218
|
+
"name": "result",
|
|
219
|
+
"type": "text",
|
|
220
|
+
"primaryKey": false,
|
|
221
|
+
"notNull": false,
|
|
222
|
+
"autoincrement": false
|
|
223
|
+
},
|
|
224
|
+
"error": {
|
|
225
|
+
"name": "error",
|
|
226
|
+
"type": "text",
|
|
227
|
+
"primaryKey": false,
|
|
228
|
+
"notNull": false,
|
|
229
|
+
"autoincrement": false
|
|
230
|
+
},
|
|
231
|
+
"started_at": {
|
|
232
|
+
"name": "started_at",
|
|
233
|
+
"type": "integer",
|
|
234
|
+
"primaryKey": false,
|
|
235
|
+
"notNull": false,
|
|
236
|
+
"autoincrement": false
|
|
237
|
+
},
|
|
238
|
+
"finished_at": {
|
|
239
|
+
"name": "finished_at",
|
|
240
|
+
"type": "integer",
|
|
241
|
+
"primaryKey": false,
|
|
242
|
+
"notNull": false,
|
|
243
|
+
"autoincrement": false
|
|
244
|
+
},
|
|
245
|
+
"created_at": {
|
|
246
|
+
"name": "created_at",
|
|
247
|
+
"type": "integer",
|
|
248
|
+
"primaryKey": false,
|
|
249
|
+
"notNull": true,
|
|
250
|
+
"autoincrement": false
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"indexes": {},
|
|
254
|
+
"foreignKeys": {},
|
|
255
|
+
"compositePrimaryKeys": {},
|
|
256
|
+
"uniqueConstraints": {},
|
|
257
|
+
"checkConstraints": {}
|
|
258
|
+
},
|
|
259
|
+
"messages": {
|
|
260
|
+
"name": "messages",
|
|
261
|
+
"columns": {
|
|
262
|
+
"id": {
|
|
263
|
+
"name": "id",
|
|
264
|
+
"type": "text",
|
|
265
|
+
"primaryKey": true,
|
|
266
|
+
"notNull": true,
|
|
267
|
+
"autoincrement": false
|
|
268
|
+
},
|
|
269
|
+
"agent_id": {
|
|
270
|
+
"name": "agent_id",
|
|
271
|
+
"type": "text",
|
|
272
|
+
"primaryKey": false,
|
|
273
|
+
"notNull": true,
|
|
274
|
+
"autoincrement": false
|
|
275
|
+
},
|
|
276
|
+
"session_id": {
|
|
277
|
+
"name": "session_id",
|
|
278
|
+
"type": "text",
|
|
279
|
+
"primaryKey": false,
|
|
280
|
+
"notNull": false,
|
|
281
|
+
"autoincrement": false
|
|
282
|
+
},
|
|
283
|
+
"channel": {
|
|
284
|
+
"name": "channel",
|
|
285
|
+
"type": "text",
|
|
286
|
+
"primaryKey": false,
|
|
287
|
+
"notNull": true,
|
|
288
|
+
"autoincrement": false
|
|
289
|
+
},
|
|
290
|
+
"direction": {
|
|
291
|
+
"name": "direction",
|
|
292
|
+
"type": "text",
|
|
293
|
+
"primaryKey": false,
|
|
294
|
+
"notNull": true,
|
|
295
|
+
"autoincrement": false
|
|
296
|
+
},
|
|
297
|
+
"sender_id": {
|
|
298
|
+
"name": "sender_id",
|
|
299
|
+
"type": "text",
|
|
300
|
+
"primaryKey": false,
|
|
301
|
+
"notNull": false,
|
|
302
|
+
"autoincrement": false
|
|
303
|
+
},
|
|
304
|
+
"chat_id": {
|
|
305
|
+
"name": "chat_id",
|
|
306
|
+
"type": "text",
|
|
307
|
+
"primaryKey": false,
|
|
308
|
+
"notNull": false,
|
|
309
|
+
"autoincrement": false
|
|
310
|
+
},
|
|
311
|
+
"thread_id": {
|
|
312
|
+
"name": "thread_id",
|
|
313
|
+
"type": "text",
|
|
314
|
+
"primaryKey": false,
|
|
315
|
+
"notNull": false,
|
|
316
|
+
"autoincrement": false
|
|
317
|
+
},
|
|
318
|
+
"content": {
|
|
319
|
+
"name": "content",
|
|
320
|
+
"type": "text",
|
|
321
|
+
"primaryKey": false,
|
|
322
|
+
"notNull": true,
|
|
323
|
+
"autoincrement": false
|
|
324
|
+
},
|
|
325
|
+
"raw_json": {
|
|
326
|
+
"name": "raw_json",
|
|
327
|
+
"type": "text",
|
|
328
|
+
"primaryKey": false,
|
|
329
|
+
"notNull": false,
|
|
330
|
+
"autoincrement": false
|
|
331
|
+
},
|
|
332
|
+
"created_at": {
|
|
333
|
+
"name": "created_at",
|
|
334
|
+
"type": "integer",
|
|
335
|
+
"primaryKey": false,
|
|
336
|
+
"notNull": true,
|
|
337
|
+
"autoincrement": false
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
"indexes": {},
|
|
341
|
+
"foreignKeys": {},
|
|
342
|
+
"compositePrimaryKeys": {},
|
|
343
|
+
"uniqueConstraints": {},
|
|
344
|
+
"checkConstraints": {}
|
|
345
|
+
},
|
|
346
|
+
"runtime_kv": {
|
|
347
|
+
"name": "runtime_kv",
|
|
348
|
+
"columns": {
|
|
349
|
+
"key": {
|
|
350
|
+
"name": "key",
|
|
351
|
+
"type": "text",
|
|
352
|
+
"primaryKey": true,
|
|
353
|
+
"notNull": true,
|
|
354
|
+
"autoincrement": false
|
|
355
|
+
},
|
|
356
|
+
"value": {
|
|
357
|
+
"name": "value",
|
|
358
|
+
"type": "text",
|
|
359
|
+
"primaryKey": false,
|
|
360
|
+
"notNull": true,
|
|
361
|
+
"autoincrement": false
|
|
362
|
+
},
|
|
363
|
+
"updated_at": {
|
|
364
|
+
"name": "updated_at",
|
|
365
|
+
"type": "integer",
|
|
366
|
+
"primaryKey": false,
|
|
367
|
+
"notNull": true,
|
|
368
|
+
"autoincrement": false
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
"indexes": {},
|
|
372
|
+
"foreignKeys": {},
|
|
373
|
+
"compositePrimaryKeys": {},
|
|
374
|
+
"uniqueConstraints": {},
|
|
375
|
+
"checkConstraints": {}
|
|
376
|
+
},
|
|
377
|
+
"telegram_chat_agents": {
|
|
378
|
+
"name": "telegram_chat_agents",
|
|
379
|
+
"columns": {
|
|
380
|
+
"id": {
|
|
381
|
+
"name": "id",
|
|
382
|
+
"type": "text",
|
|
383
|
+
"primaryKey": true,
|
|
384
|
+
"notNull": true,
|
|
385
|
+
"autoincrement": false
|
|
386
|
+
},
|
|
387
|
+
"chat_id": {
|
|
388
|
+
"name": "chat_id",
|
|
389
|
+
"type": "text",
|
|
390
|
+
"primaryKey": false,
|
|
391
|
+
"notNull": true,
|
|
392
|
+
"autoincrement": false
|
|
393
|
+
},
|
|
394
|
+
"agent_id": {
|
|
395
|
+
"name": "agent_id",
|
|
396
|
+
"type": "text",
|
|
397
|
+
"primaryKey": false,
|
|
398
|
+
"notNull": true,
|
|
399
|
+
"autoincrement": false
|
|
400
|
+
},
|
|
401
|
+
"enabled": {
|
|
402
|
+
"name": "enabled",
|
|
403
|
+
"type": "integer",
|
|
404
|
+
"primaryKey": false,
|
|
405
|
+
"notNull": true,
|
|
406
|
+
"autoincrement": false,
|
|
407
|
+
"default": true
|
|
408
|
+
},
|
|
409
|
+
"custom_instructions": {
|
|
410
|
+
"name": "custom_instructions",
|
|
411
|
+
"type": "text",
|
|
412
|
+
"primaryKey": false,
|
|
413
|
+
"notNull": true,
|
|
414
|
+
"autoincrement": false,
|
|
415
|
+
"default": "''"
|
|
416
|
+
},
|
|
417
|
+
"created_at": {
|
|
418
|
+
"name": "created_at",
|
|
419
|
+
"type": "integer",
|
|
420
|
+
"primaryKey": false,
|
|
421
|
+
"notNull": true,
|
|
422
|
+
"autoincrement": false
|
|
423
|
+
},
|
|
424
|
+
"updated_at": {
|
|
425
|
+
"name": "updated_at",
|
|
426
|
+
"type": "integer",
|
|
427
|
+
"primaryKey": false,
|
|
428
|
+
"notNull": true,
|
|
429
|
+
"autoincrement": false
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
"indexes": {
|
|
433
|
+
"telegram_chat_agents_chat_agent_unique": {
|
|
434
|
+
"name": "telegram_chat_agents_chat_agent_unique",
|
|
435
|
+
"columns": [
|
|
436
|
+
"chat_id",
|
|
437
|
+
"agent_id"
|
|
438
|
+
],
|
|
439
|
+
"isUnique": true
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
"foreignKeys": {},
|
|
443
|
+
"compositePrimaryKeys": {},
|
|
444
|
+
"uniqueConstraints": {},
|
|
445
|
+
"checkConstraints": {}
|
|
446
|
+
},
|
|
447
|
+
"telegram_chats": {
|
|
448
|
+
"name": "telegram_chats",
|
|
449
|
+
"columns": {
|
|
450
|
+
"chat_id": {
|
|
451
|
+
"name": "chat_id",
|
|
452
|
+
"type": "text",
|
|
453
|
+
"primaryKey": true,
|
|
454
|
+
"notNull": true,
|
|
455
|
+
"autoincrement": false
|
|
456
|
+
},
|
|
457
|
+
"title": {
|
|
458
|
+
"name": "title",
|
|
459
|
+
"type": "text",
|
|
460
|
+
"primaryKey": false,
|
|
461
|
+
"notNull": false,
|
|
462
|
+
"autoincrement": false
|
|
463
|
+
},
|
|
464
|
+
"default_agent_id": {
|
|
465
|
+
"name": "default_agent_id",
|
|
466
|
+
"type": "text",
|
|
467
|
+
"primaryKey": false,
|
|
468
|
+
"notNull": false,
|
|
469
|
+
"autoincrement": false
|
|
470
|
+
},
|
|
471
|
+
"instructions": {
|
|
472
|
+
"name": "instructions",
|
|
473
|
+
"type": "text",
|
|
474
|
+
"primaryKey": false,
|
|
475
|
+
"notNull": true,
|
|
476
|
+
"autoincrement": false,
|
|
477
|
+
"default": "''"
|
|
478
|
+
},
|
|
479
|
+
"enabled": {
|
|
480
|
+
"name": "enabled",
|
|
481
|
+
"type": "integer",
|
|
482
|
+
"primaryKey": false,
|
|
483
|
+
"notNull": true,
|
|
484
|
+
"autoincrement": false,
|
|
485
|
+
"default": true
|
|
486
|
+
},
|
|
487
|
+
"created_at": {
|
|
488
|
+
"name": "created_at",
|
|
489
|
+
"type": "integer",
|
|
490
|
+
"primaryKey": false,
|
|
491
|
+
"notNull": true,
|
|
492
|
+
"autoincrement": false
|
|
493
|
+
},
|
|
494
|
+
"updated_at": {
|
|
495
|
+
"name": "updated_at",
|
|
496
|
+
"type": "integer",
|
|
497
|
+
"primaryKey": false,
|
|
498
|
+
"notNull": true,
|
|
499
|
+
"autoincrement": false
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
"indexes": {},
|
|
503
|
+
"foreignKeys": {},
|
|
504
|
+
"compositePrimaryKeys": {},
|
|
505
|
+
"uniqueConstraints": {},
|
|
506
|
+
"checkConstraints": {}
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
"views": {},
|
|
510
|
+
"enums": {},
|
|
511
|
+
"_meta": {
|
|
512
|
+
"schemas": {},
|
|
513
|
+
"tables": {},
|
|
514
|
+
"columns": {}
|
|
515
|
+
},
|
|
516
|
+
"internal": {
|
|
517
|
+
"indexes": {}
|
|
518
|
+
}
|
|
519
|
+
}
|
package/package.json
CHANGED
package/profiles/assistant.yaml
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { InboundMessage } from "../channels/types";
|
|
2
2
|
import type { Repositories } from "../db/repositories";
|
|
3
|
+
import type { AgentSessionRow } from "../db/schema";
|
|
3
4
|
import { logger } from "../logging/logger";
|
|
4
5
|
import { PiAgentRunner } from "../pi/PiAgentRunner";
|
|
5
6
|
import type { AgentRegistry } from "./AgentRegistry";
|
|
@@ -53,7 +54,7 @@ export class AgentRunner {
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
const chatInstructions = await this.chatInstructions(input);
|
|
56
|
-
const response = await this.createResponse(input, chatInstructions);
|
|
57
|
+
const response = await this.createResponse(input, session, chatInstructions);
|
|
57
58
|
|
|
58
59
|
if (response.error) {
|
|
59
60
|
await this.repositories.messages.create({
|
|
@@ -100,7 +101,11 @@ export class AgentRunner {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
private async createResponse(
|
|
104
|
+
private async createResponse(
|
|
105
|
+
input: AgentRunInput,
|
|
106
|
+
session: AgentSessionRow,
|
|
107
|
+
chatInstructions: string,
|
|
108
|
+
): Promise<AgentRunResponse> {
|
|
104
109
|
if (process.env.PIGENT_FAKE_AGENT === "1") {
|
|
105
110
|
return { text: this.fakeResponse(input.agentId, input.text) };
|
|
106
111
|
}
|
|
@@ -112,6 +117,7 @@ export class AgentRunner {
|
|
|
112
117
|
const text = await this.piRunner.run({
|
|
113
118
|
agent,
|
|
114
119
|
profile: this.registry.getProfile(agent.profile),
|
|
120
|
+
session,
|
|
115
121
|
prompt: this.composePrompt(input, chatInstructions),
|
|
116
122
|
});
|
|
117
123
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { InboundMessage } from "../channels/types";
|
|
2
2
|
import type { Repositories } from "../db/repositories";
|
|
3
|
+
import { isValidModelRef } from "../pi/PiModelResolver";
|
|
3
4
|
import type { AgentRegistry } from "./AgentRegistry";
|
|
4
5
|
|
|
5
6
|
export type BotCommandResult =
|
|
@@ -11,6 +12,9 @@ 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
20
|
private readonly registry: AgentRegistry,
|
|
@@ -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");
|
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
110
|
+
if (!payload?.ok) {
|
|
111
|
+
const status = payload?.error_code;
|
|
60
112
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
package/src/config/schemas.ts
CHANGED
|
@@ -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:
|
|
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([]),
|
|
@@ -150,7 +150,11 @@ function createAdapters(repositories: Repositories): ChannelAdapter[] {
|
|
|
150
150
|
if (telegramToken) {
|
|
151
151
|
adapters.push(
|
|
152
152
|
new TelegramPollingAdapter({
|
|
153
|
-
api: new TelegramApi({
|
|
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
|
+
}),
|
|
154
158
|
runtimeKv: repositories.runtimeKv,
|
|
155
159
|
pollTimeoutSeconds: Number(process.env.TELEGRAM_POLL_TIMEOUT_SECONDS ?? 30),
|
|
156
160
|
pollIntervalMs: Number(process.env.TELEGRAM_POLL_INTERVAL_MS ?? 1000),
|
|
@@ -3,6 +3,8 @@ import { nanoid } from "nanoid";
|
|
|
3
3
|
import type { DbClient } from "../client";
|
|
4
4
|
import { agentSessions, type AgentSessionRow } from "../schema";
|
|
5
5
|
|
|
6
|
+
export type SessionThinkingLevel = "off" | "low" | "medium" | "high";
|
|
7
|
+
|
|
6
8
|
export type SessionKey = {
|
|
7
9
|
agentId: string;
|
|
8
10
|
channel: string;
|
|
@@ -38,6 +40,30 @@ export class SessionRepository {
|
|
|
38
40
|
return created;
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
async updateModel(id: string, model: string | null): Promise<AgentSessionRow> {
|
|
44
|
+
await this.db
|
|
45
|
+
.update(agentSessions)
|
|
46
|
+
.set({
|
|
47
|
+
model,
|
|
48
|
+
updatedAt: Date.now(),
|
|
49
|
+
})
|
|
50
|
+
.where(eq(agentSessions.id, id));
|
|
51
|
+
|
|
52
|
+
return this.requireById(id);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async updateThinkingLevel(id: string, thinkingLevel: SessionThinkingLevel | null): Promise<AgentSessionRow> {
|
|
56
|
+
await this.db
|
|
57
|
+
.update(agentSessions)
|
|
58
|
+
.set({
|
|
59
|
+
thinkingLevel,
|
|
60
|
+
updatedAt: Date.now(),
|
|
61
|
+
})
|
|
62
|
+
.where(eq(agentSessions.id, id));
|
|
63
|
+
|
|
64
|
+
return this.requireById(id);
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
async findById(id: string): Promise<AgentSessionRow | null> {
|
|
42
68
|
const row = await this.db.query.agentSessions.findFirst({
|
|
43
69
|
where: eq(agentSessions.id, id),
|
|
@@ -46,6 +72,13 @@ export class SessionRepository {
|
|
|
46
72
|
return row ?? null;
|
|
47
73
|
}
|
|
48
74
|
|
|
75
|
+
private async requireById(id: string): Promise<AgentSessionRow> {
|
|
76
|
+
const row = await this.findById(id);
|
|
77
|
+
if (!row) throw new Error(`session not found ${id}`);
|
|
78
|
+
|
|
79
|
+
return row;
|
|
80
|
+
}
|
|
81
|
+
|
|
49
82
|
private async findByKey(key: SessionKey): Promise<AgentSessionRow | null> {
|
|
50
83
|
const threadPredicate = key.threadId
|
|
51
84
|
? eq(agentSessions.threadId, key.threadId)
|
package/src/db/schema.ts
CHANGED
|
@@ -48,6 +48,8 @@ export const agentSessions = sqliteTable(
|
|
|
48
48
|
userId: text("user_id"),
|
|
49
49
|
piSessionId: text("pi_session_id"),
|
|
50
50
|
instructionsHash: text("instructions_hash"),
|
|
51
|
+
model: text("model"),
|
|
52
|
+
thinkingLevel: text("thinking_level", { enum: ["off", "low", "medium", "high"] }),
|
|
51
53
|
createdAt: integer("created_at").notNull(),
|
|
52
54
|
updatedAt: integer("updated_at").notNull(),
|
|
53
55
|
},
|
package/src/pi/PiAgentRunner.ts
CHANGED
|
@@ -10,10 +10,13 @@ import {
|
|
|
10
10
|
import { mkdir } from "node:fs/promises";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import type { LoadedAgentConfig, ProfileConfig } from "../config/schemas";
|
|
13
|
+
import type { AgentSessionRow } from "../db/schema";
|
|
14
|
+
import { resolveModelSelection } from "./PiModelResolver";
|
|
13
15
|
|
|
14
16
|
export type PiAgentRunInput = {
|
|
15
17
|
agent: LoadedAgentConfig;
|
|
16
18
|
profile: ProfileConfig | null;
|
|
19
|
+
session: AgentSessionRow;
|
|
17
20
|
prompt: string;
|
|
18
21
|
};
|
|
19
22
|
|
|
@@ -37,10 +40,13 @@ export class PiAgentRunner {
|
|
|
37
40
|
|
|
38
41
|
const authStorage = AuthStorage.create();
|
|
39
42
|
const modelRegistry = ModelRegistry.create(authStorage);
|
|
43
|
+
const modelSelection = resolveModelSelection(modelRegistry, [input.session, input.agent, input.profile ?? {}]);
|
|
40
44
|
const { session } = await createAgentSession({
|
|
41
45
|
cwd: workspace,
|
|
42
46
|
authStorage,
|
|
43
47
|
modelRegistry,
|
|
48
|
+
model: modelSelection.model,
|
|
49
|
+
thinkingLevel: modelSelection.thinkingLevel,
|
|
44
50
|
resourceLoader,
|
|
45
51
|
sessionManager: SessionManager.create(workspace),
|
|
46
52
|
settingsManager,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { logger } from "../logging/logger";
|
|
5
|
+
|
|
6
|
+
export type ModelSelectionConfig = {
|
|
7
|
+
model?: string | null;
|
|
8
|
+
thinkingLevel?: ThinkingLevel | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ResolvedModelSelection = {
|
|
12
|
+
model?: Model<any>;
|
|
13
|
+
thinkingLevel?: ThinkingLevel;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function resolveModelSelection(
|
|
17
|
+
modelRegistry: ModelRegistry,
|
|
18
|
+
configs: ModelSelectionConfig[],
|
|
19
|
+
): ResolvedModelSelection {
|
|
20
|
+
const configuredModel = firstConfigured(configs.map((config) => config.model));
|
|
21
|
+
const thinkingLevel = firstConfigured(configs.map((config) => config.thinkingLevel));
|
|
22
|
+
const resolved: ResolvedModelSelection = {};
|
|
23
|
+
|
|
24
|
+
if (configuredModel) {
|
|
25
|
+
const model = resolveModel(modelRegistry, configuredModel);
|
|
26
|
+
if (model) {
|
|
27
|
+
resolved.model = model;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (thinkingLevel) {
|
|
32
|
+
resolved.thinkingLevel = thinkingLevel;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveModel(modelRegistry: ModelRegistry, modelRef: string): Model<any> | undefined {
|
|
39
|
+
const parsed = parseModelRef(modelRef);
|
|
40
|
+
|
|
41
|
+
if (!parsed) {
|
|
42
|
+
logger.warn("invalid model reference; expected provider/modelId", { model: modelRef });
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const model = modelRegistry.find(parsed.provider, parsed.modelId);
|
|
47
|
+
|
|
48
|
+
if (!model) {
|
|
49
|
+
logger.warn("configured model not found; falling back to Pi default", { model: modelRef });
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return model;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isValidModelRef(modelRef: string): boolean {
|
|
57
|
+
return parseModelRef(modelRef) !== null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseModelRef(modelRef: string): { provider: string; modelId: string } | null {
|
|
61
|
+
const trimmed = modelRef.trim();
|
|
62
|
+
const separatorIndex = trimmed.indexOf("/");
|
|
63
|
+
|
|
64
|
+
if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
provider: trimmed.slice(0, separatorIndex),
|
|
70
|
+
modelId: trimmed.slice(separatorIndex + 1),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function firstConfigured<T>(values: Array<T | null | undefined>): T | undefined {
|
|
75
|
+
return values.find((value): value is T => value !== null && value !== undefined && value !== "");
|
|
76
|
+
}
|