opencode-gateway 0.1.0 → 0.2.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/README.md +26 -0
- package/dist/binding/gateway.d.ts +2 -1
- package/dist/binding/index.d.ts +1 -1
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/init.js +4 -1
- package/dist/cli/paths.js +1 -1
- package/dist/cli/templates.js +15 -0
- package/dist/cli.js +28 -4
- package/dist/config/gateway.d.ts +5 -0
- package/dist/config/gateway.js +6 -1
- package/dist/config/memory.d.ts +18 -0
- package/dist/config/memory.js +105 -0
- package/dist/config/paths.d.ts +2 -0
- package/dist/config/paths.js +5 -1
- package/dist/cron/runtime.d.ts +24 -5
- package/dist/cron/runtime.js +178 -13
- package/dist/delivery/text.js +1 -1
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +49 -37
- package/dist/host/logger.d.ts +8 -0
- package/dist/host/logger.js +53 -0
- package/dist/index.js +11 -7
- package/dist/memory/prompt.d.ts +9 -0
- package/dist/memory/prompt.js +122 -0
- package/dist/opencode/adapter.d.ts +2 -0
- package/dist/opencode/adapter.js +56 -7
- package/dist/runtime/conversation-coordinator.d.ts +4 -0
- package/dist/runtime/conversation-coordinator.js +22 -0
- package/dist/runtime/executor.d.ts +34 -5
- package/dist/runtime/executor.js +241 -22
- package/dist/runtime/runtime-singleton.d.ts +2 -0
- package/dist/runtime/runtime-singleton.js +28 -0
- package/dist/session/context.d.ts +1 -1
- package/dist/session/context.js +2 -23
- package/dist/session/system-prompt.d.ts +8 -0
- package/dist/session/system-prompt.js +52 -0
- package/dist/store/migrations.js +15 -1
- package/dist/store/sqlite.d.ts +20 -2
- package/dist/store/sqlite.js +103 -4
- package/dist/tools/channel-target.d.ts +5 -0
- package/dist/tools/channel-target.js +6 -0
- package/dist/tools/cron-run.js +1 -1
- package/dist/tools/cron-upsert.d.ts +2 -1
- package/dist/tools/cron-upsert.js +20 -6
- package/dist/tools/{cron-list.d.ts → schedule-cancel.d.ts} +1 -1
- package/dist/tools/schedule-cancel.js +12 -0
- package/dist/tools/schedule-format.d.ts +4 -0
- package/dist/tools/schedule-format.js +48 -0
- package/dist/tools/{cron-remove.d.ts → schedule-list.d.ts} +1 -1
- package/dist/tools/schedule-list.js +17 -0
- package/dist/tools/schedule-once.d.ts +4 -0
- package/dist/tools/schedule-once.js +43 -0
- package/dist/tools/schedule-status.d.ts +3 -0
- package/dist/tools/schedule-status.js +23 -0
- package/generated/wasm/pkg/opencode_gateway_ffi_bg.wasm +0 -0
- package/package.json +4 -4
- package/dist/host/noop.d.ts +0 -4
- package/dist/host/noop.js +0 -14
- package/dist/tools/cron-list.js +0 -34
- package/dist/tools/cron-remove.js +0 -12
package/dist/session/context.js
CHANGED
|
@@ -17,28 +17,7 @@ export class GatewaySessionContext {
|
|
|
17
17
|
getDefaultReplyTarget(sessionId) {
|
|
18
18
|
return this.store.getDefaultSessionReplyTarget(sessionId);
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (targets.length === 0) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
if (targets.length === 1) {
|
|
26
|
-
const target = targets[0];
|
|
27
|
-
return [
|
|
28
|
-
"Gateway context:",
|
|
29
|
-
`- Current message source channel: ${target.channel}`,
|
|
30
|
-
`- Current reply target id: ${target.target}`,
|
|
31
|
-
`- Current reply topic: ${target.topic ?? "none"}`,
|
|
32
|
-
"- Unless the user explicitly asks otherwise, channel-aware actions should default to this target.",
|
|
33
|
-
"- If the user asks to start a fresh channel session, use channel_new_session.",
|
|
34
|
-
].join("\n");
|
|
35
|
-
}
|
|
36
|
-
return [
|
|
37
|
-
"Gateway context:",
|
|
38
|
-
`- This session currently fans out to ${targets.length} reply targets.`,
|
|
39
|
-
...targets.map((target, index) => `- Target ${index + 1}: channel=${target.channel}, id=${target.target}, topic=${target.topic ?? "none"}`),
|
|
40
|
-
"- If a tool needs a single explicit target, do not guess; ask the user or use explicit tool arguments.",
|
|
41
|
-
"- If the user asks to start a fresh channel session for this route, use channel_new_session.",
|
|
42
|
-
].join("\n");
|
|
20
|
+
isGatewaySession(sessionId) {
|
|
21
|
+
return this.store.hasGatewaySession(sessionId);
|
|
43
22
|
}
|
|
44
23
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GatewayMemoryPromptProvider } from "../memory/prompt";
|
|
2
|
+
import type { GatewaySessionContext } from "./context";
|
|
3
|
+
export declare class GatewaySystemPromptBuilder {
|
|
4
|
+
private readonly sessions;
|
|
5
|
+
private readonly memory;
|
|
6
|
+
constructor(sessions: GatewaySessionContext, memory: GatewayMemoryPromptProvider);
|
|
7
|
+
buildPrompts(sessionId: string): Promise<string[]>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class GatewaySystemPromptBuilder {
|
|
2
|
+
sessions;
|
|
3
|
+
memory;
|
|
4
|
+
constructor(sessions, memory) {
|
|
5
|
+
this.sessions = sessions;
|
|
6
|
+
this.memory = memory;
|
|
7
|
+
}
|
|
8
|
+
async buildPrompts(sessionId) {
|
|
9
|
+
if (!this.sessions.isGatewaySession(sessionId)) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
const prompts = [];
|
|
13
|
+
const gatewayPrompt = buildGatewayContextPrompt(this.sessions.listReplyTargets(sessionId));
|
|
14
|
+
if (gatewayPrompt !== null) {
|
|
15
|
+
prompts.push(gatewayPrompt);
|
|
16
|
+
}
|
|
17
|
+
const memoryPrompt = await this.memory.buildPrompt();
|
|
18
|
+
if (memoryPrompt !== null) {
|
|
19
|
+
prompts.push(memoryPrompt);
|
|
20
|
+
}
|
|
21
|
+
return prompts;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function buildGatewayContextPrompt(targets) {
|
|
25
|
+
if (targets.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (targets.length === 1) {
|
|
29
|
+
const target = targets[0];
|
|
30
|
+
return [
|
|
31
|
+
"Gateway context:",
|
|
32
|
+
`- Current message source channel: ${target.channel}`,
|
|
33
|
+
`- Current reply target id: ${target.target}`,
|
|
34
|
+
`- Current reply topic: ${target.topic ?? "none"}`,
|
|
35
|
+
"- Unless the user explicitly asks otherwise, channel-aware actions should default to this target.",
|
|
36
|
+
"- If the user asks to start a fresh channel session, use channel_new_session.",
|
|
37
|
+
"- If the user asks for a one-shot reminder or relative-time follow-up, prefer schedule_once.",
|
|
38
|
+
"- If the user asks for a recurring schedule, prefer cron_upsert.",
|
|
39
|
+
"- Use schedule_list and schedule_status to inspect existing scheduled jobs and recent run results.",
|
|
40
|
+
"- Scheduled results delivered to this channel are automatically appended to this session as context.",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
return [
|
|
44
|
+
"Gateway context:",
|
|
45
|
+
`- This session currently fans out to ${targets.length} reply targets.`,
|
|
46
|
+
...targets.map((target, index) => `- Target ${index + 1}: channel=${target.channel}, id=${target.target}, topic=${target.topic ?? "none"}`),
|
|
47
|
+
"- If a tool needs a single explicit target, do not guess; ask the user or use explicit tool arguments.",
|
|
48
|
+
"- If the user asks to start a fresh channel session for this route, use channel_new_session.",
|
|
49
|
+
"- Prefer schedule_once for one-shot reminders and cron_upsert for recurring schedules.",
|
|
50
|
+
"- Use schedule_list and schedule_status to inspect scheduled jobs and recent run results.",
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
package/dist/store/migrations.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const LATEST_SCHEMA_VERSION =
|
|
1
|
+
const LATEST_SCHEMA_VERSION = 7;
|
|
2
2
|
export function migrateGatewayDatabase(db) {
|
|
3
3
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
4
4
|
db.exec("PRAGMA foreign_keys = ON;");
|
|
@@ -28,6 +28,10 @@ export function migrateGatewayDatabase(db) {
|
|
|
28
28
|
}
|
|
29
29
|
if (currentVersion === 5) {
|
|
30
30
|
migrateToV6(db);
|
|
31
|
+
currentVersion = 6;
|
|
32
|
+
}
|
|
33
|
+
if (currentVersion === 6) {
|
|
34
|
+
migrateToV7(db);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
function readUserVersion(db) {
|
|
@@ -179,5 +183,15 @@ function migrateToV6(db) {
|
|
|
179
183
|
CREATE INDEX pending_questions_session_id_created_at_ms_idx
|
|
180
184
|
ON pending_questions (session_id, created_at_ms);
|
|
181
185
|
`);
|
|
186
|
+
db.exec("PRAGMA user_version = 6;");
|
|
187
|
+
}
|
|
188
|
+
function migrateToV7(db) {
|
|
189
|
+
db.exec(`
|
|
190
|
+
ALTER TABLE cron_jobs
|
|
191
|
+
ADD COLUMN kind TEXT NOT NULL DEFAULT 'cron';
|
|
192
|
+
|
|
193
|
+
ALTER TABLE cron_jobs
|
|
194
|
+
ADD COLUMN run_at_ms INTEGER;
|
|
195
|
+
`);
|
|
182
196
|
db.exec(`PRAGMA user_version = ${LATEST_SCHEMA_VERSION};`);
|
|
183
197
|
}
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -3,6 +3,17 @@ import type { BindingDeliveryTarget } from "../binding";
|
|
|
3
3
|
import type { GatewayQuestionInfo, PendingQuestionRecord } from "../questions/types";
|
|
4
4
|
export type RuntimeJournalKind = "inbound_message" | "cron_dispatch" | "delivery" | "mailbox_enqueue" | "mailbox_flush";
|
|
5
5
|
export type CronRunStatus = "running" | "succeeded" | "failed" | "abandoned";
|
|
6
|
+
export type ScheduleJobKind = "cron" | "once";
|
|
7
|
+
export type CronRunRecord = {
|
|
8
|
+
id: number;
|
|
9
|
+
jobId: string;
|
|
10
|
+
scheduledForMs: number;
|
|
11
|
+
startedAtMs: number;
|
|
12
|
+
finishedAtMs: number | null;
|
|
13
|
+
status: CronRunStatus;
|
|
14
|
+
responseText: string | null;
|
|
15
|
+
errorMessage: string | null;
|
|
16
|
+
};
|
|
6
17
|
export type RuntimeJournalEntry = {
|
|
7
18
|
kind: RuntimeJournalKind;
|
|
8
19
|
recordedAtMs: number;
|
|
@@ -11,7 +22,9 @@ export type RuntimeJournalEntry = {
|
|
|
11
22
|
};
|
|
12
23
|
export type CronJobRecord = {
|
|
13
24
|
id: string;
|
|
14
|
-
|
|
25
|
+
kind: ScheduleJobKind;
|
|
26
|
+
schedule: string | null;
|
|
27
|
+
runAtMs: number | null;
|
|
15
28
|
prompt: string;
|
|
16
29
|
deliveryChannel: string | null;
|
|
17
30
|
deliveryTarget: string | null;
|
|
@@ -43,7 +56,9 @@ export type MailboxEntryAttachmentRecord = {
|
|
|
43
56
|
};
|
|
44
57
|
export type PersistCronJobInput = {
|
|
45
58
|
id: string;
|
|
46
|
-
|
|
59
|
+
kind: ScheduleJobKind;
|
|
60
|
+
schedule: string | null;
|
|
61
|
+
runAtMs: number | null;
|
|
47
62
|
prompt: string;
|
|
48
63
|
deliveryChannel: string | null;
|
|
49
64
|
deliveryTarget: string | null;
|
|
@@ -97,6 +112,7 @@ export declare class SqliteStore {
|
|
|
97
112
|
replaceSessionReplyTargets(input: PersistSessionReplyTargetsInput): void;
|
|
98
113
|
listSessionReplyTargets(sessionId: string): BindingDeliveryTarget[];
|
|
99
114
|
getDefaultSessionReplyTarget(sessionId: string): BindingDeliveryTarget | null;
|
|
115
|
+
hasGatewaySession(sessionId: string): boolean;
|
|
100
116
|
appendJournal(entry: RuntimeJournalEntry): void;
|
|
101
117
|
replacePendingQuestion(input: PersistPendingQuestionInput): void;
|
|
102
118
|
deletePendingQuestion(requestId: string): void;
|
|
@@ -119,6 +135,8 @@ export declare class SqliteStore {
|
|
|
119
135
|
listDueCronJobs(nowMs: number, limit: number): CronJobRecord[];
|
|
120
136
|
removeCronJob(id: string): boolean;
|
|
121
137
|
updateCronJobNextRun(id: string, nextRunAtMs: number, recordedAtMs: number): void;
|
|
138
|
+
setCronJobEnabled(id: string, enabled: boolean, recordedAtMs: number): void;
|
|
139
|
+
listCronRuns(jobId: string, limit: number): CronRunRecord[];
|
|
122
140
|
insertCronRun(jobId: string, scheduledForMs: number, startedAtMs: number): number;
|
|
123
141
|
finishCronRun(runId: number, status: Exclude<CronRunStatus, "running">, finishedAtMs: number, responseText: string | null, errorMessage: string | null): void;
|
|
124
142
|
abandonRunningCronRuns(finishedAtMs: number): number;
|
package/dist/store/sqlite.js
CHANGED
|
@@ -109,6 +109,28 @@ export class SqliteStore {
|
|
|
109
109
|
.get(sessionId);
|
|
110
110
|
return row ? mapSessionReplyTargetRow(row) : null;
|
|
111
111
|
}
|
|
112
|
+
hasGatewaySession(sessionId) {
|
|
113
|
+
const binding = this.db
|
|
114
|
+
.query(`
|
|
115
|
+
SELECT 1 AS present
|
|
116
|
+
FROM session_bindings
|
|
117
|
+
WHERE session_id = ?1
|
|
118
|
+
LIMIT 1;
|
|
119
|
+
`)
|
|
120
|
+
.get(sessionId);
|
|
121
|
+
if (binding?.present === 1) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
const replyTarget = this.db
|
|
125
|
+
.query(`
|
|
126
|
+
SELECT 1 AS present
|
|
127
|
+
FROM session_reply_targets
|
|
128
|
+
WHERE session_id = ?1
|
|
129
|
+
LIMIT 1;
|
|
130
|
+
`)
|
|
131
|
+
.get(sessionId);
|
|
132
|
+
return replyTarget?.present === 1;
|
|
133
|
+
}
|
|
112
134
|
appendJournal(entry) {
|
|
113
135
|
this.db
|
|
114
136
|
.query(`
|
|
@@ -315,11 +337,16 @@ export class SqliteStore {
|
|
|
315
337
|
upsertCronJob(input) {
|
|
316
338
|
assertSafeInteger(input.nextRunAtMs, "cron next_run_at_ms");
|
|
317
339
|
assertSafeInteger(input.recordedAtMs, "cron recordedAtMs");
|
|
340
|
+
if (input.runAtMs !== null) {
|
|
341
|
+
assertSafeInteger(input.runAtMs, "cron run_at_ms");
|
|
342
|
+
}
|
|
318
343
|
this.db
|
|
319
344
|
.query(`
|
|
320
345
|
INSERT INTO cron_jobs (
|
|
321
346
|
id,
|
|
347
|
+
kind,
|
|
322
348
|
schedule,
|
|
349
|
+
run_at_ms,
|
|
323
350
|
prompt,
|
|
324
351
|
delivery_channel,
|
|
325
352
|
delivery_target,
|
|
@@ -329,9 +356,11 @@ export class SqliteStore {
|
|
|
329
356
|
created_at_ms,
|
|
330
357
|
updated_at_ms
|
|
331
358
|
)
|
|
332
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?
|
|
359
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?11)
|
|
333
360
|
ON CONFLICT(id) DO UPDATE SET
|
|
361
|
+
kind = excluded.kind,
|
|
334
362
|
schedule = excluded.schedule,
|
|
363
|
+
run_at_ms = excluded.run_at_ms,
|
|
335
364
|
prompt = excluded.prompt,
|
|
336
365
|
delivery_channel = excluded.delivery_channel,
|
|
337
366
|
delivery_target = excluded.delivery_target,
|
|
@@ -340,14 +369,16 @@ export class SqliteStore {
|
|
|
340
369
|
next_run_at_ms = excluded.next_run_at_ms,
|
|
341
370
|
updated_at_ms = excluded.updated_at_ms;
|
|
342
371
|
`)
|
|
343
|
-
.run(input.id, input.schedule, input.prompt, input.deliveryChannel, input.deliveryTarget, input.deliveryTopic, input.enabled ? 1 : 0, input.nextRunAtMs, input.recordedAtMs);
|
|
372
|
+
.run(input.id, input.kind, encodeStoredSchedule(input.kind, input.schedule), input.runAtMs, input.prompt, input.deliveryChannel, input.deliveryTarget, input.deliveryTopic, input.enabled ? 1 : 0, input.nextRunAtMs, input.recordedAtMs);
|
|
344
373
|
}
|
|
345
374
|
getCronJob(id) {
|
|
346
375
|
const row = this.db
|
|
347
376
|
.query(`
|
|
348
377
|
SELECT
|
|
349
378
|
id,
|
|
379
|
+
kind,
|
|
350
380
|
schedule,
|
|
381
|
+
run_at_ms,
|
|
351
382
|
prompt,
|
|
352
383
|
delivery_channel,
|
|
353
384
|
delivery_target,
|
|
@@ -367,7 +398,9 @@ export class SqliteStore {
|
|
|
367
398
|
.query(`
|
|
368
399
|
SELECT
|
|
369
400
|
id,
|
|
401
|
+
kind,
|
|
370
402
|
schedule,
|
|
403
|
+
run_at_ms,
|
|
371
404
|
prompt,
|
|
372
405
|
delivery_channel,
|
|
373
406
|
delivery_target,
|
|
@@ -388,7 +421,9 @@ export class SqliteStore {
|
|
|
388
421
|
.query(`
|
|
389
422
|
SELECT
|
|
390
423
|
id,
|
|
424
|
+
kind,
|
|
391
425
|
schedule,
|
|
426
|
+
run_at_ms,
|
|
392
427
|
prompt,
|
|
393
428
|
delivery_channel,
|
|
394
429
|
delivery_target,
|
|
@@ -398,7 +433,7 @@ export class SqliteStore {
|
|
|
398
433
|
created_at_ms,
|
|
399
434
|
updated_at_ms
|
|
400
435
|
FROM cron_jobs
|
|
401
|
-
WHERE enabled = 1 AND next_run_at_ms <= ?1
|
|
436
|
+
WHERE kind = 'cron' AND enabled = 1 AND next_run_at_ms <= ?1
|
|
402
437
|
ORDER BY next_run_at_ms ASC, id ASC;
|
|
403
438
|
`)
|
|
404
439
|
.all(nowMs);
|
|
@@ -411,7 +446,9 @@ export class SqliteStore {
|
|
|
411
446
|
.query(`
|
|
412
447
|
SELECT
|
|
413
448
|
id,
|
|
449
|
+
kind,
|
|
414
450
|
schedule,
|
|
451
|
+
run_at_ms,
|
|
415
452
|
prompt,
|
|
416
453
|
delivery_channel,
|
|
417
454
|
delivery_target,
|
|
@@ -443,6 +480,47 @@ export class SqliteStore {
|
|
|
443
480
|
`)
|
|
444
481
|
.run(id, nextRunAtMs, recordedAtMs);
|
|
445
482
|
}
|
|
483
|
+
setCronJobEnabled(id, enabled, recordedAtMs) {
|
|
484
|
+
assertSafeInteger(recordedAtMs, "cron recordedAtMs");
|
|
485
|
+
this.db
|
|
486
|
+
.query(`
|
|
487
|
+
UPDATE cron_jobs
|
|
488
|
+
SET enabled = ?2,
|
|
489
|
+
updated_at_ms = ?3
|
|
490
|
+
WHERE id = ?1;
|
|
491
|
+
`)
|
|
492
|
+
.run(id, enabled ? 1 : 0, recordedAtMs);
|
|
493
|
+
}
|
|
494
|
+
listCronRuns(jobId, limit) {
|
|
495
|
+
assertSafeInteger(limit, "cron run limit");
|
|
496
|
+
const rows = this.db
|
|
497
|
+
.query(`
|
|
498
|
+
SELECT
|
|
499
|
+
id,
|
|
500
|
+
job_id,
|
|
501
|
+
scheduled_for_ms,
|
|
502
|
+
started_at_ms,
|
|
503
|
+
finished_at_ms,
|
|
504
|
+
status,
|
|
505
|
+
response_text,
|
|
506
|
+
error_message
|
|
507
|
+
FROM cron_runs
|
|
508
|
+
WHERE job_id = ?1
|
|
509
|
+
ORDER BY started_at_ms DESC, id DESC
|
|
510
|
+
LIMIT ?2;
|
|
511
|
+
`)
|
|
512
|
+
.all(jobId, limit);
|
|
513
|
+
return rows.map((row) => ({
|
|
514
|
+
id: row.id,
|
|
515
|
+
jobId: row.job_id,
|
|
516
|
+
scheduledForMs: row.scheduled_for_ms,
|
|
517
|
+
startedAtMs: row.started_at_ms,
|
|
518
|
+
finishedAtMs: row.finished_at_ms,
|
|
519
|
+
status: row.status,
|
|
520
|
+
responseText: row.response_text,
|
|
521
|
+
errorMessage: row.error_message,
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
446
524
|
insertCronRun(jobId, scheduledForMs, startedAtMs) {
|
|
447
525
|
assertSafeInteger(scheduledForMs, "cron scheduled_for_ms");
|
|
448
526
|
assertSafeInteger(startedAtMs, "cron started_at_ms");
|
|
@@ -496,9 +574,12 @@ export class SqliteStore {
|
|
|
496
574
|
}
|
|
497
575
|
}
|
|
498
576
|
function mapCronJobRow(row) {
|
|
577
|
+
const kind = parseScheduleJobKind(row.kind);
|
|
499
578
|
return {
|
|
500
579
|
id: row.id,
|
|
501
|
-
|
|
580
|
+
kind,
|
|
581
|
+
schedule: kind === "cron" ? row.schedule : null,
|
|
582
|
+
runAtMs: row.run_at_ms,
|
|
502
583
|
prompt: row.prompt,
|
|
503
584
|
deliveryChannel: row.delivery_channel,
|
|
504
585
|
deliveryTarget: row.delivery_target,
|
|
@@ -509,6 +590,24 @@ function mapCronJobRow(row) {
|
|
|
509
590
|
updatedAtMs: row.updated_at_ms,
|
|
510
591
|
};
|
|
511
592
|
}
|
|
593
|
+
function parseScheduleJobKind(value) {
|
|
594
|
+
switch (value) {
|
|
595
|
+
case "cron":
|
|
596
|
+
case "once":
|
|
597
|
+
return value;
|
|
598
|
+
default:
|
|
599
|
+
throw new Error(`stored schedule job kind is invalid: ${value}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function encodeStoredSchedule(kind, schedule) {
|
|
603
|
+
if (kind === "once") {
|
|
604
|
+
return "@once";
|
|
605
|
+
}
|
|
606
|
+
if (schedule === null) {
|
|
607
|
+
throw new Error("cron schedule must not be null");
|
|
608
|
+
}
|
|
609
|
+
return schedule;
|
|
610
|
+
}
|
|
512
611
|
function mapMailboxEntryRow(row, attachments) {
|
|
513
612
|
return {
|
|
514
613
|
id: row.id,
|
|
@@ -5,3 +5,8 @@ export declare function resolveToolDeliveryTarget(args: {
|
|
|
5
5
|
target?: string;
|
|
6
6
|
topic?: string;
|
|
7
7
|
}, sessionId: string | null | undefined, sessions: GatewaySessionContext): BindingDeliveryTarget;
|
|
8
|
+
export declare function resolveOptionalToolDeliveryTarget(args: {
|
|
9
|
+
channel?: string;
|
|
10
|
+
target?: string;
|
|
11
|
+
topic?: string;
|
|
12
|
+
}, sessionId: string | null | undefined, sessions: GatewaySessionContext): BindingDeliveryTarget | null;
|
|
@@ -9,6 +9,12 @@ export function resolveToolDeliveryTarget(args, sessionId, sessions) {
|
|
|
9
9
|
topic,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
export function resolveOptionalToolDeliveryTarget(args, sessionId, sessions) {
|
|
13
|
+
if (args.channel === undefined && args.target === undefined && args.topic === undefined) {
|
|
14
|
+
return sessionId ? sessions.getDefaultReplyTarget(sessionId) : null;
|
|
15
|
+
}
|
|
16
|
+
return resolveToolDeliveryTarget(args, sessionId, sessions);
|
|
17
|
+
}
|
|
12
18
|
function normalizeRequired(value, field) {
|
|
13
19
|
if (value === null) {
|
|
14
20
|
throw new Error(`${field} is required when the current session has no default reply target`);
|
package/dist/tools/cron-run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
export function createCronRunTool(runtime) {
|
|
3
3
|
return tool({
|
|
4
|
-
description: "Run one persisted gateway
|
|
4
|
+
description: "Run one persisted gateway schedule job immediately without changing its schedule metadata.",
|
|
5
5
|
args: {
|
|
6
6
|
id: tool.schema.string().min(1),
|
|
7
7
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
2
|
import type { GatewayCronRuntime } from "../cron/runtime";
|
|
3
|
-
|
|
3
|
+
import type { GatewaySessionContext } from "../session/context";
|
|
4
|
+
export declare function createCronUpsertTool(runtime: GatewayCronRuntime, sessions: GatewaySessionContext): ToolDefinition;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { resolveOptionalToolDeliveryTarget } from "./channel-target";
|
|
2
3
|
import { formatUnixMsAsUtc, formatUnixMsInTimeZone } from "./time";
|
|
3
|
-
export function createCronUpsertTool(runtime) {
|
|
4
|
+
export function createCronUpsertTool(runtime, sessions) {
|
|
4
5
|
return tool({
|
|
5
|
-
description: "Create or replace a
|
|
6
|
+
description: "Create or replace a recurring gateway cron job. When called from a channel-backed session, delivery defaults to the current reply target.",
|
|
6
7
|
args: {
|
|
7
8
|
id: tool.schema.string().min(1),
|
|
8
9
|
schedule: tool.schema.string().min(1),
|
|
@@ -12,26 +13,39 @@ export function createCronUpsertTool(runtime) {
|
|
|
12
13
|
delivery_target: tool.schema.string().optional(),
|
|
13
14
|
delivery_topic: tool.schema.string().optional(),
|
|
14
15
|
},
|
|
15
|
-
async execute(args) {
|
|
16
|
+
async execute(args, context) {
|
|
17
|
+
const deliveryTarget = resolveOptionalToolDeliveryTarget({
|
|
18
|
+
channel: args.delivery_channel,
|
|
19
|
+
target: args.delivery_target,
|
|
20
|
+
topic: args.delivery_topic,
|
|
21
|
+
}, context.sessionID, sessions);
|
|
16
22
|
const timeZone = runtime.timeZone();
|
|
17
23
|
const job = runtime.upsertJob({
|
|
18
24
|
id: args.id,
|
|
19
25
|
schedule: args.schedule,
|
|
20
26
|
prompt: args.prompt,
|
|
21
27
|
enabled: args.enabled ?? true,
|
|
22
|
-
deliveryChannel:
|
|
23
|
-
deliveryTarget:
|
|
24
|
-
deliveryTopic:
|
|
28
|
+
deliveryChannel: deliveryTarget?.channel ?? null,
|
|
29
|
+
deliveryTarget: deliveryTarget?.target ?? null,
|
|
30
|
+
deliveryTopic: deliveryTarget?.topic ?? null,
|
|
25
31
|
});
|
|
26
32
|
return [
|
|
27
33
|
`id=${job.id}`,
|
|
34
|
+
`kind=${job.kind}`,
|
|
28
35
|
`enabled=${job.enabled}`,
|
|
29
36
|
`schedule=${job.schedule}`,
|
|
30
37
|
`timezone=${timeZone}`,
|
|
31
38
|
`next_run_at_ms=${job.nextRunAtMs}`,
|
|
32
39
|
`next_run_at_local=${formatUnixMsInTimeZone(job.nextRunAtMs, timeZone)}`,
|
|
33
40
|
`next_run_at_utc=${formatUnixMsAsUtc(job.nextRunAtMs)}`,
|
|
41
|
+
`delivery=${formatDelivery(job.deliveryChannel, job.deliveryTarget, job.deliveryTopic)}`,
|
|
34
42
|
].join("\n");
|
|
35
43
|
},
|
|
36
44
|
});
|
|
37
45
|
}
|
|
46
|
+
function formatDelivery(channel, target, topic) {
|
|
47
|
+
if (channel === null || target === null) {
|
|
48
|
+
return "none";
|
|
49
|
+
}
|
|
50
|
+
return topic === null ? `${channel}:${target}` : `${channel}:${target}:topic:${topic}`;
|
|
51
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
2
|
import type { GatewayCronRuntime } from "../cron/runtime";
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function createScheduleCancelTool(runtime: GatewayCronRuntime): ToolDefinition;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
export function createScheduleCancelTool(runtime) {
|
|
3
|
+
return tool({
|
|
4
|
+
description: "Cancel a persisted gateway schedule job without deleting its run history.",
|
|
5
|
+
args: {
|
|
6
|
+
id: tool.schema.string().min(1),
|
|
7
|
+
},
|
|
8
|
+
async execute(args) {
|
|
9
|
+
return runtime.cancelJob(args.id) ? `canceled=${args.id}` : `inactive=${args.id}`;
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ScheduleJobStatus } from "../cron/runtime";
|
|
2
|
+
import type { CronJobRecord } from "../store/sqlite";
|
|
3
|
+
export declare function formatScheduleJob(job: CronJobRecord, timeZone: string): string;
|
|
4
|
+
export declare function formatScheduleStatus(status: ScheduleJobStatus, timeZone: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { formatUnixMsAsUtc, formatUnixMsInTimeZone } from "./time";
|
|
2
|
+
export function formatScheduleJob(job, timeZone) {
|
|
3
|
+
const lines = [`id=${job.id}`, `kind=${job.kind}`, `enabled=${job.enabled}`];
|
|
4
|
+
if (job.kind === "cron") {
|
|
5
|
+
lines.push(`schedule=${job.schedule}`);
|
|
6
|
+
lines.push(`timezone=${timeZone}`);
|
|
7
|
+
lines.push(`next_run_at_ms=${job.nextRunAtMs}`);
|
|
8
|
+
lines.push(`next_run_at_local=${formatUnixMsInTimeZone(job.nextRunAtMs, timeZone)}`);
|
|
9
|
+
lines.push(`next_run_at_utc=${formatUnixMsAsUtc(job.nextRunAtMs)}`);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
lines.push(`run_at_ms=${job.runAtMs ?? "none"}`);
|
|
13
|
+
if (job.runAtMs !== null) {
|
|
14
|
+
lines.push(`run_at_local=${formatUnixMsInTimeZone(job.runAtMs, timeZone)}`);
|
|
15
|
+
lines.push(`run_at_utc=${formatUnixMsAsUtc(job.runAtMs)}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
lines.push(`delivery=${formatDelivery(job.deliveryChannel, job.deliveryTarget, job.deliveryTopic)}`);
|
|
19
|
+
lines.push(`prompt=${job.prompt}`);
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
|
22
|
+
export function formatScheduleStatus(status, timeZone) {
|
|
23
|
+
const lines = [formatScheduleJob(status.job, timeZone), `state=${status.state}`];
|
|
24
|
+
if (status.runs.length === 0) {
|
|
25
|
+
lines.push("runs=none");
|
|
26
|
+
return lines.join("\n");
|
|
27
|
+
}
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push(...status.runs.map((run, index) => formatRun(run, index + 1)));
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
function formatRun(run, ordinal) {
|
|
33
|
+
return [
|
|
34
|
+
`run[${ordinal}].id=${run.id}`,
|
|
35
|
+
`run[${ordinal}].status=${run.status}`,
|
|
36
|
+
`run[${ordinal}].scheduled_for_ms=${run.scheduledForMs}`,
|
|
37
|
+
`run[${ordinal}].started_at_ms=${run.startedAtMs}`,
|
|
38
|
+
`run[${ordinal}].finished_at_ms=${run.finishedAtMs ?? "none"}`,
|
|
39
|
+
`run[${ordinal}].response_text=${run.responseText ?? "none"}`,
|
|
40
|
+
`run[${ordinal}].error_message=${run.errorMessage ?? "none"}`,
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
function formatDelivery(channel, target, topic) {
|
|
44
|
+
if (channel === null || target === null) {
|
|
45
|
+
return "none";
|
|
46
|
+
}
|
|
47
|
+
return topic === null ? `${channel}:${target}` : `${channel}:${target}:topic:${topic}`;
|
|
48
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
2
|
import type { GatewayCronRuntime } from "../cron/runtime";
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function createScheduleListTool(runtime: GatewayCronRuntime): ToolDefinition;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { formatScheduleJob } from "./schedule-format";
|
|
3
|
+
export function createScheduleListTool(runtime) {
|
|
4
|
+
return tool({
|
|
5
|
+
description: "List persisted gateway schedule jobs, including recurring cron jobs and one-shot timers.",
|
|
6
|
+
args: {
|
|
7
|
+
include_terminal: tool.schema.boolean().optional(),
|
|
8
|
+
},
|
|
9
|
+
async execute(args) {
|
|
10
|
+
const jobs = runtime.listJobs(args.include_terminal ?? false);
|
|
11
|
+
if (jobs.length === 0) {
|
|
12
|
+
return "no scheduled jobs";
|
|
13
|
+
}
|
|
14
|
+
return jobs.map((job) => formatScheduleJob(job, runtime.timeZone())).join("\n\n");
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
+
import type { GatewayCronRuntime } from "../cron/runtime";
|
|
3
|
+
import type { GatewaySessionContext } from "../session/context";
|
|
4
|
+
export declare function createScheduleOnceTool(runtime: GatewayCronRuntime, sessions: GatewaySessionContext): ToolDefinition;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { resolveOptionalToolDeliveryTarget } from "./channel-target";
|
|
3
|
+
import { formatScheduleJob } from "./schedule-format";
|
|
4
|
+
export function createScheduleOnceTool(runtime, sessions) {
|
|
5
|
+
return tool({
|
|
6
|
+
description: "Schedule a one-shot gateway job. When called from a channel-backed session, delivery defaults to the current reply target.",
|
|
7
|
+
args: {
|
|
8
|
+
id: tool.schema.string().min(1),
|
|
9
|
+
prompt: tool.schema.string().min(1),
|
|
10
|
+
delay_seconds: tool.schema.number().optional(),
|
|
11
|
+
run_at_ms: tool.schema.number().optional(),
|
|
12
|
+
delivery_channel: tool.schema.string().optional(),
|
|
13
|
+
delivery_target: tool.schema.string().optional(),
|
|
14
|
+
delivery_topic: tool.schema.string().optional(),
|
|
15
|
+
},
|
|
16
|
+
async execute(args, context) {
|
|
17
|
+
const deliveryTarget = resolveOptionalToolDeliveryTarget({
|
|
18
|
+
channel: args.delivery_channel,
|
|
19
|
+
target: args.delivery_target,
|
|
20
|
+
topic: args.delivery_topic,
|
|
21
|
+
}, context.sessionID, sessions);
|
|
22
|
+
const job = runtime.scheduleOnce({
|
|
23
|
+
id: args.id,
|
|
24
|
+
prompt: args.prompt,
|
|
25
|
+
delaySeconds: normalizeOptionalInteger(args.delay_seconds, "delay_seconds"),
|
|
26
|
+
runAtMs: normalizeOptionalInteger(args.run_at_ms, "run_at_ms"),
|
|
27
|
+
deliveryChannel: deliveryTarget?.channel ?? null,
|
|
28
|
+
deliveryTarget: deliveryTarget?.target ?? null,
|
|
29
|
+
deliveryTopic: deliveryTarget?.topic ?? null,
|
|
30
|
+
});
|
|
31
|
+
return formatScheduleJob(job, runtime.timeZone());
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function normalizeOptionalInteger(value, field) {
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (!Number.isSafeInteger(value)) {
|
|
40
|
+
throw new Error(`${field} must be an integer`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { formatScheduleStatus } from "./schedule-format";
|
|
3
|
+
export function createScheduleStatusTool(runtime) {
|
|
4
|
+
return tool({
|
|
5
|
+
description: "Inspect one persisted gateway schedule job and its recent run history.",
|
|
6
|
+
args: {
|
|
7
|
+
id: tool.schema.string().min(1),
|
|
8
|
+
limit: tool.schema.number().optional(),
|
|
9
|
+
},
|
|
10
|
+
async execute(args) {
|
|
11
|
+
return formatScheduleStatus(runtime.getJobStatus(args.id, normalizeOptionalLimit(args.limit)), runtime.timeZone());
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function normalizeOptionalLimit(value) {
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (!Number.isSafeInteger(value)) {
|
|
20
|
+
throw new Error("limit must be an integer");
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
Binary file
|