@dsh-enhanced/assistant-delivery 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/cordis.patch.yml +0 -2
- package/lib/agent-runtime.d.ts +21 -10
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +303 -46
- package/lib/agent-runtime.js.map +1 -1
- package/lib/service.d.ts +8 -31
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +158 -15
- package/lib/service.js.map +1 -1
- package/lib/store.d.ts +27 -0
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +70 -4
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +35 -5
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -44,8 +44,6 @@ Patch 通过 `inject: [assistantPolicy]` 固定加载策略依赖;入站 Agent
|
|
|
44
44
|
| `defaultAgentPreset` | `standard` | 新 session 解析并挂载的 preset;该内置 preset 提供 Bash、文件、检索、Skills 等完整编码能力 |
|
|
45
45
|
| `policyRef` | `owner-dm` | 固化在 binding 上的策略引用标签 |
|
|
46
46
|
| `agentProvider` / `agentModel` | `deepseek-official` / `deepseek-v4-flash` | 渠道 Agent 的部署默认模型;会话可用 `/model` 覆盖 |
|
|
47
|
-
| `toolCapableProviders` | `[deepseek-official]` | 尚未发布 registry metadata 的上游原生工具 provider 审计列表;在 `unknownRouteToolCalls: deny` 下作为白名单生效 |
|
|
48
|
-
| `unknownRouteToolCalls` | `allow` | 未发布任何工具能力声明的 route 如何准入;`deny` 切换为严格白名单。provider 显式声明 `none` 时两种取值都 fail closed |
|
|
49
47
|
| `agentMaxOutputTokens` | `8192` | 单轮模型输出上限 |
|
|
50
48
|
| `modelPickerTtlMs` | `900000` | `/model` 选择卡片的签名提交有效期;范围 1 分钟至 24 小时 |
|
|
51
49
|
| `permissionPickerTtlMs` | `900000` | `/permissions` 三档权限卡片的签名提交有效期;范围 1 分钟至 24 小时 |
|
|
@@ -106,6 +104,7 @@ rules:
|
|
|
106
104
|
- 429/明确 5xx 未发送应给出 `retryable` 与可选 `retryAfterMs`;永久 4xx 进入 dead。
|
|
107
105
|
- 请求可能已到平台但 response 丢失时返回 `unknown`。若声明 `reconcileUnknownSend` capability,必须实现对账,并使用独立的有界对账 attempt;连续无法判定达到上限后进入 dead,释放同 lane 后续消息。未声明该能力时记录保持 `unknown_after_send`,不会被领取为空对账 attempt,直到 owner 显式 retry/cancel。
|
|
108
106
|
- receipt 必须带同一个 channel、account、provider message id;状态只允许单调 `accepted→delivered→read`。
|
|
107
|
+
- `progress()` 收到的工具参数/结果仅是最多 1500 字符的展示预览,并会脱敏常见 credential 字段与 token 形态;畸形或原始输入过大的参数只产生状态标记,不回显 raw text。它不包含任何 reasoning/thinking 内容或 provider 原始错误。adapter 仍必须按当前受众决定是否展示明细,群聊等共享受众应保持 status-only。
|
|
109
108
|
- adapter 的 socket、timer、SDK client 和 listener 必须由 `start()` disposer 释放。
|
|
110
109
|
|
|
111
110
|
内部身份始终是结构化的 `ExternalPrincipalKey`、`ConversationRef` 与 `ConversationBinding`,不会从 `platform:chat:thread` 字符串反解析路由。DM 不允许 thread;group 入口要求渠道提供显式且稳定的 thread,可以是真实 provider root,也可以是渠道命名空间内按 principal 派生的顶层合成 lane。binding 仍固定精确 principal,任何同 conversation 的另一 principal 都会在进入 session 前 dead-letter,不能共享既有 owner 上下文。Lark 顶层群消息使用按发送者派生的合成 lane,真实回复串继续按 `root_id` 隔离。
|
|
@@ -150,7 +149,7 @@ rules:
|
|
|
150
149
|
|
|
151
150
|
`/model` 从宿主 `ctx.llm` 的实时 provider/model 目录生成受限的 typed `model-picker` intent,不发起模型生成;每个模型会显式关联它自己的 `effortIds`,而不是共享全局 effort 并集。支持它的渠道可以据此显示“分组、模型、effort”三级联动选择和确认按钮,不支持它的渠道会把该 intent 明确拒绝而不会降级为任意卡片 JSON。持久 selection 会先投影为不含 `updatedAt/version` 的纯路由再进入严格 intent 边界;如果目录中的异常数据仍使卡片校验失败,系统会立即退回完整纯文本目录,确定性校验错误不会重试。某个 provider 或模型能力解析失败不影响其他项,最多载入 20 个 provider、50 个模型、每模型 20 档 effort,并且全目录最多 20 个不同 effort id。
|
|
152
151
|
|
|
153
|
-
卡片提交后,Delivery 会再次核对 active binding、精确 principal/chat 和 Policy,并用实时 `resolveModelInfo()` 验证 provider/model 及该模型支持的 effort;通过后把三项选择按 canonical conversation 持久化到 Delivery SQLite
|
|
152
|
+
卡片提交后,Delivery 会再次核对 active binding、精确 principal/chat、原始 provider message id 和 Policy,并用实时 `resolveModelInfo()` 验证 provider/model 及该模型支持的 effort;通过后把三项选择按 canonical conversation 持久化到 Delivery SQLite。渠道可以等待该 durable settlement:同实例由完成通知即时唤醒,共享数据库的另一实例完成时由有界轮询感知,从而把原卡更新成不可交互的最终结果。选择从下一条普通消息生效,保留当前 session 上下文,跨 `/new` 和 Host 重启仍有效。卡片可选择“默认(由模型决定)”;`/model use` 保留为无卡片渠道和排错时的文字后备,`/model reset` 删除会话覆盖并恢复 `agentProvider` / `agentModel`。模型目录按宿主约定是建议性的,最终调用是否成功仍由对应 adapter 和账号认证决定。若某个已持久化的显式 effort 在下一条普通消息前被 DSH 核心判为不支持,Delivery 会在任何 prompt 或 durable dispatch 标记之前,以 version CAS 仅清除该 effort、重新预检同一 provider/model 的默认档位;CAS 冲突会重试而不会覆盖更晚的 `/model` 选择。模型不会被静默替换,也不会重放或重复计费。
|
|
154
153
|
|
|
155
154
|
`/permissions`(别名 `/permission`)只允许当前 active binding 的精确 owner 使用,命令本身不会进入 Agent 或触发 LLM。支持 typed `permission-picker` 的私聊渠道会显示“请求批准(ask)/帮我批准(auto)/完全访问权限(full)”三档卡片并标出当前档位;不支持卡片或渲染失败时自动退回含同等信息的文字命令。前两档都使用 `workspace-write + ask`,区别是持久 reviewer 为 `user` 或 `auto-review`。`full` 使用 `danger-full-access + never + none`,可访问网络及任意文件;卡片按钮带原生风险确认,文字入口则由 `/permission full` 显示橙色警告,必须再次发送 `/permission full confirm` 才切换。
|
|
156
155
|
|
|
@@ -164,7 +163,7 @@ rules:
|
|
|
164
163
|
|
|
165
164
|
权限命令在第一次 reviewer/policy/sandbox mutation 前还必须先取得 Inbox 的 durable dispatch/recovery marker;每个异步边界后都复核 abort 与租约。若进程退出、续租失败、flush 未确认、依赖尚未就绪或回复入队失败,该 marker 会绕过普通 `maxAttempts`,在原 binding 串行 lane 中继续恢复,直到确认已持久化目标并补发成功终态,或安全收敛到 ask 后补发失败/取消终态。`/stop` 和 `/new` 会在 abort 前把正在执行的权限命令原子改标为 cancelled recovery;旧命令不会以 `processor-ambiguous` 终止,也不会从原始文本重新制造一次可能已取消的 full 提权。
|
|
166
165
|
|
|
167
|
-
普通消息真正 resume Agent 时,Delivery
|
|
166
|
+
普通消息真正 resume Agent 时,Delivery 先解析并挂载 binding 固定的 preset,再把该 Agent 的最终 scoped tool schemas 原样交给当前 provider/model。工具和 Skills 属于 Agent/preset,模型切换不会改变它们的可见集合,Delivery 也不维护 provider 工具白名单或部署侧能力名单。协议元数据缺失、`native` 和 `bridge` 都会放行;只有 adapter 主动声明 `toolCalls: none` 且最终 scope 非空时,才会在 prompt 发送前因“未实现统一 DSH tool-call 协议”失败关闭。这个检查描述 adapter 的传输实现,不是模型权限或能力分级;实际工具执行仍逐次经过 ToolRuntime、Policy、sandbox、审批、身份与预算检查。fresh session 创建本身不执行模型,仍可先持久建立 binding。
|
|
168
167
|
|
|
169
168
|
只注册两个模型工具:
|
|
170
169
|
|
package/cordis.patch.yml
CHANGED
package/lib/agent-runtime.d.ts
CHANGED
|
@@ -26,11 +26,16 @@ interface DshDeliveryRuntimeOptions {
|
|
|
26
26
|
policyRef: string;
|
|
27
27
|
provider: string;
|
|
28
28
|
model: string;
|
|
29
|
-
toolCapableProviders: ReadonlySet<string>;
|
|
30
|
-
unknownRouteToolCalls: 'allow' | 'deny';
|
|
31
29
|
maxOutputTokens: number;
|
|
32
30
|
permissionPickerTtlMs: number;
|
|
33
31
|
getModelSelection(conversation: ConversationRef): ConversationModelSelection | undefined;
|
|
32
|
+
/** Atomically remove a stale explicit effort without overwriting a newer model choice. */
|
|
33
|
+
clearStaleModelReasoningEffort(conversation: ConversationRef, expected: ConversationModelSelection): {
|
|
34
|
+
applied: false;
|
|
35
|
+
} | {
|
|
36
|
+
applied: true;
|
|
37
|
+
selection: ConversationModelSelection;
|
|
38
|
+
};
|
|
34
39
|
imageMaterializer: Pick<InboundImageMaterializer, 'materialize'>;
|
|
35
40
|
isInboundAuthorized(binding: Readonly<ConversationBinding>, envelope: Readonly<InboundEnvelope>): boolean;
|
|
36
41
|
isPermissionController(binding: Readonly<ConversationBinding>, envelope: Readonly<InboundEnvelope>): boolean;
|
|
@@ -60,15 +65,14 @@ interface ModelCommandReply {
|
|
|
60
65
|
fallbackText?: string;
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
|
-
* Convert one durable session fact into a
|
|
64
|
-
*
|
|
68
|
+
* Convert one durable session fact into a bounded, user-visible progress update.
|
|
69
|
+
* Reasoning/thinking content and provider error messages never cross this boundary. Tool calls/results
|
|
70
|
+
* contribute bounded previews with common credential shapes redacted; the channel still decides
|
|
71
|
+
* whether its current audience is private enough to render those previews.
|
|
65
72
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* only, and the ACP thought channel is not mapped to DSH reasoning yet. `step/start` therefore also
|
|
70
|
-
* yields a neutral phase label, so a turn reports progress even on providers that never reason
|
|
71
|
-
* out loud.
|
|
73
|
+
* A DSH `reasoning` block is explicitly thinking content and is not guaranteed to be a public
|
|
74
|
+
* summary, even after assembly into `assistant/message`. `step/start` therefore provides the only
|
|
75
|
+
* reasoning-independent phase label, so a turn reports progress without exposing hidden thought.
|
|
72
76
|
*/
|
|
73
77
|
export declare function deliveryProgressFromSessionEvent(event: SessionEvent): DeliveryProgressUpdate | undefined;
|
|
74
78
|
export declare function permissionStateHash(events: readonly SessionEvent[], tableFingerprint?: string): string;
|
|
@@ -88,6 +92,13 @@ export declare class DshDeliveryRuntime implements DeliveryInboundRuntime {
|
|
|
88
92
|
/** Wait for only the turn that claims Delivery's exact message identity. */
|
|
89
93
|
private followupTurn;
|
|
90
94
|
private setupAgent;
|
|
95
|
+
/**
|
|
96
|
+
* DSH validates an explicit reasoning effort before calling an adapter. That is normally the
|
|
97
|
+
* right contract, but a persisted external-channel selection can outlive a provider's live
|
|
98
|
+
* model directory. Clear only that stale persisted effort before a prompt crosses the durable
|
|
99
|
+
* dispatch boundary; TraeX still rechecks the newly created ACP session afterwards.
|
|
100
|
+
*/
|
|
101
|
+
private resolveExecutionRoute;
|
|
91
102
|
private reconcileNativeFullPermissionReviewer;
|
|
92
103
|
private runPermissionCommand;
|
|
93
104
|
createSession(input: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-runtime.d.ts","sourceRoot":"","sources":["../src/agent-runtime.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,EAEL,KAAK,KAAK,EAGX,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAUlE,OAAO,
|
|
1
|
+
{"version":3,"file":"agent-runtime.d.ts","sourceRoot":"","sources":["../src/agent-runtime.ts"],"names":[],"mappings":"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"}
|
package/lib/agent-runtime.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdir, stat } from 'node:fs/promises';
|
|
|
3
3
|
import { installModelSelection, } from '@deepseek-ai/dsh-agent';
|
|
4
4
|
import { contentHasImage, createUserMessage, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
5
5
|
import { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
|
|
6
|
-
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
6
|
+
import { isAppendSurfaceEvent, SessionId } from '@deepseek-ai/dsh-session';
|
|
7
7
|
import { effectiveApprovalPolicy, setApprovalPolicy, } from '@deepseek-ai/dsh-user-approval';
|
|
8
8
|
import { approvalReviewerOf, setApprovalReviewer, waitForApprovalReviewerSessionEventReady, } from '@dsh-enhanced/assistant-policy';
|
|
9
9
|
import { resolveLlmRouteCapability } from '@dsh-enhanced/llm-route-capabilities';
|
|
@@ -22,6 +22,16 @@ class UserSessionCancellation extends Error {
|
|
|
22
22
|
const MAX_CATALOG_MODELS = 50;
|
|
23
23
|
const MAX_PROGRESS_TODOS = 20;
|
|
24
24
|
const MAX_PROGRESS_TEXT_CHARS = 240;
|
|
25
|
+
const MAX_PROGRESS_TOOL_PREVIEW_CHARS = 1_500;
|
|
26
|
+
const MAX_PROGRESS_JSON_INPUT_CHARS = 32_768;
|
|
27
|
+
const MAX_PROGRESS_JSON_DEPTH = 16;
|
|
28
|
+
const MAX_PROGRESS_JSON_NODES = 500;
|
|
29
|
+
const MAX_PROGRESS_RESULT_BLOCKS = 100;
|
|
30
|
+
const REDACTED_PROGRESS_VALUE = '[REDACTED]';
|
|
31
|
+
const TRUNCATED_PROGRESS_VALUE = '[TRUNCATED]';
|
|
32
|
+
const PROGRESS_ASSIGNMENT_KEY = '([A-Za-z_][A-Za-z0-9_.-]{0,127})';
|
|
33
|
+
const PROGRESS_ASSIGNMENT_START = new RegExp(`(^|[^A-Za-z0-9_])(["']?)${PROGRESS_ASSIGNMENT_KEY}\\2(\\s*[:=]\\s*)`, 'giu');
|
|
34
|
+
const ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START = new RegExp(`(^|[^A-Za-z0-9_])\\\\+(["'])${PROGRESS_ASSIGNMENT_KEY}\\\\+\\2(\\s*[:=]\\s*)`, 'giu');
|
|
25
35
|
class DurableAgentIdentityError extends Error {
|
|
26
36
|
}
|
|
27
37
|
class ApprovalReviewerReaderUnavailableError extends Error {
|
|
@@ -30,16 +40,16 @@ class ApprovalReviewerReaderUnavailableError extends Error {
|
|
|
30
40
|
this.name = 'ApprovalReviewerReaderUnavailableError';
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
|
-
class
|
|
43
|
+
class AdapterToolCallProtocolError extends Error {
|
|
34
44
|
provider;
|
|
35
45
|
model;
|
|
36
46
|
presetId;
|
|
37
47
|
constructor(provider, model, presetId) {
|
|
38
|
-
super(`assistant-delivery: ${provider}/${model}
|
|
48
|
+
super(`assistant-delivery: adapter ${provider}/${model} declares no DSH tool-call protocol`);
|
|
39
49
|
this.provider = provider;
|
|
40
50
|
this.model = model;
|
|
41
51
|
this.presetId = presetId;
|
|
42
|
-
this.name = '
|
|
52
|
+
this.name = 'AdapterToolCallProtocolError';
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
class ImageCapabilityAdmissionError extends Error {
|
|
@@ -52,32 +62,18 @@ class ImageCapabilityAdmissionError extends Error {
|
|
|
52
62
|
this.name = 'ImageCapabilityAdmissionError';
|
|
53
63
|
}
|
|
54
64
|
}
|
|
55
|
-
function
|
|
65
|
+
function requireAdapterToolCallProtocol(llm, provider, model, presetId, toolCount) {
|
|
56
66
|
if (toolCount === 0)
|
|
57
67
|
return;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// A provider-owned declaration is authoritative in both directions: an
|
|
62
|
-
// explicit `none` still fails closed even when the route is allowlisted or
|
|
63
|
-
// unknown routes are admitted.
|
|
64
|
-
if (capability?.toolCalls === 'none')
|
|
65
|
-
throw new ToolCapabilityAdmissionError(provider, model, presetId);
|
|
66
|
-
// Only routes with no declaration at all reach the host policy. The DSH
|
|
67
|
-
// `generate` contract accepts `tools` for every adapter, so tool calling is
|
|
68
|
-
// the adapter baseline rather than an opt-in: gateway and built-in providers
|
|
69
|
-
// (pi-ai and friends) never publish registry metadata, and denying them by
|
|
70
|
-
// default made every such route unusable behind a tool-mounting preset.
|
|
71
|
-
if (declaredToolCapableProviders.has(provider))
|
|
72
|
-
return;
|
|
73
|
-
if (unknownRouteToolCalls === 'allow')
|
|
74
|
-
return;
|
|
75
|
-
throw new ToolCapabilityAdmissionError(provider, model, presetId);
|
|
68
|
+
if (resolveLlmRouteCapability(llm, provider, model)?.toolCalls === 'none') {
|
|
69
|
+
throw new AdapterToolCallProtocolError(provider, model, presetId);
|
|
70
|
+
}
|
|
76
71
|
}
|
|
77
|
-
function
|
|
72
|
+
function adapterToolCallProtocolReply(error) {
|
|
78
73
|
return {
|
|
79
|
-
text:
|
|
80
|
-
+
|
|
74
|
+
text: `当前路由 ${error.provider}/${error.model} 的 adapter 明确声明未实现统一 DSH tool-call 协议,`
|
|
75
|
+
+ `因此无法承载 preset “${error.presetId}” 已挂载的工具。`
|
|
76
|
+
+ '这不是模型权限差异;请升级或修复该 provider adapter,使其实现 native/bridge 协议。',
|
|
81
77
|
format: 'plain',
|
|
82
78
|
};
|
|
83
79
|
}
|
|
@@ -97,16 +93,214 @@ async function requireImageCapability(llm, provider, model, signal) {
|
|
|
97
93
|
function boundedProgressText(value) {
|
|
98
94
|
return [...value].slice(0, MAX_PROGRESS_TEXT_CHARS).join('');
|
|
99
95
|
}
|
|
96
|
+
function boundedProgressToolPreview(value) {
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
const characters = [...trimmed];
|
|
99
|
+
return characters.length <= MAX_PROGRESS_TOOL_PREVIEW_CHARS
|
|
100
|
+
? trimmed
|
|
101
|
+
: `${characters.slice(0, MAX_PROGRESS_TOOL_PREVIEW_CHARS - 1).join('')}…`;
|
|
102
|
+
}
|
|
103
|
+
function redactProgressText(value) {
|
|
104
|
+
const common = redactEscapedKeyProgressAssignments(value)
|
|
105
|
+
.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/giu, '[REDACTED PRIVATE KEY]')
|
|
106
|
+
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s@]+@/giu, '$1[REDACTED]@')
|
|
107
|
+
.replace(/\b((?:proxy-)?authorization\s*[:=]\s*)(?:basic|bearer)\s+[^\s,;]+/giu, '$1[REDACTED]');
|
|
108
|
+
return redactProgressAssignments(common)
|
|
109
|
+
.replace(/\b(?:github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|(?:sk|xai)-[A-Za-z0-9_-]{8,})\b/gu, REDACTED_PROGRESS_VALUE)
|
|
110
|
+
.replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/gu, REDACTED_PROGRESS_VALUE);
|
|
111
|
+
}
|
|
112
|
+
function isSensitiveProgressKey(key) {
|
|
113
|
+
const words = key.normalize('NFKC')
|
|
114
|
+
.replace(/([a-z\d])([A-Z])/gu, '$1_$2')
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.split(/[^a-z\d]+/gu)
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
const joined = words.join('');
|
|
119
|
+
return words.some(word => [
|
|
120
|
+
'auth', 'authorization', 'bearer', 'cookie', 'cookies', 'credential', 'credentials',
|
|
121
|
+
'password', 'passwd', 'secret', 'session', 'token',
|
|
122
|
+
].includes(word))
|
|
123
|
+
|| ['apikey', 'accesskey', 'privatekey'].some(phrase => joined.includes(phrase))
|
|
124
|
+
|| words.some((word, index) => ['api', 'access', 'private'].includes(word) && words[index + 1] === 'key');
|
|
125
|
+
}
|
|
100
126
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
127
|
+
* Text credentials have no trustworthy terminator: quotes may be malformed or multiply escaped,
|
|
128
|
+
* while spaces, semicolons, and nested assignments can still belong to the value. Once a key is
|
|
129
|
+
* recognized, fail closed through the logical line instead of exposing an arbitrary suffix.
|
|
130
|
+
*/
|
|
131
|
+
function redactProgressAssignments(value) {
|
|
132
|
+
let retainedIndex = 0;
|
|
133
|
+
let redacted = '';
|
|
134
|
+
PROGRESS_ASSIGNMENT_START.lastIndex = 0;
|
|
135
|
+
for (let match = PROGRESS_ASSIGNMENT_START.exec(value); match !== null; match = PROGRESS_ASSIGNMENT_START.exec(value)) {
|
|
136
|
+
const key = match[3];
|
|
137
|
+
if (key === undefined || !isSensitiveProgressKey(key)) {
|
|
138
|
+
// The prefix regex consumes the separator of an innocent outer field. Resume inside that
|
|
139
|
+
// match so nested assignments on the same line remain visible to this scanner.
|
|
140
|
+
PROGRESS_ASSIGNMENT_START.lastIndex = match.index + 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const valueStart = match.index + match[0].length;
|
|
144
|
+
const lineEnd = progressRedactionEnd(value, valueStart);
|
|
145
|
+
redacted += value.slice(retainedIndex, valueStart) + REDACTED_PROGRESS_VALUE;
|
|
146
|
+
retainedIndex = lineEnd;
|
|
147
|
+
PROGRESS_ASSIGNMENT_START.lastIndex = lineEnd;
|
|
148
|
+
}
|
|
149
|
+
PROGRESS_ASSIGNMENT_START.lastIndex = 0;
|
|
150
|
+
return redacted + value.slice(retainedIndex);
|
|
151
|
+
}
|
|
152
|
+
/** Escaped JSON keys inside a log string cannot be parsed as the outer text; fail closed by line. */
|
|
153
|
+
function redactEscapedKeyProgressAssignments(value) {
|
|
154
|
+
let retainedIndex = 0;
|
|
155
|
+
let redacted = '';
|
|
156
|
+
ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.lastIndex = 0;
|
|
157
|
+
for (let match = ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.exec(value); match !== null; match = ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.exec(value)) {
|
|
158
|
+
const key = match[3];
|
|
159
|
+
if (key === undefined || !isSensitiveProgressKey(key)) {
|
|
160
|
+
ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.lastIndex = match.index + 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const valueStart = match.index + match[0].length;
|
|
164
|
+
const lineEnd = progressRedactionEnd(value, valueStart);
|
|
165
|
+
redacted += value.slice(retainedIndex, valueStart) + REDACTED_PROGRESS_VALUE;
|
|
166
|
+
retainedIndex = lineEnd;
|
|
167
|
+
ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.lastIndex = lineEnd;
|
|
168
|
+
}
|
|
169
|
+
ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.lastIndex = 0;
|
|
170
|
+
return redacted + value.slice(retainedIndex);
|
|
171
|
+
}
|
|
172
|
+
function progressRedactionEnd(value, start) {
|
|
173
|
+
let cursor = start;
|
|
174
|
+
while (cursor < value.length) {
|
|
175
|
+
const carriageReturn = value.indexOf('\r', cursor);
|
|
176
|
+
const lineFeed = value.indexOf('\n', cursor);
|
|
177
|
+
const candidates = [carriageReturn, lineFeed].filter(index => index >= 0);
|
|
178
|
+
if (candidates.length === 0)
|
|
179
|
+
return value.length;
|
|
180
|
+
const boundary = Math.min(...candidates);
|
|
181
|
+
if (value[boundary - 1] !== '\\')
|
|
182
|
+
return boundary;
|
|
183
|
+
cursor = boundary + (value[boundary] === '\r' && value[boundary + 1] === '\n' ? 2 : 1);
|
|
184
|
+
}
|
|
185
|
+
return value.length;
|
|
186
|
+
}
|
|
187
|
+
function redactProgressJson(value, state, depth = 0) {
|
|
188
|
+
state.nodes += 1;
|
|
189
|
+
if (depth > MAX_PROGRESS_JSON_DEPTH || state.nodes > MAX_PROGRESS_JSON_NODES) {
|
|
190
|
+
return TRUNCATED_PROGRESS_VALUE;
|
|
191
|
+
}
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
const items = value.slice(0, 100).map(item => redactProgressJson(item, state, depth + 1));
|
|
194
|
+
if (value.length > items.length)
|
|
195
|
+
items.push(TRUNCATED_PROGRESS_VALUE);
|
|
196
|
+
return items;
|
|
197
|
+
}
|
|
198
|
+
if (value !== null && typeof value === 'object') {
|
|
199
|
+
const entries = Object.entries(value).slice(0, 100).map(([key, item]) => [
|
|
200
|
+
key,
|
|
201
|
+
isSensitiveProgressKey(key)
|
|
202
|
+
? REDACTED_PROGRESS_VALUE
|
|
203
|
+
: redactProgressJson(item, state, depth + 1),
|
|
204
|
+
]);
|
|
205
|
+
if (Object.keys(value).length > entries.length)
|
|
206
|
+
entries.push(['…', TRUNCATED_PROGRESS_VALUE]);
|
|
207
|
+
return Object.fromEntries(entries);
|
|
208
|
+
}
|
|
209
|
+
return typeof value === 'string' ? redactProgressText(value) : value;
|
|
210
|
+
}
|
|
211
|
+
function progressArgumentsPlaceholder(value) {
|
|
212
|
+
const characters = [...redactProgressText(value.slice(0, MAX_PROGRESS_JSON_INPUT_CHARS))];
|
|
213
|
+
let retained = Math.min(characters.length, MAX_PROGRESS_TOOL_PREVIEW_CHARS);
|
|
214
|
+
while (retained >= 0) {
|
|
215
|
+
const encoded = JSON.stringify({
|
|
216
|
+
truncated: true,
|
|
217
|
+
preview: characters.slice(0, retained).join(''),
|
|
218
|
+
});
|
|
219
|
+
if ([...encoded].length <= MAX_PROGRESS_TOOL_PREVIEW_CHARS)
|
|
220
|
+
return encoded;
|
|
221
|
+
retained = retained === 0 ? -1 : Math.floor(retained * 0.75);
|
|
222
|
+
}
|
|
223
|
+
return '{"truncated":true}';
|
|
224
|
+
}
|
|
225
|
+
function progressArgumentsPreview(value) {
|
|
226
|
+
if (value.trim() === '')
|
|
227
|
+
return '{}';
|
|
228
|
+
// Never publish raw malformed or oversized model output. Normal tool calls are valid JSON; when
|
|
229
|
+
// that invariant fails, a status marker is more useful than a best-effort secret scrubber.
|
|
230
|
+
if (value.length > MAX_PROGRESS_JSON_INPUT_CHARS)
|
|
231
|
+
return '{"truncated":true}';
|
|
232
|
+
try {
|
|
233
|
+
const encoded = JSON.stringify(redactProgressJson(JSON.parse(value), { nodes: 0 }), null, 2);
|
|
234
|
+
if (encoded === undefined)
|
|
235
|
+
return '{"invalidJson":true}';
|
|
236
|
+
return [...encoded].length <= MAX_PROGRESS_TOOL_PREVIEW_CHARS
|
|
237
|
+
? encoded
|
|
238
|
+
: progressArgumentsPlaceholder(encoded);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return '{"invalidJson":true}';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function redactProgressResultText(value) {
|
|
245
|
+
if (value.length > MAX_PROGRESS_JSON_INPUT_CHARS)
|
|
246
|
+
return '{"truncated":true}';
|
|
247
|
+
const trimmed = value.trim();
|
|
248
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}'))
|
|
249
|
+
|| (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
250
|
+
try {
|
|
251
|
+
const encoded = JSON.stringify(redactProgressJson(JSON.parse(trimmed), { nodes: 0 }), null, 2);
|
|
252
|
+
if (encoded !== undefined)
|
|
253
|
+
return encoded;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Fall through to conservative line-oriented handling for JSON-like logs and fragments.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return redactProgressText(value);
|
|
260
|
+
}
|
|
261
|
+
function progressResultPreview(content, depth = 0) {
|
|
262
|
+
if (depth > 4)
|
|
263
|
+
return TRUNCATED_PROGRESS_VALUE;
|
|
264
|
+
const parts = [];
|
|
265
|
+
let approximateLength = 0;
|
|
266
|
+
const append = (value) => {
|
|
267
|
+
if (approximateLength >= MAX_PROGRESS_TOOL_PREVIEW_CHARS * 2 || value === '')
|
|
268
|
+
return;
|
|
269
|
+
const retained = value.slice(0, MAX_PROGRESS_TOOL_PREVIEW_CHARS * 2 - approximateLength);
|
|
270
|
+
parts.push(retained);
|
|
271
|
+
approximateLength += retained.length;
|
|
272
|
+
};
|
|
273
|
+
let blockCount = 0;
|
|
274
|
+
for (const block of content) {
|
|
275
|
+
if (approximateLength >= MAX_PROGRESS_TOOL_PREVIEW_CHARS * 2)
|
|
276
|
+
break;
|
|
277
|
+
if (blockCount >= MAX_PROGRESS_RESULT_BLOCKS) {
|
|
278
|
+
append(TRUNCATED_PROGRESS_VALUE);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
blockCount += 1;
|
|
282
|
+
if (block.type === 'text')
|
|
283
|
+
append(redactProgressResultText(block.text));
|
|
284
|
+
else if (block.type === 'image')
|
|
285
|
+
append('[图片]');
|
|
286
|
+
else if (block.type === 'tool-call') {
|
|
287
|
+
append(`${block.name}\n${progressArgumentsPreview(block.arguments)}`);
|
|
288
|
+
}
|
|
289
|
+
else if (block.type === 'tool-result') {
|
|
290
|
+
append(progressResultPreview(block.content, depth + 1));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return boundedProgressToolPreview(redactProgressText(parts.join('\n')));
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Convert one durable session fact into a bounded, user-visible progress update.
|
|
297
|
+
* Reasoning/thinking content and provider error messages never cross this boundary. Tool calls/results
|
|
298
|
+
* contribute bounded previews with common credential shapes redacted; the channel still decides
|
|
299
|
+
* whether its current audience is private enough to render those previews.
|
|
103
300
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* only, and the ACP thought channel is not mapped to DSH reasoning yet. `step/start` therefore also
|
|
108
|
-
* yields a neutral phase label, so a turn reports progress even on providers that never reason
|
|
109
|
-
* out loud.
|
|
301
|
+
* A DSH `reasoning` block is explicitly thinking content and is not guaranteed to be a public
|
|
302
|
+
* summary, even after assembly into `assistant/message`. `step/start` therefore provides the only
|
|
303
|
+
* reasoning-independent phase label, so a turn reports progress without exposing hidden thought.
|
|
110
304
|
*/
|
|
111
305
|
export function deliveryProgressFromSessionEvent(event) {
|
|
112
306
|
if (event.type === 'step/start') {
|
|
@@ -117,31 +311,39 @@ export function deliveryProgressFromSessionEvent(event) {
|
|
|
117
311
|
};
|
|
118
312
|
}
|
|
119
313
|
if (event.type === 'assistant/message') {
|
|
120
|
-
|
|
121
|
-
.filter(value => value.type === 'reasoning')
|
|
122
|
-
.map(value => (value.type === 'reasoning' ? value.text.trim() : ''))
|
|
123
|
-
.filter(text => text !== '')
|
|
124
|
-
.join('\n');
|
|
125
|
-
return reasoning === '' ? undefined : { kind: 'step', text: boundedProgressText(reasoning) };
|
|
314
|
+
return undefined;
|
|
126
315
|
}
|
|
127
316
|
if (event.type === 'tool/call') {
|
|
317
|
+
const argumentsPreview = progressArgumentsPreview(event.data.arguments);
|
|
128
318
|
return {
|
|
129
319
|
kind: 'tool-started',
|
|
130
320
|
callId: String(event.data.callId),
|
|
131
321
|
toolName: boundedProgressText(event.data.name),
|
|
322
|
+
...(argumentsPreview === '' ? {} : { argumentsPreview }),
|
|
132
323
|
};
|
|
133
324
|
}
|
|
134
325
|
if (event.type === 'tool/result') {
|
|
326
|
+
// Replacement copies belong only to the model-visible surface (for example after compaction).
|
|
327
|
+
// Human progress is an append-origin transcript, so projecting a replacement would make an
|
|
328
|
+
// old tool result look like a tool invocation from the active turn.
|
|
329
|
+
if (!isAppendSurfaceEvent(event))
|
|
330
|
+
return undefined;
|
|
135
331
|
const block = event.data.message.content.find(value => value.type === 'tool-result');
|
|
136
332
|
const callId = block?.type === 'tool-result'
|
|
137
333
|
? block.toolCallId
|
|
138
334
|
: event.data.message.source?.callId;
|
|
139
335
|
if (callId === undefined)
|
|
140
336
|
return undefined;
|
|
337
|
+
const resultPreview = block?.type === 'tool-result'
|
|
338
|
+
? progressResultPreview(block.content)
|
|
339
|
+
: '';
|
|
340
|
+
const code = event.data.error?.code;
|
|
141
341
|
return {
|
|
142
342
|
kind: 'tool-finished',
|
|
143
343
|
callId: String(callId),
|
|
144
344
|
failed: event.data.error !== undefined || (block?.type === 'tool-result' && block.isError === true),
|
|
345
|
+
...(resultPreview === '' ? {} : { resultPreview }),
|
|
346
|
+
...(code === undefined ? {} : { code: boundedProgressText(code) }),
|
|
145
347
|
};
|
|
146
348
|
}
|
|
147
349
|
if (event.type === 'todo/write') {
|
|
@@ -412,6 +614,17 @@ function agentSelection(route) {
|
|
|
412
614
|
: { reasoningEffort: ReasoningEffortId(route.reasoningEffort) }),
|
|
413
615
|
};
|
|
414
616
|
}
|
|
617
|
+
function errorHasCode(error, code) {
|
|
618
|
+
let current = error;
|
|
619
|
+
const visited = new Set();
|
|
620
|
+
while (typeof current === 'object' && current !== null && !visited.has(current)) {
|
|
621
|
+
visited.add(current);
|
|
622
|
+
if ('code' in current && current.code === code)
|
|
623
|
+
return true;
|
|
624
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
415
628
|
async function modelCatalog(llm, conversation, current, isDefault, eventId, expiresAt, signal) {
|
|
416
629
|
const providers = llm.listProviders();
|
|
417
630
|
const catalogs = await Promise.all(providers.map(async (provider) => {
|
|
@@ -810,6 +1023,44 @@ export class DshDeliveryRuntime {
|
|
|
810
1023
|
installModelSelection(agentCtx, { current: selected, assembled: undefined });
|
|
811
1024
|
await agentPresets?.mount(agentCtx, presetId);
|
|
812
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* DSH validates an explicit reasoning effort before calling an adapter. That is normally the
|
|
1028
|
+
* right contract, but a persisted external-channel selection can outlive a provider's live
|
|
1029
|
+
* model directory. Clear only that stale persisted effort before a prompt crosses the durable
|
|
1030
|
+
* dispatch boundary; TraeX still rechecks the newly created ACP session afterwards.
|
|
1031
|
+
*/
|
|
1032
|
+
async resolveExecutionRoute(conversation, persisted, route, signal) {
|
|
1033
|
+
if (route.reasoningEffort === undefined)
|
|
1034
|
+
return { route };
|
|
1035
|
+
const llm = this.ctx.get('llm');
|
|
1036
|
+
if (llm === undefined)
|
|
1037
|
+
throw new Error('assistant-delivery: llm service is required');
|
|
1038
|
+
try {
|
|
1039
|
+
await llm.resolveCallConfig({
|
|
1040
|
+
provider: route.provider,
|
|
1041
|
+
model: route.model,
|
|
1042
|
+
reasoningEffort: ReasoningEffortId(route.reasoningEffort),
|
|
1043
|
+
}, signal);
|
|
1044
|
+
return { route };
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
if (signal.aborted || !errorHasCode(error, 'UNSUPPORTED_REASONING_EFFORT'))
|
|
1048
|
+
throw error;
|
|
1049
|
+
const fallback = { provider: route.provider, model: route.model };
|
|
1050
|
+
// Do not turn an invalid model/default into a silent recovery. This is the same core
|
|
1051
|
+
// preflight without the stale explicit effort, still before an Agent turn is dispatched.
|
|
1052
|
+
await llm.resolveCallConfig(fallback, signal);
|
|
1053
|
+
if (persisted === undefined) {
|
|
1054
|
+
this.ctx.logger.warn(`assistant-delivery: using provider default after unsupported reasoning effort for ${route.provider}/${route.model}`);
|
|
1055
|
+
return { route: fallback };
|
|
1056
|
+
}
|
|
1057
|
+
const recovered = this.options.clearStaleModelReasoningEffort(conversation, persisted);
|
|
1058
|
+
if (!recovered.applied)
|
|
1059
|
+
return { retry: true };
|
|
1060
|
+
this.ctx.logger.warn(`assistant-delivery: cleared stale reasoning effort for ${route.provider}/${route.model} before dispatch`);
|
|
1061
|
+
return { route: toModelRoute(recovered.selection) };
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
813
1064
|
async reconcileNativeFullPermissionReviewer(session) {
|
|
814
1065
|
const result = await this.policy.reconcileNativeFullReviewer(session);
|
|
815
1066
|
if (result === 'unavailable') {
|
|
@@ -1707,9 +1958,15 @@ export class DshDeliveryRuntime {
|
|
|
1707
1958
|
if (agents === undefined || sessions === undefined) {
|
|
1708
1959
|
return { outcome: 'not-processed', failureCode: 'agent-runtime-unavailable', retryable: true };
|
|
1709
1960
|
}
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1961
|
+
const persistedSelection = this.options.getModelSelection(envelope.conversation);
|
|
1962
|
+
const requestedRoute = toModelRoute(persistedSelection
|
|
1963
|
+
?? prepared?.modelRoute
|
|
1712
1964
|
?? { provider: this.options.provider, model: this.options.model });
|
|
1965
|
+
const resolvedRoute = await this.resolveExecutionRoute(envelope.conversation, persistedSelection, requestedRoute, signal);
|
|
1966
|
+
if ('retry' in resolvedRoute) {
|
|
1967
|
+
return { outcome: 'not-processed', failureCode: 'model-selection-changed-before-dispatch', retryable: true };
|
|
1968
|
+
}
|
|
1969
|
+
const selectedRoute = resolvedRoute.route;
|
|
1713
1970
|
const selected = agentSelection(selectedRoute);
|
|
1714
1971
|
let handle;
|
|
1715
1972
|
let dispatched = false;
|
|
@@ -1743,7 +2000,7 @@ export class DshDeliveryRuntime {
|
|
|
1743
2000
|
const llm = this.ctx.get('llm');
|
|
1744
2001
|
if (llm === undefined)
|
|
1745
2002
|
throw new Error('assistant-delivery: llm service is required');
|
|
1746
|
-
|
|
2003
|
+
requireAdapterToolCallProtocol(llm, selected.provider, selected.model, presetId, agentCtx.tools.schemas(agentCtx.agent).length);
|
|
1747
2004
|
} });
|
|
1748
2005
|
const agent = handle.agent;
|
|
1749
2006
|
await this.reconcileNativeFullPermissionReviewer(agent.session);
|
|
@@ -1843,13 +2100,13 @@ export class DshDeliveryRuntime {
|
|
|
1843
2100
|
return { outcome: 'not-processed', failureCode: 'image-capability-notice-failed', retryable: true };
|
|
1844
2101
|
}
|
|
1845
2102
|
}
|
|
1846
|
-
if (error instanceof
|
|
2103
|
+
if (error instanceof AdapterToolCallProtocolError) {
|
|
1847
2104
|
try {
|
|
1848
|
-
this.options.replyCommand(binding, envelope.eventId,
|
|
2105
|
+
this.options.replyCommand(binding, envelope.eventId, adapterToolCallProtocolReply(error));
|
|
1849
2106
|
return { outcome: 'processed' };
|
|
1850
2107
|
}
|
|
1851
2108
|
catch {
|
|
1852
|
-
return { outcome: 'not-processed', failureCode: 'tool-
|
|
2109
|
+
return { outcome: 'not-processed', failureCode: 'adapter-tool-protocol-notice-failed', retryable: true };
|
|
1853
2110
|
}
|
|
1854
2111
|
}
|
|
1855
2112
|
this.ctx.logger.warn(`assistant-delivery: ${sessionResumeFailureCode(error)} for session ${sessionFingerprint(binding.sessionId)}`);
|