@dsh-edge/dsh-edge-agent 0.0.0-stage → 0.1.1
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/CHANGELOG.md +142 -0
- package/LICENSE +21 -0
- package/README.md +372 -2
- package/config/edge.example.json +179 -0
- package/config/privacy-policy.json +202 -0
- package/cordis.desktop-safe.patch.yml +129 -0
- package/cordis.patch.yml +382 -0
- package/cordis.yml +374 -0
- package/lib/attestation.d.ts +2 -0
- package/lib/attestation.d.ts.map +1 -0
- package/lib/attestation.js +2 -0
- package/lib/audit-log.d.ts +2 -0
- package/lib/audit-log.d.ts.map +1 -0
- package/lib/audit-log.js +2 -0
- package/lib/client.js +87 -0
- package/lib/cloud-llm-adapter.d.ts +2 -0
- package/lib/cloud-llm-adapter.d.ts.map +1 -0
- package/lib/cloud-llm-adapter.js +2 -0
- package/lib/dsh-adapter/attestation.d.ts +42 -0
- package/lib/dsh-adapter/attestation.d.ts.map +1 -0
- package/lib/dsh-adapter/attestation.js +40 -0
- package/lib/dsh-adapter/audit-log.d.ts +139 -0
- package/lib/dsh-adapter/audit-log.d.ts.map +1 -0
- package/lib/dsh-adapter/audit-log.js +376 -0
- package/lib/dsh-adapter/cloud-egress.d.ts +181 -0
- package/lib/dsh-adapter/cloud-egress.d.ts.map +1 -0
- package/lib/dsh-adapter/cloud-egress.js +257 -0
- package/lib/dsh-adapter/cloud-llm-adapter.d.ts +416 -0
- package/lib/dsh-adapter/cloud-llm-adapter.d.ts.map +1 -0
- package/lib/dsh-adapter/cloud-llm-adapter.js +1472 -0
- package/lib/dsh-adapter/contract.d.ts +80 -0
- package/lib/dsh-adapter/contract.d.ts.map +1 -0
- package/lib/dsh-adapter/contract.js +194 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.d.ts +214 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.d.ts.map +1 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.js +409 -0
- package/lib/dsh-adapter/dsh-edge-agent.d.ts +48 -0
- package/lib/dsh-adapter/dsh-edge-agent.d.ts.map +1 -0
- package/lib/dsh-adapter/dsh-edge-agent.js +64 -0
- package/lib/dsh-adapter/edge-config.d.ts +484 -0
- package/lib/dsh-adapter/edge-config.d.ts.map +1 -0
- package/lib/dsh-adapter/edge-config.js +898 -0
- package/lib/dsh-adapter/edge-context-view.d.ts +184 -0
- package/lib/dsh-adapter/edge-context-view.d.ts.map +1 -0
- package/lib/dsh-adapter/edge-context-view.js +483 -0
- package/lib/dsh-adapter/effective-model.d.ts +85 -0
- package/lib/dsh-adapter/effective-model.d.ts.map +1 -0
- package/lib/dsh-adapter/effective-model.js +171 -0
- package/lib/dsh-adapter/etscli-shell-guard.d.ts +244 -0
- package/lib/dsh-adapter/etscli-shell-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/etscli-shell-guard.js +420 -0
- package/lib/dsh-adapter/etscli-tools.d.ts +112 -0
- package/lib/dsh-adapter/etscli-tools.d.ts.map +1 -0
- package/lib/dsh-adapter/etscli-tools.js +393 -0
- package/lib/dsh-adapter/file-logger.d.ts +294 -0
- package/lib/dsh-adapter/file-logger.d.ts.map +1 -0
- package/lib/dsh-adapter/file-logger.js +564 -0
- package/lib/dsh-adapter/function-call-external.d.ts +357 -0
- package/lib/dsh-adapter/function-call-external.d.ts.map +1 -0
- package/lib/dsh-adapter/function-call-external.js +1005 -0
- package/lib/dsh-adapter/host-egress-guard.d.ts +161 -0
- package/lib/dsh-adapter/host-egress-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/host-egress-guard.js +213 -0
- package/lib/dsh-adapter/host-llm-trace.d.ts +198 -0
- package/lib/dsh-adapter/host-llm-trace.d.ts.map +1 -0
- package/lib/dsh-adapter/host-llm-trace.js +344 -0
- package/lib/dsh-adapter/http-api-routes.d.ts +61 -0
- package/lib/dsh-adapter/http-api-routes.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api-routes.js +79 -0
- package/lib/dsh-adapter/http-api-sessions.d.ts +105 -0
- package/lib/dsh-adapter/http-api-sessions.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api-sessions.js +173 -0
- package/lib/dsh-adapter/http-api.d.ts +221 -0
- package/lib/dsh-adapter/http-api.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api.js +1811 -0
- package/lib/dsh-adapter/index.d.ts +62 -0
- package/lib/dsh-adapter/index.d.ts.map +1 -0
- package/lib/dsh-adapter/index.js +97 -0
- package/lib/dsh-adapter/input-guard-messages.d.ts +56 -0
- package/lib/dsh-adapter/input-guard-messages.d.ts.map +1 -0
- package/lib/dsh-adapter/input-guard-messages.js +136 -0
- package/lib/dsh-adapter/input-guard.d.ts +508 -0
- package/lib/dsh-adapter/input-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/input-guard.js +2174 -0
- package/lib/dsh-adapter/intent-prompts.d.ts +77 -0
- package/lib/dsh-adapter/intent-prompts.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-prompts.js +97 -0
- package/lib/dsh-adapter/intent-router.d.ts +710 -0
- package/lib/dsh-adapter/intent-router.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-router.js +2802 -0
- package/lib/dsh-adapter/intent-signals.d.ts +208 -0
- package/lib/dsh-adapter/intent-signals.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-signals.js +391 -0
- package/lib/dsh-adapter/invariant-install.d.ts +16 -0
- package/lib/dsh-adapter/invariant-install.d.ts.map +1 -0
- package/lib/dsh-adapter/invariant-install.js +15 -0
- package/lib/dsh-adapter/invariant.d.ts +16 -0
- package/lib/dsh-adapter/invariant.d.ts.map +1 -0
- package/lib/dsh-adapter/invariant.js +22 -0
- package/lib/dsh-adapter/llm-adapters.d.ts +19 -0
- package/lib/dsh-adapter/llm-adapters.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-adapters.js +16 -0
- package/lib/dsh-adapter/llm-internal-caller.d.ts +92 -0
- package/lib/dsh-adapter/llm-internal-caller.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-internal-caller.js +100 -0
- package/lib/dsh-adapter/llm-max-turns.d.ts +55 -0
- package/lib/dsh-adapter/llm-max-turns.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-max-turns.js +127 -0
- package/lib/dsh-adapter/llm-trace-store.d.ts +131 -0
- package/lib/dsh-adapter/llm-trace-store.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-trace-store.js +205 -0
- package/lib/dsh-adapter/llm-trace.d.ts +450 -0
- package/lib/dsh-adapter/llm-trace.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-trace.js +463 -0
- package/lib/dsh-adapter/local-llm-adapter.d.ts +141 -0
- package/lib/dsh-adapter/local-llm-adapter.d.ts.map +1 -0
- package/lib/dsh-adapter/local-llm-adapter.js +787 -0
- package/lib/dsh-adapter/local-storage.d.ts +96 -0
- package/lib/dsh-adapter/local-storage.d.ts.map +1 -0
- package/lib/dsh-adapter/local-storage.js +198 -0
- package/lib/dsh-adapter/model-router.d.ts +79 -0
- package/lib/dsh-adapter/model-router.d.ts.map +1 -0
- package/lib/dsh-adapter/model-router.js +107 -0
- package/lib/dsh-adapter/ohttp-gateway.d.ts +49 -0
- package/lib/dsh-adapter/ohttp-gateway.d.ts.map +1 -0
- package/lib/dsh-adapter/ohttp-gateway.js +32 -0
- package/lib/dsh-adapter/openai-sse.d.ts +85 -0
- package/lib/dsh-adapter/openai-sse.d.ts.map +1 -0
- package/lib/dsh-adapter/openai-sse.js +462 -0
- package/lib/dsh-adapter/pii-utils.d.ts +96 -0
- package/lib/dsh-adapter/pii-utils.d.ts.map +1 -0
- package/lib/dsh-adapter/pii-utils.js +324 -0
- package/lib/dsh-adapter/plugin-host.d.ts +38 -0
- package/lib/dsh-adapter/plugin-host.d.ts.map +1 -0
- package/lib/dsh-adapter/plugin-host.js +38 -0
- package/lib/dsh-adapter/plugin.d.ts +46 -0
- package/lib/dsh-adapter/plugin.d.ts.map +1 -0
- package/lib/dsh-adapter/plugin.js +100 -0
- package/lib/dsh-adapter/privacy-policy.d.ts +237 -0
- package/lib/dsh-adapter/privacy-policy.d.ts.map +1 -0
- package/lib/dsh-adapter/privacy-policy.js +391 -0
- package/lib/dsh-adapter/prompt-guard.d.ts +152 -0
- package/lib/dsh-adapter/prompt-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/prompt-guard.js +872 -0
- package/lib/dsh-adapter/realm-skill-bridge.d.ts +365 -0
- package/lib/dsh-adapter/realm-skill-bridge.d.ts.map +1 -0
- package/lib/dsh-adapter/realm-skill-bridge.js +1180 -0
- package/lib/dsh-adapter/sanitizer-pipeline.d.ts +140 -0
- package/lib/dsh-adapter/sanitizer-pipeline.d.ts.map +1 -0
- package/lib/dsh-adapter/sanitizer-pipeline.js +431 -0
- package/lib/dsh-adapter/secure-bridge.d.ts +47 -0
- package/lib/dsh-adapter/secure-bridge.d.ts.map +1 -0
- package/lib/dsh-adapter/secure-bridge.js +35 -0
- package/lib/dsh-adapter/session-attribution.d.ts +70 -0
- package/lib/dsh-adapter/session-attribution.d.ts.map +1 -0
- package/lib/dsh-adapter/session-attribution.js +76 -0
- package/lib/dsh-adapter/sessions.d.ts +86 -0
- package/lib/dsh-adapter/sessions.d.ts.map +1 -0
- package/lib/dsh-adapter/sessions.js +179 -0
- package/lib/dsh-adapter/tcm.d.ts +55 -0
- package/lib/dsh-adapter/tcm.d.ts.map +1 -0
- package/lib/dsh-adapter/tcm.js +94 -0
- package/lib/dsh-adapter/td-bus.d.ts +87 -0
- package/lib/dsh-adapter/td-bus.d.ts.map +1 -0
- package/lib/dsh-adapter/td-bus.js +196 -0
- package/lib/dsh-adapter/tool-runner.d.ts +185 -0
- package/lib/dsh-adapter/tool-runner.d.ts.map +1 -0
- package/lib/dsh-adapter/tool-runner.js +271 -0
- package/lib/dsh-adapter/tools.d.ts +118 -0
- package/lib/dsh-adapter/tools.d.ts.map +1 -0
- package/lib/dsh-adapter/tools.js +292 -0
- package/lib/dsh-adapter/types.d.ts +23 -0
- package/lib/dsh-adapter/types.d.ts.map +1 -0
- package/lib/dsh-adapter/types.js +16 -0
- package/lib/dsh-adapter/voice-input.d.ts +30 -0
- package/lib/dsh-adapter/voice-input.d.ts.map +1 -0
- package/lib/dsh-adapter/voice-input.js +34 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.d.ts +152 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.d.ts.map +1 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.js +289 -0
- package/lib/http-api.d.ts +2 -0
- package/lib/http-api.d.ts.map +1 -0
- package/lib/http-api.js +2 -0
- package/lib/index.d.ts +48 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +49 -0
- package/lib/intent-router.d.ts +2 -0
- package/lib/intent-router.d.ts.map +1 -0
- package/lib/intent-router.js +2 -0
- package/lib/llm-adapters.d.ts +2 -0
- package/lib/llm-adapters.d.ts.map +1 -0
- package/lib/llm-adapters.js +2 -0
- package/lib/local-llm-adapter.d.ts +2 -0
- package/lib/local-llm-adapter.d.ts.map +1 -0
- package/lib/local-llm-adapter.js +2 -0
- package/lib/local-storage.d.ts +2 -0
- package/lib/local-storage.d.ts.map +1 -0
- package/lib/local-storage.js +2 -0
- package/lib/model-router.d.ts +2 -0
- package/lib/model-router.d.ts.map +1 -0
- package/lib/model-router.js +2 -0
- package/lib/ohttp-gateway.d.ts +2 -0
- package/lib/ohttp-gateway.d.ts.map +1 -0
- package/lib/ohttp-gateway.js +2 -0
- package/lib/plugin-host.d.ts +2 -0
- package/lib/plugin-host.d.ts.map +1 -0
- package/lib/plugin-host.js +2 -0
- package/lib/privacy-policy.d.ts +2 -0
- package/lib/privacy-policy.d.ts.map +1 -0
- package/lib/privacy-policy.js +2 -0
- package/lib/prompt-guard.d.ts +2 -0
- package/lib/prompt-guard.d.ts.map +1 -0
- package/lib/prompt-guard.js +2 -0
- package/lib/sanitizer-pipeline.d.ts +2 -0
- package/lib/sanitizer-pipeline.d.ts.map +1 -0
- package/lib/sanitizer-pipeline.js +2 -0
- package/lib/secure-bridge.d.ts +2 -0
- package/lib/secure-bridge.d.ts.map +1 -0
- package/lib/secure-bridge.js +2 -0
- package/lib/sessions.d.ts +2 -0
- package/lib/sessions.d.ts.map +1 -0
- package/lib/sessions.js +2 -0
- package/lib/tcm.d.ts +2 -0
- package/lib/tcm.d.ts.map +1 -0
- package/lib/tcm.js +2 -0
- package/lib/td-bus.d.ts +2 -0
- package/lib/td-bus.d.ts.map +1 -0
- package/lib/td-bus.js +2 -0
- package/lib/tools.d.ts +2 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +2 -0
- package/lib/voice-input.d.ts +2 -0
- package/lib/voice-input.d.ts.map +1 -0
- package/lib/voice-input.js +2 -0
- package/package.json +341 -3
- package/presets/agent-preset-edge.json +50 -0
- package/presets/edge.desktop-safe.patch.yml +503 -0
- package/presets/edge.patch.yml +557 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host-egress-guard — §7.151 出网那一跳的「只带云段历史」守卫
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要这个模块(主人现场,2026-10-29)
|
|
5
|
+
*
|
|
6
|
+
* 主人在 Edge 面板看到:发往云端模型(`qwen-token-plan-cn/deepseek-v4.1-flash`)
|
|
7
|
+
* 的请求里带着**全部**本地历史 —— 三条 `provider: local-llm` 的 assistant
|
|
8
|
+
* 回复原样上云。那违反 §7.131 的裁决(2026-10-27):
|
|
9
|
+
*
|
|
10
|
+
* > 发往 cloud-llm 的请求只能携带**之前上过 cloud-llm 的历史**;本地池带全部历史。
|
|
11
|
+
*
|
|
12
|
+
* ## 根因:这条出网**根本没经过**本项目的 cloud adapter
|
|
13
|
+
*
|
|
14
|
+
* §7.131 的裁剪一直住在 `cloud-llm-adapter` 里(直连路径 + delegate 路径各一份)。
|
|
15
|
+
* 但桌面部署的 CLOUD 轮次**压根不进那个 adapter**:
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* intent.classify.end category=CLOUD confidence=0.95 path=jev-adopt
|
|
19
|
+
* input-guard.routed category=CLOUD edgeRouteUsed=False routeSource=webui
|
|
20
|
+
* llm.trace.index REQ provider=qwen-token-plan-cn model=deepseek-v4.1-flash messageCount=31
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* - `edgeRouteUsed=False` ⇒ webui 入口没有 `_edgeRoute`,edge 不改写路由;
|
|
24
|
+
* - 意图判成 CLOUD ⇒ `handleAgentRequest` **不**降级 ⇒ seed 的 provider 原样保留;
|
|
25
|
+
* - seed 的 provider 是宿主自己的 `qwen-token-plan-cn`(桌面 GUI 选的模型)⇒
|
|
26
|
+
* agent-loop 直接派发给**宿主原生 provider**,不经过 `cloud-llm`。
|
|
27
|
+
*
|
|
28
|
+
* 于是 `cloud-llm-adapter` 里的 §7.131 裁剪**一次都没跑过**(审计链里
|
|
29
|
+
* `llm.cloud.delegate.resolved` / `llm.cloud.egress` 全是 0 条可证)。
|
|
30
|
+
*
|
|
31
|
+
* ## 修法:守卫落在**真正的出网那一跳**
|
|
32
|
+
*
|
|
33
|
+
* 宿主原生 provider 的那一跳只有一个必经之地:`llm/stream` waterfall
|
|
34
|
+
* (`ctx.llm.stream()` 的实现就是 `ctx.waterfall(this, 'llm/stream', options, …)`)。
|
|
35
|
+
* 守卫在那儿:
|
|
36
|
+
*
|
|
37
|
+
* 1. 判定这通调用是不是**出网**的(provider 不在本地池 / 自有池里);
|
|
38
|
+
* 2. 用 `planCloudEgress`(与 adapter 同一份口径)算出「只带云段历史」的请求体;
|
|
39
|
+
* 3. 命中本地段 ⇒ **换掉请求体重新派发**(`ctx.llm.stream({...options, messages})`)。
|
|
40
|
+
*
|
|
41
|
+
* ### 为什么必须「重新派发」而不是就地改写
|
|
42
|
+
*
|
|
43
|
+
* 宿主契约(`@deepseek-ai/dsh-llm` 的 api-catalog 明写):
|
|
44
|
+
*
|
|
45
|
+
* > A LOOP-built request carries the process-local `markAgentLoopRequest` identity
|
|
46
|
+
* > and arrives **deep-frozen (mutation throws)**: its content is a pure function
|
|
47
|
+
* > of the session log … so listeners **read it, never rewrite it**.
|
|
48
|
+
*
|
|
49
|
+
* 请求体是冻结的 ⇒ 只能换一个请求对象重新派发。这与 `cloud-llm-adapter` 的
|
|
50
|
+
* delegate 路径是同一个姿势(它也用 `{...options, messages: forwardMessages}`
|
|
51
|
+
* 重新派发),宿主侧已验证可行。
|
|
52
|
+
*
|
|
53
|
+
* 重新派发会**再进一次 waterfall**:本模块用 `markTrimmed` 打标记,
|
|
54
|
+
* 裁过的请求体进来直接放行 ⇒ 不会递归、不会二次裁剪。
|
|
55
|
+
*
|
|
56
|
+
* ## 裁剪口径与 adapter 完全同源
|
|
57
|
+
*
|
|
58
|
+
* 判据、段划分、丢弃规则全部复用 `cloud-egress.ts` 的 `planCloudEgress` ——
|
|
59
|
+
* 同一份事实源,杜绝「adapter 认为该带、守卫认为该丢」这种口径漂移。
|
|
60
|
+
*
|
|
61
|
+
* @module dsh-edge-agent/host-egress-guard
|
|
62
|
+
*/
|
|
63
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
64
|
+
import { type EgressMessage } from './cloud-egress.js';
|
|
65
|
+
import { LOCAL_POOL_FORWARD_MARKER } from './host-llm-trace.js';
|
|
66
|
+
/** 一通 `llm/stream` 调用里守卫用得到的字段(结构化描述,不依赖宿主类型)。 */
|
|
67
|
+
export interface HostEgressRequest {
|
|
68
|
+
provider?: unknown;
|
|
69
|
+
model?: unknown;
|
|
70
|
+
sessionId?: unknown;
|
|
71
|
+
purpose?: unknown;
|
|
72
|
+
messages?: unknown;
|
|
73
|
+
/** §7.171: 「本地池 delegate 转发」标记(见 `LOCAL_POOL_FORWARD_MARKER`)。 */
|
|
74
|
+
[LOCAL_POOL_FORWARD_MARKER]?: unknown;
|
|
75
|
+
}
|
|
76
|
+
/** 裁剪结果。 */
|
|
77
|
+
export interface HostEgressPlan {
|
|
78
|
+
/** 允许出网的消息(按原顺序)。 */
|
|
79
|
+
messages: EgressMessage[];
|
|
80
|
+
/** 被拦下的消息条数(本地段 / 无法证明上过云的段)。 */
|
|
81
|
+
droppedCount: number;
|
|
82
|
+
/** assistant 消息的 provider 分布(观测用:让人一眼看出这段历史是谁产的)。 */
|
|
83
|
+
histogram: Record<string, number>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 这通调用是不是**出网**的?
|
|
87
|
+
*
|
|
88
|
+
* - 自有池(`local-llm` / `cloud-llm`)⇒ **不归本守卫管**:
|
|
89
|
+
* `local-llm` 按裁决带全部历史;`cloud-llm` 由 `cloud-llm-adapter` 自己裁
|
|
90
|
+
* (直连与 delegate 两条路径都接了 §7.131,审计行也由它出)。
|
|
91
|
+
* 守卫再裁一遍会让 adapter 的 `historyDropped` 变成 0,**把证据洗掉**。
|
|
92
|
+
* - 本地池名单(model-router 的 LOCAL 名单)⇒ 本地池,带全部历史。
|
|
93
|
+
* - `local-` 前缀 ⇒ 同上(与 `providerSegment` 同源)。
|
|
94
|
+
* - 其余(宿主自选 provider,如 `qwen-token-plan-cn`)⇒ 出网,要裁。
|
|
95
|
+
*/
|
|
96
|
+
export declare function isCloudBoundProvider(provider: string, localProviders: readonly string[], own?: readonly string[]): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* 纯裁剪:只带云段历史。返回 `undefined` 表示**无需裁剪**(没有本地段)。
|
|
99
|
+
*
|
|
100
|
+
* `currentSegment: 'cloud'` —— 本轮正在出网,所以本轮归属云段。
|
|
101
|
+
*/
|
|
102
|
+
export declare function planHostEgress(messages: readonly EgressMessage[], localProviders: readonly string[]): HostEgressPlan | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* §7.171: 这通调用是不是「本地池 delegate 转发」?
|
|
105
|
+
*
|
|
106
|
+
* 本地池转发给宿主提供商时,`provider` 已经是宿主的目标路由(如
|
|
107
|
+
* `deepseek-official`)—— 与「用户直接选了那个模型」在 provider 维度上
|
|
108
|
+
* **无法区分**。唯一的区别在发起方:本地池的口径是「带全部历史」,宿主原生
|
|
109
|
+
* 路由的口径是「只带云段历史」。所以转发方必须在请求上打标记。
|
|
110
|
+
*
|
|
111
|
+
* 标记值刻意做成 truthy 判据(而不是 `=== true`):宿主 spread options 时
|
|
112
|
+
* 原样透传,值是什么不重要,**有没有**才重要。
|
|
113
|
+
*/
|
|
114
|
+
export declare function isLocalPoolForward(request: HostEgressRequest | undefined): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* 出网守卫。
|
|
117
|
+
*
|
|
118
|
+
* 持有 ctx 只为两件事:取本地 provider 名单(与 adapter 同源)、给裁过的请求
|
|
119
|
+
* 打标记(防重新派发时递归)。
|
|
120
|
+
*/
|
|
121
|
+
export declare class HostEgressGuard {
|
|
122
|
+
private readonly ctx;
|
|
123
|
+
/** 已裁过的请求对象 —— 重新派发会再进一次 waterfall,靠它放行。 */
|
|
124
|
+
private readonly trimmed;
|
|
125
|
+
constructor(ctx: Context);
|
|
126
|
+
/**
|
|
127
|
+
* 运行时取本地 provider 名单。
|
|
128
|
+
*
|
|
129
|
+
* 用 model-router 的 `listDetailed()`(带 realm 标注)—— 与路由判定、adapter
|
|
130
|
+
* 的 §7.131 裁剪同一份事实源。model-router 在 patch 里排在 input-guard 之后,
|
|
131
|
+
* 所以运行时取(与 `CloudEgressGuard.localProviders()` 同款做法)。
|
|
132
|
+
*
|
|
133
|
+
* **必须走 `ctx.get()` 而不是 `ctx.modelRouter` 直取**:cordis 4.x 的 ctx 是
|
|
134
|
+
* Proxy —— 读一个既没 provide、也不在 `inject` 声明里的属性会**直接抛**
|
|
135
|
+
* `cannot get property "modelRouter" without inject`(input-guard 刻意不把
|
|
136
|
+
* model-router 放进 `inject`,见那处的注释)。`ctx.get()` 才是「可选服务」的
|
|
137
|
+
* 安全读法(input-guard 的 `localAvailable()` 用的就是它)。
|
|
138
|
+
*/
|
|
139
|
+
localProviders(): string[];
|
|
140
|
+
/** 给裁过的请求对象打标记。 */
|
|
141
|
+
markTrimmed(options: object): void;
|
|
142
|
+
/** 这个请求对象是不是已经裁过了?(重新派发时进来 ⇒ 直接放行) */
|
|
143
|
+
isTrimmed(options: object): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* 这通调用要不要裁?裁的话给出裁剪结果。
|
|
146
|
+
*
|
|
147
|
+
* 不裁的情形(逐条都要有理由,缺一条就是一条漏):
|
|
148
|
+
* - 已经裁过的请求(重新派发进来的那一趟)⇒ 直接放行。
|
|
149
|
+
* - 没有 provider ⇒ 不是模型调用(防御)。
|
|
150
|
+
* - 自有池 / 本地池 ⇒ 按裁决不带云段 / 带全部历史。
|
|
151
|
+
* - **本地池 delegate 转发**(§7.171)⇒ 这通调用由本地池发起,口径是
|
|
152
|
+
* 「带全部历史」⇒ 不裁。转发后 `provider` 是宿主的目标 provider,光看
|
|
153
|
+
* provider 判不出来(那是宿主原生路由),只能靠转发标记区分。
|
|
154
|
+
* - `purpose` 非空 ⇒ 宿主的内部用途(compaction / session-title),
|
|
155
|
+
* 不是主人关心的「这一轮对话」,裁它会破坏子系统的语义。
|
|
156
|
+
* - 没有可识别的用户输入 ⇒ `planCloudEgress` 走退化路径(全部保留)。
|
|
157
|
+
* - 裁完一条不丢 ⇒ 没有本地段,不需要裁。
|
|
158
|
+
*/
|
|
159
|
+
plan(request: HostEgressRequest): HostEgressPlan | undefined;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=host-egress-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-egress-guard.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/host-egress-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,yBAAyB,EAAsB,MAAM,qBAAqB,CAAA;AAEnF,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,kEAAkE;IAClE,CAAC,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAA;CACtC;AAED,YAAY;AACZ,MAAM,WAAW,cAAc;IAC7B,qBAAqB;IACrB,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAA;IACpB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,SAAS,MAAM,EAAE,EACjC,GAAG,GAAE,SAAS,MAAM,EAAuB,GAC1C,OAAO,CAMT;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,cAAc,EAAE,SAAS,MAAM,EAAE,GAChC,cAAc,GAAG,SAAS,CAW5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,GAAG,OAAO,CAIlF;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAId,OAAO,CAAC,QAAQ,CAAC,GAAG;IAHhC,4CAA4C;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEnB,GAAG,EAAE,OAAO;IAEzC;;;;;;;;;;;;OAYG;IACH,cAAc,IAAI,MAAM,EAAE;IAmB1B,mBAAmB;IACnB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIlC,sCAAsC;IACtC,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAInC;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,GAAG,SAAS;CAiB7D"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host-egress-guard — §7.151 出网那一跳的「只带云段历史」守卫
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要这个模块(主人现场,2026-10-29)
|
|
5
|
+
*
|
|
6
|
+
* 主人在 Edge 面板看到:发往云端模型(`qwen-token-plan-cn/deepseek-v4.1-flash`)
|
|
7
|
+
* 的请求里带着**全部**本地历史 —— 三条 `provider: local-llm` 的 assistant
|
|
8
|
+
* 回复原样上云。那违反 §7.131 的裁决(2026-10-27):
|
|
9
|
+
*
|
|
10
|
+
* > 发往 cloud-llm 的请求只能携带**之前上过 cloud-llm 的历史**;本地池带全部历史。
|
|
11
|
+
*
|
|
12
|
+
* ## 根因:这条出网**根本没经过**本项目的 cloud adapter
|
|
13
|
+
*
|
|
14
|
+
* §7.131 的裁剪一直住在 `cloud-llm-adapter` 里(直连路径 + delegate 路径各一份)。
|
|
15
|
+
* 但桌面部署的 CLOUD 轮次**压根不进那个 adapter**:
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* intent.classify.end category=CLOUD confidence=0.95 path=jev-adopt
|
|
19
|
+
* input-guard.routed category=CLOUD edgeRouteUsed=False routeSource=webui
|
|
20
|
+
* llm.trace.index REQ provider=qwen-token-plan-cn model=deepseek-v4.1-flash messageCount=31
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* - `edgeRouteUsed=False` ⇒ webui 入口没有 `_edgeRoute`,edge 不改写路由;
|
|
24
|
+
* - 意图判成 CLOUD ⇒ `handleAgentRequest` **不**降级 ⇒ seed 的 provider 原样保留;
|
|
25
|
+
* - seed 的 provider 是宿主自己的 `qwen-token-plan-cn`(桌面 GUI 选的模型)⇒
|
|
26
|
+
* agent-loop 直接派发给**宿主原生 provider**,不经过 `cloud-llm`。
|
|
27
|
+
*
|
|
28
|
+
* 于是 `cloud-llm-adapter` 里的 §7.131 裁剪**一次都没跑过**(审计链里
|
|
29
|
+
* `llm.cloud.delegate.resolved` / `llm.cloud.egress` 全是 0 条可证)。
|
|
30
|
+
*
|
|
31
|
+
* ## 修法:守卫落在**真正的出网那一跳**
|
|
32
|
+
*
|
|
33
|
+
* 宿主原生 provider 的那一跳只有一个必经之地:`llm/stream` waterfall
|
|
34
|
+
* (`ctx.llm.stream()` 的实现就是 `ctx.waterfall(this, 'llm/stream', options, …)`)。
|
|
35
|
+
* 守卫在那儿:
|
|
36
|
+
*
|
|
37
|
+
* 1. 判定这通调用是不是**出网**的(provider 不在本地池 / 自有池里);
|
|
38
|
+
* 2. 用 `planCloudEgress`(与 adapter 同一份口径)算出「只带云段历史」的请求体;
|
|
39
|
+
* 3. 命中本地段 ⇒ **换掉请求体重新派发**(`ctx.llm.stream({...options, messages})`)。
|
|
40
|
+
*
|
|
41
|
+
* ### 为什么必须「重新派发」而不是就地改写
|
|
42
|
+
*
|
|
43
|
+
* 宿主契约(`@deepseek-ai/dsh-llm` 的 api-catalog 明写):
|
|
44
|
+
*
|
|
45
|
+
* > A LOOP-built request carries the process-local `markAgentLoopRequest` identity
|
|
46
|
+
* > and arrives **deep-frozen (mutation throws)**: its content is a pure function
|
|
47
|
+
* > of the session log … so listeners **read it, never rewrite it**.
|
|
48
|
+
*
|
|
49
|
+
* 请求体是冻结的 ⇒ 只能换一个请求对象重新派发。这与 `cloud-llm-adapter` 的
|
|
50
|
+
* delegate 路径是同一个姿势(它也用 `{...options, messages: forwardMessages}`
|
|
51
|
+
* 重新派发),宿主侧已验证可行。
|
|
52
|
+
*
|
|
53
|
+
* 重新派发会**再进一次 waterfall**:本模块用 `markTrimmed` 打标记,
|
|
54
|
+
* 裁过的请求体进来直接放行 ⇒ 不会递归、不会二次裁剪。
|
|
55
|
+
*
|
|
56
|
+
* ## 裁剪口径与 adapter 完全同源
|
|
57
|
+
*
|
|
58
|
+
* 判据、段划分、丢弃规则全部复用 `cloud-egress.ts` 的 `planCloudEgress` ——
|
|
59
|
+
* 同一份事实源,杜绝「adapter 认为该带、守卫认为该丢」这种口径漂移。
|
|
60
|
+
*
|
|
61
|
+
* @module dsh-edge-agent/host-egress-guard
|
|
62
|
+
*/
|
|
63
|
+
import { assistantProviderHistogram, planCloudEgress, } from './cloud-egress.js';
|
|
64
|
+
import { LOCAL_POOL_FORWARD_MARKER, OWN_POOL_PROVIDERS } from './host-llm-trace.js';
|
|
65
|
+
/**
|
|
66
|
+
* 这通调用是不是**出网**的?
|
|
67
|
+
*
|
|
68
|
+
* - 自有池(`local-llm` / `cloud-llm`)⇒ **不归本守卫管**:
|
|
69
|
+
* `local-llm` 按裁决带全部历史;`cloud-llm` 由 `cloud-llm-adapter` 自己裁
|
|
70
|
+
* (直连与 delegate 两条路径都接了 §7.131,审计行也由它出)。
|
|
71
|
+
* 守卫再裁一遍会让 adapter 的 `historyDropped` 变成 0,**把证据洗掉**。
|
|
72
|
+
* - 本地池名单(model-router 的 LOCAL 名单)⇒ 本地池,带全部历史。
|
|
73
|
+
* - `local-` 前缀 ⇒ 同上(与 `providerSegment` 同源)。
|
|
74
|
+
* - 其余(宿主自选 provider,如 `qwen-token-plan-cn`)⇒ 出网,要裁。
|
|
75
|
+
*/
|
|
76
|
+
export function isCloudBoundProvider(provider, localProviders, own = OWN_POOL_PROVIDERS) {
|
|
77
|
+
if (provider.length === 0)
|
|
78
|
+
return false;
|
|
79
|
+
if (own.includes(provider))
|
|
80
|
+
return false;
|
|
81
|
+
if (localProviders.includes(provider))
|
|
82
|
+
return false;
|
|
83
|
+
if (provider.startsWith('local-'))
|
|
84
|
+
return false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 纯裁剪:只带云段历史。返回 `undefined` 表示**无需裁剪**(没有本地段)。
|
|
89
|
+
*
|
|
90
|
+
* `currentSegment: 'cloud'` —— 本轮正在出网,所以本轮归属云段。
|
|
91
|
+
*/
|
|
92
|
+
export function planHostEgress(messages, localProviders) {
|
|
93
|
+
const plan = planCloudEgress(messages, {
|
|
94
|
+
currentSegment: 'cloud',
|
|
95
|
+
localProviders,
|
|
96
|
+
});
|
|
97
|
+
if (plan.droppedCount === 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
return {
|
|
100
|
+
messages: plan.kept,
|
|
101
|
+
droppedCount: plan.droppedCount,
|
|
102
|
+
histogram: assistantProviderHistogram(messages),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* §7.171: 这通调用是不是「本地池 delegate 转发」?
|
|
107
|
+
*
|
|
108
|
+
* 本地池转发给宿主提供商时,`provider` 已经是宿主的目标路由(如
|
|
109
|
+
* `deepseek-official`)—— 与「用户直接选了那个模型」在 provider 维度上
|
|
110
|
+
* **无法区分**。唯一的区别在发起方:本地池的口径是「带全部历史」,宿主原生
|
|
111
|
+
* 路由的口径是「只带云段历史」。所以转发方必须在请求上打标记。
|
|
112
|
+
*
|
|
113
|
+
* 标记值刻意做成 truthy 判据(而不是 `=== true`):宿主 spread options 时
|
|
114
|
+
* 原样透传,值是什么不重要,**有没有**才重要。
|
|
115
|
+
*/
|
|
116
|
+
export function isLocalPoolForward(request) {
|
|
117
|
+
if (request == null || typeof request !== 'object')
|
|
118
|
+
return false;
|
|
119
|
+
const flag = request[LOCAL_POOL_FORWARD_MARKER];
|
|
120
|
+
return flag !== undefined && flag !== null && flag !== false;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 出网守卫。
|
|
124
|
+
*
|
|
125
|
+
* 持有 ctx 只为两件事:取本地 provider 名单(与 adapter 同源)、给裁过的请求
|
|
126
|
+
* 打标记(防重新派发时递归)。
|
|
127
|
+
*/
|
|
128
|
+
export class HostEgressGuard {
|
|
129
|
+
ctx;
|
|
130
|
+
/** 已裁过的请求对象 —— 重新派发会再进一次 waterfall,靠它放行。 */
|
|
131
|
+
trimmed = new WeakSet();
|
|
132
|
+
constructor(ctx) {
|
|
133
|
+
this.ctx = ctx;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 运行时取本地 provider 名单。
|
|
137
|
+
*
|
|
138
|
+
* 用 model-router 的 `listDetailed()`(带 realm 标注)—— 与路由判定、adapter
|
|
139
|
+
* 的 §7.131 裁剪同一份事实源。model-router 在 patch 里排在 input-guard 之后,
|
|
140
|
+
* 所以运行时取(与 `CloudEgressGuard.localProviders()` 同款做法)。
|
|
141
|
+
*
|
|
142
|
+
* **必须走 `ctx.get()` 而不是 `ctx.modelRouter` 直取**:cordis 4.x 的 ctx 是
|
|
143
|
+
* Proxy —— 读一个既没 provide、也不在 `inject` 声明里的属性会**直接抛**
|
|
144
|
+
* `cannot get property "modelRouter" without inject`(input-guard 刻意不把
|
|
145
|
+
* model-router 放进 `inject`,见那处的注释)。`ctx.get()` 才是「可选服务」的
|
|
146
|
+
* 安全读法(input-guard 的 `localAvailable()` 用的就是它)。
|
|
147
|
+
*/
|
|
148
|
+
localProviders() {
|
|
149
|
+
let router;
|
|
150
|
+
try {
|
|
151
|
+
router = this.ctx.get?.('modelRouter');
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
router = undefined;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const detailed = router?.listDetailed?.() ?? [];
|
|
158
|
+
const locals = detailed
|
|
159
|
+
.filter(d => d.realm === 'LOCAL' && typeof d.provider === 'string')
|
|
160
|
+
.map(d => d.provider);
|
|
161
|
+
if (locals.length > 0)
|
|
162
|
+
return locals;
|
|
163
|
+
}
|
|
164
|
+
catch { /* fall through to the default */ }
|
|
165
|
+
return ['local-llm'];
|
|
166
|
+
}
|
|
167
|
+
/** 给裁过的请求对象打标记。 */
|
|
168
|
+
markTrimmed(options) {
|
|
169
|
+
this.trimmed.add(options);
|
|
170
|
+
}
|
|
171
|
+
/** 这个请求对象是不是已经裁过了?(重新派发时进来 ⇒ 直接放行) */
|
|
172
|
+
isTrimmed(options) {
|
|
173
|
+
return this.trimmed.has(options);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 这通调用要不要裁?裁的话给出裁剪结果。
|
|
177
|
+
*
|
|
178
|
+
* 不裁的情形(逐条都要有理由,缺一条就是一条漏):
|
|
179
|
+
* - 已经裁过的请求(重新派发进来的那一趟)⇒ 直接放行。
|
|
180
|
+
* - 没有 provider ⇒ 不是模型调用(防御)。
|
|
181
|
+
* - 自有池 / 本地池 ⇒ 按裁决不带云段 / 带全部历史。
|
|
182
|
+
* - **本地池 delegate 转发**(§7.171)⇒ 这通调用由本地池发起,口径是
|
|
183
|
+
* 「带全部历史」⇒ 不裁。转发后 `provider` 是宿主的目标 provider,光看
|
|
184
|
+
* provider 判不出来(那是宿主原生路由),只能靠转发标记区分。
|
|
185
|
+
* - `purpose` 非空 ⇒ 宿主的内部用途(compaction / session-title),
|
|
186
|
+
* 不是主人关心的「这一轮对话」,裁它会破坏子系统的语义。
|
|
187
|
+
* - 没有可识别的用户输入 ⇒ `planCloudEgress` 走退化路径(全部保留)。
|
|
188
|
+
* - 裁完一条不丢 ⇒ 没有本地段,不需要裁。
|
|
189
|
+
*/
|
|
190
|
+
plan(request) {
|
|
191
|
+
// 已经裁过的请求(重新派发进来的那一趟)⇒ 直接放行:再裁一次是幂等的,
|
|
192
|
+
// 但**再派发一次就是递归**。
|
|
193
|
+
if (request != null && typeof request === 'object' && this.isTrimmed(request))
|
|
194
|
+
return undefined;
|
|
195
|
+
// §7.171: 本地池 delegate 转发 ⇒ 不裁(LOCAL 口径:带全部历史)。
|
|
196
|
+
// 标记由 `CloudLlmDelegateAdapterImpl` 在 poolKind='local' 时打上。
|
|
197
|
+
if (isLocalPoolForward(request))
|
|
198
|
+
return undefined;
|
|
199
|
+
const provider = typeof request?.provider === 'string' ? request.provider : '';
|
|
200
|
+
if (provider.length === 0)
|
|
201
|
+
return undefined;
|
|
202
|
+
const purpose = typeof request?.purpose === 'string' ? request.purpose : '';
|
|
203
|
+
if (purpose.length > 0)
|
|
204
|
+
return undefined;
|
|
205
|
+
const locals = this.localProviders();
|
|
206
|
+
if (!isCloudBoundProvider(provider, locals))
|
|
207
|
+
return undefined;
|
|
208
|
+
if (!Array.isArray(request?.messages))
|
|
209
|
+
return undefined;
|
|
210
|
+
const messages = request.messages;
|
|
211
|
+
return planHostEgress(messages, locals);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host-llm-trace — §7.144 记录**宿主 provider** 发出的 LLM 调用
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要这个模块(主人现场发现)
|
|
5
|
+
*
|
|
6
|
+
* 主人在 Edge 面板看到「本轮走云端了,但 LLM 调用里没有云端模型」。查审计链
|
|
7
|
+
* 得到的硬事实是:那一轮**确实**判成了 CLOUD(jev 置信 0.83 < 门限 0.85 →
|
|
8
|
+
* 落到本地 LLM 分类器 → `llm-classified:cloud`),但整轮**零条**
|
|
9
|
+
* `llm.cloud.*` 事件 —— 只有 `llm.local.*`。
|
|
10
|
+
*
|
|
11
|
+
* 原因不是路由错了,是**观测漏了一跳**:
|
|
12
|
+
*
|
|
13
|
+
* - `config/edge.json` 的 `llm.cloud` 是 **delegate 模式**(`baseUrl` 空 +
|
|
14
|
+
* `delegate: true`,§7.100):CLOUD 请求**转发给宿主 profile 已配置的模型
|
|
15
|
+
* 提供商**(桌面 GUI 里选的那个,如 `qwen-token-plan-cn`),凭据全归宿主。
|
|
16
|
+
* - 于是主对话的请求**不经过本项目的 cloud adapter** ⇒ 没有 `llm.cloud.*`
|
|
17
|
+
* 审计事件,也没有 trace ⇒ 面板里云端模型整条缺席。
|
|
18
|
+
* - 面板是诚实的(它没编造),但**不完整** —— 主人看到的是「没发生过」,
|
|
19
|
+
* 实际是「没被记下来」。这正是 §7.129 最怕的那种误导:不是显示了一件
|
|
20
|
+
* 没发生的事,而是**漏显示**了一件真发生的事。
|
|
21
|
+
*
|
|
22
|
+
* ## 补在哪一跳
|
|
23
|
+
*
|
|
24
|
+
* 宿主的 `llm/stream` 事件带**完整信封**(`GenerateOptions`:provider /
|
|
25
|
+
* model / messages / system / tools / …),响应侧带**完整 chunk 流**
|
|
26
|
+
* (text-delta / block-end / tool-call-delta / usage / finish)。所以宿主
|
|
27
|
+
* provider 的这一跳**本来就完全可观测** —— 只是没人接。
|
|
28
|
+
*
|
|
29
|
+
* 本模块提供那一跳的记录能力;接线在 `input-guard` 的 `llm/stream` 钩子
|
|
30
|
+
* (那里已经握有 sessionId → 会话状态的映射,能区分「边缘会话」与「标准
|
|
31
|
+
* 模式会话」)。
|
|
32
|
+
*
|
|
33
|
+
* ## 与 adapter 记的那几条如何不重复
|
|
34
|
+
*
|
|
35
|
+
* 本项目自管两个池,provider 名固定:
|
|
36
|
+
*
|
|
37
|
+
* - `local-llm`(local adapter 注册名)
|
|
38
|
+
* - `cloud-llm`(cloud adapter 注册名;delegate 模式下它把请求**再**转发给
|
|
39
|
+
* 宿主 provider,那一跳的 provider 名是宿主自己的 ⇒ 正好被这里记到)
|
|
40
|
+
*
|
|
41
|
+
* 凡是 provider 属于自管池的调用,**adapter 自己已经记过**(REQ + RESP),
|
|
42
|
+
* 这里跳过 —— 否则面板上会出现同一通调用记两遍。
|
|
43
|
+
*
|
|
44
|
+
* ## 记什么
|
|
45
|
+
*
|
|
46
|
+
* | 侧 | 内容 |
|
|
47
|
+
* |---|---|
|
|
48
|
+
* | REQ | 宿主看到的信封:provider / model / messages / system / tools / 参数 |
|
|
49
|
+
* | RESP | 完整正文(block-end 优先)+ 工具调用 + finish reason + usage |
|
|
50
|
+
*
|
|
51
|
+
* `body` 快照是**宿主层信封**,不是 provider 的 wire body —— 后者在 adapter
|
|
52
|
+
* 内部,宿主这一跳看不见。面板上 provider/model 字段会显示宿主 provider
|
|
53
|
+
* 名(如 `qwen-token-plan-cn/deepseek-v4.1-flash`),一眼能认出「这是云端
|
|
54
|
+
* 模型那一条」。
|
|
55
|
+
*
|
|
56
|
+
* @module dsh-edge-agent/host-llm-trace
|
|
57
|
+
*/
|
|
58
|
+
import type { LlmRequestTrace, LlmResponseTrace, LlmTraceConfig, TraceSink } from './llm-trace.js';
|
|
59
|
+
/**
|
|
60
|
+
* 本项目自管池的 provider 名(adapter 注册名)。
|
|
61
|
+
*
|
|
62
|
+
* 这两个名字来自 adapter 的注册表(cloud adapter 的 `providers` 默认
|
|
63
|
+
* `['cloud-llm']`),不是配置项 —— 改成别的名字会破坏 §7.100 的 delegate
|
|
64
|
+
* 语义,所以这里写死成常量。
|
|
65
|
+
*/
|
|
66
|
+
export declare const OWN_POOL_PROVIDERS: readonly string[];
|
|
67
|
+
/**
|
|
68
|
+
* §7.171 (2026-11-10): 「本地池 delegate 转发」标记。
|
|
69
|
+
*
|
|
70
|
+
* `local-llm` 走 delegate 时会把请求转发给宿主提供商(§7.171 新增),转发时
|
|
71
|
+
* `provider` 变成了**目标** provider(不再是 `local-llm`)⇒ `host-egress-guard`
|
|
72
|
+
* 会把它当成「宿主原生 provider 的出网那一跳」⇒ 按 §7.131 口径裁掉本地段历史。
|
|
73
|
+
*
|
|
74
|
+
* 但本地池的口径恰恰相反:**带全部历史**(§7.131 的不对称规则第一条)。
|
|
75
|
+
* ⇒ 转发时打这个标记,出网守卫看到它就放行(不裁)。
|
|
76
|
+
*
|
|
77
|
+
* 打在 options 上是安全的:宿主 `LlmRuntime.stream` 走
|
|
78
|
+
* `{...options, ...resolvedConfig}` 的 spread(`@deepseek-ai/dsh-llm`
|
|
79
|
+
* `lib/index.js` 的 `adapterStream`),未知字段原样透传,不会被拒。
|
|
80
|
+
*
|
|
81
|
+
* 住在这里(而不是 adapter 里)是因为**打标的一方与读标的一方分属两个模块**:
|
|
82
|
+
* `cloud-llm-adapter.ts`(打)↔ `host-egress-guard.ts`(读)。与
|
|
83
|
+
* `OWN_POOL_PROVIDERS` 同处一地 —— 自有池相关的判据只有这一份事实源。
|
|
84
|
+
*/
|
|
85
|
+
export declare const LOCAL_POOL_FORWARD_MARKER = "__edgeLocalPoolForward";
|
|
86
|
+
/**
|
|
87
|
+
* 这个 provider 是不是本项目自己管的池?
|
|
88
|
+
*
|
|
89
|
+
* 是 ⇒ adapter 已经记过这次调用,这里必须跳过(否则同一通调用记两遍)。
|
|
90
|
+
* 否 ⇒ 宿主自己的 provider(delegate 出去的云池),**这里要补记**。
|
|
91
|
+
*/
|
|
92
|
+
export declare function isOwnPoolProvider(provider: string, own?: readonly string[]): boolean;
|
|
93
|
+
/** 宿主 `llm/stream` 的 options 里我们用得到的部分。 */
|
|
94
|
+
export interface HostCallRequest {
|
|
95
|
+
provider: string;
|
|
96
|
+
model: string;
|
|
97
|
+
sessionId: string;
|
|
98
|
+
messages: Array<{
|
|
99
|
+
role: string;
|
|
100
|
+
content: unknown;
|
|
101
|
+
source?: {
|
|
102
|
+
kind?: string;
|
|
103
|
+
provider?: string;
|
|
104
|
+
};
|
|
105
|
+
}>;
|
|
106
|
+
system?: string;
|
|
107
|
+
tools?: Array<{
|
|
108
|
+
name?: string;
|
|
109
|
+
function?: {
|
|
110
|
+
name?: string;
|
|
111
|
+
};
|
|
112
|
+
}>;
|
|
113
|
+
params?: {
|
|
114
|
+
temperature?: number;
|
|
115
|
+
maxTokens?: number;
|
|
116
|
+
stream?: boolean;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 构造宿主调用的 request trace。
|
|
121
|
+
*
|
|
122
|
+
* `rawBody` 记的是**宿主层信封**(provider / model / messages / system /
|
|
123
|
+
* tools / 参数)—— 宿主这一跳能看到的全部。provider 的 wire body 在 adapter
|
|
124
|
+
* 内部,从这里看不见(那一侧由 adapter 自己记)。
|
|
125
|
+
*/
|
|
126
|
+
export declare function buildHostRequestTrace(input: {
|
|
127
|
+
request: HostCallRequest;
|
|
128
|
+
config: LlmTraceConfig;
|
|
129
|
+
}): LlmRequestTrace;
|
|
130
|
+
/** 一次宿主调用的响应摘要。 */
|
|
131
|
+
export interface HostResponseSnapshot {
|
|
132
|
+
content: string;
|
|
133
|
+
toolCalls: Array<{
|
|
134
|
+
id?: string;
|
|
135
|
+
name: string;
|
|
136
|
+
arguments?: string;
|
|
137
|
+
}>;
|
|
138
|
+
finishReason?: string;
|
|
139
|
+
usage?: {
|
|
140
|
+
promptTokens?: number;
|
|
141
|
+
completionTokens?: number;
|
|
142
|
+
totalTokens?: number;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 从宿主的 chunk 流里累积一次响应。
|
|
147
|
+
*
|
|
148
|
+
* 正文优先取 `block-end` 的 `block.text`(宿主在 block 结束时给的是**全文**
|
|
149
|
+
* 比逐个 text-delta 拼更准);拿不到才回落累积 delta。
|
|
150
|
+
*/
|
|
151
|
+
export declare class HostResponseAccumulator {
|
|
152
|
+
private text;
|
|
153
|
+
private textTruncated;
|
|
154
|
+
private readonly calls;
|
|
155
|
+
private finishReason;
|
|
156
|
+
private usage;
|
|
157
|
+
/** 吃掉一个 chunk。永不抛 —— 畸形 chunk 直接忽略。 */
|
|
158
|
+
push(chunk: unknown): void;
|
|
159
|
+
private appendText;
|
|
160
|
+
/** 当前累积结果。 */
|
|
161
|
+
snapshot(): HostResponseSnapshot;
|
|
162
|
+
/** 累积时是否撞到上限(面板据此说明正文不全)。 */
|
|
163
|
+
get truncated(): boolean;
|
|
164
|
+
}
|
|
165
|
+
/** 构造宿主调用的 response trace。 */
|
|
166
|
+
export declare function buildHostResponseTrace(input: {
|
|
167
|
+
sessionId: string;
|
|
168
|
+
provider: string;
|
|
169
|
+
model: string;
|
|
170
|
+
snapshot: HostResponseSnapshot;
|
|
171
|
+
durationMs?: number;
|
|
172
|
+
config: LlmTraceConfig;
|
|
173
|
+
}): LlmResponseTrace;
|
|
174
|
+
/** 记录器的依赖面。 */
|
|
175
|
+
export interface HostCallRecorderOptions {
|
|
176
|
+
sink: TraceSink;
|
|
177
|
+
config: LlmTraceConfig;
|
|
178
|
+
request: HostCallRequest;
|
|
179
|
+
/** §7.134:与面板配对用的 callId(REQ/RESP 共用)。 */
|
|
180
|
+
callId: string;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 一次宿主 provider 调用的记录器。
|
|
184
|
+
*
|
|
185
|
+
* 构造即记 REQ;`wrap()` 包住 chunk 流,流结束时记 RESP。REQ/RESP 共用
|
|
186
|
+
* 同一个 callId ⇒ 面板上仍然「上请求、下响应」成对显示。
|
|
187
|
+
*/
|
|
188
|
+
export declare class HostCallRecorder {
|
|
189
|
+
private readonly options;
|
|
190
|
+
private readonly accumulator;
|
|
191
|
+
private readonly startedAt;
|
|
192
|
+
private responseEmitted;
|
|
193
|
+
constructor(options: HostCallRecorderOptions);
|
|
194
|
+
/** 包住 chunk 流:边转发边累积,流结束记 RESP。 */
|
|
195
|
+
wrap(stream: AsyncIterable<unknown>): AsyncIterable<unknown>;
|
|
196
|
+
private emitResponse;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=host-llm-trace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-llm-trace.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/host-llm-trace.ts"],"names":[],"mappings":"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"}
|