@fickydev/pigent 0.1.12 → 0.1.13
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 +2 -0
- package/README.md +7 -0
- package/TODO.md +3 -0
- package/drizzle/migrations/0004_active_agent_sessions.sql +4 -0
- package/drizzle/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/agents/BotCommandHandler.ts +120 -3
- package/src/daemon/AgentDaemon.ts +2 -0
- package/src/db/repositories/MessageRepository.ts +24 -0
- package/src/db/repositories/SessionRepository.ts +56 -26
- package/src/db/schema.ts +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
- Planned Telegram inline-button model and thinking level pickers backed by configured model choices.
|
|
60
60
|
- Added Telegram inline-button pickers for `/model` and `/thinking`, callback handling, and bot command menu registration.
|
|
61
61
|
- Added `/task` to Telegram bot command list (shows subcommands in help text).
|
|
62
|
+
- Added `/new` Telegram command to start a fresh active chat session.
|
|
63
|
+
- Added `/status` Telegram command with session/model/workspace/task info and estimated context usage.
|
|
62
64
|
- Changed `/model` picker to use Pi's currently available models automatically when explicit `modelChoices` are not configured.
|
|
63
65
|
- Kept daemon process alive after startup so CLI runs do not exit after `pigent ready`.
|
|
64
66
|
|
package/README.md
CHANGED
package/TODO.md
CHANGED
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
- [x] Define `runtime_kv` table for offsets and daemon state
|
|
73
73
|
- [x] Add `agent_sessions.model` for session model override
|
|
74
74
|
- [x] Add `agent_sessions.thinking_level` for session thinking override
|
|
75
|
+
- [x] Add active/inactive session lifecycle fields for `/new` sessions
|
|
75
76
|
- [x] Implement repositories
|
|
76
77
|
- [x] `AgentRepository`
|
|
77
78
|
- [x] `SessionRepository`
|
|
@@ -183,6 +184,8 @@
|
|
|
183
184
|
- [ ] `/default-agent <id>` set chat default
|
|
184
185
|
- [ ] `/instructions <text>` set chat instructions
|
|
185
186
|
- [ ] `/sessions` list active sessions for chat
|
|
187
|
+
- [x] `/new` start a fresh active chat session
|
|
188
|
+
- [x] `/status` show current session, model, workspace, tasks, and estimated context usage
|
|
186
189
|
- [ ] `/reset-session <agentId>` clear chat session
|
|
187
190
|
- [ ] `/heartbeat status` show heartbeat state
|
|
188
191
|
- [x] `/model` show current model for default chat agent session
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
ALTER TABLE `agent_sessions` ADD `active` integer DEFAULT true NOT NULL;
|
|
2
|
+
ALTER TABLE `agent_sessions` ADD `ended_at` integer;
|
|
3
|
+
DROP INDEX `agent_sessions_key_unique`;
|
|
4
|
+
CREATE INDEX `agent_sessions_active_key_idx` ON `agent_sessions` (`agent_id`, `channel`, `chat_id`, `thread_id`, `active`);
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { InboundMessage, InlineKeyboardButton } from "../channels/types";
|
|
2
3
|
import type { ModelChoiceConfig } from "../config/schemas";
|
|
3
4
|
import type { Repositories } from "../db/repositories";
|
|
4
5
|
import { getAvailableModelChoices } from "../pi/PiAvailableModels";
|
|
5
|
-
import { isValidModelRef } from "../pi/PiModelResolver";
|
|
6
|
+
import { isValidModelRef, resolveModelSelection } from "../pi/PiModelResolver";
|
|
6
7
|
import type { AgentRegistry } from "./AgentRegistry";
|
|
7
8
|
import type { Scheduler } from "../daemon/Scheduler";
|
|
8
9
|
|
|
@@ -38,6 +39,10 @@ export class BotCommandHandler {
|
|
|
38
39
|
return { handled: true, text: this.helpText() };
|
|
39
40
|
case "/agents":
|
|
40
41
|
return { handled: true, text: await this.agentsText(message) };
|
|
42
|
+
case "/new":
|
|
43
|
+
return { handled: true, text: await this.newSessionText(message) };
|
|
44
|
+
case "/status":
|
|
45
|
+
return { handled: true, text: await this.statusText(message) };
|
|
41
46
|
case "/model":
|
|
42
47
|
return await this.modelResult(message);
|
|
43
48
|
case "/thinking":
|
|
@@ -60,6 +65,8 @@ export class BotCommandHandler {
|
|
|
60
65
|
"Pigent commands:",
|
|
61
66
|
"/help - show this help",
|
|
62
67
|
"/agents - list agents for this chat",
|
|
68
|
+
"/new - start a fresh session for this chat",
|
|
69
|
+
"/status - show current session status",
|
|
63
70
|
"/model - show model picker for this chat session",
|
|
64
71
|
"/model <provider/modelId> - set model for this chat session",
|
|
65
72
|
"/model default - clear model override for this chat session",
|
|
@@ -96,6 +103,76 @@ export class BotCommandHandler {
|
|
|
96
103
|
].join("\n");
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
private async newSessionText(message: InboundMessage): Promise<string> {
|
|
107
|
+
const sessionResult = await this.getDefaultSession(message);
|
|
108
|
+
if (!sessionResult.ok) return sessionResult.message;
|
|
109
|
+
|
|
110
|
+
const session = await this.repositories.sessions.startNew({
|
|
111
|
+
agentId: sessionResult.agentId,
|
|
112
|
+
channel: message.channel,
|
|
113
|
+
chatId: message.chatId,
|
|
114
|
+
threadId: message.threadId,
|
|
115
|
+
userId: message.senderId,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return [
|
|
119
|
+
"New session created.",
|
|
120
|
+
"",
|
|
121
|
+
"Agent: " + session.agentId,
|
|
122
|
+
"Session: " + session.id,
|
|
123
|
+
"Model: " + (session.model ?? "default"),
|
|
124
|
+
"Thinking: " + (session.thinkingLevel ?? "default"),
|
|
125
|
+
"Workspace: " + formatPath(sessionResult.agent.workspace),
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async statusText(message: InboundMessage): Promise<string> {
|
|
130
|
+
const sessionResult = await this.getDefaultSession(message);
|
|
131
|
+
if (!sessionResult.ok) return sessionResult.message;
|
|
132
|
+
|
|
133
|
+
const agent = sessionResult.agent;
|
|
134
|
+
const profile = this.registry.getProfile(agent.profile);
|
|
135
|
+
const messageStats = await this.repositories.messages.statsBySession(sessionResult.session.id);
|
|
136
|
+
const modelInfo = this.resolveStatusModel(sessionResult.session, agent, profile);
|
|
137
|
+
const contextText = formatContext(messageStats.estimatedTokens, modelInfo.contextWindow);
|
|
138
|
+
const taskCount = this.scheduler
|
|
139
|
+
? this.scheduler.getTasks().filter((task) => task.chatId === message.chatId || !task.chatId).length
|
|
140
|
+
: null;
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
"Pigent status",
|
|
144
|
+
"",
|
|
145
|
+
"Agent: " + agent.id,
|
|
146
|
+
"Profile: " + agent.profile,
|
|
147
|
+
"Session: " + sessionResult.session.id,
|
|
148
|
+
"Session age: " + formatDuration(Date.now() - sessionResult.session.createdAt),
|
|
149
|
+
"Messages: " + messageStats.count,
|
|
150
|
+
"Last message: " + (messageStats.lastMessageAt ? formatDuration(Date.now() - messageStats.lastMessageAt) + " ago" : "none"),
|
|
151
|
+
"Model: " + modelInfo.model,
|
|
152
|
+
"Thinking: " + (sessionResult.session.thinkingLevel ?? agent.thinkingLevel ?? profile?.thinkingLevel ?? "default"),
|
|
153
|
+
"Context: " + contextText,
|
|
154
|
+
"Workspace: " + formatPath(agent.workspace),
|
|
155
|
+
"Chat: " + message.chatId,
|
|
156
|
+
"Thread: " + (message.threadId ?? "none"),
|
|
157
|
+
"Tasks: " + (taskCount === null ? "scheduler unavailable" : String(taskCount)),
|
|
158
|
+
"Fake runner: " + (process.env.PIGENT_FAKE_AGENT === "1" ? "on" : "off"),
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private resolveStatusModel(session: { model: string | null }, agent: { model?: string | null }, profile: { model?: string | null } | null) {
|
|
163
|
+
const configuredModel = session.model ?? agent.model ?? profile?.model ?? null;
|
|
164
|
+
if (!configuredModel) return { model: "default", contextWindow: null as number | null };
|
|
165
|
+
|
|
166
|
+
const authStorage = AuthStorage.create();
|
|
167
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
168
|
+
const resolved = resolveModelSelection(modelRegistry, [session, agent, profile ?? {}]);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
model: configuredModel,
|
|
172
|
+
contextWindow: resolved.model?.contextWindow ?? null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
99
176
|
private async modelResult(message: InboundMessage): Promise<BotCommandResult> {
|
|
100
177
|
const sessionResult = await this.getDefaultSession(message);
|
|
101
178
|
if (!sessionResult.ok) return { handled: true, text: sessionResult.message };
|
|
@@ -343,7 +420,8 @@ export class BotCommandHandler {
|
|
|
343
420
|
return { ok: false as const, message: "No default agent configured for this chat." };
|
|
344
421
|
}
|
|
345
422
|
|
|
346
|
-
|
|
423
|
+
const agent = this.registry.getAgent(agentId);
|
|
424
|
+
if (!agent) {
|
|
347
425
|
return { ok: false as const, message: "Unknown agent: " + agentId };
|
|
348
426
|
}
|
|
349
427
|
|
|
@@ -359,7 +437,7 @@ export class BotCommandHandler {
|
|
|
359
437
|
userId: message.senderId,
|
|
360
438
|
});
|
|
361
439
|
|
|
362
|
-
return { ok: true as const, agentId, session };
|
|
440
|
+
return { ok: true as const, agentId, agent, session };
|
|
363
441
|
}
|
|
364
442
|
}
|
|
365
443
|
|
|
@@ -376,3 +454,42 @@ function chunk<T>(items: T[], size: number): T[][] {
|
|
|
376
454
|
|
|
377
455
|
return rows;
|
|
378
456
|
}
|
|
457
|
+
|
|
458
|
+
function formatContext(tokens: number, contextWindow: number | null): string {
|
|
459
|
+
if (!contextWindow || contextWindow <= 0) {
|
|
460
|
+
return "~" + formatTokens(tokens) + " tokens estimated / unknown limit";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const percent = (tokens / contextWindow) * 100;
|
|
464
|
+
return "~" + percent.toFixed(1) + "% / " + formatTokens(contextWindow) + " tokens estimated";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function formatTokens(count: number): string {
|
|
468
|
+
if (count < 1000) return String(count);
|
|
469
|
+
if (count < 10000) return (count / 1000).toFixed(1) + "k";
|
|
470
|
+
if (count < 1000000) return Math.round(count / 1000) + "k";
|
|
471
|
+
if (count < 10000000) return (count / 1000000).toFixed(1) + "M";
|
|
472
|
+
return Math.round(count / 1000000) + "M";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function formatDuration(ms: number): string {
|
|
476
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
477
|
+
if (seconds < 60) return seconds + "s";
|
|
478
|
+
|
|
479
|
+
const minutes = Math.floor(seconds / 60);
|
|
480
|
+
if (minutes < 60) return minutes + "m";
|
|
481
|
+
|
|
482
|
+
const hours = Math.floor(minutes / 60);
|
|
483
|
+
if (hours < 48) return hours + "h " + (minutes % 60) + "m";
|
|
484
|
+
|
|
485
|
+
return Math.floor(hours / 24) + "d " + (hours % 24) + "h";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function formatPath(path: string): string {
|
|
489
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
490
|
+
if (home && path.startsWith(home)) {
|
|
491
|
+
return "~" + path.slice(home.length);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return path;
|
|
495
|
+
}
|
|
@@ -178,6 +178,8 @@ function telegramCommands() {
|
|
|
178
178
|
return [
|
|
179
179
|
{ command: "help", description: "Show Pigent commands" },
|
|
180
180
|
{ command: "agents", description: "List agents available in this chat" },
|
|
181
|
+
{ command: "new", description: "Start a fresh chat session" },
|
|
182
|
+
{ command: "status", description: "Show current session status" },
|
|
181
183
|
{ command: "model", description: "Choose model for this chat session" },
|
|
182
184
|
{ command: "thinking", description: "Choose thinking level for this chat session" },
|
|
183
185
|
{ command: "agent", description: "Send message to a specific agent" },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
1
2
|
import { nanoid } from "nanoid";
|
|
2
3
|
import type { DbClient } from "../client";
|
|
3
4
|
import { messages, type MessageRow } from "../schema";
|
|
@@ -42,4 +43,27 @@ export class MessageRepository {
|
|
|
42
43
|
if (!row) throw new Error(`failed to create message ${id}`);
|
|
43
44
|
return row;
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
async statsBySession(sessionId: string): Promise<{ count: number; estimatedTokens: number; lastMessageAt: number | null }> {
|
|
48
|
+
const rows = await this.db
|
|
49
|
+
.select({ content: messages.content, createdAt: messages.createdAt })
|
|
50
|
+
.from(messages)
|
|
51
|
+
.where(eq(messages.sessionId, sessionId));
|
|
52
|
+
|
|
53
|
+
let chars = 0;
|
|
54
|
+
let lastMessageAt: number | null = null;
|
|
55
|
+
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
chars += row.content.length;
|
|
58
|
+
if (lastMessageAt === null || row.createdAt > lastMessageAt) {
|
|
59
|
+
lastMessageAt = row.createdAt;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
count: rows.length,
|
|
65
|
+
estimatedTokens: Math.ceil(chars / 4),
|
|
66
|
+
lastMessageAt,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
45
69
|
}
|
|
@@ -17,27 +17,25 @@ export class SessionRepository {
|
|
|
17
17
|
constructor(private readonly db: DbClient) {}
|
|
18
18
|
|
|
19
19
|
async getOrCreate(key: SessionKey): Promise<AgentSessionRow> {
|
|
20
|
-
const existing = await this.
|
|
20
|
+
const existing = await this.findActiveByKey(key);
|
|
21
21
|
if (existing) return existing;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
return await this.create(key);
|
|
24
|
+
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
agentId: key.agentId,
|
|
29
|
-
channel: key.channel,
|
|
30
|
-
chatId: key.chatId,
|
|
31
|
-
threadId: key.threadId ?? null,
|
|
32
|
-
userId: key.userId ?? null,
|
|
33
|
-
createdAt: now,
|
|
34
|
-
updatedAt: now,
|
|
35
|
-
});
|
|
26
|
+
async startNew(key: SessionKey): Promise<AgentSessionRow> {
|
|
27
|
+
const now = Date.now();
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
await this.db
|
|
30
|
+
.update(agentSessions)
|
|
31
|
+
.set({
|
|
32
|
+
active: false,
|
|
33
|
+
endedAt: now,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
})
|
|
36
|
+
.where(this.keyPredicate(key, true));
|
|
39
37
|
|
|
40
|
-
return
|
|
38
|
+
return await this.create(key);
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
async updateModel(id: string, model: string | null): Promise<AgentSessionRow> {
|
|
@@ -79,20 +77,52 @@ export class SessionRepository {
|
|
|
79
77
|
return row;
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
private async
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
: isNull(agentSessions.threadId);
|
|
80
|
+
private async create(key: SessionKey): Promise<AgentSessionRow> {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const id = nanoid();
|
|
86
83
|
|
|
84
|
+
await this.db.insert(agentSessions).values({
|
|
85
|
+
id,
|
|
86
|
+
agentId: key.agentId,
|
|
87
|
+
channel: key.channel,
|
|
88
|
+
chatId: key.chatId,
|
|
89
|
+
threadId: key.threadId ?? null,
|
|
90
|
+
userId: key.userId ?? null,
|
|
91
|
+
active: true,
|
|
92
|
+
createdAt: now,
|
|
93
|
+
updatedAt: now,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const created = await this.findById(id);
|
|
97
|
+
if (!created) throw new Error(`failed to create session ${id}`);
|
|
98
|
+
|
|
99
|
+
return created;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async findActiveByKey(key: SessionKey): Promise<AgentSessionRow | null> {
|
|
87
103
|
const row = await this.db.query.agentSessions.findFirst({
|
|
88
|
-
where:
|
|
89
|
-
eq(agentSessions.agentId, key.agentId),
|
|
90
|
-
eq(agentSessions.channel, key.channel),
|
|
91
|
-
eq(agentSessions.chatId, key.chatId),
|
|
92
|
-
threadPredicate,
|
|
93
|
-
),
|
|
104
|
+
where: this.keyPredicate(key, true),
|
|
94
105
|
});
|
|
95
106
|
|
|
96
107
|
return row ?? null;
|
|
97
108
|
}
|
|
109
|
+
|
|
110
|
+
private keyPredicate(key: SessionKey, active?: boolean) {
|
|
111
|
+
const threadPredicate = key.threadId
|
|
112
|
+
? eq(agentSessions.threadId, key.threadId)
|
|
113
|
+
: isNull(agentSessions.threadId);
|
|
114
|
+
|
|
115
|
+
const predicates = [
|
|
116
|
+
eq(agentSessions.agentId, key.agentId),
|
|
117
|
+
eq(agentSessions.channel, key.channel),
|
|
118
|
+
eq(agentSessions.chatId, key.chatId),
|
|
119
|
+
threadPredicate,
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
if (active !== undefined) {
|
|
123
|
+
predicates.push(eq(agentSessions.active, active));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return and(...predicates);
|
|
127
|
+
}
|
|
98
128
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
2
2
|
|
|
3
3
|
export const agents = sqliteTable("agents", {
|
|
4
4
|
id: text("id").primaryKey(),
|
|
@@ -50,15 +50,18 @@ export const agentSessions = sqliteTable(
|
|
|
50
50
|
instructionsHash: text("instructions_hash"),
|
|
51
51
|
model: text("model"),
|
|
52
52
|
thinkingLevel: text("thinking_level", { enum: ["off", "low", "medium", "high"] }),
|
|
53
|
+
active: integer("active", { mode: "boolean" }).notNull().default(true),
|
|
54
|
+
endedAt: integer("ended_at"),
|
|
53
55
|
createdAt: integer("created_at").notNull(),
|
|
54
56
|
updatedAt: integer("updated_at").notNull(),
|
|
55
57
|
},
|
|
56
58
|
(table) => ({
|
|
57
|
-
|
|
59
|
+
sessionActiveKeyIndex: index("agent_sessions_active_key_idx").on(
|
|
58
60
|
table.agentId,
|
|
59
61
|
table.channel,
|
|
60
62
|
table.chatId,
|
|
61
63
|
table.threadId,
|
|
64
|
+
table.active,
|
|
62
65
|
),
|
|
63
66
|
}),
|
|
64
67
|
);
|