@linxin666/dsh-client-ui-task-board 0.4.1 → 0.4.2
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 +24 -0
- package/README.zh.md +24 -0
- package/lib/client.js +917 -81
- package/lib/client.js.map +1 -1
- package/lib/index.js +1774 -1420
- 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/locales.d.ts +33 -0
- package/lib/types/core/controller.d.ts +30 -1
- 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 +88 -4
- package/lib/types/host-runner.d.ts +39 -1
- package/lib/types/host-service.d.ts +47 -0
- package/lib/types/index.d.ts +83 -5
- package/lib/types/protocol.d.ts +14 -0
- package/package.json +23 -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 +96 -0
- package/src/client/host-api.ts +14 -4
- package/src/client/index.ts +11 -1
- package/src/client/locales.ts +70 -2
- package/src/core/controller.ts +69 -8
- 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 +323 -45
- package/src/host-runner.ts +141 -12
- package/src/host-service.ts +127 -10
- package/src/index.ts +181 -9
- package/src/protocol.ts +26 -2
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,
|
|
@@ -1740,24 +2210,29 @@ var HostTaskLedger = class {
|
|
|
1740
2210
|
throw error;
|
|
1741
2211
|
}
|
|
1742
2212
|
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Open the cascade one due schedule triggers: an empty array means nothing
|
|
2215
|
+
* ran (already running, or a participant whose elevated permission is still
|
|
2216
|
+
* unconfirmed), and the rule rolls to its next occurrence either way.
|
|
2217
|
+
*/
|
|
1743
2218
|
openScheduled(taskId, nextRunAt, triggeredAt) {
|
|
1744
2219
|
const task = this.document.tasks.find((item) => item.id === taskId);
|
|
1745
|
-
if (task === void 0 || task.archivedAt !== void 0) return
|
|
1746
|
-
|
|
2220
|
+
if (task === void 0 || task.archivedAt !== void 0) return [];
|
|
2221
|
+
const rollForward = () => {
|
|
1747
2222
|
this.document.tasks = [...applyScheduleNextRun(this.document.tasks, taskId, nextRunAt, task.schedule?.lastTriggeredAt, triggeredAt)];
|
|
1748
2223
|
this.commit();
|
|
1749
|
-
|
|
2224
|
+
};
|
|
2225
|
+
const refusal = this.bindingRefusal(task);
|
|
2226
|
+
if (refusal !== void 0) {
|
|
2227
|
+
this.document.scheduler.error = `scheduled run refused for task ${taskId}: ${bindingRefusalMessage(refusal, this.sessionDefaultPermission, "schedule")}`;
|
|
2228
|
+
rollForward();
|
|
2229
|
+
return [];
|
|
1750
2230
|
}
|
|
1751
2231
|
if (task.status === "running" || hasOpenExecution(task)) {
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
return;
|
|
2232
|
+
rollForward();
|
|
2233
|
+
return [];
|
|
1755
2234
|
}
|
|
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;
|
|
2235
|
+
return this.startCascade(task, triggeredAt, void 0, false, nextRunAt);
|
|
1761
2236
|
}
|
|
1762
2237
|
skipMissed(now) {
|
|
1763
2238
|
let changed = false;
|
|
@@ -1792,6 +2267,39 @@ var HostTaskLedger = class {
|
|
|
1792
2267
|
}
|
|
1793
2268
|
this.commit(false);
|
|
1794
2269
|
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Settle one execution. A cascade participant first records its OWN outcome
|
|
2272
|
+
* and only finalizes once every child execution in its run group has
|
|
2273
|
+
* settled, so a parent card leaves 'running' with the whole tree's verdict
|
|
2274
|
+
* rather than its own turn alone.
|
|
2275
|
+
*/
|
|
2276
|
+
settle(taskId, executionId, outcome, error) {
|
|
2277
|
+
const now = this.now();
|
|
2278
|
+
const task = this.document.tasks.find((item) => item.id === taskId);
|
|
2279
|
+
const execution = task?.executions.find((entry) => entry.id === executionId);
|
|
2280
|
+
if (task === void 0 || execution === void 0 || execution.endedAt !== void 0) return;
|
|
2281
|
+
const groupId = execution.runGroupId;
|
|
2282
|
+
if (groupId === void 0) {
|
|
2283
|
+
this.document.tasks = this.document.tasks.map((item) => item.id === taskId ? settleExecution(item, executionId, outcome, now, error) : item);
|
|
2284
|
+
this.commit();
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
let changed = false;
|
|
2288
|
+
if (execution.ownResult === void 0) {
|
|
2289
|
+
this.document.tasks = this.document.tasks.map((item) => item.id !== taskId ? item : {
|
|
2290
|
+
...item,
|
|
2291
|
+
updatedAt: now,
|
|
2292
|
+
executions: item.executions.map((entry) => entry.id === executionId ? {
|
|
2293
|
+
...entry,
|
|
2294
|
+
ownResult: outcome,
|
|
2295
|
+
ownError: error
|
|
2296
|
+
} : entry)
|
|
2297
|
+
});
|
|
2298
|
+
changed = true;
|
|
2299
|
+
}
|
|
2300
|
+
if (this.settleCascade(taskId, groupId, now)) changed = true;
|
|
2301
|
+
if (changed) this.commit();
|
|
2302
|
+
}
|
|
1795
2303
|
attachSession(taskId, executionId, sessionId) {
|
|
1796
2304
|
const now = this.now();
|
|
1797
2305
|
this.document.tasks = this.document.tasks.map((task) => task.id !== taskId ? task : {
|
|
@@ -1804,13 +2312,9 @@ var HostTaskLedger = class {
|
|
|
1804
2312
|
});
|
|
1805
2313
|
this.commit();
|
|
1806
2314
|
}
|
|
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
2315
|
apply(action, initiator) {
|
|
1812
2316
|
const now = this.now();
|
|
1813
|
-
let
|
|
2317
|
+
let runs;
|
|
1814
2318
|
switch (action.kind) {
|
|
1815
2319
|
case "import": {
|
|
1816
2320
|
const sources = new Set(this.document.scheduler.importedSources ?? []);
|
|
@@ -1819,7 +2323,7 @@ var HostTaskLedger = class {
|
|
|
1819
2323
|
const incoming = parseHostTasks(action.tasks);
|
|
1820
2324
|
const merged = new Map(this.document.tasks.map((task) => [task.id, task]));
|
|
1821
2325
|
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()];
|
|
2326
|
+
this.document.tasks = repairParentLinks([...merged.values()]);
|
|
1823
2327
|
this.document.scheduler.importedSources = [...sources, action.sourceId];
|
|
1824
2328
|
this.document.scheduler.error = invalidScheduleIds.length === 0 ? void 0 : `invalid cron disabled for task(s): ${invalidScheduleIds.join(", ")}`;
|
|
1825
2329
|
this.repairSchedules(true, false);
|
|
@@ -1836,8 +2340,8 @@ var HostTaskLedger = class {
|
|
|
1836
2340
|
frozenBy: initiator
|
|
1837
2341
|
}
|
|
1838
2342
|
};
|
|
1839
|
-
const result = applyCreateTask(this.document.tasks, input, now, action.id);
|
|
1840
|
-
if (result.task === void 0) throw new Error("invalid task");
|
|
2343
|
+
const result = applyCreateTask(this.document.tasks, input, now, action.id, this.maxSubtaskDepth);
|
|
2344
|
+
if (result.task === void 0) throw new Error(result.error ?? "invalid task");
|
|
1841
2345
|
this.document.tasks = [...result.tasks];
|
|
1842
2346
|
break;
|
|
1843
2347
|
}
|
|
@@ -1857,14 +2361,20 @@ var HostTaskLedger = class {
|
|
|
1857
2361
|
this.document.tasks = [...applyUpdateTask(this.document.tasks, action.taskId, patch, now)];
|
|
1858
2362
|
break;
|
|
1859
2363
|
}
|
|
1860
|
-
case "delete":
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
}
|
|
2364
|
+
case "delete": {
|
|
2365
|
+
const task = this.document.tasks.find((task) => task.id === action.taskId);
|
|
2366
|
+
if (task === void 0) throw new Error("task not found");
|
|
2367
|
+
if (task.status === "running" || hasOpenExecution(task)) throw new Error("running task cannot be deleted");
|
|
2368
|
+
if (this.document.tasks.some((item) => item.parentId === action.taskId)) throw new Error("task has subtasks; detach or delete them first");
|
|
1866
2369
|
this.document.tasks = [...applyDeleteTask(this.document.tasks, void 0, action.taskId).tasks];
|
|
1867
2370
|
break;
|
|
2371
|
+
}
|
|
2372
|
+
case "set-parent": {
|
|
2373
|
+
const result = applySetParent(this.document.tasks, action.taskId, action.parentId, now, this.maxSubtaskDepth);
|
|
2374
|
+
if (!result.applied) throw new Error(result.error ?? "parent link refused");
|
|
2375
|
+
this.document.tasks = [...result.tasks];
|
|
2376
|
+
break;
|
|
2377
|
+
}
|
|
1868
2378
|
case "move": {
|
|
1869
2379
|
const task = this.document.tasks.find((item) => item.id === action.taskId);
|
|
1870
2380
|
if (task === void 0) throw new Error("task not found");
|
|
@@ -1875,13 +2385,15 @@ var HostTaskLedger = class {
|
|
|
1875
2385
|
break;
|
|
1876
2386
|
}
|
|
1877
2387
|
case "archive": {
|
|
1878
|
-
|
|
2388
|
+
if (this.subtreeHasOpenExecution(action.taskId)) throw new Error("running task cannot be archived");
|
|
2389
|
+
const result = applyArchiveTask(this.document.tasks, action.taskId, now, this.maxSubtaskDepth);
|
|
1879
2390
|
if (!result.archived) throw new Error("task cannot be archived");
|
|
1880
2391
|
this.document.tasks = [...result.tasks];
|
|
1881
2392
|
break;
|
|
1882
2393
|
}
|
|
1883
2394
|
case "restore": {
|
|
1884
|
-
|
|
2395
|
+
if (this.subtreeHasOpenExecution(action.taskId)) throw new Error("running task cannot be restored");
|
|
2396
|
+
const result = applyRestoreTask(this.document.tasks, action.taskId, now, this.maxSubtaskDepth);
|
|
1885
2397
|
if (!result.archived) throw new Error("task is not archived");
|
|
1886
2398
|
this.document.tasks = [...result.tasks];
|
|
1887
2399
|
break;
|
|
@@ -1909,18 +2421,140 @@ var HostTaskLedger = class {
|
|
|
1909
2421
|
const task = this.document.tasks.find((item) => item.id === action.taskId);
|
|
1910
2422
|
if (task?.archivedAt !== void 0) throw new Error("archived task is read-only");
|
|
1911
2423
|
if (task === void 0 || task.status === "running" || hasOpenExecution(task)) throw new Error("task is already running or missing");
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2424
|
+
const refusal = this.bindingRefusal(task);
|
|
2425
|
+
if (refusal !== void 0) throw new Error(bindingRefusalMessage(refusal, this.sessionDefaultPermission, "run"));
|
|
2426
|
+
runs = this.startCascade(task, now, initiator, action.kind === "rerun");
|
|
1915
2427
|
break;
|
|
1916
2428
|
}
|
|
1917
2429
|
}
|
|
1918
|
-
this.commit();
|
|
2430
|
+
if (runs === void 0 || runs.length === 0) this.commit();
|
|
1919
2431
|
return {
|
|
1920
2432
|
state: this.state(),
|
|
1921
|
-
...
|
|
2433
|
+
...runs === void 0 ? {} : { runs }
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* The binding that makes a run illegal, if any.
|
|
2438
|
+
*
|
|
2439
|
+
* A plain cascade launches one session per participant, so every participant
|
|
2440
|
+
* carries its own resolved binding and each one gates the run. A team run
|
|
2441
|
+
* launches only the Lead session: the Lead's binding gates it, while a
|
|
2442
|
+
* subtask's OWN above-default pin cannot be applied to a teammate and is
|
|
2443
|
+
* refused instead of being silently dropped (an inherited binding is the
|
|
2444
|
+
* Lead's own and stays allowed once the Lead is confirmed).
|
|
2445
|
+
* @param root - the task being run.
|
|
2446
|
+
* @returns the first refusal, or undefined when the run may start.
|
|
2447
|
+
*/
|
|
2448
|
+
bindingRefusal(root) {
|
|
2449
|
+
const participants = this.cascadeParticipants(root.id);
|
|
2450
|
+
if (root.teamRun === true) {
|
|
2451
|
+
const lead = participants.find((participant) => participant.id === root.id);
|
|
2452
|
+
if (lead !== void 0 && requiresPermissionConfirmation(lead, this.sessionDefaultPermission)) return {
|
|
2453
|
+
kind: "root",
|
|
2454
|
+
title: lead.title
|
|
2455
|
+
};
|
|
2456
|
+
const pinned = participants.find((participant) => {
|
|
2457
|
+
if (participant.id === root.id) return false;
|
|
2458
|
+
const raw = this.document.tasks.find((task) => task.id === participant.id);
|
|
2459
|
+
return raw !== void 0 && requiresPermissionConfirmation(raw, this.sessionDefaultPermission);
|
|
2460
|
+
});
|
|
2461
|
+
return pinned === void 0 ? void 0 : {
|
|
2462
|
+
kind: "subtask-pin",
|
|
2463
|
+
title: pinned.title
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
const unconfirmed = participants.find((participant) => requiresPermissionConfirmation(participant, this.sessionDefaultPermission));
|
|
2467
|
+
if (unconfirmed === void 0) return void 0;
|
|
2468
|
+
return {
|
|
2469
|
+
kind: unconfirmed.id === root.id ? "root" : "inherited",
|
|
2470
|
+
title: unconfirmed.title
|
|
1922
2471
|
};
|
|
1923
2472
|
}
|
|
2473
|
+
/**
|
|
2474
|
+
* The tasks a cascade from `rootId` would actually open executions for, with
|
|
2475
|
+
* their effective execution targets resolved. Archived members never run, and
|
|
2476
|
+
* a member that already has an open execution is skipped (a task cannot run
|
|
2477
|
+
* twice), so the result is the participant set the launch will use.
|
|
2478
|
+
*/
|
|
2479
|
+
cascadeParticipants(rootId) {
|
|
2480
|
+
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));
|
|
2481
|
+
}
|
|
2482
|
+
/** Whether a task or any of its subtasks still has an open execution. */
|
|
2483
|
+
subtreeHasOpenExecution(id) {
|
|
2484
|
+
return cascadeTargets(this.document.tasks, id, this.maxSubtaskDepth).some((task) => task.status === "running" || hasOpenExecution(task));
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Open one execution per cascade participant (the requested task and its
|
|
2488
|
+
* on-board descendants, within the depth limit) under a single run group, and
|
|
2489
|
+
* commit the ledger once. The returned runs are ordered root-first, so the
|
|
2490
|
+
* Host launches the parent before its subtasks.
|
|
2491
|
+
* @param root - the task the user ran.
|
|
2492
|
+
* @param now - clock instant (ms epoch).
|
|
2493
|
+
* @param initiator - the DSH session that asked for the run (audit only).
|
|
2494
|
+
* @param rerun - true to reset the root to 'todo' before starting it.
|
|
2495
|
+
* @param nextRunAt - when set, the root's schedule also rolls forward.
|
|
2496
|
+
*/
|
|
2497
|
+
startCascade(root, now, initiator, rerun = false, nextRunAt) {
|
|
2498
|
+
const before = this.document.tasks;
|
|
2499
|
+
const groupId = crypto.randomUUID();
|
|
2500
|
+
const team = root.teamRun === true;
|
|
2501
|
+
const started = /* @__PURE__ */ new Map();
|
|
2502
|
+
const runs = [];
|
|
2503
|
+
for (const task of cascadeTargets(before, root.id, this.maxSubtaskDepth)) {
|
|
2504
|
+
if (task.archivedAt !== void 0) continue;
|
|
2505
|
+
if (task.status === "running" || hasOpenExecution(task)) continue;
|
|
2506
|
+
const opened = startExecution(rerun && task.id === root.id ? withStatus(task, "todo", now) : task, now, crypto.randomUUID(), initiator, groupId);
|
|
2507
|
+
started.set(task.id, opened.task);
|
|
2508
|
+
runs.push({
|
|
2509
|
+
task: resolveExecutionTargets(opened.task, before, this.maxSubtaskDepth),
|
|
2510
|
+
execution: opened.execution,
|
|
2511
|
+
...team && task.id !== root.id ? { dispatch: "teammate" } : {}
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
if (runs.length === 0) return [];
|
|
2515
|
+
let tasks = before.map((task) => started.get(task.id) ?? task);
|
|
2516
|
+
if (nextRunAt !== void 0) tasks = applyScheduleNextRun(tasks, root.id, nextRunAt, now, now);
|
|
2517
|
+
this.document.tasks = [...tasks];
|
|
2518
|
+
this.commit();
|
|
2519
|
+
return runs;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Finalize every cascade parent that is ready, walking from `taskId` up the
|
|
2523
|
+
* lineage: a member settles once its own outcome is recorded and none of its
|
|
2524
|
+
* group children is still open, and its verdict folds those children in
|
|
2525
|
+
* (failure dominates, then cancellation).
|
|
2526
|
+
*/
|
|
2527
|
+
settleCascade(taskId, groupId, now) {
|
|
2528
|
+
let changed = false;
|
|
2529
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2530
|
+
let current = taskId;
|
|
2531
|
+
while (current !== void 0 && !visited.has(current)) {
|
|
2532
|
+
visited.add(current);
|
|
2533
|
+
const task = this.document.tasks.find((item) => item.id === current);
|
|
2534
|
+
if (task === void 0) break;
|
|
2535
|
+
const execution = openGroupExecution(task, groupId);
|
|
2536
|
+
if (execution === void 0 || execution.ownResult === void 0) break;
|
|
2537
|
+
if (pendingCascadeChildren(this.document.tasks, current, groupId).length > 0) break;
|
|
2538
|
+
const entries = [{
|
|
2539
|
+
result: execution.ownResult,
|
|
2540
|
+
...execution.ownError === void 0 ? {} : { error: execution.ownError }
|
|
2541
|
+
}];
|
|
2542
|
+
for (const child of this.document.tasks) {
|
|
2543
|
+
if (child.parentId !== current) continue;
|
|
2544
|
+
const childExecution = child.executions.find((entry) => entry.runGroupId === groupId && entry.result !== void 0);
|
|
2545
|
+
if (childExecution?.result === void 0) continue;
|
|
2546
|
+
entries.push({
|
|
2547
|
+
result: childExecution.result,
|
|
2548
|
+
...childExecution.error === void 0 ? {} : { error: childExecution.error }
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
const combined = combineCascadeOutcome(entries);
|
|
2552
|
+
this.document.tasks = this.document.tasks.map((item) => item.id === current ? settleExecution(item, execution.id, combined.result, now, combined.error) : item);
|
|
2553
|
+
changed = true;
|
|
2554
|
+
current = task.parentId;
|
|
2555
|
+
}
|
|
2556
|
+
return changed;
|
|
2557
|
+
}
|
|
1924
2558
|
repairSchedules(skipPast, persist = true) {
|
|
1925
2559
|
const now = this.now();
|
|
1926
2560
|
let changed = false;
|
|
@@ -1958,13 +2592,23 @@ var HostTaskLedger = class {
|
|
|
1958
2592
|
reconcileInterruptedStarts(persist = true) {
|
|
1959
2593
|
const now = this.now();
|
|
1960
2594
|
let changed = false;
|
|
2595
|
+
const interrupted = [];
|
|
1961
2596
|
this.document.tasks = this.document.tasks.map((task) => {
|
|
1962
2597
|
if (task.status !== "running") return task;
|
|
1963
2598
|
const execution = task.executions.at(-1);
|
|
1964
2599
|
if (execution === void 0 || execution.endedAt !== void 0 || execution.sessionId !== void 0) return task;
|
|
1965
2600
|
changed = true;
|
|
2601
|
+
interrupted.push({
|
|
2602
|
+
taskId: task.id,
|
|
2603
|
+
runGroupId: execution.runGroupId
|
|
2604
|
+
});
|
|
1966
2605
|
return settleExecution(task, execution.id, "cancelled", now, "host restarted before the execution session was recorded");
|
|
1967
2606
|
});
|
|
2607
|
+
for (const entry of interrupted) {
|
|
2608
|
+
if (entry.runGroupId === void 0) continue;
|
|
2609
|
+
const parentId = this.document.tasks.find((task) => task.id === entry.taskId)?.parentId;
|
|
2610
|
+
if (parentId !== void 0 && this.settleCascade(parentId, entry.runGroupId, now)) changed = true;
|
|
2611
|
+
}
|
|
1968
2612
|
if (changed && persist) this.commit();
|
|
1969
2613
|
}
|
|
1970
2614
|
/**
|
|
@@ -2218,6 +2862,17 @@ function isInvocationUnavailable(error) {
|
|
|
2218
2862
|
const code = error.code;
|
|
2219
2863
|
return code === "invocation-unavailable" || code === "gateway/invocation-unavailable";
|
|
2220
2864
|
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Whether the endpoint exists but its strict definition was withdrawn by the
|
|
2867
|
+
* running gateway (the cohort changed under this plugin). The plugin can send
|
|
2868
|
+
* the call correctly and still be refused, and nothing in this repository can
|
|
2869
|
+
* restore the definition, so the roster is treated as unknown quietly instead
|
|
2870
|
+
* of printing an error the user has no action for (#1672).
|
|
2871
|
+
*/
|
|
2872
|
+
function isDefinitionWithdrawn(error) {
|
|
2873
|
+
const code = error.code;
|
|
2874
|
+
return code === "definition-unavailable" || code === "gateway/definition-unavailable";
|
|
2875
|
+
}
|
|
2221
2876
|
const SERVICE_UNAVAILABLE_ATTEMPTS = 5;
|
|
2222
2877
|
const SERVICE_UNAVAILABLE_BACKOFF_MS = 2e3;
|
|
2223
2878
|
function delay(ms) {
|
|
@@ -2250,20 +2905,35 @@ function escapeProvenanceDelimiter(value) {
|
|
|
2250
2905
|
return value.replaceAll("来源声明 开始", "来源声明·开始").replaceAll("来源声明 结束", "来源声明·结束");
|
|
2251
2906
|
}
|
|
2252
2907
|
/**
|
|
2253
|
-
*
|
|
2254
|
-
* a
|
|
2255
|
-
*
|
|
2256
|
-
* templated by the board, so the picking-up agent stays wary of stored
|
|
2257
|
-
* prompt-instruction injection in card text (adversarial scenario c). The
|
|
2258
|
-
* wrap composes with the T4 handover preamble: the reference preamble comes
|
|
2259
|
-
* first, the provenance wrap then encloses the instruction. Plain tasks (no
|
|
2260
|
-
* freeze) keep the bare handover preamble + prompt.
|
|
2908
|
+
* The execution-shape section: what else this run opens. A plain cascade opens
|
|
2909
|
+
* one independent session per member; a team run starts every other member as a
|
|
2910
|
+
* teammate inside THIS session, so the prompt names the Team tools instead.
|
|
2261
2911
|
*/
|
|
2262
|
-
function
|
|
2912
|
+
function peerPromptPreamble(context) {
|
|
2913
|
+
const peers = context.peers ?? [];
|
|
2914
|
+
if (peers.length === 0) return void 0;
|
|
2915
|
+
if (context.team === true) {
|
|
2916
|
+
const lines = peers.map((peer) => `- ${escapeProvenanceDelimiter(peer.title)}(teammate: ${peer.name ?? teammateName(peer.title, peer.id)})`);
|
|
2917
|
+
return `本任务是 Agent Team 的 Lead:本次运行不额外开启独立会话,以下 ${peers.length} 个子任务成员已在本会话中作为 teammate 启动。用 list_agents / send_message / wait_agent 协调它们,用任务看板工具读写它们在看板上的卡片:\n${lines.join("\n")}`;
|
|
2918
|
+
}
|
|
2919
|
+
const lines = peers.map((peer) => `- ${escapeProvenanceDelimiter(peer.title)}(任务 ${peer.id})`);
|
|
2920
|
+
return `本次运行同时并发开启 ${peers.length} 个独立 DSH 会话执行下列子任务成员(可用 task_board_* 工具查看它们的进度):\n${lines.join("\n")}`;
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Build the execution prompt for one task.
|
|
2924
|
+
* @param task - the task being launched.
|
|
2925
|
+
* @param context - the run's other members, for the execution-shape section.
|
|
2926
|
+
* @returns the prompt text.
|
|
2927
|
+
*/
|
|
2928
|
+
function promptText(task, context = {}) {
|
|
2263
2929
|
const body = task.prompt !== "" ? task.prompt : task.title;
|
|
2264
2930
|
const handover = task.handover;
|
|
2265
2931
|
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 = [
|
|
2932
|
+
const preambles = [
|
|
2933
|
+
tagPromptPreamble(task),
|
|
2934
|
+
handoverPreamble,
|
|
2935
|
+
peerPromptPreamble(context)
|
|
2936
|
+
].filter((part) => part !== void 0);
|
|
2267
2937
|
const preamble = preambles.length === 0 ? void 0 : preambles.join("\n\n");
|
|
2268
2938
|
const freeze = task.freeze;
|
|
2269
2939
|
if (freeze === void 0) return preamble === void 0 ? body : `${preamble}\n\n${body}`;
|
|
@@ -2287,10 +2957,27 @@ function tagPromptPreamble(task) {
|
|
|
2287
2957
|
if (lines.length === 0) return void 0;
|
|
2288
2958
|
return `标签提示(任务看板标签,每次执行前注入):\n${lines.join("\n")}`;
|
|
2289
2959
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2960
|
+
/**
|
|
2961
|
+
* Why a `turn/end` did not complete successfully, or undefined when it did.
|
|
2962
|
+
*
|
|
2963
|
+
* The harness treats exactly `completed` as success (`headless` maps the turn
|
|
2964
|
+
* reason to its exit code, `subagent/projection` records `lastTurnCompleted`,
|
|
2965
|
+
* and `max-tokens` is a ceiling, not a result). A task-board execution is a
|
|
2966
|
+
* SUCCESS only on positive evidence of a completed turn; every other reason —
|
|
2967
|
+
* and an unreadable one — must be reported, or the board stamps a failed run
|
|
2968
|
+
* as `succeeded` and a schedule dies silently (issue #1708). `interrupted` is
|
|
2969
|
+
* the reason a restart produces: `interruptedTurnClosers` synthesizes it for a
|
|
2970
|
+
* log whose tail turn never ended, which is exactly an aborted resume.
|
|
2971
|
+
* @param data - the `turn/end` event payload.
|
|
2972
|
+
* @returns the reason kind when it is not `completed`, else undefined.
|
|
2973
|
+
*/
|
|
2974
|
+
function nonCompletedTurnEnd(data) {
|
|
2975
|
+
if (typeof data !== "object" || data === null) return "unknown";
|
|
2292
2976
|
const reason = data.reason;
|
|
2293
|
-
|
|
2977
|
+
if (typeof reason !== "object" || reason === null) return "unknown";
|
|
2978
|
+
const kind = reason.kind;
|
|
2979
|
+
if (typeof kind !== "string") return "unknown";
|
|
2980
|
+
return kind === "completed" ? void 0 : kind;
|
|
2294
2981
|
}
|
|
2295
2982
|
/**
|
|
2296
2983
|
* Wire-argument layout of the 0.1.2-alpha.2 descriptor tables; the gateway's
|
|
@@ -2366,7 +3053,8 @@ var HostExecutionRunner = class {
|
|
|
2366
3053
|
const reused = options.reuseSessionId;
|
|
2367
3054
|
if (reused !== void 0) {
|
|
2368
3055
|
try {
|
|
2369
|
-
await this.
|
|
3056
|
+
await this.assertReusedPreset(reused, mode);
|
|
3057
|
+
await this.pinAndPrompt(reused, task, permission, options.promptContext);
|
|
2370
3058
|
} catch (error) {
|
|
2371
3059
|
throw new SessionLaunchError(reused, error);
|
|
2372
3060
|
}
|
|
@@ -2381,18 +3069,38 @@ var HostExecutionRunner = class {
|
|
|
2381
3069
|
sessionId,
|
|
2382
3070
|
title: task.title
|
|
2383
3071
|
});
|
|
2384
|
-
await this.pinAndPrompt(sessionId, task, permission);
|
|
3072
|
+
await this.pinAndPrompt(sessionId, task, permission, options.promptContext);
|
|
2385
3073
|
} catch (error) {
|
|
2386
3074
|
throw new SessionLaunchError(sessionId, error);
|
|
2387
3075
|
}
|
|
2388
3076
|
return sessionId;
|
|
2389
3077
|
}
|
|
2390
3078
|
/**
|
|
3079
|
+
* Fail closed when a reused session records a different Agent preset than
|
|
3080
|
+
* the one this task pins.
|
|
3081
|
+
*
|
|
3082
|
+
* The fresh branch asserts the pin by passing `agentPreset` to
|
|
3083
|
+
* `session/create`; the reuse branch must not call that method (it would
|
|
3084
|
+
* create or re-adopt the very session the board only meant to continue), so
|
|
3085
|
+
* the recorded value is read through `session/projections` — the documented
|
|
3086
|
+
* read that resolves no Agent — and compared here (issue #1708). A session
|
|
3087
|
+
* whose preset cannot be read is not treated as a match: silently continuing
|
|
3088
|
+
* under an unknown composition is the failure this guards.
|
|
3089
|
+
* @param sessionId - the session this execution continues in.
|
|
3090
|
+
* @param pinned - the preset id the task pins, when it pins one.
|
|
3091
|
+
*/
|
|
3092
|
+
async assertReusedPreset(sessionId, pinned) {
|
|
3093
|
+
if (pinned === void 0) return;
|
|
3094
|
+
const recorded = (await this.invoke("session", "projections", { sessionId }))?.values?.agentPreset;
|
|
3095
|
+
if (recorded === pinned) return;
|
|
3096
|
+
throw new Error("reused session " + sessionId + " was composed from agent preset " + (typeof recorded === "string" ? "\"" + recorded + "\"" : "an unreadable value") + ", but the task pins \"" + pinned + "\"");
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
2391
3099
|
* Re-assert the pinned execution contract on a session and queue the task
|
|
2392
3100
|
* prompt. Shared by the fresh-session and reuse paths so both apply exactly
|
|
2393
3101
|
* the same permission/model pins before the prompt.
|
|
2394
3102
|
*/
|
|
2395
|
-
async pinAndPrompt(sessionId, task, permission) {
|
|
3103
|
+
async pinAndPrompt(sessionId, task, permission, context = {}) {
|
|
2396
3104
|
if (permission !== void 0) {
|
|
2397
3105
|
if (this.commands === void 0) throw new Error("permission command dispatcher is unavailable");
|
|
2398
3106
|
const command = await this.commands.execute(sessionId, "/permission " + permission, AbortSignal.timeout(3e4));
|
|
@@ -2420,7 +3128,7 @@ var HostExecutionRunner = class {
|
|
|
2420
3128
|
mode: "queue",
|
|
2421
3129
|
content: [{
|
|
2422
3130
|
type: "text",
|
|
2423
|
-
text: promptText(task)
|
|
3131
|
+
text: promptText(task, context)
|
|
2424
3132
|
}]
|
|
2425
3133
|
});
|
|
2426
3134
|
}
|
|
@@ -2440,6 +3148,13 @@ var HostExecutionRunner = class {
|
|
|
2440
3148
|
}
|
|
2441
3149
|
return { known: false };
|
|
2442
3150
|
}
|
|
3151
|
+
if (isDefinitionWithdrawn(error)) {
|
|
3152
|
+
if (!this.unsupportedSessionListWarned) {
|
|
3153
|
+
this.unsupportedSessionListWarned = true;
|
|
3154
|
+
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);
|
|
3155
|
+
}
|
|
3156
|
+
return { known: false };
|
|
3157
|
+
}
|
|
2443
3158
|
if (!isServiceUnavailable(error) || attempt >= this.unavailableAttempts) {
|
|
2444
3159
|
console.error("[dsh-task-board] session/list failed; treating the host session roster as unknown", error);
|
|
2445
3160
|
return { known: false };
|
|
@@ -2463,6 +3178,10 @@ var HostExecutionRunner = class {
|
|
|
2463
3178
|
}
|
|
2464
3179
|
return { outcome: "pending" };
|
|
2465
3180
|
}
|
|
3181
|
+
if (isDefinitionWithdrawn(error)) {
|
|
3182
|
+
console.warn("[dsh-task-board] the host gateway withdrew the session/list definition for this DSH cohort; keeping the outcome pending", error);
|
|
3183
|
+
return { outcome: "pending" };
|
|
3184
|
+
}
|
|
2466
3185
|
console.warn("[dsh-task-board] session/list failed during execution inspection; keeping the outcome pending", error);
|
|
2467
3186
|
return { outcome: "pending" };
|
|
2468
3187
|
}
|
|
@@ -2533,10 +3252,15 @@ var HostExecutionRunner = class {
|
|
|
2533
3252
|
return { outcome: "pending" };
|
|
2534
3253
|
}
|
|
2535
3254
|
this.scanMemos.delete(sessionId);
|
|
2536
|
-
|
|
3255
|
+
const reason = nonCompletedTurnEnd(turnEnd.event.data);
|
|
3256
|
+
if (reason === void 0) return { outcome: "succeeded" };
|
|
3257
|
+
return reason === "error" ? {
|
|
2537
3258
|
outcome: "failed",
|
|
2538
3259
|
error: "agent turn ended with an error"
|
|
2539
|
-
} : {
|
|
3260
|
+
} : {
|
|
3261
|
+
outcome: "failed",
|
|
3262
|
+
error: "agent turn ended without completing: " + reason
|
|
3263
|
+
};
|
|
2540
3264
|
}
|
|
2541
3265
|
};
|
|
2542
3266
|
//#endregion
|
|
@@ -2865,11 +3589,16 @@ var TaskBoardHostService = class {
|
|
|
2865
3589
|
*/
|
|
2866
3590
|
idleSessionIds;
|
|
2867
3591
|
preventIdleSleep = false;
|
|
3592
|
+
team;
|
|
2868
3593
|
lastPowerJson = "";
|
|
2869
3594
|
now;
|
|
2870
3595
|
constructor(gateway, options = {}) {
|
|
2871
|
-
this.ledger = options.ledger ?? new HostTaskLedger(void 0, void 0, {
|
|
3596
|
+
this.ledger = options.ledger ?? new HostTaskLedger(void 0, void 0, {
|
|
3597
|
+
sessionDefaultPermission: options.sessionDefaultPermission,
|
|
3598
|
+
maxSubtaskDepth: options.maxSubtaskDepth
|
|
3599
|
+
});
|
|
2872
3600
|
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry);
|
|
3601
|
+
this.team = options.team;
|
|
2873
3602
|
this.power = options.power ?? new PowerInhibitor();
|
|
2874
3603
|
this.now = options.now ?? Date.now;
|
|
2875
3604
|
installStreamErrorGuards();
|
|
@@ -2923,7 +3652,9 @@ var TaskBoardHostService = class {
|
|
|
2923
3652
|
tasks: state.tasks,
|
|
2924
3653
|
scheduler: state.scheduler,
|
|
2925
3654
|
power: this.power.snapshot(),
|
|
2926
|
-
sessionDefaultPermission: this.ledger.sessionDefaultPermission
|
|
3655
|
+
sessionDefaultPermission: this.ledger.sessionDefaultPermission,
|
|
3656
|
+
maxSubtaskDepth: this.ledger.maxSubtaskDepth,
|
|
3657
|
+
teamRunAvailable: this.team !== void 0
|
|
2927
3658
|
};
|
|
2928
3659
|
}
|
|
2929
3660
|
/** SSE frame payload; deliberately skips the tasks deep-clone of {@link snapshot}. */
|
|
@@ -2943,8 +3674,11 @@ var TaskBoardHostService = class {
|
|
|
2943
3674
|
}
|
|
2944
3675
|
apply(requestId, action, initiator) {
|
|
2945
3676
|
if (!this.active) throw new Error("task board is disabled");
|
|
3677
|
+
if (this.team === void 0 && (action.kind === "run" || action.kind === "rerun")) {
|
|
3678
|
+
if (this.ledger.state().tasks.find((item) => item.id === action.taskId)?.teamRun === true) throw new Error("Agent Teams is unavailable in this deployment");
|
|
3679
|
+
}
|
|
2946
3680
|
const result = this.ledger.applyRequest(requestId, action, initiator);
|
|
2947
|
-
if (result.
|
|
3681
|
+
if (result.runs !== void 0) this.dispatchRuns(result.runs);
|
|
2948
3682
|
return {
|
|
2949
3683
|
schemaVersion: 3,
|
|
2950
3684
|
revision: result.state.revision,
|
|
@@ -2960,16 +3694,63 @@ var TaskBoardHostService = class {
|
|
|
2960
3694
|
this.ledger.dispose();
|
|
2961
3695
|
this.listeners.clear();
|
|
2962
3696
|
}
|
|
2963
|
-
async launch(opened) {
|
|
3697
|
+
async launch(opened, others = []) {
|
|
2964
3698
|
try {
|
|
2965
|
-
const
|
|
2966
|
-
const
|
|
3699
|
+
const team = opened.task.teamRun === true;
|
|
3700
|
+
const reuseSessionId = team ? void 0 : reusableSessionId(opened.task, this.idleSessionIds);
|
|
3701
|
+
const promptContext = others.length === 0 ? void 0 : {
|
|
3702
|
+
peers: others.map((other) => ({
|
|
3703
|
+
id: other.task.id,
|
|
3704
|
+
title: other.task.title,
|
|
3705
|
+
...team ? { name: teammateName(other.task.title, other.execution.runGroupId ?? other.task.id) } : {}
|
|
3706
|
+
})),
|
|
3707
|
+
...team ? { team: true } : {}
|
|
3708
|
+
};
|
|
3709
|
+
const sessionId = await this.runner.launch(opened.task, {
|
|
3710
|
+
...reuseSessionId === void 0 ? {} : { reuseSessionId },
|
|
3711
|
+
...promptContext === void 0 ? {} : { promptContext }
|
|
3712
|
+
});
|
|
2967
3713
|
this.ledger.attachSession(opened.task.id, opened.execution.id, sessionId);
|
|
3714
|
+
if (team) for (const teammate of others) this.scheduleTeammate(teammate, sessionId);
|
|
2968
3715
|
} catch (error) {
|
|
2969
3716
|
if (error instanceof SessionLaunchError) this.ledger.attachSession(opened.task.id, opened.execution.id, error.sessionId);
|
|
2970
3717
|
this.ledger.settle(opened.task.id, opened.execution.id, "failed", error instanceof Error ? error.message : String(error));
|
|
2971
3718
|
}
|
|
2972
3719
|
}
|
|
3720
|
+
scheduleTeammate(opened, leadSessionId) {
|
|
3721
|
+
this.spawnTeammate(opened, leadSessionId).catch((error) => {
|
|
3722
|
+
safeConsoleError("[dsh-task-board] teammate spawn settlement failed", error);
|
|
3723
|
+
});
|
|
3724
|
+
}
|
|
3725
|
+
/**
|
|
3726
|
+
* Spawn one teammate inside the Lead session and attach the teammate's
|
|
3727
|
+
* session to the subtask execution, so the existing session monitor settles
|
|
3728
|
+
* it from the teammate's own turn like any other execution. A spawn that
|
|
3729
|
+
* fails settles the subtask as failed immediately, which then folds into the
|
|
3730
|
+
* Lead's cascade verdict.
|
|
3731
|
+
*/
|
|
3732
|
+
async spawnTeammate(opened, leadSessionId) {
|
|
3733
|
+
const team = this.team;
|
|
3734
|
+
if (team === void 0) {
|
|
3735
|
+
this.ledger.settle(opened.task.id, opened.execution.id, "failed", "Agent Teams is unavailable in this deployment");
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
try {
|
|
3739
|
+
const member = await team.spawn({
|
|
3740
|
+
leadSessionId,
|
|
3741
|
+
name: teammateName(opened.task.title, opened.execution.runGroupId ?? opened.task.id),
|
|
3742
|
+
description: opened.task.title,
|
|
3743
|
+
prompt: promptText(opened.task)
|
|
3744
|
+
});
|
|
3745
|
+
if (member.sessionId === void 0 || member.sessionId === "") {
|
|
3746
|
+
this.ledger.settle(opened.task.id, opened.execution.id, "failed", member.error ?? "teammate provisioning failed");
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
this.ledger.attachSession(opened.task.id, opened.execution.id, member.sessionId);
|
|
3750
|
+
} catch (error) {
|
|
3751
|
+
this.ledger.settle(opened.task.id, opened.execution.id, "failed", error instanceof Error ? error.message : String(error));
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
2973
3754
|
async pollSessions() {
|
|
2974
3755
|
if (this.disposed) return;
|
|
2975
3756
|
if (!this.active && this.ledger.runtimeView().openExecutions.length === 0) return;
|
|
@@ -3016,15 +3797,28 @@ var TaskBoardHostService = class {
|
|
|
3016
3797
|
}
|
|
3017
3798
|
for (const schedule of this.ledger.dueSchedules(now)) {
|
|
3018
3799
|
const next = nextRunAtMs(schedule.cron, schedule.nextRunAt);
|
|
3019
|
-
|
|
3020
|
-
if (opened !== void 0) this.scheduleLaunch(opened);
|
|
3800
|
+
this.dispatchRuns(this.ledger.openScheduled(schedule.taskId, next, now));
|
|
3021
3801
|
}
|
|
3022
3802
|
}
|
|
3023
3803
|
armedSchedules() {
|
|
3024
3804
|
return this.ledger.armedScheduleCount();
|
|
3025
3805
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3806
|
+
/**
|
|
3807
|
+
* Launch one run set. The root goes first and receives the run shape in its
|
|
3808
|
+
* prompt (which members this run opens, and how they run); every plain-cascade
|
|
3809
|
+
* member then launches on its own so one refused participant cannot hold the
|
|
3810
|
+
* others back. A team run's members are spawned inside the root's Lead
|
|
3811
|
+
* session instead, once that session exists.
|
|
3812
|
+
*/
|
|
3813
|
+
dispatchRuns(runs) {
|
|
3814
|
+
if (runs.length === 0) return;
|
|
3815
|
+
const root = runs.find((run) => run.dispatch !== "teammate") ?? runs[0];
|
|
3816
|
+
const others = runs.filter((run) => run !== root);
|
|
3817
|
+
this.scheduleLaunch(root, others);
|
|
3818
|
+
for (const run of others) if (run.dispatch !== "teammate") this.scheduleLaunch(run);
|
|
3819
|
+
}
|
|
3820
|
+
scheduleLaunch(opened, others = []) {
|
|
3821
|
+
this.launch(opened, others).catch((error) => {
|
|
3028
3822
|
safeConsoleError("[dsh-task-board] execution launch settlement failed", error);
|
|
3029
3823
|
});
|
|
3030
3824
|
}
|
|
@@ -3079,1336 +3873,7 @@ function safeConsoleError(message, ...args) {
|
|
|
3079
3873
|
} catch {}
|
|
3080
3874
|
}
|
|
3081
3875
|
//#endregion
|
|
3082
|
-
//#region
|
|
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
|
-
};
|
|
4409
|
-
}
|
|
4410
|
-
//#endregion
|
|
4411
|
-
//#region src/host-ai.ts
|
|
3876
|
+
//#region src/host-ai.ts
|
|
4412
3877
|
/**
|
|
4413
3878
|
* One-shot model parse behind the board's "parse pasted text" action
|
|
4414
3879
|
* (issue #1540).
|
|
@@ -4554,6 +4019,780 @@ async function parseTaskDraft(llm, request, signal) {
|
|
|
4554
4019
|
}
|
|
4555
4020
|
}
|
|
4556
4021
|
//#endregion
|
|
4022
|
+
//#region src/host/agent-tools.ts
|
|
4023
|
+
/** Model-facing JSON rendering shared by every tool. */
|
|
4024
|
+
function renderJson(_args, value) {
|
|
4025
|
+
return [{
|
|
4026
|
+
type: "text",
|
|
4027
|
+
text: JSON.stringify(value, null, 2)
|
|
4028
|
+
}];
|
|
4029
|
+
}
|
|
4030
|
+
/** Mark an already JSON-safe projection as the tool's canonical value. */
|
|
4031
|
+
function json(value) {
|
|
4032
|
+
return value;
|
|
4033
|
+
}
|
|
4034
|
+
/** A domain refusal the model is expected to read and act on. */
|
|
4035
|
+
function refused(code, message) {
|
|
4036
|
+
return json({
|
|
4037
|
+
ok: false,
|
|
4038
|
+
code,
|
|
4039
|
+
message
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
function messageOf(error) {
|
|
4043
|
+
return error instanceof Error ? error.message : String(error);
|
|
4044
|
+
}
|
|
4045
|
+
function has(value, key) {
|
|
4046
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
4047
|
+
}
|
|
4048
|
+
/**
|
|
4049
|
+
* The calling session id, for the execution audit trail (the same stamp the
|
|
4050
|
+
* browser asserts). Absent when a call has no agent behind it.
|
|
4051
|
+
*/
|
|
4052
|
+
function callingSessionId(exec) {
|
|
4053
|
+
const agent = exec.agent;
|
|
4054
|
+
const id = agent?.session?.id ?? agent?.session?.header?.id;
|
|
4055
|
+
return typeof id === "string" && id !== "" ? id : void 0;
|
|
4056
|
+
}
|
|
4057
|
+
function scheduleView(schedule) {
|
|
4058
|
+
return {
|
|
4059
|
+
enabled: schedule.enabled,
|
|
4060
|
+
cron: schedule.cron,
|
|
4061
|
+
...schedule.nextRunAt === void 0 ? {} : { nextRunAt: schedule.nextRunAt },
|
|
4062
|
+
...schedule.lastTriggeredAt === void 0 ? {} : { lastTriggeredAt: schedule.lastTriggeredAt }
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
function tagView(tag) {
|
|
4066
|
+
return {
|
|
4067
|
+
name: tag.name,
|
|
4068
|
+
...tag.promptPrefix === void 0 ? {} : { promptPrefix: tag.promptPrefix }
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
4071
|
+
function executionView(execution) {
|
|
4072
|
+
return {
|
|
4073
|
+
id: execution.id,
|
|
4074
|
+
...execution.sessionId === void 0 ? {} : { sessionId: execution.sessionId },
|
|
4075
|
+
startedAt: execution.startedAt,
|
|
4076
|
+
...execution.endedAt === void 0 ? {} : { endedAt: execution.endedAt },
|
|
4077
|
+
...execution.result === void 0 ? {} : { result: execution.result },
|
|
4078
|
+
...execution.error === void 0 ? {} : { error: execution.error },
|
|
4079
|
+
...execution.initiatedBy === void 0 ? {} : { initiatedBy: execution.initiatedBy },
|
|
4080
|
+
...execution.runGroupId === void 0 ? {} : { cascade: true },
|
|
4081
|
+
...execution.ownResult === void 0 ? {} : {
|
|
4082
|
+
ownResult: execution.ownResult,
|
|
4083
|
+
awaitingSubtasks: execution.endedAt === void 0
|
|
4084
|
+
}
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
4087
|
+
/**
|
|
4088
|
+
* One compact task row for the model: identity, column, lineage, execution
|
|
4089
|
+
* targets, schedule, and the latest run. The prompt body rides only
|
|
4090
|
+
* task_board_get, so a fifty-task list stays readable.
|
|
4091
|
+
*/
|
|
4092
|
+
function taskSummary(task, tasks, sessionDefault) {
|
|
4093
|
+
const latest = task.executions.at(-1);
|
|
4094
|
+
const subtaskCount = directSubtasks(tasks, task.id).length;
|
|
4095
|
+
return {
|
|
4096
|
+
id: task.id,
|
|
4097
|
+
title: task.title,
|
|
4098
|
+
status: task.status,
|
|
4099
|
+
description: task.description,
|
|
4100
|
+
...task.parentId === void 0 ? {} : { parentId: task.parentId },
|
|
4101
|
+
...subtaskCount === 0 ? {} : { subtaskCount },
|
|
4102
|
+
...task.archivedAt === void 0 ? {} : { archivedAt: task.archivedAt },
|
|
4103
|
+
...task.tags === void 0 ? {} : { tags: task.tags.map(tagView) },
|
|
4104
|
+
...task.workspaceId === void 0 ? {} : { workspaceId: task.workspaceId },
|
|
4105
|
+
...task.mode === void 0 ? {} : { mode: task.mode },
|
|
4106
|
+
...task.permission === void 0 ? {} : { permission: task.permission },
|
|
4107
|
+
...task.model === void 0 ? {} : { model: task.model },
|
|
4108
|
+
...task.reuseSession === true ? { reuseSession: true } : {},
|
|
4109
|
+
...task.teamRun === true ? { teamRun: true } : {},
|
|
4110
|
+
...task.schedule === void 0 ? {} : { schedule: scheduleView(task.schedule) },
|
|
4111
|
+
...task.freeze === void 0 ? {} : { continuationCard: true },
|
|
4112
|
+
...task.permissionConfirmedAt === void 0 ? {} : { permissionConfirmedAt: task.permissionConfirmedAt },
|
|
4113
|
+
...requiresPermissionConfirmation(task, sessionDefault ?? "read-only") ? { permissionPending: true } : {},
|
|
4114
|
+
executionCount: task.executions.length,
|
|
4115
|
+
...latest === void 0 ? {} : { latestExecution: executionView(latest) },
|
|
4116
|
+
createdAt: task.createdAt,
|
|
4117
|
+
updatedAt: task.updatedAt
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
/** One task's full detail: content, lineage, schedule, and recent runs. */
|
|
4121
|
+
function taskDetail(task, tasks, sessionDefault) {
|
|
4122
|
+
const parent = task.parentId === void 0 ? void 0 : tasks.find((item) => item.id === task.parentId);
|
|
4123
|
+
return {
|
|
4124
|
+
...taskSummary(task, tasks, sessionDefault),
|
|
4125
|
+
prompt: task.prompt,
|
|
4126
|
+
depth: taskDepth(tasks, task.id),
|
|
4127
|
+
...parent === void 0 ? {} : { parent: {
|
|
4128
|
+
id: parent.id,
|
|
4129
|
+
title: parent.title,
|
|
4130
|
+
status: parent.status
|
|
4131
|
+
} },
|
|
4132
|
+
...task.freeze === void 0 ? {} : { freeze: {
|
|
4133
|
+
goal: task.freeze.goal,
|
|
4134
|
+
progress: task.freeze.progress,
|
|
4135
|
+
next: task.freeze.next,
|
|
4136
|
+
frozenAt: task.freeze.frozenAt,
|
|
4137
|
+
...task.freeze.frozenBy === void 0 ? {} : { frozenBy: task.freeze.frozenBy },
|
|
4138
|
+
...task.freeze.redacted === true ? { redacted: true } : {}
|
|
4139
|
+
} },
|
|
4140
|
+
...task.handover === void 0 ? {} : { handover: {
|
|
4141
|
+
...task.handover.workspaceId === void 0 ? {} : { workspaceId: task.handover.workspaceId },
|
|
4142
|
+
...task.handover.mode === void 0 ? {} : { mode: task.handover.mode },
|
|
4143
|
+
...task.handover.permission === void 0 ? {} : { permission: task.handover.permission },
|
|
4144
|
+
references: task.handover.references,
|
|
4145
|
+
bundledAt: task.handover.bundledAt
|
|
4146
|
+
} },
|
|
4147
|
+
subtasks: directSubtasks(tasks, task.id).map((child) => taskSummary(child, tasks, sessionDefault)),
|
|
4148
|
+
executions: task.executions.slice(-10).map(executionView)
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
/** The executions a run opened: everything that was not in the ledger before. */
|
|
4152
|
+
function openedExecutions(before, after) {
|
|
4153
|
+
const known = new Set(before.map((execution) => execution.id));
|
|
4154
|
+
return after.filter((execution) => !known.has(execution.id));
|
|
4155
|
+
}
|
|
4156
|
+
/** Board summary attached to task_board_list. */
|
|
4157
|
+
function boardView(snapshot, maxSubtaskDepth) {
|
|
4158
|
+
const counts = {
|
|
4159
|
+
backlog: 0,
|
|
4160
|
+
todo: 0,
|
|
4161
|
+
running: 0,
|
|
4162
|
+
done: 0,
|
|
4163
|
+
failed: 0,
|
|
4164
|
+
archived: 0
|
|
4165
|
+
};
|
|
4166
|
+
for (const task of snapshot.tasks) if (task.archivedAt !== void 0) counts.archived += 1;
|
|
4167
|
+
else counts[task.status] += 1;
|
|
4168
|
+
return {
|
|
4169
|
+
revision: snapshot.revision,
|
|
4170
|
+
timeZone: snapshot.scheduler.timeZone,
|
|
4171
|
+
...snapshot.scheduler.error === void 0 ? {} : { schedulerError: snapshot.scheduler.error },
|
|
4172
|
+
maxSubtaskDepth,
|
|
4173
|
+
sessionDefaultPermission: snapshot.sessionDefaultPermission ?? "read-only",
|
|
4174
|
+
runningSessions: snapshot.power.runningSessions,
|
|
4175
|
+
armedSchedules: snapshot.power.armedSchedules,
|
|
4176
|
+
counts
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
/**
|
|
4180
|
+
* Build every task-board tool bound to one Host service.
|
|
4181
|
+
* @param host - the Host ledger/service face (TaskBoardHostService).
|
|
4182
|
+
* @returns the tool definitions, in TASK_BOARD_TOOL_NAMES order.
|
|
4183
|
+
*/
|
|
4184
|
+
function buildTaskBoardTools(host) {
|
|
4185
|
+
return [
|
|
4186
|
+
buildListTool(host),
|
|
4187
|
+
buildGetTool(host),
|
|
4188
|
+
buildCreateTool(host),
|
|
4189
|
+
buildUpdateTool(host),
|
|
4190
|
+
buildSetParentTool(host),
|
|
4191
|
+
buildRunTool(host),
|
|
4192
|
+
buildManageTool(host),
|
|
4193
|
+
buildScheduleTool(host)
|
|
4194
|
+
];
|
|
4195
|
+
}
|
|
4196
|
+
function buildSetParentTool(host) {
|
|
4197
|
+
return defineTool({
|
|
4198
|
+
name: "task_board_set_parent",
|
|
4199
|
+
description: [
|
|
4200
|
+
"Attach an existing task under a parent (making it a subtask) or detach it back to the root board by setting its parentId.",
|
|
4201
|
+
"Pass an empty parentId to detach.",
|
|
4202
|
+
"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).",
|
|
4203
|
+
"Running a task runs its whole subtree concurrently; a parent card settles only after its own turn and every direct subtask settle.",
|
|
4204
|
+
"Triggers: 任务看板, task board, 看板, 子任务, subtask, 关联任务, link task, 解除关联, detach."
|
|
4205
|
+
].join(" "),
|
|
4206
|
+
parameters: {
|
|
4207
|
+
taskId: {
|
|
4208
|
+
type: "string",
|
|
4209
|
+
required: true,
|
|
4210
|
+
description: "The task whose parent link changes."
|
|
4211
|
+
},
|
|
4212
|
+
parentId: {
|
|
4213
|
+
type: "string",
|
|
4214
|
+
required: true,
|
|
4215
|
+
description: "Parent task id, or an empty string to detach the task back to the root board."
|
|
4216
|
+
}
|
|
4217
|
+
},
|
|
4218
|
+
output: {
|
|
4219
|
+
schema: { type: "json" },
|
|
4220
|
+
render: renderJson
|
|
4221
|
+
},
|
|
4222
|
+
async execute(args, exec) {
|
|
4223
|
+
try {
|
|
4224
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4225
|
+
kind: "set-parent",
|
|
4226
|
+
taskId: args.taskId,
|
|
4227
|
+
parentId: args.parentId === "" ? null : args.parentId
|
|
4228
|
+
}, callingSessionId(exec));
|
|
4229
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4230
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4231
|
+
return json({
|
|
4232
|
+
ok: true,
|
|
4233
|
+
task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4234
|
+
});
|
|
4235
|
+
} catch (error) {
|
|
4236
|
+
return refused("refused", messageOf(error));
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
4241
|
+
function buildRunTool(host) {
|
|
4242
|
+
return defineTool({
|
|
4243
|
+
name: "task_board_run",
|
|
4244
|
+
description: [
|
|
4245
|
+
"Run a task on the board now, or re-run a settled one.",
|
|
4246
|
+
"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.",
|
|
4247
|
+
"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.",
|
|
4248
|
+
"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.",
|
|
4249
|
+
"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.",
|
|
4250
|
+
"Executions consume real API quota. Sessions are created asynchronously, so read their ids and outcomes back with task_board_get.",
|
|
4251
|
+
"Triggers: 任务看板, task board, 看板, 执行任务, run task, 重新执行, rerun, 子任务, subtask."
|
|
4252
|
+
].join(" "),
|
|
4253
|
+
parameters: {
|
|
4254
|
+
taskId: {
|
|
4255
|
+
type: "string",
|
|
4256
|
+
required: true,
|
|
4257
|
+
description: "The task to run; running a root task runs its whole subtree."
|
|
4258
|
+
},
|
|
4259
|
+
rerun: {
|
|
4260
|
+
type: "boolean",
|
|
4261
|
+
description: "Re-run a settled task: reset its column to todo before starting (default false)."
|
|
4262
|
+
}
|
|
4263
|
+
},
|
|
4264
|
+
output: {
|
|
4265
|
+
schema: { type: "json" },
|
|
4266
|
+
render: renderJson
|
|
4267
|
+
},
|
|
4268
|
+
async execute(args, exec) {
|
|
4269
|
+
const before = host.snapshot();
|
|
4270
|
+
try {
|
|
4271
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4272
|
+
kind: args.rerun === true ? "rerun" : "run",
|
|
4273
|
+
taskId: args.taskId
|
|
4274
|
+
}, callingSessionId(exec));
|
|
4275
|
+
const beforeById = new Map(before.tasks.map((task) => [task.id, task]));
|
|
4276
|
+
const started = snapshot.tasks.flatMap((task) => {
|
|
4277
|
+
return openedExecutions(beforeById.get(task.id)?.executions ?? [], task.executions).map((execution) => ({
|
|
4278
|
+
taskId: task.id,
|
|
4279
|
+
title: task.title,
|
|
4280
|
+
executionId: execution.id,
|
|
4281
|
+
...execution.sessionId === void 0 ? {} : { sessionId: execution.sessionId }
|
|
4282
|
+
}));
|
|
4283
|
+
});
|
|
4284
|
+
const root = snapshot.tasks.find((task) => task.id === args.taskId);
|
|
4285
|
+
return json({
|
|
4286
|
+
ok: true,
|
|
4287
|
+
started,
|
|
4288
|
+
...root === void 0 ? {} : { task: taskSummary(root, snapshot.tasks, snapshot.sessionDefaultPermission) },
|
|
4289
|
+
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."
|
|
4290
|
+
});
|
|
4291
|
+
} catch (error) {
|
|
4292
|
+
const message = messageOf(error);
|
|
4293
|
+
return refused(message.startsWith("confirmation-required") ? "confirmation-required" : "refused", message);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
});
|
|
4297
|
+
}
|
|
4298
|
+
function buildManageTool(host) {
|
|
4299
|
+
return defineTool({
|
|
4300
|
+
name: "task_board_manage",
|
|
4301
|
+
description: [
|
|
4302
|
+
"Move, archive, restore, or delete one task board card.",
|
|
4303
|
+
"move-todo and move-backlog are the manual column moves; a running card cannot be moved.",
|
|
4304
|
+
"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.",
|
|
4305
|
+
"It cannot confirm a permission binding: the confirmation gate is a human act performed in the board UI.",
|
|
4306
|
+
"Triggers: 任务看板, task board, 看板, 归档, archive, 删除任务, delete task, 移动任务, move task."
|
|
4307
|
+
].join(" "),
|
|
4308
|
+
parameters: {
|
|
4309
|
+
taskId: {
|
|
4310
|
+
type: "string",
|
|
4311
|
+
required: true,
|
|
4312
|
+
description: "The task to act on."
|
|
4313
|
+
},
|
|
4314
|
+
action: {
|
|
4315
|
+
type: "string",
|
|
4316
|
+
required: true,
|
|
4317
|
+
enum: [
|
|
4318
|
+
"move-todo",
|
|
4319
|
+
"move-backlog",
|
|
4320
|
+
"archive",
|
|
4321
|
+
"restore",
|
|
4322
|
+
"delete"
|
|
4323
|
+
],
|
|
4324
|
+
description: "The lifecycle operation to perform."
|
|
4325
|
+
}
|
|
4326
|
+
},
|
|
4327
|
+
output: {
|
|
4328
|
+
schema: { type: "json" },
|
|
4329
|
+
render: renderJson
|
|
4330
|
+
},
|
|
4331
|
+
async execute(args, exec) {
|
|
4332
|
+
const initiator = callingSessionId(exec);
|
|
4333
|
+
const action = {
|
|
4334
|
+
"move-todo": {
|
|
4335
|
+
kind: "move",
|
|
4336
|
+
taskId: args.taskId,
|
|
4337
|
+
status: "todo"
|
|
4338
|
+
},
|
|
4339
|
+
"move-backlog": {
|
|
4340
|
+
kind: "move",
|
|
4341
|
+
taskId: args.taskId,
|
|
4342
|
+
status: "backlog"
|
|
4343
|
+
},
|
|
4344
|
+
archive: {
|
|
4345
|
+
kind: "archive",
|
|
4346
|
+
taskId: args.taskId
|
|
4347
|
+
},
|
|
4348
|
+
restore: {
|
|
4349
|
+
kind: "restore",
|
|
4350
|
+
taskId: args.taskId
|
|
4351
|
+
},
|
|
4352
|
+
delete: {
|
|
4353
|
+
kind: "delete",
|
|
4354
|
+
taskId: args.taskId
|
|
4355
|
+
}
|
|
4356
|
+
}[args.action];
|
|
4357
|
+
if (action === void 0) return refused("unknown-action", "unsupported action " + args.action);
|
|
4358
|
+
if (!host.snapshot().tasks.some((task) => task.id === args.taskId)) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4359
|
+
try {
|
|
4360
|
+
const snapshot = host.apply(crypto.randomUUID(), action, initiator);
|
|
4361
|
+
if (args.action === "delete") return json({
|
|
4362
|
+
ok: true,
|
|
4363
|
+
action: args.action,
|
|
4364
|
+
taskId: args.taskId
|
|
4365
|
+
});
|
|
4366
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4367
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4368
|
+
return json({
|
|
4369
|
+
ok: true,
|
|
4370
|
+
action: args.action,
|
|
4371
|
+
task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4372
|
+
});
|
|
4373
|
+
} catch (error) {
|
|
4374
|
+
return refused("refused", messageOf(error));
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
});
|
|
4378
|
+
}
|
|
4379
|
+
function buildScheduleTool(host) {
|
|
4380
|
+
return defineTool({
|
|
4381
|
+
name: "task_board_schedule",
|
|
4382
|
+
description: [
|
|
4383
|
+
"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).",
|
|
4384
|
+
"A due scheduled task runs the same cascade a manual run does, so a root task runs its whole subtask tree.",
|
|
4385
|
+
"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.",
|
|
4386
|
+
"Missed occurrences during Host downtime are skipped, never queued; a task that is already running skips its occurrence.",
|
|
4387
|
+
"Triggers: 任务看板, task board, 看板, 定时任务, schedule, cron, 每天执行."
|
|
4388
|
+
].join(" "),
|
|
4389
|
+
parameters: {
|
|
4390
|
+
taskId: {
|
|
4391
|
+
type: "string",
|
|
4392
|
+
required: true,
|
|
4393
|
+
description: "The task whose schedule changes."
|
|
4394
|
+
},
|
|
4395
|
+
enabled: {
|
|
4396
|
+
type: "boolean",
|
|
4397
|
+
description: "Arm (true) or disarm (false) the schedule."
|
|
4398
|
+
},
|
|
4399
|
+
cron: {
|
|
4400
|
+
type: "string",
|
|
4401
|
+
description: "5-field cron expression: minute hour day-of-month month day-of-week."
|
|
4402
|
+
}
|
|
4403
|
+
},
|
|
4404
|
+
output: {
|
|
4405
|
+
schema: { type: "json" },
|
|
4406
|
+
render: renderJson
|
|
4407
|
+
},
|
|
4408
|
+
async execute(args, exec) {
|
|
4409
|
+
if (args.enabled === void 0 && args.cron === void 0) return refused("nothing-to-change", "pass enabled and/or cron");
|
|
4410
|
+
if (!host.snapshot().tasks.some((task) => task.id === args.taskId)) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4411
|
+
const patch = {
|
|
4412
|
+
...args.enabled === void 0 ? {} : { enabled: args.enabled },
|
|
4413
|
+
...args.cron === void 0 ? {} : { cron: args.cron }
|
|
4414
|
+
};
|
|
4415
|
+
try {
|
|
4416
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4417
|
+
kind: "set-schedule",
|
|
4418
|
+
taskId: args.taskId,
|
|
4419
|
+
patch
|
|
4420
|
+
}, callingSessionId(exec));
|
|
4421
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4422
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4423
|
+
return json({
|
|
4424
|
+
ok: true,
|
|
4425
|
+
schedule: task.schedule === void 0 ? null : scheduleView(task.schedule),
|
|
4426
|
+
task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4427
|
+
});
|
|
4428
|
+
} catch (error) {
|
|
4429
|
+
return refused("refused", messageOf(error));
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
4434
|
+
function buildListTool(host) {
|
|
4435
|
+
return defineTool({
|
|
4436
|
+
name: "task_board_list",
|
|
4437
|
+
description: [
|
|
4438
|
+
"List tasks on the DSH task board (the Host-authoritative ledger behind the Web GUI task board).",
|
|
4439
|
+
"Filters: status column, parentId (direct subtasks), rootsOnly, tag, free-text query, and archived inclusion.",
|
|
4440
|
+
"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).",
|
|
4441
|
+
"Use task_board_get for one task in full, task_board_create to add work, and task_board_run to execute it.",
|
|
4442
|
+
"Triggers: 任务看板, task board, 看板, 子任务, subtask, task list, 定时任务, scheduled task."
|
|
4443
|
+
].join(" "),
|
|
4444
|
+
parameters: {
|
|
4445
|
+
status: {
|
|
4446
|
+
type: "string",
|
|
4447
|
+
enum: [
|
|
4448
|
+
"backlog",
|
|
4449
|
+
"todo",
|
|
4450
|
+
"running",
|
|
4451
|
+
"done",
|
|
4452
|
+
"failed"
|
|
4453
|
+
],
|
|
4454
|
+
description: "Only cards in this column."
|
|
4455
|
+
},
|
|
4456
|
+
parentId: {
|
|
4457
|
+
type: "string",
|
|
4458
|
+
description: "Only the direct subtasks of this task id."
|
|
4459
|
+
},
|
|
4460
|
+
rootsOnly: {
|
|
4461
|
+
type: "boolean",
|
|
4462
|
+
description: "Only tasks that have no parent (default false)."
|
|
4463
|
+
},
|
|
4464
|
+
includeArchived: {
|
|
4465
|
+
type: "boolean",
|
|
4466
|
+
description: "Include archived tasks (default false)."
|
|
4467
|
+
},
|
|
4468
|
+
tag: {
|
|
4469
|
+
type: "string",
|
|
4470
|
+
description: "Only tasks carrying this label name."
|
|
4471
|
+
},
|
|
4472
|
+
query: {
|
|
4473
|
+
type: "string",
|
|
4474
|
+
description: "Case-insensitive match against title, description, prompt, and label names."
|
|
4475
|
+
},
|
|
4476
|
+
limit: {
|
|
4477
|
+
type: "integer",
|
|
4478
|
+
description: "Maximum rows to return (default 50, maximum 200)."
|
|
4479
|
+
}
|
|
4480
|
+
},
|
|
4481
|
+
output: {
|
|
4482
|
+
schema: { type: "json" },
|
|
4483
|
+
render: renderJson
|
|
4484
|
+
},
|
|
4485
|
+
isConcurrencySafe: () => true,
|
|
4486
|
+
async execute(args) {
|
|
4487
|
+
const snapshot = host.snapshot();
|
|
4488
|
+
const needle = typeof args.query === "string" ? args.query.trim().toLowerCase() : "";
|
|
4489
|
+
const limit = Math.min(Math.max(typeof args.limit === "number" ? Math.trunc(args.limit) : 50, 1), 200);
|
|
4490
|
+
const matched = snapshot.tasks.filter((task) => {
|
|
4491
|
+
if (args.includeArchived !== true && task.archivedAt !== void 0) return false;
|
|
4492
|
+
if (args.status !== void 0 && task.status !== args.status) return false;
|
|
4493
|
+
if (args.parentId !== void 0 && task.parentId !== args.parentId) return false;
|
|
4494
|
+
if (args.rootsOnly === true && task.parentId !== void 0) return false;
|
|
4495
|
+
if (args.tag !== void 0 && !(task.tags ?? []).some((tag) => tag.name === args.tag)) return false;
|
|
4496
|
+
if (needle !== "") {
|
|
4497
|
+
if (![
|
|
4498
|
+
task.title,
|
|
4499
|
+
task.description,
|
|
4500
|
+
task.prompt,
|
|
4501
|
+
...(task.tags ?? []).map((tag) => tag.name)
|
|
4502
|
+
].join("\n").toLowerCase().includes(needle)) return false;
|
|
4503
|
+
}
|
|
4504
|
+
return true;
|
|
4505
|
+
});
|
|
4506
|
+
return json({
|
|
4507
|
+
ok: true,
|
|
4508
|
+
board: boardView(snapshot, snapshot.maxSubtaskDepth ?? 1),
|
|
4509
|
+
total: matched.length,
|
|
4510
|
+
count: Math.min(matched.length, limit),
|
|
4511
|
+
tasks: matched.slice(0, limit).map((task) => taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission))
|
|
4512
|
+
});
|
|
4513
|
+
}
|
|
4514
|
+
});
|
|
4515
|
+
}
|
|
4516
|
+
function buildGetTool(host) {
|
|
4517
|
+
return defineTool({
|
|
4518
|
+
name: "task_board_get",
|
|
4519
|
+
description: [
|
|
4520
|
+
"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.",
|
|
4521
|
+
"Use it before running or editing a card, and after a run to read the settled outcome.",
|
|
4522
|
+
"Triggers: 任务看板, task board, 看板, 子任务, subtask, task detail."
|
|
4523
|
+
].join(" "),
|
|
4524
|
+
parameters: { taskId: {
|
|
4525
|
+
type: "string",
|
|
4526
|
+
required: true,
|
|
4527
|
+
description: "Task id, as reported by task_board_list or task_board_create."
|
|
4528
|
+
} },
|
|
4529
|
+
output: {
|
|
4530
|
+
schema: { type: "json" },
|
|
4531
|
+
render: renderJson
|
|
4532
|
+
},
|
|
4533
|
+
isConcurrencySafe: () => true,
|
|
4534
|
+
async execute(args) {
|
|
4535
|
+
const snapshot = host.snapshot();
|
|
4536
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4537
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4538
|
+
return json({
|
|
4539
|
+
ok: true,
|
|
4540
|
+
task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4541
|
+
});
|
|
4542
|
+
}
|
|
4543
|
+
});
|
|
4544
|
+
}
|
|
4545
|
+
function buildCreateTool(host) {
|
|
4546
|
+
return defineTool({
|
|
4547
|
+
name: "task_board_create",
|
|
4548
|
+
description: [
|
|
4549
|
+
"Create a task on the DSH task board, or a subtask by passing parentId.",
|
|
4550
|
+
"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.",
|
|
4551
|
+
"The deployment caps subtask depth (default one level, maximum three): a subtask of a subtask is refused unless the board is configured deeper.",
|
|
4552
|
+
"The task lands in the todo column and does NOT run; call task_board_run to execute it.",
|
|
4553
|
+
"Triggers: 任务看板, task board, 看板, 新建任务, create task, 子任务, subtask, 定时任务."
|
|
4554
|
+
].join(" "),
|
|
4555
|
+
parameters: {
|
|
4556
|
+
title: {
|
|
4557
|
+
type: "string",
|
|
4558
|
+
required: true,
|
|
4559
|
+
description: "Short display title (one line)."
|
|
4560
|
+
},
|
|
4561
|
+
description: {
|
|
4562
|
+
type: "string",
|
|
4563
|
+
description: "Longer human description shown in the detail view."
|
|
4564
|
+
},
|
|
4565
|
+
prompt: {
|
|
4566
|
+
type: "string",
|
|
4567
|
+
description: "The instruction sent to the execution agent; the title is used when blank."
|
|
4568
|
+
},
|
|
4569
|
+
parentId: {
|
|
4570
|
+
type: "string",
|
|
4571
|
+
description: "Parent task id, making this a subtask. Omit for a root task."
|
|
4572
|
+
},
|
|
4573
|
+
workspaceId: {
|
|
4574
|
+
type: "string",
|
|
4575
|
+
description: "Workspace id the execution must run in; omit to inherit the parent value or use the most recent workspace."
|
|
4576
|
+
},
|
|
4577
|
+
mode: {
|
|
4578
|
+
type: "string",
|
|
4579
|
+
description: "Agent preset id the execution session is composed from; omit for the deployment default or the parent value."
|
|
4580
|
+
},
|
|
4581
|
+
permission: {
|
|
4582
|
+
type: "string",
|
|
4583
|
+
enum: [...TASK_PERMISSIONS],
|
|
4584
|
+
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."
|
|
4585
|
+
},
|
|
4586
|
+
model: {
|
|
4587
|
+
type: "string",
|
|
4588
|
+
description: "Pinned model as provider/model (or a model id); omit for the host default or the parent value."
|
|
4589
|
+
},
|
|
4590
|
+
reuseSession: {
|
|
4591
|
+
type: "boolean",
|
|
4592
|
+
description: "Continue later runs in the previous execution session instead of a fresh conversation."
|
|
4593
|
+
},
|
|
4594
|
+
teamRun: {
|
|
4595
|
+
type: "boolean",
|
|
4596
|
+
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."
|
|
4597
|
+
},
|
|
4598
|
+
tags: {
|
|
4599
|
+
type: "array",
|
|
4600
|
+
description: "Labels: the name renders as a badge and drives the board filter; promptPrefix is injected ahead of the execution prompt on every run.",
|
|
4601
|
+
items: {
|
|
4602
|
+
type: "object",
|
|
4603
|
+
additionalProperties: false,
|
|
4604
|
+
properties: {
|
|
4605
|
+
name: {
|
|
4606
|
+
type: "string",
|
|
4607
|
+
required: true,
|
|
4608
|
+
description: "Label name (unique within the task, at most 32 characters)."
|
|
4609
|
+
},
|
|
4610
|
+
promptPrefix: {
|
|
4611
|
+
type: "string",
|
|
4612
|
+
description: "Optional prompt line injected before the task prompt (at most 200 characters)."
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
},
|
|
4617
|
+
schedule: {
|
|
4618
|
+
type: "object",
|
|
4619
|
+
additionalProperties: false,
|
|
4620
|
+
description: "Arm a 5-field cron schedule at creation time (Host local time zone).",
|
|
4621
|
+
properties: {
|
|
4622
|
+
enabled: {
|
|
4623
|
+
type: "boolean",
|
|
4624
|
+
required: true,
|
|
4625
|
+
description: "Whether the schedule is armed."
|
|
4626
|
+
},
|
|
4627
|
+
cron: {
|
|
4628
|
+
type: "string",
|
|
4629
|
+
required: true,
|
|
4630
|
+
description: "5-field cron: minute hour day-of-month month day-of-week."
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
},
|
|
4635
|
+
output: {
|
|
4636
|
+
schema: { type: "json" },
|
|
4637
|
+
render: renderJson
|
|
4638
|
+
},
|
|
4639
|
+
async execute(args, exec) {
|
|
4640
|
+
if (args.title.trim() === "") return refused("title-required", "title must not be blank");
|
|
4641
|
+
const id = crypto.randomUUID();
|
|
4642
|
+
const tags = (args.tags ?? []).map((tag) => ({
|
|
4643
|
+
name: tag.name,
|
|
4644
|
+
...tag.promptPrefix === void 0 ? {} : { promptPrefix: tag.promptPrefix }
|
|
4645
|
+
}));
|
|
4646
|
+
const input = {
|
|
4647
|
+
title: args.title,
|
|
4648
|
+
description: args.description ?? "",
|
|
4649
|
+
prompt: args.prompt ?? "",
|
|
4650
|
+
...args.parentId === void 0 || args.parentId === "" ? {} : { parentId: args.parentId },
|
|
4651
|
+
...args.workspaceId === void 0 || args.workspaceId === "" ? {} : { workspaceId: args.workspaceId },
|
|
4652
|
+
...args.mode === void 0 || args.mode === "" ? {} : { mode: args.mode },
|
|
4653
|
+
...args.permission === void 0 ? {} : { permission: args.permission },
|
|
4654
|
+
...args.model === void 0 || args.model === "" ? {} : { model: args.model },
|
|
4655
|
+
...args.reuseSession === true ? { reuseSession: true } : {},
|
|
4656
|
+
...args.teamRun === true ? { teamRun: true } : {},
|
|
4657
|
+
...tags.length === 0 ? {} : { tags },
|
|
4658
|
+
...args.schedule === void 0 ? {} : { schedule: {
|
|
4659
|
+
enabled: args.schedule.enabled,
|
|
4660
|
+
cron: args.schedule.cron
|
|
4661
|
+
} }
|
|
4662
|
+
};
|
|
4663
|
+
try {
|
|
4664
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4665
|
+
kind: "create",
|
|
4666
|
+
id,
|
|
4667
|
+
input
|
|
4668
|
+
}, callingSessionId(exec));
|
|
4669
|
+
const task = snapshot.tasks.find((item) => item.id === id);
|
|
4670
|
+
if (task === void 0) return refused("create-failed", "the Host did not confirm the new task");
|
|
4671
|
+
return json({
|
|
4672
|
+
ok: true,
|
|
4673
|
+
task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4674
|
+
});
|
|
4675
|
+
} catch (error) {
|
|
4676
|
+
return refused("refused", messageOf(error));
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
});
|
|
4680
|
+
}
|
|
4681
|
+
function buildUpdateTool(host) {
|
|
4682
|
+
return defineTool({
|
|
4683
|
+
name: "task_board_update",
|
|
4684
|
+
description: [
|
|
4685
|
+
"Edit a task board card: title, description, prompt, labels, and the execution targets (workspace, agent preset, permission, model, session reuse).",
|
|
4686
|
+
"Only the fields you pass change; pass an empty string to clear a target, or an empty labels array to clear the labels.",
|
|
4687
|
+
"Title, description and prompt freeze once the task has started executing; the execution targets stay editable because they only affect future runs.",
|
|
4688
|
+
"Changing the pinned permission re-arms the permission confirmation gate.",
|
|
4689
|
+
"Triggers: 任务看板, task board, 看板, 修改任务, edit task, 子任务, 标签, tags."
|
|
4690
|
+
].join(" "),
|
|
4691
|
+
parameters: {
|
|
4692
|
+
taskId: {
|
|
4693
|
+
type: "string",
|
|
4694
|
+
required: true,
|
|
4695
|
+
description: "Task id to edit."
|
|
4696
|
+
},
|
|
4697
|
+
title: {
|
|
4698
|
+
type: "string",
|
|
4699
|
+
description: "New title (a blank title is refused by the Host)."
|
|
4700
|
+
},
|
|
4701
|
+
description: {
|
|
4702
|
+
type: "string",
|
|
4703
|
+
description: "New description."
|
|
4704
|
+
},
|
|
4705
|
+
prompt: {
|
|
4706
|
+
type: "string",
|
|
4707
|
+
description: "New execution prompt."
|
|
4708
|
+
},
|
|
4709
|
+
workspaceId: {
|
|
4710
|
+
type: "string",
|
|
4711
|
+
description: "New workspace id; an empty string clears it."
|
|
4712
|
+
},
|
|
4713
|
+
mode: {
|
|
4714
|
+
type: "string",
|
|
4715
|
+
description: "New agent preset id; an empty string clears it."
|
|
4716
|
+
},
|
|
4717
|
+
permission: {
|
|
4718
|
+
type: "string",
|
|
4719
|
+
enum: [...TASK_PERMISSIONS, ""],
|
|
4720
|
+
description: "New permission preset; an empty string clears it back to the session default."
|
|
4721
|
+
},
|
|
4722
|
+
model: {
|
|
4723
|
+
type: "string",
|
|
4724
|
+
description: "New pinned model; an empty string clears it."
|
|
4725
|
+
},
|
|
4726
|
+
reuseSession: {
|
|
4727
|
+
type: "boolean",
|
|
4728
|
+
description: "Continue later runs in the previous execution session."
|
|
4729
|
+
},
|
|
4730
|
+
teamRun: {
|
|
4731
|
+
type: "boolean",
|
|
4732
|
+
description: "Switches this task between a plain cascade (one session per member) and an Agent Team run (Lead session plus a teammate per subtask)."
|
|
4733
|
+
},
|
|
4734
|
+
tags: {
|
|
4735
|
+
type: "array",
|
|
4736
|
+
description: "Replacement label set; an empty array clears all labels.",
|
|
4737
|
+
items: {
|
|
4738
|
+
type: "object",
|
|
4739
|
+
additionalProperties: false,
|
|
4740
|
+
properties: {
|
|
4741
|
+
name: {
|
|
4742
|
+
type: "string",
|
|
4743
|
+
required: true,
|
|
4744
|
+
description: "Label name."
|
|
4745
|
+
},
|
|
4746
|
+
promptPrefix: {
|
|
4747
|
+
type: "string",
|
|
4748
|
+
description: "Optional prompt line injected before the task prompt."
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
},
|
|
4754
|
+
output: {
|
|
4755
|
+
schema: { type: "json" },
|
|
4756
|
+
render: renderJson
|
|
4757
|
+
},
|
|
4758
|
+
async execute(args, exec) {
|
|
4759
|
+
const patch = {};
|
|
4760
|
+
if (has(args, "title") && typeof args.title === "string") patch.title = args.title;
|
|
4761
|
+
if (has(args, "description") && typeof args.description === "string") patch.description = args.description;
|
|
4762
|
+
if (has(args, "prompt") && typeof args.prompt === "string") patch.prompt = args.prompt;
|
|
4763
|
+
if (has(args, "workspaceId") && typeof args.workspaceId === "string") patch.workspaceId = args.workspaceId;
|
|
4764
|
+
if (has(args, "mode") && typeof args.mode === "string") patch.mode = args.mode;
|
|
4765
|
+
if (has(args, "model") && typeof args.model === "string") patch.model = args.model;
|
|
4766
|
+
if (has(args, "permission") && typeof args.permission === "string") patch.permission = args.permission === "" ? void 0 : args.permission;
|
|
4767
|
+
if (has(args, "reuseSession")) patch.reuseSession = args.reuseSession === true;
|
|
4768
|
+
if (has(args, "teamRun")) patch.teamRun = args.teamRun === true;
|
|
4769
|
+
if (has(args, "tags")) {
|
|
4770
|
+
const tags = args.tags ?? [];
|
|
4771
|
+
patch.tags = tags.length === 0 ? null : tags.map((tag) => ({
|
|
4772
|
+
name: tag.name,
|
|
4773
|
+
...tag.promptPrefix === void 0 ? {} : { promptPrefix: tag.promptPrefix }
|
|
4774
|
+
}));
|
|
4775
|
+
}
|
|
4776
|
+
if (Object.keys(patch).length === 0) return refused("nothing-to-change", "pass at least one field to update");
|
|
4777
|
+
try {
|
|
4778
|
+
const snapshot = host.apply(crypto.randomUUID(), {
|
|
4779
|
+
kind: "update",
|
|
4780
|
+
taskId: args.taskId,
|
|
4781
|
+
patch
|
|
4782
|
+
}, callingSessionId(exec));
|
|
4783
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
4784
|
+
if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4785
|
+
return json({
|
|
4786
|
+
ok: true,
|
|
4787
|
+
task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission)
|
|
4788
|
+
});
|
|
4789
|
+
} catch (error) {
|
|
4790
|
+
return refused("refused", messageOf(error));
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
});
|
|
4794
|
+
}
|
|
4795
|
+
//#endregion
|
|
4557
4796
|
//#region src/http.ts
|
|
4558
4797
|
/** Family-default JSON response headers; callers may append or override. */
|
|
4559
4798
|
const JSON_HEADERS = {
|
|
@@ -4922,7 +5161,12 @@ const inject = [
|
|
|
4922
5161
|
"commands"
|
|
4923
5162
|
];
|
|
4924
5163
|
/** 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
|
|
5164
|
+
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。";
|
|
5165
|
+
/**
|
|
5166
|
+
* Continuable-subagent provider for fresh teammates, matching the Agent Teams
|
|
5167
|
+
* tool plugin's own `freshProvider` default.
|
|
5168
|
+
*/
|
|
5169
|
+
const DEFAULT_TEAM_PROVIDER = "spawn";
|
|
4926
5170
|
/**
|
|
4927
5171
|
* The schema is left to inference rather than annotated with `z<Config>`: a
|
|
4928
5172
|
* volatile field's parsed output is a `Volatile` reference while its accepted
|
|
@@ -4935,7 +5179,9 @@ const Config = z.object({
|
|
|
4935
5179
|
preventIdleSleep: z.boolean().default(false).volatile(),
|
|
4936
5180
|
trustedProxyHosts: z.array(z.string()).default([]),
|
|
4937
5181
|
proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
|
|
4938
|
-
sessionDefaultPermission: z.union(TASK_PERMISSIONS).default(DEFAULT_SESSION_PERMISSION)
|
|
5182
|
+
sessionDefaultPermission: z.union(TASK_PERMISSIONS).default(DEFAULT_SESSION_PERMISSION),
|
|
5183
|
+
maxSubtaskDepth: z.number().min(1).max(3).default(1).volatile(),
|
|
5184
|
+
teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER)
|
|
4939
5185
|
});
|
|
4940
5186
|
/**
|
|
4941
5187
|
* Read one config field's current value.
|
|
@@ -4966,9 +5212,84 @@ function resolveProxyAccess(config, env = process.env) {
|
|
|
4966
5212
|
proxyToken
|
|
4967
5213
|
};
|
|
4968
5214
|
}
|
|
5215
|
+
/**
|
|
5216
|
+
* Resolve the optional agent-tool registry. The board deliberately does not
|
|
5217
|
+
* inject it: a deployment whose runtime serves no registry must still mount the
|
|
5218
|
+
* whole board and lose only the agent-tool surface, the same tolerance the
|
|
5219
|
+
* optional llm service gets.
|
|
5220
|
+
* @param ctx - the plugin context.
|
|
5221
|
+
* @returns the registry, or undefined when this deployment serves none.
|
|
5222
|
+
*/
|
|
5223
|
+
function resolveToolRegistry(ctx) {
|
|
5224
|
+
try {
|
|
5225
|
+
const tools = ctx.get("tools");
|
|
5226
|
+
return tools !== void 0 && typeof tools.register === "function" ? tools : void 0;
|
|
5227
|
+
} catch {
|
|
5228
|
+
return;
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
4969
5231
|
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
4970
5232
|
const DEFAULT_ANNOUNCE = false;
|
|
4971
5233
|
/**
|
|
5234
|
+
* How long one teammate may take to reach its durable active or failed edge.
|
|
5235
|
+
* Agent Teams provisioning normally resolves in seconds; the bound exists so a
|
|
5236
|
+
* hung spawn settles the subtask instead of holding the cascade open forever.
|
|
5237
|
+
*/
|
|
5238
|
+
const TEAMMATE_PROVISION_TIMEOUT_MS = 6e5;
|
|
5239
|
+
/**
|
|
5240
|
+
* Resolve the optional Agent Teams service. Like the tool registry and the llm
|
|
5241
|
+
* runtime, it is deliberately not injected: a deployment without Agent Teams
|
|
5242
|
+
* must still mount the whole board and only lose team-mode runs.
|
|
5243
|
+
* @param ctx - the plugin context.
|
|
5244
|
+
* @returns the service, or undefined when this deployment serves none.
|
|
5245
|
+
*/
|
|
5246
|
+
function resolveAgentTeams(ctx) {
|
|
5247
|
+
try {
|
|
5248
|
+
const teams = ctx.get("agentTeams");
|
|
5249
|
+
return teams !== void 0 && typeof teams.spawnTeammate === "function" ? teams : void 0;
|
|
5250
|
+
} catch {
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
/**
|
|
5255
|
+
* Build the team dispatcher: the run's Lead session spawns one teammate per
|
|
5256
|
+
* subtask through the Agent Teams service, and the teammate's session id is
|
|
5257
|
+
* attached to that subtask's execution so the ordinary session monitor settles
|
|
5258
|
+
* it. Returns undefined when the deployment serves no Agent Teams service or
|
|
5259
|
+
* no live-agent registry, which makes a team run fail closed.
|
|
5260
|
+
* @param ctx - the plugin context.
|
|
5261
|
+
* @param provider - the continuable-subagent provider for fresh teammates.
|
|
5262
|
+
* @returns the dispatcher, or undefined when team runs cannot be served.
|
|
5263
|
+
*/
|
|
5264
|
+
function buildTeamDispatcher(ctx, provider) {
|
|
5265
|
+
const teams = resolveAgentTeams(ctx);
|
|
5266
|
+
if (teams === void 0) return void 0;
|
|
5267
|
+
if (typeof ctx.agents?.get !== "function") return void 0;
|
|
5268
|
+
return { async spawn(input) {
|
|
5269
|
+
const lead = ctx.agents.get(input.leadSessionId);
|
|
5270
|
+
if (lead === void 0) return { error: `lead session ${input.leadSessionId} is not live` };
|
|
5271
|
+
try {
|
|
5272
|
+
const member = (await teams.spawnTeammate(lead, {
|
|
5273
|
+
name: input.name,
|
|
5274
|
+
description: input.description,
|
|
5275
|
+
prompt: [{
|
|
5276
|
+
type: "text",
|
|
5277
|
+
text: input.prompt
|
|
5278
|
+
}],
|
|
5279
|
+
context: "fresh",
|
|
5280
|
+
provider,
|
|
5281
|
+
signal: AbortSignal.timeout(TEAMMATE_PROVISION_TIMEOUT_MS)
|
|
5282
|
+
}))?.member;
|
|
5283
|
+
if (member === void 0) return { error: "Agent Teams returned no teammate" };
|
|
5284
|
+
if (member.phase === "failed") return { error: member.error ?? "teammate provisioning failed" };
|
|
5285
|
+
if (typeof member.id !== "string" || member.id === "") return { error: "Agent Teams returned no teammate session" };
|
|
5286
|
+
return { sessionId: member.id };
|
|
5287
|
+
} catch (error) {
|
|
5288
|
+
return { error: `teammate provisioning failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
5289
|
+
}
|
|
5290
|
+
} };
|
|
5291
|
+
}
|
|
5292
|
+
/**
|
|
4972
5293
|
* Read the optional `llm` service. The board deliberately does not inject it:
|
|
4973
5294
|
* a deployment without a model must still mount the board, and the parse route
|
|
4974
5295
|
* answers a typed failure instead of the plugin failing to load (issue #1540).
|
|
@@ -4987,8 +5308,8 @@ function resolveLlmRuntime(ctx) {
|
|
|
4987
5308
|
* Activate the board's host half: ledger, routes, cron scheduler, power
|
|
4988
5309
|
* inhibitor, and the model-facing announcement.
|
|
4989
5310
|
*
|
|
4990
|
-
* The effective settings are the config the Host hands this row. The
|
|
4991
|
-
*
|
|
5311
|
+
* The effective settings are the config the Host hands this row. The fields
|
|
5312
|
+
* the browser card edits are schema-volatile, so the Loader commits an
|
|
4992
5313
|
* edit into the running fiber's references instead of remounting the row —
|
|
4993
5314
|
* `sync` therefore reads them at use time and follows
|
|
4994
5315
|
* `loader/volatile-update`, the event the Loader emits once it has committed
|
|
@@ -5004,9 +5325,13 @@ function applyImpl(ctx, config) {
|
|
|
5004
5325
|
const announceToAgent = () => readConfigField(config?.announceToAgent, DEFAULT_ANNOUNCE);
|
|
5005
5326
|
/** Current idle-sleep protection flag. */
|
|
5006
5327
|
const preventIdleSleep = () => readConfigField(config?.preventIdleSleep, false);
|
|
5328
|
+
/** Current subtask depth limit (read live: the settings card edits it in place). */
|
|
5329
|
+
const maxSubtaskDepth = () => readConfigField(config?.maxSubtaskDepth, 1);
|
|
5007
5330
|
const host = new TaskBoardHostService(ctx.typertGateway, {
|
|
5008
5331
|
workspaceRegistry: ctx.workspaceRegistry,
|
|
5009
5332
|
sessionDefaultPermission: config?.sessionDefaultPermission ?? "read-only",
|
|
5333
|
+
maxSubtaskDepth: maxSubtaskDepth(),
|
|
5334
|
+
team: buildTeamDispatcher(ctx, config?.teamProvider ?? "spawn"),
|
|
5010
5335
|
commandDispatcher: { async execute(sessionId, line, signal) {
|
|
5011
5336
|
const agent = ctx.agents.get(sessionId);
|
|
5012
5337
|
if (agent === void 0) throw new Error(`execution session ${sessionId} is not available`);
|
|
@@ -5015,6 +5340,30 @@ function applyImpl(ctx, config) {
|
|
|
5015
5340
|
});
|
|
5016
5341
|
host.setConfiguration(enabled(), preventIdleSleep());
|
|
5017
5342
|
host.start();
|
|
5343
|
+
let disposeTools;
|
|
5344
|
+
const setToolsEnabled = (active) => {
|
|
5345
|
+
if (!active) {
|
|
5346
|
+
disposeTools?.();
|
|
5347
|
+
disposeTools = void 0;
|
|
5348
|
+
return;
|
|
5349
|
+
}
|
|
5350
|
+
if (disposeTools !== void 0) return;
|
|
5351
|
+
const registry = resolveToolRegistry(ctx);
|
|
5352
|
+
if (registry === void 0) return;
|
|
5353
|
+
const disposers = buildTaskBoardTools(host).map((tool) => registry.register(tool));
|
|
5354
|
+
disposeTools = () => {
|
|
5355
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
5356
|
+
};
|
|
5357
|
+
};
|
|
5358
|
+
setToolsEnabled(enabled());
|
|
5359
|
+
const scopedInject = ctx.inject;
|
|
5360
|
+
if (typeof scopedInject === "function") scopedInject.call(ctx, ["tools"], () => {
|
|
5361
|
+
setToolsEnabled(enabled());
|
|
5362
|
+
return () => {
|
|
5363
|
+
disposeTools?.();
|
|
5364
|
+
disposeTools = void 0;
|
|
5365
|
+
};
|
|
5366
|
+
});
|
|
5018
5367
|
ctx.effect(() => {
|
|
5019
5368
|
const disposers = [];
|
|
5020
5369
|
try {
|
|
@@ -5025,11 +5374,13 @@ function applyImpl(ctx, config) {
|
|
|
5025
5374
|
} });
|
|
5026
5375
|
for (const route of routes) disposers.push(ctx.webServer.register(route));
|
|
5027
5376
|
} catch (error) {
|
|
5377
|
+
setToolsEnabled(false);
|
|
5028
5378
|
for (const dispose of disposers) dispose();
|
|
5029
5379
|
host.dispose();
|
|
5030
5380
|
throw error;
|
|
5031
5381
|
}
|
|
5032
5382
|
return () => {
|
|
5383
|
+
setToolsEnabled(false);
|
|
5033
5384
|
for (const dispose of disposers) dispose();
|
|
5034
5385
|
host.dispose();
|
|
5035
5386
|
};
|
|
@@ -5040,11 +5391,14 @@ function applyImpl(ctx, config) {
|
|
|
5040
5391
|
const next = {
|
|
5041
5392
|
enabled: enabled(),
|
|
5042
5393
|
announceToAgent: announceToAgent(),
|
|
5043
|
-
preventIdleSleep: preventIdleSleep()
|
|
5394
|
+
preventIdleSleep: preventIdleSleep(),
|
|
5395
|
+
maxSubtaskDepth: maxSubtaskDepth()
|
|
5044
5396
|
};
|
|
5045
|
-
if (applied !== void 0 && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep) return;
|
|
5397
|
+
if (applied !== void 0 && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep && applied.maxSubtaskDepth === next.maxSubtaskDepth) return;
|
|
5046
5398
|
applied = next;
|
|
5399
|
+
host.ledger.setMaxSubtaskDepth(next.maxSubtaskDepth);
|
|
5047
5400
|
host.setConfiguration(next.enabled, next.preventIdleSleep);
|
|
5401
|
+
setToolsEnabled(next.enabled);
|
|
5048
5402
|
if (disposeSection !== void 0) {
|
|
5049
5403
|
disposeSection();
|
|
5050
5404
|
disposeSection = void 0;
|
|
@@ -5062,4 +5416,4 @@ function applyImpl(ctx, config) {
|
|
|
5062
5416
|
sync();
|
|
5063
5417
|
}
|
|
5064
5418
|
//#endregion
|
|
5065
|
-
export { Config, DEFAULT_PROXY_TOKEN_ENV, TASK_BOARD_GUIDANCE, apply, inject, readConfigField, resolveLlmRuntime, resolveProxyAccess };
|
|
5419
|
+
export { Config, DEFAULT_PROXY_TOKEN_ENV, DEFAULT_TEAM_PROVIDER, TASK_BOARD_GUIDANCE, apply, buildTeamDispatcher, inject, readConfigField, resolveAgentTeams, resolveLlmRuntime, resolveProxyAccess, resolveToolRegistry };
|