@linxin666/dsh-client-ui-task-board 0.4.1 → 0.4.3
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.i18n.yaml +2 -2
- package/README.md +28 -3
- package/README.zh.md +28 -3
- package/lib/client.js +1142 -467
- package/lib/client.js.map +1 -1
- package/lib/index.js +2066 -1475
- package/lib/types/client/TaskBoardSettingsCard.d.ts +4 -0
- package/lib/types/client/board/LinkSubtaskModal.d.ts +11 -0
- package/lib/types/client/board/NewTaskModal.d.ts +7 -1
- package/lib/types/client/board/TaskCard.d.ts +11 -1
- package/lib/types/client/index.d.ts +15 -1
- package/lib/types/client/locales.d.ts +33 -0
- package/lib/types/client/native-panel.d.ts +62 -0
- package/lib/types/client/settings-entry-form.d.ts +49 -0
- package/lib/types/core/controller.d.ts +71 -3
- package/lib/types/core/subtask.d.ts +122 -0
- package/lib/types/core/tasks.d.ts +46 -3
- package/lib/types/core/use-cases/task-archive.d.ts +10 -13
- package/lib/types/core/use-cases/task-create.d.ts +13 -4
- package/lib/types/core/use-cases/task-delete.d.ts +7 -1
- package/lib/types/core/use-cases/task-parent.d.ts +21 -0
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host/agent-tools.d.ts +27 -0
- package/lib/types/host-ledger.d.ts +97 -4
- package/lib/types/host-runner.d.ts +39 -1
- package/lib/types/host-service.d.ts +98 -5
- package/lib/types/index.d.ts +93 -5
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/protocol.d.ts +14 -0
- package/package.json +26 -21
- package/src/client/TaskBoardSettingsCard.tsx +48 -3
- package/src/client/board/LinkSubtaskModal.tsx +82 -0
- package/src/client/board/NewTaskModal.tsx +40 -6
- package/src/client/board/TaskBoard.tsx +88 -8
- package/src/client/board/TaskCard.tsx +52 -2
- package/src/client/board/TaskDetail.tsx +124 -1
- package/src/client/board.module.css +112 -131
- package/src/client/host-api.ts +14 -4
- package/src/client/index.ts +56 -18
- package/src/client/locales.ts +70 -2
- package/src/client/native-panel.tsx +124 -0
- package/src/client/settings-entry-form.ts +142 -0
- package/src/core/controller.ts +128 -10
- package/src/core/store.ts +15 -0
- package/src/core/subtask.ts +286 -0
- package/src/core/tasks.ts +50 -2
- package/src/core/use-cases/task-archive.ts +46 -12
- package/src/core/use-cases/task-create.ts +42 -7
- package/src/core/use-cases/task-delete.ts +11 -1
- package/src/core/use-cases/task-parent.ts +64 -0
- package/src/core/use-cases/task-update.ts +3 -1
- package/src/host/agent-tools.ts +643 -0
- package/src/host-ledger.ts +343 -45
- package/src/host-routes.ts +37 -1
- package/src/host-runner.ts +141 -12
- package/src/host-service.ts +265 -32
- package/src/index.ts +203 -9
- package/src/mount-once.ts +107 -13
- package/src/protocol.ts +26 -2
- package/lib/types/client/board-mount.d.ts +0 -23
- package/lib/types/client/body-mutations.d.ts +0 -45
- package/lib/types/client/panel-mount-core.d.ts +0 -56
- package/lib/types/client/sidebar-entry-core.d.ts +0 -55
- package/lib/types/client/sidebar-entry.d.ts +0 -29
- package/src/client/board-mount.tsx +0 -43
- package/src/client/body-mutations.ts +0 -133
- package/src/client/panel-mount-core.ts +0 -155
- package/src/client/sidebar-entry-core.ts +0 -237
- package/src/client/sidebar-entry.ts +0 -53
package/lib/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import z from "@deepseek-ai/schemastery";
|
|
3
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
3
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
@@ -6,7 +5,8 @@ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readF
|
|
|
6
5
|
import { dirname, isAbsolute, join, win32 } from "node:path";
|
|
7
6
|
import { homedir } from "node:os";
|
|
8
7
|
import { isAbsolute as isAbsolute$1, join as join$1 } from "node:path/posix";
|
|
9
|
-
import {
|
|
8
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
9
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
10
|
//#region src/core/schedule.ts
|
|
11
11
|
/** Inclusive ranges per field, in cron order. */
|
|
12
12
|
const FIELD_RANGES = [
|
|
@@ -368,6 +368,7 @@ function createTask(input, now, id) {
|
|
|
368
368
|
title: input.title.trim(),
|
|
369
369
|
description: input.description.trim(),
|
|
370
370
|
prompt: input.prompt.trim(),
|
|
371
|
+
parentId: normalizeTargetId(input.parentId),
|
|
371
372
|
status: "todo",
|
|
372
373
|
createdAt: now,
|
|
373
374
|
updatedAt: now,
|
|
@@ -377,6 +378,7 @@ function createTask(input, now, id) {
|
|
|
377
378
|
permission: isTaskPermission(input.permission) ? input.permission : void 0,
|
|
378
379
|
model: normalizeTargetId(input.model),
|
|
379
380
|
reuseSession: input.reuseSession === true ? true : void 0,
|
|
381
|
+
teamRun: input.teamRun === true ? true : void 0,
|
|
380
382
|
...input.freeze === void 0 ? {} : { freeze: freezeOf(input.freeze, now) },
|
|
381
383
|
...input.handover === void 0 ? {} : { handover: {
|
|
382
384
|
...input.handover,
|
|
@@ -420,8 +422,10 @@ function withSchedule(task, patch, now) {
|
|
|
420
422
|
/**
|
|
421
423
|
* Open a fresh execution on a task: move it to 'running' and append a
|
|
422
424
|
* running execution record. Returns the new task and the new execution.
|
|
425
|
+
* @param runGroupId - cascade run group, when this run also opened executions
|
|
426
|
+
* for the task's subtasks (see `core/subtask.ts`).
|
|
423
427
|
*/
|
|
424
|
-
function startExecution(task, now, executionId, initiatedBy) {
|
|
428
|
+
function startExecution(task, now, executionId, initiatedBy, runGroupId) {
|
|
425
429
|
const execution = {
|
|
426
430
|
id: executionId,
|
|
427
431
|
sessionId: void 0,
|
|
@@ -429,6 +433,7 @@ function startExecution(task, now, executionId, initiatedBy) {
|
|
|
429
433
|
endedAt: void 0,
|
|
430
434
|
result: void 0,
|
|
431
435
|
error: void 0,
|
|
436
|
+
...runGroupId === void 0 || runGroupId === "" ? {} : { runGroupId },
|
|
432
437
|
...initiatedBy === void 0 || initiatedBy === "" ? {} : { initiatedBy },
|
|
433
438
|
...task.freeze === void 0 ? {} : {
|
|
434
439
|
frozenAt: task.freeze.frozenAt,
|
|
@@ -471,6 +476,234 @@ function settleExecution(task, executionId, outcome, now, error) {
|
|
|
471
476
|
executions
|
|
472
477
|
};
|
|
473
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Clamp a configured depth into the supported range. Anything unusable
|
|
481
|
+
* (absent, non-numeric, not finite) falls back to the default, so a bad
|
|
482
|
+
* configuration narrows the tree instead of lifting the guard.
|
|
483
|
+
*/
|
|
484
|
+
function normalizeSubtaskDepth(value) {
|
|
485
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 1;
|
|
486
|
+
const depth = Math.trunc(value);
|
|
487
|
+
if (depth < 1) return 1;
|
|
488
|
+
if (depth > 3) return 3;
|
|
489
|
+
return depth;
|
|
490
|
+
}
|
|
491
|
+
/** Whether an unknown value is a settled execution outcome. */
|
|
492
|
+
function isExecutionOutcome(value) {
|
|
493
|
+
return value === "succeeded" || value === "failed" || value === "cancelled";
|
|
494
|
+
}
|
|
495
|
+
function indexOf(tasks) {
|
|
496
|
+
return new Map(tasks.map((task) => [task.id, task]));
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Nearest-first ancestor chain of a task. The walk is cycle-safe and bounded
|
|
500
|
+
* by the ledger size rather than by the deployment maximum: a chain stored
|
|
501
|
+
* under a larger limit survives that limit being lowered, and restore and
|
|
502
|
+
* inheritance must still see all of it.
|
|
503
|
+
*/
|
|
504
|
+
function ancestorChain(tasks, task) {
|
|
505
|
+
const index = indexOf(tasks);
|
|
506
|
+
const chain = [];
|
|
507
|
+
const seen = /* @__PURE__ */ new Set([task.id]);
|
|
508
|
+
let current = task.parentId === void 0 ? void 0 : index.get(task.parentId);
|
|
509
|
+
while (current !== void 0 && chain.length < tasks.length) {
|
|
510
|
+
if (seen.has(current.id)) break;
|
|
511
|
+
seen.add(current.id);
|
|
512
|
+
chain.push(current);
|
|
513
|
+
current = current.parentId === void 0 ? void 0 : index.get(current.parentId);
|
|
514
|
+
}
|
|
515
|
+
return chain;
|
|
516
|
+
}
|
|
517
|
+
/** Root-to-task depth: a root task is 0, its subtask 1, and so on. */
|
|
518
|
+
function taskDepth(tasks, id) {
|
|
519
|
+
const task = indexOf(tasks).get(id);
|
|
520
|
+
return task === void 0 ? 0 : ancestorChain(tasks, task).length;
|
|
521
|
+
}
|
|
522
|
+
/** Direct subtasks of a task, in ledger order. */
|
|
523
|
+
function directSubtasks(tasks, id) {
|
|
524
|
+
return tasks.filter((task) => task.parentId === id);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Descendants of a task in breadth-first order, bounded by maxSubtaskDepth
|
|
528
|
+
* total depth. The visited set keeps a malformed ledger from looping.
|
|
529
|
+
*/
|
|
530
|
+
function descendantTasks(tasks, id, maxSubtaskDepth) {
|
|
531
|
+
const visited = /* @__PURE__ */ new Set([id]);
|
|
532
|
+
const found = [];
|
|
533
|
+
let frontier = [id];
|
|
534
|
+
for (let depth = 1; depth <= maxSubtaskDepth && frontier.length > 0; depth += 1) {
|
|
535
|
+
const next = [];
|
|
536
|
+
for (const parentId of frontier) for (const task of tasks) {
|
|
537
|
+
if (task.parentId !== parentId || visited.has(task.id)) continue;
|
|
538
|
+
visited.add(task.id);
|
|
539
|
+
found.push(task);
|
|
540
|
+
next.push(task.id);
|
|
541
|
+
}
|
|
542
|
+
frontier = next;
|
|
543
|
+
}
|
|
544
|
+
return found;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Height of a task's own subtree (0 for a leaf), bounded by the hard maximum.
|
|
548
|
+
* The visited set makes the walk terminate on a cyclic hand-edited ledger
|
|
549
|
+
* instead of recursing until the stack overflows.
|
|
550
|
+
*/
|
|
551
|
+
function subtreeHeight(tasks, id, visited = /* @__PURE__ */ new Set()) {
|
|
552
|
+
if (visited.has(id)) return 0;
|
|
553
|
+
const seen = /* @__PURE__ */ new Set([...visited, id]);
|
|
554
|
+
const children = directSubtasks(tasks, id);
|
|
555
|
+
if (children.length === 0) return 0;
|
|
556
|
+
let height = 0;
|
|
557
|
+
for (const child of children) height = Math.max(height, 1 + subtreeHeight(tasks, child.id, seen));
|
|
558
|
+
return Math.min(height, 3);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Whether childId may be attached under parentId. A null parent detaches and
|
|
562
|
+
* is always allowed for a known task. Otherwise the link must name an
|
|
563
|
+
* on-board task that is neither the child itself nor one of its descendants,
|
|
564
|
+
* and the resulting depth (parent depth + 1 + the child own subtree height)
|
|
565
|
+
* must stay within maxSubtaskDepth.
|
|
566
|
+
*/
|
|
567
|
+
function checkParentLink(tasks, childId, parentId, maxSubtaskDepth) {
|
|
568
|
+
if (tasks.find((task) => task.id === childId) === void 0) return {
|
|
569
|
+
ok: false,
|
|
570
|
+
reason: "unknown-task"
|
|
571
|
+
};
|
|
572
|
+
if (parentId === null || parentId === "") return { ok: true };
|
|
573
|
+
if (parentId === childId) return {
|
|
574
|
+
ok: false,
|
|
575
|
+
reason: "self-parent"
|
|
576
|
+
};
|
|
577
|
+
const parent = tasks.find((task) => task.id === parentId);
|
|
578
|
+
if (parent === void 0) return {
|
|
579
|
+
ok: false,
|
|
580
|
+
reason: "unknown-parent"
|
|
581
|
+
};
|
|
582
|
+
if (parent.archivedAt !== void 0) return {
|
|
583
|
+
ok: false,
|
|
584
|
+
reason: "archived-parent"
|
|
585
|
+
};
|
|
586
|
+
if (descendantTasks(tasks, childId, 3).some((task) => task.id === parentId)) return {
|
|
587
|
+
ok: false,
|
|
588
|
+
reason: "cycle"
|
|
589
|
+
};
|
|
590
|
+
if (taskDepth(tasks, parentId) + 1 + subtreeHeight(tasks, childId) > maxSubtaskDepth) return {
|
|
591
|
+
ok: false,
|
|
592
|
+
reason: "depth-exceeded"
|
|
593
|
+
};
|
|
594
|
+
return { ok: true };
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* The tasks one run request opens executions for: the requested task first,
|
|
598
|
+
* then its on-board descendants within the configured depth. Archived tasks
|
|
599
|
+
* never run; a participant that already has an open execution is skipped by
|
|
600
|
+
* the caller, which cannot start it twice.
|
|
601
|
+
*/
|
|
602
|
+
function cascadeTargets(tasks, rootId, maxSubtaskDepth) {
|
|
603
|
+
const root = tasks.find((task) => task.id === rootId);
|
|
604
|
+
if (root === void 0) return [];
|
|
605
|
+
return [root, ...descendantTasks(tasks, rootId, maxSubtaskDepth)];
|
|
606
|
+
}
|
|
607
|
+
/** The open execution of a task inside a run group, if it still exists. */
|
|
608
|
+
function openGroupExecution(task, runGroupId) {
|
|
609
|
+
return task.executions.find((execution) => execution.runGroupId === runGroupId && execution.endedAt === void 0);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* The direct subtasks of parentTaskId whose execution in this run group is
|
|
613
|
+
* still open: the parent remaining waiters. The parent may settle once this
|
|
614
|
+
* list is empty and its own outcome is known.
|
|
615
|
+
*/
|
|
616
|
+
function pendingCascadeChildren(tasks, parentTaskId, runGroupId) {
|
|
617
|
+
const pending = [];
|
|
618
|
+
for (const task of tasks) {
|
|
619
|
+
if (task.parentId !== parentTaskId) continue;
|
|
620
|
+
const execution = openGroupExecution(task, runGroupId);
|
|
621
|
+
if (execution !== void 0) pending.push({
|
|
622
|
+
taskId: task.id,
|
|
623
|
+
execution
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
return pending;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Fold a cascade parent own outcome and its children outcomes into one
|
|
630
|
+
* verdict: failure dominates, then cancellation, then success. The error text
|
|
631
|
+
* is the first failure, falling back to the first cancellation.
|
|
632
|
+
*/
|
|
633
|
+
function combineCascadeOutcome(entries) {
|
|
634
|
+
let result = "succeeded";
|
|
635
|
+
let error;
|
|
636
|
+
for (const entry of entries) {
|
|
637
|
+
if (entry.result === "failed") {
|
|
638
|
+
if (result !== "failed") error = void 0;
|
|
639
|
+
result = "failed";
|
|
640
|
+
if (error === void 0 && entry.error !== void 0 && entry.error !== "") error = entry.error;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (entry.result === "cancelled") {
|
|
644
|
+
if (result === "succeeded") result = "cancelled";
|
|
645
|
+
if (error === void 0 && entry.error !== void 0 && entry.error !== "") error = entry.error;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return error === void 0 ? {
|
|
649
|
+
result,
|
|
650
|
+
error: void 0
|
|
651
|
+
} : {
|
|
652
|
+
result,
|
|
653
|
+
error
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* The permission a task would actually run under: its handover bundle's pin
|
|
658
|
+
* wins over the plain pin field, exactly as the runner resolves it.
|
|
659
|
+
*/
|
|
660
|
+
function effectiveTaskPermission(task) {
|
|
661
|
+
return task.handover?.permission ?? task.permission;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Resolve the execution targets a (sub)task actually runs with: its own value
|
|
665
|
+
* wins, and an unset target inherits the nearest ancestor up to the root.
|
|
666
|
+
* Workspace, agent preset and model inherit the ancestor's PIN only — a
|
|
667
|
+
* handover bundle is a per-card continuation artefact and is never inherited.
|
|
668
|
+
* The permission inherits the ancestor's EFFECTIVE binding, and its human
|
|
669
|
+
* confirmation travels with it, so a subtask of an already-confirmed elevated
|
|
670
|
+
* card is runnable while a subtask's own unconfirmed binding stays gated.
|
|
671
|
+
*/
|
|
672
|
+
/**
|
|
673
|
+
* The immutable teammate name for one team-mode member. Teammate names are
|
|
674
|
+
* lower-kebab-case and must stay unique for the lifetime of the Team, so the
|
|
675
|
+
* run-group token is appended: two runs of the same tree never collide. A title
|
|
676
|
+
* with no ASCII word characters (a CJK title, for example) keeps a stable
|
|
677
|
+
* generic prefix and is still distinguished by the token.
|
|
678
|
+
* @param title - the member task's title.
|
|
679
|
+
* @param token - the run group id (the task id when no group is recorded).
|
|
680
|
+
* @returns the teammate name handed to the Agent Teams service.
|
|
681
|
+
*/
|
|
682
|
+
function teammateName(title, token) {
|
|
683
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32).replace(/-+$/g, "");
|
|
684
|
+
const suffix = token.replace(/[^a-z0-9]/gi, "").slice(0, 8).toLowerCase();
|
|
685
|
+
return `${slug === "" ? "subtask" : slug}-${suffix === "" ? "run" : suffix}`;
|
|
686
|
+
}
|
|
687
|
+
function resolveExecutionTargets(task, tasks, maxSubtaskDepth = 3) {
|
|
688
|
+
if (task.parentId === void 0) return task;
|
|
689
|
+
const chain = [task, ...ancestorChain(tasks, task).slice(0, maxSubtaskDepth)];
|
|
690
|
+
const first = (key) => {
|
|
691
|
+
for (const candidate of chain) {
|
|
692
|
+
const value = candidate[key];
|
|
693
|
+
if (value !== void 0) return value;
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
const own = effectiveTaskPermission(task);
|
|
697
|
+
const owner = own === void 0 ? chain.slice(1).find((candidate) => effectiveTaskPermission(candidate) !== void 0) : void 0;
|
|
698
|
+
return {
|
|
699
|
+
...task,
|
|
700
|
+
workspaceId: first("workspaceId"),
|
|
701
|
+
mode: first("mode"),
|
|
702
|
+
model: first("model"),
|
|
703
|
+
permission: own ?? (owner === void 0 ? void 0 : effectiveTaskPermission(owner)),
|
|
704
|
+
permissionConfirmedAt: own === void 0 ? owner?.permissionConfirmedAt : task.permissionConfirmedAt
|
|
705
|
+
};
|
|
706
|
+
}
|
|
474
707
|
/** Marker replacing every sensitive match. */
|
|
475
708
|
const REDACTED_MARKER = "[REDACTED]";
|
|
476
709
|
const SECTION_ORDER$1 = [
|
|
@@ -672,6 +905,7 @@ function isTaskRecordShape(value) {
|
|
|
672
905
|
if (typeof record.prompt !== "string") return false;
|
|
673
906
|
if (typeof record.createdAt !== "number") return false;
|
|
674
907
|
if (typeof record.updatedAt !== "number") return false;
|
|
908
|
+
if (record.parentId !== void 0 && typeof record.parentId !== "string") return false;
|
|
675
909
|
if (record.workspaceId !== void 0 && typeof record.workspaceId !== "string") return false;
|
|
676
910
|
if (record.mode !== void 0 && typeof record.mode !== "string") return false;
|
|
677
911
|
if (record.permission !== void 0 && typeof record.permission !== "string") return false;
|
|
@@ -785,6 +1019,13 @@ function parseLedger(raw) {
|
|
|
785
1019
|
status: normalizeStatus(row.status)
|
|
786
1020
|
};
|
|
787
1021
|
task.schedule = normalizeSchedule(row.schedule);
|
|
1022
|
+
task.parentId = normalizeTargetId(row.parentId);
|
|
1023
|
+
task.executions = task.executions.map((execution) => ({
|
|
1024
|
+
...execution,
|
|
1025
|
+
runGroupId: normalizeTargetId(execution.runGroupId),
|
|
1026
|
+
ownResult: isExecutionOutcome(execution.ownResult) ? execution.ownResult : void 0,
|
|
1027
|
+
ownError: typeof execution.ownError === "string" ? execution.ownError : void 0
|
|
1028
|
+
}));
|
|
788
1029
|
task.workspaceId = normalizeTargetId(row.workspaceId);
|
|
789
1030
|
task.mode = normalizeTargetId(row.mode);
|
|
790
1031
|
task.archivedAt = typeof row.archivedAt === "number" && Number.isFinite(row.archivedAt) ? row.archivedAt : void 0;
|
|
@@ -801,17 +1042,46 @@ function parseLedger(raw) {
|
|
|
801
1042
|
//#endregion
|
|
802
1043
|
//#region src/core/use-cases/task-archive.ts
|
|
803
1044
|
/**
|
|
804
|
-
* Archive
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
1045
|
+
* Archive/restore task use case: move a task off the main board and back,
|
|
1046
|
+
* whatever its status but running. The task keeps its status, execution
|
|
1047
|
+
* history, and transcript references, while archiving disarms any schedule so
|
|
1048
|
+
* it cannot create more execution records until the user restores and
|
|
1049
|
+
* re-enables it.
|
|
1050
|
+
*
|
|
1051
|
+
* Subtask trees move as a unit: archiving takes the whole subtree (an on-board
|
|
1052
|
+
* subtask under an archived parent would be unreachable from its parent) and
|
|
1053
|
+
* refuses the whole group while any member is running. Restoring brings the
|
|
1054
|
+
* task, its ancestors and its subtree back together.
|
|
1055
|
+
*/
|
|
1056
|
+
/** The archive/restore id set: the task, its subtree, and (for restore) its ancestors. */
|
|
1057
|
+
function groupIds(tasks, id, maxSubtaskDepth, withAncestors) {
|
|
1058
|
+
const target = tasks.find((task) => task.id === id);
|
|
1059
|
+
const ids = /* @__PURE__ */ new Set([id]);
|
|
1060
|
+
for (const task of descendantTasks(tasks, id, normalizeSubtaskDepth(maxSubtaskDepth))) ids.add(task.id);
|
|
1061
|
+
if (withAncestors && target !== void 0) for (const ancestor of ancestorChain(tasks, target)) ids.add(ancestor.id);
|
|
1062
|
+
return ids;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Archive one task and its subtasks: only a running member keeps the group on
|
|
1066
|
+
* the board (its runner still owns its lifecycle until the execution settles);
|
|
1067
|
+
* any other status can be archived. Archiving disarms schedules; an
|
|
1068
|
+
* already-archived task is a no-op.
|
|
808
1069
|
*/
|
|
809
|
-
function applyArchiveTask(tasks, id, now) {
|
|
1070
|
+
function applyArchiveTask(tasks, id, now, maxSubtaskDepth = 1) {
|
|
1071
|
+
const target = tasks.find((task) => task.id === id);
|
|
1072
|
+
if (target === void 0 || target.archivedAt !== void 0) return {
|
|
1073
|
+
tasks,
|
|
1074
|
+
archived: false
|
|
1075
|
+
};
|
|
1076
|
+
const ids = groupIds(tasks, id, maxSubtaskDepth, false);
|
|
1077
|
+
if (tasks.some((task) => ids.has(task.id) && !ARCHIVABLE_STATUSES.includes(task.status))) return {
|
|
1078
|
+
tasks,
|
|
1079
|
+
archived: false
|
|
1080
|
+
};
|
|
810
1081
|
let applied = false;
|
|
811
1082
|
return {
|
|
812
1083
|
tasks: tasks.map((task) => {
|
|
813
|
-
if (task.id
|
|
814
|
-
if (!ARCHIVABLE_STATUSES.includes(task.status)) return task;
|
|
1084
|
+
if (!ids.has(task.id) || task.archivedAt !== void 0) return task;
|
|
815
1085
|
applied = true;
|
|
816
1086
|
const schedule = task.schedule === void 0 ? void 0 : {
|
|
817
1087
|
...task.schedule,
|
|
@@ -828,12 +1098,21 @@ function applyArchiveTask(tasks, id, now) {
|
|
|
828
1098
|
archived: applied
|
|
829
1099
|
};
|
|
830
1100
|
}
|
|
831
|
-
/**
|
|
832
|
-
|
|
1101
|
+
/**
|
|
1102
|
+
* Restore one task back onto the main board, together with its ancestors (an
|
|
1103
|
+
* on-board subtask must not point at an archived parent) and its subtasks.
|
|
1104
|
+
*/
|
|
1105
|
+
function applyRestoreTask(tasks, id, now, maxSubtaskDepth = 1) {
|
|
1106
|
+
const target = tasks.find((task) => task.id === id);
|
|
1107
|
+
if (target === void 0 || target.archivedAt === void 0) return {
|
|
1108
|
+
tasks,
|
|
1109
|
+
archived: false
|
|
1110
|
+
};
|
|
1111
|
+
const ids = groupIds(tasks, id, maxSubtaskDepth, true);
|
|
833
1112
|
let applied = false;
|
|
834
1113
|
return {
|
|
835
1114
|
tasks: tasks.map((task) => {
|
|
836
|
-
if (task.id
|
|
1115
|
+
if (!ids.has(task.id) || task.archivedAt === void 0) return task;
|
|
837
1116
|
applied = true;
|
|
838
1117
|
const { archivedAt: _archived, ...rest } = task;
|
|
839
1118
|
return {
|
|
@@ -848,23 +1127,57 @@ function applyRestoreTask(tasks, id, now) {
|
|
|
848
1127
|
//#region src/core/use-cases/task-create.ts
|
|
849
1128
|
/**
|
|
850
1129
|
* Create-task use case: mint a new task from user input, rejecting a blank
|
|
851
|
-
* title
|
|
852
|
-
* orchestrates those), so it is
|
|
1130
|
+
* title and any parent link the lineage gate refuses. Pure ledger transition
|
|
1131
|
+
* (no persistence or notify - the controller orchestrates those), so it is
|
|
1132
|
+
* unit-testable without any runtime face.
|
|
853
1133
|
*/
|
|
854
1134
|
/**
|
|
855
1135
|
* Apply a create against the current ledger. Returns the new task and the
|
|
856
|
-
* appended ledger, or the unchanged ledger when the title is blank
|
|
1136
|
+
* appended ledger, or the unchanged ledger when the title is blank or the
|
|
1137
|
+
* requested parent link is refused.
|
|
1138
|
+
*
|
|
1139
|
+
* A subtask inherits the execution targets its parent already pins (workspace,
|
|
1140
|
+
* agent preset, permission, model) unless the form supplied its own; the
|
|
1141
|
+
* parent human permission confirmation travels with an inherited permission,
|
|
1142
|
+
* so a subtask of an already-confirmed elevated card is runnable at once.
|
|
857
1143
|
* @param tasks - current ledger.
|
|
858
|
-
* @param input - raw user input (title/description/prompt + optional schedule).
|
|
1144
|
+
* @param input - raw user input (title/description/prompt + optional schedule/parent).
|
|
859
1145
|
* @param now - clock instant (ms epoch).
|
|
860
1146
|
* @param id - minted task id.
|
|
1147
|
+
* @param maxSubtaskDepth - deployment subtask depth limit.
|
|
861
1148
|
*/
|
|
862
|
-
function applyCreateTask(tasks, input, now, id) {
|
|
1149
|
+
function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1) {
|
|
863
1150
|
if (input.title.trim() === "") return {
|
|
864
1151
|
task: void 0,
|
|
865
|
-
tasks
|
|
1152
|
+
tasks,
|
|
1153
|
+
error: "title is required"
|
|
866
1154
|
};
|
|
867
|
-
|
|
1155
|
+
const limit = normalizeSubtaskDepth(maxSubtaskDepth);
|
|
1156
|
+
let parent;
|
|
1157
|
+
if (input.parentId !== void 0) {
|
|
1158
|
+
parent = tasks.find((task) => task.id === input.parentId);
|
|
1159
|
+
if (parent === void 0) return {
|
|
1160
|
+
task: void 0,
|
|
1161
|
+
tasks,
|
|
1162
|
+
error: "parent task not found"
|
|
1163
|
+
};
|
|
1164
|
+
if (parent.archivedAt !== void 0) return {
|
|
1165
|
+
task: void 0,
|
|
1166
|
+
tasks,
|
|
1167
|
+
error: "parent task is archived"
|
|
1168
|
+
};
|
|
1169
|
+
if (taskDepth(tasks, parent.id) + 1 > limit) return {
|
|
1170
|
+
task: void 0,
|
|
1171
|
+
tasks,
|
|
1172
|
+
error: `subtask depth limit (${limit}) exceeded`
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
let task = createTask(parent === void 0 ? input : {
|
|
1176
|
+
...input,
|
|
1177
|
+
workspaceId: input.workspaceId ?? parent.workspaceId,
|
|
1178
|
+
mode: input.mode ?? parent.mode,
|
|
1179
|
+
model: input.model ?? parent.model
|
|
1180
|
+
}, now, id);
|
|
868
1181
|
const requested = input.schedule;
|
|
869
1182
|
if (requested?.enabled === true && requested.cron.trim() !== "" && isValidCron(requested.cron)) {
|
|
870
1183
|
const cron = requested.cron.trim();
|
|
@@ -889,9 +1202,15 @@ function applyCreateTask(tasks, input, now, id) {
|
|
|
889
1202
|
* @param id - the task to remove.
|
|
890
1203
|
*/
|
|
891
1204
|
function applyDeleteTask(tasks, selectedTaskId, id) {
|
|
1205
|
+
if (tasks.some((task) => task.parentId === id)) return {
|
|
1206
|
+
tasks,
|
|
1207
|
+
selectionCleared: false,
|
|
1208
|
+
blocked: true
|
|
1209
|
+
};
|
|
892
1210
|
return {
|
|
893
1211
|
tasks: tasks.filter((task) => task.id !== id),
|
|
894
|
-
selectionCleared: selectedTaskId === id
|
|
1212
|
+
selectionCleared: selectedTaskId === id,
|
|
1213
|
+
blocked: false
|
|
895
1214
|
};
|
|
896
1215
|
}
|
|
897
1216
|
//#endregion
|
|
@@ -955,6 +1274,70 @@ function applyScheduleNextRun(tasks, id, nextRunAt, lastTriggeredAt, now) {
|
|
|
955
1274
|
}, now) : task);
|
|
956
1275
|
}
|
|
957
1276
|
//#endregion
|
|
1277
|
+
//#region src/core/use-cases/task-parent.ts
|
|
1278
|
+
/**
|
|
1279
|
+
* Parent-link use case: attach an existing on-board task under another as a
|
|
1280
|
+
* subtask, or detach it back to the root. The lineage gate is the same one
|
|
1281
|
+
* creation uses (unknown or archived parent, self, cycle, depth), so the two
|
|
1282
|
+
* ways into the tree cannot disagree. Pure ledger transition.
|
|
1283
|
+
*/
|
|
1284
|
+
/** Host error text per refusal reason (the wire carries the same strings). */
|
|
1285
|
+
const REJECTION_TEXT = {
|
|
1286
|
+
"unknown-task": "task not found",
|
|
1287
|
+
"unknown-parent": "parent task not found",
|
|
1288
|
+
"archived-parent": "parent task is archived",
|
|
1289
|
+
"self-parent": "a task cannot be its own parent",
|
|
1290
|
+
"cycle": "a task cannot be attached under its own subtask",
|
|
1291
|
+
"depth-exceeded": "subtask depth limit exceeded"
|
|
1292
|
+
};
|
|
1293
|
+
/**
|
|
1294
|
+
* Attach or detach one task. A null (or blank) parent detaches, which is always
|
|
1295
|
+
* allowed for a known on-board task; attaching validates the whole chain so a
|
|
1296
|
+
* cyclic or too-deep ledger can never be stored.
|
|
1297
|
+
* @param tasks - current ledger.
|
|
1298
|
+
* @param id - the task being moved.
|
|
1299
|
+
* @param parentId - the new parent, or null to detach.
|
|
1300
|
+
* @param now - clock instant (ms epoch).
|
|
1301
|
+
* @param maxSubtaskDepth - deployment subtask depth limit.
|
|
1302
|
+
*/
|
|
1303
|
+
function applySetParent(tasks, id, parentId, now, maxSubtaskDepth = 1) {
|
|
1304
|
+
const task = tasks.find((candidate) => candidate.id === id);
|
|
1305
|
+
if (task === void 0) return {
|
|
1306
|
+
tasks,
|
|
1307
|
+
applied: false,
|
|
1308
|
+
error: REJECTION_TEXT["unknown-task"]
|
|
1309
|
+
};
|
|
1310
|
+
if (task.archivedAt !== void 0) return {
|
|
1311
|
+
tasks,
|
|
1312
|
+
applied: false,
|
|
1313
|
+
error: "archived task is read-only"
|
|
1314
|
+
};
|
|
1315
|
+
if (task.status === "running" || task.executions.some((execution) => execution.endedAt === void 0)) return {
|
|
1316
|
+
tasks,
|
|
1317
|
+
applied: false,
|
|
1318
|
+
error: "running task cannot be re-parented"
|
|
1319
|
+
};
|
|
1320
|
+
const check = checkParentLink(tasks, id, parentId, normalizeSubtaskDepth(maxSubtaskDepth));
|
|
1321
|
+
if (!check.ok) return {
|
|
1322
|
+
tasks,
|
|
1323
|
+
applied: false,
|
|
1324
|
+
error: REJECTION_TEXT[check.reason ?? "unknown-task"]
|
|
1325
|
+
};
|
|
1326
|
+
const nextParentId = parentId === null || parentId === "" ? void 0 : parentId;
|
|
1327
|
+
if (nextParentId === task.parentId) return {
|
|
1328
|
+
tasks: [...tasks],
|
|
1329
|
+
applied: true
|
|
1330
|
+
};
|
|
1331
|
+
return {
|
|
1332
|
+
tasks: tasks.map((candidate) => candidate.id === id ? {
|
|
1333
|
+
...candidate,
|
|
1334
|
+
parentId: nextParentId,
|
|
1335
|
+
updatedAt: now
|
|
1336
|
+
} : candidate),
|
|
1337
|
+
applied: true
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
//#endregion
|
|
958
1341
|
//#region src/core/use-cases/task-update.ts
|
|
959
1342
|
/**
|
|
960
1343
|
* Update-task use case: apply an editable-field patch (title/description/
|
|
@@ -1027,6 +1410,7 @@ function applyUpdateTask(tasks, id, patch, now) {
|
|
|
1027
1410
|
if ("tags" in patch) next.tags = tagsPatch == null ? void 0 : normalizeTags(tagsPatch);
|
|
1028
1411
|
if ("permission" in patch && patch.permission !== void 0 && patch.permission !== task.permission || "handover" in patch) next.permissionConfirmedAt = void 0;
|
|
1029
1412
|
if ("reuseSession" in patch) next.reuseSession = patch.reuseSession === true ? true : void 0;
|
|
1413
|
+
if ("teamRun" in patch) next.teamRun = patch.teamRun === true ? true : void 0;
|
|
1030
1414
|
if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
|
|
1031
1415
|
if (mode !== void 0 || "mode" in patch) next.mode = mode;
|
|
1032
1416
|
if (permission !== void 0 || "permission" in patch) next.permission = permission;
|
|
@@ -1130,6 +1514,7 @@ function importedTask(value) {
|
|
|
1130
1514
|
nextRunAt: task.schedule.nextRunAt,
|
|
1131
1515
|
lastTriggeredAt: task.schedule.lastTriggeredAt
|
|
1132
1516
|
} },
|
|
1517
|
+
...task.parentId === void 0 ? {} : { parentId: task.parentId },
|
|
1133
1518
|
...task.workspaceId === void 0 ? {} : { workspaceId: task.workspaceId },
|
|
1134
1519
|
...task.mode === void 0 ? {} : { mode: task.mode },
|
|
1135
1520
|
...task.permission === void 0 ? {} : { permission: task.permission },
|
|
@@ -1170,6 +1555,7 @@ function createInput(value) {
|
|
|
1170
1555
|
"title",
|
|
1171
1556
|
"description",
|
|
1172
1557
|
"prompt",
|
|
1558
|
+
"parentId",
|
|
1173
1559
|
"workspaceId",
|
|
1174
1560
|
"mode",
|
|
1175
1561
|
"permission",
|
|
@@ -1178,11 +1564,14 @@ function createInput(value) {
|
|
|
1178
1564
|
"handover",
|
|
1179
1565
|
"model",
|
|
1180
1566
|
"reuseSession",
|
|
1567
|
+
"teamRun",
|
|
1181
1568
|
"tags"
|
|
1182
1569
|
])) return false;
|
|
1570
|
+
if (input.parentId !== void 0 && (typeof input.parentId !== "string" || input.parentId.trim() === "")) return false;
|
|
1183
1571
|
if (typeof input.title !== "string" || typeof input.description !== "string" || typeof input.prompt !== "string") return false;
|
|
1184
1572
|
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false;
|
|
1185
1573
|
if (input.reuseSession !== void 0 && typeof input.reuseSession !== "boolean") return false;
|
|
1574
|
+
if (input.teamRun !== void 0 && typeof input.teamRun !== "boolean") return false;
|
|
1186
1575
|
if (input.permission !== void 0 && !isTaskPermission(input.permission)) return false;
|
|
1187
1576
|
if (input.tags !== void 0 && !isTaskTagList(input.tags)) return false;
|
|
1188
1577
|
if (input.freeze !== void 0 && freezePayload(input.freeze) === void 0) return false;
|
|
@@ -1207,9 +1596,11 @@ function updatePatch(value) {
|
|
|
1207
1596
|
"handover",
|
|
1208
1597
|
"model",
|
|
1209
1598
|
"reuseSession",
|
|
1599
|
+
"teamRun",
|
|
1210
1600
|
"tags"
|
|
1211
1601
|
])) return false;
|
|
1212
1602
|
if (patch.reuseSession !== void 0 && patch.reuseSession !== null && typeof patch.reuseSession !== "boolean") return false;
|
|
1603
|
+
if (patch.teamRun !== void 0 && patch.teamRun !== null && typeof patch.teamRun !== "boolean") return false;
|
|
1213
1604
|
for (const key of [
|
|
1214
1605
|
"title",
|
|
1215
1606
|
"description",
|
|
@@ -1319,6 +1710,24 @@ function parseEnvelopeAction(value) {
|
|
|
1319
1710
|
}
|
|
1320
1711
|
};
|
|
1321
1712
|
}
|
|
1713
|
+
case "set-parent": {
|
|
1714
|
+
if (!exactKeys(action, [
|
|
1715
|
+
"kind",
|
|
1716
|
+
"taskId",
|
|
1717
|
+
"parentId"
|
|
1718
|
+
])) return void 0;
|
|
1719
|
+
if (taskId === void 0) return void 0;
|
|
1720
|
+
const parentId = action.parentId;
|
|
1721
|
+
if (parentId !== null && (typeof parentId !== "string" || parentId.trim() === "")) return void 0;
|
|
1722
|
+
return {
|
|
1723
|
+
requestId: envelope.requestId,
|
|
1724
|
+
action: {
|
|
1725
|
+
kind: "set-parent",
|
|
1726
|
+
taskId,
|
|
1727
|
+
parentId
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1322
1731
|
case "set-schedule":
|
|
1323
1732
|
if (!exactKeys(action, [
|
|
1324
1733
|
"kind",
|
|
@@ -1355,6 +1764,13 @@ function parseEnvelopeAction(value) {
|
|
|
1355
1764
|
}
|
|
1356
1765
|
//#endregion
|
|
1357
1766
|
//#region src/host-ledger.ts
|
|
1767
|
+
/** Human-facing text for a refused binding, per surface. */
|
|
1768
|
+
function bindingRefusalMessage(refusal, sessionDefault, surface) {
|
|
1769
|
+
if (refusal.kind === "subtask-pin") return `team run cannot honor the permission binding of subtask "${refusal.title}": a teammate runs inside the Lead session; clear that card's permission or run the tree without Agent Team`;
|
|
1770
|
+
if (surface === "schedule") return `task "${refusal.title}" has an unconfirmed above-default permission`;
|
|
1771
|
+
if (refusal.kind === "root") return `confirmation-required: the effective permission is above the session default (${sessionDefault}); confirm the card's permission binding first`;
|
|
1772
|
+
return `confirmation-required: subtask "${refusal.title}" inherits an above-default permission; confirm that card's permission binding first`;
|
|
1773
|
+
}
|
|
1358
1774
|
const MAX_REQUEST_CACHE = 256;
|
|
1359
1775
|
function timeZone() {
|
|
1360
1776
|
return Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
|
|
@@ -1580,6 +1996,37 @@ function mergeTask(a, b) {
|
|
|
1580
1996
|
executions: retainRecentExecutions(executions)
|
|
1581
1997
|
};
|
|
1582
1998
|
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Repair parent links after a load or an import: a link whose parent row is
|
|
2001
|
+
* missing, cyclic, or names an archived parent while the child is on board is
|
|
2002
|
+
* dropped (the task becomes a root) instead of dropping the task. A chain
|
|
2003
|
+
* deeper than the current `maxSubtaskDepth` is deliberately KEPT: the limit
|
|
2004
|
+
* gates new links, it never deletes a stored one, and every cascade walk is
|
|
2005
|
+
* bounded by the limit anyway. The Host is the only writer of these links, so
|
|
2006
|
+
* this is a last-resort guard against a hand-edited or imported document.
|
|
2007
|
+
*/
|
|
2008
|
+
function repairParentLinks(tasks) {
|
|
2009
|
+
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
2010
|
+
return tasks.map((task) => {
|
|
2011
|
+
if (task.parentId === void 0) return task;
|
|
2012
|
+
const seen = /* @__PURE__ */ new Set([task.id]);
|
|
2013
|
+
let current = byId.get(task.parentId);
|
|
2014
|
+
let depth = 0;
|
|
2015
|
+
while (current !== void 0) {
|
|
2016
|
+
if (seen.has(current.id) || depth === 0 && current.archivedAt !== void 0 && task.archivedAt === void 0) return {
|
|
2017
|
+
...task,
|
|
2018
|
+
parentId: void 0
|
|
2019
|
+
};
|
|
2020
|
+
seen.add(current.id);
|
|
2021
|
+
depth += 1;
|
|
2022
|
+
current = current.parentId === void 0 ? void 0 : byId.get(current.parentId);
|
|
2023
|
+
}
|
|
2024
|
+
return depth === 0 ? {
|
|
2025
|
+
...task,
|
|
2026
|
+
parentId: void 0
|
|
2027
|
+
} : task;
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
1583
2030
|
function parseHostTasks(values) {
|
|
1584
2031
|
const rawById = /* @__PURE__ */ new Map();
|
|
1585
2032
|
for (const value of values) {
|
|
@@ -1587,7 +2034,7 @@ function parseHostTasks(values) {
|
|
|
1587
2034
|
const raw = value;
|
|
1588
2035
|
if (typeof raw.id === "string") rawById.set(raw.id, raw);
|
|
1589
2036
|
}
|
|
1590
|
-
return parseLedger(JSON.stringify(values)).map((task) => {
|
|
2037
|
+
return repairParentLinks(parseLedger(JSON.stringify(values))).map((task) => {
|
|
1591
2038
|
const rawSchedule = rawById.get(task.id)?.schedule;
|
|
1592
2039
|
if (typeof rawSchedule !== "object" || rawSchedule === null) return task;
|
|
1593
2040
|
const schedule = rawSchedule;
|
|
@@ -1616,9 +2063,16 @@ var HostTaskLedger = class {
|
|
|
1616
2063
|
schedulerFile;
|
|
1617
2064
|
/** Session-default permission the confirmation gate compares against. */
|
|
1618
2065
|
sessionDefaultPermission;
|
|
2066
|
+
/**
|
|
2067
|
+
* Deployment subtask depth limit (1..3): the lineage gate every write obeys.
|
|
2068
|
+
* The settings card edits it live, so {@link setMaxSubtaskDepth} mutates it
|
|
2069
|
+
* instead of remounting the row.
|
|
2070
|
+
*/
|
|
2071
|
+
depthLimit;
|
|
1619
2072
|
constructor(dir = join(dshHome(), "task-board"), now = Date.now, options = {}) {
|
|
1620
2073
|
this.now = now;
|
|
1621
2074
|
this.sessionDefaultPermission = options.sessionDefaultPermission ?? "read-only";
|
|
2075
|
+
this.depthLimit = normalizeSubtaskDepth(options.maxSubtaskDepth ?? 1);
|
|
1622
2076
|
mkdirSync(dir, { recursive: true });
|
|
1623
2077
|
this.file = join(dir, "ledger-v2.json");
|
|
1624
2078
|
this.lockFile = join(dir, "ledger-v2.lock");
|
|
@@ -1636,6 +2090,21 @@ var HostTaskLedger = class {
|
|
|
1636
2090
|
throw error;
|
|
1637
2091
|
}
|
|
1638
2092
|
}
|
|
2093
|
+
/** Current subtask depth limit (1..3). */
|
|
2094
|
+
get maxSubtaskDepth() {
|
|
2095
|
+
return this.depthLimit;
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Apply a live settings edit of the subtask depth limit. A no-op when the
|
|
2099
|
+
* normalized limit is unchanged, so a coarse volatile invalidation that
|
|
2100
|
+
* changed nothing emits nothing.
|
|
2101
|
+
*/
|
|
2102
|
+
setMaxSubtaskDepth(depth) {
|
|
2103
|
+
const next = normalizeSubtaskDepth(depth);
|
|
2104
|
+
if (next === this.depthLimit) return;
|
|
2105
|
+
this.depthLimit = next;
|
|
2106
|
+
this.notify();
|
|
2107
|
+
}
|
|
1639
2108
|
/** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
|
|
1640
2109
|
cleanStaleTemporaryFiles(dir) {
|
|
1641
2110
|
try {
|
|
@@ -1673,6 +2142,7 @@ var HostTaskLedger = class {
|
|
|
1673
2142
|
if (task.archivedAt === void 0 && task.schedule?.enabled === true) armedSchedules += 1;
|
|
1674
2143
|
for (const execution of task.executions) {
|
|
1675
2144
|
if (execution.endedAt !== void 0) continue;
|
|
2145
|
+
if (execution.ownResult !== void 0) continue;
|
|
1676
2146
|
openExecutions.push({
|
|
1677
2147
|
taskId: task.id,
|
|
1678
2148
|
executionId: execution.id,
|
|
@@ -1692,6 +2162,25 @@ var HostTaskLedger = class {
|
|
|
1692
2162
|
for (const task of this.document.tasks) if (task.archivedAt === void 0 && task.schedule?.enabled === true) count += 1;
|
|
1693
2163
|
return count;
|
|
1694
2164
|
}
|
|
2165
|
+
/**
|
|
2166
|
+
* The earliest armed `nextRunAt` strictly after the supplied Host time, or
|
|
2167
|
+
* undefined when no schedule is armed in the future. This is the instant the
|
|
2168
|
+
* Host arms its native timer at, so the board wakes exactly when work is due
|
|
2169
|
+
* instead of polling a fixed heartbeat.
|
|
2170
|
+
* @param now - current Host time in ms epoch.
|
|
2171
|
+
* @returns the nearest future trigger, or undefined when none is armed.
|
|
2172
|
+
*/
|
|
2173
|
+
nextArmedRunAt(now) {
|
|
2174
|
+
let nearest;
|
|
2175
|
+
for (const task of this.document.tasks) {
|
|
2176
|
+
if (task.archivedAt !== void 0) continue;
|
|
2177
|
+
const schedule = task.schedule;
|
|
2178
|
+
if (schedule === void 0 || !schedule.enabled || schedule.nextRunAt === void 0) continue;
|
|
2179
|
+
if (schedule.nextRunAt <= now) continue;
|
|
2180
|
+
if (nearest === void 0 || schedule.nextRunAt < nearest) nearest = schedule.nextRunAt;
|
|
2181
|
+
}
|
|
2182
|
+
return nearest;
|
|
2183
|
+
}
|
|
1695
2184
|
/** Return value-only references for schedules due at the supplied Host time. */
|
|
1696
2185
|
dueSchedules(now) {
|
|
1697
2186
|
const due = [];
|
|
@@ -1740,24 +2229,29 @@ var HostTaskLedger = class {
|
|
|
1740
2229
|
throw error;
|
|
1741
2230
|
}
|
|
1742
2231
|
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Open the cascade one due schedule triggers: an empty array means nothing
|
|
2234
|
+
* ran (already running, or a participant whose elevated permission is still
|
|
2235
|
+
* unconfirmed), and the rule rolls to its next occurrence either way.
|
|
2236
|
+
*/
|
|
1743
2237
|
openScheduled(taskId, nextRunAt, triggeredAt) {
|
|
1744
2238
|
const task = this.document.tasks.find((item) => item.id === taskId);
|
|
1745
|
-
if (task === void 0 || task.archivedAt !== void 0) return
|
|
1746
|
-
|
|
2239
|
+
if (task === void 0 || task.archivedAt !== void 0) return [];
|
|
2240
|
+
const rollForward = () => {
|
|
1747
2241
|
this.document.tasks = [...applyScheduleNextRun(this.document.tasks, taskId, nextRunAt, task.schedule?.lastTriggeredAt, triggeredAt)];
|
|
1748
2242
|
this.commit();
|
|
1749
|
-
|
|
2243
|
+
};
|
|
2244
|
+
const refusal = this.bindingRefusal(task);
|
|
2245
|
+
if (refusal !== void 0) {
|
|
2246
|
+
this.document.scheduler.error = `scheduled run refused for task ${taskId}: ${bindingRefusalMessage(refusal, this.sessionDefaultPermission, "schedule")}`;
|
|
2247
|
+
rollForward();
|
|
2248
|
+
return [];
|
|
1750
2249
|
}
|
|
1751
2250
|
if (task.status === "running" || hasOpenExecution(task)) {
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
return;
|
|
2251
|
+
rollForward();
|
|
2252
|
+
return [];
|
|
1755
2253
|
}
|
|
1756
|
-
|
|
1757
|
-
this.document.tasks = this.document.tasks.map((item) => item.id === taskId ? opened.task : item);
|
|
1758
|
-
this.document.tasks = [...applyScheduleNextRun(this.document.tasks, taskId, nextRunAt, triggeredAt, triggeredAt)];
|
|
1759
|
-
this.commit();
|
|
1760
|
-
return opened;
|
|
2254
|
+
return this.startCascade(task, triggeredAt, void 0, false, nextRunAt);
|
|
1761
2255
|
}
|
|
1762
2256
|
skipMissed(now) {
|
|
1763
2257
|
let changed = false;
|
|
@@ -1792,6 +2286,39 @@ var HostTaskLedger = class {
|
|
|
1792
2286
|
}
|
|
1793
2287
|
this.commit(false);
|
|
1794
2288
|
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Settle one execution. A cascade participant first records its OWN outcome
|
|
2291
|
+
* and only finalizes once every child execution in its run group has
|
|
2292
|
+
* settled, so a parent card leaves 'running' with the whole tree's verdict
|
|
2293
|
+
* rather than its own turn alone.
|
|
2294
|
+
*/
|
|
2295
|
+
settle(taskId, executionId, outcome, error) {
|
|
2296
|
+
const now = this.now();
|
|
2297
|
+
const task = this.document.tasks.find((item) => item.id === taskId);
|
|
2298
|
+
const execution = task?.executions.find((entry) => entry.id === executionId);
|
|
2299
|
+
if (task === void 0 || execution === void 0 || execution.endedAt !== void 0) return;
|
|
2300
|
+
const groupId = execution.runGroupId;
|
|
2301
|
+
if (groupId === void 0) {
|
|
2302
|
+
this.document.tasks = this.document.tasks.map((item) => item.id === taskId ? settleExecution(item, executionId, outcome, now, error) : item);
|
|
2303
|
+
this.commit();
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
let changed = false;
|
|
2307
|
+
if (execution.ownResult === void 0) {
|
|
2308
|
+
this.document.tasks = this.document.tasks.map((item) => item.id !== taskId ? item : {
|
|
2309
|
+
...item,
|
|
2310
|
+
updatedAt: now,
|
|
2311
|
+
executions: item.executions.map((entry) => entry.id === executionId ? {
|
|
2312
|
+
...entry,
|
|
2313
|
+
ownResult: outcome,
|
|
2314
|
+
ownError: error
|
|
2315
|
+
} : entry)
|
|
2316
|
+
});
|
|
2317
|
+
changed = true;
|
|
2318
|
+
}
|
|
2319
|
+
if (this.settleCascade(taskId, groupId, now)) changed = true;
|
|
2320
|
+
if (changed) this.commit();
|
|
2321
|
+
}
|
|
1795
2322
|
attachSession(taskId, executionId, sessionId) {
|
|
1796
2323
|
const now = this.now();
|
|
1797
2324
|
this.document.tasks = this.document.tasks.map((task) => task.id !== taskId ? task : {
|
|
@@ -1804,13 +2331,9 @@ var HostTaskLedger = class {
|
|
|
1804
2331
|
});
|
|
1805
2332
|
this.commit();
|
|
1806
2333
|
}
|
|
1807
|
-
settle(taskId, executionId, outcome, error) {
|
|
1808
|
-
this.document.tasks = this.document.tasks.map((task) => task.id === taskId ? settleExecution(task, executionId, outcome, this.now(), error) : task);
|
|
1809
|
-
this.commit();
|
|
1810
|
-
}
|
|
1811
2334
|
apply(action, initiator) {
|
|
1812
2335
|
const now = this.now();
|
|
1813
|
-
let
|
|
2336
|
+
let runs;
|
|
1814
2337
|
switch (action.kind) {
|
|
1815
2338
|
case "import": {
|
|
1816
2339
|
const sources = new Set(this.document.scheduler.importedSources ?? []);
|
|
@@ -1819,7 +2342,7 @@ var HostTaskLedger = class {
|
|
|
1819
2342
|
const incoming = parseHostTasks(action.tasks);
|
|
1820
2343
|
const merged = new Map(this.document.tasks.map((task) => [task.id, task]));
|
|
1821
2344
|
for (const task of incoming) merged.set(task.id, merged.has(task.id) ? mergeTask(merged.get(task.id), task) : task);
|
|
1822
|
-
this.document.tasks = [...merged.values()];
|
|
2345
|
+
this.document.tasks = repairParentLinks([...merged.values()]);
|
|
1823
2346
|
this.document.scheduler.importedSources = [...sources, action.sourceId];
|
|
1824
2347
|
this.document.scheduler.error = invalidScheduleIds.length === 0 ? void 0 : `invalid cron disabled for task(s): ${invalidScheduleIds.join(", ")}`;
|
|
1825
2348
|
this.repairSchedules(true, false);
|
|
@@ -1836,8 +2359,8 @@ var HostTaskLedger = class {
|
|
|
1836
2359
|
frozenBy: initiator
|
|
1837
2360
|
}
|
|
1838
2361
|
};
|
|
1839
|
-
const result = applyCreateTask(this.document.tasks, input, now, action.id);
|
|
1840
|
-
if (result.task === void 0) throw new Error("invalid task");
|
|
2362
|
+
const result = applyCreateTask(this.document.tasks, input, now, action.id, this.maxSubtaskDepth);
|
|
2363
|
+
if (result.task === void 0) throw new Error(result.error ?? "invalid task");
|
|
1841
2364
|
this.document.tasks = [...result.tasks];
|
|
1842
2365
|
break;
|
|
1843
2366
|
}
|
|
@@ -1857,14 +2380,20 @@ var HostTaskLedger = class {
|
|
|
1857
2380
|
this.document.tasks = [...applyUpdateTask(this.document.tasks, action.taskId, patch, now)];
|
|
1858
2381
|
break;
|
|
1859
2382
|
}
|
|
1860
|
-
case "delete":
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
}
|
|
2383
|
+
case "delete": {
|
|
2384
|
+
const task = this.document.tasks.find((task) => task.id === action.taskId);
|
|
2385
|
+
if (task === void 0) throw new Error("task not found");
|
|
2386
|
+
if (task.status === "running" || hasOpenExecution(task)) throw new Error("running task cannot be deleted");
|
|
2387
|
+
if (this.document.tasks.some((item) => item.parentId === action.taskId)) throw new Error("task has subtasks; detach or delete them first");
|
|
1866
2388
|
this.document.tasks = [...applyDeleteTask(this.document.tasks, void 0, action.taskId).tasks];
|
|
1867
2389
|
break;
|
|
2390
|
+
}
|
|
2391
|
+
case "set-parent": {
|
|
2392
|
+
const result = applySetParent(this.document.tasks, action.taskId, action.parentId, now, this.maxSubtaskDepth);
|
|
2393
|
+
if (!result.applied) throw new Error(result.error ?? "parent link refused");
|
|
2394
|
+
this.document.tasks = [...result.tasks];
|
|
2395
|
+
break;
|
|
2396
|
+
}
|
|
1868
2397
|
case "move": {
|
|
1869
2398
|
const task = this.document.tasks.find((item) => item.id === action.taskId);
|
|
1870
2399
|
if (task === void 0) throw new Error("task not found");
|
|
@@ -1875,13 +2404,15 @@ var HostTaskLedger = class {
|
|
|
1875
2404
|
break;
|
|
1876
2405
|
}
|
|
1877
2406
|
case "archive": {
|
|
1878
|
-
|
|
2407
|
+
if (this.subtreeHasOpenExecution(action.taskId)) throw new Error("running task cannot be archived");
|
|
2408
|
+
const result = applyArchiveTask(this.document.tasks, action.taskId, now, this.maxSubtaskDepth);
|
|
1879
2409
|
if (!result.archived) throw new Error("task cannot be archived");
|
|
1880
2410
|
this.document.tasks = [...result.tasks];
|
|
1881
2411
|
break;
|
|
1882
2412
|
}
|
|
1883
2413
|
case "restore": {
|
|
1884
|
-
|
|
2414
|
+
if (this.subtreeHasOpenExecution(action.taskId)) throw new Error("running task cannot be restored");
|
|
2415
|
+
const result = applyRestoreTask(this.document.tasks, action.taskId, now, this.maxSubtaskDepth);
|
|
1885
2416
|
if (!result.archived) throw new Error("task is not archived");
|
|
1886
2417
|
this.document.tasks = [...result.tasks];
|
|
1887
2418
|
break;
|
|
@@ -1909,18 +2440,140 @@ var HostTaskLedger = class {
|
|
|
1909
2440
|
const task = this.document.tasks.find((item) => item.id === action.taskId);
|
|
1910
2441
|
if (task?.archivedAt !== void 0) throw new Error("archived task is read-only");
|
|
1911
2442
|
if (task === void 0 || task.status === "running" || hasOpenExecution(task)) throw new Error("task is already running or missing");
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2443
|
+
const refusal = this.bindingRefusal(task);
|
|
2444
|
+
if (refusal !== void 0) throw new Error(bindingRefusalMessage(refusal, this.sessionDefaultPermission, "run"));
|
|
2445
|
+
runs = this.startCascade(task, now, initiator, action.kind === "rerun");
|
|
1915
2446
|
break;
|
|
1916
2447
|
}
|
|
1917
2448
|
}
|
|
1918
|
-
this.commit();
|
|
2449
|
+
if (runs === void 0 || runs.length === 0) this.commit();
|
|
1919
2450
|
return {
|
|
1920
2451
|
state: this.state(),
|
|
1921
|
-
...
|
|
2452
|
+
...runs === void 0 ? {} : { runs }
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* The binding that makes a run illegal, if any.
|
|
2457
|
+
*
|
|
2458
|
+
* A plain cascade launches one session per participant, so every participant
|
|
2459
|
+
* carries its own resolved binding and each one gates the run. A team run
|
|
2460
|
+
* launches only the Lead session: the Lead's binding gates it, while a
|
|
2461
|
+
* subtask's OWN above-default pin cannot be applied to a teammate and is
|
|
2462
|
+
* refused instead of being silently dropped (an inherited binding is the
|
|
2463
|
+
* Lead's own and stays allowed once the Lead is confirmed).
|
|
2464
|
+
* @param root - the task being run.
|
|
2465
|
+
* @returns the first refusal, or undefined when the run may start.
|
|
2466
|
+
*/
|
|
2467
|
+
bindingRefusal(root) {
|
|
2468
|
+
const participants = this.cascadeParticipants(root.id);
|
|
2469
|
+
if (root.teamRun === true) {
|
|
2470
|
+
const lead = participants.find((participant) => participant.id === root.id);
|
|
2471
|
+
if (lead !== void 0 && requiresPermissionConfirmation(lead, this.sessionDefaultPermission)) return {
|
|
2472
|
+
kind: "root",
|
|
2473
|
+
title: lead.title
|
|
2474
|
+
};
|
|
2475
|
+
const pinned = participants.find((participant) => {
|
|
2476
|
+
if (participant.id === root.id) return false;
|
|
2477
|
+
const raw = this.document.tasks.find((task) => task.id === participant.id);
|
|
2478
|
+
return raw !== void 0 && requiresPermissionConfirmation(raw, this.sessionDefaultPermission);
|
|
2479
|
+
});
|
|
2480
|
+
return pinned === void 0 ? void 0 : {
|
|
2481
|
+
kind: "subtask-pin",
|
|
2482
|
+
title: pinned.title
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
const unconfirmed = participants.find((participant) => requiresPermissionConfirmation(participant, this.sessionDefaultPermission));
|
|
2486
|
+
if (unconfirmed === void 0) return void 0;
|
|
2487
|
+
return {
|
|
2488
|
+
kind: unconfirmed.id === root.id ? "root" : "inherited",
|
|
2489
|
+
title: unconfirmed.title
|
|
1922
2490
|
};
|
|
1923
2491
|
}
|
|
2492
|
+
/**
|
|
2493
|
+
* The tasks a cascade from `rootId` would actually open executions for, with
|
|
2494
|
+
* their effective execution targets resolved. Archived members never run, and
|
|
2495
|
+
* a member that already has an open execution is skipped (a task cannot run
|
|
2496
|
+
* twice), so the result is the participant set the launch will use.
|
|
2497
|
+
*/
|
|
2498
|
+
cascadeParticipants(rootId) {
|
|
2499
|
+
return cascadeTargets(this.document.tasks, rootId, this.maxSubtaskDepth).map((participant) => resolveExecutionTargets(participant, this.document.tasks, this.maxSubtaskDepth)).filter((participant) => participant.archivedAt === void 0 && participant.status !== "running" && !hasOpenExecution(participant));
|
|
2500
|
+
}
|
|
2501
|
+
/** Whether a task or any of its subtasks still has an open execution. */
|
|
2502
|
+
subtreeHasOpenExecution(id) {
|
|
2503
|
+
return cascadeTargets(this.document.tasks, id, this.maxSubtaskDepth).some((task) => task.status === "running" || hasOpenExecution(task));
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Open one execution per cascade participant (the requested task and its
|
|
2507
|
+
* on-board descendants, within the depth limit) under a single run group, and
|
|
2508
|
+
* commit the ledger once. The returned runs are ordered root-first, so the
|
|
2509
|
+
* Host launches the parent before its subtasks.
|
|
2510
|
+
* @param root - the task the user ran.
|
|
2511
|
+
* @param now - clock instant (ms epoch).
|
|
2512
|
+
* @param initiator - the DSH session that asked for the run (audit only).
|
|
2513
|
+
* @param rerun - true to reset the root to 'todo' before starting it.
|
|
2514
|
+
* @param nextRunAt - when set, the root's schedule also rolls forward.
|
|
2515
|
+
*/
|
|
2516
|
+
startCascade(root, now, initiator, rerun = false, nextRunAt) {
|
|
2517
|
+
const before = this.document.tasks;
|
|
2518
|
+
const groupId = crypto.randomUUID();
|
|
2519
|
+
const team = root.teamRun === true;
|
|
2520
|
+
const started = /* @__PURE__ */ new Map();
|
|
2521
|
+
const runs = [];
|
|
2522
|
+
for (const task of cascadeTargets(before, root.id, this.maxSubtaskDepth)) {
|
|
2523
|
+
if (task.archivedAt !== void 0) continue;
|
|
2524
|
+
if (task.status === "running" || hasOpenExecution(task)) continue;
|
|
2525
|
+
const opened = startExecution(rerun && task.id === root.id ? withStatus(task, "todo", now) : task, now, crypto.randomUUID(), initiator, groupId);
|
|
2526
|
+
started.set(task.id, opened.task);
|
|
2527
|
+
runs.push({
|
|
2528
|
+
task: resolveExecutionTargets(opened.task, before, this.maxSubtaskDepth),
|
|
2529
|
+
execution: opened.execution,
|
|
2530
|
+
...team && task.id !== root.id ? { dispatch: "teammate" } : {}
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
if (runs.length === 0) return [];
|
|
2534
|
+
let tasks = before.map((task) => started.get(task.id) ?? task);
|
|
2535
|
+
if (nextRunAt !== void 0) tasks = applyScheduleNextRun(tasks, root.id, nextRunAt, now, now);
|
|
2536
|
+
this.document.tasks = [...tasks];
|
|
2537
|
+
this.commit();
|
|
2538
|
+
return runs;
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Finalize every cascade parent that is ready, walking from `taskId` up the
|
|
2542
|
+
* lineage: a member settles once its own outcome is recorded and none of its
|
|
2543
|
+
* group children is still open, and its verdict folds those children in
|
|
2544
|
+
* (failure dominates, then cancellation).
|
|
2545
|
+
*/
|
|
2546
|
+
settleCascade(taskId, groupId, now) {
|
|
2547
|
+
let changed = false;
|
|
2548
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2549
|
+
let current = taskId;
|
|
2550
|
+
while (current !== void 0 && !visited.has(current)) {
|
|
2551
|
+
visited.add(current);
|
|
2552
|
+
const task = this.document.tasks.find((item) => item.id === current);
|
|
2553
|
+
if (task === void 0) break;
|
|
2554
|
+
const execution = openGroupExecution(task, groupId);
|
|
2555
|
+
if (execution === void 0 || execution.ownResult === void 0) break;
|
|
2556
|
+
if (pendingCascadeChildren(this.document.tasks, current, groupId).length > 0) break;
|
|
2557
|
+
const entries = [{
|
|
2558
|
+
result: execution.ownResult,
|
|
2559
|
+
...execution.ownError === void 0 ? {} : { error: execution.ownError }
|
|
2560
|
+
}];
|
|
2561
|
+
for (const child of this.document.tasks) {
|
|
2562
|
+
if (child.parentId !== current) continue;
|
|
2563
|
+
const childExecution = child.executions.find((entry) => entry.runGroupId === groupId && entry.result !== void 0);
|
|
2564
|
+
if (childExecution?.result === void 0) continue;
|
|
2565
|
+
entries.push({
|
|
2566
|
+
result: childExecution.result,
|
|
2567
|
+
...childExecution.error === void 0 ? {} : { error: childExecution.error }
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
const combined = combineCascadeOutcome(entries);
|
|
2571
|
+
this.document.tasks = this.document.tasks.map((item) => item.id === current ? settleExecution(item, execution.id, combined.result, now, combined.error) : item);
|
|
2572
|
+
changed = true;
|
|
2573
|
+
current = task.parentId;
|
|
2574
|
+
}
|
|
2575
|
+
return changed;
|
|
2576
|
+
}
|
|
1924
2577
|
repairSchedules(skipPast, persist = true) {
|
|
1925
2578
|
const now = this.now();
|
|
1926
2579
|
let changed = false;
|
|
@@ -1958,13 +2611,23 @@ var HostTaskLedger = class {
|
|
|
1958
2611
|
reconcileInterruptedStarts(persist = true) {
|
|
1959
2612
|
const now = this.now();
|
|
1960
2613
|
let changed = false;
|
|
2614
|
+
const interrupted = [];
|
|
1961
2615
|
this.document.tasks = this.document.tasks.map((task) => {
|
|
1962
2616
|
if (task.status !== "running") return task;
|
|
1963
2617
|
const execution = task.executions.at(-1);
|
|
1964
2618
|
if (execution === void 0 || execution.endedAt !== void 0 || execution.sessionId !== void 0) return task;
|
|
1965
2619
|
changed = true;
|
|
2620
|
+
interrupted.push({
|
|
2621
|
+
taskId: task.id,
|
|
2622
|
+
runGroupId: execution.runGroupId
|
|
2623
|
+
});
|
|
1966
2624
|
return settleExecution(task, execution.id, "cancelled", now, "host restarted before the execution session was recorded");
|
|
1967
2625
|
});
|
|
2626
|
+
for (const entry of interrupted) {
|
|
2627
|
+
if (entry.runGroupId === void 0) continue;
|
|
2628
|
+
const parentId = this.document.tasks.find((task) => task.id === entry.taskId)?.parentId;
|
|
2629
|
+
if (parentId !== void 0 && this.settleCascade(parentId, entry.runGroupId, now)) changed = true;
|
|
2630
|
+
}
|
|
1968
2631
|
if (changed && persist) this.commit();
|
|
1969
2632
|
}
|
|
1970
2633
|
/**
|
|
@@ -2218,6 +2881,17 @@ function isInvocationUnavailable(error) {
|
|
|
2218
2881
|
const code = error.code;
|
|
2219
2882
|
return code === "invocation-unavailable" || code === "gateway/invocation-unavailable";
|
|
2220
2883
|
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Whether the endpoint exists but its strict definition was withdrawn by the
|
|
2886
|
+
* running gateway (the cohort changed under this plugin). The plugin can send
|
|
2887
|
+
* the call correctly and still be refused, and nothing in this repository can
|
|
2888
|
+
* restore the definition, so the roster is treated as unknown quietly instead
|
|
2889
|
+
* of printing an error the user has no action for (#1672).
|
|
2890
|
+
*/
|
|
2891
|
+
function isDefinitionWithdrawn(error) {
|
|
2892
|
+
const code = error.code;
|
|
2893
|
+
return code === "definition-unavailable" || code === "gateway/definition-unavailable";
|
|
2894
|
+
}
|
|
2221
2895
|
const SERVICE_UNAVAILABLE_ATTEMPTS = 5;
|
|
2222
2896
|
const SERVICE_UNAVAILABLE_BACKOFF_MS = 2e3;
|
|
2223
2897
|
function delay(ms) {
|
|
@@ -2250,20 +2924,35 @@ function escapeProvenanceDelimiter(value) {
|
|
|
2250
2924
|
return value.replaceAll("来源声明 开始", "来源声明·开始").replaceAll("来源声明 结束", "来源声明·结束");
|
|
2251
2925
|
}
|
|
2252
2926
|
/**
|
|
2253
|
-
*
|
|
2254
|
-
* a
|
|
2255
|
-
*
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2927
|
+
* The execution-shape section: what else this run opens. A plain cascade opens
|
|
2928
|
+
* one independent session per member; a team run starts every other member as a
|
|
2929
|
+
* teammate inside THIS session, so the prompt names the Team tools instead.
|
|
2930
|
+
*/
|
|
2931
|
+
function peerPromptPreamble(context) {
|
|
2932
|
+
const peers = context.peers ?? [];
|
|
2933
|
+
if (peers.length === 0) return void 0;
|
|
2934
|
+
if (context.team === true) {
|
|
2935
|
+
const lines = peers.map((peer) => `- ${escapeProvenanceDelimiter(peer.title)}(teammate: ${peer.name ?? teammateName(peer.title, peer.id)})`);
|
|
2936
|
+
return `本任务是 Agent Team 的 Lead:本次运行不额外开启独立会话,以下 ${peers.length} 个子任务成员已在本会话中作为 teammate 启动。用 list_agents / send_message / wait_agent 协调它们,用任务看板工具读写它们在看板上的卡片:\n${lines.join("\n")}`;
|
|
2937
|
+
}
|
|
2938
|
+
const lines = peers.map((peer) => `- ${escapeProvenanceDelimiter(peer.title)}(任务 ${peer.id})`);
|
|
2939
|
+
return `本次运行同时并发开启 ${peers.length} 个独立 DSH 会话执行下列子任务成员(可用 task_board_* 工具查看它们的进度):\n${lines.join("\n")}`;
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Build the execution prompt for one task.
|
|
2943
|
+
* @param task - the task being launched.
|
|
2944
|
+
* @param context - the run's other members, for the execution-shape section.
|
|
2945
|
+
* @returns the prompt text.
|
|
2261
2946
|
*/
|
|
2262
|
-
function promptText(task) {
|
|
2947
|
+
function promptText(task, context = {}) {
|
|
2263
2948
|
const body = task.prompt !== "" ? task.prompt : task.title;
|
|
2264
2949
|
const handover = task.handover;
|
|
2265
2950
|
const handoverPreamble = handover === void 0 || handover.references.length === 0 ? void 0 : `交接包引用(来自任务看板续接卡片,冻结于 ${new Date(handover.bundledAt).toISOString()}):\n${handover.references.map((reference) => `- ${reference}`).join("\n")}`;
|
|
2266
|
-
const preambles = [
|
|
2951
|
+
const preambles = [
|
|
2952
|
+
tagPromptPreamble(task),
|
|
2953
|
+
handoverPreamble,
|
|
2954
|
+
peerPromptPreamble(context)
|
|
2955
|
+
].filter((part) => part !== void 0);
|
|
2267
2956
|
const preamble = preambles.length === 0 ? void 0 : preambles.join("\n\n");
|
|
2268
2957
|
const freeze = task.freeze;
|
|
2269
2958
|
if (freeze === void 0) return preamble === void 0 ? body : `${preamble}\n\n${body}`;
|
|
@@ -2287,10 +2976,27 @@ function tagPromptPreamble(task) {
|
|
|
2287
2976
|
if (lines.length === 0) return void 0;
|
|
2288
2977
|
return `标签提示(任务看板标签,每次执行前注入):\n${lines.join("\n")}`;
|
|
2289
2978
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2979
|
+
/**
|
|
2980
|
+
* Why a `turn/end` did not complete successfully, or undefined when it did.
|
|
2981
|
+
*
|
|
2982
|
+
* The harness treats exactly `completed` as success (`headless` maps the turn
|
|
2983
|
+
* reason to its exit code, `subagent/projection` records `lastTurnCompleted`,
|
|
2984
|
+
* and `max-tokens` is a ceiling, not a result). A task-board execution is a
|
|
2985
|
+
* SUCCESS only on positive evidence of a completed turn; every other reason —
|
|
2986
|
+
* and an unreadable one — must be reported, or the board stamps a failed run
|
|
2987
|
+
* as `succeeded` and a schedule dies silently (issue #1708). `interrupted` is
|
|
2988
|
+
* the reason a restart produces: `interruptedTurnClosers` synthesizes it for a
|
|
2989
|
+
* log whose tail turn never ended, which is exactly an aborted resume.
|
|
2990
|
+
* @param data - the `turn/end` event payload.
|
|
2991
|
+
* @returns the reason kind when it is not `completed`, else undefined.
|
|
2992
|
+
*/
|
|
2993
|
+
function nonCompletedTurnEnd(data) {
|
|
2994
|
+
if (typeof data !== "object" || data === null) return "unknown";
|
|
2292
2995
|
const reason = data.reason;
|
|
2293
|
-
|
|
2996
|
+
if (typeof reason !== "object" || reason === null) return "unknown";
|
|
2997
|
+
const kind = reason.kind;
|
|
2998
|
+
if (typeof kind !== "string") return "unknown";
|
|
2999
|
+
return kind === "completed" ? void 0 : kind;
|
|
2294
3000
|
}
|
|
2295
3001
|
/**
|
|
2296
3002
|
* Wire-argument layout of the 0.1.2-alpha.2 descriptor tables; the gateway's
|
|
@@ -2366,7 +3072,8 @@ var HostExecutionRunner = class {
|
|
|
2366
3072
|
const reused = options.reuseSessionId;
|
|
2367
3073
|
if (reused !== void 0) {
|
|
2368
3074
|
try {
|
|
2369
|
-
await this.
|
|
3075
|
+
await this.assertReusedPreset(reused, mode);
|
|
3076
|
+
await this.pinAndPrompt(reused, task, permission, options.promptContext);
|
|
2370
3077
|
} catch (error) {
|
|
2371
3078
|
throw new SessionLaunchError(reused, error);
|
|
2372
3079
|
}
|
|
@@ -2381,18 +3088,38 @@ var HostExecutionRunner = class {
|
|
|
2381
3088
|
sessionId,
|
|
2382
3089
|
title: task.title
|
|
2383
3090
|
});
|
|
2384
|
-
await this.pinAndPrompt(sessionId, task, permission);
|
|
3091
|
+
await this.pinAndPrompt(sessionId, task, permission, options.promptContext);
|
|
2385
3092
|
} catch (error) {
|
|
2386
3093
|
throw new SessionLaunchError(sessionId, error);
|
|
2387
3094
|
}
|
|
2388
3095
|
return sessionId;
|
|
2389
3096
|
}
|
|
2390
3097
|
/**
|
|
3098
|
+
* Fail closed when a reused session records a different Agent preset than
|
|
3099
|
+
* the one this task pins.
|
|
3100
|
+
*
|
|
3101
|
+
* The fresh branch asserts the pin by passing `agentPreset` to
|
|
3102
|
+
* `session/create`; the reuse branch must not call that method (it would
|
|
3103
|
+
* create or re-adopt the very session the board only meant to continue), so
|
|
3104
|
+
* the recorded value is read through `session/projections` — the documented
|
|
3105
|
+
* read that resolves no Agent — and compared here (issue #1708). A session
|
|
3106
|
+
* whose preset cannot be read is not treated as a match: silently continuing
|
|
3107
|
+
* under an unknown composition is the failure this guards.
|
|
3108
|
+
* @param sessionId - the session this execution continues in.
|
|
3109
|
+
* @param pinned - the preset id the task pins, when it pins one.
|
|
3110
|
+
*/
|
|
3111
|
+
async assertReusedPreset(sessionId, pinned) {
|
|
3112
|
+
if (pinned === void 0) return;
|
|
3113
|
+
const recorded = (await this.invoke("session", "projections", { sessionId }))?.values?.agentPreset;
|
|
3114
|
+
if (recorded === pinned) return;
|
|
3115
|
+
throw new Error("reused session " + sessionId + " was composed from agent preset " + (typeof recorded === "string" ? "\"" + recorded + "\"" : "an unreadable value") + ", but the task pins \"" + pinned + "\"");
|
|
3116
|
+
}
|
|
3117
|
+
/**
|
|
2391
3118
|
* Re-assert the pinned execution contract on a session and queue the task
|
|
2392
3119
|
* prompt. Shared by the fresh-session and reuse paths so both apply exactly
|
|
2393
3120
|
* the same permission/model pins before the prompt.
|
|
2394
3121
|
*/
|
|
2395
|
-
async pinAndPrompt(sessionId, task, permission) {
|
|
3122
|
+
async pinAndPrompt(sessionId, task, permission, context = {}) {
|
|
2396
3123
|
if (permission !== void 0) {
|
|
2397
3124
|
if (this.commands === void 0) throw new Error("permission command dispatcher is unavailable");
|
|
2398
3125
|
const command = await this.commands.execute(sessionId, "/permission " + permission, AbortSignal.timeout(3e4));
|
|
@@ -2420,7 +3147,7 @@ var HostExecutionRunner = class {
|
|
|
2420
3147
|
mode: "queue",
|
|
2421
3148
|
content: [{
|
|
2422
3149
|
type: "text",
|
|
2423
|
-
text: promptText(task)
|
|
3150
|
+
text: promptText(task, context)
|
|
2424
3151
|
}]
|
|
2425
3152
|
});
|
|
2426
3153
|
}
|
|
@@ -2440,6 +3167,13 @@ var HostExecutionRunner = class {
|
|
|
2440
3167
|
}
|
|
2441
3168
|
return { known: false };
|
|
2442
3169
|
}
|
|
3170
|
+
if (isDefinitionWithdrawn(error)) {
|
|
3171
|
+
if (!this.unsupportedSessionListWarned) {
|
|
3172
|
+
this.unsupportedSessionListWarned = true;
|
|
3173
|
+
console.warn("[dsh-task-board] the host gateway withdrew the session/list definition for this DSH cohort; task board roster auto-discovery is disabled", error);
|
|
3174
|
+
}
|
|
3175
|
+
return { known: false };
|
|
3176
|
+
}
|
|
2443
3177
|
if (!isServiceUnavailable(error) || attempt >= this.unavailableAttempts) {
|
|
2444
3178
|
console.error("[dsh-task-board] session/list failed; treating the host session roster as unknown", error);
|
|
2445
3179
|
return { known: false };
|
|
@@ -2463,6 +3197,10 @@ var HostExecutionRunner = class {
|
|
|
2463
3197
|
}
|
|
2464
3198
|
return { outcome: "pending" };
|
|
2465
3199
|
}
|
|
3200
|
+
if (isDefinitionWithdrawn(error)) {
|
|
3201
|
+
console.warn("[dsh-task-board] the host gateway withdrew the session/list definition for this DSH cohort; keeping the outcome pending", error);
|
|
3202
|
+
return { outcome: "pending" };
|
|
3203
|
+
}
|
|
2466
3204
|
console.warn("[dsh-task-board] session/list failed during execution inspection; keeping the outcome pending", error);
|
|
2467
3205
|
return { outcome: "pending" };
|
|
2468
3206
|
}
|
|
@@ -2533,10 +3271,15 @@ var HostExecutionRunner = class {
|
|
|
2533
3271
|
return { outcome: "pending" };
|
|
2534
3272
|
}
|
|
2535
3273
|
this.scanMemos.delete(sessionId);
|
|
2536
|
-
|
|
3274
|
+
const reason = nonCompletedTurnEnd(turnEnd.event.data);
|
|
3275
|
+
if (reason === void 0) return { outcome: "succeeded" };
|
|
3276
|
+
return reason === "error" ? {
|
|
2537
3277
|
outcome: "failed",
|
|
2538
3278
|
error: "agent turn ended with an error"
|
|
2539
|
-
} : {
|
|
3279
|
+
} : {
|
|
3280
|
+
outcome: "failed",
|
|
3281
|
+
error: "agent turn ended without completing: " + reason
|
|
3282
|
+
};
|
|
2540
3283
|
}
|
|
2541
3284
|
};
|
|
2542
3285
|
//#endregion
|
|
@@ -2843,19 +3586,55 @@ var PowerInhibitor = class {
|
|
|
2843
3586
|
};
|
|
2844
3587
|
//#endregion
|
|
2845
3588
|
//#region src/host-service.ts
|
|
3589
|
+
/** Session-roster poll cadence — the one recurring Host timer this service still holds. */
|
|
2846
3590
|
const SESSION_POLL_MS = 5e3;
|
|
2847
|
-
|
|
2848
|
-
|
|
3591
|
+
/**
|
|
3592
|
+
* How late an armed schedule fire may be before it counts as a resume rather
|
|
3593
|
+
* than a normal occurrence. The schedule timer is armed AT the next due
|
|
3594
|
+
* instant, so landing this far past its target means the Host was suspended,
|
|
3595
|
+
* the process throttled, or the wall clock jumped forward — the same condition
|
|
3596
|
+
* the old fixed 30 s heartbeat detected through its own gap threshold.
|
|
3597
|
+
*/
|
|
3598
|
+
const RECOVERY_TOLERANCE_MS = 6e4;
|
|
3599
|
+
/** Largest delay a Node timer represents without clamping; longer targets re-arm in segments. */
|
|
3600
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
3601
|
+
/**
|
|
3602
|
+
* Actions that can move an armed trigger. Only these re-arm the native timer;
|
|
3603
|
+
* an unrelated card edit leaves the pending fire untouched.
|
|
3604
|
+
*/
|
|
3605
|
+
const SCHEDULE_WRITE_ACTIONS = /* @__PURE__ */ new Set([
|
|
3606
|
+
"set-schedule",
|
|
3607
|
+
"delete",
|
|
3608
|
+
"archive"
|
|
3609
|
+
]);
|
|
3610
|
+
/** Process-global fallback used when the deployment serves no cordis timer service. */
|
|
3611
|
+
const PROCESS_TIMERS = {
|
|
3612
|
+
timeout(callback, delay) {
|
|
3613
|
+
const handle = setTimeout(callback, delay);
|
|
3614
|
+
return () => {
|
|
3615
|
+
clearTimeout(handle);
|
|
3616
|
+
};
|
|
3617
|
+
},
|
|
3618
|
+
interval(callback, delay) {
|
|
3619
|
+
const handle = setInterval(callback, delay);
|
|
3620
|
+
return () => {
|
|
3621
|
+
clearInterval(handle);
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
};
|
|
2849
3625
|
var TaskBoardHostService = class {
|
|
2850
3626
|
ledger;
|
|
2851
3627
|
runner;
|
|
2852
3628
|
power;
|
|
2853
3629
|
listeners = /* @__PURE__ */ new Set();
|
|
2854
|
-
|
|
2855
|
-
|
|
3630
|
+
/** The one recurring timer: the session-roster poll. */
|
|
3631
|
+
pollTimer;
|
|
3632
|
+
/** The armed schedule timer, if a trigger is pending. */
|
|
3633
|
+
scheduleTimer;
|
|
3634
|
+
/** The instant the armed schedule timer targets (ms epoch), for resume detection. */
|
|
3635
|
+
scheduleTarget;
|
|
2856
3636
|
disposed = false;
|
|
2857
3637
|
pollInFlight = false;
|
|
2858
|
-
tickInFlight = false;
|
|
2859
3638
|
active = true;
|
|
2860
3639
|
/**
|
|
2861
3640
|
* Ids the last roster poll saw as present and idle; undefined while the
|
|
@@ -2865,11 +3644,18 @@ var TaskBoardHostService = class {
|
|
|
2865
3644
|
*/
|
|
2866
3645
|
idleSessionIds;
|
|
2867
3646
|
preventIdleSleep = false;
|
|
3647
|
+
team;
|
|
3648
|
+
timers;
|
|
2868
3649
|
lastPowerJson = "";
|
|
2869
3650
|
now;
|
|
2870
3651
|
constructor(gateway, options = {}) {
|
|
2871
|
-
this.ledger = options.ledger ?? new HostTaskLedger(void 0, void 0, {
|
|
3652
|
+
this.ledger = options.ledger ?? new HostTaskLedger(void 0, void 0, {
|
|
3653
|
+
sessionDefaultPermission: options.sessionDefaultPermission,
|
|
3654
|
+
maxSubtaskDepth: options.maxSubtaskDepth
|
|
3655
|
+
});
|
|
2872
3656
|
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry);
|
|
3657
|
+
this.team = options.team;
|
|
3658
|
+
this.timers = options.timers ?? PROCESS_TIMERS;
|
|
2873
3659
|
this.power = options.power ?? new PowerInhibitor();
|
|
2874
3660
|
this.now = options.now ?? Date.now;
|
|
2875
3661
|
installStreamErrorGuards();
|
|
@@ -2885,16 +3671,13 @@ var TaskBoardHostService = class {
|
|
|
2885
3671
|
});
|
|
2886
3672
|
}
|
|
2887
3673
|
start() {
|
|
2888
|
-
if (this.disposed || this.
|
|
3674
|
+
if (this.disposed || this.pollTimer !== void 0) return;
|
|
2889
3675
|
this.syncPowerReasons();
|
|
2890
|
-
this.timers.
|
|
3676
|
+
this.pollTimer = this.timers.interval(() => {
|
|
2891
3677
|
this.schedulePoll();
|
|
2892
|
-
}, SESSION_POLL_MS)
|
|
2893
|
-
this.timers.push(setInterval(() => {
|
|
2894
|
-
this.scheduleTick(false);
|
|
2895
|
-
}, SCHEDULE_TICK_MS));
|
|
3678
|
+
}, SESSION_POLL_MS);
|
|
2896
3679
|
this.schedulePoll();
|
|
2897
|
-
this.
|
|
3680
|
+
this.recoverSchedule();
|
|
2898
3681
|
}
|
|
2899
3682
|
setConfiguration(active, preventIdleSleep) {
|
|
2900
3683
|
const resumed = !this.active && active;
|
|
@@ -2911,8 +3694,8 @@ var TaskBoardHostService = class {
|
|
|
2911
3694
|
this.power.setEnabled(active && preventIdleSleep);
|
|
2912
3695
|
if (resumed) {
|
|
2913
3696
|
this.schedulePoll();
|
|
2914
|
-
this.
|
|
2915
|
-
}
|
|
3697
|
+
this.recoverSchedule();
|
|
3698
|
+
} else if (!active) this.clearScheduleTimer();
|
|
2916
3699
|
this.emit();
|
|
2917
3700
|
}
|
|
2918
3701
|
snapshot() {
|
|
@@ -2923,7 +3706,9 @@ var TaskBoardHostService = class {
|
|
|
2923
3706
|
tasks: state.tasks,
|
|
2924
3707
|
scheduler: state.scheduler,
|
|
2925
3708
|
power: this.power.snapshot(),
|
|
2926
|
-
sessionDefaultPermission: this.ledger.sessionDefaultPermission
|
|
3709
|
+
sessionDefaultPermission: this.ledger.sessionDefaultPermission,
|
|
3710
|
+
maxSubtaskDepth: this.ledger.maxSubtaskDepth,
|
|
3711
|
+
teamRunAvailable: this.team !== void 0
|
|
2927
3712
|
};
|
|
2928
3713
|
}
|
|
2929
3714
|
/** SSE frame payload; deliberately skips the tasks deep-clone of {@link snapshot}. */
|
|
@@ -2943,8 +3728,12 @@ var TaskBoardHostService = class {
|
|
|
2943
3728
|
}
|
|
2944
3729
|
apply(requestId, action, initiator) {
|
|
2945
3730
|
if (!this.active) throw new Error("task board is disabled");
|
|
3731
|
+
if (this.team === void 0 && (action.kind === "run" || action.kind === "rerun")) {
|
|
3732
|
+
if (this.ledger.state().tasks.find((item) => item.id === action.taskId)?.teamRun === true) throw new Error("Agent Teams is unavailable in this deployment");
|
|
3733
|
+
}
|
|
2946
3734
|
const result = this.ledger.applyRequest(requestId, action, initiator);
|
|
2947
|
-
if (result.
|
|
3735
|
+
if (result.runs !== void 0) this.dispatchRuns(result.runs);
|
|
3736
|
+
if (SCHEDULE_WRITE_ACTIONS.has(action.kind)) this.refreshSchedule();
|
|
2948
3737
|
return {
|
|
2949
3738
|
schemaVersion: 3,
|
|
2950
3739
|
revision: result.state.revision,
|
|
@@ -2955,25 +3744,74 @@ var TaskBoardHostService = class {
|
|
|
2955
3744
|
}
|
|
2956
3745
|
dispose() {
|
|
2957
3746
|
this.disposed = true;
|
|
2958
|
-
|
|
3747
|
+
this.clearScheduleTimer();
|
|
3748
|
+
this.pollTimer?.();
|
|
3749
|
+
this.pollTimer = void 0;
|
|
2959
3750
|
this.power.dispose();
|
|
2960
3751
|
this.ledger.dispose();
|
|
2961
3752
|
this.listeners.clear();
|
|
2962
3753
|
}
|
|
2963
|
-
async launch(opened) {
|
|
3754
|
+
async launch(opened, others = []) {
|
|
2964
3755
|
try {
|
|
2965
|
-
const
|
|
2966
|
-
const
|
|
3756
|
+
const team = opened.task.teamRun === true;
|
|
3757
|
+
const reuseSessionId = team ? void 0 : reusableSessionId(opened.task, this.idleSessionIds);
|
|
3758
|
+
const promptContext = others.length === 0 ? void 0 : {
|
|
3759
|
+
peers: others.map((other) => ({
|
|
3760
|
+
id: other.task.id,
|
|
3761
|
+
title: other.task.title,
|
|
3762
|
+
...team ? { name: teammateName(other.task.title, other.execution.runGroupId ?? other.task.id) } : {}
|
|
3763
|
+
})),
|
|
3764
|
+
...team ? { team: true } : {}
|
|
3765
|
+
};
|
|
3766
|
+
const sessionId = await this.runner.launch(opened.task, {
|
|
3767
|
+
...reuseSessionId === void 0 ? {} : { reuseSessionId },
|
|
3768
|
+
...promptContext === void 0 ? {} : { promptContext }
|
|
3769
|
+
});
|
|
2967
3770
|
this.ledger.attachSession(opened.task.id, opened.execution.id, sessionId);
|
|
3771
|
+
if (team) for (const teammate of others) this.scheduleTeammate(teammate, sessionId);
|
|
2968
3772
|
} catch (error) {
|
|
2969
3773
|
if (error instanceof SessionLaunchError) this.ledger.attachSession(opened.task.id, opened.execution.id, error.sessionId);
|
|
2970
3774
|
this.ledger.settle(opened.task.id, opened.execution.id, "failed", error instanceof Error ? error.message : String(error));
|
|
2971
3775
|
}
|
|
2972
3776
|
}
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3777
|
+
scheduleTeammate(opened, leadSessionId) {
|
|
3778
|
+
this.spawnTeammate(opened, leadSessionId).catch((error) => {
|
|
3779
|
+
safeConsoleError("[dsh-task-board] teammate spawn settlement failed", error);
|
|
3780
|
+
});
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Spawn one teammate inside the Lead session and attach the teammate's
|
|
3784
|
+
* session to the subtask execution, so the existing session monitor settles
|
|
3785
|
+
* it from the teammate's own turn like any other execution. A spawn that
|
|
3786
|
+
* fails settles the subtask as failed immediately, which then folds into the
|
|
3787
|
+
* Lead's cascade verdict.
|
|
3788
|
+
*/
|
|
3789
|
+
async spawnTeammate(opened, leadSessionId) {
|
|
3790
|
+
const team = this.team;
|
|
3791
|
+
if (team === void 0) {
|
|
3792
|
+
this.ledger.settle(opened.task.id, opened.execution.id, "failed", "Agent Teams is unavailable in this deployment");
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
try {
|
|
3796
|
+
const member = await team.spawn({
|
|
3797
|
+
leadSessionId,
|
|
3798
|
+
name: teammateName(opened.task.title, opened.execution.runGroupId ?? opened.task.id),
|
|
3799
|
+
description: opened.task.title,
|
|
3800
|
+
prompt: promptText(opened.task)
|
|
3801
|
+
});
|
|
3802
|
+
if (member.sessionId === void 0 || member.sessionId === "") {
|
|
3803
|
+
this.ledger.settle(opened.task.id, opened.execution.id, "failed", member.error ?? "teammate provisioning failed");
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3806
|
+
this.ledger.attachSession(opened.task.id, opened.execution.id, member.sessionId);
|
|
3807
|
+
} catch (error) {
|
|
3808
|
+
this.ledger.settle(opened.task.id, opened.execution.id, "failed", error instanceof Error ? error.message : String(error));
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
async pollSessions() {
|
|
3812
|
+
if (this.disposed) return;
|
|
3813
|
+
if (!this.active && this.ledger.runtimeView().openExecutions.length === 0) return;
|
|
3814
|
+
const running = await this.runner.listRunning();
|
|
2977
3815
|
const previous = this.power.snapshot();
|
|
2978
3816
|
if (!running.known) {
|
|
2979
3817
|
this.idleSessionIds = void 0;
|
|
@@ -3004,27 +3842,87 @@ var TaskBoardHostService = class {
|
|
|
3004
3842
|
} catch {}
|
|
3005
3843
|
}
|
|
3006
3844
|
}
|
|
3007
|
-
|
|
3845
|
+
/** Drop the armed schedule timer and forget its target. */
|
|
3846
|
+
clearScheduleTimer() {
|
|
3847
|
+
this.scheduleTimer?.();
|
|
3848
|
+
this.scheduleTimer = void 0;
|
|
3849
|
+
this.scheduleTarget = void 0;
|
|
3850
|
+
}
|
|
3851
|
+
/**
|
|
3852
|
+
* Boot / resume recovery: skip every occurrence that came due while the
|
|
3853
|
+
* board was not running and roll each schedule to its next future target,
|
|
3854
|
+
* then arm the timer for the nearest one. Rendering the occurrence is
|
|
3855
|
+
* deliberately not attempted: the ACL of a card that fired hours ago is
|
|
3856
|
+
* stale, and the board's own recovery contract is "missed triggers are
|
|
3857
|
+
* skipped, never replayed".
|
|
3858
|
+
*/
|
|
3859
|
+
recoverSchedule() {
|
|
3860
|
+
if (this.disposed) return;
|
|
3861
|
+
this.clearScheduleTimer();
|
|
3862
|
+
const now = this.now();
|
|
3863
|
+
this.ledger.setScheduler({ lastTickAt: now });
|
|
3864
|
+
this.ledger.skipMissed(now);
|
|
3865
|
+
this.armSchedule();
|
|
3866
|
+
}
|
|
3867
|
+
/**
|
|
3868
|
+
* Arm the native timer at the nearest armed future trigger. One timer serves
|
|
3869
|
+
* every schedule: the ledger's next target is the only instant the Host has
|
|
3870
|
+
* to wake for. A target beyond the platform's timer ceiling re-arms in
|
|
3871
|
+
* segments, and a target already past (the wall clock jumped, or the process
|
|
3872
|
+
* was suspended) is handled immediately as a recovery.
|
|
3873
|
+
*/
|
|
3874
|
+
armSchedule() {
|
|
3875
|
+
if (this.disposed || !this.active) return;
|
|
3876
|
+
this.clearScheduleTimer();
|
|
3877
|
+
const target = this.ledger.nextArmedRunAt(this.now());
|
|
3878
|
+
if (target === void 0) return;
|
|
3879
|
+
this.scheduleTarget = target;
|
|
3880
|
+
const delay = Math.max(0, Math.min(target - this.now(), MAX_TIMER_DELAY_MS));
|
|
3881
|
+
this.scheduleTimer = this.timers.timeout(() => {
|
|
3882
|
+
this.scheduleTimer = void 0;
|
|
3883
|
+
this.onScheduleFire(target);
|
|
3884
|
+
}, delay);
|
|
3885
|
+
}
|
|
3886
|
+
/**
|
|
3887
|
+
* One armed target became due. A fire landing well past its target is a
|
|
3888
|
+
* resume (suspend, throttle, forward clock jump) rather than a normal
|
|
3889
|
+
* occurrence, so it takes the recovery path instead of launching a run for a
|
|
3890
|
+
* long-stale instant.
|
|
3891
|
+
*/
|
|
3892
|
+
onScheduleFire(target) {
|
|
3008
3893
|
if (this.disposed || !this.active) return;
|
|
3009
3894
|
const now = this.now();
|
|
3010
|
-
|
|
3011
|
-
this.lastScheduleTick = now;
|
|
3895
|
+
this.scheduleTarget = void 0;
|
|
3012
3896
|
this.ledger.setScheduler({ lastTickAt: now });
|
|
3013
|
-
if (
|
|
3014
|
-
this.
|
|
3897
|
+
if (now - target > RECOVERY_TOLERANCE_MS) {
|
|
3898
|
+
this.recoverSchedule();
|
|
3015
3899
|
return;
|
|
3016
3900
|
}
|
|
3017
3901
|
for (const schedule of this.ledger.dueSchedules(now)) {
|
|
3018
3902
|
const next = nextRunAtMs(schedule.cron, schedule.nextRunAt);
|
|
3019
|
-
|
|
3020
|
-
if (opened !== void 0) this.scheduleLaunch(opened);
|
|
3903
|
+
this.dispatchRuns(this.ledger.openScheduled(schedule.taskId, next, now));
|
|
3021
3904
|
}
|
|
3905
|
+
this.armSchedule();
|
|
3022
3906
|
}
|
|
3023
3907
|
armedSchedules() {
|
|
3024
3908
|
return this.ledger.armedScheduleCount();
|
|
3025
3909
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3910
|
+
/**
|
|
3911
|
+
* Launch one run set. The root goes first and receives the run shape in its
|
|
3912
|
+
* prompt (which members this run opens, and how they run); every plain-cascade
|
|
3913
|
+
* member then launches on its own so one refused participant cannot hold the
|
|
3914
|
+
* others back. A team run's members are spawned inside the root's Lead
|
|
3915
|
+
* session instead, once that session exists.
|
|
3916
|
+
*/
|
|
3917
|
+
dispatchRuns(runs) {
|
|
3918
|
+
if (runs.length === 0) return;
|
|
3919
|
+
const root = runs.find((run) => run.dispatch !== "teammate") ?? runs[0];
|
|
3920
|
+
const others = runs.filter((run) => run !== root);
|
|
3921
|
+
this.scheduleLaunch(root, others);
|
|
3922
|
+
for (const run of others) if (run.dispatch !== "teammate") this.scheduleLaunch(run);
|
|
3923
|
+
}
|
|
3924
|
+
scheduleLaunch(opened, others = []) {
|
|
3925
|
+
this.launch(opened, others).catch((error) => {
|
|
3028
3926
|
safeConsoleError("[dsh-task-board] execution launch settlement failed", error);
|
|
3029
3927
|
});
|
|
3030
3928
|
}
|
|
@@ -3037,14 +3935,18 @@ var TaskBoardHostService = class {
|
|
|
3037
3935
|
this.pollInFlight = false;
|
|
3038
3936
|
});
|
|
3039
3937
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3938
|
+
/**
|
|
3939
|
+
* Re-arm from the ledger's current targets. Callers that just changed a
|
|
3940
|
+
* schedule (the host routes, the agent tools) invoke this after the write
|
|
3941
|
+
* commits, so a new or edited trigger arms without waiting for the next fire.
|
|
3942
|
+
*/
|
|
3943
|
+
refreshSchedule() {
|
|
3944
|
+
if (this.disposed) return;
|
|
3945
|
+
if (!this.active) {
|
|
3946
|
+
this.clearScheduleTimer();
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
this.armSchedule();
|
|
3048
3950
|
}
|
|
3049
3951
|
syncPowerReasons() {
|
|
3050
3952
|
const current = this.power.snapshot();
|
|
@@ -3066,1346 +3968,17 @@ var TaskBoardHostService = class {
|
|
|
3066
3968
|
*/
|
|
3067
3969
|
function installStreamErrorGuards() {
|
|
3068
3970
|
for (const stream of [process.stderr, process.stdout]) if (stream && typeof stream.on === "function") {
|
|
3069
|
-
if (!(typeof stream.listenerCount === "function" && stream.listenerCount("error") > 0)) stream.on("error", () => {});
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
/**
|
|
3073
|
-
* Defensively log to console.error without letting stderr write failures
|
|
3074
|
-
* (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
|
|
3075
|
-
*/
|
|
3076
|
-
function safeConsoleError(message, ...args) {
|
|
3077
|
-
try {
|
|
3078
|
-
console.error(message, ...args);
|
|
3079
|
-
} catch {}
|
|
3080
|
-
}
|
|
3081
|
-
//#endregion
|
|
3082
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
|
|
3083
|
-
/** The one Remote failure class shared by owners, the Gateway, and consumers. */
|
|
3084
|
-
/**
|
|
3085
|
-
* One Remote call failure: a real Error carrying its stable code and typed
|
|
3086
|
-
* details. Owners throw it at the failure point; the Host Gateway encodes it
|
|
3087
|
-
* onto the wire unchanged; the Client face rebuilds an instance for the
|
|
3088
|
-
* `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
|
|
3089
|
-
* Discrimination is always by `code`, never by instanceof.
|
|
3090
|
-
*/
|
|
3091
|
-
var RemoteError = class extends Error {
|
|
3092
|
-
code;
|
|
3093
|
-
details;
|
|
3094
|
-
/** Structural marker: cross-realm/bundle identification never uses instanceof. */
|
|
3095
|
-
isDSHRemoteError = true;
|
|
3096
|
-
/**
|
|
3097
|
-
* @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
|
|
3098
|
-
* @param message - human diagnostic carried across the wire.
|
|
3099
|
-
* @param details - structured payload typed by the code.
|
|
3100
|
-
* @param options - standard Error options (`cause` survives in-process only).
|
|
3101
|
-
*/
|
|
3102
|
-
constructor(code, message, details, options) {
|
|
3103
|
-
super(message, options);
|
|
3104
|
-
this.code = code;
|
|
3105
|
-
this.details = details;
|
|
3106
|
-
this.name = "RemoteError";
|
|
3107
|
-
}
|
|
3108
|
-
};
|
|
3109
|
-
/**
|
|
3110
|
-
* Remote decorators and explicit Gateway bindings backed by versioned
|
|
3111
|
-
* descriptors carried on decorated class prototypes. Strict reflection
|
|
3112
|
-
* remains a Typert compiler responsibility.
|
|
3113
|
-
* @module @deepseek-ai/dsh-typert-protocol
|
|
3114
|
-
*/
|
|
3115
|
-
const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
|
|
3116
|
-
/**
|
|
3117
|
-
* Test one generated Remote name against the Connection endpoint grammar.
|
|
3118
|
-
* @param value - namespace, method, lookup, or Context segment.
|
|
3119
|
-
* @returns whether the value can cross the shared RPC carrier unchanged.
|
|
3120
|
-
*/
|
|
3121
|
-
function isTypertRemoteSegment(value) {
|
|
3122
|
-
return value !== "." && value !== ".." && TYPERT_REMOTE_SEGMENT_PATTERN.test(value);
|
|
3123
|
-
}
|
|
3124
|
-
const REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-methods";
|
|
3125
|
-
/**
|
|
3126
|
-
* Bind one visible Service field to a Cordis key and Remote namespace. A
|
|
3127
|
-
* service that owns a Cordis Context also gives its tree `ctx.invocation`,
|
|
3128
|
-
* `undefined` outside a Remote call, so no `TypertRemoteService` is needed for
|
|
3129
|
-
* a Host composition to read it.
|
|
3130
|
-
* @param service - owning Service instance, normally `this`.
|
|
3131
|
-
* @param serviceKey - exact Cordis service key.
|
|
3132
|
-
* @param options - optional distinct wire namespace.
|
|
3133
|
-
* @returns a frozen, inspectable binding with no compiler-injected metadata.
|
|
3134
|
-
*/
|
|
3135
|
-
function bindTypertRemote(service, serviceKey, options = {}) {
|
|
3136
|
-
validateName("service key", serviceKey);
|
|
3137
|
-
const namespace = options.namespace ?? serviceKey;
|
|
3138
|
-
validateName("namespace", namespace);
|
|
3139
|
-
const ctx = Reflect.get(service, "ctx");
|
|
3140
|
-
if (ctx instanceof Context) provideInvocationAccessor(ctx);
|
|
3141
|
-
return Object.freeze({
|
|
3142
|
-
service,
|
|
3143
|
-
serviceKey,
|
|
3144
|
-
namespace
|
|
3145
|
-
});
|
|
3146
|
-
}
|
|
3147
|
-
/** Cordis Service base that exposes its registered name through Typert Gateway. */
|
|
3148
|
-
var TypertRemoteService = class extends Service {
|
|
3149
|
-
/** Visible binding consumed by the Gateway's source-mode discovery. */
|
|
3150
|
-
typertRemote;
|
|
3151
|
-
/**
|
|
3152
|
-
* Register the Service and bind the same key to Typert Gateway.
|
|
3153
|
-
* @param ctx - owning Cordis Context.
|
|
3154
|
-
* @param serviceKey - exact Cordis service key and default wire namespace.
|
|
3155
|
-
* @param options - optional distinct wire namespace.
|
|
3156
|
-
*/
|
|
3157
|
-
constructor(ctx, serviceKey, options = {}) {
|
|
3158
|
-
super(ctx, serviceKey);
|
|
3159
|
-
this.typertRemote = bindTypertRemote(this, this.name, options);
|
|
3160
|
-
}
|
|
3161
|
-
};
|
|
3162
|
-
/**
|
|
3163
|
-
* Make `ctx.invocation` read as `undefined` outside a Remote call instead of the
|
|
3164
|
-
* reflect service's "cannot get property" error; a call-derived Context shadows
|
|
3165
|
-
* the accessor with its own property. The first Remote Service constructed in a
|
|
3166
|
-
* tree registers it on the root, where it outlives any one Service.
|
|
3167
|
-
*/
|
|
3168
|
-
function provideInvocationAccessor(ctx) {
|
|
3169
|
-
if (Object.hasOwn(ctx.root.reflect.props, "invocation")) return;
|
|
3170
|
-
ctx.root.accessor("invocation", { get: () => void 0 });
|
|
3171
|
-
}
|
|
3172
|
-
function Remote(methodExportOrOptions, context) {
|
|
3173
|
-
if (typeof methodExportOrOptions === "string") {
|
|
3174
|
-
validateName("Remote export name", methodExportOrOptions);
|
|
3175
|
-
return remoteDecorator({ kind: "direct" }, void 0, methodExportOrOptions);
|
|
3176
|
-
}
|
|
3177
|
-
if (typeof methodExportOrOptions === "object") {
|
|
3178
|
-
if (remoteOptionMode(methodExportOrOptions) !== "stream" || Reflect.ownKeys(methodExportOrOptions).length !== 1) throw new TypeError("typert-protocol: Remote options must contain exactly mode: \"stream\"");
|
|
3179
|
-
return remoteDecorator({ kind: "direct" }, "stream");
|
|
3180
|
-
}
|
|
3181
|
-
if (context === void 0) throw new TypeError("typert-protocol: Remote decorator context is missing");
|
|
3182
|
-
addMarkerInitializer(context, { kind: "direct" });
|
|
3183
|
-
}
|
|
3184
|
-
function remoteOptionMode(options) {
|
|
3185
|
-
return Reflect.get(options, "mode");
|
|
3186
|
-
}
|
|
3187
|
-
function remoteDecorator(invocation, mode, exportName) {
|
|
3188
|
-
return function(_method, context) {
|
|
3189
|
-
addMarkerInitializer(context, invocation, mode, exportName);
|
|
3190
|
-
};
|
|
3191
|
-
}
|
|
3192
|
-
function readRemoteMethodDescriptor(prototype) {
|
|
3193
|
-
const property = Object.getOwnPropertyDescriptor(prototype, REMOTE_METHOD_DESCRIPTOR);
|
|
3194
|
-
if (property === void 0) return void 0;
|
|
3195
|
-
const descriptor = property.value;
|
|
3196
|
-
if (descriptor === null || typeof descriptor !== "object") throw new TypeError("typert-protocol: Remote method descriptor must be an object");
|
|
3197
|
-
const version = Reflect.get(descriptor, "version");
|
|
3198
|
-
if (version !== 1) throw new TypeError(`typert-protocol: unsupported Remote method descriptor version ${String(version)}`);
|
|
3199
|
-
const methods = Reflect.get(descriptor, "methods");
|
|
3200
|
-
if (!Array.isArray(methods)) throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
|
|
3201
|
-
return descriptor;
|
|
3202
|
-
}
|
|
3203
|
-
function addMarkerInitializer(context, invocation, mode, exportName) {
|
|
3204
|
-
if (context.private || context.static || typeof context.name !== "string") throw new TypeError("typert-protocol: Remote decorators require a public instance method with a string name");
|
|
3205
|
-
const method = context.name;
|
|
3206
|
-
context.addInitializer(function() {
|
|
3207
|
-
const prototype = Object.getPrototypeOf(this);
|
|
3208
|
-
if (prototype === null) throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`);
|
|
3209
|
-
mark(prototype, method, invocation, mode, exportName);
|
|
3210
|
-
});
|
|
3211
|
-
}
|
|
3212
|
-
function mark(prototype, method, invocation, mode, exportName) {
|
|
3213
|
-
const descriptor = readRemoteMethodDescriptor(prototype);
|
|
3214
|
-
const marker = Object.freeze({
|
|
3215
|
-
method,
|
|
3216
|
-
...exportName === void 0 || exportName === method ? {} : { exportName },
|
|
3217
|
-
...mode === void 0 ? {} : { mode },
|
|
3218
|
-
invocation: Object.freeze(invocation)
|
|
3219
|
-
});
|
|
3220
|
-
const current = descriptor?.methods.find((candidate) => candidate.method === method);
|
|
3221
|
-
if (current !== void 0) {
|
|
3222
|
-
if (current.exportName === marker.exportName && current.mode === marker.mode && sameInvocation(current.invocation, invocation)) return;
|
|
3223
|
-
throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
|
|
3224
|
-
}
|
|
3225
|
-
Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
|
|
3226
|
-
configurable: true,
|
|
3227
|
-
value: Object.freeze({
|
|
3228
|
-
version: 1,
|
|
3229
|
-
methods: Object.freeze([...descriptor?.methods ?? [], marker])
|
|
3230
|
-
})
|
|
3231
|
-
});
|
|
3232
|
-
}
|
|
3233
|
-
function sameInvocation(left, right) {
|
|
3234
|
-
if (left.kind === "direct") return right.kind === "direct";
|
|
3235
|
-
if (right.kind === "direct") return false;
|
|
3236
|
-
return left.context === right.context;
|
|
3237
|
-
}
|
|
3238
|
-
function validateName(subject, value) {
|
|
3239
|
-
if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
|
|
3240
|
-
}
|
|
3241
|
-
//#endregion
|
|
3242
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
|
|
3243
|
-
/**
|
|
3244
|
-
* Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
|
|
3245
|
-
* @param value - value to freeze.
|
|
3246
|
-
* @returns the same value after every reachable enumerable child is frozen.
|
|
3247
|
-
*/
|
|
3248
|
-
function deepFreeze(value) {
|
|
3249
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
3250
|
-
const pending = [{
|
|
3251
|
-
kind: "visit",
|
|
3252
|
-
node: value
|
|
3253
|
-
}];
|
|
3254
|
-
while (pending.length > 0) {
|
|
3255
|
-
const task = pending.pop();
|
|
3256
|
-
/* v8 ignore next -- the loop condition guarantees one pending task. */
|
|
3257
|
-
if (task === void 0) continue;
|
|
3258
|
-
if (task.kind === "property") {
|
|
3259
|
-
pending.push({
|
|
3260
|
-
kind: "visit",
|
|
3261
|
-
node: task.source[task.key]
|
|
3262
|
-
});
|
|
3263
|
-
continue;
|
|
3264
|
-
}
|
|
3265
|
-
const node = task.node;
|
|
3266
|
-
if (node === null || typeof node !== "object") continue;
|
|
3267
|
-
if (node instanceof AbortSignal) continue;
|
|
3268
|
-
if (seen.has(node)) continue;
|
|
3269
|
-
seen.add(node);
|
|
3270
|
-
Object.freeze(node);
|
|
3271
|
-
const keys = Object.keys(node);
|
|
3272
|
-
for (let index = keys.length - 1; index >= 0; index--) {
|
|
3273
|
-
const key = keys[index];
|
|
3274
|
-
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
3275
|
-
if (key === void 0) continue;
|
|
3276
|
-
pending.push({
|
|
3277
|
-
kind: "property",
|
|
3278
|
-
source: node,
|
|
3279
|
-
key
|
|
3280
|
-
});
|
|
3281
|
-
}
|
|
3282
|
-
}
|
|
3283
|
-
return value;
|
|
3284
|
-
}
|
|
3285
|
-
//#endregion
|
|
3286
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
|
|
3287
|
-
/**
|
|
3288
|
-
* Random v4 UUID, minted from `crypto.getRandomValues`.
|
|
3289
|
-
* @returns the UUID string.
|
|
3290
|
-
*/
|
|
3291
|
-
function randomUUID() {
|
|
3292
|
-
const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
3293
|
-
const hex = Array.from(bytes, (byte, index) => {
|
|
3294
|
-
return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
|
|
3295
|
-
}).join("");
|
|
3296
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
3297
|
-
}
|
|
3298
|
-
//#endregion
|
|
3299
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-brand/lib/index.js
|
|
3300
|
-
/**
|
|
3301
|
-
* Duplicate-install-safe nominal primitive helpers.
|
|
3302
|
-
*
|
|
3303
|
-
* A brand makes structurally identical strings or numbers non-interchangeable
|
|
3304
|
-
* at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
|
|
3305
|
-
* expected, and an event sequence cannot be passed as a log offset. Comparison,
|
|
3306
|
-
* logging, and serialization retain the underlying primitive behavior.
|
|
3307
|
-
*
|
|
3308
|
-
* This package owns no concrete domain value and keeps no runtime identity or mutable
|
|
3309
|
-
* state, so independently installed copies produce interchangeable values.
|
|
3310
|
-
*
|
|
3311
|
-
* @module @deepseek-ai/dsh-brand
|
|
3312
|
-
*/
|
|
3313
|
-
/**
|
|
3314
|
-
* Apply a compile-time string brand without changing the value.
|
|
3315
|
-
* @param value - string admitted by the domain that owns the target brand.
|
|
3316
|
-
* @returns the same string with the requested compile-time brand.
|
|
3317
|
-
*/
|
|
3318
|
-
function brandString(value) {
|
|
3319
|
-
return value;
|
|
3320
|
-
}
|
|
3321
|
-
//#endregion
|
|
3322
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
|
|
3323
|
-
/** Largest delay Node schedules without clamping it to one millisecond. */
|
|
3324
|
-
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
3325
|
-
//#endregion
|
|
3326
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-llm/lib/index.js
|
|
3327
|
-
/**
|
|
3328
|
-
* Detach and deep-freeze a message whose identity already exists.
|
|
3329
|
-
* @param message - complete message, including its stable identity.
|
|
3330
|
-
* @returns an immutable snapshot that preserves the identity.
|
|
3331
|
-
*/
|
|
3332
|
-
function freezeMessage(message) {
|
|
3333
|
-
return deepFreeze(structuredClone(message));
|
|
3334
|
-
}
|
|
3335
|
-
/**
|
|
3336
|
-
* Create one identified message and freeze it before publication.
|
|
3337
|
-
* @param input - complete role, content, and source for a new message.
|
|
3338
|
-
* @returns an immutable message with a fresh stable identity.
|
|
3339
|
-
*/
|
|
3340
|
-
function createMessage(input) {
|
|
3341
|
-
return deepFreeze(structuredClone({
|
|
3342
|
-
...input,
|
|
3343
|
-
id: brandString(randomUUID())
|
|
3344
|
-
}));
|
|
3345
|
-
}
|
|
3346
|
-
/**
|
|
3347
|
-
* Create one identified user-role message and freeze it before publication.
|
|
3348
|
-
* @param input - complete content and source for a new user message.
|
|
3349
|
-
* @returns an immutable user message with a fresh stable identity.
|
|
3350
|
-
*/
|
|
3351
|
-
function createUserMessage(input) {
|
|
3352
|
-
return createMessage({
|
|
3353
|
-
...input,
|
|
3354
|
-
role: "user"
|
|
3355
|
-
});
|
|
3356
|
-
}
|
|
3357
|
-
/**
|
|
3358
|
-
* Harness error base with a stable machine-routable code and chained cause.
|
|
3359
|
-
* Package errors extend it so tool results and replay can retain failure class.
|
|
3360
|
-
* @module @deepseek-ai/dsh-llm/error
|
|
3361
|
-
*/
|
|
3362
|
-
/**
|
|
3363
|
-
* Base class for all harness errors. Carries a `code` (stable, programmatic —
|
|
3364
|
-
* e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
|
|
3365
|
-
* human-readable `message`, and supports `cause` chaining via the standard
|
|
3366
|
-
* `ErrorOptions`. `name` defaults to the subclass constructor name.
|
|
3367
|
-
*/
|
|
3368
|
-
var HarnessError = class extends Error {
|
|
3369
|
-
/** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
|
|
3370
|
-
code;
|
|
3371
|
-
constructor(message, code, options) {
|
|
3372
|
-
super(message, options);
|
|
3373
|
-
this.code = code;
|
|
3374
|
-
this.name = new.target.name;
|
|
3375
|
-
}
|
|
3376
|
-
};
|
|
3377
|
-
/**
|
|
3378
|
-
* Canonical provider-neutral code for a response that completed normally but
|
|
3379
|
-
* carried no content blocks at all. Providers occasionally emit a degenerate
|
|
3380
|
-
* completion (a terminal stop with zero output); adapters classify it as this
|
|
3381
|
-
* failure instead of yielding an empty assistant message, because an empty
|
|
3382
|
-
* message silently ends the turn with nothing for the user or the loop to act
|
|
3383
|
-
* on. The attempt produced nothing durable, so retry policy treats it as safe
|
|
3384
|
-
* to repeat.
|
|
3385
|
-
*/
|
|
3386
|
-
const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
|
|
3387
|
-
new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
|
|
3388
|
-
new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
|
|
3389
|
-
new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
|
|
3390
|
-
/**
|
|
3391
|
-
* Provider-owned request-retry policy configuration and resolution.
|
|
3392
|
-
*
|
|
3393
|
-
* Adapters expose one resolved policy per registered provider route; the
|
|
3394
|
-
* optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
|
|
3395
|
-
*
|
|
3396
|
-
* @module @deepseek-ai/dsh-llm/retry-policy
|
|
3397
|
-
*/
|
|
3398
|
-
const DEFAULT_MAX_RETRIES = 5;
|
|
3399
|
-
const DEFAULT_INITIAL_DELAY_MS = 500;
|
|
3400
|
-
const DEFAULT_MAX_DELAY_MS = 1e4;
|
|
3401
|
-
const DEFAULT_JITTER_RATIO = .1;
|
|
3402
|
-
const DEFAULT_RETRYABLE_CODES = Object.freeze([
|
|
3403
|
-
EMPTY_RESPONSE_CODE,
|
|
3404
|
-
"RATE_LIMIT",
|
|
3405
|
-
"SERVER",
|
|
3406
|
-
"TIMEOUT",
|
|
3407
|
-
"TRANSPORT"
|
|
3408
|
-
]);
|
|
3409
|
-
const backoffSchema = z.object({
|
|
3410
|
-
initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
|
|
3411
|
-
maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
|
|
3412
|
-
jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
|
|
3413
|
-
});
|
|
3414
|
-
const normalPolicySchema = z.object({
|
|
3415
|
-
mode: z.const("normal").required(),
|
|
3416
|
-
maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
|
|
3417
|
-
retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
|
|
3418
|
-
backoff: backoffSchema
|
|
3419
|
-
});
|
|
3420
|
-
const alwaysPolicySchema = z.object({
|
|
3421
|
-
mode: z.const("always").required(),
|
|
3422
|
-
backoff: backoffSchema
|
|
3423
|
-
});
|
|
3424
|
-
z.union([normalPolicySchema, alwaysPolicySchema]);
|
|
3425
|
-
const NORMAL_POLICY_KEYS = /* @__PURE__ */ new Set([
|
|
3426
|
-
"mode",
|
|
3427
|
-
"maxRetries",
|
|
3428
|
-
"retryableCodes",
|
|
3429
|
-
"backoff"
|
|
3430
|
-
]);
|
|
3431
|
-
const ALWAYS_POLICY_KEYS = /* @__PURE__ */ new Set([
|
|
3432
|
-
"mode",
|
|
3433
|
-
"maxRetries",
|
|
3434
|
-
"retryableCodes",
|
|
3435
|
-
"backoff"
|
|
3436
|
-
]);
|
|
3437
|
-
const BACKOFF_KEYS = /* @__PURE__ */ new Set([
|
|
3438
|
-
"initialDelayMs",
|
|
3439
|
-
"maxDelayMs",
|
|
3440
|
-
"jitterRatio"
|
|
3441
|
-
]);
|
|
3442
|
-
function validateKeys(value, allowed, path) {
|
|
3443
|
-
for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${path}: unknown key "${key}"`);
|
|
3444
|
-
}
|
|
3445
|
-
function resolveBackoff(config, path) {
|
|
3446
|
-
if (config !== void 0) validateKeys(config, BACKOFF_KEYS, path);
|
|
3447
|
-
const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
|
|
3448
|
-
const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
3449
|
-
const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
|
|
3450
|
-
if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > 2147483647) throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
3451
|
-
if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > 2147483647) throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
3452
|
-
if (initialDelayMs > maxDelayMs) throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
|
|
3453
|
-
if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) throw new Error(`${path}.jitterRatio must be between 0 and 1`);
|
|
3454
|
-
return Object.freeze({
|
|
3455
|
-
initialDelayMs,
|
|
3456
|
-
maxDelayMs,
|
|
3457
|
-
jitterRatio
|
|
3458
|
-
});
|
|
3459
|
-
}
|
|
3460
|
-
/**
|
|
3461
|
-
* Validate, default, and detach one provider-owned retry policy.
|
|
3462
|
-
* @param config - optional provider configuration; omission selects normal defaults.
|
|
3463
|
-
* @param path - diagnostic path naming the provider config that owns the value.
|
|
3464
|
-
* @returns an immutable policy safe to capture in provider registration state.
|
|
3465
|
-
*/
|
|
3466
|
-
function resolveRetryPolicy(config, path) {
|
|
3467
|
-
if (config === void 0) return Object.freeze({
|
|
3468
|
-
mode: "normal",
|
|
3469
|
-
maxRetries: DEFAULT_MAX_RETRIES,
|
|
3470
|
-
retryableCodes: DEFAULT_RETRYABLE_CODES,
|
|
3471
|
-
...resolveBackoff(void 0, `${path}.backoff`)
|
|
3472
|
-
});
|
|
3473
|
-
switch (config.mode) {
|
|
3474
|
-
case "normal": {
|
|
3475
|
-
validateKeys(config, NORMAL_POLICY_KEYS, path);
|
|
3476
|
-
const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
3477
|
-
const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
|
|
3478
|
-
if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
|
|
3479
|
-
if (retryableCodes.length === 0) throw new Error(`${path}.retryableCodes must not be empty`);
|
|
3480
|
-
if (retryableCodes.some((code) => typeof code !== "string" || code.length === 0)) throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
|
|
3481
|
-
if (new Set(retryableCodes).size !== retryableCodes.length) throw new Error(`${path}.retryableCodes must not contain duplicates`);
|
|
3482
|
-
return Object.freeze({
|
|
3483
|
-
mode: "normal",
|
|
3484
|
-
maxRetries,
|
|
3485
|
-
retryableCodes: Object.freeze([...retryableCodes]),
|
|
3486
|
-
...resolveBackoff(config.backoff, `${path}.backoff`)
|
|
3487
|
-
});
|
|
3488
|
-
}
|
|
3489
|
-
case "always":
|
|
3490
|
-
validateKeys(config, ALWAYS_POLICY_KEYS, path);
|
|
3491
|
-
return Object.freeze({
|
|
3492
|
-
mode: "always",
|
|
3493
|
-
...resolveBackoff(config.backoff, `${path}.backoff`)
|
|
3494
|
-
});
|
|
3495
|
-
default: throw new Error(`${path}.mode must be "normal" or "always"`);
|
|
3496
|
-
}
|
|
3497
|
-
}
|
|
3498
|
-
/**
|
|
3499
|
-
* Field-wise equality over {@link LlmCallConfig} — the comparison a caller
|
|
3500
|
-
* runs to decide whether a proposed configuration is a real change (worth a
|
|
3501
|
-
* logged header snapshot) or the held one restated.
|
|
3502
|
-
* @param a - one configuration.
|
|
3503
|
-
* @param b - the other.
|
|
3504
|
-
* @returns whether every field (including the `stop` list, element-wise) matches.
|
|
3505
|
-
*/
|
|
3506
|
-
function callConfigEquals(a, b) {
|
|
3507
|
-
if (a.provider !== b.provider || a.model !== b.model || a.reasoningEffort !== b.reasoningEffort || a.temperature !== b.temperature || a.maxTokens !== b.maxTokens) return false;
|
|
3508
|
-
if (a.stop === void 0 || b.stop === void 0) return a.stop === b.stop;
|
|
3509
|
-
return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
|
|
3510
|
-
}
|
|
3511
|
-
/**
|
|
3512
|
-
* Normalization for values thrown by a final LLM adapter boundary.
|
|
3513
|
-
*
|
|
3514
|
-
* @module @deepseek-ai/dsh-llm/adapter-failure
|
|
3515
|
-
*/
|
|
3516
|
-
/**
|
|
3517
|
-
* Detach serializable provider facts from a value thrown by an adapter.
|
|
3518
|
-
* @param value - arbitrary value thrown during adapter dispatch or iteration.
|
|
3519
|
-
* @returns immutable provider-neutral facts suitable for a terminal finish chunk.
|
|
3520
|
-
* @internal
|
|
3521
|
-
*/
|
|
3522
|
-
function normalizeLlmFailure(value) {
|
|
3523
|
-
const error = value instanceof Error ? value : new HarnessError(thrownMessage(value), "UNKNOWN", { cause: value });
|
|
3524
|
-
const carried = ownFailureSnapshot(error);
|
|
3525
|
-
if (carried !== void 0 && carried.code === ownErrorCode(error)) return carried;
|
|
3526
|
-
return Object.freeze({
|
|
3527
|
-
message: errorMessage(error),
|
|
3528
|
-
code: harnessErrorCode(error)
|
|
3529
|
-
});
|
|
3530
|
-
}
|
|
3531
|
-
/** Render a non-Error throw without letting hostile coercion escape normalization. */
|
|
3532
|
-
function thrownMessage(value) {
|
|
3533
|
-
try {
|
|
3534
|
-
const message = String(value);
|
|
3535
|
-
return message.length > 0 ? message : "LLM adapter failed";
|
|
3536
|
-
} catch (_hostileThrownValue) {
|
|
3537
|
-
return "LLM adapter failed";
|
|
3538
|
-
}
|
|
3539
|
-
}
|
|
3540
|
-
/** Read a foreign error's own data-backed `code` without invoking accessors. */
|
|
3541
|
-
function ownErrorCode(error) {
|
|
3542
|
-
try {
|
|
3543
|
-
const descriptor = Object.getOwnPropertyDescriptor(error, "code");
|
|
3544
|
-
return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
|
|
3545
|
-
} catch (_sdkPropertyTrap) {
|
|
3546
|
-
return;
|
|
3547
|
-
}
|
|
3548
|
-
}
|
|
3549
|
-
/** Snapshot an own data property without invoking an SDK-defined accessor. */
|
|
3550
|
-
function ownFailureSnapshot(error) {
|
|
3551
|
-
try {
|
|
3552
|
-
const descriptor = Object.getOwnPropertyDescriptor(error, "failure");
|
|
3553
|
-
return descriptor !== void 0 && "value" in descriptor ? failureSnapshot(descriptor.value) : void 0;
|
|
3554
|
-
} catch (_sdkPropertyTrap) {
|
|
3555
|
-
return;
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
/** Validate and detach an arbitrary serializable failure payload. */
|
|
3559
|
-
function failureSnapshot(value) {
|
|
3560
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
3561
|
-
try {
|
|
3562
|
-
const candidate = value;
|
|
3563
|
-
const message = candidate.message;
|
|
3564
|
-
const code = candidate.code;
|
|
3565
|
-
const status = candidate.status;
|
|
3566
|
-
const providerRetryAfterMs = candidate.providerRetryAfterMs;
|
|
3567
|
-
const requestId = candidate.requestId;
|
|
3568
|
-
const offloadImages = candidate.offloadImages;
|
|
3569
|
-
if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0) || offloadImages !== void 0 && (!Number.isSafeInteger(offloadImages) || offloadImages <= 0)) return void 0;
|
|
3570
|
-
return Object.freeze({
|
|
3571
|
-
message,
|
|
3572
|
-
code,
|
|
3573
|
-
...status === void 0 ? {} : { status },
|
|
3574
|
-
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
|
|
3575
|
-
...requestId === void 0 ? {} : { requestId },
|
|
3576
|
-
...offloadImages === void 0 ? {} : { offloadImages }
|
|
3577
|
-
});
|
|
3578
|
-
} catch (_sdkFailureGetter) {
|
|
3579
|
-
return;
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
/** Read an SDK error message without letting an accessor replace the primary failure. */
|
|
3583
|
-
function errorMessage(error) {
|
|
3584
|
-
try {
|
|
3585
|
-
const message = error.message;
|
|
3586
|
-
if (typeof message === "string" && message.length > 0) return message;
|
|
3587
|
-
} catch (_sdkMessageGetter) {}
|
|
3588
|
-
return "LLM adapter failed";
|
|
3589
|
-
}
|
|
3590
|
-
/** Trust only Harness-owned codes; third-party SDK codes are not our taxonomy. */
|
|
3591
|
-
function harnessErrorCode(error) {
|
|
3592
|
-
return error instanceof HarnessError ? error.code : "UNKNOWN";
|
|
3593
|
-
}
|
|
3594
|
-
function quoted(value) {
|
|
3595
|
-
return JSON.stringify(value);
|
|
3596
|
-
}
|
|
3597
|
-
/**
|
|
3598
|
-
* Stable text shown to a model that cannot accept one durable image reference.
|
|
3599
|
-
* @param ref - durable normalized attachment omitted from the request.
|
|
3600
|
-
* @returns deterministic text-only placeholder.
|
|
3601
|
-
*/
|
|
3602
|
-
function textOnlyImageText(ref) {
|
|
3603
|
-
return `[image omitted because this model accepts text only; attachment sha256:${String(ref.attachmentId).slice(7, 15)}]`;
|
|
3604
|
-
}
|
|
3605
|
-
/**
|
|
3606
|
-
* True when typed model content contains an image block. This is the one image
|
|
3607
|
-
* walk shared by every image policy (capability gating, text-only
|
|
3608
|
-
* serialization, compaction survey), so a consumer cannot silently diverge.
|
|
3609
|
-
* @param content - typed model content blocks.
|
|
3610
|
-
* @returns whether any block is an image.
|
|
3611
|
-
*/
|
|
3612
|
-
function contentHasImage(content) {
|
|
3613
|
-
return content.some((block) => block.type === "image");
|
|
3614
|
-
}
|
|
3615
|
-
/**
|
|
3616
|
-
* True when typed model content contains a file block.
|
|
3617
|
-
* Reads current content on every call without retaining scan results.
|
|
3618
|
-
* @param content - typed model content blocks.
|
|
3619
|
-
* @returns whether any block is a file.
|
|
3620
|
-
*/
|
|
3621
|
-
function contentHasFile(content) {
|
|
3622
|
-
for (const block of content) if (block.type === "file") return true;
|
|
3623
|
-
return false;
|
|
3624
|
-
}
|
|
3625
|
-
/**
|
|
3626
|
-
* Stable model-facing handle for one durable file reference: the address of
|
|
3627
|
-
* the verbatim stored copy and the instruction to read it on demand. This is
|
|
3628
|
-
* the only representation a provider ever receives for a file.
|
|
3629
|
-
* @param ref - durable verbatim file reference.
|
|
3630
|
-
* @param readonlyPath - execution-world path of the stored copy, when resolvable.
|
|
3631
|
-
* @returns deterministic handle text naming the file, its size, and its address.
|
|
3632
|
-
*/
|
|
3633
|
-
function fileHandleText(ref, readonlyPath) {
|
|
3634
|
-
const digest = String(ref.attachmentId).slice(7, 15);
|
|
3635
|
-
const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
|
|
3636
|
-
if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
|
|
3637
|
-
return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
|
|
3638
|
-
}
|
|
3639
|
-
/** Replace every file occurrence with handle text. */
|
|
3640
|
-
function replaceFilesWithHandles(blocks, resolvePath) {
|
|
3641
|
-
let next;
|
|
3642
|
-
for (const [index, block] of blocks.entries()) {
|
|
3643
|
-
if (block.type === "file") {
|
|
3644
|
-
next ??= blocks.slice(0, index);
|
|
3645
|
-
next.push({
|
|
3646
|
-
type: "text",
|
|
3647
|
-
text: fileHandleText(block.attachment, resolvePath(block.attachment))
|
|
3648
|
-
});
|
|
3649
|
-
continue;
|
|
3650
|
-
}
|
|
3651
|
-
next?.push(block);
|
|
3652
|
-
}
|
|
3653
|
-
return next ?? blocks;
|
|
3654
|
-
}
|
|
3655
|
-
function projectFilesToText(messages, resolvePath) {
|
|
3656
|
-
if (!messages.some((message) => contentHasFile(message.content))) return messages;
|
|
3657
|
-
return messages.map((message) => {
|
|
3658
|
-
const content = replaceFilesWithHandles(message.content, resolvePath);
|
|
3659
|
-
return content === message.content ? message : {
|
|
3660
|
-
...message,
|
|
3661
|
-
content
|
|
3662
|
-
};
|
|
3663
|
-
});
|
|
3664
|
-
}
|
|
3665
|
-
/** Replace every image occurrence for a text-only model. */
|
|
3666
|
-
function replaceImagesForTextModel(blocks) {
|
|
3667
|
-
let next;
|
|
3668
|
-
for (const [index, block] of blocks.entries()) {
|
|
3669
|
-
if (block.type === "image") {
|
|
3670
|
-
next ??= blocks.slice(0, index);
|
|
3671
|
-
next.push({
|
|
3672
|
-
type: "text",
|
|
3673
|
-
text: textOnlyImageText(block.attachment)
|
|
3674
|
-
});
|
|
3675
|
-
continue;
|
|
3676
|
-
}
|
|
3677
|
-
next?.push(block);
|
|
3678
|
-
}
|
|
3679
|
-
return next ?? blocks;
|
|
3680
|
-
}
|
|
3681
|
-
function projectImagesForTextModel(messages) {
|
|
3682
|
-
if (!messages.some((message) => contentHasImage(message.content))) return messages;
|
|
3683
|
-
return messages.map((message) => {
|
|
3684
|
-
const content = replaceImagesForTextModel(message.content);
|
|
3685
|
-
return content === message.content ? message : {
|
|
3686
|
-
...message,
|
|
3687
|
-
content
|
|
3688
|
-
};
|
|
3689
|
-
});
|
|
3690
|
-
}
|
|
3691
|
-
/**
|
|
3692
|
-
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
|
3693
|
-
* adapters from drifting. See
|
|
3694
|
-
* `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
|
3695
|
-
*
|
|
3696
|
-
* App-attribution vocabulary for provider requests.
|
|
3697
|
-
* @module @deepseek-ai/dsh-llm/attribution
|
|
3698
|
-
*/
|
|
3699
|
-
const { version } = createRequire(import.meta.url)("../package.json");
|
|
3700
|
-
/**
|
|
3701
|
-
* LLM service: adapter registry with a waterfall-interceptable streaming call
|
|
3702
|
-
* API. Exports the `LlmRuntime` default, the abstract `LlmAdapter` for
|
|
3703
|
-
* provider backends, and `BlockAssembler` for chunk assembly.
|
|
3704
|
-
*
|
|
3705
|
-
* @module @deepseek-ai/dsh-llm
|
|
3706
|
-
*/
|
|
3707
|
-
var __runInitializers = function(thisArg, initializers, value) {
|
|
3708
|
-
var useValue = arguments.length > 2;
|
|
3709
|
-
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
3710
|
-
return useValue ? value : void 0;
|
|
3711
|
-
};
|
|
3712
|
-
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
3713
|
-
function accept(f) {
|
|
3714
|
-
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
3715
|
-
return f;
|
|
3716
|
-
}
|
|
3717
|
-
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
3718
|
-
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
3719
|
-
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
3720
|
-
var _, done = false;
|
|
3721
|
-
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
3722
|
-
var context = {};
|
|
3723
|
-
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
3724
|
-
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
3725
|
-
context.addInitializer = function(f) {
|
|
3726
|
-
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
3727
|
-
extraInitializers.push(accept(f || null));
|
|
3728
|
-
};
|
|
3729
|
-
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
3730
|
-
get: descriptor.get,
|
|
3731
|
-
set: descriptor.set
|
|
3732
|
-
} : descriptor[key], context);
|
|
3733
|
-
if (kind === "accessor") {
|
|
3734
|
-
if (result === void 0) continue;
|
|
3735
|
-
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
3736
|
-
if (_ = accept(result.get)) descriptor.get = _;
|
|
3737
|
-
if (_ = accept(result.set)) descriptor.set = _;
|
|
3738
|
-
if (_ = accept(result.init)) initializers.unshift(_);
|
|
3739
|
-
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
3740
|
-
else descriptor[key] = _;
|
|
3741
|
-
}
|
|
3742
|
-
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
3743
|
-
done = true;
|
|
3744
|
-
};
|
|
3745
|
-
/**
|
|
3746
|
-
* Typed error for LLM-related failures. Extends {@link HarnessError}, so the
|
|
3747
|
-
* `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
|
|
3748
|
-
*/
|
|
3749
|
-
var LlmError = class extends HarnessError {
|
|
3750
|
-
/** Serializable facts retained beside this live Error. */
|
|
3751
|
-
failure;
|
|
3752
|
-
/**
|
|
3753
|
-
* @param message - non-empty human-readable failure summary.
|
|
3754
|
-
* @param code - non-empty stable provider-neutral machine code.
|
|
3755
|
-
* @param options - optional cause and validated serializable provider facts.
|
|
3756
|
-
*/
|
|
3757
|
-
constructor(message, code, options) {
|
|
3758
|
-
if (typeof message !== "string" || message.length === 0) throw new Error("LlmError message must be a non-empty string");
|
|
3759
|
-
if (typeof code !== "string" || code.length === 0) throw new Error("LlmError code must be a non-empty string");
|
|
3760
|
-
if (options?.status !== void 0 && (!Number.isInteger(options.status) || options.status < 100 || options.status > 599)) throw new Error("LlmError status must be an integer from 100 through 599");
|
|
3761
|
-
if (options?.providerRetryAfterMs !== void 0 && (!Number.isFinite(options.providerRetryAfterMs) || options.providerRetryAfterMs <= 0)) throw new Error("LlmError providerRetryAfterMs must be a positive finite number");
|
|
3762
|
-
if (options?.requestId !== void 0 && (typeof options.requestId !== "string" || options.requestId.length === 0)) throw new Error("LlmError requestId must be a non-empty string");
|
|
3763
|
-
super(message, code, options);
|
|
3764
|
-
this.name = "LlmError";
|
|
3765
|
-
this.failure = Object.freeze({
|
|
3766
|
-
message,
|
|
3767
|
-
code,
|
|
3768
|
-
...options?.status === void 0 ? {} : { status: options.status },
|
|
3769
|
-
...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
|
|
3770
|
-
...options?.requestId === void 0 ? {} : { requestId: options.requestId },
|
|
3771
|
-
...options?.offloadImages === void 0 ? {} : { offloadImages: options.offloadImages }
|
|
3772
|
-
});
|
|
3773
|
-
}
|
|
3774
|
-
};
|
|
3775
|
-
(() => {
|
|
3776
|
-
let _classSuper = TypertRemoteService;
|
|
3777
|
-
let _instanceExtraInitializers = [];
|
|
3778
|
-
let _listProviders_decorators;
|
|
3779
|
-
let _listConfigurableProviders_decorators;
|
|
3780
|
-
let _remoteDiscoverModels_decorators;
|
|
3781
|
-
return class LlmRuntime extends _classSuper {
|
|
3782
|
-
static {
|
|
3783
|
-
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
3784
|
-
_listProviders_decorators = [Remote];
|
|
3785
|
-
_listConfigurableProviders_decorators = [Remote];
|
|
3786
|
-
_remoteDiscoverModels_decorators = [Remote("discoverModels")];
|
|
3787
|
-
__esDecorate(this, null, _listProviders_decorators, {
|
|
3788
|
-
kind: "method",
|
|
3789
|
-
name: "listProviders",
|
|
3790
|
-
static: false,
|
|
3791
|
-
private: false,
|
|
3792
|
-
access: {
|
|
3793
|
-
has: (obj) => "listProviders" in obj,
|
|
3794
|
-
get: (obj) => obj.listProviders
|
|
3795
|
-
},
|
|
3796
|
-
metadata: _metadata
|
|
3797
|
-
}, null, _instanceExtraInitializers);
|
|
3798
|
-
__esDecorate(this, null, _listConfigurableProviders_decorators, {
|
|
3799
|
-
kind: "method",
|
|
3800
|
-
name: "listConfigurableProviders",
|
|
3801
|
-
static: false,
|
|
3802
|
-
private: false,
|
|
3803
|
-
access: {
|
|
3804
|
-
has: (obj) => "listConfigurableProviders" in obj,
|
|
3805
|
-
get: (obj) => obj.listConfigurableProviders
|
|
3806
|
-
},
|
|
3807
|
-
metadata: _metadata
|
|
3808
|
-
}, null, _instanceExtraInitializers);
|
|
3809
|
-
__esDecorate(this, null, _remoteDiscoverModels_decorators, {
|
|
3810
|
-
kind: "method",
|
|
3811
|
-
name: "remoteDiscoverModels",
|
|
3812
|
-
static: false,
|
|
3813
|
-
private: false,
|
|
3814
|
-
access: {
|
|
3815
|
-
has: (obj) => "remoteDiscoverModels" in obj,
|
|
3816
|
-
get: (obj) => obj.remoteDiscoverModels
|
|
3817
|
-
},
|
|
3818
|
-
metadata: _metadata
|
|
3819
|
-
}, null, _instanceExtraInitializers);
|
|
3820
|
-
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
3821
|
-
enumerable: true,
|
|
3822
|
-
configurable: true,
|
|
3823
|
-
writable: true,
|
|
3824
|
-
value: _metadata
|
|
3825
|
-
});
|
|
3826
|
-
}
|
|
3827
|
-
adapters = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Map());
|
|
3828
|
-
directory = /* @__PURE__ */ new Map();
|
|
3829
|
-
discoveries = /* @__PURE__ */ new Map();
|
|
3830
|
-
constructor(ctx) {
|
|
3831
|
-
super(ctx, "llm");
|
|
3832
|
-
}
|
|
3833
|
-
/** Notify topology observers without letting one broken listener veto the commit. */
|
|
3834
|
-
emitAdaptersUpdated() {
|
|
3835
|
-
let invariantFailure;
|
|
3836
|
-
for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
|
|
3837
|
-
const returned = listener();
|
|
3838
|
-
if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
|
|
3839
|
-
this.warnAdaptersListenerFailure(error);
|
|
3840
|
-
});
|
|
3841
|
-
} catch (error) {
|
|
3842
|
-
if (error?.code === "INVARIANT") {
|
|
3843
|
-
invariantFailure ??= error;
|
|
3844
|
-
continue;
|
|
3845
|
-
}
|
|
3846
|
-
this.warnAdaptersListenerFailure(error);
|
|
3847
|
-
}
|
|
3848
|
-
if (invariantFailure !== void 0) throw invariantFailure;
|
|
3849
|
-
}
|
|
3850
|
-
/** Contained-listener diagnostic shared by the sync and async failure paths. */
|
|
3851
|
-
warnAdaptersListenerFailure(error) {
|
|
3852
|
-
this.ctx.logger.warn("llm: an llm/adapters-updated listener failed");
|
|
3853
|
-
this.ctx.logger.warn(error);
|
|
3854
|
-
}
|
|
3855
|
-
/**
|
|
3856
|
-
* Register an adapter for the given provider routes. Throws `LlmError` with code
|
|
3857
|
-
* `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
|
|
3858
|
-
* Disposed with the fiber.
|
|
3859
|
-
* @param providers - every provider route this adapter should serve.
|
|
3860
|
-
* @param adapter - the adapter that streams calls for those providers.
|
|
3861
|
-
* @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
|
|
3862
|
-
*/
|
|
3863
|
-
registerAdapter(providers, adapter) {
|
|
3864
|
-
const owned = /* @__PURE__ */ new Set();
|
|
3865
|
-
let released = false;
|
|
3866
|
-
const dispose = this.ctx.effect(function* () {
|
|
3867
|
-
if (providers.length === 0) throw new LlmError("an adapter must register at least one provider", "INVALID_ADAPTER");
|
|
3868
|
-
this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
|
|
3869
|
-
yield () => {
|
|
3870
|
-
released = true;
|
|
3871
|
-
for (const provider of owned) this.adapters.delete(provider);
|
|
3872
|
-
owned.clear();
|
|
3873
|
-
this.emitAdaptersUpdated();
|
|
3874
|
-
};
|
|
3875
|
-
}.bind(this), "llm.registerAdapter()");
|
|
3876
|
-
const handle = (() => void dispose());
|
|
3877
|
-
handle.replace = (next) => {
|
|
3878
|
-
if (released) throw new LlmError("a disposed adapter registration cannot replace its routes", "REGISTRATION_DISPOSED");
|
|
3879
|
-
this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
|
|
3880
|
-
};
|
|
3881
|
-
return handle;
|
|
3882
|
-
}
|
|
3883
|
-
/**
|
|
3884
|
-
* Validate one candidate route set for `adapter`, treating routes this
|
|
3885
|
-
* registration already holds as available. Nothing is mutated: a rejected
|
|
3886
|
-
* candidate leaves the registry exactly as it was.
|
|
3887
|
-
*/
|
|
3888
|
-
prepareRoutes(providers, adapter, owned) {
|
|
3889
|
-
const unique = /* @__PURE__ */ new Set();
|
|
3890
|
-
const registrations = [];
|
|
3891
|
-
for (const provider of providers) {
|
|
3892
|
-
if (provider.length === 0) throw new LlmError("adapter provider names must be non-empty", "INVALID_ADAPTER");
|
|
3893
|
-
if (unique.has(provider) || this.adapters.has(provider) && !owned.has(provider)) throw new LlmError(`an adapter for provider "${provider}" is already registered`, "DUPLICATE_ADAPTER");
|
|
3894
|
-
const info = adapter.providerInfo(provider);
|
|
3895
|
-
if (typeof info.id !== "string" || info.id !== provider || typeof info.name !== "string" || info.name.length === 0) throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, "INVALID_ADAPTER");
|
|
3896
|
-
unique.add(provider);
|
|
3897
|
-
const retryPolicy = adapter.providerRetryPolicy(provider) ?? resolveRetryPolicy(void 0, `llm: provider "${provider}" retryPolicy`);
|
|
3898
|
-
registrations.push({
|
|
3899
|
-
adapter,
|
|
3900
|
-
provider: {
|
|
3901
|
-
id: info.id,
|
|
3902
|
-
name: info.name
|
|
3903
|
-
},
|
|
3904
|
-
retryPolicy
|
|
3905
|
-
});
|
|
3906
|
-
}
|
|
3907
|
-
return registrations;
|
|
3908
|
-
}
|
|
3909
|
-
/**
|
|
3910
|
-
* Swap this registration's routes for the prepared ones in one synchronous
|
|
3911
|
-
* section, so no observer can see the registry between the release and the
|
|
3912
|
-
* re-registration. The route set's one mutation point is also where
|
|
3913
|
-
* `llm/adapters-updated` is published, so a `replace` announces itself
|
|
3914
|
-
* exactly like a first registration.
|
|
3915
|
-
*/
|
|
3916
|
-
commitRoutes(owned, registrations) {
|
|
3917
|
-
for (const provider of owned) this.adapters.delete(provider);
|
|
3918
|
-
owned.clear();
|
|
3919
|
-
for (const registration of registrations) {
|
|
3920
|
-
this.adapters.set(registration.provider.id, registration);
|
|
3921
|
-
owned.add(registration.provider.id);
|
|
3922
|
-
}
|
|
3923
|
-
this.emitAdaptersUpdated();
|
|
3924
|
-
}
|
|
3925
|
-
/**
|
|
3926
|
-
* Describe provider routes with a registered adapter.
|
|
3927
|
-
* @returns detached provider metadata in registration order.
|
|
3928
|
-
*/
|
|
3929
|
-
listProviders() {
|
|
3930
|
-
return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
|
|
3931
|
-
}
|
|
3932
|
-
/**
|
|
3933
|
-
* Declare provider routes an adapter plugin can activate through
|
|
3934
|
-
* configuration. Registration is all-or-nothing: an empty list, invalid
|
|
3935
|
-
* entry, or a provider already declared by any registration throws
|
|
3936
|
-
* `LlmError` without registering the rest. Disposed with the fiber.
|
|
3937
|
-
* @param entries - every configurable provider this plugin owns.
|
|
3938
|
-
* @returns a handle that withdraws all of them, and can atomically replace them.
|
|
3939
|
-
*/
|
|
3940
|
-
registerConfigurableProviders(entries) {
|
|
3941
|
-
let held = [];
|
|
3942
|
-
let disposed = false;
|
|
3943
|
-
/**
|
|
3944
|
-
* Validate a candidate set in full against everything this registration
|
|
3945
|
-
* does not already hold, then publish it. Nothing is written until the
|
|
3946
|
-
* whole set passes, so a refused candidate leaves the current entries in
|
|
3947
|
-
* place — the property that makes `replace` a swap rather than a
|
|
3948
|
-
* delete-then-add that can strand the directory empty.
|
|
3949
|
-
*/
|
|
3950
|
-
const commit = (candidates) => {
|
|
3951
|
-
const detached = [];
|
|
3952
|
-
const own = new Set(held.map((entry) => entry.provider));
|
|
3953
|
-
for (const entry of candidates) {
|
|
3954
|
-
if (entry.provider.length === 0 || entry.displayName.length === 0 || entry.settingsNs.length === 0) throw new LlmError("configurable providers need a non-empty provider, displayName, and settingsNs", "INVALID_DIRECTORY");
|
|
3955
|
-
if (entry.settingsPath.some((segment) => segment.length === 0)) throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, "INVALID_DIRECTORY");
|
|
3956
|
-
if (this.directory.has(entry.provider) && !own.has(entry.provider) || detached.some((seen) => seen.provider === entry.provider)) throw new LlmError(`configurable provider "${entry.provider}" is already declared`, "DUPLICATE_DIRECTORY");
|
|
3957
|
-
detached.push({
|
|
3958
|
-
...entry,
|
|
3959
|
-
settingsPath: [...entry.settingsPath]
|
|
3960
|
-
});
|
|
3961
|
-
}
|
|
3962
|
-
for (const entry of held) this.directory.delete(entry.provider);
|
|
3963
|
-
for (const entry of detached) this.directory.set(entry.provider, entry);
|
|
3964
|
-
held = detached;
|
|
3965
|
-
this.emitAdaptersUpdated();
|
|
3966
|
-
};
|
|
3967
|
-
const dispose = this.ctx.effect(function* () {
|
|
3968
|
-
if (entries.length === 0) throw new LlmError("a configurable-provider registration must declare at least one provider", "INVALID_DIRECTORY");
|
|
3969
|
-
commit(entries);
|
|
3970
|
-
yield () => {
|
|
3971
|
-
disposed = true;
|
|
3972
|
-
for (const entry of held) this.directory.delete(entry.provider);
|
|
3973
|
-
held = [];
|
|
3974
|
-
this.emitAdaptersUpdated();
|
|
3975
|
-
};
|
|
3976
|
-
}.bind(this), "llm.registerConfigurableProviders()");
|
|
3977
|
-
const handle = (() => void dispose());
|
|
3978
|
-
handle.replace = (next) => {
|
|
3979
|
-
if (disposed) throw new LlmError("this configurable-provider registration was disposed", "REGISTRATION_DISPOSED");
|
|
3980
|
-
commit(next);
|
|
3981
|
-
};
|
|
3982
|
-
return handle;
|
|
3983
|
-
}
|
|
3984
|
-
/**
|
|
3985
|
-
* List every declared configurable provider, registered or dormant.
|
|
3986
|
-
* @returns detached directory entries in declaration order.
|
|
3987
|
-
*/
|
|
3988
|
-
listConfigurableProviders() {
|
|
3989
|
-
return [...this.directory.values()].map((entry) => ({
|
|
3990
|
-
...entry,
|
|
3991
|
-
settingsPath: [...entry.settingsPath]
|
|
3992
|
-
}));
|
|
3993
|
-
}
|
|
3994
|
-
/**
|
|
3995
|
-
* Offer to interrogate provider endpoints on behalf of the settings
|
|
3996
|
-
* namespace this plugin owns. The namespace is the key because that is what
|
|
3997
|
-
* a configuration surface already holds from the configurable-provider
|
|
3998
|
-
* directory, and because a provider being *added* has no route to name yet.
|
|
3999
|
-
* Disposed with the fiber.
|
|
4000
|
-
* @param settingsNs - the namespace whose profiles this discovery serves.
|
|
4001
|
-
* @param discover - interrogates one endpoint and must honor the supplied signal.
|
|
4002
|
-
* @returns the disposer that withdraws the offer.
|
|
4003
|
-
*/
|
|
4004
|
-
registerModelDiscovery(settingsNs, discover) {
|
|
4005
|
-
const dispose = this.ctx.effect(function* () {
|
|
4006
|
-
if (settingsNs.length === 0) throw new LlmError("model discovery needs a non-empty settings namespace", "INVALID_DISCOVERY");
|
|
4007
|
-
if (this.discoveries.has(settingsNs)) throw new LlmError(`model discovery for "${settingsNs}" is already registered`, "DUPLICATE_DISCOVERY");
|
|
4008
|
-
this.discoveries.set(settingsNs, discover);
|
|
4009
|
-
yield () => {
|
|
4010
|
-
this.discoveries.delete(settingsNs);
|
|
4011
|
-
};
|
|
4012
|
-
}.bind(this), "llm.registerModelDiscovery()");
|
|
4013
|
-
return () => void dispose();
|
|
4014
|
-
}
|
|
4015
|
-
/**
|
|
4016
|
-
* Interrogate one provider endpoint for the models it advertises. The
|
|
4017
|
-
* request describes a draft, not a stored route, so nothing here reads or
|
|
4018
|
-
* writes settings or credentials — the caller owns both, and the reply is
|
|
4019
|
-
* candidate metadata a surface may offer for adoption.
|
|
4020
|
-
* @param settingsNs - namespace whose registered discovery serves this draft.
|
|
4021
|
-
* @param request - the endpoint, protocol, and one-shot credential to use.
|
|
4022
|
-
* @param signal - caller cancellation.
|
|
4023
|
-
* @returns the advertised models, deduplicated in endpoint order.
|
|
4024
|
-
*/
|
|
4025
|
-
async discoverModels(settingsNs, request, signal) {
|
|
4026
|
-
const discover = this.discoveries.get(settingsNs);
|
|
4027
|
-
if (discover === void 0) throw new LlmError(`no model discovery is registered for "${settingsNs}"`, "NO_DISCOVERY");
|
|
4028
|
-
if ((request.provider ?? "").length === 0 && (request.baseURL ?? "").length === 0) throw new LlmError("model discovery needs a provider route or a baseURL", "INVALID_DISCOVERY");
|
|
4029
|
-
const discovered = signal === void 0 ? await discover(request) : await discover(request, signal);
|
|
4030
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4031
|
-
const models = [];
|
|
4032
|
-
for (const model of discovered) {
|
|
4033
|
-
if (typeof model.id !== "string" || model.id.length === 0 || seen.has(model.id)) continue;
|
|
4034
|
-
seen.add(model.id);
|
|
4035
|
-
models.push({
|
|
4036
|
-
id: model.id,
|
|
4037
|
-
...model.name === void 0 ? {} : { name: model.name },
|
|
4038
|
-
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
4039
|
-
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
|
|
4040
|
-
...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
|
|
4041
|
-
});
|
|
4042
|
-
}
|
|
4043
|
-
return models;
|
|
4044
|
-
}
|
|
4045
|
-
/**
|
|
4046
|
-
* Remote adapter for one draft provider interrogation.
|
|
4047
|
-
* @param settingsNs - namespace whose registered discovery serves this draft.
|
|
4048
|
-
* @param request - endpoint, protocol, and one-shot credential to use.
|
|
4049
|
-
* @param signal - caller cancellation supplied by the Remote carrier.
|
|
4050
|
-
* @returns advertised models in endpoint order.
|
|
4051
|
-
* @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
|
|
4052
|
-
*/
|
|
4053
|
-
async remoteDiscoverModels(settingsNs, request, signal) {
|
|
4054
|
-
try {
|
|
4055
|
-
return await this.discoverModels(settingsNs, request, signal);
|
|
4056
|
-
} catch (error) {
|
|
4057
|
-
throw new RemoteError("llm/model-discovery-rejected", error instanceof Error ? error.message : String(error), {
|
|
4058
|
-
settingsNs,
|
|
4059
|
-
...request.baseURL === void 0 ? {} : { baseURL: request.baseURL }
|
|
4060
|
-
}, { cause: error });
|
|
4061
|
-
}
|
|
4062
|
-
}
|
|
4063
|
-
/**
|
|
4064
|
-
* Resolve the retry policy captured when one provider route was registered.
|
|
4065
|
-
* @param provider - registered provider route to inspect.
|
|
4066
|
-
* @returns the provider-owned policy, with normal defaults already resolved.
|
|
4067
|
-
*/
|
|
4068
|
-
providerRetryPolicy(provider) {
|
|
4069
|
-
return this.registration(provider).retryPolicy;
|
|
4070
|
-
}
|
|
4071
|
-
/**
|
|
4072
|
-
* Resolve provider-side request-image pricing for one exact route, or
|
|
4073
|
-
* `undefined` when the provider is unregistered or declares none. Unknown
|
|
4074
|
-
* providers degrade to `undefined` rather than throwing because callers
|
|
4075
|
-
* price durable history whose route may no longer be mounted.
|
|
4076
|
-
* @param provider - provider route named by a request header.
|
|
4077
|
-
* @param model - exact model id named by the same header.
|
|
4078
|
-
* @returns the owning adapter's image pricing for the route, when declared.
|
|
4079
|
-
*/
|
|
4080
|
-
imageRequestPricing(provider, model) {
|
|
4081
|
-
return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
|
|
4082
|
-
}
|
|
4083
|
-
/**
|
|
4084
|
-
* Resolve the exact text one durable file occurrence contributes to every
|
|
4085
|
-
* provider request in the current execution environment.
|
|
4086
|
-
* @param ref - durable verbatim file reference from model history.
|
|
4087
|
-
* @returns the same deterministic handle text used at adapter dispatch.
|
|
4088
|
-
*/
|
|
4089
|
-
fileRequestText(ref) {
|
|
4090
|
-
return fileHandleText(ref, this.fileReadPath(ref));
|
|
4091
|
-
}
|
|
4092
|
-
/** Detach typed adapter-owned modality metadata. */
|
|
4093
|
-
detachedModalities(modalities) {
|
|
4094
|
-
return modalities === void 0 ? void 0 : [...modalities];
|
|
4095
|
-
}
|
|
4096
|
-
/**
|
|
4097
|
-
* Discover models advertised by one registered provider. Catalog membership
|
|
4098
|
-
* is advisory and never changes routing or request validation.
|
|
4099
|
-
* @param provider - registered provider route to inspect.
|
|
4100
|
-
* @returns detached model metadata in adapter-preferred order.
|
|
4101
|
-
*/
|
|
4102
|
-
async listModels(provider) {
|
|
4103
|
-
const models = await this.registration(provider).adapter.listModels(provider);
|
|
4104
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4105
|
-
return models.map((model) => {
|
|
4106
|
-
if (typeof model.provider !== "string" || model.provider !== provider || typeof model.id !== "string" || model.id.length === 0 || typeof model.name !== "string" || model.name.length === 0 || model.description !== void 0 && typeof model.description !== "string" || seen.has(model.id)) throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, "INVALID_CATALOG");
|
|
4107
|
-
seen.add(model.id);
|
|
4108
|
-
const inputModalities = this.detachedModalities(model.inputModalities);
|
|
4109
|
-
return {
|
|
4110
|
-
provider: model.provider,
|
|
4111
|
-
id: model.id,
|
|
4112
|
-
name: model.name,
|
|
4113
|
-
...model.description === void 0 ? {} : { description: model.description },
|
|
4114
|
-
...inputModalities === void 0 ? {} : { inputModalities }
|
|
4115
|
-
};
|
|
4116
|
-
});
|
|
4117
|
-
}
|
|
4118
|
-
/**
|
|
4119
|
-
* Resolve and validate all metadata from the adapter that owns one exact
|
|
4120
|
-
* route. The result is detached from adapter-owned objects; catalog
|
|
4121
|
-
* membership remains advisory and does not control request routing.
|
|
4122
|
-
* @param provider - registered provider route to inspect.
|
|
4123
|
-
* @param model - exact model id passed to the adapter.
|
|
4124
|
-
* @param signal - optional cancellation for adapter-owned asynchronous lookup.
|
|
4125
|
-
* @returns exact model identity plus available context and reasoning metadata.
|
|
4126
|
-
*/
|
|
4127
|
-
async resolveModelInfo(provider, model, signal) {
|
|
4128
|
-
return this.resolveModelInfoFor(this.registration(provider), model, signal);
|
|
4129
|
-
}
|
|
4130
|
-
async resolveModelInfoFor(registration, model, signal) {
|
|
4131
|
-
const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
|
|
4132
|
-
return this.normalizeModelInfo(registration, model, resolved);
|
|
4133
|
-
}
|
|
4134
|
-
/** Validate and detach one adapter-returned exact model result. */
|
|
4135
|
-
normalizeModelInfo(registration, model, resolved) {
|
|
4136
|
-
const provider = registration.provider.id;
|
|
4137
|
-
if (typeof resolved.provider !== "string" || resolved.provider !== provider || typeof resolved.id !== "string" || resolved.id !== model || typeof resolved.name !== "string" || resolved.name.length === 0 || resolved.description !== void 0 && typeof resolved.description !== "string") throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
|
|
4138
|
-
const context = resolved.context;
|
|
4139
|
-
if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
|
|
4140
|
-
const inputModalities = this.detachedModalities(resolved.inputModalities);
|
|
4141
|
-
const systemPromptUpdate = resolved.systemPromptUpdate;
|
|
4142
|
-
if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
|
|
4143
|
-
const defaultMaxTokens = resolved.defaultMaxTokens;
|
|
4144
|
-
if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
|
|
4145
|
-
const info = {
|
|
4146
|
-
provider,
|
|
4147
|
-
id: model,
|
|
4148
|
-
name: resolved.name,
|
|
4149
|
-
...resolved.description === void 0 ? {} : { description: resolved.description },
|
|
4150
|
-
...inputModalities === void 0 ? {} : { inputModalities },
|
|
4151
|
-
...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
|
|
4152
|
-
...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
|
|
4153
|
-
...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate }
|
|
4154
|
-
};
|
|
4155
|
-
const reasoning = resolved.reasoning;
|
|
4156
|
-
if (reasoning === void 0) return info;
|
|
4157
|
-
if (reasoning.efforts.length === 0) throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
|
|
4158
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4159
|
-
const efforts = reasoning.efforts.map((effort) => {
|
|
4160
|
-
if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) throw new LlmError(`adapter returned invalid or duplicate reasoning effort metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
|
|
4161
|
-
seen.add(effort.id);
|
|
4162
|
-
return {
|
|
4163
|
-
id: effort.id,
|
|
4164
|
-
name: effort.name,
|
|
4165
|
-
...effort.description === void 0 ? {} : { description: effort.description }
|
|
4166
|
-
};
|
|
4167
|
-
});
|
|
4168
|
-
if (reasoning.defaultEffort !== void 0 && !seen.has(reasoning.defaultEffort)) throw new LlmError(`adapter returned an unknown default reasoning effort for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
|
|
4169
|
-
return {
|
|
4170
|
-
...info,
|
|
4171
|
-
reasoning: {
|
|
4172
|
-
efforts,
|
|
4173
|
-
...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
|
|
4174
|
-
}
|
|
4175
|
-
};
|
|
4176
|
-
}
|
|
4177
|
-
/**
|
|
4178
|
-
* Validate a conversation call config against its exact model capability and
|
|
4179
|
-
* materialize adapter-configured defaults. Unsupported explicit efforts
|
|
4180
|
-
* reject before provider I/O; no clamping or aliasing is performed. This
|
|
4181
|
-
* standalone query does not bind a later dispatch; use {@link prepareCall}
|
|
4182
|
-
* when logging and streaming must share one adapter registration.
|
|
4183
|
-
* @param config - provider/model route and optional request controls.
|
|
4184
|
-
* @param signal - optional cancellation for adapter-owned capability lookup.
|
|
4185
|
-
* @returns a detached config only when a default must be materialized.
|
|
4186
|
-
*/
|
|
4187
|
-
async resolveCallConfig(config, signal) {
|
|
4188
|
-
return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
|
|
4189
|
-
}
|
|
4190
|
-
async resolveCallFor(registration, config, signal) {
|
|
4191
|
-
const info = await this.resolveModelInfoFor(registration, config.model, signal);
|
|
4192
|
-
return this.resolveCallWithInfo(config, info);
|
|
4193
|
-
}
|
|
4194
|
-
/** Validate request controls against one already-bound exact model result. */
|
|
4195
|
-
resolveCallWithInfo(config, info) {
|
|
4196
|
-
const defaulted = config.maxTokens === void 0 && info.defaultMaxTokens !== void 0 ? {
|
|
4197
|
-
...config,
|
|
4198
|
-
maxTokens: info.defaultMaxTokens
|
|
4199
|
-
} : config;
|
|
4200
|
-
const reasoning = info.reasoning;
|
|
4201
|
-
const requested = defaulted.reasoningEffort;
|
|
4202
|
-
let resolvedConfig = defaulted;
|
|
4203
|
-
if (reasoning === void 0) {
|
|
4204
|
-
if (requested !== void 0) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, "UNSUPPORTED_REASONING_EFFORT");
|
|
4205
|
-
} else {
|
|
4206
|
-
const effective = requested ?? reasoning.defaultEffort;
|
|
4207
|
-
if (effective !== void 0) {
|
|
4208
|
-
if (!reasoning.efforts.some((effort) => effort.id === effective)) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`, "UNSUPPORTED_REASONING_EFFORT");
|
|
4209
|
-
if (requested !== effective) resolvedConfig = {
|
|
4210
|
-
...defaulted,
|
|
4211
|
-
reasoningEffort: effective
|
|
4212
|
-
};
|
|
4213
|
-
}
|
|
4214
|
-
}
|
|
4215
|
-
return {
|
|
4216
|
-
config: resolvedConfig,
|
|
4217
|
-
...info.context === void 0 ? {} : { context: info.context },
|
|
4218
|
-
modelInfo: info
|
|
4219
|
-
};
|
|
4220
|
-
}
|
|
4221
|
-
/**
|
|
4222
|
-
* Resolve one call under its current adapter registration. The returned
|
|
4223
|
-
* one-shot handle keeps that registration across header logging and dispatch,
|
|
4224
|
-
* so HMR cannot combine one adapter's capability result with another adapter.
|
|
4225
|
-
* @param config - provider/model route and optional request controls.
|
|
4226
|
-
* @param signal - optional cancellation for adapter-owned capability lookup.
|
|
4227
|
-
* @returns a prepared config and its registration-bound stream entry point.
|
|
4228
|
-
*/
|
|
4229
|
-
async prepareCall(config, signal) {
|
|
4230
|
-
const registration = this.registration(config.provider);
|
|
4231
|
-
const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
|
|
4232
|
-
const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
|
|
4233
|
-
const resolved = this.resolveCallWithInfo(config, modelInfo);
|
|
4234
|
-
const resolvedConfig = deepFreeze(structuredClone(resolved.config));
|
|
4235
|
-
const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
|
|
4236
|
-
const adapterDefaults = deepFreeze({
|
|
4237
|
-
...config.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
|
|
4238
|
-
...config.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
|
|
4239
|
-
});
|
|
4240
|
-
let dispatched = false;
|
|
4241
|
-
return Object.freeze({
|
|
4242
|
-
config: resolvedConfig,
|
|
4243
|
-
retryPolicy: registration.retryPolicy,
|
|
4244
|
-
adapterDefaults,
|
|
4245
|
-
...context === void 0 ? {} : { context },
|
|
4246
|
-
...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
|
|
4247
|
-
...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
|
|
4248
|
-
stream: (options) => {
|
|
4249
|
-
if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
|
|
4250
|
-
if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
4251
|
-
dispatched = true;
|
|
4252
|
-
return this.streamWithRegistration(options, {
|
|
4253
|
-
registration,
|
|
4254
|
-
config: resolvedConfig,
|
|
4255
|
-
modelInfo,
|
|
4256
|
-
dispatch: (options) => adapterCall.stream(options)
|
|
4257
|
-
});
|
|
4258
|
-
}
|
|
4259
|
-
});
|
|
4260
|
-
}
|
|
4261
|
-
registration(provider) {
|
|
4262
|
-
const registration = this.adapters.get(provider);
|
|
4263
|
-
if (!registration) throw new LlmError(`no adapter registered for provider "${provider}"`, "NO_ADAPTER");
|
|
4264
|
-
return registration;
|
|
4265
|
-
}
|
|
4266
|
-
/** Remove replay state whose historical route is owned by another adapter. */
|
|
4267
|
-
forAdapter(options, adapter) {
|
|
4268
|
-
const messages = options.messages.map((message) => {
|
|
4269
|
-
if (message.role !== "assistant") return message;
|
|
4270
|
-
const source = message.source;
|
|
4271
|
-
if (source.replayState === void 0) return message;
|
|
4272
|
-
if (this.adapters.get(source.provider)?.adapter === adapter) return message;
|
|
4273
|
-
return freezeMessage({
|
|
4274
|
-
...message,
|
|
4275
|
-
source: {
|
|
4276
|
-
kind: "model",
|
|
4277
|
-
provider: source.provider,
|
|
4278
|
-
model: source.model
|
|
4279
|
-
}
|
|
4280
|
-
});
|
|
4281
|
-
});
|
|
4282
|
-
if (messages.every((message, index) => message === options.messages[index])) return options;
|
|
4283
|
-
const filtered = {
|
|
4284
|
-
...options,
|
|
4285
|
-
messages
|
|
4286
|
-
};
|
|
4287
|
-
return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
|
|
4288
|
-
}
|
|
4289
|
-
/**
|
|
4290
|
-
* Resolve the current execution-world read path of one durable file
|
|
4291
|
-
* reference through the mounted attachment and filesystem providers.
|
|
4292
|
-
*/
|
|
4293
|
-
fileReadPath(ref) {
|
|
4294
|
-
let hostPath;
|
|
4295
|
-
try {
|
|
4296
|
-
hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
|
|
4297
|
-
} catch {
|
|
4298
|
-
return;
|
|
4299
|
-
}
|
|
4300
|
-
if (hostPath === void 0) return void 0;
|
|
4301
|
-
return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
|
|
4302
|
-
}
|
|
4303
|
-
/**
|
|
4304
|
-
* Final adapter boundary. Adapter selection, dispatch, iterator construction,
|
|
4305
|
-
* and iteration failures become one terminal failure chunk. Middleware and
|
|
4306
|
-
* downstream consumer failures remain thrown plugin or consumer errors.
|
|
4307
|
-
*/
|
|
4308
|
-
async *adapterStream(options, prepared) {
|
|
4309
|
-
let iterator;
|
|
4310
|
-
try {
|
|
4311
|
-
const registration = prepared?.registration ?? this.registration(options.provider);
|
|
4312
|
-
const adapter = registration.adapter;
|
|
4313
|
-
let modelInfo;
|
|
4314
|
-
let resolvedConfig;
|
|
4315
|
-
let dispatch;
|
|
4316
|
-
if (prepared === void 0) {
|
|
4317
|
-
const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
|
|
4318
|
-
modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
|
|
4319
|
-
resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
|
|
4320
|
-
dispatch = (options) => adapterCall.stream(options);
|
|
4321
|
-
} else {
|
|
4322
|
-
modelInfo = prepared.modelInfo;
|
|
4323
|
-
resolvedConfig = prepared.config;
|
|
4324
|
-
dispatch = prepared.dispatch;
|
|
4325
|
-
}
|
|
4326
|
-
if (prepared !== void 0 && !callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
4327
|
-
const resolvedOptions = callConfigEquals(options, resolvedConfig) ? options : Object.isFrozen(options) ? deepFreeze({
|
|
4328
|
-
...options,
|
|
4329
|
-
...resolvedConfig
|
|
4330
|
-
}) : {
|
|
4331
|
-
...options,
|
|
4332
|
-
...resolvedConfig
|
|
4333
|
-
};
|
|
4334
|
-
let projectedMessages = resolvedOptions.messages;
|
|
4335
|
-
if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
|
|
4336
|
-
if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
|
|
4337
|
-
const projectedOptions = projectedMessages === resolvedOptions.messages ? resolvedOptions : Object.isFrozen(resolvedOptions) ? deepFreeze({
|
|
4338
|
-
...resolvedOptions,
|
|
4339
|
-
messages: projectedMessages
|
|
4340
|
-
}) : {
|
|
4341
|
-
...resolvedOptions,
|
|
4342
|
-
messages: projectedMessages
|
|
4343
|
-
};
|
|
4344
|
-
iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
|
|
4345
|
-
} catch (error) {
|
|
4346
|
-
yield adapterFailureChunk(error, options.signal);
|
|
4347
|
-
return;
|
|
4348
|
-
}
|
|
4349
|
-
let completed = false;
|
|
4350
|
-
try {
|
|
4351
|
-
while (true) {
|
|
4352
|
-
let item;
|
|
4353
|
-
try {
|
|
4354
|
-
const next = await iterator.next();
|
|
4355
|
-
item = next.done ? { done: true } : {
|
|
4356
|
-
done: false,
|
|
4357
|
-
value: next.value
|
|
4358
|
-
};
|
|
4359
|
-
} catch (error) {
|
|
4360
|
-
completed = true;
|
|
4361
|
-
yield adapterFailureChunk(error, options.signal);
|
|
4362
|
-
return;
|
|
4363
|
-
}
|
|
4364
|
-
if (item.done) {
|
|
4365
|
-
completed = true;
|
|
4366
|
-
return;
|
|
4367
|
-
}
|
|
4368
|
-
yield item.value;
|
|
4369
|
-
}
|
|
4370
|
-
} finally {
|
|
4371
|
-
if (!completed) {
|
|
4372
|
-
const close = iterator.return?.bind(iterator);
|
|
4373
|
-
if (close) await close();
|
|
4374
|
-
}
|
|
4375
|
-
}
|
|
4376
|
-
}
|
|
4377
|
-
/**
|
|
4378
|
-
* Stream one model call as raw chunks (token-level deltas). Replay state is
|
|
4379
|
-
* retained only when the same adapter instance owns its historical provider
|
|
4380
|
-
* and the target provider. Final adapter selection remains fixed through
|
|
4381
|
-
* asynchronous exact-model resolution and dispatch. Adapter selection,
|
|
4382
|
-
* dispatch, and iteration failures become terminal `error` or `aborted`
|
|
4383
|
-
* finish chunks; middleware, nested-call, cleanup, and consumer failures
|
|
4384
|
-
* remain thrown.
|
|
4385
|
-
* @param options - the full request; `options.provider` selects the adapter.
|
|
4386
|
-
* @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
|
|
4387
|
-
*/
|
|
4388
|
-
stream(options) {
|
|
4389
|
-
return this.streamWithRegistration(options);
|
|
4390
|
-
}
|
|
4391
|
-
streamWithRegistration(options, prepared) {
|
|
4392
|
-
return this.ctx.waterfall(this, "llm/stream", options, () => this.adapterStream(options, prepared));
|
|
4393
|
-
}
|
|
4394
|
-
};
|
|
4395
|
-
})();
|
|
4396
|
-
/** Convert one adapter throw into the stream protocol's terminal outcome. */
|
|
4397
|
-
function adapterFailureChunk(error, signal) {
|
|
4398
|
-
const failure = normalizeLlmFailure(error);
|
|
4399
|
-
return {
|
|
4400
|
-
type: "finish",
|
|
4401
|
-
reason: signal?.aborted || failure.code === "ABORTED" ? {
|
|
4402
|
-
kind: "aborted",
|
|
4403
|
-
failure
|
|
4404
|
-
} : {
|
|
4405
|
-
kind: "error",
|
|
4406
|
-
failure
|
|
4407
|
-
}
|
|
4408
|
-
};
|
|
3971
|
+
if (!(typeof stream.listenerCount === "function" && stream.listenerCount("error") > 0)) stream.on("error", () => {});
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* Defensively log to console.error without letting stderr write failures
|
|
3976
|
+
* (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
|
|
3977
|
+
*/
|
|
3978
|
+
function safeConsoleError(message, ...args) {
|
|
3979
|
+
try {
|
|
3980
|
+
console.error(message, ...args);
|
|
3981
|
+
} catch {}
|
|
4409
3982
|
}
|
|
4410
3983
|
//#endregion
|
|
4411
3984
|
//#region src/host-ai.ts
|
|
@@ -4554,6 +4127,780 @@ async function parseTaskDraft(llm, request, signal) {
|
|
|
4554
4127
|
}
|
|
4555
4128
|
}
|
|
4556
4129
|
//#endregion
|
|
4130
|
+
//#region src/host/agent-tools.ts
|
|
4131
|
+
/** Model-facing JSON rendering shared by every tool. */
|
|
4132
|
+
function renderJson(_args, value) {
|
|
4133
|
+
return [{
|
|
4134
|
+
type: "text",
|
|
4135
|
+
text: JSON.stringify(value, null, 2)
|
|
4136
|
+
}];
|
|
4137
|
+
}
|
|
4138
|
+
/** Mark an already JSON-safe projection as the tool's canonical value. */
|
|
4139
|
+
function json(value) {
|
|
4140
|
+
return value;
|
|
4141
|
+
}
|
|
4142
|
+
/** A domain refusal the model is expected to read and act on. */
|
|
4143
|
+
function refused(code, message) {
|
|
4144
|
+
return json({
|
|
4145
|
+
ok: false,
|
|
4146
|
+
code,
|
|
4147
|
+
message
|
|
4148
|
+
});
|
|
4149
|
+
}
|
|
4150
|
+
function messageOf(error) {
|
|
4151
|
+
return error instanceof Error ? error.message : String(error);
|
|
4152
|
+
}
|
|
4153
|
+
function has(value, key) {
|
|
4154
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
4155
|
+
}
|
|
4156
|
+
/**
|
|
4157
|
+
* The calling session id, for the execution audit trail (the same stamp the
|
|
4158
|
+
* browser asserts). Absent when a call has no agent behind it.
|
|
4159
|
+
*/
|
|
4160
|
+
function callingSessionId(exec) {
|
|
4161
|
+
const agent = exec.agent;
|
|
4162
|
+
const id = agent?.session?.id ?? agent?.session?.header?.id;
|
|
4163
|
+
return typeof id === "string" && id !== "" ? id : void 0;
|
|
4164
|
+
}
|
|
4165
|
+
function scheduleView(schedule) {
|
|
4166
|
+
return {
|
|
4167
|
+
enabled: schedule.enabled,
|
|
4168
|
+
cron: schedule.cron,
|
|
4169
|
+
...schedule.nextRunAt === void 0 ? {} : { nextRunAt: schedule.nextRunAt },
|
|
4170
|
+
...schedule.lastTriggeredAt === void 0 ? {} : { lastTriggeredAt: schedule.lastTriggeredAt }
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
function tagView(tag) {
|
|
4174
|
+
return {
|
|
4175
|
+
name: tag.name,
|
|
4176
|
+
...tag.promptPrefix === void 0 ? {} : { promptPrefix: tag.promptPrefix }
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
function executionView(execution) {
|
|
4180
|
+
return {
|
|
4181
|
+
id: execution.id,
|
|
4182
|
+
...execution.sessionId === void 0 ? {} : { sessionId: execution.sessionId },
|
|
4183
|
+
startedAt: execution.startedAt,
|
|
4184
|
+
...execution.endedAt === void 0 ? {} : { endedAt: execution.endedAt },
|
|
4185
|
+
...execution.result === void 0 ? {} : { result: execution.result },
|
|
4186
|
+
...execution.error === void 0 ? {} : { error: execution.error },
|
|
4187
|
+
...execution.initiatedBy === void 0 ? {} : { initiatedBy: execution.initiatedBy },
|
|
4188
|
+
...execution.runGroupId === void 0 ? {} : { cascade: true },
|
|
4189
|
+
...execution.ownResult === void 0 ? {} : {
|
|
4190
|
+
ownResult: execution.ownResult,
|
|
4191
|
+
awaitingSubtasks: execution.endedAt === void 0
|
|
4192
|
+
}
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* One compact task row for the model: identity, column, lineage, execution
|
|
4197
|
+
* targets, schedule, and the latest run. The prompt body rides only
|
|
4198
|
+
* task_board_get, so a fifty-task list stays readable.
|
|
4199
|
+
*/
|
|
4200
|
+
function taskSummary(task, tasks, sessionDefault) {
|
|
4201
|
+
const latest = task.executions.at(-1);
|
|
4202
|
+
const subtaskCount = directSubtasks(tasks, task.id).length;
|
|
4203
|
+
return {
|
|
4204
|
+
id: task.id,
|
|
4205
|
+
title: task.title,
|
|
4206
|
+
status: task.status,
|
|
4207
|
+
description: task.description,
|
|
4208
|
+
...task.parentId === void 0 ? {} : { parentId: task.parentId },
|
|
4209
|
+
...subtaskCount === 0 ? {} : { subtaskCount },
|
|
4210
|
+
...task.archivedAt === void 0 ? {} : { archivedAt: task.archivedAt },
|
|
4211
|
+
...task.tags === void 0 ? {} : { tags: task.tags.map(tagView) },
|
|
4212
|
+
...task.workspaceId === void 0 ? {} : { workspaceId: task.workspaceId },
|
|
4213
|
+
...task.mode === void 0 ? {} : { mode: task.mode },
|
|
4214
|
+
...task.permission === void 0 ? {} : { permission: task.permission },
|
|
4215
|
+
...task.model === void 0 ? {} : { model: task.model },
|
|
4216
|
+
...task.reuseSession === true ? { reuseSession: true } : {},
|
|
4217
|
+
...task.teamRun === true ? { teamRun: true } : {},
|
|
4218
|
+
...task.schedule === void 0 ? {} : { schedule: scheduleView(task.schedule) },
|
|
4219
|
+
...task.freeze === void 0 ? {} : { continuationCard: true },
|
|
4220
|
+
...task.permissionConfirmedAt === void 0 ? {} : { permissionConfirmedAt: task.permissionConfirmedAt },
|
|
4221
|
+
...requiresPermissionConfirmation(task, sessionDefault ?? "read-only") ? { permissionPending: true } : {},
|
|
4222
|
+
executionCount: task.executions.length,
|
|
4223
|
+
...latest === void 0 ? {} : { latestExecution: executionView(latest) },
|
|
4224
|
+
createdAt: task.createdAt,
|
|
4225
|
+
updatedAt: task.updatedAt
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
4228
|
+
/** One task's full detail: content, lineage, schedule, and recent runs. */
|
|
4229
|
+
function taskDetail(task, tasks, sessionDefault) {
|
|
4230
|
+
const parent = task.parentId === void 0 ? void 0 : tasks.find((item) => item.id === task.parentId);
|
|
4231
|
+
return {
|
|
4232
|
+
...taskSummary(task, tasks, sessionDefault),
|
|
4233
|
+
prompt: task.prompt,
|
|
4234
|
+
depth: taskDepth(tasks, task.id),
|
|
4235
|
+
...parent === void 0 ? {} : { parent: {
|
|
4236
|
+
id: parent.id,
|
|
4237
|
+
title: parent.title,
|
|
4238
|
+
status: parent.status
|
|
4239
|
+
} },
|
|
4240
|
+
...task.freeze === void 0 ? {} : { freeze: {
|
|
4241
|
+
goal: task.freeze.goal,
|
|
4242
|
+
progress: task.freeze.progress,
|
|
4243
|
+
next: task.freeze.next,
|
|
4244
|
+
frozenAt: task.freeze.frozenAt,
|
|
4245
|
+
...task.freeze.frozenBy === void 0 ? {} : { frozenBy: task.freeze.frozenBy },
|
|
4246
|
+
...task.freeze.redacted === true ? { redacted: true } : {}
|
|
4247
|
+
} },
|
|
4248
|
+
...task.handover === void 0 ? {} : { handover: {
|
|
4249
|
+
...task.handover.workspaceId === void 0 ? {} : { workspaceId: task.handover.workspaceId },
|
|
4250
|
+
...task.handover.mode === void 0 ? {} : { mode: task.handover.mode },
|
|
4251
|
+
...task.handover.permission === void 0 ? {} : { permission: task.handover.permission },
|
|
4252
|
+
references: task.handover.references,
|
|
4253
|
+
bundledAt: task.handover.bundledAt
|
|
4254
|
+
} },
|
|
4255
|
+
subtasks: directSubtasks(tasks, task.id).map((child) => taskSummary(child, tasks, sessionDefault)),
|
|
4256
|
+
executions: task.executions.slice(-10).map(executionView)
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
/** The executions a run opened: everything that was not in the ledger before. */
|
|
4260
|
+
function openedExecutions(before, after) {
|
|
4261
|
+
const known = new Set(before.map((execution) => execution.id));
|
|
4262
|
+
return after.filter((execution) => !known.has(execution.id));
|
|
4263
|
+
}
|
|
4264
|
+
/** Board summary attached to task_board_list. */
|
|
4265
|
+
function boardView(snapshot, maxSubtaskDepth) {
|
|
4266
|
+
const counts = {
|
|
4267
|
+
backlog: 0,
|
|
4268
|
+
todo: 0,
|
|
4269
|
+
running: 0,
|
|
4270
|
+
done: 0,
|
|
4271
|
+
failed: 0,
|
|
4272
|
+
archived: 0
|
|
4273
|
+
};
|
|
4274
|
+
for (const task of snapshot.tasks) if (task.archivedAt !== void 0) counts.archived += 1;
|
|
4275
|
+
else counts[task.status] += 1;
|
|
4276
|
+
return {
|
|
4277
|
+
revision: snapshot.revision,
|
|
4278
|
+
timeZone: snapshot.scheduler.timeZone,
|
|
4279
|
+
...snapshot.scheduler.error === void 0 ? {} : { schedulerError: snapshot.scheduler.error },
|
|
4280
|
+
maxSubtaskDepth,
|
|
4281
|
+
sessionDefaultPermission: snapshot.sessionDefaultPermission ?? "read-only",
|
|
4282
|
+
runningSessions: snapshot.power.runningSessions,
|
|
4283
|
+
armedSchedules: snapshot.power.armedSchedules,
|
|
4284
|
+
counts
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
4287
|
+
/**
|
|
4288
|
+
* Build every task-board tool bound to one Host service.
|
|
4289
|
+
* @param host - the Host ledger/service face (TaskBoardHostService).
|
|
4290
|
+
* @returns the tool definitions, in TASK_BOARD_TOOL_NAMES order.
|
|
4291
|
+
*/
|
|
4292
|
+
function buildTaskBoardTools(host) {
|
|
4293
|
+
return [
|
|
4294
|
+
buildListTool(host),
|
|
4295
|
+
buildGetTool(host),
|
|
4296
|
+
buildCreateTool(host),
|
|
4297
|
+
buildUpdateTool(host),
|
|
4298
|
+
buildSetParentTool(host),
|
|
4299
|
+
buildRunTool(host),
|
|
4300
|
+
buildManageTool(host),
|
|
4301
|
+
buildScheduleTool(host)
|
|
4302
|
+
];
|
|
4303
|
+
}
|
|
4304
|
+
function buildSetParentTool(host) {
|
|
4305
|
+
return defineTool({
|
|
4306
|
+
name: "task_board_set_parent",
|
|
4307
|
+
description: [
|
|
4308
|
+
"Attach an existing task under a parent (making it a subtask) or detach it back to the root board by setting its parentId.",
|
|
4309
|
+
"Pass an empty parentId to detach.",
|
|
4310
|
+
"The Host refuses the link when the parent is unknown or archived, when the moved task is one of its own descendants, when the resulting depth exceeds the deployment subtask limit (default one level, maximum three), or while the task has an unsettled execution (its link may still be a running cascade parent pointer).",
|
|
4311
|
+
"Running a task runs its whole subtree concurrently; a parent card settles only after its own turn and every direct subtask settle.",
|
|
4312
|
+
"Triggers: 任务看板, task board, 看板, 子任务, subtask, 关联任务, link task, 解除关联, detach."
|
|
4313
|
+
].join(" "),
|
|
4314
|
+
parameters: {
|
|
4315
|
+
taskId: {
|
|
4316
|
+
type: "string",
|
|
4317
|
+
required: true,
|
|
4318
|
+
description: "The task whose parent link changes."
|
|
4319
|
+
},
|
|
4320
|
+
parentId: {
|
|
4321
|
+
type: "string",
|
|
4322
|
+
required: true,
|
|
4323
|
+
description: "Parent task id, or an empty string to detach the task back to the root board."
|
|
4324
|
+
}
|
|
4325
|
+
},
|
|
4326
|
+
output: {
|
|
4327
|
+
schema: { type: "json" },
|
|
4328
|
+
render: renderJson
|
|
4329
|
+
},
|
|
4330
|
+
async execute(args, exec) {
|
|
4331
|
+
try {
|
|
4332
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4333
|
+
kind: "set-parent",
|
|
4334
|
+
taskId: args.taskId,
|
|
4335
|
+
parentId: args.parentId === "" ? null : args.parentId
|
|
4336
|
+
}, callingSessionId(exec));
|
|
4337
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4338
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4339
|
+
return json({
|
|
4340
|
+
ok: true,
|
|
4341
|
+
task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4342
|
+
});
|
|
4343
|
+
} catch (error) {
|
|
4344
|
+
return refused("refused", messageOf(error));
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
function buildRunTool(host) {
|
|
4350
|
+
return defineTool({
|
|
4351
|
+
name: "task_board_run",
|
|
4352
|
+
description: [
|
|
4353
|
+
"Run a task on the board now, or re-run a settled one.",
|
|
4354
|
+
"Running a task also runs its whole subtask tree CONCURRENTLY: the Host opens one real DSH session per member under one run group, each replaying its card pinned workspace, agent preset, permission and model; a subtask that does not pin them inherits its nearest ancestor.",
|
|
4355
|
+
"The parent card stays in the running column until its own turn and every direct subtask have settled, and it fails when any member failed; one failing subtask never stops the others.",
|
|
4356
|
+
"A card whose effective permission is above the deployment session default is refused with confirmation-required until a HUMAN confirms the binding in the board UI; this tool cannot confirm it.",
|
|
4357
|
+
"A card whose task carries teamRun instead starts ONE Team Lead session and the Host spawns a teammate per subtask inside it, so those cards run as an Agent Team rather than as independent sessions; that mode needs the deployment Agent Teams service and is refused when it is missing.",
|
|
4358
|
+
"Executions consume real API quota. Sessions are created asynchronously, so read their ids and outcomes back with task_board_get.",
|
|
4359
|
+
"Triggers: 任务看板, task board, 看板, 执行任务, run task, 重新执行, rerun, 子任务, subtask."
|
|
4360
|
+
].join(" "),
|
|
4361
|
+
parameters: {
|
|
4362
|
+
taskId: {
|
|
4363
|
+
type: "string",
|
|
4364
|
+
required: true,
|
|
4365
|
+
description: "The task to run; running a root task runs its whole subtree."
|
|
4366
|
+
},
|
|
4367
|
+
rerun: {
|
|
4368
|
+
type: "boolean",
|
|
4369
|
+
description: "Re-run a settled task: reset its column to todo before starting (default false)."
|
|
4370
|
+
}
|
|
4371
|
+
},
|
|
4372
|
+
output: {
|
|
4373
|
+
schema: { type: "json" },
|
|
4374
|
+
render: renderJson
|
|
4375
|
+
},
|
|
4376
|
+
async execute(args, exec) {
|
|
4377
|
+
const before = host.snapshot();
|
|
4378
|
+
try {
|
|
4379
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4380
|
+
kind: args.rerun === true ? "rerun" : "run",
|
|
4381
|
+
taskId: args.taskId
|
|
4382
|
+
}, callingSessionId(exec));
|
|
4383
|
+
const beforeById = new Map(before.tasks.map((task) => [task.id, task]));
|
|
4384
|
+
const started = snapshot.tasks.flatMap((task) => {
|
|
4385
|
+
return openedExecutions(beforeById.get(task.id)?.executions ?? [], task.executions).map((execution) => ({
|
|
4386
|
+
taskId: task.id,
|
|
4387
|
+
title: task.title,
|
|
4388
|
+
executionId: execution.id,
|
|
4389
|
+
...execution.sessionId === void 0 ? {} : { sessionId: execution.sessionId }
|
|
4390
|
+
}));
|
|
4391
|
+
});
|
|
4392
|
+
const root = snapshot.tasks.find((task) => task.id === args.taskId);
|
|
4393
|
+
return json({
|
|
4394
|
+
ok: true,
|
|
4395
|
+
started,
|
|
4396
|
+
...root === void 0 ? {} : { task: taskSummary(root, snapshot.tasks, snapshot.sessionDefaultPermission) },
|
|
4397
|
+
note: "Executions run asynchronously; call task_board_get to read session ids and the settled outcome. The parent settles only after its own turn and every direct subtask settle."
|
|
4398
|
+
});
|
|
4399
|
+
} catch (error) {
|
|
4400
|
+
const message = messageOf(error);
|
|
4401
|
+
return refused(message.startsWith("confirmation-required") ? "confirmation-required" : "refused", message);
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
function buildManageTool(host) {
|
|
4407
|
+
return defineTool({
|
|
4408
|
+
name: "task_board_manage",
|
|
4409
|
+
description: [
|
|
4410
|
+
"Move, archive, restore, or delete one task board card.",
|
|
4411
|
+
"move-todo and move-backlog are the manual column moves; a running card cannot be moved.",
|
|
4412
|
+
"archive takes the whole subtask tree off the board and is refused while any member has an unsettled execution; restore brings the task, its ancestors and its subtree back; delete removes one card and is refused while it still has subtasks (detach or delete them first) or while it runs.",
|
|
4413
|
+
"It cannot confirm a permission binding: the confirmation gate is a human act performed in the board UI.",
|
|
4414
|
+
"Triggers: 任务看板, task board, 看板, 归档, archive, 删除任务, delete task, 移动任务, move task."
|
|
4415
|
+
].join(" "),
|
|
4416
|
+
parameters: {
|
|
4417
|
+
taskId: {
|
|
4418
|
+
type: "string",
|
|
4419
|
+
required: true,
|
|
4420
|
+
description: "The task to act on."
|
|
4421
|
+
},
|
|
4422
|
+
action: {
|
|
4423
|
+
type: "string",
|
|
4424
|
+
required: true,
|
|
4425
|
+
enum: [
|
|
4426
|
+
"move-todo",
|
|
4427
|
+
"move-backlog",
|
|
4428
|
+
"archive",
|
|
4429
|
+
"restore",
|
|
4430
|
+
"delete"
|
|
4431
|
+
],
|
|
4432
|
+
description: "The lifecycle operation to perform."
|
|
4433
|
+
}
|
|
4434
|
+
},
|
|
4435
|
+
output: {
|
|
4436
|
+
schema: { type: "json" },
|
|
4437
|
+
render: renderJson
|
|
4438
|
+
},
|
|
4439
|
+
async execute(args, exec) {
|
|
4440
|
+
const initiator = callingSessionId(exec);
|
|
4441
|
+
const action = {
|
|
4442
|
+
"move-todo": {
|
|
4443
|
+
kind: "move",
|
|
4444
|
+
taskId: args.taskId,
|
|
4445
|
+
status: "todo"
|
|
4446
|
+
},
|
|
4447
|
+
"move-backlog": {
|
|
4448
|
+
kind: "move",
|
|
4449
|
+
taskId: args.taskId,
|
|
4450
|
+
status: "backlog"
|
|
4451
|
+
},
|
|
4452
|
+
archive: {
|
|
4453
|
+
kind: "archive",
|
|
4454
|
+
taskId: args.taskId
|
|
4455
|
+
},
|
|
4456
|
+
restore: {
|
|
4457
|
+
kind: "restore",
|
|
4458
|
+
taskId: args.taskId
|
|
4459
|
+
},
|
|
4460
|
+
delete: {
|
|
4461
|
+
kind: "delete",
|
|
4462
|
+
taskId: args.taskId
|
|
4463
|
+
}
|
|
4464
|
+
}[args.action];
|
|
4465
|
+
if (action === void 0) return refused("unknown-action", "unsupported action " + args.action);
|
|
4466
|
+
if (!host.snapshot().tasks.some((task) => task.id === args.taskId)) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4467
|
+
try {
|
|
4468
|
+
const snapshot = host.apply(crypto.randomUUID(), action, initiator);
|
|
4469
|
+
if (args.action === "delete") return json({
|
|
4470
|
+
ok: true,
|
|
4471
|
+
action: args.action,
|
|
4472
|
+
taskId: args.taskId
|
|
4473
|
+
});
|
|
4474
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4475
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4476
|
+
return json({
|
|
4477
|
+
ok: true,
|
|
4478
|
+
action: args.action,
|
|
4479
|
+
task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4480
|
+
});
|
|
4481
|
+
} catch (error) {
|
|
4482
|
+
return refused("refused", messageOf(error));
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
});
|
|
4486
|
+
}
|
|
4487
|
+
function buildScheduleTool(host) {
|
|
4488
|
+
return defineTool({
|
|
4489
|
+
name: "task_board_schedule",
|
|
4490
|
+
description: [
|
|
4491
|
+
"Arm, change, or disarm a task scheduled runs (5-field cron in the Host local time zone; day-of-month and day-of-week use OR semantics).",
|
|
4492
|
+
"A due scheduled task runs the same cascade a manual run does, so a root task runs its whole subtask tree.",
|
|
4493
|
+
"A schedule whose tree contains an unconfirmed above-default permission is refused and rolls to its next occurrence; the reason is reported in the list board summary as schedulerError.",
|
|
4494
|
+
"Missed occurrences during Host downtime are skipped, never queued; a task that is already running skips its occurrence.",
|
|
4495
|
+
"Triggers: 任务看板, task board, 看板, 定时任务, schedule, cron, 每天执行."
|
|
4496
|
+
].join(" "),
|
|
4497
|
+
parameters: {
|
|
4498
|
+
taskId: {
|
|
4499
|
+
type: "string",
|
|
4500
|
+
required: true,
|
|
4501
|
+
description: "The task whose schedule changes."
|
|
4502
|
+
},
|
|
4503
|
+
enabled: {
|
|
4504
|
+
type: "boolean",
|
|
4505
|
+
description: "Arm (true) or disarm (false) the schedule."
|
|
4506
|
+
},
|
|
4507
|
+
cron: {
|
|
4508
|
+
type: "string",
|
|
4509
|
+
description: "5-field cron expression: minute hour day-of-month month day-of-week."
|
|
4510
|
+
}
|
|
4511
|
+
},
|
|
4512
|
+
output: {
|
|
4513
|
+
schema: { type: "json" },
|
|
4514
|
+
render: renderJson
|
|
4515
|
+
},
|
|
4516
|
+
async execute(args, exec) {
|
|
4517
|
+
if (args.enabled === void 0 && args.cron === void 0) return refused("nothing-to-change", "pass enabled and/or cron");
|
|
4518
|
+
if (!host.snapshot().tasks.some((task) => task.id === args.taskId)) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4519
|
+
const patch = {
|
|
4520
|
+
...args.enabled === void 0 ? {} : { enabled: args.enabled },
|
|
4521
|
+
...args.cron === void 0 ? {} : { cron: args.cron }
|
|
4522
|
+
};
|
|
4523
|
+
try {
|
|
4524
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4525
|
+
kind: "set-schedule",
|
|
4526
|
+
taskId: args.taskId,
|
|
4527
|
+
patch
|
|
4528
|
+
}, callingSessionId(exec));
|
|
4529
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4530
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4531
|
+
return json({
|
|
4532
|
+
ok: true,
|
|
4533
|
+
schedule: task.schedule === void 0 ? null : scheduleView(task.schedule),
|
|
4534
|
+
task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4535
|
+
});
|
|
4536
|
+
} catch (error) {
|
|
4537
|
+
return refused("refused", messageOf(error));
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
});
|
|
4541
|
+
}
|
|
4542
|
+
function buildListTool(host) {
|
|
4543
|
+
return defineTool({
|
|
4544
|
+
name: "task_board_list",
|
|
4545
|
+
description: [
|
|
4546
|
+
"List tasks on the DSH task board (the Host-authoritative ledger behind the Web GUI task board).",
|
|
4547
|
+
"Filters: status column, parentId (direct subtasks), rootsOnly, tag, free-text query, and archived inclusion.",
|
|
4548
|
+
"Returns each task id, title, column, parent/subtask link, execution targets, schedule and latest run outcome, plus the board summary (revision, time zone, subtask depth limit, session-default permission).",
|
|
4549
|
+
"Use task_board_get for one task in full, task_board_create to add work, and task_board_run to execute it.",
|
|
4550
|
+
"Triggers: 任务看板, task board, 看板, 子任务, subtask, task list, 定时任务, scheduled task."
|
|
4551
|
+
].join(" "),
|
|
4552
|
+
parameters: {
|
|
4553
|
+
status: {
|
|
4554
|
+
type: "string",
|
|
4555
|
+
enum: [
|
|
4556
|
+
"backlog",
|
|
4557
|
+
"todo",
|
|
4558
|
+
"running",
|
|
4559
|
+
"done",
|
|
4560
|
+
"failed"
|
|
4561
|
+
],
|
|
4562
|
+
description: "Only cards in this column."
|
|
4563
|
+
},
|
|
4564
|
+
parentId: {
|
|
4565
|
+
type: "string",
|
|
4566
|
+
description: "Only the direct subtasks of this task id."
|
|
4567
|
+
},
|
|
4568
|
+
rootsOnly: {
|
|
4569
|
+
type: "boolean",
|
|
4570
|
+
description: "Only tasks that have no parent (default false)."
|
|
4571
|
+
},
|
|
4572
|
+
includeArchived: {
|
|
4573
|
+
type: "boolean",
|
|
4574
|
+
description: "Include archived tasks (default false)."
|
|
4575
|
+
},
|
|
4576
|
+
tag: {
|
|
4577
|
+
type: "string",
|
|
4578
|
+
description: "Only tasks carrying this label name."
|
|
4579
|
+
},
|
|
4580
|
+
query: {
|
|
4581
|
+
type: "string",
|
|
4582
|
+
description: "Case-insensitive match against title, description, prompt, and label names."
|
|
4583
|
+
},
|
|
4584
|
+
limit: {
|
|
4585
|
+
type: "integer",
|
|
4586
|
+
description: "Maximum rows to return (default 50, maximum 200)."
|
|
4587
|
+
}
|
|
4588
|
+
},
|
|
4589
|
+
output: {
|
|
4590
|
+
schema: { type: "json" },
|
|
4591
|
+
render: renderJson
|
|
4592
|
+
},
|
|
4593
|
+
isConcurrencySafe: () => true,
|
|
4594
|
+
async execute(args) {
|
|
4595
|
+
const snapshot = host.snapshot();
|
|
4596
|
+
const needle = typeof args.query === "string" ? args.query.trim().toLowerCase() : "";
|
|
4597
|
+
const limit = Math.min(Math.max(typeof args.limit === "number" ? Math.trunc(args.limit) : 50, 1), 200);
|
|
4598
|
+
const matched = snapshot.tasks.filter((task) => {
|
|
4599
|
+
if (args.includeArchived !== true && task.archivedAt !== void 0) return false;
|
|
4600
|
+
if (args.status !== void 0 && task.status !== args.status) return false;
|
|
4601
|
+
if (args.parentId !== void 0 && task.parentId !== args.parentId) return false;
|
|
4602
|
+
if (args.rootsOnly === true && task.parentId !== void 0) return false;
|
|
4603
|
+
if (args.tag !== void 0 && !(task.tags ?? []).some((tag) => tag.name === args.tag)) return false;
|
|
4604
|
+
if (needle !== "") {
|
|
4605
|
+
if (![
|
|
4606
|
+
task.title,
|
|
4607
|
+
task.description,
|
|
4608
|
+
task.prompt,
|
|
4609
|
+
...(task.tags ?? []).map((tag) => tag.name)
|
|
4610
|
+
].join("\n").toLowerCase().includes(needle)) return false;
|
|
4611
|
+
}
|
|
4612
|
+
return true;
|
|
4613
|
+
});
|
|
4614
|
+
return json({
|
|
4615
|
+
ok: true,
|
|
4616
|
+
board: boardView(snapshot, snapshot.maxSubtaskDepth ?? 1),
|
|
4617
|
+
total: matched.length,
|
|
4618
|
+
count: Math.min(matched.length, limit),
|
|
4619
|
+
tasks: matched.slice(0, limit).map((task) => taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission))
|
|
4620
|
+
});
|
|
4621
|
+
}
|
|
4622
|
+
});
|
|
4623
|
+
}
|
|
4624
|
+
function buildGetTool(host) {
|
|
4625
|
+
return defineTool({
|
|
4626
|
+
name: "task_board_get",
|
|
4627
|
+
description: [
|
|
4628
|
+
"Read one task board card in full: prompt, description, labels, column, parent link, direct subtasks, execution targets, schedule, permission gate state, and the last ten execution attempts with their session ids and outcomes.",
|
|
4629
|
+
"Use it before running or editing a card, and after a run to read the settled outcome.",
|
|
4630
|
+
"Triggers: 任务看板, task board, 看板, 子任务, subtask, task detail."
|
|
4631
|
+
].join(" "),
|
|
4632
|
+
parameters: { taskId: {
|
|
4633
|
+
type: "string",
|
|
4634
|
+
required: true,
|
|
4635
|
+
description: "Task id, as reported by task_board_list or task_board_create."
|
|
4636
|
+
} },
|
|
4637
|
+
output: {
|
|
4638
|
+
schema: { type: "json" },
|
|
4639
|
+
render: renderJson
|
|
4640
|
+
},
|
|
4641
|
+
isConcurrencySafe: () => true,
|
|
4642
|
+
async execute(args) {
|
|
4643
|
+
const snapshot = host.snapshot();
|
|
4644
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4645
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4646
|
+
return json({
|
|
4647
|
+
ok: true,
|
|
4648
|
+
task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
function buildCreateTool(host) {
|
|
4654
|
+
return defineTool({
|
|
4655
|
+
name: "task_board_create",
|
|
4656
|
+
description: [
|
|
4657
|
+
"Create a task on the DSH task board, or a subtask by passing parentId.",
|
|
4658
|
+
"A subtask inherits every execution target it does not override (workspace, agent preset, permission, model) from its parent, and inherits the parent human permission confirmation together with an inherited binding.",
|
|
4659
|
+
"The deployment caps subtask depth (default one level, maximum three): a subtask of a subtask is refused unless the board is configured deeper.",
|
|
4660
|
+
"The task lands in the todo column and does NOT run; call task_board_run to execute it.",
|
|
4661
|
+
"Triggers: 任务看板, task board, 看板, 新建任务, create task, 子任务, subtask, 定时任务."
|
|
4662
|
+
].join(" "),
|
|
4663
|
+
parameters: {
|
|
4664
|
+
title: {
|
|
4665
|
+
type: "string",
|
|
4666
|
+
required: true,
|
|
4667
|
+
description: "Short display title (one line)."
|
|
4668
|
+
},
|
|
4669
|
+
description: {
|
|
4670
|
+
type: "string",
|
|
4671
|
+
description: "Longer human description shown in the detail view."
|
|
4672
|
+
},
|
|
4673
|
+
prompt: {
|
|
4674
|
+
type: "string",
|
|
4675
|
+
description: "The instruction sent to the execution agent; the title is used when blank."
|
|
4676
|
+
},
|
|
4677
|
+
parentId: {
|
|
4678
|
+
type: "string",
|
|
4679
|
+
description: "Parent task id, making this a subtask. Omit for a root task."
|
|
4680
|
+
},
|
|
4681
|
+
workspaceId: {
|
|
4682
|
+
type: "string",
|
|
4683
|
+
description: "Workspace id the execution must run in; omit to inherit the parent value or use the most recent workspace."
|
|
4684
|
+
},
|
|
4685
|
+
mode: {
|
|
4686
|
+
type: "string",
|
|
4687
|
+
description: "Agent preset id the execution session is composed from; omit for the deployment default or the parent value."
|
|
4688
|
+
},
|
|
4689
|
+
permission: {
|
|
4690
|
+
type: "string",
|
|
4691
|
+
enum: [...TASK_PERMISSIONS],
|
|
4692
|
+
description: "Permission preset for the execution session. Omit to inherit the parent binding; a value above the session default needs a human confirmation in the board UI before the card can run."
|
|
4693
|
+
},
|
|
4694
|
+
model: {
|
|
4695
|
+
type: "string",
|
|
4696
|
+
description: "Pinned model as provider/model (or a model id); omit for the host default or the parent value."
|
|
4697
|
+
},
|
|
4698
|
+
reuseSession: {
|
|
4699
|
+
type: "boolean",
|
|
4700
|
+
description: "Continue later runs in the previous execution session instead of a fresh conversation."
|
|
4701
|
+
},
|
|
4702
|
+
teamRun: {
|
|
4703
|
+
type: "boolean",
|
|
4704
|
+
description: "Run this task as an Agent Team: running it starts one Team Lead session and the Host spawns a teammate per subtask inside it. Omit or false for one independent session per member. Refused when the deployment serves no Agent Teams service."
|
|
4705
|
+
},
|
|
4706
|
+
tags: {
|
|
4707
|
+
type: "array",
|
|
4708
|
+
description: "Labels: the name renders as a badge and drives the board filter; promptPrefix is injected ahead of the execution prompt on every run.",
|
|
4709
|
+
items: {
|
|
4710
|
+
type: "object",
|
|
4711
|
+
additionalProperties: false,
|
|
4712
|
+
properties: {
|
|
4713
|
+
name: {
|
|
4714
|
+
type: "string",
|
|
4715
|
+
required: true,
|
|
4716
|
+
description: "Label name (unique within the task, at most 32 characters)."
|
|
4717
|
+
},
|
|
4718
|
+
promptPrefix: {
|
|
4719
|
+
type: "string",
|
|
4720
|
+
description: "Optional prompt line injected before the task prompt (at most 200 characters)."
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
},
|
|
4725
|
+
schedule: {
|
|
4726
|
+
type: "object",
|
|
4727
|
+
additionalProperties: false,
|
|
4728
|
+
description: "Arm a 5-field cron schedule at creation time (Host local time zone).",
|
|
4729
|
+
properties: {
|
|
4730
|
+
enabled: {
|
|
4731
|
+
type: "boolean",
|
|
4732
|
+
required: true,
|
|
4733
|
+
description: "Whether the schedule is armed."
|
|
4734
|
+
},
|
|
4735
|
+
cron: {
|
|
4736
|
+
type: "string",
|
|
4737
|
+
required: true,
|
|
4738
|
+
description: "5-field cron: minute hour day-of-month month day-of-week."
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
},
|
|
4743
|
+
output: {
|
|
4744
|
+
schema: { type: "json" },
|
|
4745
|
+
render: renderJson
|
|
4746
|
+
},
|
|
4747
|
+
async execute(args, exec) {
|
|
4748
|
+
if (args.title.trim() === "") return refused("title-required", "title must not be blank");
|
|
4749
|
+
const id = crypto.randomUUID();
|
|
4750
|
+
const tags = (args.tags ?? []).map((tag) => ({
|
|
4751
|
+
name: tag.name,
|
|
4752
|
+
...tag.promptPrefix === void 0 ? {} : { promptPrefix: tag.promptPrefix }
|
|
4753
|
+
}));
|
|
4754
|
+
const input = {
|
|
4755
|
+
title: args.title,
|
|
4756
|
+
description: args.description ?? "",
|
|
4757
|
+
prompt: args.prompt ?? "",
|
|
4758
|
+
...args.parentId === void 0 || args.parentId === "" ? {} : { parentId: args.parentId },
|
|
4759
|
+
...args.workspaceId === void 0 || args.workspaceId === "" ? {} : { workspaceId: args.workspaceId },
|
|
4760
|
+
...args.mode === void 0 || args.mode === "" ? {} : { mode: args.mode },
|
|
4761
|
+
...args.permission === void 0 ? {} : { permission: args.permission },
|
|
4762
|
+
...args.model === void 0 || args.model === "" ? {} : { model: args.model },
|
|
4763
|
+
...args.reuseSession === true ? { reuseSession: true } : {},
|
|
4764
|
+
...args.teamRun === true ? { teamRun: true } : {},
|
|
4765
|
+
...tags.length === 0 ? {} : { tags },
|
|
4766
|
+
...args.schedule === void 0 ? {} : { schedule: {
|
|
4767
|
+
enabled: args.schedule.enabled,
|
|
4768
|
+
cron: args.schedule.cron
|
|
4769
|
+
} }
|
|
4770
|
+
};
|
|
4771
|
+
try {
|
|
4772
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4773
|
+
kind: "create",
|
|
4774
|
+
id,
|
|
4775
|
+
input
|
|
4776
|
+
}, callingSessionId(exec));
|
|
4777
|
+
const task = snapshot.tasks.find((item) => item.id === id);
|
|
4778
|
+
if (task === void 0) return refused("create-failed", "the Host did not confirm the new task");
|
|
4779
|
+
return json({
|
|
4780
|
+
ok: true,
|
|
4781
|
+
task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4782
|
+
});
|
|
4783
|
+
} catch (error) {
|
|
4784
|
+
return refused("refused", messageOf(error));
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
function buildUpdateTool(host) {
|
|
4790
|
+
return defineTool({
|
|
4791
|
+
name: "task_board_update",
|
|
4792
|
+
description: [
|
|
4793
|
+
"Edit a task board card: title, description, prompt, labels, and the execution targets (workspace, agent preset, permission, model, session reuse).",
|
|
4794
|
+
"Only the fields you pass change; pass an empty string to clear a target, or an empty labels array to clear the labels.",
|
|
4795
|
+
"Title, description and prompt freeze once the task has started executing; the execution targets stay editable because they only affect future runs.",
|
|
4796
|
+
"Changing the pinned permission re-arms the permission confirmation gate.",
|
|
4797
|
+
"Triggers: 任务看板, task board, 看板, 修改任务, edit task, 子任务, 标签, tags."
|
|
4798
|
+
].join(" "),
|
|
4799
|
+
parameters: {
|
|
4800
|
+
taskId: {
|
|
4801
|
+
type: "string",
|
|
4802
|
+
required: true,
|
|
4803
|
+
description: "Task id to edit."
|
|
4804
|
+
},
|
|
4805
|
+
title: {
|
|
4806
|
+
type: "string",
|
|
4807
|
+
description: "New title (a blank title is refused by the Host)."
|
|
4808
|
+
},
|
|
4809
|
+
description: {
|
|
4810
|
+
type: "string",
|
|
4811
|
+
description: "New description."
|
|
4812
|
+
},
|
|
4813
|
+
prompt: {
|
|
4814
|
+
type: "string",
|
|
4815
|
+
description: "New execution prompt."
|
|
4816
|
+
},
|
|
4817
|
+
workspaceId: {
|
|
4818
|
+
type: "string",
|
|
4819
|
+
description: "New workspace id; an empty string clears it."
|
|
4820
|
+
},
|
|
4821
|
+
mode: {
|
|
4822
|
+
type: "string",
|
|
4823
|
+
description: "New agent preset id; an empty string clears it."
|
|
4824
|
+
},
|
|
4825
|
+
permission: {
|
|
4826
|
+
type: "string",
|
|
4827
|
+
enum: [...TASK_PERMISSIONS, ""],
|
|
4828
|
+
description: "New permission preset; an empty string clears it back to the session default."
|
|
4829
|
+
},
|
|
4830
|
+
model: {
|
|
4831
|
+
type: "string",
|
|
4832
|
+
description: "New pinned model; an empty string clears it."
|
|
4833
|
+
},
|
|
4834
|
+
reuseSession: {
|
|
4835
|
+
type: "boolean",
|
|
4836
|
+
description: "Continue later runs in the previous execution session."
|
|
4837
|
+
},
|
|
4838
|
+
teamRun: {
|
|
4839
|
+
type: "boolean",
|
|
4840
|
+
description: "Switches this task between a plain cascade (one session per member) and an Agent Team run (Lead session plus a teammate per subtask)."
|
|
4841
|
+
},
|
|
4842
|
+
tags: {
|
|
4843
|
+
type: "array",
|
|
4844
|
+
description: "Replacement label set; an empty array clears all labels.",
|
|
4845
|
+
items: {
|
|
4846
|
+
type: "object",
|
|
4847
|
+
additionalProperties: false,
|
|
4848
|
+
properties: {
|
|
4849
|
+
name: {
|
|
4850
|
+
type: "string",
|
|
4851
|
+
required: true,
|
|
4852
|
+
description: "Label name."
|
|
4853
|
+
},
|
|
4854
|
+
promptPrefix: {
|
|
4855
|
+
type: "string",
|
|
4856
|
+
description: "Optional prompt line injected before the task prompt."
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
},
|
|
4862
|
+
output: {
|
|
4863
|
+
schema: { type: "json" },
|
|
4864
|
+
render: renderJson
|
|
4865
|
+
},
|
|
4866
|
+
async execute(args, exec) {
|
|
4867
|
+
const patch = {};
|
|
4868
|
+
if (has(args, "title") && typeof args.title === "string") patch.title = args.title;
|
|
4869
|
+
if (has(args, "description") && typeof args.description === "string") patch.description = args.description;
|
|
4870
|
+
if (has(args, "prompt") && typeof args.prompt === "string") patch.prompt = args.prompt;
|
|
4871
|
+
if (has(args, "workspaceId") && typeof args.workspaceId === "string") patch.workspaceId = args.workspaceId;
|
|
4872
|
+
if (has(args, "mode") && typeof args.mode === "string") patch.mode = args.mode;
|
|
4873
|
+
if (has(args, "model") && typeof args.model === "string") patch.model = args.model;
|
|
4874
|
+
if (has(args, "permission") && typeof args.permission === "string") patch.permission = args.permission === "" ? void 0 : args.permission;
|
|
4875
|
+
if (has(args, "reuseSession")) patch.reuseSession = args.reuseSession === true;
|
|
4876
|
+
if (has(args, "teamRun")) patch.teamRun = args.teamRun === true;
|
|
4877
|
+
if (has(args, "tags")) {
|
|
4878
|
+
const tags = args.tags ?? [];
|
|
4879
|
+
patch.tags = tags.length === 0 ? null : tags.map((tag) => ({
|
|
4880
|
+
name: tag.name,
|
|
4881
|
+
...tag.promptPrefix === void 0 ? {} : { promptPrefix: tag.promptPrefix }
|
|
4882
|
+
}));
|
|
4883
|
+
}
|
|
4884
|
+
if (Object.keys(patch).length === 0) return refused("nothing-to-change", "pass at least one field to update");
|
|
4885
|
+
try {
|
|
4886
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4887
|
+
kind: "update",
|
|
4888
|
+
taskId: args.taskId,
|
|
4889
|
+
patch
|
|
4890
|
+
}, callingSessionId(exec));
|
|
4891
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4892
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4893
|
+
return json({
|
|
4894
|
+
ok: true,
|
|
4895
|
+
task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4896
|
+
});
|
|
4897
|
+
} catch (error) {
|
|
4898
|
+
return refused("refused", messageOf(error));
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
});
|
|
4902
|
+
}
|
|
4903
|
+
//#endregion
|
|
4557
4904
|
//#region src/http.ts
|
|
4558
4905
|
/** Family-default JSON response headers; callers may append or override. */
|
|
4559
4906
|
const JSON_HEADERS = {
|
|
@@ -4656,13 +5003,47 @@ function resolveAccess(access) {
|
|
|
4656
5003
|
};
|
|
4657
5004
|
}
|
|
4658
5005
|
/**
|
|
5006
|
+
* Harness browser-auth cookie prefix (dsh-client-connection): the
|
|
5007
|
+
* authority-bound signed cookie the Host mints in exchange for its launch
|
|
5008
|
+
* token. dsh-web's remote channel redeems that token before re-issuing paired
|
|
5009
|
+
* traffic to 127.0.0.1 (packages/dsh-remote-web-ui/src/inner-auth.ts), and the
|
|
5010
|
+
* official DSH Desktop shell keeps its own copy from the same exchange and
|
|
5011
|
+
* attaches it to every request it forwards for the dsh-app://app/ page. The
|
|
5012
|
+
* two constants describe one fact and move together.
|
|
5013
|
+
*/
|
|
5014
|
+
const BROWSER_AUTH_COOKIE_PREFIX = "dsh-auth-";
|
|
5015
|
+
/**
|
|
5016
|
+
* Whether a Cookie header carries the Host's browser-auth credential. Only an
|
|
5017
|
+
* application on this machine can hold it: the desktop shell never lets it
|
|
5018
|
+
* reach the page's cookie jar, and SameSite=Strict keeps a cross-site page
|
|
5019
|
+
* from attaching it.
|
|
5020
|
+
* @param header - the raw Cookie header value.
|
|
5021
|
+
*/
|
|
5022
|
+
function carriesBrowserAuthCookie(header) {
|
|
5023
|
+
if (header === void 0) return false;
|
|
5024
|
+
return header.split(";").some((segment) => segment.trim().startsWith(BROWSER_AUTH_COOKIE_PREFIX));
|
|
5025
|
+
}
|
|
5026
|
+
/**
|
|
4659
5027
|
* Browser-signal tripwire, NOT an authority check: a bare curl sends neither
|
|
4660
5028
|
* header and is refused, but a curl with a forged Origin passes this too.
|
|
4661
5029
|
* The real boundary is the loopback socket + Host + origin-equality checks
|
|
4662
5030
|
* in isTrustedTaskBoardRequest below; do not rely on this marker alone.
|
|
5031
|
+
*
|
|
5032
|
+
* A first-party client that presents NEITHER header must still pass. The DSH
|
|
5033
|
+
* Desktop shell serves the Web GUI from dsh-app://app/ and forwards that
|
|
5034
|
+
* page's Host requests itself, deleting origin and sec-fetch-site on the
|
|
5035
|
+
* way (dsh-desktop-host's forwardWebRequest), so the panel's own fetch
|
|
5036
|
+
* arrives marker-less and every route behind this guard answered 403 - which
|
|
5037
|
+
* the panel renders as its board.hostError.unauthorized message. The shell
|
|
5038
|
+
* does attach the Host's browser-auth cookie, redeemed from the Host's launch
|
|
5039
|
+
* URL at startup and deliberately withheld from the page. That credential,
|
|
5040
|
+
* not a header the shell strips, is the browser signal of an application on
|
|
5041
|
+
* this machine; a marker-less, credential-less request stays refused.
|
|
4663
5042
|
*/
|
|
4664
5043
|
function browserSameOriginMarker(req) {
|
|
4665
|
-
|
|
5044
|
+
if (req.headers["sec-fetch-site"] === "same-origin") return true;
|
|
5045
|
+
if (typeof req.headers.origin === "string") return true;
|
|
5046
|
+
return carriesBrowserAuthCookie(req.headers.cookie);
|
|
4666
5047
|
}
|
|
4667
5048
|
function sameAuthority(req, host) {
|
|
4668
5049
|
if (req.headers["sec-fetch-site"] === "cross-site") return false;
|
|
@@ -4874,38 +5255,111 @@ function makeTaskBoardRoutes(service, access = {}, options = {}) {
|
|
|
4874
5255
|
* the loader accepts a standalone install of the same package side by side;
|
|
4875
5256
|
* without this guard the second instance would still re-register the same
|
|
4876
5257
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
4877
|
-
* and fail the boot. mountOnce makes
|
|
4878
|
-
*
|
|
4879
|
-
* package name in the client module host).
|
|
5258
|
+
* and fail the boot. mountOnce makes a mount of an already-mounted package a
|
|
5259
|
+
* no-op for as long as the first instance lives (the browser half is already
|
|
5260
|
+
* deduped by package name in the client module host).
|
|
5261
|
+
*
|
|
5262
|
+
* A no-op is only safe while the holder is ALIVE. The holder can be disposed
|
|
5263
|
+
* long after the refused mount ran its course: the Host reloads a profile by
|
|
5264
|
+
* creating the new loader entries before the old ones are torn down (a
|
|
5265
|
+
* plugin-manager enable/disable/install write, a settings-driven row reload,
|
|
5266
|
+
* HMR), so the new aggregate shell entry mounts its family plugin while the
|
|
5267
|
+
* previous entry still owns the name. Dropping that refused mount lost the
|
|
5268
|
+
* plugin for good - the previous entry then disposed its own mount, releasing
|
|
5269
|
+
* the name with nobody left to take it, and the family row stayed listed as
|
|
5270
|
+
* active while its host routes 404ed (the task-board panel showed
|
|
5271
|
+
* "board.hostError.notMounted", no degraded record appeared, and only a Host
|
|
5272
|
+
* restart recovered it).
|
|
5273
|
+
*
|
|
5274
|
+
* The refused mount is therefore QUEUED, not dropped, and replayed the moment
|
|
5275
|
+
* the holder releases the name - if the waiting fiber is still alive then. The
|
|
5276
|
+
* single-instance guarantee is unchanged: exactly one mount is live per
|
|
5277
|
+
* package name, and the replay re-enters the guard so a later mount still
|
|
5278
|
+
* dedupes against it.
|
|
4880
5279
|
*
|
|
4881
5280
|
* The registry rides a global symbol so two module instances of the same
|
|
4882
|
-
* package (npm copy vs repository link) still share one verdict.
|
|
4883
|
-
*
|
|
5281
|
+
* package (npm copy vs repository link) still share one verdict. That symbol
|
|
5282
|
+
* is a CROSS-REPOSITORY contract, not this file's private state: the four
|
|
5283
|
+
* satellite packages (dsh-skins / dsh-pet / dsh-presets /
|
|
5284
|
+
* dsh-community-plugins) are separate repositories carrying their own copy of
|
|
5285
|
+
* this guard, rebuilt on their own schedule, so the value under `MOUNTED`
|
|
5286
|
+
* must keep the shape every published copy reads (a `Set` of package names
|
|
5287
|
+
* with `has`/`add`/`delete`). The wait queues this guard added therefore live
|
|
5288
|
+
* under their own additive key, and the registry reads back a `Set` even when
|
|
5289
|
+
* some other build left a different value there. Changing `MOUNTED`'s shape
|
|
5290
|
+
* in place broke that contract once: a satellite's legacy copy created a
|
|
5291
|
+
* `Set`, the family's new copy read it as a `Map`, and every family row
|
|
5292
|
+
* mounted after it failed with "claims.get is not a function".
|
|
5293
|
+
*
|
|
5294
|
+
* cordis `ctx.effect` runs its callback immediately and treats the callback's
|
|
4884
5295
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
4885
5296
|
*/
|
|
5297
|
+
/** Published cross-repository contract: package names currently mounted. */
|
|
4886
5298
|
const MOUNTED = Symbol.for("dsh-web.mounted-plugins");
|
|
5299
|
+
/** Additive key this guard owns: refused mounts waiting for the name. */
|
|
5300
|
+
const WAITERS = Symbol.for("dsh-web.mounted-plugins.waiters");
|
|
5301
|
+
/**
|
|
5302
|
+
* The shared name registry, always a `Set` whatever another build stored here:
|
|
5303
|
+
* a foreign value (an interim shape, a hand-written global) must not take every
|
|
5304
|
+
* family plugin down with it.
|
|
5305
|
+
* @returns the process-wide set of mounted package names.
|
|
5306
|
+
*/
|
|
4887
5307
|
function mountedSet() {
|
|
4888
5308
|
const registry = globalThis;
|
|
4889
|
-
|
|
5309
|
+
const existing = registry[MOUNTED];
|
|
5310
|
+
if (existing instanceof Set) return existing;
|
|
5311
|
+
const created = /* @__PURE__ */ new Set();
|
|
5312
|
+
registry[MOUNTED] = created;
|
|
5313
|
+
return created;
|
|
5314
|
+
}
|
|
5315
|
+
/** Queue per package name for mounts refused while a holder was alive. */
|
|
5316
|
+
function mountWaiters() {
|
|
5317
|
+
const registry = globalThis;
|
|
5318
|
+
return registry[WAITERS] ??= /* @__PURE__ */ new Map();
|
|
4890
5319
|
}
|
|
4891
5320
|
/**
|
|
4892
5321
|
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
4893
|
-
* The first mount registers normally and
|
|
4894
|
-
*
|
|
5322
|
+
* The first mount registers normally and releases the name when its fiber
|
|
5323
|
+
* disposes; a mount refused while that name is held waits for the release and
|
|
5324
|
+
* then runs, unless its own fiber disposes first.
|
|
4895
5325
|
* @param packageName - npm package identity shared by every install source.
|
|
4896
5326
|
* @param fn - the original plugin apply.
|
|
4897
5327
|
* @returns an apply of the same shape.
|
|
4898
5328
|
*/
|
|
4899
5329
|
function mountOnce(packageName, fn) {
|
|
4900
|
-
|
|
5330
|
+
const mount = (...args) => {
|
|
4901
5331
|
const mounted = mountedSet();
|
|
4902
|
-
|
|
5332
|
+
const ctx = args[0];
|
|
5333
|
+
if (mounted.has(packageName)) {
|
|
5334
|
+
const waiters = mountWaiters();
|
|
5335
|
+
const queue = waiters.get(packageName) ?? [];
|
|
5336
|
+
let alive = true;
|
|
5337
|
+
const pending = { run: () => {
|
|
5338
|
+
if (alive) mount(...args);
|
|
5339
|
+
} };
|
|
5340
|
+
ctx?.effect?.(() => () => {
|
|
5341
|
+
alive = false;
|
|
5342
|
+
const index = queue.indexOf(pending);
|
|
5343
|
+
if (index >= 0) queue.splice(index, 1);
|
|
5344
|
+
});
|
|
5345
|
+
queue.push(pending);
|
|
5346
|
+
waiters.set(packageName, queue);
|
|
5347
|
+
return;
|
|
5348
|
+
}
|
|
4903
5349
|
mounted.add(packageName);
|
|
4904
|
-
|
|
5350
|
+
ctx?.effect?.(() => () => {
|
|
4905
5351
|
mounted.delete(packageName);
|
|
5352
|
+
const waiters = mountWaiters();
|
|
5353
|
+
const queue = waiters.get(packageName);
|
|
5354
|
+
if (queue === void 0) return;
|
|
5355
|
+
waiters.delete(packageName);
|
|
5356
|
+
for (const waiter of queue.splice(0)) queueMicrotask(() => {
|
|
5357
|
+
waiter.run();
|
|
5358
|
+
});
|
|
4906
5359
|
});
|
|
4907
5360
|
return fn(...args);
|
|
4908
|
-
}
|
|
5361
|
+
};
|
|
5362
|
+
return mount;
|
|
4909
5363
|
}
|
|
4910
5364
|
//#endregion
|
|
4911
5365
|
//#region src/index.ts
|
|
@@ -4922,7 +5376,12 @@ const inject = [
|
|
|
4922
5376
|
"commands"
|
|
4923
5377
|
];
|
|
4924
5378
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
4925
|
-
const TASK_BOARD_GUIDANCE = "本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;Host 权威账本;关闭浏览器后仍由 Host 执行和结算;任务可钉住工作区、agent
|
|
5379
|
+
const TASK_BOARD_GUIDANCE = "本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;Host 权威账本;关闭浏览器后仍由 Host 执行和结算;任务可钉住工作区、agent 预设和权限;任务可建子任务(深度上限可配 1..3,默认 1 层,子任务不能再带子任务),执行父任务会并发执行其子任务树,子任务可单独覆盖权限与模型;另注册 task_board_* agent 工具(list/get/create/update/set_parent/run/manage/schedule),任何会话都可直接读写看板、子任务与定时计划,但运行任务会真实执行并消耗额度,高于会话默认权限的绑定仍必须由用户在 GUI 人工确认(工具刻意不提供确认能力);支持 Host 本地时区的 5 段 cron,错过的触发点不补跑;可选且默认关闭的空闲系统睡眠保护允许屏幕熄灭,但不承诺拦截合盖、手动睡眠、休眠、关机或唤醒已睡眠机器。执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。若你同时用 todo_write 维护会话顶部的可见计划列表,最终回复前必须再次调用 todo_write 收尾:没有剩余工作时不要保留 in_progress,已完成的最后一步要标为 completed。";
|
|
5380
|
+
/**
|
|
5381
|
+
* Continuable-subagent provider for fresh teammates, matching the Agent Teams
|
|
5382
|
+
* tool plugin's own `freshProvider` default.
|
|
5383
|
+
*/
|
|
5384
|
+
const DEFAULT_TEAM_PROVIDER = "spawn";
|
|
4926
5385
|
/**
|
|
4927
5386
|
* The schema is left to inference rather than annotated with `z<Config>`: a
|
|
4928
5387
|
* volatile field's parsed output is a `Volatile` reference while its accepted
|
|
@@ -4935,7 +5394,9 @@ const Config = z.object({
|
|
|
4935
5394
|
preventIdleSleep: z.boolean().default(false).volatile(),
|
|
4936
5395
|
trustedProxyHosts: z.array(z.string()).default([]),
|
|
4937
5396
|
proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
|
|
4938
|
-
sessionDefaultPermission: z.union(TASK_PERMISSIONS).default(DEFAULT_SESSION_PERMISSION)
|
|
5397
|
+
sessionDefaultPermission: z.union(TASK_PERMISSIONS).default(DEFAULT_SESSION_PERMISSION),
|
|
5398
|
+
maxSubtaskDepth: z.number().min(1).max(3).default(1).volatile(),
|
|
5399
|
+
teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER)
|
|
4939
5400
|
});
|
|
4940
5401
|
/**
|
|
4941
5402
|
* Read one config field's current value.
|
|
@@ -4966,9 +5427,105 @@ function resolveProxyAccess(config, env = process.env) {
|
|
|
4966
5427
|
proxyToken
|
|
4967
5428
|
};
|
|
4968
5429
|
}
|
|
5430
|
+
/**
|
|
5431
|
+
* Resolve the optional agent-tool registry. The board deliberately does not
|
|
5432
|
+
* inject it: a deployment whose runtime serves no registry must still mount the
|
|
5433
|
+
* whole board and lose only the agent-tool surface, the same tolerance the
|
|
5434
|
+
* optional llm service gets.
|
|
5435
|
+
* @param ctx - the plugin context.
|
|
5436
|
+
* @returns the registry, or undefined when this deployment serves none.
|
|
5437
|
+
*/
|
|
5438
|
+
function resolveToolRegistry(ctx) {
|
|
5439
|
+
try {
|
|
5440
|
+
const tools = ctx.get("tools");
|
|
5441
|
+
return tools !== void 0 && typeof tools.register === "function" ? tools : void 0;
|
|
5442
|
+
} catch {
|
|
5443
|
+
return;
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
/**
|
|
5447
|
+
* Resolve the native cordis timer service (dsh-base's own `cordis-plugin-timer`
|
|
5448
|
+
* row). The board arms its schedule through it so unloading the row clears
|
|
5449
|
+
* every pending timer with the owning fiber, instead of the row tracking raw
|
|
5450
|
+
* process handles by hand. A composition that serves no timer service keeps
|
|
5451
|
+
* working on the process globals.
|
|
5452
|
+
* @param ctx - the plugin context.
|
|
5453
|
+
* @returns the timer face, or undefined when this deployment serves none.
|
|
5454
|
+
*/
|
|
5455
|
+
function resolveHostTimers(ctx) {
|
|
5456
|
+
try {
|
|
5457
|
+
const timer = ctx.get("timer");
|
|
5458
|
+
if (timer === void 0) return void 0;
|
|
5459
|
+
return typeof timer.timeout === "function" && typeof timer.interval === "function" ? {
|
|
5460
|
+
timeout: timer.timeout.bind(timer),
|
|
5461
|
+
interval: timer.interval.bind(timer)
|
|
5462
|
+
} : void 0;
|
|
5463
|
+
} catch {
|
|
5464
|
+
return;
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
4969
5467
|
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
4970
5468
|
const DEFAULT_ANNOUNCE = false;
|
|
4971
5469
|
/**
|
|
5470
|
+
* How long one teammate may take to reach its durable active or failed edge.
|
|
5471
|
+
* Agent Teams provisioning normally resolves in seconds; the bound exists so a
|
|
5472
|
+
* hung spawn settles the subtask instead of holding the cascade open forever.
|
|
5473
|
+
*/
|
|
5474
|
+
const TEAMMATE_PROVISION_TIMEOUT_MS = 6e5;
|
|
5475
|
+
/**
|
|
5476
|
+
* Resolve the optional Agent Teams service. Like the tool registry and the llm
|
|
5477
|
+
* runtime, it is deliberately not injected: a deployment without Agent Teams
|
|
5478
|
+
* must still mount the whole board and only lose team-mode runs.
|
|
5479
|
+
* @param ctx - the plugin context.
|
|
5480
|
+
* @returns the service, or undefined when this deployment serves none.
|
|
5481
|
+
*/
|
|
5482
|
+
function resolveAgentTeams(ctx) {
|
|
5483
|
+
try {
|
|
5484
|
+
const teams = ctx.get("agentTeams");
|
|
5485
|
+
return teams !== void 0 && typeof teams.spawnTeammate === "function" ? teams : void 0;
|
|
5486
|
+
} catch {
|
|
5487
|
+
return;
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
/**
|
|
5491
|
+
* Build the team dispatcher: the run's Lead session spawns one teammate per
|
|
5492
|
+
* subtask through the Agent Teams service, and the teammate's session id is
|
|
5493
|
+
* attached to that subtask's execution so the ordinary session monitor settles
|
|
5494
|
+
* it. Returns undefined when the deployment serves no Agent Teams service or
|
|
5495
|
+
* no live-agent registry, which makes a team run fail closed.
|
|
5496
|
+
* @param ctx - the plugin context.
|
|
5497
|
+
* @param provider - the continuable-subagent provider for fresh teammates.
|
|
5498
|
+
* @returns the dispatcher, or undefined when team runs cannot be served.
|
|
5499
|
+
*/
|
|
5500
|
+
function buildTeamDispatcher(ctx, provider) {
|
|
5501
|
+
const teams = resolveAgentTeams(ctx);
|
|
5502
|
+
if (teams === void 0) return void 0;
|
|
5503
|
+
if (typeof ctx.agents?.get !== "function") return void 0;
|
|
5504
|
+
return { async spawn(input) {
|
|
5505
|
+
const lead = ctx.agents.get(input.leadSessionId);
|
|
5506
|
+
if (lead === void 0) return { error: `lead session ${input.leadSessionId} is not live` };
|
|
5507
|
+
try {
|
|
5508
|
+
const member = (await teams.spawnTeammate(lead, {
|
|
5509
|
+
name: input.name,
|
|
5510
|
+
description: input.description,
|
|
5511
|
+
prompt: [{
|
|
5512
|
+
type: "text",
|
|
5513
|
+
text: input.prompt
|
|
5514
|
+
}],
|
|
5515
|
+
context: "fresh",
|
|
5516
|
+
provider,
|
|
5517
|
+
signal: AbortSignal.timeout(TEAMMATE_PROVISION_TIMEOUT_MS)
|
|
5518
|
+
}))?.member;
|
|
5519
|
+
if (member === void 0) return { error: "Agent Teams returned no teammate" };
|
|
5520
|
+
if (member.phase === "failed") return { error: member.error ?? "teammate provisioning failed" };
|
|
5521
|
+
if (typeof member.id !== "string" || member.id === "") return { error: "Agent Teams returned no teammate session" };
|
|
5522
|
+
return { sessionId: member.id };
|
|
5523
|
+
} catch (error) {
|
|
5524
|
+
return { error: `teammate provisioning failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
5525
|
+
}
|
|
5526
|
+
} };
|
|
5527
|
+
}
|
|
5528
|
+
/**
|
|
4972
5529
|
* Read the optional `llm` service. The board deliberately does not inject it:
|
|
4973
5530
|
* a deployment without a model must still mount the board, and the parse route
|
|
4974
5531
|
* answers a typed failure instead of the plugin failing to load (issue #1540).
|
|
@@ -4987,8 +5544,8 @@ function resolveLlmRuntime(ctx) {
|
|
|
4987
5544
|
* Activate the board's host half: ledger, routes, cron scheduler, power
|
|
4988
5545
|
* inhibitor, and the model-facing announcement.
|
|
4989
5546
|
*
|
|
4990
|
-
* The effective settings are the config the Host hands this row. The
|
|
4991
|
-
*
|
|
5547
|
+
* The effective settings are the config the Host hands this row. The fields
|
|
5548
|
+
* the browser card edits are schema-volatile, so the Loader commits an
|
|
4992
5549
|
* edit into the running fiber's references instead of remounting the row —
|
|
4993
5550
|
* `sync` therefore reads them at use time and follows
|
|
4994
5551
|
* `loader/volatile-update`, the event the Loader emits once it has committed
|
|
@@ -5004,9 +5561,14 @@ function applyImpl(ctx, config) {
|
|
|
5004
5561
|
const announceToAgent = () => readConfigField(config?.announceToAgent, DEFAULT_ANNOUNCE);
|
|
5005
5562
|
/** Current idle-sleep protection flag. */
|
|
5006
5563
|
const preventIdleSleep = () => readConfigField(config?.preventIdleSleep, false);
|
|
5564
|
+
/** Current subtask depth limit (read live: the settings card edits it in place). */
|
|
5565
|
+
const maxSubtaskDepth = () => readConfigField(config?.maxSubtaskDepth, 1);
|
|
5007
5566
|
const host = new TaskBoardHostService(ctx.typertGateway, {
|
|
5008
5567
|
workspaceRegistry: ctx.workspaceRegistry,
|
|
5009
5568
|
sessionDefaultPermission: config?.sessionDefaultPermission ?? "read-only",
|
|
5569
|
+
maxSubtaskDepth: maxSubtaskDepth(),
|
|
5570
|
+
timers: resolveHostTimers(ctx),
|
|
5571
|
+
team: buildTeamDispatcher(ctx, config?.teamProvider ?? "spawn"),
|
|
5010
5572
|
commandDispatcher: { async execute(sessionId, line, signal) {
|
|
5011
5573
|
const agent = ctx.agents.get(sessionId);
|
|
5012
5574
|
if (agent === void 0) throw new Error(`execution session ${sessionId} is not available`);
|
|
@@ -5015,6 +5577,30 @@ function applyImpl(ctx, config) {
|
|
|
5015
5577
|
});
|
|
5016
5578
|
host.setConfiguration(enabled(), preventIdleSleep());
|
|
5017
5579
|
host.start();
|
|
5580
|
+
let disposeTools;
|
|
5581
|
+
const setToolsEnabled = (active) => {
|
|
5582
|
+
if (!active) {
|
|
5583
|
+
disposeTools?.();
|
|
5584
|
+
disposeTools = void 0;
|
|
5585
|
+
return;
|
|
5586
|
+
}
|
|
5587
|
+
if (disposeTools !== void 0) return;
|
|
5588
|
+
const registry = resolveToolRegistry(ctx);
|
|
5589
|
+
if (registry === void 0) return;
|
|
5590
|
+
const disposers = buildTaskBoardTools(host).map((tool) => registry.register(tool));
|
|
5591
|
+
disposeTools = () => {
|
|
5592
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
5593
|
+
};
|
|
5594
|
+
};
|
|
5595
|
+
setToolsEnabled(enabled());
|
|
5596
|
+
const scopedInject = ctx.inject;
|
|
5597
|
+
if (typeof scopedInject === "function") scopedInject.call(ctx, ["tools"], () => {
|
|
5598
|
+
setToolsEnabled(enabled());
|
|
5599
|
+
return () => {
|
|
5600
|
+
disposeTools?.();
|
|
5601
|
+
disposeTools = void 0;
|
|
5602
|
+
};
|
|
5603
|
+
});
|
|
5018
5604
|
ctx.effect(() => {
|
|
5019
5605
|
const disposers = [];
|
|
5020
5606
|
try {
|
|
@@ -5025,11 +5611,13 @@ function applyImpl(ctx, config) {
|
|
|
5025
5611
|
} });
|
|
5026
5612
|
for (const route of routes) disposers.push(ctx.webServer.register(route));
|
|
5027
5613
|
} catch (error) {
|
|
5614
|
+
setToolsEnabled(false);
|
|
5028
5615
|
for (const dispose of disposers) dispose();
|
|
5029
5616
|
host.dispose();
|
|
5030
5617
|
throw error;
|
|
5031
5618
|
}
|
|
5032
5619
|
return () => {
|
|
5620
|
+
setToolsEnabled(false);
|
|
5033
5621
|
for (const dispose of disposers) dispose();
|
|
5034
5622
|
host.dispose();
|
|
5035
5623
|
};
|
|
@@ -5040,11 +5628,14 @@ function applyImpl(ctx, config) {
|
|
|
5040
5628
|
const next = {
|
|
5041
5629
|
enabled: enabled(),
|
|
5042
5630
|
announceToAgent: announceToAgent(),
|
|
5043
|
-
preventIdleSleep: preventIdleSleep()
|
|
5631
|
+
preventIdleSleep: preventIdleSleep(),
|
|
5632
|
+
maxSubtaskDepth: maxSubtaskDepth()
|
|
5044
5633
|
};
|
|
5045
|
-
if (applied !== void 0 && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep) return;
|
|
5634
|
+
if (applied !== void 0 && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep && applied.maxSubtaskDepth === next.maxSubtaskDepth) return;
|
|
5046
5635
|
applied = next;
|
|
5636
|
+
host.ledger.setMaxSubtaskDepth(next.maxSubtaskDepth);
|
|
5047
5637
|
host.setConfiguration(next.enabled, next.preventIdleSleep);
|
|
5638
|
+
setToolsEnabled(next.enabled);
|
|
5048
5639
|
if (disposeSection !== void 0) {
|
|
5049
5640
|
disposeSection();
|
|
5050
5641
|
disposeSection = void 0;
|
|
@@ -5062,4 +5653,4 @@ function applyImpl(ctx, config) {
|
|
|
5062
5653
|
sync();
|
|
5063
5654
|
}
|
|
5064
5655
|
//#endregion
|
|
5065
|
-
export { Config, DEFAULT_PROXY_TOKEN_ENV, TASK_BOARD_GUIDANCE, apply, inject, readConfigField, resolveLlmRuntime, resolveProxyAccess };
|
|
5656
|
+
export { Config, DEFAULT_PROXY_TOKEN_ENV, DEFAULT_TEAM_PROVIDER, TASK_BOARD_GUIDANCE, apply, buildTeamDispatcher, inject, readConfigField, resolveAgentTeams, resolveHostTimers, resolveLlmRuntime, resolveProxyAccess, resolveToolRegistry };
|