@fickydev/pigent 0.1.10 → 0.1.11
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 +3 -0
- package/PLAN.md +29 -18
- package/TODO.md +1 -0
- package/package.json +1 -1
- package/src/agents/AgentRunner.ts +46 -48
- package/src/channels/types.ts +3 -0
- package/src/daemon/AgentDaemon.ts +2 -2
- package/src/daemon/Scheduler.ts +18 -19
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
- Added `task_configs` table and `TaskConfigRepository` for runtime task CRUD.
|
|
14
14
|
- Added `/task list`, `/task create <intervalMs> <prompt>`, `/task remove <id>` Telegram commands.
|
|
15
15
|
- Scheduler loads tasks from both config file and database, supports hot-add and hot-remove.
|
|
16
|
+
- Scheduler now shares AgentRunner's per-session lock instead of using its own PiAgentRunner.
|
|
17
|
+
- Tasks skip message persistence (inbound/outbound) in AgentRunner to avoid DB noise.
|
|
18
|
+
- Added `source` field to InboundMessage type (`"user"` default, `"task"` for scheduled runs).
|
|
16
19
|
|
|
17
20
|
- Added `bunx @fickydev/pigent <dir>` scaffold/setup/run CLI path.
|
|
18
21
|
- Added one-command setup-and-run CLI at `src/cli/run.ts` and `bun run run`.
|
package/PLAN.md
CHANGED
|
@@ -77,17 +77,18 @@ Skill is Pi-compatible instruction bundle, usually a `SKILL.md` folder. Agents c
|
|
|
77
77
|
|
|
78
78
|
Extension is Pi-compatible TypeScript/JavaScript module that adds tools, hooks, commands, or runtime behavior. Agents can load different extensions.
|
|
79
79
|
|
|
80
|
-
###
|
|
80
|
+
### Scheduled Tasks
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Scheduled tasks are periodic self-activations defined in `pigent.yaml` or created at runtime via `/task create`. Each task references an agent, an interval, a prompt, and a target channel.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
The global `Scheduler` ticks on a configurable interval and evaluates every task's due-ness against its last run timestamp.
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
86
|
+
Constraints:
|
|
87
|
+
|
|
88
|
+
- `NOOP` convention — agent returns "NOOP" to skip output
|
|
89
|
+
- Per-task lock — prevent stacking runs of the same task
|
|
90
|
+
- Max messages per hour — rate limit (not yet implemented)
|
|
91
|
+
- Tasks must share the same session lock as user messages to avoid concurrent Pi runs for the same agent+channel+chat
|
|
91
92
|
|
|
92
93
|
## Target Architecture
|
|
93
94
|
|
|
@@ -252,9 +253,10 @@ Initial entities:
|
|
|
252
253
|
- telegram chats
|
|
253
254
|
- telegram chat agents
|
|
254
255
|
- messages
|
|
255
|
-
-
|
|
256
|
+
- task_runs (history)
|
|
257
|
+
- task_configs (runtime task definitions)
|
|
258
|
+
- runtime_kv (offsets, daemon state)
|
|
256
259
|
- session model/thinking overrides
|
|
257
|
-
- tasks/events later
|
|
258
260
|
|
|
259
261
|
SQLite first. Keep schema types portable so PostgreSQL migration is straightforward.
|
|
260
262
|
|
|
@@ -282,7 +284,8 @@ src/
|
|
|
282
284
|
|
|
283
285
|
daemon/
|
|
284
286
|
AgentDaemon.ts
|
|
285
|
-
|
|
287
|
+
Scheduler.ts
|
|
288
|
+
taskDue.ts
|
|
286
289
|
|
|
287
290
|
channels/
|
|
288
291
|
types.ts
|
|
@@ -299,15 +302,19 @@ src/
|
|
|
299
302
|
|
|
300
303
|
pi/
|
|
301
304
|
PiAgentRunner.ts
|
|
302
|
-
|
|
305
|
+
PiAvailableModels.ts
|
|
306
|
+
PiModelResolver.ts
|
|
303
307
|
|
|
304
308
|
db/
|
|
305
309
|
client.ts
|
|
306
310
|
schema.ts
|
|
307
311
|
repositories/
|
|
308
312
|
AgentRepository.ts
|
|
313
|
+
HeartbeatRepository.ts
|
|
309
314
|
MessageRepository.ts
|
|
310
315
|
SessionRepository.ts
|
|
316
|
+
TaskRepository.ts
|
|
317
|
+
TaskConfigRepository.ts
|
|
311
318
|
TelegramRepository.ts
|
|
312
319
|
|
|
313
320
|
config/
|
|
@@ -384,13 +391,16 @@ drizzle/
|
|
|
384
391
|
- return responses
|
|
385
392
|
- persist inbound/outbound messages
|
|
386
393
|
|
|
387
|
-
### Milestone 7:
|
|
394
|
+
### Milestone 7: Scheduler
|
|
388
395
|
|
|
389
|
-
- scheduler
|
|
390
|
-
-
|
|
396
|
+
- global scheduler tick
|
|
397
|
+
- config-based tasks from `pigent.yaml`
|
|
398
|
+
- DB-based tasks (runtime CRUD via `/task` commands)
|
|
399
|
+
- per-task lock
|
|
400
|
+
- shared session lock with user messages (prevent concurrent Pi runs)
|
|
391
401
|
- `NOOP` handling
|
|
392
|
-
- notification
|
|
393
|
-
-
|
|
402
|
+
- notification rate limits (future)
|
|
403
|
+
- `/task list`, `/task create`, `/task remove` commands
|
|
394
404
|
|
|
395
405
|
### Milestone 8: Safety
|
|
396
406
|
|
|
@@ -425,8 +435,9 @@ Add Hono only after daemon works. Use it for:
|
|
|
425
435
|
|
|
426
436
|
- Should `/model <agentId> ...` support non-default agent sessions in the command itself?
|
|
427
437
|
- Should model availability errors block `/model` saves or save with a warning?
|
|
428
|
-
- Which Pi SDK session manager should be used for persistent sessions?
|
|
429
438
|
- How exactly should per-agent skills/extensions be loaded into Pi runtime?
|
|
430
439
|
- Should group instructions live in YAML, DB, or both?
|
|
431
440
|
- Should each chat have one default agent or multiple active agents?
|
|
432
441
|
- What minimum approval flow is needed before shell/file-write tools?
|
|
442
|
+
- Should scheduled tasks share the same session lock as user messages, or allow concurrent Pi runs?
|
|
443
|
+
- Should `/task create` enforce permission checks before allowing runtime task creation?
|
package/TODO.md
CHANGED
|
@@ -160,6 +160,7 @@
|
|
|
160
160
|
- [x] Add `/task create <intervalMs> <prompt>` command
|
|
161
161
|
- [x] Add `/task remove <id>` command
|
|
162
162
|
- [x] Scheduler loads tasks from both config file + DB
|
|
163
|
+
- [x] Share session lock between Scheduler and AgentRunner (prevent concurrent Pi runs for same agent+chat)
|
|
163
164
|
- [ ] Add max task runs per hour rate limit
|
|
164
165
|
- [ ] Add `/task status` or similar command
|
|
165
166
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { InboundMessage } from "../channels/types";
|
|
2
2
|
import type { Repositories } from "../db/repositories";
|
|
3
|
-
import type { AgentSessionRow } from "../db/schema";
|
|
4
3
|
import { logger } from "../logging/logger";
|
|
5
4
|
import { PiAgentRunner } from "../pi/PiAgentRunner";
|
|
6
5
|
import type { AgentRegistry } from "./AgentRegistry";
|
|
@@ -11,17 +10,12 @@ export type AgentRunInput = {
|
|
|
11
10
|
message: InboundMessage;
|
|
12
11
|
};
|
|
13
12
|
|
|
14
|
-
type AgentRunResponse = {
|
|
15
|
-
text: string;
|
|
16
|
-
error?: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
13
|
function sessionLockKey(input: AgentRunInput): string {
|
|
20
14
|
return [input.agentId, input.message.channel, input.message.chatId, input.message.threadId ?? ""].join(":");
|
|
21
15
|
}
|
|
22
16
|
|
|
23
17
|
export class AgentRunner {
|
|
24
|
-
|
|
18
|
+
readonly piRunner = new PiAgentRunner();
|
|
25
19
|
private readonly sessionLocks = new Map<string, Promise<void>>();
|
|
26
20
|
|
|
27
21
|
constructor(
|
|
@@ -41,42 +35,38 @@ export class AgentRunner {
|
|
|
41
35
|
threadId: input.message.threadId,
|
|
42
36
|
});
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
sessionId: session.id,
|
|
47
|
-
channel: input.message.channel,
|
|
48
|
-
direction: "inbound",
|
|
49
|
-
senderId: input.message.senderId,
|
|
50
|
-
chatId: input.message.chatId,
|
|
51
|
-
threadId: input.message.threadId,
|
|
52
|
-
content: input.text,
|
|
53
|
-
rawJson: input.message.raw,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const chatInstructions = await this.chatInstructions(input);
|
|
57
|
-
const response = await this.createResponse(input, session, chatInstructions);
|
|
58
|
-
|
|
59
|
-
if (response.error) {
|
|
38
|
+
// Skip DB persistence for task-triggered runs (Scheduler handles its own tracking)
|
|
39
|
+
if (input.message.source !== "task") {
|
|
60
40
|
await this.repositories.messages.create({
|
|
61
41
|
agentId: input.agentId,
|
|
62
42
|
sessionId: session.id,
|
|
63
43
|
channel: input.message.channel,
|
|
64
|
-
direction: "
|
|
44
|
+
direction: "inbound",
|
|
45
|
+
senderId: input.message.senderId,
|
|
65
46
|
chatId: input.message.chatId,
|
|
66
47
|
threadId: input.message.threadId,
|
|
67
|
-
content:
|
|
48
|
+
content: input.text,
|
|
49
|
+
rawJson: input.message.raw,
|
|
68
50
|
});
|
|
69
51
|
}
|
|
70
52
|
|
|
71
|
-
await this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
53
|
+
const chatInstructions = await this.chatInstructions(input);
|
|
54
|
+
const response = await this.createResponse(input, session, chatInstructions);
|
|
55
|
+
|
|
56
|
+
if (!response.handlerError) {
|
|
57
|
+
// Task runs persist their own messages via TaskRepository
|
|
58
|
+
if (input.message.source !== "task") {
|
|
59
|
+
await this.repositories.messages.create({
|
|
60
|
+
agentId: input.agentId,
|
|
61
|
+
sessionId: session.id,
|
|
62
|
+
channel: input.message.channel,
|
|
63
|
+
direction: "outbound",
|
|
64
|
+
chatId: input.message.chatId,
|
|
65
|
+
threadId: input.message.threadId,
|
|
66
|
+
content: response.text,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
80
70
|
|
|
81
71
|
return response.text;
|
|
82
72
|
}
|
|
@@ -103,9 +93,9 @@ export class AgentRunner {
|
|
|
103
93
|
|
|
104
94
|
private async createResponse(
|
|
105
95
|
input: AgentRunInput,
|
|
106
|
-
session:
|
|
96
|
+
session: any,
|
|
107
97
|
chatInstructions: string,
|
|
108
|
-
): Promise<
|
|
98
|
+
): Promise<{ text: string; handlerError?: string }> {
|
|
109
99
|
if (process.env.PIGENT_FAKE_AGENT === "1") {
|
|
110
100
|
return { text: this.fakeResponse(input.agentId, input.text) };
|
|
111
101
|
}
|
|
@@ -131,26 +121,34 @@ export class AgentRunner {
|
|
|
131
121
|
});
|
|
132
122
|
|
|
133
123
|
if (process.env.PIGENT_FALLBACK_FAKE_AGENT === "1") {
|
|
134
|
-
return { text: this.fakeResponse(input.agentId, input.text),
|
|
124
|
+
return { text: this.fakeResponse(input.agentId, input.text), handlerError: errorMessage };
|
|
135
125
|
}
|
|
136
126
|
|
|
137
|
-
return { text: "Agent failed to respond. Please try again later.",
|
|
127
|
+
return { text: "Agent failed to respond. Please try again later.", handlerError: errorMessage };
|
|
138
128
|
}
|
|
139
129
|
}
|
|
140
130
|
|
|
141
131
|
private composePrompt(input: AgentRunInput, chatInstructions: string): string {
|
|
142
|
-
|
|
132
|
+
const parts: string[] = [
|
|
143
133
|
`[Channel]\n${input.message.channel}`,
|
|
144
134
|
`[Chat]\n${input.message.chatId}`,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.
|
|
153
|
-
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
if (input.message.threadId) {
|
|
138
|
+
parts.push(`[Thread]\n${input.message.threadId}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (input.message.senderName || input.message.senderId) {
|
|
142
|
+
parts.push(`[User]\n${input.message.senderName ?? input.message.senderId}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (chatInstructions) {
|
|
146
|
+
parts.push(`[Chat Instructions]\n${chatInstructions}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
parts.push(`[Message]\n${input.text}`);
|
|
150
|
+
|
|
151
|
+
return parts.join("\n\n");
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
private async chatInstructions(input: AgentRunInput): Promise<string> {
|
package/src/channels/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type ChannelId = "telegram";
|
|
2
2
|
|
|
3
|
+
export type MessageSource = "user" | "task";
|
|
4
|
+
|
|
3
5
|
export type InboundMessage = {
|
|
4
6
|
id: string;
|
|
5
7
|
channel: ChannelId;
|
|
@@ -10,6 +12,7 @@ export type InboundMessage = {
|
|
|
10
12
|
text: string;
|
|
11
13
|
raw: unknown;
|
|
12
14
|
receivedAt: Date;
|
|
15
|
+
source?: MessageSource;
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
export type InlineKeyboardButton = {
|
|
@@ -26,10 +26,10 @@ export class AgentDaemon {
|
|
|
26
26
|
) {
|
|
27
27
|
this.adapters = createAdapters(repositories);
|
|
28
28
|
this.registry = new AgentRegistry(config, repositories);
|
|
29
|
-
this.
|
|
29
|
+
this.runner = new AgentRunner(this.registry, repositories);
|
|
30
|
+
this.scheduler = new Scheduler(config.scheduler, this.registry, repositories, this.adapters, this.runner);
|
|
30
31
|
this.commands = new BotCommandHandler(this.registry, repositories, config.modelChoices, this.scheduler);
|
|
31
32
|
this.router = new MessageRouter(repositories, this.registry);
|
|
32
|
-
this.runner = new AgentRunner(this.registry, repositories);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
static async create(): Promise<AgentDaemon> {
|
package/src/daemon/Scheduler.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type { SchedulerConfig, TaskConfig } from "../config/schemas";
|
|
|
2
2
|
import type { Repositories } from "../db/repositories";
|
|
3
3
|
import type { TaskConfigRow } from "../db/schema";
|
|
4
4
|
import { logger } from "../logging/logger";
|
|
5
|
-
import { PiAgentRunner } from "../pi/PiAgentRunner";
|
|
6
5
|
import type { AgentRegistry } from "../agents/AgentRegistry";
|
|
6
|
+
import type { AgentRunner } from "../agents/AgentRunner";
|
|
7
7
|
import type { ChannelAdapter } from "../channels/types";
|
|
8
8
|
import { isTaskDue } from "./taskDue";
|
|
9
9
|
|
|
@@ -11,7 +11,6 @@ export class Scheduler {
|
|
|
11
11
|
private running = false;
|
|
12
12
|
private tickTimer: ReturnType<typeof setTimeout> | null = null;
|
|
13
13
|
private readonly taskLocks = new Map<string, boolean>();
|
|
14
|
-
private readonly piRunner = new PiAgentRunner();
|
|
15
14
|
private tasks: TaskConfig[] = [];
|
|
16
15
|
|
|
17
16
|
constructor(
|
|
@@ -19,6 +18,7 @@ export class Scheduler {
|
|
|
19
18
|
private readonly registry: AgentRegistry,
|
|
20
19
|
private readonly repositories: Repositories,
|
|
21
20
|
private readonly adapters: ChannelAdapter[],
|
|
21
|
+
private readonly agentRunner: AgentRunner,
|
|
22
22
|
) {}
|
|
23
23
|
|
|
24
24
|
async start(): Promise<void> {
|
|
@@ -124,8 +124,7 @@ export class Scheduler {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
private async runTaskIfDue(task: TaskConfig): Promise<void> {
|
|
127
|
-
|
|
128
|
-
if (this.taskLocks.get(lockKey)) return;
|
|
127
|
+
if (this.taskLocks.get(task.id)) return;
|
|
129
128
|
|
|
130
129
|
const lastRun = await this.repositories.taskRuns.findLatest(task.agent, task.id);
|
|
131
130
|
if (!isTaskDue(lastRun?.createdAt ?? null, task.intervalMs)) return;
|
|
@@ -136,12 +135,12 @@ export class Scheduler {
|
|
|
136
135
|
return;
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
this.taskLocks.set(
|
|
138
|
+
this.taskLocks.set(task.id, true);
|
|
140
139
|
|
|
141
140
|
try {
|
|
142
141
|
await this.executeTask(task, agent);
|
|
143
142
|
} finally {
|
|
144
|
-
this.taskLocks.set(
|
|
143
|
+
this.taskLocks.set(task.id, false);
|
|
145
144
|
}
|
|
146
145
|
}
|
|
147
146
|
|
|
@@ -157,21 +156,21 @@ export class Scheduler {
|
|
|
157
156
|
await this.repositories.taskRuns.markRunning(run.id);
|
|
158
157
|
|
|
159
158
|
try {
|
|
160
|
-
const session = await this.repositories.sessions.getOrCreate({
|
|
161
|
-
agentId: task.agent,
|
|
162
|
-
channel: task.channel,
|
|
163
|
-
chatId: task.chatId ?? `task:${task.id}`,
|
|
164
|
-
threadId: null,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
159
|
const fullPrompt = this.composeTaskPrompt(task);
|
|
168
|
-
const profile = this.registry.getProfile(agent.profile);
|
|
169
160
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
161
|
+
// Use AgentRunner to run the prompt — shares the session lock with user messages
|
|
162
|
+
const response = await this.agentRunner.run({
|
|
163
|
+
agentId: task.agent,
|
|
164
|
+
text: fullPrompt,
|
|
165
|
+
message: {
|
|
166
|
+
id: "task:" + task.id,
|
|
167
|
+
channel: task.channel as any,
|
|
168
|
+
chatId: task.chatId!,
|
|
169
|
+
text: fullPrompt,
|
|
170
|
+
raw: { type: "task", taskId: task.id },
|
|
171
|
+
receivedAt: new Date(),
|
|
172
|
+
source: "task",
|
|
173
|
+
},
|
|
175
174
|
});
|
|
176
175
|
|
|
177
176
|
const trimmed = response.trim();
|