dsh-deeppilot 0.9.14 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -1140,6 +1140,42 @@ interface PushOutlet {
1140
1140
  */
1141
1141
  isAvailable(): boolean;
1142
1142
  }
1143
+ interface QuietCompletionResult {
1144
+ suppressed: boolean;
1145
+ note?: string;
1146
+ }
1147
+ interface TurnVerdict {
1148
+ /** 本轮最近一条驱动消息是计划任务投递(且其后没有用户消息)。 */
1149
+ scheduled: boolean;
1150
+ /** 本轮 `turn/start` 的轮次号;日志里读不到时为 undefined。 */
1151
+ turn?: number;
1152
+ }
1153
+ declare class QuietCompletionTracker {
1154
+ /**
1155
+ * 判定过计划任务轮次的会话。粘性集合:加入后不再移除,工具不按轮出现或消失。
1156
+ * 宿主每次工具列表变化都会往会话追加一条工具增减消息并让提示词缓存失效,
1157
+ * 按轮开关会让同一会话反复产生这类消息。
1158
+ */
1159
+ private readonly scheduleSessions;
1160
+ private readonly quiet;
1161
+ /** 启动时用宿主任务列表里的投递目标会话预热,避免重启后工具先消失再出现。 */
1162
+ seed(sessionIds: Iterable<string>): void;
1163
+ isScheduleSession(sessionId: string): boolean;
1164
+ /** 加入粘性集合;返回是否为新加入(调用方据此记一次日志)。 */
1165
+ markScheduleSession(sessionId: string): boolean;
1166
+ /** 工具执行:只在计划任务轮次内生效。 */
1167
+ requestQuiet(sessionId: string | undefined, verdict: TurnVerdict, seq: number): QuietCompletionResult;
1168
+ /**
1169
+ * 桥接层观察到 user/message:用户在请求静默之后插话(日志位置更晚)就撤销静默,
1170
+ * 本轮照常提醒。计划任务与注入上下文不影响。
1171
+ */
1172
+ noteUserMessage(sessionId: string, sourceKind: string | undefined, seq: number | undefined): void;
1173
+ /**
1174
+ * turn.end 时调用。返回 true 表示本轮完成通知应被抑制:成功结束、且本轮请求过静默。
1175
+ * 轮次号能对上时按轮次号判定;迟到的上一轮 turn.end 保留本轮的请求。
1176
+ */
1177
+ consumeTurnEnd(sessionId: string, ok: boolean, turn: number | undefined, seq: number | undefined): boolean;
1178
+ }
1143
1179
  interface PromptDocument {
1144
1180
  name: string;
1145
1181
  mediaType: string;
@@ -1191,6 +1227,8 @@ export declare class HostBridge {
1191
1227
  * and restore them. Excluded from `summaries` and from the live broadcast. */
1192
1228
  private archivedSummaries;
1193
1229
  private subagentSessionIds;
1230
+ /** 计划任务按需完成通知(quiet-completion.ts):index.ts 用它注册 Agent 工具。 */
1231
+ readonly quietCompletion: QuietCompletionTracker;
1194
1232
  /**
1195
1233
  * 每个会话最近一次已知的队列(inbox 投影的折叠结果)及其指纹。来源是实时
1196
1234
  * 投影通知或打开会话时读到的基线;指纹用来丢掉内容没变的重复通知。
@@ -1274,6 +1312,13 @@ export declare class HostBridge {
1274
1312
  private record;
1275
1313
  /** Start consuming host + mux streams. Idempotent; aborts on dispose(). */
1276
1314
  start(): void;
1315
+ /**
1316
+ * 用宿主任务列表里的投递目标会话预热计划任务会话集合,避免插件重启后工具
1317
+ * 先消失、等下一次投递再出现(每次变化都会给会话追加工具增减消息)。
1318
+ * `new` 目标没有固定会话,只能从最近一次投递回执里取。失败只记日志:集合仍会
1319
+ * 在下一次投递时补上。由 index.ts 在 schedule 服务就绪、注册工具时调用。
1320
+ */
1321
+ seedQuietCompletionSessions(): Promise<void>;
1277
1322
  dispose(): void;
1278
1323
  private runHostStream;
1279
1324
  private runMuxStream;
package/lib/index.js CHANGED
@@ -485,7 +485,7 @@ function defaultLevel(kind) {
485
485
  function sanitizeText(value, maxChars) {
486
486
  return value.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, maxChars);
487
487
  }
488
- function normalizeReason(reason) {
488
+ function normalizeReason$1(reason) {
489
489
  return sanitizeText(reason, MAX_REASON_CHARS).replace(/\s+/g, "-");
490
490
  }
491
491
  /**
@@ -566,7 +566,7 @@ var DiagnosticsStore = class {
566
566
  const event = {
567
567
  ts,
568
568
  kind,
569
- reason: normalizeReason(reason),
569
+ reason: normalizeReason$1(reason),
570
570
  level: extra.level ?? defaultLevel(kind),
571
571
  ...attemptId !== void 0 ? { attemptId } : {},
572
572
  ...source !== void 0 ? { source } : {},
@@ -1991,6 +1991,12 @@ function userMessageSource(data) {
1991
1991
  if (obj.source && typeof obj.source === "object") return obj.source;
1992
1992
  if (obj.message && typeof obj.message === "object" && obj.message.source && typeof obj.message.source === "object") return obj.message.source;
1993
1993
  }
1994
+ /** Source kind of one user/message payload (`user`, `schedule`, injected
1995
+ * context kinds, …); undefined on legacy hosts that record no source. */
1996
+ function userMessageSourceKind(data) {
1997
+ const kind = userMessageSource(data)?.kind;
1998
+ return typeof kind === "string" ? kind : void 0;
1999
+ }
1994
2000
  /** Wire role for one user/message payload. A payload without any readable
1995
2001
  * source degrades to 'user' so history written by older hosts stays visible;
1996
2002
  * a present source follows the host's own trajectory rule — anything whose
@@ -6905,6 +6911,230 @@ function unwrapStreamItem(item) {
6905
6911
  } : nested;
6906
6912
  return item;
6907
6913
  }
6914
+ // src/quiet-completion.ts
6915
+ /**
6916
+ * 计划任务按需完成通知(#35)。
6917
+ *
6918
+ * heartbeat 式计划任务大多数轮次"一切正常",每轮都推「任务完成」是噪音。插件
6919
+ * 给计划任务会话提供一个 Agent 工具:Agent 按任务提示词里的规则判断本轮不需要
6920
+ * 提醒时调用它,本轮成功结束就不发完成通知(不写 s2c.notify、不推 APNs)。
6921
+ * 失败、审批、提问照常通知;模型忘了调用时照常提醒,宁可多一次打扰也不漏报。
6922
+ *
6923
+ * 职责划分:
6924
+ * - scheduledTurnOf:同步读宿主会话日志,判定当前这一轮是否由计划任务投递驱动。
6925
+ * 工具的 `when` 与 `execute` 都在宿主进程内同步调用它,不依赖桥接层异步收到的
6926
+ * 事件(实测 `when` 求值时本轮 user/message 尚未落盘,桥接层也还没收到)。
6927
+ * - QuietCompletionTracker:计划任务会话的粘性集合,以及每个会话"本轮已请求静默"
6928
+ * 的记录;HostBridge 在 user/message 与 turn.end 处调用它。
6929
+ * - quietCompletionTool:面向模型的工具定义,由 index.ts 注册到宿主 `ctx.tools`。
6930
+ *
6931
+ * 只在宿主级计划任务(`schedulesHost`,DSH 0.2.2 起)上注册:0.2.1 的
6932
+ * `tools.register` 不支持 `when`,多余参数会被静默忽略,工具会变成全局可见。
6933
+ */
6934
+ const QUIET_COMPLETION_TOOL_NAME = "deeppilot_skip_completion_notice";
6935
+ /** 往回扫描会话日志的上限:一轮内的事件远少于此,防止异常长轮次拖慢步骤准备。 */
6936
+ const MAX_TURN_SCAN_EVENTS = 2e3;
6937
+ function sessionLogOf(agent) {
6938
+ const session = agent?.session;
6939
+ if (!session || typeof session.id !== "string" || typeof session.seq !== "number" || typeof session.eventAt !== "function") return;
6940
+ return session;
6941
+ }
6942
+ function sourceKindOf(value) {
6943
+ const kind = value?.source?.kind;
6944
+ return typeof kind === "string" ? kind : void 0;
6945
+ }
6946
+ /** 一次 inbox 拼接里插入的条目(按插入顺序)及其目标队列。 */
6947
+ function insertedOf(event) {
6948
+ if (event.type !== "agent/inbox/spliced") return void 0;
6949
+ const data = event.data;
6950
+ const items = Array.isArray(data?.inserted) ? data.inserted : [];
6951
+ return items.length > 0 ? {
6952
+ target: data?.target,
6953
+ items
6954
+ } : void 0;
6955
+ }
6956
+ /**
6957
+ * 判定当前这一轮是否由计划任务投递驱动(同步读宿主会话日志,DSH 0.2.2-rc.1 实测形状)。
6958
+ *
6959
+ * 宿主把待处理消息先拼进 inbox(`agent/inbox/spliced`,带条目本体与目标队列),
6960
+ * `turn/start` 之后的拼接只是取走(`removedCount`,不带内容),条目随后才落成
6961
+ * user/message。因此按 inbox 插入判定:
6962
+ * - 本轮开始之后插入 `next-step` 的用户条目 = 用户中途插话,不算;
6963
+ * - 本轮开始时取走的是上一轮开始之后插入 `next-turn` 的条目;其中最后插入的一条
6964
+ * 来源为 `schedule` 才算(计划任务投递);来源为 `user` 不算;
6965
+ * - 同为 `schedule` 来源的任务变更提示插入的是 `next-step`(宿主注明"不是投递"),不参与判定;
6966
+ * - 扫不到本轮 `turn/start` 时按普通轮次处理,宁可照常提醒。
6967
+ */
6968
+ function scheduledTurnOf(session) {
6969
+ const floor = Math.max(0, session.seq - MAX_TURN_SCAN_EVENTS);
6970
+ let turn;
6971
+ let seenTurnStart = false;
6972
+ for (let seq = session.seq - 1; seq >= floor; seq--) {
6973
+ const event = session.eventAt(seq);
6974
+ if (!event) continue;
6975
+ if (event.type === "turn/start") {
6976
+ if (seenTurnStart) break;
6977
+ seenTurnStart = true;
6978
+ const value = event.data?.turn;
6979
+ if (typeof value === "number") turn = value;
6980
+ continue;
6981
+ }
6982
+ const splice = insertedOf(event);
6983
+ if (splice === void 0) continue;
6984
+ if (!seenTurnStart) {
6985
+ if (splice.target === "next-step" && splice.items.some((item) => sourceKindOf(item) === "user")) return {
6986
+ scheduled: false,
6987
+ ...turn !== void 0 ? { turn } : {}
6988
+ };
6989
+ continue;
6990
+ }
6991
+ if (splice.target !== "next-turn") continue;
6992
+ for (let index = splice.items.length - 1; index >= 0; index--) {
6993
+ const kind = sourceKindOf(splice.items[index]);
6994
+ if (kind === "user") return {
6995
+ scheduled: false,
6996
+ ...turn !== void 0 ? { turn } : {}
6997
+ };
6998
+ if (kind === "schedule") return {
6999
+ scheduled: true,
7000
+ ...turn !== void 0 ? { turn } : {}
7001
+ };
7002
+ }
7003
+ }
7004
+ return {
7005
+ scheduled: false,
7006
+ ...turn !== void 0 ? { turn } : {}
7007
+ };
7008
+ }
7009
+ var QuietCompletionTracker = class {
7010
+ /**
7011
+ * 判定过计划任务轮次的会话。粘性集合:加入后不再移除,工具不按轮出现或消失。
7012
+ * 宿主每次工具列表变化都会往会话追加一条工具增减消息并让提示词缓存失效,
7013
+ * 按轮开关会让同一会话反复产生这类消息。
7014
+ */
7015
+ scheduleSessions = /* @__PURE__ */ new Set();
7016
+ quiet = /* @__PURE__ */ new Map();
7017
+ /** 启动时用宿主任务列表里的投递目标会话预热,避免重启后工具先消失再出现。 */
7018
+ seed(sessionIds) {
7019
+ for (const sessionId of sessionIds) if (sessionId) this.scheduleSessions.add(sessionId);
7020
+ }
7021
+ isScheduleSession(sessionId) {
7022
+ return this.scheduleSessions.has(sessionId);
7023
+ }
7024
+ /** 加入粘性集合;返回是否为新加入(调用方据此记一次日志)。 */
7025
+ markScheduleSession(sessionId) {
7026
+ if (this.scheduleSessions.has(sessionId)) return false;
7027
+ this.scheduleSessions.add(sessionId);
7028
+ return true;
7029
+ }
7030
+ /** 工具执行:只在计划任务轮次内生效。 */
7031
+ requestQuiet(sessionId, verdict, seq) {
7032
+ if (sessionId === void 0 || !verdict.scheduled) return {
7033
+ suppressed: false,
7034
+ note: "This run was not started by a scheduled task; the completion notification will be sent as usual."
7035
+ };
7036
+ this.quiet.set(sessionId, {
7037
+ ...verdict.turn !== void 0 ? { turn: verdict.turn } : {},
7038
+ seq
7039
+ });
7040
+ return { suppressed: true };
7041
+ }
7042
+ /**
7043
+ * 桥接层观察到 user/message:用户在请求静默之后插话(日志位置更晚)就撤销静默,
7044
+ * 本轮照常提醒。计划任务与注入上下文不影响。
7045
+ */
7046
+ noteUserMessage(sessionId, sourceKind, seq) {
7047
+ const request = this.quiet.get(sessionId);
7048
+ if (request === void 0 || sourceKind !== "user") return;
7049
+ if (seq === void 0 || seq > request.seq) this.quiet.delete(sessionId);
7050
+ }
7051
+ /**
7052
+ * turn.end 时调用。返回 true 表示本轮完成通知应被抑制:成功结束、且本轮请求过静默。
7053
+ * 轮次号能对上时按轮次号判定;迟到的上一轮 turn.end 保留本轮的请求。
7054
+ */
7055
+ consumeTurnEnd(sessionId, ok, turn, seq) {
7056
+ const request = this.quiet.get(sessionId);
7057
+ if (request === void 0) return false;
7058
+ if (turn !== void 0 && request.turn !== void 0) {
7059
+ if (turn < request.turn) return false;
7060
+ this.quiet.delete(sessionId);
7061
+ return ok && turn === request.turn;
7062
+ }
7063
+ if (seq !== void 0 && seq < request.seq) return false;
7064
+ this.quiet.delete(sessionId);
7065
+ return ok;
7066
+ }
7067
+ };
7068
+ const DESCRIPTION = [
7069
+ "Call this during a scheduled run only when the task prompt says the user does not need to be notified",
7070
+ "for results like this one. It suppresses only this run's \"task completed\" notification; approvals,",
7071
+ "questions and errors still notify. Do not call it if anything needs the user's attention, or if the",
7072
+ "prompt gives no such rule."
7073
+ ].join(" ");
7074
+ /** reason 归一化:非字符串按空串处理,去首尾空白并截断。 */
7075
+ function normalizeReason(raw) {
7076
+ const text = typeof raw === "string" ? raw.trim() : "";
7077
+ return text.length > 200 ? text.slice(0, 200) : text;
7078
+ }
7079
+ /**
7080
+ * 面向模型的工具定义(宿主 ToolDefinition 原始形状:JSON Schema 参数、输出 schema
7081
+ * 与 render、execute)。`log` 只记会话 ID、结果与 reason 长度:reason 由模型撰写,
7082
+ * 可能引用结果内容,按插件日志约定不打正文。
7083
+ */
7084
+ function quietCompletionTool(tracker, log = () => {}) {
7085
+ return {
7086
+ name: QUIET_COMPLETION_TOOL_NAME,
7087
+ description: DESCRIPTION,
7088
+ parameters: {
7089
+ type: "object",
7090
+ additionalProperties: false,
7091
+ required: ["reason"],
7092
+ properties: { reason: {
7093
+ type: "string",
7094
+ description: `Why this run does not need the user's attention (at most 200 characters).`
7095
+ } }
7096
+ },
7097
+ output: {
7098
+ schema: {
7099
+ type: "object",
7100
+ additionalProperties: false,
7101
+ required: ["suppressed"],
7102
+ properties: {
7103
+ suppressed: { type: "boolean" },
7104
+ note: { type: "string" }
7105
+ }
7106
+ },
7107
+ render(_args, value) {
7108
+ return [{
7109
+ type: "text",
7110
+ text: value.suppressed ? "The completion notification for this run will not be sent." : value.note ?? "The completion notification will be sent as usual."
7111
+ }];
7112
+ }
7113
+ },
7114
+ async execute(args, exec) {
7115
+ const session = sessionLogOf(exec.agent);
7116
+ const reason = normalizeReason(args?.reason);
7117
+ const result = session === void 0 ? tracker.requestQuiet(void 0, { scheduled: false }, 0) : tracker.requestQuiet(session.id, scheduledTurnOf(session), session.seq);
7118
+ log(`quiet completion requested session=${session?.id ?? "?"} suppressed=${result.suppressed} reasonChars=${reason.length}`);
7119
+ return result;
7120
+ }
7121
+ };
7122
+ }
7123
+ /**
7124
+ * 注册工具。`when`:粘性集合内的会话直接放行;否则同步判定本轮是否计划任务轮次,
7125
+ * 是则加入集合(首轮即可见,`new` 目标每次新建的会话同样可见)。普通会话看不到。
7126
+ * 返回宿主给的注销函数。
7127
+ */
7128
+ function registerQuietCompletionTool(tools, tracker, log = () => {}) {
7129
+ return tools.register(quietCompletionTool(tracker, log), { when: (context) => {
7130
+ const session = sessionLogOf(context.agent);
7131
+ if (session === void 0) return false;
7132
+ if (tracker.isScheduleSession(session.id)) return true;
7133
+ if (!scheduledTurnOf(session).scheduled) return false;
7134
+ if (tracker.markScheduleSession(session.id)) log(`quiet completion: session joined scheduled set session=${session.id}`);
7135
+ return true;
7136
+ } });
7137
+ }
6908
7138
  // src/host-capabilities.ts
6909
7139
  const pluginUpdateProbes = /* @__PURE__ */ new WeakMap();
6910
7140
  function setPluginUpdateProbe(proxy, probe) {
@@ -7233,6 +7463,8 @@ var HostBridge = class {
7233
7463
  * and restore them. Excluded from `summaries` and from the live broadcast. */
7234
7464
  archivedSummaries = /* @__PURE__ */ new Map();
7235
7465
  subagentSessionIds = /* @__PURE__ */ new Set();
7466
+ /** 计划任务按需完成通知(quiet-completion.ts):index.ts 用它注册 Agent 工具。 */
7467
+ quietCompletion = new QuietCompletionTracker();
7236
7468
  /**
7237
7469
  * 每个会话最近一次已知的队列(inbox 投影的折叠结果)及其指纹。来源是实时
7238
7470
  * 投影通知或打开会话时读到的基线;指纹用来丢掉内容没变的重复通知。
@@ -7480,6 +7712,35 @@ var HostBridge = class {
7480
7712
  this.runMuxStream();
7481
7713
  this.refreshSummaries();
7482
7714
  }
7715
+ /**
7716
+ * 用宿主任务列表里的投递目标会话预热计划任务会话集合,避免插件重启后工具
7717
+ * 先消失、等下一次投递再出现(每次变化都会给会话追加工具增减消息)。
7718
+ * `new` 目标没有固定会话,只能从最近一次投递回执里取。失败只记日志:集合仍会
7719
+ * 在下一次投递时补上。由 index.ts 在 schedule 服务就绪、注册工具时调用。
7720
+ */
7721
+ async seedQuietCompletionSessions() {
7722
+ if (this.hostSchedule === void 0) return;
7723
+ const sessionIds = [];
7724
+ let offset = 0;
7725
+ for (;;) {
7726
+ const result = await this.listHostSchedules({
7727
+ limit: HOST_SCHEDULE_PAGE_SIZE,
7728
+ offset
7729
+ });
7730
+ if (!result.ok) {
7731
+ this.log?.(`quiet completion seed skipped: ${result.code}`);
7732
+ return;
7733
+ }
7734
+ for (const task of result.value.tasks) {
7735
+ if (task.target.kind !== "new") sessionIds.push(task.target.sessionId);
7736
+ if (task.lastDelivery) sessionIds.push(task.lastDelivery.targetSessionId);
7737
+ }
7738
+ offset += result.value.tasks.length;
7739
+ if (result.value.tasks.length === 0 || offset >= result.value.total) break;
7740
+ }
7741
+ if (this.disposed) return;
7742
+ this.quietCompletion.seed(sessionIds);
7743
+ }
7483
7744
  dispose() {
7484
7745
  if (this.disposed) return;
7485
7746
  this.disposed = true;
@@ -7570,6 +7831,7 @@ var HostBridge = class {
7570
7831
  if (event.type === "turn/end") this.assistantStream.abandon(sessionId);
7571
7832
  if (event.type === "agent-preset/selected") this.record("s2c.skills.changed", { sessionId });
7572
7833
  this.noteActivity(sessionId, event);
7834
+ if (event.type === "user/message") this.quietCompletion.noteUserMessage(sessionId, userMessageSourceKind(event.data), event.seq);
7573
7835
  this.captureAssistantText(sessionId, event);
7574
7836
  const projection = projectEvent(sessionId, event);
7575
7837
  this.captureActivity(sessionId, event, projection?.data.tool);
@@ -7582,7 +7844,12 @@ var HostBridge = class {
7582
7844
  ...streamId && projection.kind === "message.final" ? { streamId } : {}
7583
7845
  }
7584
7846
  });
7585
- if (projection?.kind === "turn.end") this.emitTurnCompletedNotify(sessionId, projection.data.ok === true);
7847
+ if (projection?.kind === "turn.end") {
7848
+ const ok = projection.data.ok === true;
7849
+ const turn = event.data?.turn;
7850
+ if (this.quietCompletion.consumeTurnEnd(sessionId, ok, typeof turn === "number" ? turn : void 0, event.seq)) this.log?.(`completion notice skipped by scheduled run session=${sessionId}`);
7851
+ else this.emitTurnCompletedNotify(sessionId, ok);
7852
+ }
7586
7853
  break;
7587
7854
  }
7588
7855
  case "session/projection": {
@@ -12634,6 +12901,20 @@ function apply(ctx, options) {
12634
12901
  if (state.bridge === bridge) state.bridge = void 0;
12635
12902
  bridge.dispose();
12636
12903
  }, "deeppilot: host streams");
12904
+ apiCtx.inject(["tools", "schedule"], (sub) => {
12905
+ if (bridge.capabilities.schedulesHost !== true) {
12906
+ log("quiet completion tool not registered: host-level schedules unavailable");
12907
+ return;
12908
+ }
12909
+ const tools = sub.tools;
12910
+ if (typeof tools?.register !== "function") {
12911
+ log("quiet completion tool not registered: tools service unavailable", "warn");
12912
+ return;
12913
+ }
12914
+ sub.effect(() => registerQuietCompletionTool(tools, bridge.quietCompletion, (message) => log(message)), "deeppilot: quiet completion tool");
12915
+ log("quiet completion tool registered");
12916
+ bridge.seedQuietCompletionSessions();
12917
+ });
12637
12918
  });
12638
12919
  ctx.effect(() => async () => {
12639
12920
  closeAllConnections();