@fickydev/pigent 0.1.4 → 0.1.6
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 +5 -0
- package/TODO.md +8 -8
- package/package.json +1 -1
- package/src/agents/AgentRegistry.ts +45 -0
- package/src/agents/AgentRunner.ts +76 -25
- package/src/agents/BotCommandHandler.ts +3 -3
- package/src/agents/MessageRouter.ts +6 -3
- package/src/cli/run.ts +18 -1
- package/src/daemon/AgentDaemon.ts +10 -9
- package/src/db/repositories/HeartbeatRepository.ts +129 -0
- package/src/db/repositories/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
- Added short `pigent` command shim for `status`, `logs`, `start`, `stop`, `restart`, `update`, and `setup`.
|
|
35
35
|
- Simplified published CLI flow so `bunx @fickydev/pigent` runs setup and starts Pigent without install/start/typecheck prompts.
|
|
36
36
|
- Added automatic database migrations at daemon startup.
|
|
37
|
+
- Added `HeartbeatRepository` for creating, updating, and querying heartbeat runs.
|
|
38
|
+
- Added `AgentRegistry` to centralize loaded agent/profile lookup and agent DB sync.
|
|
39
|
+
- Added safe Pi runner failure replies with internal error persistence.
|
|
40
|
+
- Added per-session locking to serialize Pi runs for the same agent/channel/chat/thread.
|
|
41
|
+
- Added Telegram chat instructions to Pi prompt composition.
|
|
37
42
|
- Kept daemon process alive after startup so CLI runs do not exit after `pigent ready`.
|
|
38
43
|
|
|
39
44
|
### Changed
|
package/TODO.md
CHANGED
|
@@ -66,12 +66,12 @@
|
|
|
66
66
|
- [x] Define `messages` table
|
|
67
67
|
- [x] Define `heartbeats` table
|
|
68
68
|
- [x] Define `runtime_kv` table for offsets and daemon state
|
|
69
|
-
- [
|
|
69
|
+
- [x] Implement repositories
|
|
70
70
|
- [x] `AgentRepository`
|
|
71
71
|
- [x] `SessionRepository`
|
|
72
72
|
- [x] `MessageRepository`
|
|
73
73
|
- [x] `TelegramRepository`
|
|
74
|
-
- [
|
|
74
|
+
- [x] `HeartbeatRepository`
|
|
75
75
|
|
|
76
76
|
## Channel Layer
|
|
77
77
|
|
|
@@ -106,18 +106,18 @@
|
|
|
106
106
|
|
|
107
107
|
## Agent Runtime
|
|
108
108
|
|
|
109
|
-
- [
|
|
109
|
+
- [x] Implement `AgentRegistry`
|
|
110
110
|
- [x] Implement `AgentDaemon`
|
|
111
111
|
- [x] Implement `AgentRunner`
|
|
112
112
|
- [ ] Implement `PiSessionFactory`
|
|
113
113
|
- [x] Implement `PiAgentRunner`
|
|
114
114
|
- [x] Create/get session by `agentId + channel + chatId + threadId`
|
|
115
|
-
- [
|
|
115
|
+
- [x] Compose prompt with chat instructions
|
|
116
116
|
- [x] Compose basic channel/chat/user prompt for Pi runner
|
|
117
117
|
- [x] Persist inbound message before Pi run
|
|
118
118
|
- [x] Persist outbound response after Pi run
|
|
119
|
-
- [
|
|
120
|
-
- [
|
|
119
|
+
- [x] Handle Pi errors and send safe error reply
|
|
120
|
+
- [x] Add per-session lock to prevent concurrent Pi runs
|
|
121
121
|
|
|
122
122
|
## Pi Integration
|
|
123
123
|
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
- [ ] Add per-agent heartbeat lock
|
|
137
137
|
- [ ] Add heartbeat prompt composition
|
|
138
138
|
- [ ] Add `NOOP` handling
|
|
139
|
-
- [
|
|
139
|
+
- [x] Persist heartbeat start/result/failure
|
|
140
140
|
- [ ] Add notification cooldown
|
|
141
141
|
- [ ] Add max heartbeat messages per hour
|
|
142
142
|
- [ ] Send heartbeat output to configured channel only when useful
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
## Commands UX
|
|
157
157
|
|
|
158
158
|
- [x] `/agents` list agents available in chat
|
|
159
|
-
- [
|
|
159
|
+
- [x] `/agent <id> <message>` route message
|
|
160
160
|
- [ ] `/default-agent <id>` set chat default
|
|
161
161
|
- [ ] `/instructions <text>` set chat instructions
|
|
162
162
|
- [ ] `/sessions` list active sessions for chat
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { LoadedAgentConfig, LoadedConfig, ProfileConfig } from "../config/schemas";
|
|
2
|
+
import type { Repositories } from "../db/repositories";
|
|
3
|
+
|
|
4
|
+
export class AgentRegistry {
|
|
5
|
+
private readonly agentsById: Map<string, LoadedAgentConfig>;
|
|
6
|
+
private readonly profilesById: Map<string, ProfileConfig>;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly config: LoadedConfig,
|
|
10
|
+
private readonly repositories: Repositories,
|
|
11
|
+
) {
|
|
12
|
+
this.agentsById = new Map(config.agents.map((agent) => [agent.id, agent]));
|
|
13
|
+
this.profilesById = new Map(config.profiles.map((profile) => [profile.id, profile]));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async syncConfiguredAgents(): Promise<void> {
|
|
17
|
+
for (const agent of this.listAgents()) {
|
|
18
|
+
await this.repositories.agents.upsertLoadedAgent(agent);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
listAgents(): LoadedAgentConfig[] {
|
|
23
|
+
return [...this.agentsById.values()];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
listProfiles(): ProfileConfig[] {
|
|
27
|
+
return [...this.profilesById.values()];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getAgent(id: string): LoadedAgentConfig | null {
|
|
31
|
+
return this.agentsById.get(id) ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getProfile(id: string): ProfileConfig | null {
|
|
35
|
+
return this.profilesById.get(id) ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
hasAgent(id: string): boolean {
|
|
39
|
+
return this.agentsById.has(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
defaultAgentId(): string | null {
|
|
43
|
+
return this.listAgents()[0]?.id ?? null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { InboundMessage } from "../channels/types";
|
|
2
|
-
import type { LoadedAgentConfig, LoadedConfig, ProfileConfig } from "../config/schemas";
|
|
3
2
|
import type { Repositories } from "../db/repositories";
|
|
4
3
|
import { logger } from "../logging/logger";
|
|
5
4
|
import { PiAgentRunner } from "../pi/PiAgentRunner";
|
|
5
|
+
import type { AgentRegistry } from "./AgentRegistry";
|
|
6
6
|
|
|
7
7
|
export type AgentRunInput = {
|
|
8
8
|
agentId: string;
|
|
@@ -10,15 +10,29 @@ export type AgentRunInput = {
|
|
|
10
10
|
message: InboundMessage;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
type AgentRunResponse = {
|
|
14
|
+
text: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function sessionLockKey(input: AgentRunInput): string {
|
|
19
|
+
return [input.agentId, input.message.channel, input.message.chatId, input.message.threadId ?? ""].join(":");
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
export class AgentRunner {
|
|
14
23
|
private readonly piRunner = new PiAgentRunner();
|
|
24
|
+
private readonly sessionLocks = new Map<string, Promise<void>>();
|
|
15
25
|
|
|
16
26
|
constructor(
|
|
17
|
-
private readonly
|
|
27
|
+
private readonly registry: AgentRegistry,
|
|
18
28
|
private readonly repositories: Repositories,
|
|
19
29
|
) {}
|
|
20
30
|
|
|
21
31
|
async run(input: AgentRunInput): Promise<string> {
|
|
32
|
+
return this.withSessionLock(input, () => this.runUnlocked(input));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async runUnlocked(input: AgentRunInput): Promise<string> {
|
|
22
36
|
const session = await this.repositories.sessions.getOrCreate({
|
|
23
37
|
agentId: input.agentId,
|
|
24
38
|
channel: input.message.channel,
|
|
@@ -38,7 +52,20 @@ export class AgentRunner {
|
|
|
38
52
|
rawJson: input.message.raw,
|
|
39
53
|
});
|
|
40
54
|
|
|
41
|
-
const
|
|
55
|
+
const chatInstructions = await this.chatInstructions(input);
|
|
56
|
+
const response = await this.createResponse(input, chatInstructions);
|
|
57
|
+
|
|
58
|
+
if (response.error) {
|
|
59
|
+
await this.repositories.messages.create({
|
|
60
|
+
agentId: input.agentId,
|
|
61
|
+
sessionId: session.id,
|
|
62
|
+
channel: input.message.channel,
|
|
63
|
+
direction: "internal",
|
|
64
|
+
chatId: input.message.chatId,
|
|
65
|
+
threadId: input.message.threadId,
|
|
66
|
+
content: response.error,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
42
69
|
|
|
43
70
|
await this.repositories.messages.create({
|
|
44
71
|
agentId: input.agentId,
|
|
@@ -47,41 +74,65 @@ export class AgentRunner {
|
|
|
47
74
|
direction: "outbound",
|
|
48
75
|
chatId: input.message.chatId,
|
|
49
76
|
threadId: input.message.threadId,
|
|
50
|
-
content: response,
|
|
77
|
+
content: response.text,
|
|
51
78
|
});
|
|
52
79
|
|
|
53
|
-
return response;
|
|
80
|
+
return response.text;
|
|
54
81
|
}
|
|
55
82
|
|
|
56
|
-
private async
|
|
83
|
+
private async withSessionLock<T>(input: AgentRunInput, task: () => Promise<T>): Promise<T> {
|
|
84
|
+
const key = sessionLockKey(input);
|
|
85
|
+
const previous = this.sessionLocks.get(key) ?? Promise.resolve();
|
|
86
|
+
const run = previous.catch(() => undefined).then(task);
|
|
87
|
+
const tail = run.then(
|
|
88
|
+
() => undefined,
|
|
89
|
+
() => undefined,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
this.sessionLocks.set(key, tail);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return await run;
|
|
96
|
+
} finally {
|
|
97
|
+
if (this.sessionLocks.get(key) === tail) {
|
|
98
|
+
this.sessionLocks.delete(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async createResponse(input: AgentRunInput, chatInstructions: string): Promise<AgentRunResponse> {
|
|
57
104
|
if (process.env.PIGENT_FAKE_AGENT === "1") {
|
|
58
|
-
return this.fakeResponse(input.agentId, input.text);
|
|
105
|
+
return { text: this.fakeResponse(input.agentId, input.text) };
|
|
59
106
|
}
|
|
60
107
|
|
|
61
|
-
const agent = this.
|
|
62
|
-
if (!agent) return `Unknown agent: ${input.agentId}
|
|
108
|
+
const agent = this.registry.getAgent(input.agentId);
|
|
109
|
+
if (!agent) return { text: `Unknown agent: ${input.agentId}` };
|
|
63
110
|
|
|
64
111
|
try {
|
|
65
|
-
|
|
112
|
+
const text = await this.piRunner.run({
|
|
66
113
|
agent,
|
|
67
|
-
profile: this.
|
|
68
|
-
prompt: this.composePrompt(input),
|
|
114
|
+
profile: this.registry.getProfile(agent.profile),
|
|
115
|
+
prompt: this.composePrompt(input, chatInstructions),
|
|
69
116
|
});
|
|
117
|
+
|
|
118
|
+
return { text };
|
|
70
119
|
} catch (error) {
|
|
120
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
121
|
+
|
|
71
122
|
logger.error("pi runner failed", {
|
|
72
123
|
agentId: input.agentId,
|
|
73
|
-
error:
|
|
124
|
+
error: errorMessage,
|
|
74
125
|
});
|
|
75
126
|
|
|
76
127
|
if (process.env.PIGENT_FALLBACK_FAKE_AGENT === "1") {
|
|
77
|
-
return this.fakeResponse(input.agentId, input.text);
|
|
128
|
+
return { text: this.fakeResponse(input.agentId, input.text), error: errorMessage };
|
|
78
129
|
}
|
|
79
130
|
|
|
80
|
-
return "Agent
|
|
131
|
+
return { text: "Agent failed to respond. Please try again later.", error: errorMessage };
|
|
81
132
|
}
|
|
82
133
|
}
|
|
83
134
|
|
|
84
|
-
private composePrompt(input: AgentRunInput): string {
|
|
135
|
+
private composePrompt(input: AgentRunInput, chatInstructions: string): string {
|
|
85
136
|
return [
|
|
86
137
|
`[Channel]\n${input.message.channel}`,
|
|
87
138
|
`[Chat]\n${input.message.chatId}`,
|
|
@@ -89,24 +140,24 @@ export class AgentRunner {
|
|
|
89
140
|
input.message.senderName || input.message.senderId
|
|
90
141
|
? `[User]\n${input.message.senderName ?? input.message.senderId}`
|
|
91
142
|
: null,
|
|
143
|
+
chatInstructions ? `[Chat Instructions]\n${chatInstructions}` : null,
|
|
92
144
|
`[Message]\n${input.text}`,
|
|
93
145
|
]
|
|
94
146
|
.filter(Boolean)
|
|
95
147
|
.join("\n\n");
|
|
96
148
|
}
|
|
97
149
|
|
|
98
|
-
private
|
|
99
|
-
|
|
100
|
-
const name = agent?.name ?? agentId;
|
|
150
|
+
private async chatInstructions(input: AgentRunInput): Promise<string> {
|
|
151
|
+
if (input.message.channel !== "telegram") return "";
|
|
101
152
|
|
|
102
|
-
|
|
153
|
+
const chat = await this.repositories.telegram.findChat(input.message.chatId);
|
|
154
|
+
return chat?.instructions.trim() ?? "";
|
|
103
155
|
}
|
|
104
156
|
|
|
105
|
-
private
|
|
106
|
-
|
|
107
|
-
|
|
157
|
+
private fakeResponse(agentId: string, text: string): string {
|
|
158
|
+
const agent = this.registry.getAgent(agentId);
|
|
159
|
+
const name = agent?.name ?? agentId;
|
|
108
160
|
|
|
109
|
-
|
|
110
|
-
return this.config.profiles.find((candidate) => candidate.id === profileId) ?? null;
|
|
161
|
+
return `[${name}] fake runner received: ${text || "(empty message)"}`;
|
|
111
162
|
}
|
|
112
163
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InboundMessage } from "../channels/types";
|
|
2
|
-
import type { LoadedConfig } from "../config/schemas";
|
|
3
2
|
import type { Repositories } from "../db/repositories";
|
|
3
|
+
import type { AgentRegistry } from "./AgentRegistry";
|
|
4
4
|
|
|
5
5
|
export type BotCommandResult =
|
|
6
6
|
| {
|
|
@@ -13,7 +13,7 @@ export type BotCommandResult =
|
|
|
13
13
|
|
|
14
14
|
export class BotCommandHandler {
|
|
15
15
|
constructor(
|
|
16
|
-
private readonly
|
|
16
|
+
private readonly registry: AgentRegistry,
|
|
17
17
|
private readonly repositories: Repositories,
|
|
18
18
|
) {}
|
|
19
19
|
|
|
@@ -48,7 +48,7 @@ export class BotCommandHandler {
|
|
|
48
48
|
return "No agents configured for this chat.";
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const allAgentIds = this.
|
|
51
|
+
const allAgentIds = this.registry.listAgents().map((agent) => agent.id);
|
|
52
52
|
const allowedAgentIds = [];
|
|
53
53
|
|
|
54
54
|
for (const agentId of allAgentIds) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { InboundMessage } from "../channels/types";
|
|
2
2
|
import type { Repositories } from "../db/repositories";
|
|
3
|
+
import type { AgentRegistry } from "./AgentRegistry";
|
|
3
4
|
|
|
4
5
|
export type RouteResult =
|
|
5
6
|
| {
|
|
@@ -14,7 +15,10 @@ export type RouteResult =
|
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export class MessageRouter {
|
|
17
|
-
constructor(
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly repositories: Repositories,
|
|
20
|
+
private readonly registry: AgentRegistry,
|
|
21
|
+
) {}
|
|
18
22
|
|
|
19
23
|
async route(message: InboundMessage): Promise<RouteResult> {
|
|
20
24
|
if (message.channel !== "telegram") {
|
|
@@ -54,8 +58,7 @@ export class MessageRouter {
|
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
if (!agent) {
|
|
61
|
+
if (!this.registry.hasAgent(agentId)) {
|
|
59
62
|
return {
|
|
60
63
|
ok: false,
|
|
61
64
|
reason: "unknown_agent",
|
package/src/cli/run.ts
CHANGED
|
@@ -63,12 +63,17 @@ async function main(): Promise<void> {
|
|
|
63
63
|
await runForeground(resolveForegroundDir());
|
|
64
64
|
return;
|
|
65
65
|
default:
|
|
66
|
+
if (await isServiceActive()) {
|
|
67
|
+
console.log("Pigent already running.");
|
|
68
|
+
console.log("Commands: pigent status, pigent logs, pigent restart, pigent update");
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
66
71
|
await installAndStartService(appDir);
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
async function installAndStartService(targetDir: string): Promise<void> {
|
|
71
|
-
await ensureConfiguredApp(targetDir,
|
|
76
|
+
await ensureConfiguredApp(targetDir, needsSetup(targetDir));
|
|
72
77
|
|
|
73
78
|
if (!(await hasSystemctl())) {
|
|
74
79
|
console.log("[pigent] systemd user service unavailable; running in foreground");
|
|
@@ -89,6 +94,7 @@ async function installAndStartService(targetDir: string): Promise<void> {
|
|
|
89
94
|
console.log(" pigent stop");
|
|
90
95
|
console.log(" pigent restart");
|
|
91
96
|
console.log(" pigent update");
|
|
97
|
+
process.exit(0);
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
async function startService(targetDir: string): Promise<void> {
|
|
@@ -98,6 +104,7 @@ async function startService(targetDir: string): Promise<void> {
|
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
await systemctl("start");
|
|
107
|
+
process.exit(0);
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
async function updateApp(targetDir: string): Promise<void> {
|
|
@@ -117,6 +124,7 @@ async function updateApp(targetDir: string): Promise<void> {
|
|
|
117
124
|
await systemctl("enable", true, `${serviceName}.service`);
|
|
118
125
|
await systemctl("restart");
|
|
119
126
|
console.log("[pigent] updated and restarted service");
|
|
127
|
+
process.exit(0);
|
|
120
128
|
} else {
|
|
121
129
|
console.log("[pigent] updated. Run `pigent run` to start foreground daemon.");
|
|
122
130
|
}
|
|
@@ -259,6 +267,15 @@ async function hasSystemctl(): Promise<boolean> {
|
|
|
259
267
|
return result.exitCode === 0;
|
|
260
268
|
}
|
|
261
269
|
|
|
270
|
+
async function isServiceActive(): Promise<boolean> {
|
|
271
|
+
if (!(await hasSystemctl())) return false;
|
|
272
|
+
const proc = Bun.spawn(["systemctl", "--user", "is-active", "--quiet", `${serviceName}.service`], {
|
|
273
|
+
stdout: "ignore",
|
|
274
|
+
stderr: "ignore",
|
|
275
|
+
});
|
|
276
|
+
return (await proc.exited) === 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
262
279
|
function servicePath(): string {
|
|
263
280
|
return join(homedir(), ".config", "systemd", "user", `${serviceName}.service`);
|
|
264
281
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AgentRegistry } from "../agents/AgentRegistry";
|
|
1
2
|
import { AgentRunner } from "../agents/AgentRunner";
|
|
2
3
|
import { BotCommandHandler } from "../agents/BotCommandHandler";
|
|
3
4
|
import { MessageRouter } from "../agents/MessageRouter";
|
|
@@ -13,6 +14,7 @@ export class AgentDaemon {
|
|
|
13
14
|
private running = false;
|
|
14
15
|
private readonly adapters: ChannelAdapter[];
|
|
15
16
|
private readonly commands: BotCommandHandler;
|
|
17
|
+
private readonly registry: AgentRegistry;
|
|
16
18
|
private readonly router: MessageRouter;
|
|
17
19
|
private readonly runner: AgentRunner;
|
|
18
20
|
|
|
@@ -21,9 +23,10 @@ export class AgentDaemon {
|
|
|
21
23
|
private readonly repositories: Repositories,
|
|
22
24
|
) {
|
|
23
25
|
this.adapters = createAdapters(repositories);
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
26
|
+
this.registry = new AgentRegistry(config, repositories);
|
|
27
|
+
this.commands = new BotCommandHandler(this.registry, repositories);
|
|
28
|
+
this.router = new MessageRouter(repositories, this.registry);
|
|
29
|
+
this.runner = new AgentRunner(this.registry, repositories);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
static async create(): Promise<AgentDaemon> {
|
|
@@ -43,8 +46,8 @@ export class AgentDaemon {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
logger.info("pigent daemon started", {
|
|
46
|
-
agents: this.
|
|
47
|
-
profiles: this.
|
|
49
|
+
agents: this.registry.listAgents().length,
|
|
50
|
+
profiles: this.registry.listProfiles().length,
|
|
48
51
|
telegramChats: this.config.telegramChats.length,
|
|
49
52
|
adapters: this.adapters.map((adapter) => adapter.id),
|
|
50
53
|
});
|
|
@@ -63,9 +66,7 @@ export class AgentDaemon {
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
private async syncConfiguredState(): Promise<void> {
|
|
66
|
-
|
|
67
|
-
await this.repositories.agents.upsertLoadedAgent(agent);
|
|
68
|
-
}
|
|
69
|
+
await this.registry.syncConfiguredAgents();
|
|
69
70
|
|
|
70
71
|
for (const chat of this.config.telegramChats) {
|
|
71
72
|
await this.repositories.telegram.upsertConfiguredChat(chat);
|
|
@@ -125,7 +126,7 @@ export class AgentDaemon {
|
|
|
125
126
|
if (message.channel !== "telegram") return;
|
|
126
127
|
if (process.env.PIGENT_AUTO_SETUP_CHATS !== "1") return;
|
|
127
128
|
|
|
128
|
-
const defaultAgentId = process.env.PIGENT_AUTO_SETUP_DEFAULT_AGENT ?? this.
|
|
129
|
+
const defaultAgentId = process.env.PIGENT_AUTO_SETUP_DEFAULT_AGENT ?? this.registry.defaultAgentId();
|
|
129
130
|
if (!defaultAgentId) return;
|
|
130
131
|
|
|
131
132
|
await this.repositories.telegram.autoConfigureChat(message, defaultAgentId);
|
|
@@ -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
|
+
}
|
|
@@ -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),
|