@kevin5251984/guild 0.2.20 → 0.2.22
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/cordis.yml +2 -0
- package/package.json +1 -1
- package/src/cron-schedule.ts +388 -0
- package/src/cron.ts +403 -0
- package/src/db.ts +135 -0
- package/src/generate.ts +9 -0
- package/src/handlers.ts +102 -5
- package/src/harness.ts +25 -5
- package/src/llm.ts +1 -1
- package/src/plugins/cron.ts +26 -0
- package/src/public/chat.css +118 -9
- package/src/public/chat.html +475 -43
- package/src/public/i18n.js +23 -0
- package/src/public/md.js +109 -15
- package/src/public/mobile.css +116 -2
- package/src/public/mobile.html +185 -15
- package/src/router.ts +161 -2
- package/src/store.ts +116 -33
- package/src/subagent.ts +3 -0
- package/src/tools.ts +57 -1
- package/vendor/protocol/src/index.ts +7 -2
package/src/router.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { extname } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import type { LibraryKind, ModelRef, ModelsFile } from "@guild/protocol";
|
|
5
6
|
import {
|
|
@@ -24,6 +25,8 @@ import {
|
|
|
24
25
|
getBotDetail,
|
|
25
26
|
getLiveTurn,
|
|
26
27
|
abortLiveTurn,
|
|
28
|
+
pauseLiveTurn,
|
|
29
|
+
continueLiveTurn,
|
|
27
30
|
healthPayload,
|
|
28
31
|
importSkills,
|
|
29
32
|
mergeModelsFile,
|
|
@@ -64,6 +67,30 @@ import { hostGit, hostList, hostRead, hostTree } from "./host-browse.ts";
|
|
|
64
67
|
import { listHostSkills } from "./host-skills.ts";
|
|
65
68
|
import { listHostAgents } from "./host-agents.ts";
|
|
66
69
|
import { generatedDir, isSafeGeneratedName } from "./image-gen.ts";
|
|
70
|
+
import {
|
|
71
|
+
createCronJob,
|
|
72
|
+
fireCronJob,
|
|
73
|
+
pauseCronJob,
|
|
74
|
+
publicCronJob,
|
|
75
|
+
removeCronJob,
|
|
76
|
+
resumeCronJob,
|
|
77
|
+
} from "./cron.ts";
|
|
78
|
+
import {
|
|
79
|
+
allowedWorkspaceWritePath,
|
|
80
|
+
resolveToolPath,
|
|
81
|
+
workspaceFromEnv,
|
|
82
|
+
} from "./harness.ts";
|
|
83
|
+
|
|
84
|
+
const LOCAL_IMAGE_CAP = 12 * 1024 * 1024;
|
|
85
|
+
|
|
86
|
+
function localImageType(path: string): string | null {
|
|
87
|
+
const ext = extname(path).toLowerCase();
|
|
88
|
+
if (ext === ".png") return "image/png";
|
|
89
|
+
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
90
|
+
if (ext === ".gif") return "image/gif";
|
|
91
|
+
if (ext === ".webp") return "image/webp";
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
67
94
|
|
|
68
95
|
const PUBLIC = fileURLToPath(new URL("./public/", import.meta.url));
|
|
69
96
|
|
|
@@ -271,7 +298,8 @@ function modelRefFrom(value: unknown): ModelRef | null {
|
|
|
271
298
|
const provider = str(rec, "provider").trim();
|
|
272
299
|
const model = str(rec, "model").trim();
|
|
273
300
|
if (!provider || !model) return null;
|
|
274
|
-
|
|
301
|
+
const reasoning = str(rec, "reasoning").trim();
|
|
302
|
+
return reasoning ? { provider, model, reasoning } : { provider, model };
|
|
275
303
|
}
|
|
276
304
|
|
|
277
305
|
function strList(record: Record<string, unknown>, key: string): string[] {
|
|
@@ -388,6 +416,38 @@ export async function handleRequest(
|
|
|
388
416
|
return;
|
|
389
417
|
}
|
|
390
418
|
|
|
419
|
+
if (method === "GET" && path === "/local") {
|
|
420
|
+
const raw = requestUrl(req).searchParams.get("p") || "";
|
|
421
|
+
const type = localImageType(raw);
|
|
422
|
+
if (!raw.startsWith("/") || raw.includes("\0") || raw.includes("..") || !type) {
|
|
423
|
+
json(res, 404, { error: "not_found", path });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const target = resolveToolPath(raw);
|
|
427
|
+
if (!allowedWorkspaceWritePath(target, workspaceFromEnv(env), store.dataDir)) {
|
|
428
|
+
json(res, 404, { error: "not_found", path });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!existsSync(target)) {
|
|
432
|
+
json(res, 404, { error: "not_found", path });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const st = statSync(target);
|
|
436
|
+
if (!st.isFile() || st.size > LOCAL_IMAGE_CAP) {
|
|
437
|
+
json(res, 404, { error: "not_found", path });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const bytes = readFileSync(target);
|
|
441
|
+
res.writeHead(200, {
|
|
442
|
+
"content-type": type,
|
|
443
|
+
"content-length": bytes.length,
|
|
444
|
+
"cache-control": "private, max-age=60",
|
|
445
|
+
"x-content-type-options": "nosniff",
|
|
446
|
+
});
|
|
447
|
+
res.end(bytes);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
391
451
|
if (method === "GET" && path.startsWith("/generated/")) {
|
|
392
452
|
const name = decodeURIComponent(path.slice("/generated/".length));
|
|
393
453
|
if (!isSafeGeneratedName(name)) {
|
|
@@ -441,6 +501,45 @@ export async function handleRequest(
|
|
|
441
501
|
return;
|
|
442
502
|
}
|
|
443
503
|
|
|
504
|
+
if (path === "/cron" && method === "GET") {
|
|
505
|
+
const room = requestUrl(req).searchParams.get("room") || "";
|
|
506
|
+
json(res, 200, {
|
|
507
|
+
jobs: store.listCronJobs(room || undefined).map(publicCronJob),
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (path === "/cron" && method === "POST") {
|
|
512
|
+
const body = asRecord(await readJson(req));
|
|
513
|
+
const job = createCronJob(store, {
|
|
514
|
+
roomId: str(body, "roomId") || str(body, "room_id"),
|
|
515
|
+
botId: str(body, "botId") || str(body, "bot_id"),
|
|
516
|
+
prompt: str(body, "prompt"),
|
|
517
|
+
schedule: str(body, "schedule"),
|
|
518
|
+
name: str(body, "name"),
|
|
519
|
+
});
|
|
520
|
+
json(res, 201, publicCronJob(job));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const cronAct = path.match(/^\/cron\/([^/]+)\/(pause|resume|run)$/);
|
|
524
|
+
if (cronAct && method === "POST") {
|
|
525
|
+
const id = decodeURIComponent(cronAct[1]);
|
|
526
|
+
if (cronAct[2] === "pause") {
|
|
527
|
+
json(res, 200, publicCronJob(pauseCronJob(store, id)));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (cronAct[2] === "resume") {
|
|
531
|
+
json(res, 200, publicCronJob(resumeCronJob(store, id)));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
json(res, 200, await fireCronJob(store, id, env));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const cronOne = path.match(/^\/cron\/([^/]+)$/);
|
|
538
|
+
if (cronOne && method === "DELETE") {
|
|
539
|
+
json(res, 200, removeCronJob(store, decodeURIComponent(cronOne[1])));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
444
543
|
if (method === "GET" && (path === "/bots" || path === "/bench")) {
|
|
445
544
|
json(res, 200, listBench(store));
|
|
446
545
|
return;
|
|
@@ -709,6 +808,66 @@ export async function handleRequest(
|
|
|
709
808
|
return;
|
|
710
809
|
}
|
|
711
810
|
|
|
811
|
+
const channelPause = path.match(/^\/channels\/([^/]+)\/pause$/);
|
|
812
|
+
if (channelPause && method === "POST") {
|
|
813
|
+
const body = asRecord(await readJson(req));
|
|
814
|
+
json(
|
|
815
|
+
res,
|
|
816
|
+
200,
|
|
817
|
+
pauseLiveTurn(
|
|
818
|
+
store,
|
|
819
|
+
decodeURIComponent(channelPause[1]),
|
|
820
|
+
str(body, "botId") || undefined,
|
|
821
|
+
),
|
|
822
|
+
);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const dmPause = path.match(/^\/dms\/([^/]+)\/pause$/);
|
|
826
|
+
if (dmPause && method === "POST") {
|
|
827
|
+
const room = openDm(store, decodeURIComponent(dmPause[1]));
|
|
828
|
+
const body = asRecord(await readJson(req));
|
|
829
|
+
json(
|
|
830
|
+
res,
|
|
831
|
+
200,
|
|
832
|
+
pauseLiveTurn(store, room.id, str(body, "botId") || undefined),
|
|
833
|
+
);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const channelContinue = path.match(/^\/channels\/([^/]+)\/continue$/);
|
|
838
|
+
if (channelContinue && method === "POST") {
|
|
839
|
+
const body = asRecord(await readJson(req));
|
|
840
|
+
json(
|
|
841
|
+
res,
|
|
842
|
+
200,
|
|
843
|
+
await continueLiveTurn(
|
|
844
|
+
store,
|
|
845
|
+
decodeURIComponent(channelContinue[1]),
|
|
846
|
+
str(body, "botId"),
|
|
847
|
+
env,
|
|
848
|
+
extras,
|
|
849
|
+
),
|
|
850
|
+
);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const dmContinue = path.match(/^\/dms\/([^/]+)\/continue$/);
|
|
854
|
+
if (dmContinue && method === "POST") {
|
|
855
|
+
const room = openDm(store, decodeURIComponent(dmContinue[1]));
|
|
856
|
+
const body = asRecord(await readJson(req));
|
|
857
|
+
json(
|
|
858
|
+
res,
|
|
859
|
+
200,
|
|
860
|
+
await continueLiveTurn(
|
|
861
|
+
store,
|
|
862
|
+
room.id,
|
|
863
|
+
str(body, "botId"),
|
|
864
|
+
env,
|
|
865
|
+
extras,
|
|
866
|
+
),
|
|
867
|
+
);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
712
871
|
const channelSteer = path.match(/^\/channels\/([^/]+)\/steer$/);
|
|
713
872
|
if (channelSteer && method === "POST") {
|
|
714
873
|
const body = asRecord(await readJson(req));
|
package/src/store.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { TrajectoryDraft, TrajectoryEvent } from "./trajectory.ts";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
|
-
import { closeGuildDb, openGuildDb, type GuildDb } from "./db.ts";
|
|
13
|
+
import { closeGuildDb, openGuildDb, type CronJobRow, type GuildDb } from "./db.ts";
|
|
14
14
|
import type {
|
|
15
15
|
Bot,
|
|
16
16
|
ChatAttachment,
|
|
@@ -101,6 +101,9 @@ export type LiveTurn = {
|
|
|
101
101
|
startedAt?: string;
|
|
102
102
|
/** Full-ish tool history for Trajectory. Stripped from GET /live. */
|
|
103
103
|
traces?: LiveTrace[];
|
|
104
|
+
/** Seat assignment text, kept so Continue can resume after Pause. */
|
|
105
|
+
asked?: string;
|
|
106
|
+
paused?: boolean;
|
|
104
107
|
};
|
|
105
108
|
|
|
106
109
|
export class GuildStore {
|
|
@@ -366,7 +369,6 @@ export class GuildStore {
|
|
|
366
369
|
beginTurn(roomId: string, botIds: string[] = [""]): AbortSignal {
|
|
367
370
|
const controller = new AbortController();
|
|
368
371
|
const ids = botIds.length ? botIds : [""];
|
|
369
|
-
for (const botId of ids) this.bindBotAbort(roomId, botId, controller);
|
|
370
372
|
this.turnGroups.set(controller.signal, {
|
|
371
373
|
roomId,
|
|
372
374
|
botIds: new Set(ids),
|
|
@@ -375,43 +377,54 @@ export class GuildStore {
|
|
|
375
377
|
return controller.signal;
|
|
376
378
|
}
|
|
377
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Per-seat AbortController, child of the turn group. Pause/Stop one bot
|
|
382
|
+
* without taking the rest of the wave down.
|
|
383
|
+
*/
|
|
384
|
+
armBotTurn(roomId: string, botId: string, parent: AbortSignal): AbortSignal {
|
|
385
|
+
const controller = new AbortController();
|
|
386
|
+
const onParent = () => {
|
|
387
|
+
if (!controller.signal.aborted) controller.abort();
|
|
388
|
+
};
|
|
389
|
+
if (parent.aborted) onParent();
|
|
390
|
+
else parent.addEventListener("abort", onParent, { once: true });
|
|
391
|
+
this.bindBotAbort(roomId, botId, controller);
|
|
392
|
+
const group = this.turnGroups.get(parent);
|
|
393
|
+
if (group && group.roomId === roomId) group.botIds.add(botId);
|
|
394
|
+
return controller.signal;
|
|
395
|
+
}
|
|
396
|
+
|
|
378
397
|
adoptTurn(roomId: string, botId: string, signal: AbortSignal): void {
|
|
379
398
|
const group = this.turnGroups.get(signal);
|
|
380
399
|
if (!group || group.roomId !== roomId) return;
|
|
381
|
-
this.bindBotAbort(roomId, botId, group.controller);
|
|
382
400
|
group.botIds.add(botId);
|
|
383
401
|
}
|
|
384
402
|
|
|
403
|
+
private dropBotLive(roomId: string, botId: string): boolean {
|
|
404
|
+
const live = this.liveTurns.get(roomId);
|
|
405
|
+
const steers = this.pendingSteers.get(roomId);
|
|
406
|
+
const room = this.botAborts.get(roomId);
|
|
407
|
+
const hadLive = Boolean(live?.delete(botId));
|
|
408
|
+
steers?.delete(botId);
|
|
409
|
+
room?.delete(botId);
|
|
410
|
+
if (live && live.size === 0) this.liveTurns.delete(roomId);
|
|
411
|
+
if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
|
|
412
|
+
if (room && room.size === 0) this.botAborts.delete(roomId);
|
|
413
|
+
for (const [sig, group] of this.turnGroups) {
|
|
414
|
+
if (group.roomId !== roomId || !group.botIds.has(botId)) continue;
|
|
415
|
+
group.botIds.delete(botId);
|
|
416
|
+
if (group.botIds.size === 0) this.turnGroups.delete(sig);
|
|
417
|
+
}
|
|
418
|
+
return hadLive;
|
|
419
|
+
}
|
|
420
|
+
|
|
385
421
|
abortTurn(roomId: string, botId?: string): boolean {
|
|
386
422
|
if (botId) {
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
if (!controller)
|
|
390
|
-
const live = this.liveTurns.get(roomId);
|
|
391
|
-
const steers = this.pendingSteers.get(roomId);
|
|
392
|
-
const hadLive = Boolean(live?.delete(botId));
|
|
393
|
-
steers?.delete(botId);
|
|
394
|
-
if (live && live.size === 0) this.liveTurns.delete(roomId);
|
|
395
|
-
if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
|
|
396
|
-
this.spillTrajectoryIfIdle(roomId);
|
|
397
|
-
return hadLive;
|
|
398
|
-
}
|
|
399
|
-
const group = this.turnGroups.get(controller.signal);
|
|
400
|
-
const ids = group ? [...group.botIds] : [botId];
|
|
401
|
-
this.turnGroups.delete(controller.signal);
|
|
402
|
-
const live = this.liveTurns.get(roomId);
|
|
403
|
-
const steers = this.pendingSteers.get(roomId);
|
|
404
|
-
for (const id of ids) {
|
|
405
|
-
room?.delete(id);
|
|
406
|
-
live?.delete(id);
|
|
407
|
-
steers?.delete(id);
|
|
408
|
-
}
|
|
409
|
-
if (room && room.size === 0) this.botAborts.delete(roomId);
|
|
410
|
-
if (live && live.size === 0) this.liveTurns.delete(roomId);
|
|
411
|
-
if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
|
|
412
|
-
if (!controller.signal.aborted) controller.abort();
|
|
423
|
+
const controller = this.botAborts.get(roomId)?.get(botId);
|
|
424
|
+
const hadLive = this.dropBotLive(roomId, botId);
|
|
425
|
+
if (controller && !controller.signal.aborted) controller.abort();
|
|
413
426
|
this.spillTrajectoryIfIdle(roomId);
|
|
414
|
-
return
|
|
427
|
+
return hadLive || Boolean(controller);
|
|
415
428
|
}
|
|
416
429
|
const room = this.botAborts.get(roomId);
|
|
417
430
|
this.botAborts.delete(roomId);
|
|
@@ -419,10 +432,18 @@ export class GuildStore {
|
|
|
419
432
|
this.pendingSteers.delete(roomId);
|
|
420
433
|
let aborted = false;
|
|
421
434
|
const seen = new Set<AbortController>();
|
|
435
|
+
for (const [sig, group] of [...this.turnGroups]) {
|
|
436
|
+
if (group.roomId !== roomId) continue;
|
|
437
|
+
this.turnGroups.delete(sig);
|
|
438
|
+
if (!group.controller.signal.aborted) {
|
|
439
|
+
group.controller.abort();
|
|
440
|
+
aborted = true;
|
|
441
|
+
}
|
|
442
|
+
seen.add(group.controller);
|
|
443
|
+
}
|
|
422
444
|
for (const controller of room?.values() ?? []) {
|
|
423
445
|
if (seen.has(controller)) continue;
|
|
424
446
|
seen.add(controller);
|
|
425
|
-
this.turnGroups.delete(controller.signal);
|
|
426
447
|
if (!controller.signal.aborted) {
|
|
427
448
|
controller.abort();
|
|
428
449
|
aborted = true;
|
|
@@ -432,6 +453,37 @@ export class GuildStore {
|
|
|
432
453
|
return aborted || Boolean(room);
|
|
433
454
|
}
|
|
434
455
|
|
|
456
|
+
pauseTurn(roomId: string, botId?: string): boolean {
|
|
457
|
+
const ids = botId
|
|
458
|
+
? [botId]
|
|
459
|
+
: [...(this.liveTurns.get(roomId)?.keys() ?? [])];
|
|
460
|
+
let any = false;
|
|
461
|
+
for (const id of ids) {
|
|
462
|
+
if (!id) continue;
|
|
463
|
+
const live = this.getLiveBotTurn(roomId, id);
|
|
464
|
+
if (!live) continue;
|
|
465
|
+
const traces = (live.traces || []).map((tr) =>
|
|
466
|
+
tr.running ? { ...tr, running: false, text: tr.text || "paused" } : tr,
|
|
467
|
+
);
|
|
468
|
+
const steps = (live.steps || []).map((step) =>
|
|
469
|
+
step.running ? { ...step, running: false } : step,
|
|
470
|
+
);
|
|
471
|
+
this.setLiveTurn(roomId, {
|
|
472
|
+
...live,
|
|
473
|
+
traces,
|
|
474
|
+
steps,
|
|
475
|
+
paused: true,
|
|
476
|
+
});
|
|
477
|
+
const controller = this.botAborts.get(roomId)?.get(id);
|
|
478
|
+
this.botAborts.get(roomId)?.delete(id);
|
|
479
|
+
const room = this.botAborts.get(roomId);
|
|
480
|
+
if (room && room.size === 0) this.botAborts.delete(roomId);
|
|
481
|
+
if (controller && !controller.signal.aborted) controller.abort();
|
|
482
|
+
any = true;
|
|
483
|
+
}
|
|
484
|
+
return any;
|
|
485
|
+
}
|
|
486
|
+
|
|
435
487
|
endTurn(roomId: string, signal?: AbortSignal): void {
|
|
436
488
|
const group = signal ? this.turnGroups.get(signal) : undefined;
|
|
437
489
|
if (group) {
|
|
@@ -440,7 +492,11 @@ export class GuildStore {
|
|
|
440
492
|
const live = this.liveTurns.get(roomId);
|
|
441
493
|
const steers = this.pendingSteers.get(roomId);
|
|
442
494
|
for (const botId of group.botIds) {
|
|
443
|
-
if (
|
|
495
|
+
if (live?.get(botId)?.paused) {
|
|
496
|
+
room?.delete(botId);
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
room?.delete(botId);
|
|
444
500
|
live?.delete(botId);
|
|
445
501
|
steers?.delete(botId);
|
|
446
502
|
}
|
|
@@ -449,8 +505,14 @@ export class GuildStore {
|
|
|
449
505
|
if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
|
|
450
506
|
if (!group.controller.signal.aborted) group.controller.abort();
|
|
451
507
|
} else {
|
|
508
|
+
const live = this.liveTurns.get(roomId);
|
|
509
|
+
const kept = new Map<string, LiveTurn>();
|
|
510
|
+
for (const [id, turn] of live ?? []) {
|
|
511
|
+
if (turn.paused) kept.set(id, turn);
|
|
512
|
+
}
|
|
452
513
|
this.botAborts.delete(roomId);
|
|
453
|
-
this.
|
|
514
|
+
if (kept.size) this.liveTurns.set(roomId, kept);
|
|
515
|
+
else this.clearLiveTurn(roomId);
|
|
454
516
|
}
|
|
455
517
|
this.spillTrajectoryIfIdle(roomId);
|
|
456
518
|
}
|
|
@@ -1267,6 +1329,27 @@ export class GuildStore {
|
|
|
1267
1329
|
if (!this.getRoom(roomId)) throw new StoreError(404, "room not found");
|
|
1268
1330
|
this.db.writeCompact(roomId, compact);
|
|
1269
1331
|
}
|
|
1332
|
+
|
|
1333
|
+
listCronJobs(roomId?: string) {
|
|
1334
|
+
if (roomId && !this.getRoom(roomId)) throw new StoreError(404, "room not found");
|
|
1335
|
+
return this.db.listCronJobs(roomId);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
getCronJob(id: string) {
|
|
1339
|
+
const job = this.db.getCronJob(id);
|
|
1340
|
+
if (!job) throw new StoreError(404, "cron job not found");
|
|
1341
|
+
return job;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
writeCronJob(job: CronJobRow): void {
|
|
1345
|
+
if (!this.getRoom(job.roomId)) throw new StoreError(404, "room not found");
|
|
1346
|
+
if (!this.getBot(job.botId)) throw new StoreError(400, "bot not found");
|
|
1347
|
+
this.db.upsertCronJob(job);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
deleteCronJob(id: string): boolean {
|
|
1351
|
+
return this.db.deleteCronJob(id);
|
|
1352
|
+
}
|
|
1270
1353
|
}
|
|
1271
1354
|
|
|
1272
1355
|
function normalizePortrait(raw: string | null | undefined): string | undefined {
|
package/src/subagent.ts
CHANGED
|
@@ -373,6 +373,9 @@ export async function spawnSubagent(input: {
|
|
|
373
373
|
allowWrite: child.allowWrite,
|
|
374
374
|
sandbox: child.sandbox,
|
|
375
375
|
workspace: input.ctx.workspace,
|
|
376
|
+
roomId: input.ctx.roomId,
|
|
377
|
+
botId: input.ctx.botId,
|
|
378
|
+
cronRun: input.ctx.cronRun,
|
|
376
379
|
dispatch: input.ctx.dispatch,
|
|
377
380
|
signal: input.ctx.signal,
|
|
378
381
|
},
|
package/src/tools.ts
CHANGED
|
@@ -78,6 +78,12 @@ export type ToolContext = {
|
|
|
78
78
|
) => Promise<ToolOutcome>;
|
|
79
79
|
/** Devin-style background spawn handles for this turn. Same Map across dispatch clones. */
|
|
80
80
|
spawnHandles?: Map<string, SpawnHandle>;
|
|
81
|
+
/** Channel or DM this turn is in. Cron jobs default here. */
|
|
82
|
+
roomId?: string;
|
|
83
|
+
/** Seat running this turn. cronjob create defaults here. */
|
|
84
|
+
botId?: string;
|
|
85
|
+
/** Hermes: cron child sessions cannot manage cron. */
|
|
86
|
+
cronRun?: boolean;
|
|
81
87
|
};
|
|
82
88
|
|
|
83
89
|
export type SpawnHandle = {
|
|
@@ -191,6 +197,33 @@ export function guildTools(
|
|
|
191
197
|
(tool) => tool.name !== "image_gen" && tool.name !== "browser",
|
|
192
198
|
);
|
|
193
199
|
}
|
|
200
|
+
if (!ctx.cronRun) {
|
|
201
|
+
tools.push({
|
|
202
|
+
name: "cronjob",
|
|
203
|
+
description:
|
|
204
|
+
"Schedule a later hall turn (Hermes cronjob). Fresh @handle turn with a self-contained prompt. Actions: create, list, pause, resume, run, remove. schedule may be natural language (每10分鐘, 10分鐘後, 每天9點, in 30 minutes, every 2h, 0 9 * * *, ISO). Split when vs task: schedule is the time phrase, prompt is the work. bot_id defaults to this seat. Do not create cron jobs from a cron run.",
|
|
205
|
+
parameters: Type.Object({
|
|
206
|
+
action: Type.String({
|
|
207
|
+
description: "create | list | pause | resume | run | remove",
|
|
208
|
+
}),
|
|
209
|
+
schedule: Type.Optional(
|
|
210
|
+
Type.String({
|
|
211
|
+
description:
|
|
212
|
+
"Natural language or Hermes form: 每10分鐘, in 30m, every 2h, 每天9點, 0 9 * * *, ISO",
|
|
213
|
+
}),
|
|
214
|
+
),
|
|
215
|
+
prompt: Type.Optional(
|
|
216
|
+
Type.String({ description: "Self-contained task for the seat" }),
|
|
217
|
+
),
|
|
218
|
+
name: Type.Optional(Type.String({ description: "Short job name" })),
|
|
219
|
+
job_id: Type.Optional(Type.String({ description: "Job id or name" })),
|
|
220
|
+
bot_id: Type.Optional(Type.String({ description: "Seat to run" })),
|
|
221
|
+
room_id: Type.Optional(
|
|
222
|
+
Type.String({ description: "Room id. Defaults to this hall." }),
|
|
223
|
+
),
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
194
227
|
tools.push({
|
|
195
228
|
name: "skill",
|
|
196
229
|
description: `Load a staffed skill's full instructions by name.${available}`,
|
|
@@ -387,6 +420,21 @@ function openaiParameters(name: string): {
|
|
|
387
420
|
required: ["prompt"],
|
|
388
421
|
};
|
|
389
422
|
}
|
|
423
|
+
if (name === "cronjob") {
|
|
424
|
+
return {
|
|
425
|
+
type: "object",
|
|
426
|
+
properties: {
|
|
427
|
+
action: { type: "string", description: "create | list | pause | resume | run | remove" },
|
|
428
|
+
schedule: { type: "string" },
|
|
429
|
+
prompt: { type: "string" },
|
|
430
|
+
name: { type: "string" },
|
|
431
|
+
job_id: { type: "string" },
|
|
432
|
+
bot_id: { type: "string" },
|
|
433
|
+
room_id: { type: "string" },
|
|
434
|
+
},
|
|
435
|
+
required: ["action"],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
390
438
|
if (name === "browser") {
|
|
391
439
|
return {
|
|
392
440
|
type: "object",
|
|
@@ -437,6 +485,7 @@ export const BUILTIN_TOOL_NAMES = [
|
|
|
437
485
|
"read_spawn",
|
|
438
486
|
"image_gen",
|
|
439
487
|
"browser",
|
|
488
|
+
"cronjob",
|
|
440
489
|
] as const;
|
|
441
490
|
|
|
442
491
|
export async function executeTool(
|
|
@@ -512,6 +561,12 @@ export async function builtinExecute(
|
|
|
512
561
|
env: ctx.env,
|
|
513
562
|
});
|
|
514
563
|
}
|
|
564
|
+
if (name === "cronjob") {
|
|
565
|
+
return {
|
|
566
|
+
text: "cronjob needs guildd (the cron plugin)",
|
|
567
|
+
isError: true,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
515
570
|
if (name === "browser") {
|
|
516
571
|
const { runBrowser } = await import("./browser.ts");
|
|
517
572
|
return runBrowser(args, {
|
|
@@ -817,7 +872,7 @@ export function nextToolRound(round: number): ToolRoundPhase {
|
|
|
817
872
|
}
|
|
818
873
|
|
|
819
874
|
export const TOOL_SYSTEM = `You ARE already running on the user's local computer (Guild, same design as Pi / DeepSeek Harness).
|
|
820
|
-
Tools: run, read, write, list, skill, spawn, image_gen, browser, plus any connected MCP tools (names start with mcp__).
|
|
875
|
+
Tools: run, read, write, list, skill, spawn, image_gen, browser, cronjob, plus any connected MCP tools (names start with mcp__).
|
|
821
876
|
You can inspect RAM, disk, CPU, processes, files, and run shell commands.
|
|
822
877
|
Never say you cannot access this machine. Never tell the user to run the command themselves.
|
|
823
878
|
When the question is about this computer, call tools first, then answer with evidence from the output.
|
|
@@ -827,4 +882,5 @@ You stay coordinator. Spawn is the specialist, not a last resort (Devin run_suba
|
|
|
827
882
|
Independent tool calls in one round also run in parallel — fire several reads/searches together.
|
|
828
883
|
Check the [exit code: N] marker on every run result; investigate failures before moving on. Prefer the workdir argument over cd.
|
|
829
884
|
To follow a staffed skill, call skill with its exact name (or slug) before applying it. Relative paths in a skill resolve against that skill's base directory.
|
|
885
|
+
When the user asks to 排程 / schedule a later hall turn — including natural-language times like 每10分鐘, 10分鐘後, tomorrow 9am — call cronjob create. schedule is the time phrase; prompt is the self-contained task (the job will not see this live turn). bot_id defaults to you. Also accepts in 30m, every 2h, 0 9 * * *, ISO. A cron run cannot create more cron jobs.
|
|
830
886
|
Prefer small commands. macOS RAM: sysctl hw.memsize ; memory_pressure. Disk: df -h.`;
|
|
@@ -6,7 +6,12 @@ export type HealthResponse = {
|
|
|
6
6
|
|
|
7
7
|
export type BotStatus = "bench" | "staffed" | "running" | "retired";
|
|
8
8
|
|
|
9
|
-
export type ModelRef = {
|
|
9
|
+
export type ModelRef = {
|
|
10
|
+
provider: string;
|
|
11
|
+
model: string;
|
|
12
|
+
/** Seat-specific effort. Missing → that model's catalog default. */
|
|
13
|
+
reasoning?: string;
|
|
14
|
+
};
|
|
10
15
|
|
|
11
16
|
export type LibraryKind =
|
|
12
17
|
| "souls"
|
|
@@ -169,7 +174,7 @@ export type AuxRole =
|
|
|
169
174
|
|
|
170
175
|
export type ModelsFile = {
|
|
171
176
|
default?: ModelRef | null;
|
|
172
|
-
/**
|
|
177
|
+
/** Guild-default effort when a seat has no `model.reasoning`. */
|
|
173
178
|
reasoning?: string;
|
|
174
179
|
fast?: boolean;
|
|
175
180
|
aux?: Partial<Record<AuxRole, ModelRef | null>>;
|