@tencent-ai/agent-harness 0.2.0-beta.3 → 0.2.0-beta.4

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 (62) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/cli/dist-server/shared.cjs +267 -210
  3. package/cli/dist-server/types/node/acp/acp-protocol.d.ts +8 -0
  4. package/cli/dist-server/types/node/loopback/server-protocol.d.ts +5 -0
  5. package/cli/dist-server/types/node/tools/tool-protocol.d.ts +23 -22
  6. package/cli/dist-server/types/vendor/cbc-core/node/agent/runner-factory.d.ts +18 -0
  7. package/cli/dist-server/types/vendor/cbc-core/node/context/index.d.ts +1 -0
  8. package/cli/dist-server/types/vendor/cbc-core/node/context/mods-bridge.d.ts +115 -0
  9. package/cli/dist-server/types/vendor/cbc-core/node/message-queue/followup-queue-protocol.d.ts +3 -2
  10. package/cli/dist-server/types/vendor/cbc-core/node/message-queue/followup-queue.d.ts +1 -1
  11. package/cli/dist-server/types/vendor/cbc-core/node/message-queue/steer-input-buffer-protocol.d.ts +4 -0
  12. package/cli/dist-server/types/vendor/cbc-core/node/session/can-use-tool.d.ts +4 -0
  13. package/cli/dist-server/types/vendor/cbc-core/node/session/session-manager.d.ts +30 -0
  14. package/cli/dist-server/types/vendor/cbc-core/node/session/session-protocol.d.ts +12 -0
  15. package/cli/dist-server/types/vendor/cbc-core/node/utils/input-item-utils.d.ts +1 -1
  16. package/cli/dist-server/types/workspace/agent-client-protocol/lib/common/prompt-codec.d.ts +4 -0
  17. package/cli/dist-server/types/workspace/agent-client-protocol/lib/common/types.d.ts +15 -1
  18. package/cli/dist-server/types/workspace/cbc-core/lib/node/agent/runner-factory.d.ts +18 -0
  19. package/cli/dist-server/types/workspace/cbc-core/lib/node/context/index.d.ts +1 -0
  20. package/cli/dist-server/types/workspace/cbc-core/lib/node/context/mods-bridge.d.ts +115 -0
  21. package/cli/dist-server/types/workspace/cbc-core/lib/node/message-queue/followup-queue-protocol.d.ts +3 -2
  22. package/cli/dist-server/types/workspace/cbc-core/lib/node/message-queue/followup-queue.d.ts +1 -1
  23. package/cli/dist-server/types/workspace/cbc-core/lib/node/message-queue/steer-input-buffer-protocol.d.ts +4 -0
  24. package/cli/dist-server/types/workspace/cbc-core/lib/node/session/can-use-tool.d.ts +4 -0
  25. package/cli/dist-server/types/workspace/cbc-core/lib/node/session/session-manager.d.ts +30 -0
  26. package/cli/dist-server/types/workspace/cbc-core/lib/node/session/session-protocol.d.ts +12 -0
  27. package/cli/dist-server/types/workspace/cbc-core/lib/node/utils/input-item-utils.d.ts +1 -1
  28. package/cli/dist-server/types/workspace/product/lib/common/types/features.d.ts +16 -0
  29. package/cli/dist-server/types/workspace/prompts/lib/common/product-prompt-provider.d.ts +3 -1
  30. package/cli/dist-server/types/workspace/prompts/lib/common/prompt-protocol.d.ts +6 -0
  31. package/cli/product.cloudhosted.json +1 -803
  32. package/cli/product.internal.json +1 -1061
  33. package/cli/product.ioa.json +1 -1523
  34. package/cli/product.json +1 -2217
  35. package/cli/product.selfhosted.json +1 -364
  36. package/docs/acp-v2.md +19 -0
  37. package/docs/client.md +7 -0
  38. package/docs/product.md +4 -0
  39. package/docs/protocol.md +3 -1
  40. package/lib/agent/v2.js +1 -1
  41. package/lib/application/services.js +1 -1
  42. package/lib/auth/module.d.ts +3 -1
  43. package/lib/auth/module.js +1 -1
  44. package/lib/client/client.js +1 -1
  45. package/lib/components/sandbox.d.mts +2 -0
  46. package/lib/components/sandbox.mjs +1 -0
  47. package/lib/components/session-replay.d.mts +2 -0
  48. package/lib/components/session-replay.mjs +1 -1
  49. package/lib/effect-runtime/concurrency.d.mts +2 -4
  50. package/lib/effect-runtime/concurrency.mjs +1 -1
  51. package/lib/effect-runtime/process-runtime-context.d.ts +5 -1
  52. package/lib/protocol/acp-v2-credits.d.ts +10 -0
  53. package/lib/protocol/acp-v2-credits.js +1 -0
  54. package/lib/protocol/acp-v2-private-client.d.ts +1 -10
  55. package/lib/runtime/codebuddy-auth.d.ts +5 -3
  56. package/lib/runtime/codebuddy-auth.js +1 -1
  57. package/lib/session/replay.d.ts +1 -1
  58. package/lib/session/replay.js +1 -1
  59. package/lib/session/server-events.js +1 -1
  60. package/lib/session/server-interactions.js +1 -1
  61. package/lib/subagent/history.js +1 -1
  62. package/package.json +14 -9
@@ -737,6 +737,8 @@ export interface AcpRequestTracking {
737
737
  conversationRequestId?: string;
738
738
  }
739
739
  export interface AcpPromptItem {
740
+ /** Host-validated mod receipt; never copied from client metadata. */
741
+ modsPromptSubmitted?: true;
740
742
  request: PromptRequest;
741
743
  responseDeferred: Deferred<PromptResponse>;
742
744
  tracking: AcpRequestTracking;
@@ -954,6 +956,12 @@ export interface AcpAgentManager {
954
956
  * 获取所有已注册的 AcpAgent 实例
955
957
  */
956
958
  getAll(): Map<string, AcpAgent>;
959
+ /**
960
+ * Subscribe to agent-registered events. Enables the mods paint pipe
961
+ * to push a first-frame MOD_RENDER to a newly-connected webui
962
+ * without waiting for the mod's own `$.ui.invalidate` cadence.
963
+ */
964
+ onAgentRegistered?(cb: (connectionId: string, agent: AcpAgent) => void): () => void;
957
965
  /**
958
966
  * 移除并释放指定连接的 AcpAgent 实例。重复移除同一连接是 no-op。
959
967
  * @param connectionId 连接 ID
@@ -9,6 +9,11 @@ export interface CommonRequest extends SessionOptions {
9
9
  * 字段,只给 native 会话创建时盖站立键用。ACP 请求走 `acp:true`,不在这里盖。
10
10
  */
11
11
  agent?: string;
12
+ /** Request-level debug options forwarded from CLI/serve callers. */
13
+ debug?: boolean | string;
14
+ debugToStderr?: boolean;
15
+ /** Automatically connect to a single available IDE on startup. */
16
+ ide?: boolean;
12
17
  }
13
18
  export interface ResumeRequest {
14
19
  resume: boolean | string;
@@ -261,6 +261,8 @@ export interface Tool<TParameters extends ToolInputParameters = any, Context = U
261
261
  * The name of the tool. Must be unique within the agent.
262
262
  */
263
263
  name: string;
264
+ /** Host-created mod proxies dispatch once, inside their own execute method. */
265
+ readonly dispatchesModToolCall?: boolean;
264
266
  /**
265
267
  * Stability of the model-facing definition while the process is alive.
266
268
  * Agent tools default to stable; runtime-varying definitions opt into volatile.
@@ -349,6 +351,12 @@ export interface Tool<TParameters extends ToolInputParameters = any, Context = U
349
351
  }
350
352
  export type { BackgroundTask, TerminalEvidence } from '../../vendor/cbc-core/node/index.js';
351
353
  export interface ToolManager {
354
+ /** Dynamic definitions share the registry used by get/execute. Builtins cannot be replaced. */
355
+ register(tool: Tool): Promise<void>;
356
+ unregister(name: string): Promise<void>;
357
+ listDynamicToolNames(): string[];
358
+ /** Agent tool snapshots must be rebuilt when this version changes. */
359
+ readonly registryVersion: number;
352
360
  get<TParameters extends ToolInputParameters = any, Context = UnknownContext>(name: string, agentConfig?: ProductAgent, cachedProductConfig?: {
353
361
  agents?: any[];
354
362
  models?: any[];
@@ -940,26 +948,15 @@ export interface VideoService {
940
948
  }
941
949
  export declare const AudioTranscribeService: unique symbol;
942
950
  /**
943
- * 录音文件识别(长音频转文本)请求参数。
944
- * `channel_num:1` 由 service 固定注入,不暴露给调用方。
951
+ * tool 层已判定好的音频来源。
952
+ * 本地文件需要先上传 COS 才有公网可下载地址;调用方给出的 URL 直接提交。
945
953
  */
946
- export interface AudioTranscribeOptions {
947
- /** 说话人分离:0 关闭(默认)/ 1 开启(需 channel_num=1) */
948
- speaker_diarization?: 0 | 1;
949
- /** 临时热词表 "词|权重,..."(最多 128) */
950
- hotword_list?: string;
951
- /** 阿拉伯数字智能转换:0 / 1 / 3 */
952
- convert_num_mode?: 0 | 1 | 3;
953
- /** 脏词过滤:0 / 1 / 2 */
954
- filter_dirty?: 0 | 1 | 2;
955
- /** 标点过滤:0 / 1 / 2 */
956
- filter_punc?: 0 | 1 | 2;
957
- /** 语气词过滤:0 / 1 / 2 */
958
- filter_modal?: 0 | 1 | 2;
959
- /** 上游附加参数(合法 JSON 字符串),如 '{"Domain":3}' */
960
- extra?: string;
961
- }
962
- /** service 内部解析出的音频来源(COS-only:始终为 audio_url)。 */
954
+ export type AudioTranscribeSource = {
955
+ readonly filePath: string;
956
+ } | {
957
+ readonly audioUrl: string;
958
+ };
959
+ /** 提交给上游的音频来源(本服务始终用 audio_url,从不用 audio_data)。 */
963
960
  export interface AudioSource {
964
961
  audio_url: string;
965
962
  }
@@ -1036,11 +1033,15 @@ export interface AudioTranscribeResult {
1036
1033
  }
1037
1034
  /**
1038
1035
  * 录音文件识别服务接口。
1039
- * COS-only:本地文件先经后端预签名上传 COS,再以 audio_url 提交异步转写并轮询。
1036
+ * 本地文件先经后端预签名上传 COS 换取 download_url;调用方给的 URL 直接用。
1037
+ * 两种来源都以 audio_url 提交异步转写并轮询。
1038
+ *
1039
+ * 识别参数(`channel_num` / `speaker_diarization`)由 service 固定注入,
1040
+ * 按 #128310 不向 Agent 暴露开关。
1040
1041
  */
1041
1042
  export interface AudioTranscribeService {
1042
1043
  /**
1043
- * 转写本地音频文件:预签名上传 → 提交任务 → 轮询直至终态。
1044
+ * 转写音频:解析来源(本地文件则预签名上传)→ 提交任务 → 轮询直至终态。
1044
1045
  */
1045
- transcribe(filePath: string, options: AudioTranscribeOptions, context: RunContext<Session>): Promise<AudioTranscribeResult>;
1046
+ transcribe(source: AudioTranscribeSource, context: RunContext<Session>): Promise<AudioTranscribeResult>;
1046
1047
  }
@@ -21,6 +21,14 @@ export interface ModelInputMemoryPressure {
21
21
  headroom: number;
22
22
  inputItems: number;
23
23
  estimatedInputBytes?: number;
24
+ /**
25
+ * Whether the retained-size walk finished (fully traversed, or provably ≥ the byte budget).
26
+ * `false` means the walk stopped on the node cap with bytes still under budget — the graph is
27
+ * deep/wide but not necessarily large, so `estimatedInputBytes` must NOT drive a rejection.
28
+ */
29
+ estimateComplete: boolean;
30
+ /** Real, content-independent OOM signal: heap is already near its ceiling. */
31
+ heapCritical: boolean;
24
32
  reject: boolean;
25
33
  }
26
34
  export declare function assessModelInputMemoryPressure(input: AgentInputItem[], heapUsed?: number, heapLimit?: number): ModelInputMemoryPressure;
@@ -80,8 +88,18 @@ export declare class RunnerFactoryImpl implements RunnerFactory {
80
88
  * 否则前端的 sending 态无人清除、永久转圈(#73022)。
81
89
  */
82
90
  protected readonly acpBroadcastService?: AgentQueueOutput;
91
+ /** Sessions with a deferred emergency compaction already scheduled (see scheduleEmergencyCompact). */
92
+ private readonly emergencyCompactInFlight;
83
93
  create(config?: Partial<RunConfig>): Promise<Runner>;
84
94
  private assertModelInputCloneHeadroom;
95
+ /**
96
+ * Fire a one-shot emergency compaction for an over-large history so the user's retry can
97
+ * succeed. Deferred + de-duplicated per session; failures are swallowed because the surfaced
98
+ * error already tells the user how to recover. Mirrors checkAutoCompact's non-blocking branch
99
+ * (setImmediate keeps it off the current runner's abort signal). compactAndSummarize has its
100
+ * own session-level lock, so the in-flight set only avoids scheduling redundant callbacks.
101
+ */
102
+ private scheduleEmergencyCompact;
85
103
  /**
86
104
  * 在每次模型请求前检查是否需要自动压缩。
87
105
  * 此时所有工具都已执行完毕,可以安全地触发压缩。
@@ -1,6 +1,7 @@
1
1
  export * from './context-protocol';
2
2
  export * from './compaction-lineage';
3
3
  export * from './context-host-protocol';
4
+ export * from './mods-bridge';
4
5
  export * from './prompt-context-provider';
5
6
  export * from './prompt-renderer';
6
7
  export * from './context-compact-summarizer';
@@ -0,0 +1,115 @@
1
+ import type { AgentInputItem } from '../../../cbc-shared/node/index.js';
2
+ import type { PromptContext } from './context-protocol';
3
+ export interface ModsPromptSubmitResult {
4
+ text?: string;
5
+ drop?: string;
6
+ }
7
+ /** Host-only provenance: a mod initiated submission has already traversed the fold. */
8
+ export declare const MODS_PROMPT_SUBMITTED_KEY = "modsPromptSubmitted";
9
+ /**
10
+ * Function hooks(Claude mods)在 core 侧的**窄接口**。
11
+ *
12
+ * ## 为什么 core 不直接调 mods
13
+ *
14
+ * `cbc-core` 只允许依赖 `@genie/cbc-shared`(见仓库 CODEBUDDY.md 的单向依赖卡点,
15
+ * `package-boundaries.spec.ts` 会扫源码 import、动态 import 与依赖声明)。而 mods
16
+ * 引擎住在 `cbc-plugins`。所以 core 这一侧只声明「我会在这几个时刻问一句」,具体
17
+ * 分发由宿主实现后注入。
18
+ *
19
+ * ## 为什么形状映射也不在 core
20
+ *
21
+ * cbc 的 `PromptContext` 是一个松散字典,官方 `prompt.context` 事件是
22
+ * `{ blocks, instructionFiles }`。两者的对应关系属于**适配**,而适配是宿主的事 ——
23
+ * 让 core 认识官方 payload 等于把上游协议渗进核心包,将来上游改一次形状,core 就
24
+ * 得跟着改。因此接口按 core 自己的类型说话,桥的实现负责翻译。
25
+ *
26
+ * ## 未注入时必须是零开销
27
+ *
28
+ * function hooks 默认关闭。没有桥时每个调用点只多一次 `=== undefined` 判断,
29
+ * 不构造对象、不进 async、不改变任何既有行为。
30
+ */
31
+ export interface ModsBridge {
32
+ /**
33
+ * 上下文装配完成、尚未拼成最终 prompt 时问一句。
34
+ *
35
+ * @param context 当前这一版上下文
36
+ * @returns 可能被 mod 改写过的上下文;桥自身出问题时应原样返回入参
37
+ */
38
+ promptContext(context: PromptContext): Promise<PromptContext>;
39
+ /**
40
+ * 会话建立时通知一次。
41
+ *
42
+ * @param input 会话 cwd 与是否交互式 —— 官方 `session.start` 的入参
43
+ */
44
+ sessionStart(input: {
45
+ cwd: string;
46
+ isInteractive: boolean;
47
+ }): Promise<void>;
48
+ /**
49
+ * 用户提交输入时问一句。对齐官方 `prompt.submit`。
50
+ *
51
+ * @param input 即将送达模型的文本
52
+ * @returns `{ drop }` 阻断输入,`{ text }` 改写可见文本;附件与隐藏上下文由 core 保留。
53
+ */
54
+ promptSubmit(input: {
55
+ text: string;
56
+ sessionId?: string;
57
+ signal?: AbortSignal;
58
+ }): Promise<ModsPromptSubmitResult | undefined>;
59
+ /**
60
+ * 把一条 mod 打出来的 `$.ui.log` 行落到用户可见的会话里。
61
+ *
62
+ * ## 为什么走桥而不是让 core 直接推消息
63
+ *
64
+ * `$.ui.log` 是 mod 侧发起的用户可见事件(官方语义:进 transcript),但 core
65
+ * 不知道"当前会话"是哪一个(AsyncLocalStorage 上下文由宿主入口决定),也不认
66
+ * `plugin` 这个身份维度(那是 mods 引擎的概念)。宿主拿到这一对
67
+ * `(text, plugin)` 之后,负责取当前 session 并 push 进 `inputSubject` /
68
+ * `combineSubject` / `sessionStore.appendHistory`,就跟宿主自己的系统消息
69
+ * 是同一条路径。
70
+ *
71
+ * ## 未装桥时的兜底在哪
72
+ *
73
+ * 宿主没实现这一项时,`bindings.log` 侧应当把行文继续写到 stderr(headless /
74
+ * bootstrap 早期 / 无当前 session 时用得到)—— 那种场景下用户看不到"消息历史",
75
+ * 但仍能通过日志追溯这一行,与既有行为保持一致。
76
+ *
77
+ * @param text 已由宿主前缀好 `<plugin>: ` 的完整行文(core 不重复加前缀)
78
+ * @param meta 至少携带 `plugin`;用于前端识别 / 分组
79
+ */
80
+ pushModLog?(text: string, meta: {
81
+ plugin: string;
82
+ }): void;
83
+ }
84
+ /**
85
+ * 注入桥。宿主在 mods 运行时起来之后调用一次。
86
+ *
87
+ * @param next 桥实现;传 `undefined` 卸载(测试用)
88
+ */
89
+ export declare function setModsBridge(next: ModsBridge | undefined): void;
90
+ /** 当前桥;未注入时 undefined。 */
91
+ export declare function currentModsBridge(): ModsBridge | undefined;
92
+ /**
93
+ * 经桥过一遍上下文。
94
+ *
95
+ * **桥出问题不能影响主流程**:mod 是第三方代码,它抛错、超时、返回垃圾,都只该
96
+ * 让 function hooks 这一层失效,而不是让会话起不来。因此这里兜住一切异常并回退到
97
+ * 原上下文 —— 与「加载失败不阻塞宿主」同一条原则。
98
+ *
99
+ * @param context 当前上下文
100
+ * @returns 改写后的上下文;无桥或出错时是原对象
101
+ */
102
+ export declare function throughModsPromptContext(context: PromptContext): Promise<PromptContext>;
103
+ /** A failed third-party fold must not corrupt or lose the original input. */
104
+ export declare function throughModsPromptSubmit(text: string, sessionId?: string, signal?: AbortSignal): Promise<ModsPromptSubmitResult | undefined>;
105
+ /** Mutates only user text, before the shared history/model consumers see these items. */
106
+ export declare function applyModsPromptSubmit(items: AgentInputItem[], sessionId: string, signal?: AbortSignal): Promise<string | undefined>;
107
+ /**
108
+ * 通知会话开始。同样绝不向外抛。
109
+ *
110
+ * @param input 会话 cwd 与是否交互式
111
+ */
112
+ export declare function notifyModsSessionStart(input: {
113
+ cwd: string;
114
+ isInteractive: boolean;
115
+ }): Promise<void>;
@@ -30,12 +30,13 @@ export interface FollowupQueueSnapshot {
30
30
  items: FollowupQueueItemView[];
31
31
  }
32
32
  /** ACP `session/steer` 回调。服务不直接碰 ACP,方便单测。 */
33
- export type FollowupSteerFn = (contentBlocks: unknown[]) => Promise<{
33
+ export type FollowupSteerFn = (contentBlocks: unknown[], modsPromptSubmitted?: true) => Promise<{
34
34
  steered: boolean;
35
35
  }>;
36
36
  export interface FollowupQueueService {
37
37
  snapshot(session: Session): FollowupQueueSnapshot;
38
- enqueue(session: Session, contentBlocks: unknown[], hold: boolean): FollowupQueueSnapshot;
38
+ /** modsPromptSubmitted is host-validated provenance, never a client flag. */
39
+ enqueue(session: Session, contentBlocks: unknown[], hold: boolean, modsPromptSubmitted?: true): FollowupQueueSnapshot;
39
40
  remove(session: Session, itemId: string): FollowupQueueSnapshot;
40
41
  /**
41
42
  * 立即发送:能插入当前回合就 `steer` 并从队列拿掉;否则只摘 queueHold。
@@ -23,7 +23,7 @@ export declare class FollowupQueueServiceImpl implements FollowupQueueService {
23
23
  */
24
24
  private readonly pendingBroadcasts;
25
25
  snapshot(session: Session): FollowupQueueSnapshot;
26
- enqueue(session: Session, contentBlocks: unknown[], hold: boolean): FollowupQueueSnapshot;
26
+ enqueue(session: Session, contentBlocks: unknown[], hold: boolean, modsPromptSubmitted?: true): FollowupQueueSnapshot;
27
27
  remove(session: Session, itemId: string): FollowupQueueSnapshot;
28
28
  sendNow(session: Session, itemId: string, steer?: FollowupSteerFn): Promise<FollowupQueueSnapshot>;
29
29
  watch(session: Session): void;
@@ -28,6 +28,8 @@ export declare const SteerInputBuffer: unique symbol;
28
28
  * drain 返回的一组:待插入内容 + 独立对账 id + 客户端不透明 metadata。
29
29
  */
30
30
  export interface SteerDrainedGroup {
31
+ /** Set only after the host validates a one-time mod prompt receipt. */
32
+ readonly modsPromptSubmitted?: true;
31
33
  readonly blocks: unknown[];
32
34
  /** Native, already reviewed messages. Kept by reference for in-memory admission markers. */
33
35
  readonly messages?: readonly UserMessageItem[];
@@ -49,6 +51,8 @@ export interface SteerDrainedGroup {
49
51
  * push 的归属 + 客户端不透明 metadata 入参(合并为一个对象,避免 push 参数过多)。
50
52
  */
51
53
  export interface SteerPushInput {
54
+ /** Server-only provenance, independent of untrusted clientMeta. */
55
+ readonly modsPromptSubmitted?: true;
52
56
  /**
53
57
  * 这组消息归属的请求 id(当前正在执行的 turn 的 requestId);
54
58
  * `undefined` 表示无归属信息(drain 时按「当前也无归属」放行,退化为旧行为)。
@@ -40,6 +40,8 @@ export type PermissionResult = {
40
40
  updatedPermissions?: PermissionUpdate[];
41
41
  toolUseID?: string;
42
42
  decisionClassification?: PermissionDecisionClassification;
43
+ /** Optional host response metadata; interpreted only by the requesting tool. */
44
+ metadata?: Record<string, unknown>;
43
45
  } | {
44
46
  behavior: 'deny';
45
47
  message: string;
@@ -68,6 +70,8 @@ export interface CanUseToolOptions {
68
70
  displayName?: string;
69
71
  /** 桥端渲染的副标题说明。 */
70
72
  description?: string;
73
+ /** Tool-provided display/context metadata for host permission UIs. Not an authorization grant. */
74
+ metadata?: Record<string, unknown>;
71
75
  /** 本次工具调用在 assistant 消息内的唯一 id。 */
72
76
  toolUseID: string;
73
77
  /** 子代理上下文中触发时,子代理的 id。 */
@@ -32,6 +32,7 @@ export declare class SessionManagerImpl implements SessionManager, ApplicationLi
32
32
  private skipHistoryContext;
33
33
  private sessionStartTimes;
34
34
  private addHistoryLocks;
35
+ private promptAdmissions;
35
36
  private resultSubscriptions;
36
37
  private readonly resultSubscriptionOwners;
37
38
  /**
@@ -108,6 +109,7 @@ export declare class SessionManagerImpl implements SessionManager, ApplicationLi
108
109
  private readonly loadedSessionIds;
109
110
  private resetIdleTimer;
110
111
  addHistory(session: Session, input: string | AgentInputItem[]): Promise<void>;
112
+ private writeHistory;
111
113
  /**
112
114
  * Record a UserPromptSubmit hook block as an in-band assistant message
113
115
  * (issue #71907, cc-aligned: cc surfaces a warning system message and ends the
@@ -125,6 +127,34 @@ export declare class SessionManagerImpl implements SessionManager, ApplicationLi
125
127
  * (use-message-sender). chalk=false because ACP does not strip ANSI codes.
126
128
  */
127
129
  private recordHookBlockedMessage;
130
+ /**
131
+ * 把一条 mod 打出来的 `$.ui.log` 落到当前 session 的可见消息流里。
132
+ *
133
+ * ## 与 `recordHookBlockedMessage` 的关系
134
+ *
135
+ * 二者走**同一条落地路径**(history + inputSubject + combineSubject +
136
+ * sessionStore.appendHistory),因此 TUI / ACP / persisted transcript 都用
137
+ * 既有渲染,不必新增 sessionUpdate 类型(对齐根 CODEBUDDY.md 的 ACP 约束)。
138
+ *
139
+ * ## 与 hook 阻断消息刻意不同的三点
140
+ *
141
+ * 1. **不动 `session.lastMessageId`**:hook-block 是回合终点,需要挂上 replay
142
+ * 分支;mod-log 是**装饰性**通知,如果推进 lastMessageId 会让 `/rewind`
143
+ * 退到"一条 log 中间",语义混乱。
144
+ * 2. **不挂 `parentId`**:同样出于 replay 分支洁净。
145
+ * 3. **`chalk: false`**:ACP 侧不剥 ANSI,chalk 转义码会露出到 Web UI;此外
146
+ * 宿主已在 `<plugin>: <text>` 里给足了归属信号,terminal 也不再需要额外
147
+ * 颜色包装(TUI 侧后续可按 `hookStatus === 'mod-log'` 做专属渲染)。
148
+ *
149
+ * ## 失败语义
150
+ *
151
+ * 与 hook-block 同款:写盘失败只记日志,不向外抛 —— mod 打日志不该把 CLI 弄挂。
152
+ *
153
+ * @param session 目标 session(宿主取当前 session 后传入)
154
+ * @param text 已由宿主前缀成 `<plugin>: <text>` 的完整行文
155
+ * @param plugin 发起本次调用的 mod 名(`origin.plugin`),用于前端识别
156
+ */
157
+ recordModLogMessage(session: Session, text: string, plugin: string): Promise<void>;
128
158
  private readonly inflightSessionStarts;
129
159
  emitSessionStart(session: Session, source: SessionStartSource): Promise<void>;
130
160
  create(options?: Partial<Session>, needHandleRunResult?: boolean, parentSession?: Session): Promise<Session>;
@@ -1117,6 +1117,18 @@ export interface SessionManager {
1117
1117
  * before injecting `additionalContext`.
1118
1118
  */
1119
1119
  emitSessionStart(session: Session, source: SessionStartSource): Promise<void>;
1120
+ /**
1121
+ * Record a mod-plugin `$.ui.log` line as a user-visible message on the
1122
+ * session transcript (mirrors `recordHookBlockedMessage` — same
1123
+ * inputSubject / combineSubject / sessionStore path — but leaves
1124
+ * `lastMessageId` and `parentId` untouched because mod logs are
1125
+ * decorative and must not become anchors on the replay branch).
1126
+ *
1127
+ * @param session target session (host resolves current session and passes it in)
1128
+ * @param text already prefixed as `<plugin>: <text>` by the host bridge
1129
+ * @param plugin the calling mod's name (`origin.plugin`), for provider metadata
1130
+ */
1131
+ recordModLogMessage(session: Session, text: string, plugin: string): Promise<void>;
1120
1132
  addHistory(session: Session, input: string | AgentInputItem[]): Promise<void>;
1121
1133
  save(session: Session): Promise<void>;
1122
1134
  /** Initialize an immutable full snapshot for a new fork; target must be absent or empty. */
@@ -1,6 +1,6 @@
1
1
  import { type AcpTerminalStateItem, type AgentInputItem, type AssistantMessageItem, type CustomTitleItem, type FileHistorySnapshotItem, type FunctionCallItem, type FunctionCallResultItem, type HookBlockingDetail, HookEvent, type ResendForkNoticeItem, type UserMessageItem } from '../../../cbc-shared/node/index.js';
2
2
  type ItemStatus = 'in_progress' | 'completed' | 'incomplete';
3
- export type HookStatus = 'success' | 'blocked' | 'warning' | 'error';
3
+ export type HookStatus = 'success' | 'blocked' | 'warning' | 'error' | 'mod-log';
4
4
  interface ItemOptions {
5
5
  readonly skipRun?: boolean;
6
6
  readonly status?: ItemStatus;
@@ -24,6 +24,8 @@ export interface AcpResourceLinkProviderData {
24
24
  [extra: string]: unknown;
25
25
  }
26
26
  export interface DecodeInputTextToAcpContentBlocksOptions {
27
+ /** Defaults to legacy badge parsing. False restores only explicit resource metadata. */
28
+ parseInlineReferences?: boolean;
27
29
  resolveImageOrdinal?: (ordinal: number, label: string) => ContentBlock | undefined;
28
30
  /** 标题/摘要等纯展示场景允许缺少 providerData 的 badge 降级为不可交互的可读块。 */
29
31
  allowDisplayOnlyFallback?: boolean;
@@ -36,6 +38,8 @@ export interface DecodeInputTextToAcpContentBlocksOptions {
36
38
  allowUnicodeSlashTokens?: boolean;
37
39
  }
38
40
  export interface DecodePersistedUserContentToAcpContentBlocksOptions {
41
+ /** Keep ordinary text literal while restoring explicitly encoded attachments. */
42
+ parseInlineReferences?: boolean;
39
43
  convertPersistedImageBlock?: (block: Record<string, any>) => ContentBlock | undefined;
40
44
  }
41
45
  export declare function getAcpResourceLinkDisplayName(uri: string, name?: string): string;
@@ -41,12 +41,26 @@ export declare const ExtensionMethod: {
41
41
  readonly DELEGATE_TOOLS_CHANGED: '_codebuddy.ai/delegateToolsChanged';
42
42
  /** UI control for operating Web UI elements */
43
43
  readonly UI_CONTROL: '_codebuddy.ai/uiControl';
44
+ /**
45
+ * A `--plugin-dir` mod's `ui.render` output — one serialized tree per
46
+ * paint. Payload: `{ sessionId, plugin, tree }`. The webui / desktop
47
+ * surface re-hydrates the tree into React nodes; a `<Button>` in the
48
+ * tree renders as a real HTML button whose click sends MOD_PRESS.
49
+ */
50
+ readonly MOD_RENDER: '_codebuddy.ai/modRender';
51
+ /**
52
+ * Interaction back-channel for MOD_RENDER trees. Payload:
53
+ * `{ sessionId, plugin, element, surface }`. The agent dispatches
54
+ * `ui.press` and runs the closure the mod registered under that
55
+ * element key.
56
+ */
57
+ readonly MOD_PRESS: '_codebuddy.ai/modPress';
44
58
  };
45
59
  export type ExtensionMethodName = (typeof ExtensionMethod)[keyof typeof ExtensionMethod];
46
60
  /**
47
61
  * All known extension methods
48
62
  */
49
- export declare const KNOWN_EXTENSIONS: readonly ["_codebuddy.ai/artifact", "_codebuddy.ai/artifactBatch", "_codebuddy.ai/question", "_codebuddy.ai/checkpoint", "_codebuddy.ai/command", "_codebuddy.ai/authUrl", "_codebuddy.ai/file_history_snapshot", "_codebuddy.ai/delegateTool", "_codebuddy.ai/delegateToolsChanged"];
63
+ export declare const KNOWN_EXTENSIONS: readonly ["_codebuddy.ai/artifact", "_codebuddy.ai/artifactBatch", "_codebuddy.ai/question", "_codebuddy.ai/checkpoint", "_codebuddy.ai/command", "_codebuddy.ai/authUrl", "_codebuddy.ai/file_history_snapshot", "_codebuddy.ai/delegateTool", "_codebuddy.ai/delegateToolsChanged", "_codebuddy.ai/modRender", "_codebuddy.ai/modPress"];
50
64
  /**
51
65
  * 恶意网站拦截的错误码标记,随工具调用结果经 ACP 传给客户端。
52
66
  *
@@ -21,6 +21,14 @@ export interface ModelInputMemoryPressure {
21
21
  headroom: number;
22
22
  inputItems: number;
23
23
  estimatedInputBytes?: number;
24
+ /**
25
+ * Whether the retained-size walk finished (fully traversed, or provably ≥ the byte budget).
26
+ * `false` means the walk stopped on the node cap with bytes still under budget — the graph is
27
+ * deep/wide but not necessarily large, so `estimatedInputBytes` must NOT drive a rejection.
28
+ */
29
+ estimateComplete: boolean;
30
+ /** Real, content-independent OOM signal: heap is already near its ceiling. */
31
+ heapCritical: boolean;
24
32
  reject: boolean;
25
33
  }
26
34
  export declare function assessModelInputMemoryPressure(input: AgentInputItem[], heapUsed?: number, heapLimit?: number): ModelInputMemoryPressure;
@@ -80,8 +88,18 @@ export declare class RunnerFactoryImpl implements RunnerFactory {
80
88
  * 否则前端的 sending 态无人清除、永久转圈(#73022)。
81
89
  */
82
90
  protected readonly acpBroadcastService?: AgentQueueOutput;
91
+ /** Sessions with a deferred emergency compaction already scheduled (see scheduleEmergencyCompact). */
92
+ private readonly emergencyCompactInFlight;
83
93
  create(config?: Partial<RunConfig>): Promise<Runner>;
84
94
  private assertModelInputCloneHeadroom;
95
+ /**
96
+ * Fire a one-shot emergency compaction for an over-large history so the user's retry can
97
+ * succeed. Deferred + de-duplicated per session; failures are swallowed because the surfaced
98
+ * error already tells the user how to recover. Mirrors checkAutoCompact's non-blocking branch
99
+ * (setImmediate keeps it off the current runner's abort signal). compactAndSummarize has its
100
+ * own session-level lock, so the in-flight set only avoids scheduling redundant callbacks.
101
+ */
102
+ private scheduleEmergencyCompact;
85
103
  /**
86
104
  * 在每次模型请求前检查是否需要自动压缩。
87
105
  * 此时所有工具都已执行完毕,可以安全地触发压缩。
@@ -1,6 +1,7 @@
1
1
  export * from './context-protocol.js';
2
2
  export * from './compaction-lineage.js';
3
3
  export * from './context-host-protocol.js';
4
+ export * from './mods-bridge.js';
4
5
  export * from './prompt-context-provider.js';
5
6
  export * from './prompt-renderer.js';
6
7
  export * from './context-compact-summarizer.js';
@@ -0,0 +1,115 @@
1
+ import type { AgentInputItem } from '../../../../cbc-shared/lib/node/index.js';
2
+ import type { PromptContext } from './context-protocol.js';
3
+ export interface ModsPromptSubmitResult {
4
+ text?: string;
5
+ drop?: string;
6
+ }
7
+ /** Host-only provenance: a mod initiated submission has already traversed the fold. */
8
+ export declare const MODS_PROMPT_SUBMITTED_KEY = "modsPromptSubmitted";
9
+ /**
10
+ * Function hooks(Claude mods)在 core 侧的**窄接口**。
11
+ *
12
+ * ## 为什么 core 不直接调 mods
13
+ *
14
+ * `cbc-core` 只允许依赖 `@genie/cbc-shared`(见仓库 CODEBUDDY.md 的单向依赖卡点,
15
+ * `package-boundaries.spec.ts` 会扫源码 import、动态 import 与依赖声明)。而 mods
16
+ * 引擎住在 `cbc-plugins`。所以 core 这一侧只声明「我会在这几个时刻问一句」,具体
17
+ * 分发由宿主实现后注入。
18
+ *
19
+ * ## 为什么形状映射也不在 core
20
+ *
21
+ * cbc 的 `PromptContext` 是一个松散字典,官方 `prompt.context` 事件是
22
+ * `{ blocks, instructionFiles }`。两者的对应关系属于**适配**,而适配是宿主的事 ——
23
+ * 让 core 认识官方 payload 等于把上游协议渗进核心包,将来上游改一次形状,core 就
24
+ * 得跟着改。因此接口按 core 自己的类型说话,桥的实现负责翻译。
25
+ *
26
+ * ## 未注入时必须是零开销
27
+ *
28
+ * function hooks 默认关闭。没有桥时每个调用点只多一次 `=== undefined` 判断,
29
+ * 不构造对象、不进 async、不改变任何既有行为。
30
+ */
31
+ export interface ModsBridge {
32
+ /**
33
+ * 上下文装配完成、尚未拼成最终 prompt 时问一句。
34
+ *
35
+ * @param context 当前这一版上下文
36
+ * @returns 可能被 mod 改写过的上下文;桥自身出问题时应原样返回入参
37
+ */
38
+ promptContext(context: PromptContext): Promise<PromptContext>;
39
+ /**
40
+ * 会话建立时通知一次。
41
+ *
42
+ * @param input 会话 cwd 与是否交互式 —— 官方 `session.start` 的入参
43
+ */
44
+ sessionStart(input: {
45
+ cwd: string;
46
+ isInteractive: boolean;
47
+ }): Promise<void>;
48
+ /**
49
+ * 用户提交输入时问一句。对齐官方 `prompt.submit`。
50
+ *
51
+ * @param input 即将送达模型的文本
52
+ * @returns `{ drop }` 阻断输入,`{ text }` 改写可见文本;附件与隐藏上下文由 core 保留。
53
+ */
54
+ promptSubmit(input: {
55
+ text: string;
56
+ sessionId?: string;
57
+ signal?: AbortSignal;
58
+ }): Promise<ModsPromptSubmitResult | undefined>;
59
+ /**
60
+ * 把一条 mod 打出来的 `$.ui.log` 行落到用户可见的会话里。
61
+ *
62
+ * ## 为什么走桥而不是让 core 直接推消息
63
+ *
64
+ * `$.ui.log` 是 mod 侧发起的用户可见事件(官方语义:进 transcript),但 core
65
+ * 不知道"当前会话"是哪一个(AsyncLocalStorage 上下文由宿主入口决定),也不认
66
+ * `plugin` 这个身份维度(那是 mods 引擎的概念)。宿主拿到这一对
67
+ * `(text, plugin)` 之后,负责取当前 session 并 push 进 `inputSubject` /
68
+ * `combineSubject` / `sessionStore.appendHistory`,就跟宿主自己的系统消息
69
+ * 是同一条路径。
70
+ *
71
+ * ## 未装桥时的兜底在哪
72
+ *
73
+ * 宿主没实现这一项时,`bindings.log` 侧应当把行文继续写到 stderr(headless /
74
+ * bootstrap 早期 / 无当前 session 时用得到)—— 那种场景下用户看不到"消息历史",
75
+ * 但仍能通过日志追溯这一行,与既有行为保持一致。
76
+ *
77
+ * @param text 已由宿主前缀好 `<plugin>: ` 的完整行文(core 不重复加前缀)
78
+ * @param meta 至少携带 `plugin`;用于前端识别 / 分组
79
+ */
80
+ pushModLog?(text: string, meta: {
81
+ plugin: string;
82
+ }): void;
83
+ }
84
+ /**
85
+ * 注入桥。宿主在 mods 运行时起来之后调用一次。
86
+ *
87
+ * @param next 桥实现;传 `undefined` 卸载(测试用)
88
+ */
89
+ export declare function setModsBridge(next: ModsBridge | undefined): void;
90
+ /** 当前桥;未注入时 undefined。 */
91
+ export declare function currentModsBridge(): ModsBridge | undefined;
92
+ /**
93
+ * 经桥过一遍上下文。
94
+ *
95
+ * **桥出问题不能影响主流程**:mod 是第三方代码,它抛错、超时、返回垃圾,都只该
96
+ * 让 function hooks 这一层失效,而不是让会话起不来。因此这里兜住一切异常并回退到
97
+ * 原上下文 —— 与「加载失败不阻塞宿主」同一条原则。
98
+ *
99
+ * @param context 当前上下文
100
+ * @returns 改写后的上下文;无桥或出错时是原对象
101
+ */
102
+ export declare function throughModsPromptContext(context: PromptContext): Promise<PromptContext>;
103
+ /** A failed third-party fold must not corrupt or lose the original input. */
104
+ export declare function throughModsPromptSubmit(text: string, sessionId?: string, signal?: AbortSignal): Promise<ModsPromptSubmitResult | undefined>;
105
+ /** Mutates only user text, before the shared history/model consumers see these items. */
106
+ export declare function applyModsPromptSubmit(items: AgentInputItem[], sessionId: string, signal?: AbortSignal): Promise<string | undefined>;
107
+ /**
108
+ * 通知会话开始。同样绝不向外抛。
109
+ *
110
+ * @param input 会话 cwd 与是否交互式
111
+ */
112
+ export declare function notifyModsSessionStart(input: {
113
+ cwd: string;
114
+ isInteractive: boolean;
115
+ }): Promise<void>;
@@ -30,12 +30,13 @@ export interface FollowupQueueSnapshot {
30
30
  items: FollowupQueueItemView[];
31
31
  }
32
32
  /** ACP `session/steer` 回调。服务不直接碰 ACP,方便单测。 */
33
- export type FollowupSteerFn = (contentBlocks: unknown[]) => Promise<{
33
+ export type FollowupSteerFn = (contentBlocks: unknown[], modsPromptSubmitted?: true) => Promise<{
34
34
  steered: boolean;
35
35
  }>;
36
36
  export interface FollowupQueueService {
37
37
  snapshot(session: Session): FollowupQueueSnapshot;
38
- enqueue(session: Session, contentBlocks: unknown[], hold: boolean): FollowupQueueSnapshot;
38
+ /** modsPromptSubmitted is host-validated provenance, never a client flag. */
39
+ enqueue(session: Session, contentBlocks: unknown[], hold: boolean, modsPromptSubmitted?: true): FollowupQueueSnapshot;
39
40
  remove(session: Session, itemId: string): FollowupQueueSnapshot;
40
41
  /**
41
42
  * 立即发送:能插入当前回合就 `steer` 并从队列拿掉;否则只摘 queueHold。
@@ -23,7 +23,7 @@ export declare class FollowupQueueServiceImpl implements FollowupQueueService {
23
23
  */
24
24
  private readonly pendingBroadcasts;
25
25
  snapshot(session: Session): FollowupQueueSnapshot;
26
- enqueue(session: Session, contentBlocks: unknown[], hold: boolean): FollowupQueueSnapshot;
26
+ enqueue(session: Session, contentBlocks: unknown[], hold: boolean, modsPromptSubmitted?: true): FollowupQueueSnapshot;
27
27
  remove(session: Session, itemId: string): FollowupQueueSnapshot;
28
28
  sendNow(session: Session, itemId: string, steer?: FollowupSteerFn): Promise<FollowupQueueSnapshot>;
29
29
  watch(session: Session): void;