@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.
- package/CHANGELOG.md +9 -0
- package/PLAN.md +44 -1
- package/TODO.md +30 -9
- 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/AgentRegistry.ts +45 -0
- package/src/agents/AgentRunner.ts +82 -25
- package/src/agents/BotCommandHandler.ts +115 -3
- package/src/agents/MessageRouter.ts +6 -3
- package/src/channels/telegram/TelegramApi.ts +125 -11
- package/src/config/schemas.ts +6 -1
- package/src/daemon/AgentDaemon.ts +15 -10
- package/src/db/repositories/HeartbeatRepository.ts +129 -0
- package/src/db/repositories/SessionRepository.ts +33 -0
- package/src/db/repositories/index.ts +2 -0
- package/src/db/schema.ts +2 -0
- package/src/pi/PiAgentRunner.ts +6 -0
- package/src/pi/PiModelResolver.ts +76 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { and, desc, eq, gte, isNull } from "drizzle-orm";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import type { DbClient } from "../client";
|
|
4
|
+
import { heartbeats, type HeartbeatRow } from "../schema";
|
|
5
|
+
|
|
6
|
+
export type HeartbeatStatus = "pending" | "running" | "noop" | "notified" | "failed";
|
|
7
|
+
|
|
8
|
+
export type CreateHeartbeatInput = {
|
|
9
|
+
agentId: string;
|
|
10
|
+
sessionId?: string | null;
|
|
11
|
+
prompt: string;
|
|
12
|
+
status?: Extract<HeartbeatStatus, "pending" | "running">;
|
|
13
|
+
startedAt?: number | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type FinishHeartbeatInput = {
|
|
17
|
+
result?: string | null;
|
|
18
|
+
error?: string | null;
|
|
19
|
+
finishedAt?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class HeartbeatRepository {
|
|
23
|
+
constructor(private readonly db: DbClient) {}
|
|
24
|
+
|
|
25
|
+
async create(input: CreateHeartbeatInput): Promise<HeartbeatRow> {
|
|
26
|
+
const id = nanoid();
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
|
|
29
|
+
await this.db.insert(heartbeats).values({
|
|
30
|
+
id,
|
|
31
|
+
agentId: input.agentId,
|
|
32
|
+
sessionId: input.sessionId ?? null,
|
|
33
|
+
status: input.status ?? "pending",
|
|
34
|
+
prompt: input.prompt,
|
|
35
|
+
result: null,
|
|
36
|
+
error: null,
|
|
37
|
+
startedAt: input.startedAt ?? null,
|
|
38
|
+
finishedAt: null,
|
|
39
|
+
createdAt: now,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const row = await this.findById(id);
|
|
43
|
+
if (!row) throw new Error(`failed to create heartbeat ${id}`);
|
|
44
|
+
|
|
45
|
+
return row;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async markRunning(id: string, startedAt = Date.now()): Promise<HeartbeatRow> {
|
|
49
|
+
await this.db
|
|
50
|
+
.update(heartbeats)
|
|
51
|
+
.set({
|
|
52
|
+
status: "running",
|
|
53
|
+
startedAt,
|
|
54
|
+
})
|
|
55
|
+
.where(eq(heartbeats.id, id));
|
|
56
|
+
|
|
57
|
+
return this.requireById(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async markNoop(id: string, input: FinishHeartbeatInput = {}): Promise<HeartbeatRow> {
|
|
61
|
+
return this.finish(id, "noop", input);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async markNotified(id: string, input: FinishHeartbeatInput = {}): Promise<HeartbeatRow> {
|
|
65
|
+
return this.finish(id, "notified", input);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async markFailed(id: string, input: FinishHeartbeatInput): Promise<HeartbeatRow> {
|
|
69
|
+
return this.finish(id, "failed", input);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async findById(id: string): Promise<HeartbeatRow | null> {
|
|
73
|
+
const row = await this.db.query.heartbeats.findFirst({
|
|
74
|
+
where: eq(heartbeats.id, id),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return row ?? null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async findLatest(agentId: string, sessionId?: string | null): Promise<HeartbeatRow | null> {
|
|
81
|
+
const sessionPredicate = sessionId ? eq(heartbeats.sessionId, sessionId) : isNull(heartbeats.sessionId);
|
|
82
|
+
|
|
83
|
+
const row = await this.db.query.heartbeats.findFirst({
|
|
84
|
+
where: and(eq(heartbeats.agentId, agentId), sessionPredicate),
|
|
85
|
+
orderBy: desc(heartbeats.createdAt),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return row ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async countRecentNotified(agentId: string, since: number, sessionId?: string | null): Promise<number> {
|
|
92
|
+
const sessionPredicate = sessionId ? eq(heartbeats.sessionId, sessionId) : isNull(heartbeats.sessionId);
|
|
93
|
+
|
|
94
|
+
const rows = await this.db.query.heartbeats.findMany({
|
|
95
|
+
where: and(
|
|
96
|
+
eq(heartbeats.agentId, agentId),
|
|
97
|
+
sessionPredicate,
|
|
98
|
+
eq(heartbeats.status, "notified"),
|
|
99
|
+
gte(heartbeats.finishedAt, since),
|
|
100
|
+
),
|
|
101
|
+
columns: {
|
|
102
|
+
id: true,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return rows.length;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async finish(id: string, status: Extract<HeartbeatStatus, "noop" | "notified" | "failed">, input: FinishHeartbeatInput): Promise<HeartbeatRow> {
|
|
110
|
+
await this.db
|
|
111
|
+
.update(heartbeats)
|
|
112
|
+
.set({
|
|
113
|
+
status,
|
|
114
|
+
result: input.result ?? null,
|
|
115
|
+
error: input.error ?? null,
|
|
116
|
+
finishedAt: input.finishedAt ?? Date.now(),
|
|
117
|
+
})
|
|
118
|
+
.where(eq(heartbeats.id, id));
|
|
119
|
+
|
|
120
|
+
return this.requireById(id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async requireById(id: string): Promise<HeartbeatRow> {
|
|
124
|
+
const row = await this.findById(id);
|
|
125
|
+
if (!row) throw new Error(`heartbeat not found ${id}`);
|
|
126
|
+
|
|
127
|
+
return row;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -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)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { db } from "../client";
|
|
2
2
|
import { AgentRepository } from "./AgentRepository";
|
|
3
|
+
import { HeartbeatRepository } from "./HeartbeatRepository";
|
|
3
4
|
import { MessageRepository } from "./MessageRepository";
|
|
4
5
|
import { RuntimeKvRepository } from "./RuntimeKvRepository";
|
|
5
6
|
import { SessionRepository } from "./SessionRepository";
|
|
@@ -10,6 +11,7 @@ export type Repositories = ReturnType<typeof createRepositories>;
|
|
|
10
11
|
export function createRepositories(dbClient = db) {
|
|
11
12
|
return {
|
|
12
13
|
agents: new AgentRepository(dbClient),
|
|
14
|
+
heartbeats: new HeartbeatRepository(dbClient),
|
|
13
15
|
messages: new MessageRepository(dbClient),
|
|
14
16
|
runtimeKv: new RuntimeKvRepository(dbClient),
|
|
15
17
|
sessions: new SessionRepository(dbClient),
|
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
|
+
}
|