@linxin666/dsh-client-ui-task-board 0.4.0 → 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.
Files changed (51) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +24 -0
  3. package/README.zh.md +24 -0
  4. package/lib/client.js +917 -81
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1774 -1420
  7. package/lib/types/client/TaskBoardSettingsCard.d.ts +4 -0
  8. package/lib/types/client/board/LinkSubtaskModal.d.ts +11 -0
  9. package/lib/types/client/board/NewTaskModal.d.ts +7 -1
  10. package/lib/types/client/board/TaskCard.d.ts +11 -1
  11. package/lib/types/client/locales.d.ts +33 -0
  12. package/lib/types/core/controller.d.ts +30 -1
  13. package/lib/types/core/subtask.d.ts +122 -0
  14. package/lib/types/core/tasks.d.ts +46 -3
  15. package/lib/types/core/use-cases/task-archive.d.ts +10 -13
  16. package/lib/types/core/use-cases/task-create.d.ts +13 -4
  17. package/lib/types/core/use-cases/task-delete.d.ts +7 -1
  18. package/lib/types/core/use-cases/task-parent.d.ts +21 -0
  19. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  20. package/lib/types/host/agent-tools.d.ts +27 -0
  21. package/lib/types/host-ledger.d.ts +88 -4
  22. package/lib/types/host-runner.d.ts +39 -1
  23. package/lib/types/host-service.d.ts +47 -0
  24. package/lib/types/index.d.ts +83 -5
  25. package/lib/types/protocol.d.ts +14 -0
  26. package/package.json +23 -21
  27. package/src/client/TaskBoardSettingsCard.tsx +48 -3
  28. package/src/client/board/LinkSubtaskModal.tsx +82 -0
  29. package/src/client/board/NewTaskModal.tsx +40 -6
  30. package/src/client/board/TaskBoard.tsx +88 -8
  31. package/src/client/board/TaskCard.tsx +52 -2
  32. package/src/client/board/TaskDetail.tsx +124 -1
  33. package/src/client/board.module.css +96 -0
  34. package/src/client/host-api.ts +14 -4
  35. package/src/client/index.ts +11 -1
  36. package/src/client/locales.ts +70 -2
  37. package/src/core/controller.ts +69 -8
  38. package/src/core/store.ts +15 -0
  39. package/src/core/subtask.ts +286 -0
  40. package/src/core/tasks.ts +50 -2
  41. package/src/core/use-cases/task-archive.ts +46 -12
  42. package/src/core/use-cases/task-create.ts +42 -7
  43. package/src/core/use-cases/task-delete.ts +11 -1
  44. package/src/core/use-cases/task-parent.ts +64 -0
  45. package/src/core/use-cases/task-update.ts +3 -1
  46. package/src/host/agent-tools.ts +643 -0
  47. package/src/host-ledger.ts +323 -45
  48. package/src/host-runner.ts +141 -12
  49. package/src/host-service.ts +127 -10
  50. package/src/index.ts +181 -9
  51. 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 { Context, Service } from "@deepseek-ai/cordis";
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 one task: only a `running` task stays on the board (its runner
805
- * still owns its lifecycle until the execution settles); every other status
806
- * can be archived. Archiving disarms a schedule; already-archived tasks are
807
- * a no-op.
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 !== id || task.archivedAt !== void 0) return task;
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
- /** Restore one task back onto the main board (clears the archive marker). */
832
- function applyRestoreTask(tasks, id, now) {
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 !== id || task.archivedAt === void 0) return task;
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. Pure ledger transition (no persistence or notify — the controller
852
- * orchestrates those), so it is unit-testable without any runtime face.
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
- let task = createTask(input, now, id);
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 void 0;
1746
- if (requiresPermissionConfirmation(task, this.sessionDefaultPermission)) {
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
- return;
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
- this.document.tasks = [...applyScheduleNextRun(this.document.tasks, taskId, nextRunAt, task.schedule?.lastTriggeredAt, triggeredAt)];
1753
- this.commit();
1754
- return;
2232
+ rollForward();
2233
+ return [];
1755
2234
  }
1756
- const opened = startExecution(task, triggeredAt, crypto.randomUUID());
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 run;
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
- const task = this.document.tasks.find((task) => task.id === action.taskId);
1863
- if (task === void 0) throw new Error("task not found");
1864
- if (task.status === "running" || hasOpenExecution(task)) throw new Error("running task cannot be deleted");
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
- const result = applyArchiveTask(this.document.tasks, action.taskId, now);
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
- const result = applyRestoreTask(this.document.tasks, action.taskId, now);
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
- if (requiresPermissionConfirmation(task, this.sessionDefaultPermission)) throw new Error(`confirmation-required: the effective permission is above the session default (${this.sessionDefaultPermission}); confirm the card's permission binding first`);
1913
- run = startExecution(action.kind === "rerun" ? withStatus(task, "todo", now) : task, now, crypto.randomUUID(), initiator);
1914
- this.document.tasks = this.document.tasks.map((item) => item.id === task.id ? run.task : item);
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
- ...run === void 0 ? {} : { run }
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
- * Compose the execution prompt (issue #6): a continuation card (one carrying
2254
- * a frozen snapshot) has its instruction mandatorily wrapped in a source
2255
- * declaration (freeze instant, source session, unreviewed-content warning)
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 promptText(task) {
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 = [tagPromptPreamble(task), handoverPreamble].filter((part) => part !== void 0);
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
- function isErrorTurnEnd(data) {
2291
- if (typeof data !== "object" || data === null) return false;
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
- return typeof reason === "object" && reason !== null && reason.kind === "error";
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.pinAndPrompt(reused, task, permission);
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
- return isErrorTurnEnd(turnEnd.event.data) ? {
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
- } : { outcome: "succeeded" };
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, { sessionDefaultPermission: options.sessionDefaultPermission });
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.run !== void 0) this.scheduleLaunch(result.run);
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 reuseSessionId = reusableSessionId(opened.task, this.idleSessionIds);
2966
- const sessionId = await this.runner.launch(opened.task, reuseSessionId === void 0 ? {} : { reuseSessionId });
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
- const opened = this.ledger.openScheduled(schedule.taskId, next, now);
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
- scheduleLaunch(opened) {
3027
- this.launch(opened).catch((error) => {
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 ../../node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
3083
- /** The one Remote failure class shared by owners, the Gateway, and consumers. */
3084
- /**
3085
- * One Remote call failure: a real Error carrying its stable code and typed
3086
- * details. Owners throw it at the failure point; the Host Gateway encodes it
3087
- * onto the wire unchanged; the Client face rebuilds an instance for the
3088
- * `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
3089
- * Discrimination is always by `code`, never by instanceof.
3090
- */
3091
- var RemoteError = class extends Error {
3092
- code;
3093
- details;
3094
- /** Structural marker: cross-realm/bundle identification never uses instanceof. */
3095
- isDSHRemoteError = true;
3096
- /**
3097
- * @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
3098
- * @param message - human diagnostic carried across the wire.
3099
- * @param details - structured payload typed by the code.
3100
- * @param options - standard Error options (`cause` survives in-process only).
3101
- */
3102
- constructor(code, message, details, options) {
3103
- super(message, options);
3104
- this.code = code;
3105
- this.details = details;
3106
- this.name = "RemoteError";
3107
- }
3108
- };
3109
- /**
3110
- * Remote decorators and explicit Gateway bindings backed by versioned
3111
- * descriptors carried on decorated class prototypes. Strict reflection
3112
- * remains a Typert compiler responsibility.
3113
- * @module @deepseek-ai/dsh-typert-protocol
3114
- */
3115
- const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
3116
- /**
3117
- * Test one generated Remote name against the Connection endpoint grammar.
3118
- * @param value - namespace, method, lookup, or Context segment.
3119
- * @returns whether the value can cross the shared RPC carrier unchanged.
3120
- */
3121
- function isTypertRemoteSegment(value) {
3122
- return value !== "." && value !== ".." && TYPERT_REMOTE_SEGMENT_PATTERN.test(value);
3123
- }
3124
- const REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-methods";
3125
- /**
3126
- * Bind one visible Service field to a Cordis key and Remote namespace. A
3127
- * service that owns a Cordis Context also gives its tree `ctx.invocation`,
3128
- * `undefined` outside a Remote call, so no `TypertRemoteService` is needed for
3129
- * a Host composition to read it.
3130
- * @param service - owning Service instance, normally `this`.
3131
- * @param serviceKey - exact Cordis service key.
3132
- * @param options - optional distinct wire namespace.
3133
- * @returns a frozen, inspectable binding with no compiler-injected metadata.
3134
- */
3135
- function bindTypertRemote(service, serviceKey, options = {}) {
3136
- validateName("service key", serviceKey);
3137
- const namespace = options.namespace ?? serviceKey;
3138
- validateName("namespace", namespace);
3139
- const ctx = Reflect.get(service, "ctx");
3140
- if (ctx instanceof Context) provideInvocationAccessor(ctx);
3141
- return Object.freeze({
3142
- service,
3143
- serviceKey,
3144
- namespace
3145
- });
3146
- }
3147
- /** Cordis Service base that exposes its registered name through Typert Gateway. */
3148
- var TypertRemoteService = class extends Service {
3149
- /** Visible binding consumed by the Gateway's source-mode discovery. */
3150
- typertRemote;
3151
- /**
3152
- * Register the Service and bind the same key to Typert Gateway.
3153
- * @param ctx - owning Cordis Context.
3154
- * @param serviceKey - exact Cordis service key and default wire namespace.
3155
- * @param options - optional distinct wire namespace.
3156
- */
3157
- constructor(ctx, serviceKey, options = {}) {
3158
- super(ctx, serviceKey);
3159
- this.typertRemote = bindTypertRemote(this, this.name, options);
3160
- }
3161
- };
3162
- /**
3163
- * Make `ctx.invocation` read as `undefined` outside a Remote call instead of the
3164
- * reflect service's "cannot get property" error; a call-derived Context shadows
3165
- * the accessor with its own property. The first Remote Service constructed in a
3166
- * tree registers it on the root, where it outlives any one Service.
3167
- */
3168
- function provideInvocationAccessor(ctx) {
3169
- if (Object.hasOwn(ctx.root.reflect.props, "invocation")) return;
3170
- ctx.root.accessor("invocation", { get: () => void 0 });
3171
- }
3172
- function Remote(methodExportOrOptions, context) {
3173
- if (typeof methodExportOrOptions === "string") {
3174
- validateName("Remote export name", methodExportOrOptions);
3175
- return remoteDecorator({ kind: "direct" }, void 0, methodExportOrOptions);
3176
- }
3177
- if (typeof methodExportOrOptions === "object") {
3178
- if (remoteOptionMode(methodExportOrOptions) !== "stream" || Reflect.ownKeys(methodExportOrOptions).length !== 1) throw new TypeError("typert-protocol: Remote options must contain exactly mode: \"stream\"");
3179
- return remoteDecorator({ kind: "direct" }, "stream");
3180
- }
3181
- if (context === void 0) throw new TypeError("typert-protocol: Remote decorator context is missing");
3182
- addMarkerInitializer(context, { kind: "direct" });
3183
- }
3184
- function remoteOptionMode(options) {
3185
- return Reflect.get(options, "mode");
3186
- }
3187
- function remoteDecorator(invocation, mode, exportName) {
3188
- return function(_method, context) {
3189
- addMarkerInitializer(context, invocation, mode, exportName);
3190
- };
3191
- }
3192
- function readRemoteMethodDescriptor(prototype) {
3193
- const property = Object.getOwnPropertyDescriptor(prototype, REMOTE_METHOD_DESCRIPTOR);
3194
- if (property === void 0) return void 0;
3195
- const descriptor = property.value;
3196
- if (descriptor === null || typeof descriptor !== "object") throw new TypeError("typert-protocol: Remote method descriptor must be an object");
3197
- const version = Reflect.get(descriptor, "version");
3198
- if (version !== 1) throw new TypeError(`typert-protocol: unsupported Remote method descriptor version ${String(version)}`);
3199
- const methods = Reflect.get(descriptor, "methods");
3200
- if (!Array.isArray(methods)) throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
3201
- return descriptor;
3202
- }
3203
- function addMarkerInitializer(context, invocation, mode, exportName) {
3204
- if (context.private || context.static || typeof context.name !== "string") throw new TypeError("typert-protocol: Remote decorators require a public instance method with a string name");
3205
- const method = context.name;
3206
- context.addInitializer(function() {
3207
- const prototype = Object.getPrototypeOf(this);
3208
- if (prototype === null) throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`);
3209
- mark(prototype, method, invocation, mode, exportName);
3210
- });
3211
- }
3212
- function mark(prototype, method, invocation, mode, exportName) {
3213
- const descriptor = readRemoteMethodDescriptor(prototype);
3214
- const marker = Object.freeze({
3215
- method,
3216
- ...exportName === void 0 || exportName === method ? {} : { exportName },
3217
- ...mode === void 0 ? {} : { mode },
3218
- invocation: Object.freeze(invocation)
3219
- });
3220
- const current = descriptor?.methods.find((candidate) => candidate.method === method);
3221
- if (current !== void 0) {
3222
- if (current.exportName === marker.exportName && current.mode === marker.mode && sameInvocation(current.invocation, invocation)) return;
3223
- throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
3224
- }
3225
- Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
3226
- configurable: true,
3227
- value: Object.freeze({
3228
- version: 1,
3229
- methods: Object.freeze([...descriptor?.methods ?? [], marker])
3230
- })
3231
- });
3232
- }
3233
- function sameInvocation(left, right) {
3234
- if (left.kind === "direct") return right.kind === "direct";
3235
- if (right.kind === "direct") return false;
3236
- return left.context === right.context;
3237
- }
3238
- function validateName(subject, value) {
3239
- if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
3240
- }
3241
- //#endregion
3242
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
3243
- /**
3244
- * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
3245
- * @param value - value to freeze.
3246
- * @returns the same value after every reachable enumerable child is frozen.
3247
- */
3248
- function deepFreeze(value) {
3249
- const seen = /* @__PURE__ */ new WeakSet();
3250
- const pending = [{
3251
- kind: "visit",
3252
- node: value
3253
- }];
3254
- while (pending.length > 0) {
3255
- const task = pending.pop();
3256
- /* v8 ignore next -- the loop condition guarantees one pending task. */
3257
- if (task === void 0) continue;
3258
- if (task.kind === "property") {
3259
- pending.push({
3260
- kind: "visit",
3261
- node: task.source[task.key]
3262
- });
3263
- continue;
3264
- }
3265
- const node = task.node;
3266
- if (node === null || typeof node !== "object") continue;
3267
- if (node instanceof AbortSignal) continue;
3268
- if (seen.has(node)) continue;
3269
- seen.add(node);
3270
- Object.freeze(node);
3271
- const keys = Object.keys(node);
3272
- for (let index = keys.length - 1; index >= 0; index--) {
3273
- const key = keys[index];
3274
- /* v8 ignore next -- the loop is bounded by the captured key count. */
3275
- if (key === void 0) continue;
3276
- pending.push({
3277
- kind: "property",
3278
- source: node,
3279
- key
3280
- });
3281
- }
3282
- }
3283
- return value;
3284
- }
3285
- //#endregion
3286
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
3287
- /**
3288
- * Random v4 UUID, minted from `crypto.getRandomValues`.
3289
- * @returns the UUID string.
3290
- */
3291
- function randomUUID() {
3292
- const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
3293
- const hex = Array.from(bytes, (byte, index) => {
3294
- return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
3295
- }).join("");
3296
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
3297
- }
3298
- //#endregion
3299
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-brand/lib/index.js
3300
- /**
3301
- * Duplicate-install-safe nominal primitive helpers.
3302
- *
3303
- * A brand makes structurally identical strings or numbers non-interchangeable
3304
- * at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
3305
- * expected, and an event sequence cannot be passed as a log offset. Comparison,
3306
- * logging, and serialization retain the underlying primitive behavior.
3307
- *
3308
- * This package owns no concrete domain value and keeps no runtime identity or mutable
3309
- * state, so independently installed copies produce interchangeable values.
3310
- *
3311
- * @module @deepseek-ai/dsh-brand
3312
- */
3313
- /**
3314
- * Apply a compile-time string brand without changing the value.
3315
- * @param value - string admitted by the domain that owns the target brand.
3316
- * @returns the same string with the requested compile-time brand.
3317
- */
3318
- function brandString(value) {
3319
- return value;
3320
- }
3321
- //#endregion
3322
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
3323
- /** Largest delay Node schedules without clamping it to one millisecond. */
3324
- const MAX_TIMER_DELAY_MS = 2147483647;
3325
- //#endregion
3326
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-llm/lib/index.js
3327
- /**
3328
- * Detach and deep-freeze a message whose identity already exists.
3329
- * @param message - complete message, including its stable identity.
3330
- * @returns an immutable snapshot that preserves the identity.
3331
- */
3332
- function freezeMessage(message) {
3333
- return deepFreeze(structuredClone(message));
3334
- }
3335
- /**
3336
- * Create one identified message and freeze it before publication.
3337
- * @param input - complete role, content, and source for a new message.
3338
- * @returns an immutable message with a fresh stable identity.
3339
- */
3340
- function createMessage(input) {
3341
- return deepFreeze(structuredClone({
3342
- ...input,
3343
- id: brandString(randomUUID())
3344
- }));
3345
- }
3346
- /**
3347
- * Create one identified user-role message and freeze it before publication.
3348
- * @param input - complete content and source for a new user message.
3349
- * @returns an immutable user message with a fresh stable identity.
3350
- */
3351
- function createUserMessage(input) {
3352
- return createMessage({
3353
- ...input,
3354
- role: "user"
3355
- });
3356
- }
3357
- /**
3358
- * Harness error base with a stable machine-routable code and chained cause.
3359
- * Package errors extend it so tool results and replay can retain failure class.
3360
- * @module @deepseek-ai/dsh-llm/error
3361
- */
3362
- /**
3363
- * Base class for all harness errors. Carries a `code` (stable, programmatic —
3364
- * e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
3365
- * human-readable `message`, and supports `cause` chaining via the standard
3366
- * `ErrorOptions`. `name` defaults to the subclass constructor name.
3367
- */
3368
- var HarnessError = class extends Error {
3369
- /** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
3370
- code;
3371
- constructor(message, code, options) {
3372
- super(message, options);
3373
- this.code = code;
3374
- this.name = new.target.name;
3375
- }
3376
- };
3377
- /**
3378
- * Canonical provider-neutral code for a response that completed normally but
3379
- * carried no content blocks at all. Providers occasionally emit a degenerate
3380
- * completion (a terminal stop with zero output); adapters classify it as this
3381
- * failure instead of yielding an empty assistant message, because an empty
3382
- * message silently ends the turn with nothing for the user or the loop to act
3383
- * on. The attempt produced nothing durable, so retry policy treats it as safe
3384
- * to repeat.
3385
- */
3386
- const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
3387
- new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
3388
- new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
3389
- new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
3390
- /**
3391
- * Provider-owned request-retry policy configuration and resolution.
3392
- *
3393
- * Adapters expose one resolved policy per registered provider route; the
3394
- * optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
3395
- *
3396
- * @module @deepseek-ai/dsh-llm/retry-policy
3397
- */
3398
- const DEFAULT_MAX_RETRIES = 5;
3399
- const DEFAULT_INITIAL_DELAY_MS = 500;
3400
- const DEFAULT_MAX_DELAY_MS = 1e4;
3401
- const DEFAULT_JITTER_RATIO = .1;
3402
- const DEFAULT_RETRYABLE_CODES = Object.freeze([
3403
- EMPTY_RESPONSE_CODE,
3404
- "RATE_LIMIT",
3405
- "SERVER",
3406
- "TIMEOUT",
3407
- "TRANSPORT"
3408
- ]);
3409
- const backoffSchema = z.object({
3410
- initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
3411
- maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
3412
- jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
3413
- });
3414
- const normalPolicySchema = z.object({
3415
- mode: z.const("normal").required(),
3416
- maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
3417
- retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
3418
- backoff: backoffSchema
3419
- });
3420
- const alwaysPolicySchema = z.object({
3421
- mode: z.const("always").required(),
3422
- backoff: backoffSchema
3423
- });
3424
- z.union([normalPolicySchema, alwaysPolicySchema]);
3425
- const NORMAL_POLICY_KEYS = /* @__PURE__ */ new Set([
3426
- "mode",
3427
- "maxRetries",
3428
- "retryableCodes",
3429
- "backoff"
3430
- ]);
3431
- const ALWAYS_POLICY_KEYS = /* @__PURE__ */ new Set([
3432
- "mode",
3433
- "maxRetries",
3434
- "retryableCodes",
3435
- "backoff"
3436
- ]);
3437
- const BACKOFF_KEYS = /* @__PURE__ */ new Set([
3438
- "initialDelayMs",
3439
- "maxDelayMs",
3440
- "jitterRatio"
3441
- ]);
3442
- function validateKeys(value, allowed, path) {
3443
- for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${path}: unknown key "${key}"`);
3444
- }
3445
- function resolveBackoff(config, path) {
3446
- if (config !== void 0) validateKeys(config, BACKOFF_KEYS, path);
3447
- const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
3448
- const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
3449
- const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
3450
- if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > 2147483647) throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
3451
- if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > 2147483647) throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
3452
- if (initialDelayMs > maxDelayMs) throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
3453
- if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) throw new Error(`${path}.jitterRatio must be between 0 and 1`);
3454
- return Object.freeze({
3455
- initialDelayMs,
3456
- maxDelayMs,
3457
- jitterRatio
3458
- });
3459
- }
3460
- /**
3461
- * Validate, default, and detach one provider-owned retry policy.
3462
- * @param config - optional provider configuration; omission selects normal defaults.
3463
- * @param path - diagnostic path naming the provider config that owns the value.
3464
- * @returns an immutable policy safe to capture in provider registration state.
3465
- */
3466
- function resolveRetryPolicy(config, path) {
3467
- if (config === void 0) return Object.freeze({
3468
- mode: "normal",
3469
- maxRetries: DEFAULT_MAX_RETRIES,
3470
- retryableCodes: DEFAULT_RETRYABLE_CODES,
3471
- ...resolveBackoff(void 0, `${path}.backoff`)
3472
- });
3473
- switch (config.mode) {
3474
- case "normal": {
3475
- validateKeys(config, NORMAL_POLICY_KEYS, path);
3476
- const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
3477
- const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
3478
- if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
3479
- if (retryableCodes.length === 0) throw new Error(`${path}.retryableCodes must not be empty`);
3480
- if (retryableCodes.some((code) => typeof code !== "string" || code.length === 0)) throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
3481
- if (new Set(retryableCodes).size !== retryableCodes.length) throw new Error(`${path}.retryableCodes must not contain duplicates`);
3482
- return Object.freeze({
3483
- mode: "normal",
3484
- maxRetries,
3485
- retryableCodes: Object.freeze([...retryableCodes]),
3486
- ...resolveBackoff(config.backoff, `${path}.backoff`)
3487
- });
3488
- }
3489
- case "always":
3490
- validateKeys(config, ALWAYS_POLICY_KEYS, path);
3491
- return Object.freeze({
3492
- mode: "always",
3493
- ...resolveBackoff(config.backoff, `${path}.backoff`)
3494
- });
3495
- default: throw new Error(`${path}.mode must be "normal" or "always"`);
3496
- }
3497
- }
3498
- /**
3499
- * Field-wise equality over {@link LlmCallConfig} — the comparison a caller
3500
- * runs to decide whether a proposed configuration is a real change (worth a
3501
- * logged header snapshot) or the held one restated.
3502
- * @param a - one configuration.
3503
- * @param b - the other.
3504
- * @returns whether every field (including the `stop` list, element-wise) matches.
3505
- */
3506
- function callConfigEquals(a, b) {
3507
- if (a.provider !== b.provider || a.model !== b.model || a.reasoningEffort !== b.reasoningEffort || a.temperature !== b.temperature || a.maxTokens !== b.maxTokens) return false;
3508
- if (a.stop === void 0 || b.stop === void 0) return a.stop === b.stop;
3509
- return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
3510
- }
3511
- /**
3512
- * Normalization for values thrown by a final LLM adapter boundary.
3513
- *
3514
- * @module @deepseek-ai/dsh-llm/adapter-failure
3515
- */
3516
- /**
3517
- * Detach serializable provider facts from a value thrown by an adapter.
3518
- * @param value - arbitrary value thrown during adapter dispatch or iteration.
3519
- * @returns immutable provider-neutral facts suitable for a terminal finish chunk.
3520
- * @internal
3521
- */
3522
- function normalizeLlmFailure(value) {
3523
- const error = value instanceof Error ? value : new HarnessError(thrownMessage(value), "UNKNOWN", { cause: value });
3524
- const carried = ownFailureSnapshot(error);
3525
- if (carried !== void 0 && carried.code === ownErrorCode(error)) return carried;
3526
- return Object.freeze({
3527
- message: errorMessage(error),
3528
- code: harnessErrorCode(error)
3529
- });
3530
- }
3531
- /** Render a non-Error throw without letting hostile coercion escape normalization. */
3532
- function thrownMessage(value) {
3533
- try {
3534
- const message = String(value);
3535
- return message.length > 0 ? message : "LLM adapter failed";
3536
- } catch (_hostileThrownValue) {
3537
- return "LLM adapter failed";
3538
- }
3539
- }
3540
- /** Read a foreign error's own data-backed `code` without invoking accessors. */
3541
- function ownErrorCode(error) {
3542
- try {
3543
- const descriptor = Object.getOwnPropertyDescriptor(error, "code");
3544
- return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
3545
- } catch (_sdkPropertyTrap) {
3546
- return;
3547
- }
3548
- }
3549
- /** Snapshot an own data property without invoking an SDK-defined accessor. */
3550
- function ownFailureSnapshot(error) {
3551
- try {
3552
- const descriptor = Object.getOwnPropertyDescriptor(error, "failure");
3553
- return descriptor !== void 0 && "value" in descriptor ? failureSnapshot(descriptor.value) : void 0;
3554
- } catch (_sdkPropertyTrap) {
3555
- return;
3556
- }
3557
- }
3558
- /** Validate and detach an arbitrary serializable failure payload. */
3559
- function failureSnapshot(value) {
3560
- if (typeof value !== "object" || value === null) return void 0;
3561
- try {
3562
- const candidate = value;
3563
- const message = candidate.message;
3564
- const code = candidate.code;
3565
- const status = candidate.status;
3566
- const providerRetryAfterMs = candidate.providerRetryAfterMs;
3567
- const requestId = candidate.requestId;
3568
- const offloadImages = candidate.offloadImages;
3569
- if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0) || offloadImages !== void 0 && (!Number.isSafeInteger(offloadImages) || offloadImages <= 0)) return void 0;
3570
- return Object.freeze({
3571
- message,
3572
- code,
3573
- ...status === void 0 ? {} : { status },
3574
- ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
3575
- ...requestId === void 0 ? {} : { requestId },
3576
- ...offloadImages === void 0 ? {} : { offloadImages }
3577
- });
3578
- } catch (_sdkFailureGetter) {
3579
- return;
3580
- }
3581
- }
3582
- /** Read an SDK error message without letting an accessor replace the primary failure. */
3583
- function errorMessage(error) {
3584
- try {
3585
- const message = error.message;
3586
- if (typeof message === "string" && message.length > 0) return message;
3587
- } catch (_sdkMessageGetter) {}
3588
- return "LLM adapter failed";
3589
- }
3590
- /** Trust only Harness-owned codes; third-party SDK codes are not our taxonomy. */
3591
- function harnessErrorCode(error) {
3592
- return error instanceof HarnessError ? error.code : "UNKNOWN";
3593
- }
3594
- function quoted(value) {
3595
- return JSON.stringify(value);
3596
- }
3597
- /**
3598
- * Stable text shown to a model that cannot accept one durable image reference.
3599
- * @param ref - durable normalized attachment omitted from the request.
3600
- * @returns deterministic text-only placeholder.
3601
- */
3602
- function textOnlyImageText(ref) {
3603
- return `[image omitted because this model accepts text only; attachment sha256:${String(ref.attachmentId).slice(7, 15)}]`;
3604
- }
3605
- /**
3606
- * True when typed model content contains an image block. This is the one image
3607
- * walk shared by every image policy (capability gating, text-only
3608
- * serialization, compaction survey), so a consumer cannot silently diverge.
3609
- * @param content - typed model content blocks.
3610
- * @returns whether any block is an image.
3611
- */
3612
- function contentHasImage(content) {
3613
- return content.some((block) => block.type === "image");
3614
- }
3615
- /**
3616
- * True when typed model content contains a file block.
3617
- * Reads current content on every call without retaining scan results.
3618
- * @param content - typed model content blocks.
3619
- * @returns whether any block is a file.
3620
- */
3621
- function contentHasFile(content) {
3622
- for (const block of content) if (block.type === "file") return true;
3623
- return false;
3624
- }
3625
- /**
3626
- * Stable model-facing handle for one durable file reference: the address of
3627
- * the verbatim stored copy and the instruction to read it on demand. This is
3628
- * the only representation a provider ever receives for a file.
3629
- * @param ref - durable verbatim file reference.
3630
- * @param readonlyPath - execution-world path of the stored copy, when resolvable.
3631
- * @returns deterministic handle text naming the file, its size, and its address.
3632
- */
3633
- function fileHandleText(ref, readonlyPath) {
3634
- const digest = String(ref.attachmentId).slice(7, 15);
3635
- const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
3636
- if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
3637
- return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
3638
- }
3639
- /** Replace every file occurrence with handle text. */
3640
- function replaceFilesWithHandles(blocks, resolvePath) {
3641
- let next;
3642
- for (const [index, block] of blocks.entries()) {
3643
- if (block.type === "file") {
3644
- next ??= blocks.slice(0, index);
3645
- next.push({
3646
- type: "text",
3647
- text: fileHandleText(block.attachment, resolvePath(block.attachment))
3648
- });
3649
- continue;
3650
- }
3651
- next?.push(block);
3652
- }
3653
- return next ?? blocks;
3654
- }
3655
- function projectFilesToText(messages, resolvePath) {
3656
- if (!messages.some((message) => contentHasFile(message.content))) return messages;
3657
- return messages.map((message) => {
3658
- const content = replaceFilesWithHandles(message.content, resolvePath);
3659
- return content === message.content ? message : {
3660
- ...message,
3661
- content
3662
- };
3663
- });
3664
- }
3665
- /** Replace every image occurrence for a text-only model. */
3666
- function replaceImagesForTextModel(blocks) {
3667
- let next;
3668
- for (const [index, block] of blocks.entries()) {
3669
- if (block.type === "image") {
3670
- next ??= blocks.slice(0, index);
3671
- next.push({
3672
- type: "text",
3673
- text: textOnlyImageText(block.attachment)
3674
- });
3675
- continue;
3676
- }
3677
- next?.push(block);
3678
- }
3679
- return next ?? blocks;
3680
- }
3681
- function projectImagesForTextModel(messages) {
3682
- if (!messages.some((message) => contentHasImage(message.content))) return messages;
3683
- return messages.map((message) => {
3684
- const content = replaceImagesForTextModel(message.content);
3685
- return content === message.content ? message : {
3686
- ...message,
3687
- content
3688
- };
3689
- });
3690
- }
3691
- /**
3692
- * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
3693
- * adapters from drifting. See
3694
- * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
3695
- *
3696
- * App-attribution vocabulary for provider requests.
3697
- * @module @deepseek-ai/dsh-llm/attribution
3698
- */
3699
- const { version } = createRequire(import.meta.url)("../package.json");
3700
- /**
3701
- * LLM service: adapter registry with a waterfall-interceptable streaming call
3702
- * API. Exports the `LlmRuntime` default, the abstract `LlmAdapter` for
3703
- * provider backends, and `BlockAssembler` for chunk assembly.
3704
- *
3705
- * @module @deepseek-ai/dsh-llm
3706
- */
3707
- var __runInitializers = function(thisArg, initializers, value) {
3708
- var useValue = arguments.length > 2;
3709
- for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
3710
- return useValue ? value : void 0;
3711
- };
3712
- var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
3713
- function accept(f) {
3714
- if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
3715
- return f;
3716
- }
3717
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
3718
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
3719
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
3720
- var _, done = false;
3721
- for (var i = decorators.length - 1; i >= 0; i--) {
3722
- var context = {};
3723
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
3724
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
3725
- context.addInitializer = function(f) {
3726
- if (done) throw new TypeError("Cannot add initializers after decoration has completed");
3727
- extraInitializers.push(accept(f || null));
3728
- };
3729
- var result = (0, decorators[i])(kind === "accessor" ? {
3730
- get: descriptor.get,
3731
- set: descriptor.set
3732
- } : descriptor[key], context);
3733
- if (kind === "accessor") {
3734
- if (result === void 0) continue;
3735
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
3736
- if (_ = accept(result.get)) descriptor.get = _;
3737
- if (_ = accept(result.set)) descriptor.set = _;
3738
- if (_ = accept(result.init)) initializers.unshift(_);
3739
- } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
3740
- else descriptor[key] = _;
3741
- }
3742
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
3743
- done = true;
3744
- };
3745
- /**
3746
- * Typed error for LLM-related failures. Extends {@link HarnessError}, so the
3747
- * `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
3748
- */
3749
- var LlmError = class extends HarnessError {
3750
- /** Serializable facts retained beside this live Error. */
3751
- failure;
3752
- /**
3753
- * @param message - non-empty human-readable failure summary.
3754
- * @param code - non-empty stable provider-neutral machine code.
3755
- * @param options - optional cause and validated serializable provider facts.
3756
- */
3757
- constructor(message, code, options) {
3758
- if (typeof message !== "string" || message.length === 0) throw new Error("LlmError message must be a non-empty string");
3759
- if (typeof code !== "string" || code.length === 0) throw new Error("LlmError code must be a non-empty string");
3760
- if (options?.status !== void 0 && (!Number.isInteger(options.status) || options.status < 100 || options.status > 599)) throw new Error("LlmError status must be an integer from 100 through 599");
3761
- if (options?.providerRetryAfterMs !== void 0 && (!Number.isFinite(options.providerRetryAfterMs) || options.providerRetryAfterMs <= 0)) throw new Error("LlmError providerRetryAfterMs must be a positive finite number");
3762
- if (options?.requestId !== void 0 && (typeof options.requestId !== "string" || options.requestId.length === 0)) throw new Error("LlmError requestId must be a non-empty string");
3763
- super(message, code, options);
3764
- this.name = "LlmError";
3765
- this.failure = Object.freeze({
3766
- message,
3767
- code,
3768
- ...options?.status === void 0 ? {} : { status: options.status },
3769
- ...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
3770
- ...options?.requestId === void 0 ? {} : { requestId: options.requestId },
3771
- ...options?.offloadImages === void 0 ? {} : { offloadImages: options.offloadImages }
3772
- });
3773
- }
3774
- };
3775
- (() => {
3776
- let _classSuper = TypertRemoteService;
3777
- let _instanceExtraInitializers = [];
3778
- let _listProviders_decorators;
3779
- let _listConfigurableProviders_decorators;
3780
- let _remoteDiscoverModels_decorators;
3781
- return class LlmRuntime extends _classSuper {
3782
- static {
3783
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
3784
- _listProviders_decorators = [Remote];
3785
- _listConfigurableProviders_decorators = [Remote];
3786
- _remoteDiscoverModels_decorators = [Remote("discoverModels")];
3787
- __esDecorate(this, null, _listProviders_decorators, {
3788
- kind: "method",
3789
- name: "listProviders",
3790
- static: false,
3791
- private: false,
3792
- access: {
3793
- has: (obj) => "listProviders" in obj,
3794
- get: (obj) => obj.listProviders
3795
- },
3796
- metadata: _metadata
3797
- }, null, _instanceExtraInitializers);
3798
- __esDecorate(this, null, _listConfigurableProviders_decorators, {
3799
- kind: "method",
3800
- name: "listConfigurableProviders",
3801
- static: false,
3802
- private: false,
3803
- access: {
3804
- has: (obj) => "listConfigurableProviders" in obj,
3805
- get: (obj) => obj.listConfigurableProviders
3806
- },
3807
- metadata: _metadata
3808
- }, null, _instanceExtraInitializers);
3809
- __esDecorate(this, null, _remoteDiscoverModels_decorators, {
3810
- kind: "method",
3811
- name: "remoteDiscoverModels",
3812
- static: false,
3813
- private: false,
3814
- access: {
3815
- has: (obj) => "remoteDiscoverModels" in obj,
3816
- get: (obj) => obj.remoteDiscoverModels
3817
- },
3818
- metadata: _metadata
3819
- }, null, _instanceExtraInitializers);
3820
- if (_metadata) Object.defineProperty(this, Symbol.metadata, {
3821
- enumerable: true,
3822
- configurable: true,
3823
- writable: true,
3824
- value: _metadata
3825
- });
3826
- }
3827
- adapters = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Map());
3828
- directory = /* @__PURE__ */ new Map();
3829
- discoveries = /* @__PURE__ */ new Map();
3830
- constructor(ctx) {
3831
- super(ctx, "llm");
3832
- }
3833
- /** Notify topology observers without letting one broken listener veto the commit. */
3834
- emitAdaptersUpdated() {
3835
- let invariantFailure;
3836
- for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
3837
- const returned = listener();
3838
- if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
3839
- this.warnAdaptersListenerFailure(error);
3840
- });
3841
- } catch (error) {
3842
- if (error?.code === "INVARIANT") {
3843
- invariantFailure ??= error;
3844
- continue;
3845
- }
3846
- this.warnAdaptersListenerFailure(error);
3847
- }
3848
- if (invariantFailure !== void 0) throw invariantFailure;
3849
- }
3850
- /** Contained-listener diagnostic shared by the sync and async failure paths. */
3851
- warnAdaptersListenerFailure(error) {
3852
- this.ctx.logger.warn("llm: an llm/adapters-updated listener failed");
3853
- this.ctx.logger.warn(error);
3854
- }
3855
- /**
3856
- * Register an adapter for the given provider routes. Throws `LlmError` with code
3857
- * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
3858
- * Disposed with the fiber.
3859
- * @param providers - every provider route this adapter should serve.
3860
- * @param adapter - the adapter that streams calls for those providers.
3861
- * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
3862
- */
3863
- registerAdapter(providers, adapter) {
3864
- const owned = /* @__PURE__ */ new Set();
3865
- let released = false;
3866
- const dispose = this.ctx.effect(function* () {
3867
- if (providers.length === 0) throw new LlmError("an adapter must register at least one provider", "INVALID_ADAPTER");
3868
- this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
3869
- yield () => {
3870
- released = true;
3871
- for (const provider of owned) this.adapters.delete(provider);
3872
- owned.clear();
3873
- this.emitAdaptersUpdated();
3874
- };
3875
- }.bind(this), "llm.registerAdapter()");
3876
- const handle = (() => void dispose());
3877
- handle.replace = (next) => {
3878
- if (released) throw new LlmError("a disposed adapter registration cannot replace its routes", "REGISTRATION_DISPOSED");
3879
- this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
3880
- };
3881
- return handle;
3882
- }
3883
- /**
3884
- * Validate one candidate route set for `adapter`, treating routes this
3885
- * registration already holds as available. Nothing is mutated: a rejected
3886
- * candidate leaves the registry exactly as it was.
3887
- */
3888
- prepareRoutes(providers, adapter, owned) {
3889
- const unique = /* @__PURE__ */ new Set();
3890
- const registrations = [];
3891
- for (const provider of providers) {
3892
- if (provider.length === 0) throw new LlmError("adapter provider names must be non-empty", "INVALID_ADAPTER");
3893
- if (unique.has(provider) || this.adapters.has(provider) && !owned.has(provider)) throw new LlmError(`an adapter for provider "${provider}" is already registered`, "DUPLICATE_ADAPTER");
3894
- const info = adapter.providerInfo(provider);
3895
- if (typeof info.id !== "string" || info.id !== provider || typeof info.name !== "string" || info.name.length === 0) throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, "INVALID_ADAPTER");
3896
- unique.add(provider);
3897
- const retryPolicy = adapter.providerRetryPolicy(provider) ?? resolveRetryPolicy(void 0, `llm: provider "${provider}" retryPolicy`);
3898
- registrations.push({
3899
- adapter,
3900
- provider: {
3901
- id: info.id,
3902
- name: info.name
3903
- },
3904
- retryPolicy
3905
- });
3906
- }
3907
- return registrations;
3908
- }
3909
- /**
3910
- * Swap this registration's routes for the prepared ones in one synchronous
3911
- * section, so no observer can see the registry between the release and the
3912
- * re-registration. The route set's one mutation point is also where
3913
- * `llm/adapters-updated` is published, so a `replace` announces itself
3914
- * exactly like a first registration.
3915
- */
3916
- commitRoutes(owned, registrations) {
3917
- for (const provider of owned) this.adapters.delete(provider);
3918
- owned.clear();
3919
- for (const registration of registrations) {
3920
- this.adapters.set(registration.provider.id, registration);
3921
- owned.add(registration.provider.id);
3922
- }
3923
- this.emitAdaptersUpdated();
3924
- }
3925
- /**
3926
- * Describe provider routes with a registered adapter.
3927
- * @returns detached provider metadata in registration order.
3928
- */
3929
- listProviders() {
3930
- return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
3931
- }
3932
- /**
3933
- * Declare provider routes an adapter plugin can activate through
3934
- * configuration. Registration is all-or-nothing: an empty list, invalid
3935
- * entry, or a provider already declared by any registration throws
3936
- * `LlmError` without registering the rest. Disposed with the fiber.
3937
- * @param entries - every configurable provider this plugin owns.
3938
- * @returns a handle that withdraws all of them, and can atomically replace them.
3939
- */
3940
- registerConfigurableProviders(entries) {
3941
- let held = [];
3942
- let disposed = false;
3943
- /**
3944
- * Validate a candidate set in full against everything this registration
3945
- * does not already hold, then publish it. Nothing is written until the
3946
- * whole set passes, so a refused candidate leaves the current entries in
3947
- * place — the property that makes `replace` a swap rather than a
3948
- * delete-then-add that can strand the directory empty.
3949
- */
3950
- const commit = (candidates) => {
3951
- const detached = [];
3952
- const own = new Set(held.map((entry) => entry.provider));
3953
- for (const entry of candidates) {
3954
- if (entry.provider.length === 0 || entry.displayName.length === 0 || entry.settingsNs.length === 0) throw new LlmError("configurable providers need a non-empty provider, displayName, and settingsNs", "INVALID_DIRECTORY");
3955
- if (entry.settingsPath.some((segment) => segment.length === 0)) throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, "INVALID_DIRECTORY");
3956
- if (this.directory.has(entry.provider) && !own.has(entry.provider) || detached.some((seen) => seen.provider === entry.provider)) throw new LlmError(`configurable provider "${entry.provider}" is already declared`, "DUPLICATE_DIRECTORY");
3957
- detached.push({
3958
- ...entry,
3959
- settingsPath: [...entry.settingsPath]
3960
- });
3961
- }
3962
- for (const entry of held) this.directory.delete(entry.provider);
3963
- for (const entry of detached) this.directory.set(entry.provider, entry);
3964
- held = detached;
3965
- this.emitAdaptersUpdated();
3966
- };
3967
- const dispose = this.ctx.effect(function* () {
3968
- if (entries.length === 0) throw new LlmError("a configurable-provider registration must declare at least one provider", "INVALID_DIRECTORY");
3969
- commit(entries);
3970
- yield () => {
3971
- disposed = true;
3972
- for (const entry of held) this.directory.delete(entry.provider);
3973
- held = [];
3974
- this.emitAdaptersUpdated();
3975
- };
3976
- }.bind(this), "llm.registerConfigurableProviders()");
3977
- const handle = (() => void dispose());
3978
- handle.replace = (next) => {
3979
- if (disposed) throw new LlmError("this configurable-provider registration was disposed", "REGISTRATION_DISPOSED");
3980
- commit(next);
3981
- };
3982
- return handle;
3983
- }
3984
- /**
3985
- * List every declared configurable provider, registered or dormant.
3986
- * @returns detached directory entries in declaration order.
3987
- */
3988
- listConfigurableProviders() {
3989
- return [...this.directory.values()].map((entry) => ({
3990
- ...entry,
3991
- settingsPath: [...entry.settingsPath]
3992
- }));
3993
- }
3994
- /**
3995
- * Offer to interrogate provider endpoints on behalf of the settings
3996
- * namespace this plugin owns. The namespace is the key because that is what
3997
- * a configuration surface already holds from the configurable-provider
3998
- * directory, and because a provider being *added* has no route to name yet.
3999
- * Disposed with the fiber.
4000
- * @param settingsNs - the namespace whose profiles this discovery serves.
4001
- * @param discover - interrogates one endpoint and must honor the supplied signal.
4002
- * @returns the disposer that withdraws the offer.
4003
- */
4004
- registerModelDiscovery(settingsNs, discover) {
4005
- const dispose = this.ctx.effect(function* () {
4006
- if (settingsNs.length === 0) throw new LlmError("model discovery needs a non-empty settings namespace", "INVALID_DISCOVERY");
4007
- if (this.discoveries.has(settingsNs)) throw new LlmError(`model discovery for "${settingsNs}" is already registered`, "DUPLICATE_DISCOVERY");
4008
- this.discoveries.set(settingsNs, discover);
4009
- yield () => {
4010
- this.discoveries.delete(settingsNs);
4011
- };
4012
- }.bind(this), "llm.registerModelDiscovery()");
4013
- return () => void dispose();
4014
- }
4015
- /**
4016
- * Interrogate one provider endpoint for the models it advertises. The
4017
- * request describes a draft, not a stored route, so nothing here reads or
4018
- * writes settings or credentials — the caller owns both, and the reply is
4019
- * candidate metadata a surface may offer for adoption.
4020
- * @param settingsNs - namespace whose registered discovery serves this draft.
4021
- * @param request - the endpoint, protocol, and one-shot credential to use.
4022
- * @param signal - caller cancellation.
4023
- * @returns the advertised models, deduplicated in endpoint order.
4024
- */
4025
- async discoverModels(settingsNs, request, signal) {
4026
- const discover = this.discoveries.get(settingsNs);
4027
- if (discover === void 0) throw new LlmError(`no model discovery is registered for "${settingsNs}"`, "NO_DISCOVERY");
4028
- if ((request.provider ?? "").length === 0 && (request.baseURL ?? "").length === 0) throw new LlmError("model discovery needs a provider route or a baseURL", "INVALID_DISCOVERY");
4029
- const discovered = signal === void 0 ? await discover(request) : await discover(request, signal);
4030
- const seen = /* @__PURE__ */ new Set();
4031
- const models = [];
4032
- for (const model of discovered) {
4033
- if (typeof model.id !== "string" || model.id.length === 0 || seen.has(model.id)) continue;
4034
- seen.add(model.id);
4035
- models.push({
4036
- id: model.id,
4037
- ...model.name === void 0 ? {} : { name: model.name },
4038
- ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
4039
- ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
4040
- ...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
4041
- });
4042
- }
4043
- return models;
4044
- }
4045
- /**
4046
- * Remote adapter for one draft provider interrogation.
4047
- * @param settingsNs - namespace whose registered discovery serves this draft.
4048
- * @param request - endpoint, protocol, and one-shot credential to use.
4049
- * @param signal - caller cancellation supplied by the Remote carrier.
4050
- * @returns advertised models in endpoint order.
4051
- * @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
4052
- */
4053
- async remoteDiscoverModels(settingsNs, request, signal) {
4054
- try {
4055
- return await this.discoverModels(settingsNs, request, signal);
4056
- } catch (error) {
4057
- throw new RemoteError("llm/model-discovery-rejected", error instanceof Error ? error.message : String(error), {
4058
- settingsNs,
4059
- ...request.baseURL === void 0 ? {} : { baseURL: request.baseURL }
4060
- }, { cause: error });
4061
- }
4062
- }
4063
- /**
4064
- * Resolve the retry policy captured when one provider route was registered.
4065
- * @param provider - registered provider route to inspect.
4066
- * @returns the provider-owned policy, with normal defaults already resolved.
4067
- */
4068
- providerRetryPolicy(provider) {
4069
- return this.registration(provider).retryPolicy;
4070
- }
4071
- /**
4072
- * Resolve provider-side request-image pricing for one exact route, or
4073
- * `undefined` when the provider is unregistered or declares none. Unknown
4074
- * providers degrade to `undefined` rather than throwing because callers
4075
- * price durable history whose route may no longer be mounted.
4076
- * @param provider - provider route named by a request header.
4077
- * @param model - exact model id named by the same header.
4078
- * @returns the owning adapter's image pricing for the route, when declared.
4079
- */
4080
- imageRequestPricing(provider, model) {
4081
- return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
4082
- }
4083
- /**
4084
- * Resolve the exact text one durable file occurrence contributes to every
4085
- * provider request in the current execution environment.
4086
- * @param ref - durable verbatim file reference from model history.
4087
- * @returns the same deterministic handle text used at adapter dispatch.
4088
- */
4089
- fileRequestText(ref) {
4090
- return fileHandleText(ref, this.fileReadPath(ref));
4091
- }
4092
- /** Detach typed adapter-owned modality metadata. */
4093
- detachedModalities(modalities) {
4094
- return modalities === void 0 ? void 0 : [...modalities];
4095
- }
4096
- /**
4097
- * Discover models advertised by one registered provider. Catalog membership
4098
- * is advisory and never changes routing or request validation.
4099
- * @param provider - registered provider route to inspect.
4100
- * @returns detached model metadata in adapter-preferred order.
4101
- */
4102
- async listModels(provider) {
4103
- const models = await this.registration(provider).adapter.listModels(provider);
4104
- const seen = /* @__PURE__ */ new Set();
4105
- return models.map((model) => {
4106
- if (typeof model.provider !== "string" || model.provider !== provider || typeof model.id !== "string" || model.id.length === 0 || typeof model.name !== "string" || model.name.length === 0 || model.description !== void 0 && typeof model.description !== "string" || seen.has(model.id)) throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, "INVALID_CATALOG");
4107
- seen.add(model.id);
4108
- const inputModalities = this.detachedModalities(model.inputModalities);
4109
- return {
4110
- provider: model.provider,
4111
- id: model.id,
4112
- name: model.name,
4113
- ...model.description === void 0 ? {} : { description: model.description },
4114
- ...inputModalities === void 0 ? {} : { inputModalities }
4115
- };
4116
- });
4117
- }
4118
- /**
4119
- * Resolve and validate all metadata from the adapter that owns one exact
4120
- * route. The result is detached from adapter-owned objects; catalog
4121
- * membership remains advisory and does not control request routing.
4122
- * @param provider - registered provider route to inspect.
4123
- * @param model - exact model id passed to the adapter.
4124
- * @param signal - optional cancellation for adapter-owned asynchronous lookup.
4125
- * @returns exact model identity plus available context and reasoning metadata.
4126
- */
4127
- async resolveModelInfo(provider, model, signal) {
4128
- return this.resolveModelInfoFor(this.registration(provider), model, signal);
4129
- }
4130
- async resolveModelInfoFor(registration, model, signal) {
4131
- const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
4132
- return this.normalizeModelInfo(registration, model, resolved);
4133
- }
4134
- /** Validate and detach one adapter-returned exact model result. */
4135
- normalizeModelInfo(registration, model, resolved) {
4136
- const provider = registration.provider.id;
4137
- if (typeof resolved.provider !== "string" || resolved.provider !== provider || typeof resolved.id !== "string" || resolved.id !== model || typeof resolved.name !== "string" || resolved.name.length === 0 || resolved.description !== void 0 && typeof resolved.description !== "string") throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
4138
- const context = resolved.context;
4139
- if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
4140
- const inputModalities = this.detachedModalities(resolved.inputModalities);
4141
- const systemPromptUpdate = resolved.systemPromptUpdate;
4142
- if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
4143
- const defaultMaxTokens = resolved.defaultMaxTokens;
4144
- if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
4145
- const info = {
4146
- provider,
4147
- id: model,
4148
- name: resolved.name,
4149
- ...resolved.description === void 0 ? {} : { description: resolved.description },
4150
- ...inputModalities === void 0 ? {} : { inputModalities },
4151
- ...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
4152
- ...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
4153
- ...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate }
4154
- };
4155
- const reasoning = resolved.reasoning;
4156
- if (reasoning === void 0) return info;
4157
- if (reasoning.efforts.length === 0) throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
4158
- const seen = /* @__PURE__ */ new Set();
4159
- const efforts = reasoning.efforts.map((effort) => {
4160
- if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) throw new LlmError(`adapter returned invalid or duplicate reasoning effort metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
4161
- seen.add(effort.id);
4162
- return {
4163
- id: effort.id,
4164
- name: effort.name,
4165
- ...effort.description === void 0 ? {} : { description: effort.description }
4166
- };
4167
- });
4168
- if (reasoning.defaultEffort !== void 0 && !seen.has(reasoning.defaultEffort)) throw new LlmError(`adapter returned an unknown default reasoning effort for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
4169
- return {
4170
- ...info,
4171
- reasoning: {
4172
- efforts,
4173
- ...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
4174
- }
4175
- };
4176
- }
4177
- /**
4178
- * Validate a conversation call config against its exact model capability and
4179
- * materialize adapter-configured defaults. Unsupported explicit efforts
4180
- * reject before provider I/O; no clamping or aliasing is performed. This
4181
- * standalone query does not bind a later dispatch; use {@link prepareCall}
4182
- * when logging and streaming must share one adapter registration.
4183
- * @param config - provider/model route and optional request controls.
4184
- * @param signal - optional cancellation for adapter-owned capability lookup.
4185
- * @returns a detached config only when a default must be materialized.
4186
- */
4187
- async resolveCallConfig(config, signal) {
4188
- return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
4189
- }
4190
- async resolveCallFor(registration, config, signal) {
4191
- const info = await this.resolveModelInfoFor(registration, config.model, signal);
4192
- return this.resolveCallWithInfo(config, info);
4193
- }
4194
- /** Validate request controls against one already-bound exact model result. */
4195
- resolveCallWithInfo(config, info) {
4196
- const defaulted = config.maxTokens === void 0 && info.defaultMaxTokens !== void 0 ? {
4197
- ...config,
4198
- maxTokens: info.defaultMaxTokens
4199
- } : config;
4200
- const reasoning = info.reasoning;
4201
- const requested = defaulted.reasoningEffort;
4202
- let resolvedConfig = defaulted;
4203
- if (reasoning === void 0) {
4204
- if (requested !== void 0) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, "UNSUPPORTED_REASONING_EFFORT");
4205
- } else {
4206
- const effective = requested ?? reasoning.defaultEffort;
4207
- if (effective !== void 0) {
4208
- if (!reasoning.efforts.some((effort) => effort.id === effective)) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`, "UNSUPPORTED_REASONING_EFFORT");
4209
- if (requested !== effective) resolvedConfig = {
4210
- ...defaulted,
4211
- reasoningEffort: effective
4212
- };
4213
- }
4214
- }
4215
- return {
4216
- config: resolvedConfig,
4217
- ...info.context === void 0 ? {} : { context: info.context },
4218
- modelInfo: info
4219
- };
4220
- }
4221
- /**
4222
- * Resolve one call under its current adapter registration. The returned
4223
- * one-shot handle keeps that registration across header logging and dispatch,
4224
- * so HMR cannot combine one adapter's capability result with another adapter.
4225
- * @param config - provider/model route and optional request controls.
4226
- * @param signal - optional cancellation for adapter-owned capability lookup.
4227
- * @returns a prepared config and its registration-bound stream entry point.
4228
- */
4229
- async prepareCall(config, signal) {
4230
- const registration = this.registration(config.provider);
4231
- const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
4232
- const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
4233
- const resolved = this.resolveCallWithInfo(config, modelInfo);
4234
- const resolvedConfig = deepFreeze(structuredClone(resolved.config));
4235
- const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
4236
- const adapterDefaults = deepFreeze({
4237
- ...config.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
4238
- ...config.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
4239
- });
4240
- let dispatched = false;
4241
- return Object.freeze({
4242
- config: resolvedConfig,
4243
- retryPolicy: registration.retryPolicy,
4244
- adapterDefaults,
4245
- ...context === void 0 ? {} : { context },
4246
- ...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
4247
- ...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
4248
- stream: (options) => {
4249
- if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
4250
- if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
4251
- dispatched = true;
4252
- return this.streamWithRegistration(options, {
4253
- registration,
4254
- config: resolvedConfig,
4255
- modelInfo,
4256
- dispatch: (options) => adapterCall.stream(options)
4257
- });
4258
- }
4259
- });
4260
- }
4261
- registration(provider) {
4262
- const registration = this.adapters.get(provider);
4263
- if (!registration) throw new LlmError(`no adapter registered for provider "${provider}"`, "NO_ADAPTER");
4264
- return registration;
4265
- }
4266
- /** Remove replay state whose historical route is owned by another adapter. */
4267
- forAdapter(options, adapter) {
4268
- const messages = options.messages.map((message) => {
4269
- if (message.role !== "assistant") return message;
4270
- const source = message.source;
4271
- if (source.replayState === void 0) return message;
4272
- if (this.adapters.get(source.provider)?.adapter === adapter) return message;
4273
- return freezeMessage({
4274
- ...message,
4275
- source: {
4276
- kind: "model",
4277
- provider: source.provider,
4278
- model: source.model
4279
- }
4280
- });
4281
- });
4282
- if (messages.every((message, index) => message === options.messages[index])) return options;
4283
- const filtered = {
4284
- ...options,
4285
- messages
4286
- };
4287
- return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
4288
- }
4289
- /**
4290
- * Resolve the current execution-world read path of one durable file
4291
- * reference through the mounted attachment and filesystem providers.
4292
- */
4293
- fileReadPath(ref) {
4294
- let hostPath;
4295
- try {
4296
- hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
4297
- } catch {
4298
- return;
4299
- }
4300
- if (hostPath === void 0) return void 0;
4301
- return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
4302
- }
4303
- /**
4304
- * Final adapter boundary. Adapter selection, dispatch, iterator construction,
4305
- * and iteration failures become one terminal failure chunk. Middleware and
4306
- * downstream consumer failures remain thrown plugin or consumer errors.
4307
- */
4308
- async *adapterStream(options, prepared) {
4309
- let iterator;
4310
- try {
4311
- const registration = prepared?.registration ?? this.registration(options.provider);
4312
- const adapter = registration.adapter;
4313
- let modelInfo;
4314
- let resolvedConfig;
4315
- let dispatch;
4316
- if (prepared === void 0) {
4317
- const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
4318
- modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
4319
- resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
4320
- dispatch = (options) => adapterCall.stream(options);
4321
- } else {
4322
- modelInfo = prepared.modelInfo;
4323
- resolvedConfig = prepared.config;
4324
- dispatch = prepared.dispatch;
4325
- }
4326
- if (prepared !== void 0 && !callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
4327
- const resolvedOptions = callConfigEquals(options, resolvedConfig) ? options : Object.isFrozen(options) ? deepFreeze({
4328
- ...options,
4329
- ...resolvedConfig
4330
- }) : {
4331
- ...options,
4332
- ...resolvedConfig
4333
- };
4334
- let projectedMessages = resolvedOptions.messages;
4335
- if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
4336
- if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
4337
- const projectedOptions = projectedMessages === resolvedOptions.messages ? resolvedOptions : Object.isFrozen(resolvedOptions) ? deepFreeze({
4338
- ...resolvedOptions,
4339
- messages: projectedMessages
4340
- }) : {
4341
- ...resolvedOptions,
4342
- messages: projectedMessages
4343
- };
4344
- iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
4345
- } catch (error) {
4346
- yield adapterFailureChunk(error, options.signal);
4347
- return;
4348
- }
4349
- let completed = false;
4350
- try {
4351
- while (true) {
4352
- let item;
4353
- try {
4354
- const next = await iterator.next();
4355
- item = next.done ? { done: true } : {
4356
- done: false,
4357
- value: next.value
4358
- };
4359
- } catch (error) {
4360
- completed = true;
4361
- yield adapterFailureChunk(error, options.signal);
4362
- return;
4363
- }
4364
- if (item.done) {
4365
- completed = true;
4366
- return;
4367
- }
4368
- yield item.value;
4369
- }
4370
- } finally {
4371
- if (!completed) {
4372
- const close = iterator.return?.bind(iterator);
4373
- if (close) await close();
4374
- }
4375
- }
4376
- }
4377
- /**
4378
- * Stream one model call as raw chunks (token-level deltas). Replay state is
4379
- * retained only when the same adapter instance owns its historical provider
4380
- * and the target provider. Final adapter selection remains fixed through
4381
- * asynchronous exact-model resolution and dispatch. Adapter selection,
4382
- * dispatch, and iteration failures become terminal `error` or `aborted`
4383
- * finish chunks; middleware, nested-call, cleanup, and consumer failures
4384
- * remain thrown.
4385
- * @param options - the full request; `options.provider` selects the adapter.
4386
- * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
4387
- */
4388
- stream(options) {
4389
- return this.streamWithRegistration(options);
4390
- }
4391
- streamWithRegistration(options, prepared) {
4392
- return this.ctx.waterfall(this, "llm/stream", options, () => this.adapterStream(options, prepared));
4393
- }
4394
- };
4395
- })();
4396
- /** Convert one adapter throw into the stream protocol's terminal outcome. */
4397
- function adapterFailureChunk(error, signal) {
4398
- const failure = normalizeLlmFailure(error);
4399
- return {
4400
- type: "finish",
4401
- reason: signal?.aborted || failure.code === "ABORTED" ? {
4402
- kind: "aborted",
4403
- failure
4404
- } : {
4405
- kind: "error",
4406
- failure
4407
- }
4408
- };
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 预设和权限;支持 Host 本地时区的 5 段 cron,错过的触发点不补跑;可选且默认关闭的空闲系统睡眠保护允许屏幕熄灭,但不承诺拦截合盖、手动睡眠、休眠、关机或唤醒已睡眠机器。执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。若你同时用 todo_write 维护会话顶部的可见计划列表,最终回复前必须再次调用 todo_write 收尾:没有剩余工作时不要保留 in_progress,已完成的最后一步要标为 completed。";
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 three
4991
- * fields the browser card edits are schema-volatile, so the Loader commits an
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 };