@poncho-ai/harness 0.37.2 → 0.38.0
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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +20 -0
- package/dist/index.d.ts +66 -34
- package/dist/index.js +341 -50
- package/package.json +1 -1
- package/src/harness.ts +4 -3
- package/src/reminder-store.ts +183 -16
- package/src/reminder-tools.ts +102 -6
- package/src/state.ts +29 -3
- package/src/storage/engine.ts +10 -10
- package/src/storage/memory-engine.ts +25 -10
- package/src/storage/schema.ts +11 -0
- package/src/storage/sql-dialect.ts +80 -15
- package/src/storage/store-adapters.ts +11 -11
- package/src/vfs/bash-manager.ts +16 -2
- package/src/vfs/edit-file-tool.ts +9 -8
- package/src/vfs/read-file-tool.ts +14 -15
- package/src/vfs/write-file-tool.ts +6 -5
- package/test/reminder-store.test.ts +193 -4
- package/test/storage-engine.test.ts +25 -0
package/package.json
CHANGED
package/src/harness.ts
CHANGED
|
@@ -1477,9 +1477,10 @@ export class AgentHarness {
|
|
|
1477
1477
|
);
|
|
1478
1478
|
// Register VFS tools
|
|
1479
1479
|
this.registerIfMissing(createBashTool(this.bashManager));
|
|
1480
|
-
this.
|
|
1481
|
-
this.registerIfMissing(
|
|
1482
|
-
this.registerIfMissing(
|
|
1480
|
+
const getFs = (tenantId: string) => this.bashManager!.getFs(tenantId);
|
|
1481
|
+
this.registerIfMissing(createReadFileTool(getFs));
|
|
1482
|
+
this.registerIfMissing(createEditFileTool(getFs));
|
|
1483
|
+
this.registerIfMissing(createWriteFileTool(getFs));
|
|
1483
1484
|
|
|
1484
1485
|
// --- Isolate (V8 sandboxed code execution) ---
|
|
1485
1486
|
if (config?.isolate) {
|
package/src/reminder-store.ts
CHANGED
|
@@ -6,6 +6,22 @@ import type { StateConfig } from "./state.js";
|
|
|
6
6
|
|
|
7
7
|
export type ReminderStatus = "pending" | "fired" | "cancelled";
|
|
8
8
|
|
|
9
|
+
export type RecurrenceType = "daily" | "weekly" | "monthly" | "cron";
|
|
10
|
+
|
|
11
|
+
export interface Recurrence {
|
|
12
|
+
type: RecurrenceType;
|
|
13
|
+
/** Repeat every N units (e.g. every 2 days). Defaults to 1. */
|
|
14
|
+
interval?: number;
|
|
15
|
+
/** For weekly: which days (0=Sun … 6=Sat). */
|
|
16
|
+
daysOfWeek?: number[];
|
|
17
|
+
/** For type "cron": a 5-field cron expression. */
|
|
18
|
+
expression?: string;
|
|
19
|
+
/** Stop recurring after this epoch-ms timestamp. */
|
|
20
|
+
endsAt?: number;
|
|
21
|
+
/** Stop recurring after this many total firings. */
|
|
22
|
+
maxOccurrences?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
export interface Reminder {
|
|
10
26
|
id: string;
|
|
11
27
|
task: string;
|
|
@@ -16,18 +32,24 @@ export interface Reminder {
|
|
|
16
32
|
conversationId: string;
|
|
17
33
|
ownerId?: string;
|
|
18
34
|
tenantId?: string | null;
|
|
35
|
+
recurrence?: Recurrence | null;
|
|
36
|
+
occurrenceCount?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ReminderCreateInput {
|
|
40
|
+
task: string;
|
|
41
|
+
scheduledAt: number;
|
|
42
|
+
timezone?: string;
|
|
43
|
+
conversationId: string;
|
|
44
|
+
ownerId?: string;
|
|
45
|
+
tenantId?: string | null;
|
|
46
|
+
recurrence?: Recurrence | null;
|
|
19
47
|
}
|
|
20
48
|
|
|
21
49
|
export interface ReminderStore {
|
|
22
50
|
list(): Promise<Reminder[]>;
|
|
23
|
-
create(input:
|
|
24
|
-
|
|
25
|
-
scheduledAt: number;
|
|
26
|
-
timezone?: string;
|
|
27
|
-
conversationId: string;
|
|
28
|
-
ownerId?: string;
|
|
29
|
-
tenantId?: string | null;
|
|
30
|
-
}): Promise<Reminder>;
|
|
51
|
+
create(input: ReminderCreateInput): Promise<Reminder>;
|
|
52
|
+
update(id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder>;
|
|
31
53
|
cancel(id: string): Promise<Reminder>;
|
|
32
54
|
delete(id: string): Promise<void>;
|
|
33
55
|
}
|
|
@@ -63,14 +85,7 @@ class InMemoryReminderStore implements ReminderStore {
|
|
|
63
85
|
return [...this.reminders];
|
|
64
86
|
}
|
|
65
87
|
|
|
66
|
-
async create(input: {
|
|
67
|
-
task: string;
|
|
68
|
-
scheduledAt: number;
|
|
69
|
-
timezone?: string;
|
|
70
|
-
conversationId: string;
|
|
71
|
-
ownerId?: string;
|
|
72
|
-
tenantId?: string | null;
|
|
73
|
-
}): Promise<Reminder> {
|
|
88
|
+
async create(input: ReminderCreateInput): Promise<Reminder> {
|
|
74
89
|
const reminder: Reminder = {
|
|
75
90
|
id: generateId(),
|
|
76
91
|
task: input.task,
|
|
@@ -81,12 +96,23 @@ class InMemoryReminderStore implements ReminderStore {
|
|
|
81
96
|
conversationId: input.conversationId,
|
|
82
97
|
ownerId: input.ownerId,
|
|
83
98
|
tenantId: input.tenantId,
|
|
99
|
+
recurrence: input.recurrence ?? null,
|
|
100
|
+
occurrenceCount: 0,
|
|
84
101
|
};
|
|
85
102
|
this.reminders = pruneStale(this.reminders);
|
|
86
103
|
this.reminders.push(reminder);
|
|
87
104
|
return reminder;
|
|
88
105
|
}
|
|
89
106
|
|
|
107
|
+
async update(id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder> {
|
|
108
|
+
const reminder = this.reminders.find((r) => r.id === id);
|
|
109
|
+
if (!reminder) throw new Error(`Reminder "${id}" not found`);
|
|
110
|
+
if (fields.scheduledAt !== undefined) reminder.scheduledAt = fields.scheduledAt;
|
|
111
|
+
if (fields.occurrenceCount !== undefined) reminder.occurrenceCount = fields.occurrenceCount;
|
|
112
|
+
if (fields.status !== undefined) reminder.status = fields.status;
|
|
113
|
+
return reminder;
|
|
114
|
+
}
|
|
115
|
+
|
|
90
116
|
async cancel(id: string): Promise<Reminder> {
|
|
91
117
|
const reminder = this.reminders.find((r) => r.id === id);
|
|
92
118
|
if (!reminder) throw new Error(`Reminder "${id}" not found`);
|
|
@@ -102,6 +128,147 @@ class InMemoryReminderStore implements ReminderStore {
|
|
|
102
128
|
}
|
|
103
129
|
}
|
|
104
130
|
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Recurrence helpers
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Given a reminder's current scheduledAt and its recurrence config, compute the
|
|
137
|
+
* next fire time. Returns null if the recurrence is exhausted (maxOccurrences
|
|
138
|
+
* reached or endsAt passed).
|
|
139
|
+
*/
|
|
140
|
+
export const computeNextOccurrence = (reminder: Reminder): number | null => {
|
|
141
|
+
const rec = reminder.recurrence;
|
|
142
|
+
if (!rec) return null;
|
|
143
|
+
|
|
144
|
+
const fired = (reminder.occurrenceCount ?? 0) + 1;
|
|
145
|
+
if (rec.maxOccurrences && fired >= rec.maxOccurrences) return null;
|
|
146
|
+
|
|
147
|
+
const interval = rec.interval ?? 1;
|
|
148
|
+
const prev = reminder.scheduledAt;
|
|
149
|
+
let next: number;
|
|
150
|
+
|
|
151
|
+
switch (rec.type) {
|
|
152
|
+
case "daily": {
|
|
153
|
+
next = prev + interval * 24 * 60 * 60 * 1000;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "weekly": {
|
|
157
|
+
if (rec.daysOfWeek && rec.daysOfWeek.length > 0) {
|
|
158
|
+
// Advance to next matching day-of-week
|
|
159
|
+
const d = new Date(prev);
|
|
160
|
+
const days = [...rec.daysOfWeek].sort((a, b) => a - b);
|
|
161
|
+
const currentDay = d.getUTCDay();
|
|
162
|
+
// Find the next day in the list that is strictly after currentDay
|
|
163
|
+
let nextDay = days.find((day) => day > currentDay);
|
|
164
|
+
if (nextDay !== undefined) {
|
|
165
|
+
// Same week
|
|
166
|
+
const delta = nextDay - currentDay;
|
|
167
|
+
next = prev + delta * 24 * 60 * 60 * 1000;
|
|
168
|
+
} else {
|
|
169
|
+
// Wrap to next week (+ interval weeks if interval > 1)
|
|
170
|
+
const delta = (7 * interval) - currentDay + days[0];
|
|
171
|
+
next = prev + delta * 24 * 60 * 60 * 1000;
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// No specific days — just advance by N weeks
|
|
175
|
+
next = prev + interval * 7 * 24 * 60 * 60 * 1000;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case "monthly": {
|
|
180
|
+
const d = new Date(prev);
|
|
181
|
+
d.setUTCMonth(d.getUTCMonth() + interval);
|
|
182
|
+
next = d.getTime();
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "cron": {
|
|
186
|
+
// Minimal cron: parse 5-field expression and find the next matching
|
|
187
|
+
// minute after `prev`. We support basic values, ranges, steps and *.
|
|
188
|
+
if (!rec.expression) return null;
|
|
189
|
+
const parsed = parseCronExpression(rec.expression);
|
|
190
|
+
if (!parsed) return null;
|
|
191
|
+
next = nextCronOccurrence(prev, parsed);
|
|
192
|
+
if (next <= prev) return null; // safety
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (rec.endsAt && next > rec.endsAt) return null;
|
|
200
|
+
return next;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Minimal cron parser (5-field: min hour dom month dow)
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
interface CronFields {
|
|
208
|
+
minutes: Set<number>;
|
|
209
|
+
hours: Set<number>;
|
|
210
|
+
daysOfMonth: Set<number>;
|
|
211
|
+
months: Set<number>;
|
|
212
|
+
daysOfWeek: Set<number>;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const expandField = (field: string, min: number, max: number): Set<number> | null => {
|
|
216
|
+
const values = new Set<number>();
|
|
217
|
+
for (const part of field.split(",")) {
|
|
218
|
+
const stepMatch = part.match(/^(.+)\/(\d+)$/);
|
|
219
|
+
const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
|
|
220
|
+
const range = stepMatch ? stepMatch[1] : part;
|
|
221
|
+
|
|
222
|
+
if (range === "*") {
|
|
223
|
+
for (let i = min; i <= max; i += step) values.add(i);
|
|
224
|
+
} else if (range.includes("-")) {
|
|
225
|
+
const [lo, hi] = range.split("-").map(Number);
|
|
226
|
+
if (isNaN(lo) || isNaN(hi)) return null;
|
|
227
|
+
for (let i = lo; i <= hi; i += step) values.add(i);
|
|
228
|
+
} else {
|
|
229
|
+
const n = parseInt(range, 10);
|
|
230
|
+
if (isNaN(n)) return null;
|
|
231
|
+
values.add(n);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return values;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const parseCronExpression = (expr: string): CronFields | null => {
|
|
238
|
+
const parts = expr.trim().split(/\s+/);
|
|
239
|
+
if (parts.length !== 5) return null;
|
|
240
|
+
const minutes = expandField(parts[0], 0, 59);
|
|
241
|
+
const hours = expandField(parts[1], 0, 23);
|
|
242
|
+
const daysOfMonth = expandField(parts[2], 1, 31);
|
|
243
|
+
const months = expandField(parts[3], 1, 12);
|
|
244
|
+
const daysOfWeek = expandField(parts[4], 0, 6);
|
|
245
|
+
if (!minutes || !hours || !daysOfMonth || !months || !daysOfWeek) return null;
|
|
246
|
+
return { minutes, hours, daysOfMonth, months, daysOfWeek };
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const nextCronOccurrence = (afterMs: number, fields: CronFields): number => {
|
|
250
|
+
// Start from the minute after `afterMs`
|
|
251
|
+
const d = new Date(afterMs);
|
|
252
|
+
d.setUTCSeconds(0, 0);
|
|
253
|
+
d.setUTCMinutes(d.getUTCMinutes() + 1);
|
|
254
|
+
|
|
255
|
+
// Cap search to ~1 year to avoid infinite loops
|
|
256
|
+
const limit = afterMs + 366 * 24 * 60 * 60 * 1000;
|
|
257
|
+
while (d.getTime() < limit) {
|
|
258
|
+
if (
|
|
259
|
+
fields.months.has(d.getUTCMonth() + 1) &&
|
|
260
|
+
fields.daysOfMonth.has(d.getUTCDate()) &&
|
|
261
|
+
fields.daysOfWeek.has(d.getUTCDay()) &&
|
|
262
|
+
fields.hours.has(d.getUTCHours()) &&
|
|
263
|
+
fields.minutes.has(d.getUTCMinutes())
|
|
264
|
+
) {
|
|
265
|
+
return d.getTime();
|
|
266
|
+
}
|
|
267
|
+
d.setUTCMinutes(d.getUTCMinutes() + 1);
|
|
268
|
+
}
|
|
269
|
+
return afterMs; // no match within a year — treat as exhausted
|
|
270
|
+
};
|
|
271
|
+
|
|
105
272
|
// ---------------------------------------------------------------------------
|
|
106
273
|
// Factory
|
|
107
274
|
// ---------------------------------------------------------------------------
|
package/src/reminder-tools.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
|
|
2
|
-
import type { ReminderStore, ReminderStatus } from "./reminder-store.js";
|
|
2
|
+
import type { ReminderStore, ReminderStatus, Recurrence, RecurrenceType } from "./reminder-store.js";
|
|
3
3
|
|
|
4
4
|
const VALID_STATUSES: ReminderStatus[] = ["pending", "cancelled"];
|
|
5
|
+
const VALID_RECURRENCE_TYPES: RecurrenceType[] = ["daily", "weekly", "monthly", "cron"];
|
|
5
6
|
|
|
6
7
|
export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
|
|
7
8
|
defineTool({
|
|
8
9
|
name: "set_reminder",
|
|
9
10
|
description:
|
|
10
|
-
"Set a
|
|
11
|
+
"Set a reminder that will fire at the specified date and time. " +
|
|
11
12
|
"Use this when the user asks to be reminded about something. " +
|
|
12
13
|
"The datetime must be an ISO 8601 string in the future. " +
|
|
13
|
-
"When the reminder fires, the task message will be delivered to the user."
|
|
14
|
+
"When the reminder fires, the task message will be delivered to the user. " +
|
|
15
|
+
"Supports optional recurrence for recurring reminders (daily, weekly, monthly, or cron).",
|
|
14
16
|
inputSchema: {
|
|
15
17
|
type: "object",
|
|
16
18
|
properties: {
|
|
@@ -21,13 +23,54 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
|
|
|
21
23
|
datetime: {
|
|
22
24
|
type: "string",
|
|
23
25
|
description:
|
|
24
|
-
"ISO 8601 datetime for when the reminder should fire (e.g. '2026-03-23T09:00:00Z')",
|
|
26
|
+
"ISO 8601 datetime for when the reminder should first fire (e.g. '2026-03-23T09:00:00Z')",
|
|
25
27
|
},
|
|
26
28
|
timezone: {
|
|
27
29
|
type: "string",
|
|
28
30
|
description:
|
|
29
31
|
"IANA timezone for interpreting the datetime if it lacks an offset (e.g. 'America/New_York'). Defaults to UTC.",
|
|
30
32
|
},
|
|
33
|
+
recurrence: {
|
|
34
|
+
type: "object",
|
|
35
|
+
description:
|
|
36
|
+
"Optional. Set this to make the reminder repeat. Omit for a one-time reminder.",
|
|
37
|
+
properties: {
|
|
38
|
+
type: {
|
|
39
|
+
type: "string",
|
|
40
|
+
enum: VALID_RECURRENCE_TYPES,
|
|
41
|
+
description:
|
|
42
|
+
"How often to repeat: 'daily', 'weekly', 'monthly', or 'cron'.",
|
|
43
|
+
},
|
|
44
|
+
interval: {
|
|
45
|
+
type: "number",
|
|
46
|
+
description:
|
|
47
|
+
"Repeat every N units (e.g. 2 = every 2 days/weeks/months). Defaults to 1.",
|
|
48
|
+
},
|
|
49
|
+
daysOfWeek: {
|
|
50
|
+
type: "array",
|
|
51
|
+
items: { type: "number" },
|
|
52
|
+
description:
|
|
53
|
+
"For weekly: which days to fire (0=Sunday, 1=Monday, ..., 6=Saturday).",
|
|
54
|
+
},
|
|
55
|
+
expression: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description:
|
|
58
|
+
"For type 'cron': a 5-field cron expression (e.g. '0 9 * * 1-5' for weekdays at 9am).",
|
|
59
|
+
},
|
|
60
|
+
endsAt: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description:
|
|
63
|
+
"ISO 8601 datetime after which the recurrence should stop.",
|
|
64
|
+
},
|
|
65
|
+
maxOccurrences: {
|
|
66
|
+
type: "number",
|
|
67
|
+
description:
|
|
68
|
+
"Maximum number of times the reminder should fire before stopping.",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
required: ["type"],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
31
74
|
},
|
|
32
75
|
required: ["task", "datetime"],
|
|
33
76
|
additionalProperties: false,
|
|
@@ -78,6 +121,50 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
|
|
|
78
121
|
throw new Error("Reminder datetime must be in the future");
|
|
79
122
|
}
|
|
80
123
|
|
|
124
|
+
// Parse recurrence if provided
|
|
125
|
+
let recurrence: Recurrence | null = null;
|
|
126
|
+
if (input.recurrence && typeof input.recurrence === "object") {
|
|
127
|
+
const rec = input.recurrence as Record<string, unknown>;
|
|
128
|
+
const recType = rec.type as string;
|
|
129
|
+
if (!VALID_RECURRENCE_TYPES.includes(recType as RecurrenceType)) {
|
|
130
|
+
throw new Error(`Invalid recurrence type: "${recType}". Must be one of: ${VALID_RECURRENCE_TYPES.join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
recurrence = { type: recType as RecurrenceType };
|
|
133
|
+
if (rec.interval !== undefined) {
|
|
134
|
+
const interval = Number(rec.interval);
|
|
135
|
+
if (!Number.isInteger(interval) || interval < 1) {
|
|
136
|
+
throw new Error("recurrence.interval must be a positive integer");
|
|
137
|
+
}
|
|
138
|
+
recurrence.interval = interval;
|
|
139
|
+
}
|
|
140
|
+
if (rec.daysOfWeek !== undefined) {
|
|
141
|
+
if (!Array.isArray(rec.daysOfWeek)) throw new Error("recurrence.daysOfWeek must be an array");
|
|
142
|
+
const days = (rec.daysOfWeek as unknown[]).map(Number);
|
|
143
|
+
if (days.some((d) => !Number.isInteger(d) || d < 0 || d > 6)) {
|
|
144
|
+
throw new Error("recurrence.daysOfWeek values must be integers 0-6");
|
|
145
|
+
}
|
|
146
|
+
recurrence.daysOfWeek = days;
|
|
147
|
+
}
|
|
148
|
+
if (rec.expression !== undefined) {
|
|
149
|
+
if (typeof rec.expression !== "string") throw new Error("recurrence.expression must be a string");
|
|
150
|
+
recurrence.expression = rec.expression;
|
|
151
|
+
}
|
|
152
|
+
if (rec.endsAt !== undefined) {
|
|
153
|
+
const endsAtDate = new Date(rec.endsAt as string);
|
|
154
|
+
if (isNaN(endsAtDate.getTime())) {
|
|
155
|
+
throw new Error(`Invalid recurrence.endsAt: "${rec.endsAt}"`);
|
|
156
|
+
}
|
|
157
|
+
recurrence.endsAt = endsAtDate.getTime();
|
|
158
|
+
}
|
|
159
|
+
if (rec.maxOccurrences !== undefined) {
|
|
160
|
+
const max = Number(rec.maxOccurrences);
|
|
161
|
+
if (!Number.isInteger(max) || max < 1) {
|
|
162
|
+
throw new Error("recurrence.maxOccurrences must be a positive integer");
|
|
163
|
+
}
|
|
164
|
+
recurrence.maxOccurrences = max;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
81
168
|
const conversationId = context.conversationId || context.runId;
|
|
82
169
|
const reminder = await store.create({
|
|
83
170
|
task,
|
|
@@ -85,6 +172,7 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
|
|
|
85
172
|
timezone,
|
|
86
173
|
conversationId,
|
|
87
174
|
tenantId: context.tenantId,
|
|
175
|
+
recurrence,
|
|
88
176
|
});
|
|
89
177
|
|
|
90
178
|
return {
|
|
@@ -95,6 +183,8 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
|
|
|
95
183
|
scheduledAt: new Date(reminder.scheduledAt).toISOString(),
|
|
96
184
|
timezone: reminder.timezone ?? "UTC",
|
|
97
185
|
status: reminder.status,
|
|
186
|
+
recurrence: reminder.recurrence ?? undefined,
|
|
187
|
+
occurrenceCount: reminder.occurrenceCount ?? 0,
|
|
98
188
|
},
|
|
99
189
|
};
|
|
100
190
|
},
|
|
@@ -105,7 +195,8 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
|
|
|
105
195
|
description:
|
|
106
196
|
"List reminders for this agent. Returns all reminders by default; " +
|
|
107
197
|
"use the status filter to show only pending or cancelled ones. " +
|
|
108
|
-
"Fired reminders are automatically deleted after delivery."
|
|
198
|
+
"Fired one-time reminders are automatically deleted after delivery. " +
|
|
199
|
+
"Recurring reminders stay active and show their recurrence config and fire count.",
|
|
109
200
|
inputSchema: {
|
|
110
201
|
type: "object",
|
|
111
202
|
properties: {
|
|
@@ -135,6 +226,8 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
|
|
|
135
226
|
timezone: r.timezone ?? "UTC",
|
|
136
227
|
status: r.status,
|
|
137
228
|
createdAt: new Date(r.createdAt).toISOString(),
|
|
229
|
+
recurrence: r.recurrence ?? undefined,
|
|
230
|
+
occurrenceCount: r.occurrenceCount ?? 0,
|
|
138
231
|
})),
|
|
139
232
|
count: reminders.length,
|
|
140
233
|
};
|
|
@@ -143,7 +236,10 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
|
|
|
143
236
|
|
|
144
237
|
defineTool({
|
|
145
238
|
name: "cancel_reminder",
|
|
146
|
-
description:
|
|
239
|
+
description:
|
|
240
|
+
"Cancel a pending reminder by its ID. " +
|
|
241
|
+
"This works for both one-time and recurring reminders — " +
|
|
242
|
+
"cancelling a recurring reminder stops all future occurrences.",
|
|
147
243
|
inputSchema: {
|
|
148
244
|
type: "object",
|
|
149
245
|
properties: {
|
package/src/state.ts
CHANGED
|
@@ -77,11 +77,23 @@ export interface Conversation {
|
|
|
77
77
|
updatedAt: number;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
export interface ConversationCreateInit {
|
|
81
|
+
parentConversationId?: string;
|
|
82
|
+
subagentMeta?: Conversation["subagentMeta"];
|
|
83
|
+
messages?: Message[];
|
|
84
|
+
channelMeta?: Conversation["channelMeta"];
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
export interface ConversationStore {
|
|
81
88
|
list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]>;
|
|
82
89
|
listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]>;
|
|
83
90
|
get(conversationId: string): Promise<Conversation | undefined>;
|
|
84
|
-
create(
|
|
91
|
+
create(
|
|
92
|
+
ownerId?: string,
|
|
93
|
+
title?: string,
|
|
94
|
+
tenantId?: string | null,
|
|
95
|
+
init?: ConversationCreateInit,
|
|
96
|
+
): Promise<Conversation>;
|
|
85
97
|
update(conversation: Conversation): Promise<void>;
|
|
86
98
|
rename(conversationId: string, title: string): Promise<Conversation | undefined>;
|
|
87
99
|
delete(conversationId: string): Promise<boolean>;
|
|
@@ -197,16 +209,30 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
197
209
|
return this.conversations.get(conversationId);
|
|
198
210
|
}
|
|
199
211
|
|
|
200
|
-
async create(
|
|
212
|
+
async create(
|
|
213
|
+
ownerId = DEFAULT_OWNER,
|
|
214
|
+
title?: string,
|
|
215
|
+
tenantId: string | null = null,
|
|
216
|
+
init?: ConversationCreateInit,
|
|
217
|
+
): Promise<Conversation> {
|
|
201
218
|
const now = Date.now();
|
|
202
219
|
const conversation: Conversation = {
|
|
203
220
|
conversationId: globalThis.crypto?.randomUUID?.() ?? `${now}-${Math.random()}`,
|
|
204
221
|
title: normalizeTitle(title),
|
|
205
|
-
messages: [],
|
|
222
|
+
messages: init?.messages ?? [],
|
|
206
223
|
ownerId,
|
|
207
224
|
tenantId,
|
|
208
225
|
createdAt: now,
|
|
209
226
|
updatedAt: now,
|
|
227
|
+
...(init?.parentConversationId !== undefined
|
|
228
|
+
? { parentConversationId: init.parentConversationId }
|
|
229
|
+
: {}),
|
|
230
|
+
...(init?.subagentMeta !== undefined
|
|
231
|
+
? { subagentMeta: init.subagentMeta }
|
|
232
|
+
: {}),
|
|
233
|
+
...(init?.channelMeta !== undefined
|
|
234
|
+
? { channelMeta: init.channelMeta }
|
|
235
|
+
: {}),
|
|
210
236
|
};
|
|
211
237
|
this.conversations.set(conversation.conversationId, conversation);
|
|
212
238
|
return conversation;
|
package/src/storage/engine.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Conversation,
|
|
3
|
+
ConversationCreateInit,
|
|
3
4
|
ConversationSummary,
|
|
4
5
|
PendingSubagentResult,
|
|
5
6
|
} from "../state.js";
|
|
6
7
|
import type { MainMemory } from "../memory.js";
|
|
7
8
|
import type { TodoItem } from "../todo-tools.js";
|
|
8
|
-
import type { Reminder } from "../reminder-store.js";
|
|
9
|
+
import type { Reminder, ReminderCreateInput, ReminderStatus } from "../reminder-store.js";
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// VFS types
|
|
@@ -40,7 +41,12 @@ export interface StorageEngine {
|
|
|
40
41
|
conversations: {
|
|
41
42
|
list(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]>;
|
|
42
43
|
get(conversationId: string): Promise<Conversation | undefined>;
|
|
43
|
-
create(
|
|
44
|
+
create(
|
|
45
|
+
ownerId?: string,
|
|
46
|
+
title?: string,
|
|
47
|
+
tenantId?: string | null,
|
|
48
|
+
init?: ConversationCreateInit,
|
|
49
|
+
): Promise<Conversation>;
|
|
44
50
|
update(conversation: Conversation): Promise<void>;
|
|
45
51
|
rename(conversationId: string, title: string): Promise<Conversation | undefined>;
|
|
46
52
|
delete(conversationId: string): Promise<boolean>;
|
|
@@ -67,14 +73,8 @@ export interface StorageEngine {
|
|
|
67
73
|
// --- Reminders (replaces ReminderStore) ---
|
|
68
74
|
reminders: {
|
|
69
75
|
list(tenantId?: string | null): Promise<Reminder[]>;
|
|
70
|
-
create(input:
|
|
71
|
-
|
|
72
|
-
scheduledAt: number;
|
|
73
|
-
timezone?: string;
|
|
74
|
-
conversationId: string;
|
|
75
|
-
ownerId?: string;
|
|
76
|
-
tenantId?: string | null;
|
|
77
|
-
}): Promise<Reminder>;
|
|
76
|
+
create(input: ReminderCreateInput): Promise<Reminder>;
|
|
77
|
+
update(id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder>;
|
|
78
78
|
cancel(id: string): Promise<Reminder>;
|
|
79
79
|
delete(id: string): Promise<void>;
|
|
80
80
|
};
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import type {
|
|
7
7
|
Conversation,
|
|
8
|
+
ConversationCreateInit,
|
|
8
9
|
ConversationSummary,
|
|
9
10
|
PendingSubagentResult,
|
|
10
11
|
} from "../state.js";
|
|
11
12
|
import type { MainMemory } from "../memory.js";
|
|
12
13
|
import type { TodoItem } from "../todo-tools.js";
|
|
13
|
-
import type { Reminder } from "../reminder-store.js";
|
|
14
|
+
import type { Reminder, ReminderCreateInput, ReminderStatus } from "../reminder-store.js";
|
|
14
15
|
import type { StorageEngine, VfsDirEntry, VfsStat } from "./engine.js";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -105,16 +106,26 @@ export class InMemoryEngine implements StorageEngine {
|
|
|
105
106
|
ownerId?: string,
|
|
106
107
|
title?: string,
|
|
107
108
|
tenantId?: string | null,
|
|
109
|
+
init?: ConversationCreateInit,
|
|
108
110
|
): Promise<Conversation> => {
|
|
109
111
|
const now = Date.now();
|
|
110
112
|
const conv: Conversation = {
|
|
111
113
|
conversationId: randomUUID(),
|
|
112
114
|
title: normalizeTitle(title),
|
|
113
|
-
messages: [],
|
|
115
|
+
messages: init?.messages ?? [],
|
|
114
116
|
ownerId: ownerId ?? DEFAULT_OWNER,
|
|
115
117
|
tenantId: tenantId === undefined ? null : tenantId,
|
|
116
118
|
createdAt: now,
|
|
117
119
|
updatedAt: now,
|
|
120
|
+
...(init?.parentConversationId !== undefined
|
|
121
|
+
? { parentConversationId: init.parentConversationId }
|
|
122
|
+
: {}),
|
|
123
|
+
...(init?.subagentMeta !== undefined
|
|
124
|
+
? { subagentMeta: init.subagentMeta }
|
|
125
|
+
: {}),
|
|
126
|
+
...(init?.channelMeta !== undefined
|
|
127
|
+
? { channelMeta: init.channelMeta }
|
|
128
|
+
: {}),
|
|
118
129
|
};
|
|
119
130
|
this.convs.set(conv.conversationId, conv);
|
|
120
131
|
return conv;
|
|
@@ -237,14 +248,7 @@ export class InMemoryEngine implements StorageEngine {
|
|
|
237
248
|
return results;
|
|
238
249
|
},
|
|
239
250
|
|
|
240
|
-
create: async (input: {
|
|
241
|
-
task: string;
|
|
242
|
-
scheduledAt: number;
|
|
243
|
-
timezone?: string;
|
|
244
|
-
conversationId: string;
|
|
245
|
-
ownerId?: string;
|
|
246
|
-
tenantId?: string | null;
|
|
247
|
-
}): Promise<Reminder> => {
|
|
251
|
+
create: async (input: ReminderCreateInput): Promise<Reminder> => {
|
|
248
252
|
const r: Reminder = {
|
|
249
253
|
id: randomUUID(),
|
|
250
254
|
task: input.task,
|
|
@@ -255,11 +259,22 @@ export class InMemoryEngine implements StorageEngine {
|
|
|
255
259
|
conversationId: input.conversationId,
|
|
256
260
|
ownerId: input.ownerId,
|
|
257
261
|
tenantId: input.tenantId,
|
|
262
|
+
recurrence: input.recurrence ?? null,
|
|
263
|
+
occurrenceCount: 0,
|
|
258
264
|
};
|
|
259
265
|
this.reminderData.set(r.id, r);
|
|
260
266
|
return r;
|
|
261
267
|
},
|
|
262
268
|
|
|
269
|
+
update: async (id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder> => {
|
|
270
|
+
const r = this.reminderData.get(id);
|
|
271
|
+
if (!r) throw new Error(`Reminder ${id} not found`);
|
|
272
|
+
if (fields.scheduledAt !== undefined) r.scheduledAt = fields.scheduledAt;
|
|
273
|
+
if (fields.occurrenceCount !== undefined) r.occurrenceCount = fields.occurrenceCount;
|
|
274
|
+
if (fields.status !== undefined) r.status = fields.status;
|
|
275
|
+
return r;
|
|
276
|
+
},
|
|
277
|
+
|
|
263
278
|
cancel: async (id: string): Promise<Reminder> => {
|
|
264
279
|
const r = this.reminderData.get(id);
|
|
265
280
|
if (!r) throw new Error(`Reminder ${id} not found`);
|
package/src/storage/schema.ts
CHANGED
|
@@ -174,4 +174,15 @@ export const migrations: Migration[] = [
|
|
|
174
174
|
];
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
|
+
{
|
|
178
|
+
version: 5,
|
|
179
|
+
name: "add_reminder_recurrence",
|
|
180
|
+
up: (d) => {
|
|
181
|
+
const jsonType = d === "sqlite" ? "TEXT" : "JSONB";
|
|
182
|
+
return [
|
|
183
|
+
`ALTER TABLE reminders ADD COLUMN recurrence ${jsonType}`,
|
|
184
|
+
`ALTER TABLE reminders ADD COLUMN occurrence_count INTEGER NOT NULL DEFAULT 0`,
|
|
185
|
+
];
|
|
186
|
+
},
|
|
187
|
+
},
|
|
177
188
|
];
|