@fickydev/pigent 0.1.8 → 0.1.10
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 +11 -0
- package/PLAN.md +2 -2
- package/TODO.md +20 -13
- package/agents/assistant/agent.yaml +0 -5
- package/drizzle/migrations/0002_bouncy_leper_queen.sql +13 -0
- package/drizzle/migrations/0003_secret_stone_men.sql +11 -0
- package/drizzle/migrations/meta/0002_snapshot.json +606 -0
- package/drizzle/migrations/meta/0003_snapshot.json +681 -0
- package/drizzle/migrations/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/pigent.yaml +18 -1
- package/src/agents/BotCommandHandler.ts +127 -23
- package/src/config/loadConfig.ts +1 -0
- package/src/config/schemas.ts +14 -8
- package/src/daemon/AgentDaemon.ts +8 -1
- package/src/daemon/Scheduler.ts +224 -0
- package/src/daemon/taskDue.ts +4 -0
- package/src/db/repositories/TaskConfigRepository.ts +63 -0
- package/src/db/repositories/TaskRepository.ts +129 -0
- package/src/db/repositories/index.ts +4 -0
- package/src/db/schema.ts +30 -0
- package/src/pi/PiAvailableModels.ts +23 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { and, desc, eq, gte } from "drizzle-orm";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import type { DbClient } from "../client";
|
|
4
|
+
import { taskRuns, type TaskRunRow } from "../schema";
|
|
5
|
+
|
|
6
|
+
export type TaskRunStatus = "pending" | "running" | "noop" | "notified" | "failed";
|
|
7
|
+
|
|
8
|
+
export type CreateTaskRunInput = {
|
|
9
|
+
agentId: string;
|
|
10
|
+
taskId: string;
|
|
11
|
+
prompt: string;
|
|
12
|
+
sessionId?: string | null;
|
|
13
|
+
status?: Extract<TaskRunStatus, "pending" | "running">;
|
|
14
|
+
startedAt?: number | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type FinishTaskRunInput = {
|
|
18
|
+
result?: string | null;
|
|
19
|
+
error?: string | null;
|
|
20
|
+
finishedAt?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class TaskRepository {
|
|
24
|
+
constructor(private readonly db: DbClient) {}
|
|
25
|
+
|
|
26
|
+
async create(input: CreateTaskRunInput): Promise<TaskRunRow> {
|
|
27
|
+
const id = nanoid();
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
|
|
30
|
+
await this.db.insert(taskRuns).values({
|
|
31
|
+
id,
|
|
32
|
+
agentId: input.agentId,
|
|
33
|
+
taskId: input.taskId,
|
|
34
|
+
prompt: input.prompt,
|
|
35
|
+
sessionId: input.sessionId ?? null,
|
|
36
|
+
status: input.status ?? "pending",
|
|
37
|
+
result: null,
|
|
38
|
+
error: null,
|
|
39
|
+
startedAt: input.startedAt ?? null,
|
|
40
|
+
finishedAt: null,
|
|
41
|
+
createdAt: now,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const row = await this.findById(id);
|
|
45
|
+
if (!row) throw new Error(`failed to create task run ${id}`);
|
|
46
|
+
|
|
47
|
+
return row;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async markRunning(id: string, startedAt = Date.now()): Promise<TaskRunRow> {
|
|
51
|
+
await this.db
|
|
52
|
+
.update(taskRuns)
|
|
53
|
+
.set({ status: "running", startedAt })
|
|
54
|
+
.where(eq(taskRuns.id, id));
|
|
55
|
+
|
|
56
|
+
return this.requireById(id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async markNoop(id: string, input: FinishTaskRunInput = {}): Promise<TaskRunRow> {
|
|
60
|
+
return this.finish(id, "noop", input);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async markNotified(id: string, input: FinishTaskRunInput = {}): Promise<TaskRunRow> {
|
|
64
|
+
return this.finish(id, "notified", input);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async markFailed(id: string, input: FinishTaskRunInput): Promise<TaskRunRow> {
|
|
68
|
+
return this.finish(id, "failed", input);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async findById(id: string): Promise<TaskRunRow | null> {
|
|
72
|
+
const row = await this.db.query.taskRuns.findFirst({
|
|
73
|
+
where: eq(taskRuns.id, id),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return row ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async findLatest(agentId: string, taskId: string): Promise<TaskRunRow | null> {
|
|
80
|
+
const row = await this.db.query.taskRuns.findFirst({
|
|
81
|
+
where: and(eq(taskRuns.agentId, agentId), eq(taskRuns.taskId, taskId)),
|
|
82
|
+
orderBy: desc(taskRuns.createdAt),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return row ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async countRecentNotified(agentId: string, taskId: string, since: number): Promise<number> {
|
|
89
|
+
const conditions: any[] = [
|
|
90
|
+
eq(taskRuns.agentId, agentId),
|
|
91
|
+
eq(taskRuns.taskId, taskId),
|
|
92
|
+
eq(taskRuns.status, "notified"),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const rows = await this.db.query.taskRuns.findMany({
|
|
96
|
+
where: and(gte(taskRuns.finishedAt, since), ...conditions),
|
|
97
|
+
columns: { id: true },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return rows.length;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async finish(
|
|
104
|
+
id: string,
|
|
105
|
+
status: Extract<TaskRunStatus, "noop" | "notified" | "failed">,
|
|
106
|
+
input: FinishTaskRunInput,
|
|
107
|
+
): Promise<TaskRunRow> {
|
|
108
|
+
await this.db
|
|
109
|
+
.update(taskRuns)
|
|
110
|
+
.set({
|
|
111
|
+
status,
|
|
112
|
+
result: input.result ?? null,
|
|
113
|
+
error: input.error ?? null,
|
|
114
|
+
finishedAt: input.finishedAt ?? Date.now(),
|
|
115
|
+
})
|
|
116
|
+
.where(eq(taskRuns.id, id));
|
|
117
|
+
|
|
118
|
+
return this.requireById(id);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async requireById(id: string): Promise<TaskRunRow> {
|
|
122
|
+
const row = await this.findById(id);
|
|
123
|
+
if (!row) throw new Error(`task run not found ${id}`);
|
|
124
|
+
|
|
125
|
+
return row;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
@@ -4,6 +4,8 @@ import { HeartbeatRepository } from "./HeartbeatRepository";
|
|
|
4
4
|
import { MessageRepository } from "./MessageRepository";
|
|
5
5
|
import { RuntimeKvRepository } from "./RuntimeKvRepository";
|
|
6
6
|
import { SessionRepository } from "./SessionRepository";
|
|
7
|
+
import { TaskConfigRepository } from "./TaskConfigRepository";
|
|
8
|
+
import { TaskRepository } from "./TaskRepository";
|
|
7
9
|
import { TelegramRepository } from "./TelegramRepository";
|
|
8
10
|
|
|
9
11
|
export type Repositories = ReturnType<typeof createRepositories>;
|
|
@@ -15,6 +17,8 @@ export function createRepositories(dbClient = db) {
|
|
|
15
17
|
messages: new MessageRepository(dbClient),
|
|
16
18
|
runtimeKv: new RuntimeKvRepository(dbClient),
|
|
17
19
|
sessions: new SessionRepository(dbClient),
|
|
20
|
+
taskConfigs: new TaskConfigRepository(dbClient),
|
|
21
|
+
taskRuns: new TaskRepository(dbClient),
|
|
18
22
|
telegram: new TelegramRepository(dbClient),
|
|
19
23
|
};
|
|
20
24
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -90,6 +90,32 @@ export const heartbeats = sqliteTable("heartbeats", {
|
|
|
90
90
|
createdAt: integer("created_at").notNull(),
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
export const taskRuns = sqliteTable("task_runs", {
|
|
94
|
+
id: text("id").primaryKey(),
|
|
95
|
+
agentId: text("agent_id").notNull(),
|
|
96
|
+
taskId: text("task_id").notNull(),
|
|
97
|
+
prompt: text("prompt").notNull(),
|
|
98
|
+
sessionId: text("session_id"),
|
|
99
|
+
status: text("status", { enum: ["pending", "running", "noop", "notified", "failed"] }).notNull(),
|
|
100
|
+
result: text("result"),
|
|
101
|
+
error: text("error"),
|
|
102
|
+
startedAt: integer("started_at"),
|
|
103
|
+
finishedAt: integer("finished_at"),
|
|
104
|
+
createdAt: integer("created_at").notNull(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export const taskConfigs = sqliteTable("task_configs", {
|
|
108
|
+
id: text("id").primaryKey(),
|
|
109
|
+
agent: text("agent").notNull(),
|
|
110
|
+
intervalMs: integer("interval_ms").notNull(),
|
|
111
|
+
prompt: text("prompt").notNull(),
|
|
112
|
+
channel: text("channel").notNull().default("telegram"),
|
|
113
|
+
chatId: text("chat_id").notNull(),
|
|
114
|
+
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
|
115
|
+
createdAt: integer("created_at").notNull(),
|
|
116
|
+
updatedAt: integer("updated_at").notNull(),
|
|
117
|
+
});
|
|
118
|
+
|
|
93
119
|
export const runtimeKv = sqliteTable("runtime_kv", {
|
|
94
120
|
key: text("key").primaryKey(),
|
|
95
121
|
value: text("value").notNull(),
|
|
@@ -106,3 +132,7 @@ export type MessageRow = typeof messages.$inferSelect;
|
|
|
106
132
|
export type NewMessageRow = typeof messages.$inferInsert;
|
|
107
133
|
export type HeartbeatRow = typeof heartbeats.$inferSelect;
|
|
108
134
|
export type NewHeartbeatRow = typeof heartbeats.$inferInsert;
|
|
135
|
+
export type TaskRunRow = typeof taskRuns.$inferSelect;
|
|
136
|
+
export type NewTaskRunRow = typeof taskRuns.$inferInsert;
|
|
137
|
+
export type TaskConfigRow = typeof taskConfigs.$inferSelect;
|
|
138
|
+
export type NewTaskConfigRow = typeof taskConfigs.$inferInsert;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
3
|
+
|
|
4
|
+
export type AvailableModelChoice = {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function getAvailableModelChoices(): Promise<AvailableModelChoice[]> {
|
|
10
|
+
const authStorage = AuthStorage.create();
|
|
11
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
12
|
+
const models = await modelRegistry.getAvailable();
|
|
13
|
+
|
|
14
|
+
return models.map(toChoice).sort((a, b) => a.label.localeCompare(b.label));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toChoice(model: Model<any>): AvailableModelChoice {
|
|
18
|
+
const modelWithName = model as Model<any> & { name?: string };
|
|
19
|
+
const id = `${model.provider}/${model.id}`;
|
|
20
|
+
const label = modelWithName.name ? `${modelWithName.name} (${id})` : id;
|
|
21
|
+
|
|
22
|
+
return { id, label };
|
|
23
|
+
}
|