@fickydev/pigent 0.1.13 → 0.1.15
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/AGENTS.md +1 -0
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/TODO.md +4 -1
- package/drizzle/migrations/0005_pi_session_path.sql +1 -0
- package/drizzle/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/agents/AgentRunner.ts +9 -2
- package/src/agents/BotCommandHandler.ts +29 -3
- package/src/db/client.ts +14 -0
- package/src/db/repositories/SessionRepository.ts +13 -0
- package/src/db/schema.ts +1 -0
- package/src/pi/PiAgentRunner.ts +55 -13
- package/src/pi/PiSessionFactory.ts +49 -0
package/AGENTS.md
CHANGED
|
@@ -26,6 +26,7 @@ Add Hono later only for webhooks, Slack, WhatsApp, admin API, health endpoints,
|
|
|
26
26
|
- Always update `CHANGELOG.md` after working.
|
|
27
27
|
- Always update `TODO.md` after working.
|
|
28
28
|
- Whenever user says `remember this`, update `AGENTS.md` accordingly.
|
|
29
|
+
- When user says `release package`, commit current changes, push, and publish a patch release.
|
|
29
30
|
- Do not add Hono until explicitly requested.
|
|
30
31
|
- Do not let channel adapters call Pi directly.
|
|
31
32
|
- Do not let Pi runtime know channel secrets.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.15 - 2026-05-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Added startup schema self-heal for shipped `agent_sessions` columns when a migration journal is ahead of the actual SQLite table.
|
|
10
|
+
|
|
11
|
+
## 0.1.14 - 2026-05-18
|
|
12
|
+
|
|
5
13
|
### Added
|
|
6
14
|
|
|
7
15
|
- Added `task_runs` table and `TaskRepository` for scheduled task persistence.
|
|
@@ -61,6 +69,8 @@
|
|
|
61
69
|
- Added `/task` to Telegram bot command list (shows subcommands in help text).
|
|
62
70
|
- Added `/new` Telegram command to start a fresh active chat session.
|
|
63
71
|
- Added `/status` Telegram command with session/model/workspace/task info and estimated context usage.
|
|
72
|
+
- Added persistent Pi session files per Pigent session so chat memory survives across messages and daemon restarts.
|
|
73
|
+
- Added Pi session metadata to `/status`, with real Pi context usage when available.
|
|
64
74
|
- Changed `/model` picker to use Pi's currently available models automatically when explicit `modelChoices` are not configured.
|
|
65
75
|
- Kept daemon process alive after startup so CLI runs do not exit after `pigent ready`.
|
|
66
76
|
|
package/README.md
CHANGED
|
@@ -380,6 +380,7 @@ Planned commands:
|
|
|
380
380
|
- Keep Telegram-specific logic inside `src/channels/telegram` or routing config.
|
|
381
381
|
- Channel adapters must not call Pi directly.
|
|
382
382
|
- Pi runner must not know channel secrets.
|
|
383
|
+
- Pi sessions are persisted under `~/.pigent/pi-sessions` by default (`PIGENT_PI_SESSION_DIR` overrides this).
|
|
383
384
|
- Use repositories for database access.
|
|
384
385
|
- Keep SQLite/PostgreSQL portability in mind.
|
|
385
386
|
- Never inject secrets or raw `.env` values into model prompts.
|
package/TODO.md
CHANGED
|
@@ -139,7 +139,10 @@
|
|
|
139
139
|
- [x] Apply session model override
|
|
140
140
|
- [x] Apply thinking level selection
|
|
141
141
|
- [x] Confirm SDK APIs needed for session creation
|
|
142
|
-
- [
|
|
142
|
+
- [x] Confirm persistent session manager approach
|
|
143
|
+
- [x] Persist Pi session file path per active Pigent session
|
|
144
|
+
- [x] Reuse Pi session files across messages
|
|
145
|
+
- [x] Self-heal missing runtime `agent_sessions` columns after partial/mismatched migration state
|
|
143
146
|
- [ ] Confirm per-agent system prompt injection
|
|
144
147
|
- [ ] Confirm per-agent skills loading
|
|
145
148
|
- [ ] Confirm per-agent extensions loading
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `agent_sessions` ADD `pi_session_path` text;
|
package/package.json
CHANGED
|
@@ -104,14 +104,21 @@ export class AgentRunner {
|
|
|
104
104
|
if (!agent) return { text: `Unknown agent: ${input.agentId}` };
|
|
105
105
|
|
|
106
106
|
try {
|
|
107
|
-
const
|
|
107
|
+
const result = await this.piRunner.run({
|
|
108
108
|
agent,
|
|
109
109
|
profile: this.registry.getProfile(agent.profile),
|
|
110
110
|
session,
|
|
111
111
|
prompt: this.composePrompt(input, chatInstructions),
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
if (session.piSessionId !== result.piSessionId || session.piSessionPath !== result.piSessionPath) {
|
|
115
|
+
await this.repositories.sessions.updatePiSession(session.id, {
|
|
116
|
+
piSessionId: result.piSessionId,
|
|
117
|
+
piSessionPath: result.piSessionPath,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { text: result.text };
|
|
115
122
|
} catch (error) {
|
|
116
123
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
117
124
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { InboundMessage, InlineKeyboardButton } from "../channels/types";
|
|
3
|
-
import type { ModelChoiceConfig } from "../config/schemas";
|
|
3
|
+
import type { LoadedAgentConfig, ModelChoiceConfig, ProfileConfig } from "../config/schemas";
|
|
4
|
+
import type { AgentSessionRow } from "../db/schema";
|
|
4
5
|
import type { Repositories } from "../db/repositories";
|
|
6
|
+
import { PiAgentRunner, type PiContextUsage } from "../pi/PiAgentRunner";
|
|
5
7
|
import { getAvailableModelChoices } from "../pi/PiAvailableModels";
|
|
6
8
|
import { isValidModelRef, resolveModelSelection } from "../pi/PiModelResolver";
|
|
7
9
|
import type { AgentRegistry } from "./AgentRegistry";
|
|
@@ -21,6 +23,8 @@ const THINKING_LEVELS = ["off", "low", "medium", "high"] as const;
|
|
|
21
23
|
type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
22
24
|
|
|
23
25
|
export class BotCommandHandler {
|
|
26
|
+
private readonly piRunner = new PiAgentRunner();
|
|
27
|
+
|
|
24
28
|
constructor(
|
|
25
29
|
private readonly registry: AgentRegistry,
|
|
26
30
|
private readonly repositories: Repositories,
|
|
@@ -134,7 +138,11 @@ export class BotCommandHandler {
|
|
|
134
138
|
const profile = this.registry.getProfile(agent.profile);
|
|
135
139
|
const messageStats = await this.repositories.messages.statsBySession(sessionResult.session.id);
|
|
136
140
|
const modelInfo = this.resolveStatusModel(sessionResult.session, agent, profile);
|
|
137
|
-
const
|
|
141
|
+
const piStatus = await this.loadPiStatus(sessionResult.session, agent, profile);
|
|
142
|
+
const contextText = piStatus?.contextUsage
|
|
143
|
+
? formatPiContext(piStatus.contextUsage)
|
|
144
|
+
: formatEstimatedContext(messageStats.estimatedTokens, modelInfo.contextWindow);
|
|
145
|
+
const contextSource = piStatus?.contextUsage ? "Pi session" : "Pigent DB estimate";
|
|
138
146
|
const taskCount = this.scheduler
|
|
139
147
|
? this.scheduler.getTasks().filter((task) => task.chatId === message.chatId || !task.chatId).length
|
|
140
148
|
: null;
|
|
@@ -151,6 +159,8 @@ export class BotCommandHandler {
|
|
|
151
159
|
"Model: " + modelInfo.model,
|
|
152
160
|
"Thinking: " + (sessionResult.session.thinkingLevel ?? agent.thinkingLevel ?? profile?.thinkingLevel ?? "default"),
|
|
153
161
|
"Context: " + contextText,
|
|
162
|
+
"Context source: " + contextSource,
|
|
163
|
+
"Pi session: " + (piStatus?.piSessionId ?? sessionResult.session.piSessionId ?? "not created"),
|
|
154
164
|
"Workspace: " + formatPath(agent.workspace),
|
|
155
165
|
"Chat: " + message.chatId,
|
|
156
166
|
"Thread: " + (message.threadId ?? "none"),
|
|
@@ -159,6 +169,14 @@ export class BotCommandHandler {
|
|
|
159
169
|
].join("\n");
|
|
160
170
|
}
|
|
161
171
|
|
|
172
|
+
private async loadPiStatus(session: AgentSessionRow, agent: LoadedAgentConfig, profile: ProfileConfig | null) {
|
|
173
|
+
try {
|
|
174
|
+
return await this.piRunner.status({ agent, profile, session });
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
162
180
|
private resolveStatusModel(session: { model: string | null }, agent: { model?: string | null }, profile: { model?: string | null } | null) {
|
|
163
181
|
const configuredModel = session.model ?? agent.model ?? profile?.model ?? null;
|
|
164
182
|
if (!configuredModel) return { model: "default", contextWindow: null as number | null };
|
|
@@ -455,7 +473,15 @@ function chunk<T>(items: T[], size: number): T[][] {
|
|
|
455
473
|
return rows;
|
|
456
474
|
}
|
|
457
475
|
|
|
458
|
-
function
|
|
476
|
+
function formatPiContext(contextUsage: PiContextUsage): string {
|
|
477
|
+
if (contextUsage.percent === null || contextUsage.tokens === null) {
|
|
478
|
+
return "? / " + formatTokens(contextUsage.contextWindow);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return contextUsage.percent.toFixed(1) + "% / " + formatTokens(contextUsage.contextWindow);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function formatEstimatedContext(tokens: number, contextWindow: number | null): string {
|
|
459
485
|
if (!contextWindow || contextWindow <= 0) {
|
|
460
486
|
return "~" + formatTokens(tokens) + " tokens estimated / unknown limit";
|
|
461
487
|
}
|
package/src/db/client.ts
CHANGED
|
@@ -18,9 +18,23 @@ export const sqlite = new Database(databasePath);
|
|
|
18
18
|
export const db = drizzle(sqlite, { schema });
|
|
19
19
|
|
|
20
20
|
migrate(db, { migrationsFolder: "./drizzle/migrations" });
|
|
21
|
+
ensureRuntimeSchema();
|
|
21
22
|
|
|
22
23
|
export type DbClient = typeof db;
|
|
23
24
|
|
|
24
25
|
export function getDatabasePath(): string {
|
|
25
26
|
return databasePath;
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
function ensureRuntimeSchema(): void {
|
|
30
|
+
ensureColumn("agent_sessions", "active", "ALTER TABLE `agent_sessions` ADD `active` integer DEFAULT true NOT NULL");
|
|
31
|
+
ensureColumn("agent_sessions", "ended_at", "ALTER TABLE `agent_sessions` ADD `ended_at` integer");
|
|
32
|
+
ensureColumn("agent_sessions", "pi_session_path", "ALTER TABLE `agent_sessions` ADD `pi_session_path` text");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureColumn(table: string, column: string, statement: string): void {
|
|
36
|
+
const columns = sqlite.query<{ name: string }, []>(`PRAGMA table_info(${table})`).all();
|
|
37
|
+
if (!columns.some((item) => item.name === column)) {
|
|
38
|
+
sqlite.exec(statement);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -62,6 +62,19 @@ export class SessionRepository {
|
|
|
62
62
|
return this.requireById(id);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
async updatePiSession(id: string, input: { piSessionId: string; piSessionPath: string }): Promise<AgentSessionRow> {
|
|
66
|
+
await this.db
|
|
67
|
+
.update(agentSessions)
|
|
68
|
+
.set({
|
|
69
|
+
piSessionId: input.piSessionId,
|
|
70
|
+
piSessionPath: input.piSessionPath,
|
|
71
|
+
updatedAt: Date.now(),
|
|
72
|
+
})
|
|
73
|
+
.where(eq(agentSessions.id, id));
|
|
74
|
+
|
|
75
|
+
return this.requireById(id);
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
async findById(id: string): Promise<AgentSessionRow | null> {
|
|
66
79
|
const row = await this.db.query.agentSessions.findFirst({
|
|
67
80
|
where: eq(agentSessions.id, id),
|
package/src/db/schema.ts
CHANGED
|
@@ -47,6 +47,7 @@ export const agentSessions = sqliteTable(
|
|
|
47
47
|
threadId: text("thread_id"),
|
|
48
48
|
userId: text("user_id"),
|
|
49
49
|
piSessionId: text("pi_session_id"),
|
|
50
|
+
piSessionPath: text("pi_session_path"),
|
|
50
51
|
instructionsHash: text("instructions_hash"),
|
|
51
52
|
model: text("model"),
|
|
52
53
|
thinkingLevel: text("thinking_level", { enum: ["off", "low", "medium", "high"] }),
|
package/src/pi/PiAgentRunner.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
DefaultResourceLoader,
|
|
5
5
|
ModelRegistry,
|
|
6
6
|
getAgentDir,
|
|
7
|
-
SessionManager,
|
|
8
7
|
SettingsManager,
|
|
9
8
|
} from "@earendil-works/pi-coding-agent";
|
|
10
9
|
import { mkdir } from "node:fs/promises";
|
|
@@ -12,6 +11,7 @@ import { resolve } from "node:path";
|
|
|
12
11
|
import type { LoadedAgentConfig, ProfileConfig } from "../config/schemas";
|
|
13
12
|
import type { AgentSessionRow } from "../db/schema";
|
|
14
13
|
import { resolveModelSelection } from "./PiModelResolver";
|
|
14
|
+
import { loadOrCreatePiSession } from "./PiSessionFactory";
|
|
15
15
|
|
|
16
16
|
export type PiAgentRunInput = {
|
|
17
17
|
agent: LoadedAgentConfig;
|
|
@@ -20,8 +20,58 @@ export type PiAgentRunInput = {
|
|
|
20
20
|
prompt: string;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
export type PiContextUsage = {
|
|
24
|
+
tokens: number | null;
|
|
25
|
+
contextWindow: number;
|
|
26
|
+
percent: number | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type PiAgentRunResult = {
|
|
30
|
+
text: string;
|
|
31
|
+
piSessionId: string;
|
|
32
|
+
piSessionPath: string;
|
|
33
|
+
contextUsage?: PiContextUsage;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type PiSessionStatus = {
|
|
37
|
+
piSessionId: string;
|
|
38
|
+
piSessionPath: string;
|
|
39
|
+
contextUsage?: PiContextUsage;
|
|
40
|
+
};
|
|
41
|
+
|
|
23
42
|
export class PiAgentRunner {
|
|
24
|
-
async run(input: PiAgentRunInput): Promise<
|
|
43
|
+
async run(input: PiAgentRunInput): Promise<PiAgentRunResult> {
|
|
44
|
+
const prepared = await this.prepare(input);
|
|
45
|
+
|
|
46
|
+
let response = "";
|
|
47
|
+
prepared.session.subscribe((event) => {
|
|
48
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
49
|
+
response += event.assistantMessageEvent.delta;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await prepared.session.prompt(input.prompt);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
text: response.trim() || "(no response)",
|
|
57
|
+
piSessionId: prepared.piSession.sessionId,
|
|
58
|
+
piSessionPath: prepared.piSession.sessionPath,
|
|
59
|
+
contextUsage: prepared.session.getContextUsage(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async status(input: Omit<PiAgentRunInput, "prompt">): Promise<PiSessionStatus | null> {
|
|
64
|
+
if (!input.session.piSessionPath) return null;
|
|
65
|
+
|
|
66
|
+
const prepared = await this.prepare({ ...input, prompt: "" });
|
|
67
|
+
return {
|
|
68
|
+
piSessionId: prepared.piSession.sessionId,
|
|
69
|
+
piSessionPath: prepared.piSession.sessionPath,
|
|
70
|
+
contextUsage: prepared.session.getContextUsage(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async prepare(input: PiAgentRunInput) {
|
|
25
75
|
const workspace = resolve(input.agent.workspace);
|
|
26
76
|
await mkdir(workspace, { recursive: true });
|
|
27
77
|
|
|
@@ -41,6 +91,7 @@ export class PiAgentRunner {
|
|
|
41
91
|
const authStorage = AuthStorage.create();
|
|
42
92
|
const modelRegistry = ModelRegistry.create(authStorage);
|
|
43
93
|
const modelSelection = resolveModelSelection(modelRegistry, [input.session, input.agent, input.profile ?? {}]);
|
|
94
|
+
const piSession = await loadOrCreatePiSession(workspace, input.session);
|
|
44
95
|
const { session } = await createAgentSession({
|
|
45
96
|
cwd: workspace,
|
|
46
97
|
authStorage,
|
|
@@ -48,22 +99,13 @@ export class PiAgentRunner {
|
|
|
48
99
|
model: modelSelection.model,
|
|
49
100
|
thinkingLevel: modelSelection.thinkingLevel,
|
|
50
101
|
resourceLoader,
|
|
51
|
-
sessionManager:
|
|
102
|
+
sessionManager: piSession.manager,
|
|
52
103
|
settingsManager,
|
|
53
104
|
agentDir,
|
|
54
105
|
tools: [],
|
|
55
106
|
});
|
|
56
107
|
|
|
57
|
-
|
|
58
|
-
session.subscribe((event) => {
|
|
59
|
-
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
60
|
-
response += event.assistantMessageEvent.delta;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
await session.prompt(input.prompt);
|
|
65
|
-
|
|
66
|
-
return response.trim() || "(no response)";
|
|
108
|
+
return { session, piSession };
|
|
67
109
|
}
|
|
68
110
|
}
|
|
69
111
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import type { AgentSessionRow } from "../db/schema";
|
|
7
|
+
|
|
8
|
+
export type PiSessionHandle = {
|
|
9
|
+
manager: SessionManager;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
sessionPath: string;
|
|
12
|
+
created: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function loadOrCreatePiSession(workspace: string, session: AgentSessionRow): Promise<PiSessionHandle> {
|
|
16
|
+
const sessionDir = piSessionDir();
|
|
17
|
+
await mkdir(sessionDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
if (session.piSessionPath && existsSync(session.piSessionPath)) {
|
|
20
|
+
const manager = SessionManager.open(session.piSessionPath, sessionDir, workspace);
|
|
21
|
+
return {
|
|
22
|
+
manager,
|
|
23
|
+
sessionId: manager.getSessionId(),
|
|
24
|
+
sessionPath: manager.getSessionFile() ?? session.piSessionPath,
|
|
25
|
+
created: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const manager = SessionManager.create(workspace, sessionDir);
|
|
30
|
+
manager.newSession({ id: session.piSessionId ?? session.id });
|
|
31
|
+
|
|
32
|
+
const sessionPath = manager.getSessionFile();
|
|
33
|
+
if (!sessionPath) {
|
|
34
|
+
throw new Error("failed to create persisted Pi session file");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await mkdir(dirname(sessionPath), { recursive: true });
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
manager,
|
|
41
|
+
sessionId: manager.getSessionId(),
|
|
42
|
+
sessionPath,
|
|
43
|
+
created: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function piSessionDir(): string {
|
|
48
|
+
return resolve(process.env.PIGENT_PI_SESSION_DIR ?? join(process.env.PIGENT_HOME ?? join(homedir(), ".pigent"), "pi-sessions"));
|
|
49
|
+
}
|