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/cron/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatError } from "../utils/error";
|
|
2
2
|
const CRON_EFFECTIVE_TIME_ZONE_KEY = "cron.effective_timezone";
|
|
3
3
|
const LEGACY_CRON_TIME_ZONE = "UTC";
|
|
4
|
+
const MAX_STATUS_RUNS = 20;
|
|
4
5
|
export class GatewayCronRuntime {
|
|
5
6
|
executor;
|
|
6
7
|
contract;
|
|
@@ -8,15 +9,17 @@ export class GatewayCronRuntime {
|
|
|
8
9
|
logger;
|
|
9
10
|
config;
|
|
10
11
|
effectiveTimeZone;
|
|
12
|
+
resolveConversationKeyForTarget;
|
|
11
13
|
runningJobIds = new Set();
|
|
12
14
|
running = false;
|
|
13
|
-
constructor(executor, contract, store, logger, config, effectiveTimeZone) {
|
|
15
|
+
constructor(executor, contract, store, logger, config, effectiveTimeZone, resolveConversationKeyForTarget) {
|
|
14
16
|
this.executor = executor;
|
|
15
17
|
this.contract = contract;
|
|
16
18
|
this.store = store;
|
|
17
19
|
this.logger = logger;
|
|
18
20
|
this.config = config;
|
|
19
21
|
this.effectiveTimeZone = effectiveTimeZone;
|
|
22
|
+
this.resolveConversationKeyForTarget = resolveConversationKeyForTarget;
|
|
20
23
|
}
|
|
21
24
|
isEnabled() {
|
|
22
25
|
return this.config.enabled;
|
|
@@ -39,8 +42,21 @@ export class GatewayCronRuntime {
|
|
|
39
42
|
this.running = false;
|
|
40
43
|
});
|
|
41
44
|
}
|
|
42
|
-
listJobs() {
|
|
43
|
-
|
|
45
|
+
listJobs(includeTerminal = false) {
|
|
46
|
+
const jobs = this.store.listCronJobs();
|
|
47
|
+
if (includeTerminal) {
|
|
48
|
+
return jobs;
|
|
49
|
+
}
|
|
50
|
+
return jobs.filter((job) => !isTerminalJob(job));
|
|
51
|
+
}
|
|
52
|
+
getJobStatus(id, limit = 5) {
|
|
53
|
+
const job = this.requireJob(normalizeId(id));
|
|
54
|
+
const runs = this.store.listCronRuns(job.id, clampStatusLimit(limit));
|
|
55
|
+
return {
|
|
56
|
+
job,
|
|
57
|
+
state: deriveJobState(job, runs[0] ?? null),
|
|
58
|
+
runs,
|
|
59
|
+
};
|
|
44
60
|
}
|
|
45
61
|
upsertJob(input) {
|
|
46
62
|
const normalized = normalizeUpsertInput(input);
|
|
@@ -53,13 +69,31 @@ export class GatewayCronRuntime {
|
|
|
53
69
|
});
|
|
54
70
|
return this.requireJob(normalized.id);
|
|
55
71
|
}
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
scheduleOnce(input) {
|
|
73
|
+
const normalized = normalizeOnceInput(input);
|
|
74
|
+
const recordedAtMs = Date.now();
|
|
75
|
+
this.store.upsertCronJob({
|
|
76
|
+
...normalized,
|
|
77
|
+
nextRunAtMs: normalized.runAtMs ?? recordedAtMs,
|
|
78
|
+
recordedAtMs,
|
|
79
|
+
});
|
|
80
|
+
return this.requireJob(normalized.id);
|
|
81
|
+
}
|
|
82
|
+
cancelJob(id) {
|
|
83
|
+
const job = this.store.getCronJob(normalizeId(id));
|
|
84
|
+
if (job === null || !job.enabled) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
this.store.setCronJobEnabled(job.id, false, Date.now());
|
|
88
|
+
return true;
|
|
58
89
|
}
|
|
59
90
|
async runNow(id) {
|
|
60
91
|
const job = this.requireJob(normalizeId(id));
|
|
92
|
+
if (!job.enabled) {
|
|
93
|
+
throw new Error(`schedule job is not active: ${job.id}`);
|
|
94
|
+
}
|
|
61
95
|
if (this.runningJobIds.has(job.id)) {
|
|
62
|
-
throw new Error(`
|
|
96
|
+
throw new Error(`schedule job is already running: ${job.id}`);
|
|
63
97
|
}
|
|
64
98
|
this.runningJobIds.add(job.id);
|
|
65
99
|
try {
|
|
@@ -88,7 +122,7 @@ export class GatewayCronRuntime {
|
|
|
88
122
|
? `rebasing enabled cron jobs from legacy ${LEGACY_CRON_TIME_ZONE} semantics to ${this.effectiveTimeZone}`
|
|
89
123
|
: `cron time zone changed from ${previousTimeZone} to ${this.effectiveTimeZone}; rebasing enabled jobs`;
|
|
90
124
|
this.logger.log("warn", message);
|
|
91
|
-
this.rebaseJobs(this.store.listCronJobs().filter((job) => job.enabled), nowMs);
|
|
125
|
+
this.rebaseJobs(this.store.listCronJobs().filter((job) => job.enabled && job.kind === "cron"), nowMs);
|
|
92
126
|
}
|
|
93
127
|
else {
|
|
94
128
|
this.rebaseJobs(this.store.listOverdueCronJobs(nowMs), nowMs);
|
|
@@ -108,7 +142,7 @@ export class GatewayCronRuntime {
|
|
|
108
142
|
this.runningJobIds.add(job.id);
|
|
109
143
|
void this.executeJob(job, job.nextRunAtMs, nowMs)
|
|
110
144
|
.catch((error) => {
|
|
111
|
-
this.logger.log("error", `
|
|
145
|
+
this.logger.log("error", `schedule job ${job.id} failed: ${formatError(error)}`);
|
|
112
146
|
})
|
|
113
147
|
.finally(() => {
|
|
114
148
|
this.runningJobIds.delete(job.id);
|
|
@@ -120,26 +154,60 @@ export class GatewayCronRuntime {
|
|
|
120
154
|
}
|
|
121
155
|
async executeJob(job, scheduledForMs, nextRunBaseMs) {
|
|
122
156
|
const startedAtMs = Date.now();
|
|
123
|
-
if (nextRunBaseMs !== null) {
|
|
157
|
+
if (job.kind === "cron" && nextRunBaseMs !== null) {
|
|
124
158
|
const nextRunAtMs = computeNextRunAt(this.contract, job, Math.max(nextRunBaseMs, scheduledForMs), this.effectiveTimeZone);
|
|
125
159
|
this.store.updateCronJobNextRun(job.id, nextRunAtMs, startedAtMs);
|
|
126
160
|
}
|
|
161
|
+
else if (job.kind === "once") {
|
|
162
|
+
this.store.setCronJobEnabled(job.id, false, startedAtMs);
|
|
163
|
+
}
|
|
127
164
|
const runId = this.store.insertCronRun(job.id, scheduledForMs, startedAtMs);
|
|
128
165
|
try {
|
|
129
|
-
const report = await this.executor.
|
|
166
|
+
const report = await this.executor.dispatchScheduledJob({
|
|
167
|
+
jobId: job.id,
|
|
168
|
+
jobKind: job.kind,
|
|
169
|
+
conversationKey: conversationKeyForJob(job),
|
|
170
|
+
prompt: job.prompt,
|
|
171
|
+
replyTarget: toReplyTarget(job),
|
|
172
|
+
});
|
|
130
173
|
this.store.finishCronRun(runId, "succeeded", Date.now(), report.responseText, null);
|
|
174
|
+
await this.appendScheduleResultToTarget(job, scheduledForMs, {
|
|
175
|
+
kind: "success",
|
|
176
|
+
responseText: report.responseText,
|
|
177
|
+
});
|
|
131
178
|
return report;
|
|
132
179
|
}
|
|
133
180
|
catch (error) {
|
|
134
181
|
const message = formatError(error);
|
|
135
182
|
this.store.finishCronRun(runId, "failed", Date.now(), null, message);
|
|
183
|
+
await this.appendScheduleResultToTarget(job, scheduledForMs, {
|
|
184
|
+
kind: "failure",
|
|
185
|
+
errorMessage: message,
|
|
186
|
+
});
|
|
136
187
|
throw error;
|
|
137
188
|
}
|
|
138
189
|
}
|
|
190
|
+
async appendScheduleResultToTarget(job, scheduledForMs, outcome) {
|
|
191
|
+
const replyTarget = toReplyTarget(job);
|
|
192
|
+
if (replyTarget === null) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await this.executor.appendContextToConversation({
|
|
197
|
+
conversationKey: this.resolveConversationKeyForTarget(replyTarget),
|
|
198
|
+
replyTarget,
|
|
199
|
+
body: formatScheduleContextNote(job, scheduledForMs, outcome),
|
|
200
|
+
recordedAtMs: Date.now(),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
this.logger.log("warn", `failed to append schedule result to ${replyTarget.channel}:${replyTarget.target}: ${formatError(error)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
139
207
|
requireJob(id) {
|
|
140
208
|
const job = this.store.getCronJob(id);
|
|
141
209
|
if (job === null) {
|
|
142
|
-
throw new Error(`unknown
|
|
210
|
+
throw new Error(`unknown schedule job: ${id}`);
|
|
143
211
|
}
|
|
144
212
|
return job;
|
|
145
213
|
}
|
|
@@ -186,7 +254,9 @@ function normalizeUpsertInput(input) {
|
|
|
186
254
|
}
|
|
187
255
|
return {
|
|
188
256
|
id,
|
|
257
|
+
kind: "cron",
|
|
189
258
|
schedule,
|
|
259
|
+
runAtMs: null,
|
|
190
260
|
prompt,
|
|
191
261
|
enabled: input.enabled,
|
|
192
262
|
deliveryChannel,
|
|
@@ -196,8 +266,57 @@ function normalizeUpsertInput(input) {
|
|
|
196
266
|
recordedAtMs: 0,
|
|
197
267
|
};
|
|
198
268
|
}
|
|
269
|
+
function normalizeOnceInput(input) {
|
|
270
|
+
const id = normalizeId(input.id);
|
|
271
|
+
const prompt = normalizeRequiredField(input.prompt, "schedule prompt");
|
|
272
|
+
const deliveryChannel = normalizeOptionalField(input.deliveryChannel);
|
|
273
|
+
const deliveryTarget = normalizeOptionalField(input.deliveryTarget);
|
|
274
|
+
const deliveryTopic = normalizeOptionalField(input.deliveryTopic);
|
|
275
|
+
if ((deliveryChannel === null) !== (deliveryTarget === null)) {
|
|
276
|
+
throw new Error("schedule delivery_channel and delivery_target must be provided together");
|
|
277
|
+
}
|
|
278
|
+
if (deliveryChannel === null && deliveryTopic !== null) {
|
|
279
|
+
throw new Error("schedule delivery_topic requires delivery_channel and delivery_target");
|
|
280
|
+
}
|
|
281
|
+
if (deliveryChannel !== null && deliveryChannel !== "telegram") {
|
|
282
|
+
throw new Error(`unsupported schedule delivery channel: ${deliveryChannel}`);
|
|
283
|
+
}
|
|
284
|
+
const runAtMs = resolveOnceRunAt(input);
|
|
285
|
+
return {
|
|
286
|
+
id,
|
|
287
|
+
kind: "once",
|
|
288
|
+
schedule: null,
|
|
289
|
+
runAtMs,
|
|
290
|
+
prompt,
|
|
291
|
+
enabled: true,
|
|
292
|
+
deliveryChannel,
|
|
293
|
+
deliveryTarget,
|
|
294
|
+
deliveryTopic,
|
|
295
|
+
nextRunAtMs: runAtMs,
|
|
296
|
+
recordedAtMs: 0,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function resolveOnceRunAt(input) {
|
|
300
|
+
if (input.delaySeconds === null && input.runAtMs === null) {
|
|
301
|
+
throw new Error("schedule_once requires delay_seconds or run_at_ms");
|
|
302
|
+
}
|
|
303
|
+
if (input.delaySeconds !== null && input.runAtMs !== null) {
|
|
304
|
+
throw new Error("schedule_once accepts only one of delay_seconds or run_at_ms");
|
|
305
|
+
}
|
|
306
|
+
if (input.runAtMs !== null) {
|
|
307
|
+
if (!Number.isSafeInteger(input.runAtMs) || input.runAtMs < 0) {
|
|
308
|
+
throw new Error("schedule run_at_ms must be a non-negative integer");
|
|
309
|
+
}
|
|
310
|
+
return input.runAtMs;
|
|
311
|
+
}
|
|
312
|
+
const delaySeconds = input.delaySeconds ?? 0;
|
|
313
|
+
if (!Number.isSafeInteger(delaySeconds) || delaySeconds < 0) {
|
|
314
|
+
throw new Error("schedule delay_seconds must be a non-negative integer");
|
|
315
|
+
}
|
|
316
|
+
return Date.now() + delaySeconds * 1_000;
|
|
317
|
+
}
|
|
199
318
|
function normalizeId(id) {
|
|
200
|
-
return normalizeRequiredField(id, "
|
|
319
|
+
return normalizeRequiredField(id, "schedule id");
|
|
201
320
|
}
|
|
202
321
|
function normalizeRequiredField(value, field) {
|
|
203
322
|
const trimmed = value.trim();
|
|
@@ -216,7 +335,7 @@ function normalizeOptionalField(value) {
|
|
|
216
335
|
function toBindingCronJobSpec(job) {
|
|
217
336
|
return {
|
|
218
337
|
id: job.id,
|
|
219
|
-
schedule: job.schedule,
|
|
338
|
+
schedule: normalizeRequiredField(job.schedule ?? "", "cron schedule"),
|
|
220
339
|
prompt: job.prompt,
|
|
221
340
|
deliveryChannel: job.deliveryChannel,
|
|
222
341
|
deliveryTarget: job.deliveryTarget,
|
|
@@ -235,3 +354,49 @@ function sleep(durationMs) {
|
|
|
235
354
|
setTimeout(resolve, durationMs);
|
|
236
355
|
});
|
|
237
356
|
}
|
|
357
|
+
function clampStatusLimit(limit) {
|
|
358
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
359
|
+
throw new Error("schedule_status limit must be a positive integer");
|
|
360
|
+
}
|
|
361
|
+
return Math.min(limit, MAX_STATUS_RUNS);
|
|
362
|
+
}
|
|
363
|
+
function deriveJobState(job, latestRun) {
|
|
364
|
+
if (latestRun?.status === "running") {
|
|
365
|
+
return "running";
|
|
366
|
+
}
|
|
367
|
+
if (job.enabled) {
|
|
368
|
+
return "scheduled";
|
|
369
|
+
}
|
|
370
|
+
if (latestRun !== null) {
|
|
371
|
+
return latestRun.status;
|
|
372
|
+
}
|
|
373
|
+
return "canceled";
|
|
374
|
+
}
|
|
375
|
+
function isTerminalJob(job) {
|
|
376
|
+
return !job.enabled;
|
|
377
|
+
}
|
|
378
|
+
function toReplyTarget(job) {
|
|
379
|
+
if (job.deliveryChannel === null || job.deliveryTarget === null) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
channel: job.deliveryChannel,
|
|
384
|
+
target: job.deliveryTarget,
|
|
385
|
+
topic: job.deliveryTopic,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function conversationKeyForJob(job) {
|
|
389
|
+
return job.kind === "cron" ? `cron:${job.id}` : `once:${job.id}`;
|
|
390
|
+
}
|
|
391
|
+
function formatScheduleContextNote(job, scheduledForMs, outcome) {
|
|
392
|
+
const header = [
|
|
393
|
+
"[Gateway schedule result]",
|
|
394
|
+
`job_id=${job.id}`,
|
|
395
|
+
`job_kind=${job.kind}`,
|
|
396
|
+
`scheduled_for_ms=${scheduledForMs}`,
|
|
397
|
+
];
|
|
398
|
+
if (outcome.kind === "success") {
|
|
399
|
+
return [...header, "status=succeeded", "", outcome.responseText].join("\n");
|
|
400
|
+
}
|
|
401
|
+
return [...header, "status=failed", "", outcome.errorMessage].join("\n");
|
|
402
|
+
}
|
package/dist/delivery/text.js
CHANGED
|
@@ -114,7 +114,7 @@ class ProgressiveTextDeliverySession {
|
|
|
114
114
|
this.telegramSupport.startTyping(this.target);
|
|
115
115
|
}
|
|
116
116
|
async preview(text) {
|
|
117
|
-
if (this.previewFailed || this.closed) {
|
|
117
|
+
if (this.previewFailed || this.closed || text.trim().length === 0) {
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
const runPreview = async () => {
|
package/dist/gateway.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ChannelFileSender } from "./host/file-sender";
|
|
|
5
5
|
import { GatewayExecutor } from "./runtime/executor";
|
|
6
6
|
import { GatewaySessionContext } from "./session/context";
|
|
7
7
|
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
8
|
+
import { GatewaySystemPromptBuilder } from "./session/system-prompt";
|
|
8
9
|
import { GatewayTelegramRuntime } from "./telegram/runtime";
|
|
9
10
|
export type GatewayPluginStatus = {
|
|
10
11
|
runtimeMode: string;
|
|
@@ -27,7 +28,8 @@ export declare class GatewayPluginRuntime {
|
|
|
27
28
|
readonly files: ChannelFileSender;
|
|
28
29
|
readonly channelSessions: ChannelSessionSwitcher;
|
|
29
30
|
readonly sessionContext: GatewaySessionContext;
|
|
30
|
-
|
|
31
|
+
readonly systemPrompts: GatewaySystemPromptBuilder;
|
|
32
|
+
constructor(contract: GatewayContract, executor: GatewayExecutor, cron: GatewayCronRuntime, telegram: GatewayTelegramRuntime, files: ChannelFileSender, channelSessions: ChannelSessionSwitcher, sessionContext: GatewaySessionContext, systemPrompts: GatewaySystemPromptBuilder);
|
|
31
33
|
status(): GatewayPluginStatus;
|
|
32
34
|
}
|
|
33
35
|
export declare function createGatewayRuntime(module: GatewayBindingModule, input: PluginInput): Promise<GatewayPluginRuntime>;
|
package/dist/gateway.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
1
2
|
import { loadGatewayConfig } from "./config/gateway";
|
|
2
3
|
import { GatewayCronRuntime } from "./cron/runtime";
|
|
3
4
|
import { TelegramProgressiveSupport } from "./delivery/telegram";
|
|
4
5
|
import { GatewayTextDelivery } from "./delivery/text";
|
|
5
6
|
import { ChannelFileSender } from "./host/file-sender";
|
|
6
|
-
import { ConsoleLoggerHost } from "./host/
|
|
7
|
+
import { ConsoleLoggerHost } from "./host/logger";
|
|
7
8
|
import { GatewayTransportHost } from "./host/transport";
|
|
8
9
|
import { GatewayMailboxRouter } from "./mailbox/router";
|
|
10
|
+
import { GatewayMemoryPromptProvider } from "./memory/prompt";
|
|
9
11
|
import { OpencodeSdkAdapter } from "./opencode/adapter";
|
|
10
12
|
import { OpencodeEventStream } from "./opencode/event-stream";
|
|
11
13
|
import { OpencodeEventHub } from "./opencode/events";
|
|
@@ -13,8 +15,11 @@ import { createQuestionClient } from "./questions/client";
|
|
|
13
15
|
import { GatewayQuestionRuntime } from "./questions/runtime";
|
|
14
16
|
import { GatewayExecutor } from "./runtime/executor";
|
|
15
17
|
import { GatewayMailboxRuntime } from "./runtime/mailbox";
|
|
18
|
+
import { getOrCreateRuntimeSingleton } from "./runtime/runtime-singleton";
|
|
16
19
|
import { GatewaySessionContext } from "./session/context";
|
|
20
|
+
import { resolveConversationKeyForTarget } from "./session/conversation-key";
|
|
17
21
|
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
22
|
+
import { GatewaySystemPromptBuilder } from "./session/system-prompt";
|
|
18
23
|
import { openSqliteStore } from "./store/sqlite";
|
|
19
24
|
import { TelegramBotClient } from "./telegram/client";
|
|
20
25
|
import { TelegramInboundMediaStore } from "./telegram/media";
|
|
@@ -28,7 +33,8 @@ export class GatewayPluginRuntime {
|
|
|
28
33
|
files;
|
|
29
34
|
channelSessions;
|
|
30
35
|
sessionContext;
|
|
31
|
-
|
|
36
|
+
systemPrompts;
|
|
37
|
+
constructor(contract, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts) {
|
|
32
38
|
this.contract = contract;
|
|
33
39
|
this.executor = executor;
|
|
34
40
|
this.cron = cron;
|
|
@@ -36,6 +42,7 @@ export class GatewayPluginRuntime {
|
|
|
36
42
|
this.files = files;
|
|
37
43
|
this.channelSessions = channelSessions;
|
|
38
44
|
this.sessionContext = sessionContext;
|
|
45
|
+
this.systemPrompts = systemPrompts;
|
|
39
46
|
}
|
|
40
47
|
status() {
|
|
41
48
|
const rustStatus = this.contract.gatewayStatus();
|
|
@@ -56,41 +63,46 @@ export class GatewayPluginRuntime {
|
|
|
56
63
|
}
|
|
57
64
|
export async function createGatewayRuntime(module, input) {
|
|
58
65
|
const config = await loadGatewayConfig();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
66
|
+
return await getOrCreateRuntimeSingleton(config.configPath, async () => {
|
|
67
|
+
await mkdir(config.workspaceDirPath, { recursive: true });
|
|
68
|
+
const logger = new ConsoleLoggerHost(config.logLevel);
|
|
69
|
+
if (config.hasLegacyGatewayTimezone) {
|
|
70
|
+
const suffix = config.legacyGatewayTimezone === null ? "" : ` (${config.legacyGatewayTimezone})`;
|
|
71
|
+
logger.log("warn", `gateway.timezone${suffix} is ignored; use cron.timezone instead`);
|
|
72
|
+
}
|
|
73
|
+
const effectiveCronTimeZone = resolveEffectiveCronTimeZone(module, config);
|
|
74
|
+
const store = await openSqliteStore(config.stateDbPath);
|
|
75
|
+
const sessionContext = new GatewaySessionContext(store);
|
|
76
|
+
const memoryPrompts = new GatewayMemoryPromptProvider(config.memory, logger);
|
|
77
|
+
const systemPrompts = new GatewaySystemPromptBuilder(sessionContext, memoryPrompts);
|
|
78
|
+
const telegramClient = config.telegram.enabled ? new TelegramBotClient(config.telegram.botToken) : null;
|
|
79
|
+
const telegramMediaStore = config.telegram.enabled && telegramClient !== null
|
|
80
|
+
? new TelegramInboundMediaStore(telegramClient, config.mediaRootPath)
|
|
81
|
+
: null;
|
|
82
|
+
const mailboxRouter = new GatewayMailboxRouter(config.mailbox.routes);
|
|
83
|
+
const opencodeEvents = new OpencodeEventHub();
|
|
84
|
+
const opencode = new OpencodeSdkAdapter(input.client, config.workspaceDirPath);
|
|
85
|
+
const questionClient = createQuestionClient(input.client, input.serverUrl, config.workspaceDirPath);
|
|
86
|
+
const transport = new GatewayTransportHost(telegramClient, store);
|
|
87
|
+
const files = new ChannelFileSender(telegramClient);
|
|
88
|
+
const channelSessions = new ChannelSessionSwitcher(store, sessionContext, mailboxRouter, module, opencode, config.telegram.enabled);
|
|
89
|
+
const questions = new GatewayQuestionRuntime(questionClient, config.workspaceDirPath, store, sessionContext, transport, telegramClient, logger);
|
|
90
|
+
const progressiveSupport = new TelegramProgressiveSupport(telegramClient, store, logger);
|
|
91
|
+
const delivery = new GatewayTextDelivery(transport, store, progressiveSupport);
|
|
92
|
+
const executor = new GatewayExecutor(module, store, opencode, opencodeEvents, delivery, logger);
|
|
93
|
+
const mailbox = new GatewayMailboxRuntime(executor, store, logger, config.mailbox, questions);
|
|
94
|
+
const cron = new GatewayCronRuntime(executor, module, store, logger, config.cron, effectiveCronTimeZone, (target) => resolveConversationKeyForTarget(target, mailboxRouter, module));
|
|
95
|
+
const eventStream = new OpencodeEventStream(input.client, config.workspaceDirPath, opencodeEvents, [questions], logger);
|
|
96
|
+
const telegramPolling = config.telegram.enabled && telegramClient !== null && telegramMediaStore !== null
|
|
97
|
+
? new TelegramPollingService(telegramClient, mailbox, store, logger, config.telegram, mailboxRouter, telegramMediaStore, questions)
|
|
98
|
+
: null;
|
|
99
|
+
const telegram = new GatewayTelegramRuntime(telegramClient, delivery, store, logger, config.telegram, telegramPolling, eventStream);
|
|
100
|
+
eventStream.start();
|
|
101
|
+
cron.start();
|
|
102
|
+
mailbox.start();
|
|
103
|
+
telegram.start();
|
|
104
|
+
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts);
|
|
105
|
+
});
|
|
94
106
|
}
|
|
95
107
|
function resolveEffectiveCronTimeZone(module, config) {
|
|
96
108
|
const candidate = config.cron.timezone ?? resolveRuntimeLocalTimeZone();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BindingLoggerHost, BindingLogLevel } from "../binding";
|
|
2
|
+
export type GatewayLogLevel = BindingLogLevel | "off";
|
|
3
|
+
export declare class ConsoleLoggerHost implements BindingLoggerHost {
|
|
4
|
+
private readonly threshold;
|
|
5
|
+
constructor(threshold: GatewayLogLevel);
|
|
6
|
+
log(level: BindingLogLevel, message: string): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseGatewayLogLevel(value: unknown, field: string): GatewayLogLevel;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const LOG_LEVEL_PRIORITY = {
|
|
2
|
+
debug: 10,
|
|
3
|
+
info: 20,
|
|
4
|
+
warn: 30,
|
|
5
|
+
error: 40,
|
|
6
|
+
};
|
|
7
|
+
export class ConsoleLoggerHost {
|
|
8
|
+
threshold;
|
|
9
|
+
constructor(threshold) {
|
|
10
|
+
this.threshold = threshold;
|
|
11
|
+
}
|
|
12
|
+
log(level, message) {
|
|
13
|
+
if (!shouldLog(level, this.threshold)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const line = `[gateway:${level}] ${message}`;
|
|
17
|
+
switch (level) {
|
|
18
|
+
case "error":
|
|
19
|
+
console.error(line);
|
|
20
|
+
return;
|
|
21
|
+
case "warn":
|
|
22
|
+
console.warn(line);
|
|
23
|
+
return;
|
|
24
|
+
default:
|
|
25
|
+
console.info(line);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function parseGatewayLogLevel(value, field) {
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
return "off";
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== "string") {
|
|
34
|
+
throw new Error(`${field} must be a string when present`);
|
|
35
|
+
}
|
|
36
|
+
const normalized = value.trim().toLowerCase();
|
|
37
|
+
if (normalized === "off") {
|
|
38
|
+
return "off";
|
|
39
|
+
}
|
|
40
|
+
if (isBindingLogLevel(normalized)) {
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`${field} must be one of: off, error, warn, info, debug`);
|
|
44
|
+
}
|
|
45
|
+
function shouldLog(level, threshold) {
|
|
46
|
+
if (threshold === "off") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[threshold];
|
|
50
|
+
}
|
|
51
|
+
function isBindingLogLevel(value) {
|
|
52
|
+
return value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
53
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,14 @@ import { loadGatewayBindingModule } from "./binding";
|
|
|
2
2
|
import { createGatewayRuntime } from "./gateway";
|
|
3
3
|
import { createChannelNewSessionTool } from "./tools/channel-new-session";
|
|
4
4
|
import { createChannelSendFileTool } from "./tools/channel-send-file";
|
|
5
|
-
import { createCronListTool } from "./tools/cron-list";
|
|
6
|
-
import { createCronRemoveTool } from "./tools/cron-remove";
|
|
7
5
|
import { createCronRunTool } from "./tools/cron-run";
|
|
8
6
|
import { createCronUpsertTool } from "./tools/cron-upsert";
|
|
9
7
|
import { createGatewayDispatchCronTool } from "./tools/gateway-dispatch-cron";
|
|
10
8
|
import { createGatewayStatusTool } from "./tools/gateway-status";
|
|
9
|
+
import { createScheduleCancelTool } from "./tools/schedule-cancel";
|
|
10
|
+
import { createScheduleListTool } from "./tools/schedule-list";
|
|
11
|
+
import { createScheduleOnceTool } from "./tools/schedule-once";
|
|
12
|
+
import { createScheduleStatusTool } from "./tools/schedule-status";
|
|
11
13
|
import { createTelegramSendTestTool } from "./tools/telegram-send-test";
|
|
12
14
|
import { createTelegramStatusTool } from "./tools/telegram-status";
|
|
13
15
|
/**
|
|
@@ -18,12 +20,14 @@ export const OpencodeGatewayPlugin = async (input) => {
|
|
|
18
20
|
const gatewayModule = await loadGatewayBindingModule();
|
|
19
21
|
const runtime = await createGatewayRuntime(gatewayModule, input);
|
|
20
22
|
const tools = {
|
|
21
|
-
cron_list: createCronListTool(runtime.cron),
|
|
22
|
-
cron_remove: createCronRemoveTool(runtime.cron),
|
|
23
23
|
cron_run: createCronRunTool(runtime.cron),
|
|
24
|
-
cron_upsert: createCronUpsertTool(runtime.cron),
|
|
24
|
+
cron_upsert: createCronUpsertTool(runtime.cron, runtime.sessionContext),
|
|
25
25
|
gateway_status: createGatewayStatusTool(runtime),
|
|
26
26
|
gateway_dispatch_cron: createGatewayDispatchCronTool(runtime.executor),
|
|
27
|
+
schedule_cancel: createScheduleCancelTool(runtime.cron),
|
|
28
|
+
schedule_list: createScheduleListTool(runtime.cron),
|
|
29
|
+
schedule_once: createScheduleOnceTool(runtime.cron, runtime.sessionContext),
|
|
30
|
+
schedule_status: createScheduleStatusTool(runtime.cron),
|
|
27
31
|
};
|
|
28
32
|
if (runtime.files.hasEnabledChannel()) {
|
|
29
33
|
tools.channel_send_file = createChannelSendFileTool(runtime.files, runtime.sessionContext);
|
|
@@ -42,8 +46,8 @@ export const OpencodeGatewayPlugin = async (input) => {
|
|
|
42
46
|
if (!sessionId) {
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
49
|
+
const systemPrompts = await runtime.systemPrompts.buildPrompts(sessionId);
|
|
50
|
+
for (const systemPrompt of systemPrompts) {
|
|
47
51
|
output.system.push(systemPrompt);
|
|
48
52
|
}
|
|
49
53
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BindingLoggerHost } from "../binding";
|
|
2
|
+
import type { GatewayMemoryConfig } from "../config/memory";
|
|
3
|
+
export declare class GatewayMemoryPromptProvider {
|
|
4
|
+
private readonly config;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
constructor(config: GatewayMemoryConfig, logger: Pick<BindingLoggerHost, "log">);
|
|
7
|
+
buildPrompt(): Promise<string | null>;
|
|
8
|
+
private buildEntrySection;
|
|
9
|
+
}
|