@winmatrix/agent-sdk 1.0.4 → 1.0.6
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/dist/adapters/claude/ClaudeAdapter.d.ts +71 -1
- package/dist/adapters/claude/ClaudeAdapter.d.ts.map +1 -1
- package/dist/adapters/claude/ClaudeAdapter.js +490 -86
- package/dist/adapters/claude/ClaudeAdapter.js.map +1 -1
- package/dist/adapters/claude/ClaudeInvocationUsageTracker.d.ts +17 -0
- package/dist/adapters/claude/ClaudeInvocationUsageTracker.d.ts.map +1 -0
- package/dist/adapters/claude/ClaudeInvocationUsageTracker.js +79 -0
- package/dist/adapters/claude/ClaudeInvocationUsageTracker.js.map +1 -0
- package/dist/adapters/claude/ClaudeStreamParser.d.ts +19 -0
- package/dist/adapters/claude/ClaudeStreamParser.d.ts.map +1 -1
- package/dist/adapters/claude/ClaudeStreamParser.js +92 -0
- package/dist/adapters/claude/ClaudeStreamParser.js.map +1 -1
- package/dist/adapters/codex/CodexAdapter.d.ts +15 -1
- package/dist/adapters/codex/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/codex/CodexAdapter.js +164 -14
- package/dist/adapters/codex/CodexAdapter.js.map +1 -1
- package/dist/adapters/codex/CodexSessionStore.d.ts +42 -0
- package/dist/adapters/codex/CodexSessionStore.d.ts.map +1 -0
- package/dist/adapters/codex/CodexSessionStore.js +279 -0
- package/dist/adapters/codex/CodexSessionStore.js.map +1 -0
- package/dist/adapters/hermes/HermesAdapter.d.ts +10 -1
- package/dist/adapters/hermes/HermesAdapter.d.ts.map +1 -1
- package/dist/adapters/hermes/HermesAdapter.js +177 -10
- package/dist/adapters/hermes/HermesAdapter.js.map +1 -1
- package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts +26 -0
- package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts.map +1 -0
- package/dist/adapters/hermes/HermesInvocationUsageReader.js +128 -0
- package/dist/adapters/hermes/HermesInvocationUsageReader.js.map +1 -0
- package/dist/adapters/openclaw/OpenClawAdapter.d.ts +3 -7
- package/dist/adapters/openclaw/OpenClawAdapter.d.ts.map +1 -1
- package/dist/adapters/openclaw/OpenClawAdapter.js +116 -45
- package/dist/adapters/openclaw/OpenClawAdapter.js.map +1 -1
- package/dist/core/AgentAdapter.d.ts +51 -2
- package/dist/core/AgentAdapter.d.ts.map +1 -1
- package/dist/core/AgentAdapter.js.map +1 -1
- package/dist/core/WinMatrixAgentClient.d.ts +23 -1
- package/dist/core/WinMatrixAgentClient.d.ts.map +1 -1
- package/dist/core/WinMatrixAgentClient.js +115 -12
- package/dist/core/WinMatrixAgentClient.js.map +1 -1
- package/dist/index.d.ts +18 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -13
- package/dist/index.js.map +1 -1
- package/dist/observability/WorkstationTokenUsageRecorder.d.ts +54 -0
- package/dist/observability/WorkstationTokenUsageRecorder.d.ts.map +1 -0
- package/dist/observability/WorkstationTokenUsageRecorder.js +264 -0
- package/dist/observability/WorkstationTokenUsageRecorder.js.map +1 -0
- package/dist/utils/contextBudget.d.ts +27 -0
- package/dist/utils/contextBudget.d.ts.map +1 -0
- package/dist/utils/contextBudget.js +86 -0
- package/dist/utils/contextBudget.js.map +1 -0
- package/dist/utils/envSanitizer.d.ts +38 -0
- package/dist/utils/envSanitizer.d.ts.map +1 -0
- package/dist/utils/envSanitizer.js +94 -0
- package/dist/utils/envSanitizer.js.map +1 -0
- package/dist/utils/errorClassification.d.ts +26 -0
- package/dist/utils/errorClassification.d.ts.map +1 -0
- package/dist/utils/errorClassification.js +52 -0
- package/dist/utils/errorClassification.js.map +1 -0
- package/dist/utils/mcpUrlValidation.d.ts +10 -0
- package/dist/utils/mcpUrlValidation.d.ts.map +1 -0
- package/dist/utils/mcpUrlValidation.js +18 -0
- package/dist/utils/mcpUrlValidation.js.map +1 -0
- package/dist/utils/processUtils.d.ts +10 -0
- package/dist/utils/processUtils.d.ts.map +1 -1
- package/dist/utils/processUtils.js +42 -9
- package/dist/utils/processUtils.js.map +1 -1
- package/dist/utils/promptGuardrails.d.ts +15 -0
- package/dist/utils/promptGuardrails.d.ts.map +1 -0
- package/dist/utils/promptGuardrails.js +26 -0
- package/dist/utils/promptGuardrails.js.map +1 -0
- package/dist/utils/resumeFallback.d.ts.map +1 -1
- package/dist/utils/resumeFallback.js +39 -1
- package/dist/utils/resumeFallback.js.map +1 -1
- package/package.json +3 -4
|
@@ -7,12 +7,19 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFile
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
10
11
|
import { parseSkillMd } from '../../utils/parseSkillMd.js';
|
|
11
12
|
import { waitForExit, killProcess } from '../../utils/processUtils.js';
|
|
13
|
+
import { sanitizeTaskEnv } from '../../utils/envSanitizer.js';
|
|
12
14
|
import { executeWithResumeFallback } from '../../utils/resumeFallback.js';
|
|
15
|
+
import { validateMcpBridgeUrl } from '../../utils/mcpUrlValidation.js';
|
|
16
|
+
import { withSecurityPrefix } from '../../utils/promptGuardrails.js';
|
|
17
|
+
import { classifyAdapterError } from '../../utils/errorClassification.js';
|
|
18
|
+
import { applyContextBudget } from '../../utils/contextBudget.js';
|
|
13
19
|
export { parseSkillMd };
|
|
14
20
|
import { isLocalTool } from '../../core/AgentAdapter.js';
|
|
15
|
-
import { parseClaudeLine, splitLines, } from './ClaudeStreamParser.js';
|
|
21
|
+
import { parseClaudeLine, parseClaudeInvocationUsageFrame, splitLines, } from './ClaudeStreamParser.js';
|
|
22
|
+
import { ClaudeInvocationUsageTracker } from './ClaudeInvocationUsageTracker.js';
|
|
16
23
|
import { resolveClaudeCommand } from './claudeCommand.js';
|
|
17
24
|
const CLAUDE_BASE_ARGS = [
|
|
18
25
|
'--print',
|
|
@@ -21,6 +28,20 @@ const CLAUDE_BASE_ARGS = [
|
|
|
21
28
|
'--output-format', 'stream-json',
|
|
22
29
|
'--include-partial-messages',
|
|
23
30
|
];
|
|
31
|
+
/**
|
|
32
|
+
* 权限默认档(harden-external-computer-daemon P1/D4)。
|
|
33
|
+
*
|
|
34
|
+
* 优先级:task 显式声明 > daemon 环境变量 > 'acceptEdits'(安全默认)。
|
|
35
|
+
* 交互确认通道(TASK_INTERACTION_RESPOND)当前未接通,print 模式下权限
|
|
36
|
+
* 提示会被自动拒绝——acceptEdits 下 Bash 等未预批工具将被拒;需要旧行为
|
|
37
|
+
* 的部署显式设 WINMATRIX_DAEMON_CLAUDE_PERMISSION_MODE=bypassPermissions,
|
|
38
|
+
* 或在任务配置里声明 permissionMode/allowedTools。
|
|
39
|
+
*/
|
|
40
|
+
function resolvePermissionMode(task) {
|
|
41
|
+
return task.claude?.permissionMode
|
|
42
|
+
?? process.env.WINMATRIX_DAEMON_CLAUDE_PERMISSION_MODE
|
|
43
|
+
?? 'acceptEdits';
|
|
44
|
+
}
|
|
24
45
|
export class ClaudeAdapter {
|
|
25
46
|
meta = {
|
|
26
47
|
agentType: 'claude-code',
|
|
@@ -45,25 +66,63 @@ export class ClaudeAdapter {
|
|
|
45
66
|
claudePath;
|
|
46
67
|
mcpBridgeUrl;
|
|
47
68
|
mcpApiKey;
|
|
69
|
+
/** steer 递话队列与当前 run 的 stdin 句柄(P2.1/D5) */
|
|
70
|
+
steerQueue = [];
|
|
71
|
+
activeStdin = null;
|
|
72
|
+
workstationId;
|
|
73
|
+
tokenUsageRecorder;
|
|
48
74
|
mcpConfigWritten = new Map();
|
|
49
75
|
constructor(config) {
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
76
|
+
const options = config;
|
|
77
|
+
this.claudePath = options?.claudePath ?? 'claude';
|
|
78
|
+
this.mcpBridgeUrl = options?.mcpBridgeUrl;
|
|
79
|
+
this.mcpApiKey = options?.mcpApiKey;
|
|
80
|
+
this.workstationId = options?.workstationId;
|
|
81
|
+
this.tokenUsageRecorder = options?.tokenUsageRecorder;
|
|
53
82
|
}
|
|
54
83
|
// ── execute ──
|
|
55
84
|
async *execute(task, signal) {
|
|
56
85
|
yield* executeWithResumeFallback(task, signal, (nextTask, nextSignal) => this.executeOnce(nextTask, nextSignal));
|
|
57
86
|
}
|
|
58
87
|
async *executeOnce(task, signal) {
|
|
88
|
+
// Pod 场景:通过 @anthropic-ai/claude-agent-sdk.query() 调 claude
|
|
89
|
+
if (task.claude?.useAgentSdk) {
|
|
90
|
+
yield* this.executeViaAgentSdk(task, signal);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
59
93
|
const workDir = task.workDir;
|
|
60
94
|
if (workDir && !existsSync(workDir)) {
|
|
61
|
-
yield {
|
|
95
|
+
yield {
|
|
96
|
+
type: 'error',
|
|
97
|
+
message: `Work directory does not exist: ${workDir}`,
|
|
98
|
+
code: 'cli_crash',
|
|
99
|
+
retryable: false,
|
|
100
|
+
};
|
|
62
101
|
return;
|
|
63
102
|
}
|
|
103
|
+
const budgeted = applyContextBudget({
|
|
104
|
+
...task,
|
|
105
|
+
systemPrompt: task.systemPrompt ?? task.claude?.systemPrompt,
|
|
106
|
+
});
|
|
107
|
+
if (budgeted.diagnostics) {
|
|
108
|
+
const d = budgeted.diagnostics;
|
|
109
|
+
yield {
|
|
110
|
+
type: 'thinking',
|
|
111
|
+
content: `[ContextBudget] input ${d.originalInputChars}→${d.finalInputChars} chars (${d.compression})`,
|
|
112
|
+
engineEvent: {
|
|
113
|
+
type: 'status',
|
|
114
|
+
text: `[ContextBudget] input ${d.originalInputChars}→${d.finalInputChars} chars (${d.compression})`,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
64
118
|
const args = [...CLAUDE_BASE_ARGS];
|
|
65
|
-
// Permission mode
|
|
66
|
-
|
|
119
|
+
// Permission mode(harden-external-computer-daemon P1/D4:默认档降为 acceptEdits)
|
|
120
|
+
const permissionMode = resolvePermissionMode(task);
|
|
121
|
+
if (permissionMode === 'bypassPermissions') {
|
|
122
|
+
console.warn('[ClaudeAdapter] 本次任务以 bypassPermissions 运行(显式声明)。' +
|
|
123
|
+
'外部 CLI 将不做任何权限确认,请确认任务来源可信。');
|
|
124
|
+
}
|
|
125
|
+
args.push('--permission-mode', permissionMode);
|
|
67
126
|
// workDir
|
|
68
127
|
if (workDir) {
|
|
69
128
|
args.push('--add-dir', workDir);
|
|
@@ -89,8 +148,11 @@ export class ClaudeAdapter {
|
|
|
89
148
|
args.push('--session-id', sessionId);
|
|
90
149
|
}
|
|
91
150
|
}
|
|
92
|
-
// Local tools (whitelist)
|
|
93
|
-
|
|
151
|
+
// Local tools (whitelist)。CLI 默认须与 Agent SDK 路径对齐:含 Skill + mcp__*,
|
|
152
|
+
// 否则 print/acceptEdits 下 WinMatrix MCP 会全部变成 "haven't granted it yet"。
|
|
153
|
+
const tools = task.claude?.tools?.length
|
|
154
|
+
? task.claude.tools
|
|
155
|
+
: ['Skill', 'Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'mcp__*'];
|
|
94
156
|
args.push('--allowedTools', tools.join(','));
|
|
95
157
|
// Disallowed tools
|
|
96
158
|
if (task.claude?.disallowedTools?.length) {
|
|
@@ -101,14 +163,14 @@ export class ClaudeAdapter {
|
|
|
101
163
|
args.push('--max-budget-usd', String(task.claude.maxBudgetUsd));
|
|
102
164
|
}
|
|
103
165
|
// System prompt: task.systemPrompt 优先�?task.claude.systemPrompt
|
|
104
|
-
const effectiveSystemPrompt =
|
|
166
|
+
const effectiveSystemPrompt = budgeted.systemPrompt ?? task.claude?.systemPrompt;
|
|
105
167
|
if (effectiveSystemPrompt) {
|
|
106
168
|
args.push('--append-system-prompt', effectiveSystemPrompt);
|
|
107
169
|
}
|
|
108
|
-
// MCP Bridge 配置:在 workDir 下生成 .claude/mcp.json,让 Claude Code 能调用 WinMatrix
|
|
109
|
-
//
|
|
170
|
+
// MCP Bridge 配置:在 workDir 下生成 .claude/mcp.json,让 Claude Code 能调用 WinMatrix 工具。
|
|
171
|
+
// 任务 token 已由服务端签发,必须优先使用,不能因集群内短服务名启发式而回退。
|
|
110
172
|
let mcpConfigFile;
|
|
111
|
-
if (this.mcpBridgeUrl && this.isMcpBridgeUrlLikelyReachable()) {
|
|
173
|
+
if (this.mcpBridgeUrl && (task.mcpToken || this.isMcpBridgeUrlLikelyReachable())) {
|
|
112
174
|
const mcpArgs = this.generateMcpConfig(workDir, task.mcpToken);
|
|
113
175
|
if (mcpArgs) {
|
|
114
176
|
args.push(...mcpArgs);
|
|
@@ -121,8 +183,11 @@ export class ClaudeAdapter {
|
|
|
121
183
|
else if (this.mcpBridgeUrl) {
|
|
122
184
|
console.warn(`[ClaudeAdapter] Skipping MCP config: URL likely unreachable: ${this.mcpBridgeUrl}`);
|
|
123
185
|
}
|
|
124
|
-
// Build env
|
|
125
|
-
const
|
|
186
|
+
// Build env(先剥凭据后显式加,harden-external-computer-daemon P1/D3)
|
|
187
|
+
const { env, strippedKeys } = sanitizeTaskEnv(task.env);
|
|
188
|
+
if (strippedKeys.length > 0) {
|
|
189
|
+
console.warn(`[ClaudeAdapter] 已剥离 ${strippedKeys.length} 个凭据形态环境变量(仅列 key 名): ${strippedKeys.join(', ')}`);
|
|
190
|
+
}
|
|
126
191
|
// Spawn
|
|
127
192
|
let proc;
|
|
128
193
|
try {
|
|
@@ -132,10 +197,17 @@ export class ClaudeAdapter {
|
|
|
132
197
|
env,
|
|
133
198
|
cwd: workDir ?? process.cwd(),
|
|
134
199
|
shell: spawnSpec.shell,
|
|
200
|
+
// POSIX 下独立进程组,killProcess 才能 kill(-pid) 杀整棵树
|
|
201
|
+
detached: process.platform !== 'win32',
|
|
135
202
|
});
|
|
136
203
|
}
|
|
137
204
|
catch (err) {
|
|
138
|
-
yield {
|
|
205
|
+
yield {
|
|
206
|
+
type: 'error',
|
|
207
|
+
message: `Failed to spawn claude: ${err instanceof Error ? err.message : String(err)}`,
|
|
208
|
+
code: 'cli_crash',
|
|
209
|
+
retryable: false,
|
|
210
|
+
};
|
|
139
211
|
return;
|
|
140
212
|
}
|
|
141
213
|
// Send user message via stdin (stream-json protocol)
|
|
@@ -145,10 +217,12 @@ export class ClaudeAdapter {
|
|
|
145
217
|
});
|
|
146
218
|
const userMessage = JSON.stringify({
|
|
147
219
|
type: 'user',
|
|
148
|
-
message: { role: 'user', content:
|
|
220
|
+
message: { role: 'user', content: withSecurityPrefix(budgeted.input) },
|
|
149
221
|
}) + '\n';
|
|
150
222
|
proc.stdin?.write(userMessage);
|
|
151
|
-
|
|
223
|
+
// steer(P2.1/D5):stdin 保持打开,turn 结束(result 帧)时若有排队递话
|
|
224
|
+
// 则注入下一 turn;无排队才关闭 stdin 让 CLI 退出
|
|
225
|
+
this.activeStdin = proc.stdin ?? null;
|
|
152
226
|
// Drain stderr to prevent backpressure stall, while keeping a bounded error summary.
|
|
153
227
|
let stderrText = '';
|
|
154
228
|
proc.stderr?.on('data', (chunk) => {
|
|
@@ -162,6 +236,7 @@ export class ClaudeAdapter {
|
|
|
162
236
|
let extractedResultText;
|
|
163
237
|
let extractedUsage;
|
|
164
238
|
let completed = false;
|
|
239
|
+
const invocationUsageTracker = new ClaudeInvocationUsageTracker();
|
|
165
240
|
// Delta text accumulator: fallback for empty/missing result frame
|
|
166
241
|
let accumulatedDeltaText = '';
|
|
167
242
|
// Abort handler
|
|
@@ -180,9 +255,39 @@ export class ClaudeAdapter {
|
|
|
180
255
|
const { lines, remainder } = splitLines(stdoutBuffer, text);
|
|
181
256
|
stdoutBuffer = remainder;
|
|
182
257
|
for (const line of lines) {
|
|
258
|
+
const observedAt = new Date().toISOString();
|
|
259
|
+
const usageFrame = parseClaudeInvocationUsageFrame(line);
|
|
260
|
+
if (usageFrame && this.tokenUsageRecorder) {
|
|
261
|
+
const completedUsage = invocationUsageTracker.accept(usageFrame, observedAt);
|
|
262
|
+
if (completedUsage) {
|
|
263
|
+
const workstationId = this.workstationId ?? task.env?.WINMATRIX_WORKSTATION_ID ?? process.env.WINMATRIX_WORKSTATION_ID;
|
|
264
|
+
if (!workstationId?.trim()) {
|
|
265
|
+
throw new Error('WORKSTATION_TOKEN_USAGE_WORKSTATION_ID_MISSING');
|
|
266
|
+
}
|
|
267
|
+
await this.tokenUsageRecorder.recordCompletedInvocation({
|
|
268
|
+
workstationId,
|
|
269
|
+
runtime: 'claude-code',
|
|
270
|
+
sourceInvocationId: completedUsage.sourceInvocationId,
|
|
271
|
+
provider: 'claude',
|
|
272
|
+
startedAt: completedUsage.startedAt,
|
|
273
|
+
occurredAt: completedUsage.occurredAt,
|
|
274
|
+
timeSource: completedUsage.timeSource,
|
|
275
|
+
sessionId: completedUsage.sessionId ?? extractedSessionId ?? sessionId,
|
|
276
|
+
taskId: task.taskId,
|
|
277
|
+
modelUsages: [{
|
|
278
|
+
sourceModelUsageId: `${completedUsage.sourceInvocationId}:model:0`,
|
|
279
|
+
model: completedUsage.model,
|
|
280
|
+
inputTokens: completedUsage.usage.inputTokens,
|
|
281
|
+
outputTokens: completedUsage.usage.outputTokens,
|
|
282
|
+
cacheReadTokens: completedUsage.usage.cacheReadTokens,
|
|
283
|
+
cacheCreationTokens: completedUsage.usage.cacheCreationTokens,
|
|
284
|
+
}],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
183
288
|
const action = parseClaudeLine(line);
|
|
184
289
|
if (action.type === 'tool_use' && isLocalTool(action.toolName)) {
|
|
185
|
-
yield { type: 'thinking', content:
|
|
290
|
+
yield { type: 'thinking', content: `调用本地工具: ${action.toolName}` };
|
|
186
291
|
continue;
|
|
187
292
|
}
|
|
188
293
|
switch (action.type) {
|
|
@@ -206,20 +311,22 @@ export class ClaudeAdapter {
|
|
|
206
311
|
yield { type: 'thinking', content: `调用本地工具: ${action.toolName}` };
|
|
207
312
|
}
|
|
208
313
|
else {
|
|
314
|
+
const callId = randomUUID();
|
|
209
315
|
yield {
|
|
210
316
|
type: 'tool_call',
|
|
211
|
-
callId
|
|
317
|
+
callId,
|
|
212
318
|
name: action.toolName,
|
|
213
319
|
args: action.toolInput ?? {},
|
|
214
320
|
engineEvent: {
|
|
215
321
|
type: 'tool_call',
|
|
322
|
+
callId,
|
|
216
323
|
name: action.toolName,
|
|
217
324
|
input: action.toolInput,
|
|
218
325
|
},
|
|
219
326
|
};
|
|
220
327
|
}
|
|
221
328
|
break;
|
|
222
|
-
case 'result':
|
|
329
|
+
case 'result': {
|
|
223
330
|
if (action.sessionId)
|
|
224
331
|
extractedSessionId = action.sessionId;
|
|
225
332
|
if (action.result)
|
|
@@ -230,11 +337,35 @@ export class ClaudeAdapter {
|
|
|
230
337
|
output_tokens: action.usage.output_tokens,
|
|
231
338
|
};
|
|
232
339
|
}
|
|
340
|
+
// steer:本 turn 结束。有排队递话则注入下一 turn 继续;
|
|
341
|
+
// 否则关闭 stdin,CLI 处理完退出
|
|
342
|
+
const nextSteer = this.steerQueue.shift();
|
|
343
|
+
if (nextSteer && proc.stdin && !proc.stdin.destroyed && proc.stdin.writable) {
|
|
344
|
+
accumulatedDeltaText = '';
|
|
345
|
+
proc.stdin.write(JSON.stringify({
|
|
346
|
+
type: 'user',
|
|
347
|
+
message: { role: 'user', content: nextSteer },
|
|
348
|
+
}) + '\n');
|
|
349
|
+
yield {
|
|
350
|
+
type: 'thinking',
|
|
351
|
+
content: `[steer] 已注入纠偏消息(队列剩余 ${this.steerQueue.length} 条),继续执行`,
|
|
352
|
+
engineEvent: { type: 'status', text: 'steer_applied' },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
try {
|
|
357
|
+
proc.stdin?.end();
|
|
358
|
+
}
|
|
359
|
+
catch { /* already closed */ }
|
|
360
|
+
}
|
|
233
361
|
break;
|
|
234
|
-
|
|
362
|
+
}
|
|
363
|
+
case 'error': {
|
|
235
364
|
completed = true;
|
|
236
|
-
|
|
365
|
+
const classified = classifyAdapterError({ message: action.message, aborted: signal?.aborted });
|
|
366
|
+
yield { type: 'error', message: action.message, code: classified.code, retryable: classified.retryable };
|
|
237
367
|
return;
|
|
368
|
+
}
|
|
238
369
|
case 'skip':
|
|
239
370
|
break;
|
|
240
371
|
}
|
|
@@ -245,7 +376,8 @@ export class ClaudeAdapter {
|
|
|
245
376
|
if (!completed) {
|
|
246
377
|
completed = true;
|
|
247
378
|
if (startupError) {
|
|
248
|
-
|
|
379
|
+
const classified = classifyAdapterError({ message: startupError, aborted: signal?.aborted });
|
|
380
|
+
yield { type: 'error', message: startupError, code: classified.code, retryable: classified.retryable };
|
|
249
381
|
return;
|
|
250
382
|
}
|
|
251
383
|
if (extractedSessionId || exitResult.code === 0) {
|
|
@@ -261,10 +393,13 @@ export class ClaudeAdapter {
|
|
|
261
393
|
};
|
|
262
394
|
}
|
|
263
395
|
else {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
message
|
|
267
|
-
|
|
396
|
+
const message = stderrText.trim() || exitResult.error || `Claude Code exited with code ${exitResult.code}`;
|
|
397
|
+
const classified = classifyAdapterError({
|
|
398
|
+
message,
|
|
399
|
+
exitCode: exitResult.code,
|
|
400
|
+
aborted: signal?.aborted,
|
|
401
|
+
});
|
|
402
|
+
yield { type: 'error', message, code: classified.code, retryable: classified.retryable };
|
|
268
403
|
}
|
|
269
404
|
}
|
|
270
405
|
}
|
|
@@ -277,7 +412,25 @@ export class ClaudeAdapter {
|
|
|
277
412
|
}
|
|
278
413
|
catch { /* best-effort */ }
|
|
279
414
|
}
|
|
415
|
+
this.activeStdin = null;
|
|
416
|
+
this.steerQueue = [];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* 中途递话(P2.1/D5):将纠偏消息排队,当前 turn 结束(result 帧)时注入
|
|
421
|
+
* stdin 开启下一 turn(dsh steer 语义的 stream-json 落地——stream-json 协议
|
|
422
|
+
* 在 turn 边界处理排队的 user 消息)。
|
|
423
|
+
*/
|
|
424
|
+
steer(text) {
|
|
425
|
+
const stdin = this.activeStdin;
|
|
426
|
+
if (!stdin || stdin.destroyed || !stdin.writable) {
|
|
427
|
+
return { accepted: false, reason: 'no_active_run' };
|
|
280
428
|
}
|
|
429
|
+
const trimmed = text.trim();
|
|
430
|
+
if (!trimmed)
|
|
431
|
+
return { accepted: false, reason: 'empty_text' };
|
|
432
|
+
this.steerQueue.push(trimmed);
|
|
433
|
+
return { accepted: true };
|
|
281
434
|
}
|
|
282
435
|
// ── healthCheck ──
|
|
283
436
|
async healthCheck() {
|
|
@@ -307,71 +460,81 @@ export class ClaudeAdapter {
|
|
|
307
460
|
async listSessions(options) {
|
|
308
461
|
try {
|
|
309
462
|
const dir = options.dir?.trim();
|
|
310
|
-
const
|
|
311
|
-
if (projectDirs.length === 0) {
|
|
312
|
-
return { sessions: [] };
|
|
313
|
-
}
|
|
314
|
-
// load session index from ~/.claude/sessions/
|
|
315
|
-
const sessionsIndex = this.loadSessionsIndex();
|
|
316
|
-
const entries = [];
|
|
317
|
-
const seen = new Set();
|
|
318
|
-
for (const projectDir of projectDirs) {
|
|
319
|
-
const files = readdirSync(projectDir.path, { withFileTypes: true })
|
|
320
|
-
.filter((d) => d.isFile() && d.name.endsWith('.jsonl'));
|
|
321
|
-
for (const file of files) {
|
|
322
|
-
const sessionId = file.name.replace(/\.jsonl$/, '');
|
|
323
|
-
if (seen.has(sessionId))
|
|
324
|
-
continue;
|
|
325
|
-
seen.add(sessionId);
|
|
326
|
-
const indexPath = join(projectDir.path, file.name);
|
|
327
|
-
const idxMeta = sessionsIndex.get(sessionId);
|
|
328
|
-
if (idxMeta) {
|
|
329
|
-
entries.push({
|
|
330
|
-
id: sessionId,
|
|
331
|
-
cwd: idxMeta.cwd,
|
|
332
|
-
projectPath: idxMeta.cwd ?? projectDir.projectPath,
|
|
333
|
-
summary: idxMeta.summary,
|
|
334
|
-
createdAt: idxMeta.createdAt,
|
|
335
|
-
lastActivityAt: idxMeta.lastActivityAt,
|
|
336
|
-
messageCount: idxMeta.messageCount,
|
|
337
|
-
status: idxMeta.status,
|
|
338
|
-
});
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
try {
|
|
342
|
-
const stat = readFileSync(indexPath, 'utf-8');
|
|
343
|
-
const lines = stat.trim().length > 0 ? stat.trim().split('\n') : [];
|
|
344
|
-
const firstLine = lines.length > 0 ? JSON.parse(lines[0]) : null;
|
|
345
|
-
const lastLine = lines.length > 0 ? JSON.parse(lines[lines.length - 1]) : null;
|
|
346
|
-
entries.push({
|
|
347
|
-
id: sessionId,
|
|
348
|
-
projectPath: projectDir.projectPath,
|
|
349
|
-
summary: typeof firstLine?.content === 'string'
|
|
350
|
-
? firstLine.content.slice(0, 120)
|
|
351
|
-
: undefined,
|
|
352
|
-
createdAt: firstLine?.timestamp,
|
|
353
|
-
lastActivityAt: lastLine?.timestamp,
|
|
354
|
-
messageCount: lines.length,
|
|
355
|
-
status: firstLine?.status,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
entries.push({ id: sessionId, projectPath: projectDir.projectPath });
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
463
|
+
const entries = this.collectSessionEntries(dir);
|
|
363
464
|
const limit = options.limit ?? 50;
|
|
364
|
-
entries.sort((a, b) => {
|
|
365
|
-
const da = a.lastActivityAt ?? a.createdAt ?? '';
|
|
366
|
-
const db = b.lastActivityAt ?? b.createdAt ?? '';
|
|
367
|
-
return db.localeCompare(da);
|
|
368
|
-
});
|
|
369
465
|
return { sessions: entries.slice(0, limit) };
|
|
370
466
|
}
|
|
371
467
|
catch (err) {
|
|
372
468
|
return { sessions: [], error: err instanceof Error ? err.message : String(err) };
|
|
373
469
|
}
|
|
374
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* 收集给定工作目录范围内的全部会话条目(task 6.10)。
|
|
473
|
+
*
|
|
474
|
+
* 复用 listSessions 的遍历逻辑:索引元数据优先(`~/.claude/sessions/*.json`),
|
|
475
|
+
* 缺失时回退到读取 `.jsonl` 首尾行。结果按 lastActivityAt/createdAt 倒序排列。
|
|
476
|
+
* searchSessions 与 listSessions 共享该集合以保持一致行为。
|
|
477
|
+
*/
|
|
478
|
+
collectSessionEntries(dir) {
|
|
479
|
+
const projectDirs = this.resolveProjectDirs(dir);
|
|
480
|
+
if (projectDirs.length === 0) {
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
const sessionsIndex = this.loadSessionsIndex();
|
|
484
|
+
const entries = [];
|
|
485
|
+
const seen = new Set();
|
|
486
|
+
for (const projectDir of projectDirs) {
|
|
487
|
+
const files = readdirSync(projectDir.path, { withFileTypes: true })
|
|
488
|
+
.filter((d) => d.isFile() && d.name.endsWith('.jsonl'));
|
|
489
|
+
for (const file of files) {
|
|
490
|
+
const sessionId = file.name.replace(/\.jsonl$/, '');
|
|
491
|
+
if (seen.has(sessionId))
|
|
492
|
+
continue;
|
|
493
|
+
seen.add(sessionId);
|
|
494
|
+
const indexPath = join(projectDir.path, file.name);
|
|
495
|
+
const idxMeta = sessionsIndex.get(sessionId);
|
|
496
|
+
if (idxMeta) {
|
|
497
|
+
entries.push({
|
|
498
|
+
id: sessionId,
|
|
499
|
+
cwd: idxMeta.cwd,
|
|
500
|
+
projectPath: idxMeta.cwd ?? projectDir.projectPath,
|
|
501
|
+
summary: idxMeta.summary,
|
|
502
|
+
createdAt: idxMeta.createdAt,
|
|
503
|
+
lastActivityAt: idxMeta.lastActivityAt,
|
|
504
|
+
messageCount: idxMeta.messageCount,
|
|
505
|
+
status: idxMeta.status,
|
|
506
|
+
});
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const stat = readFileSync(indexPath, 'utf-8');
|
|
511
|
+
const lines = stat.trim().length > 0 ? stat.trim().split('\n') : [];
|
|
512
|
+
const firstLine = lines.length > 0 ? JSON.parse(lines[0]) : null;
|
|
513
|
+
const lastLine = lines.length > 0 ? JSON.parse(lines[lines.length - 1]) : null;
|
|
514
|
+
entries.push({
|
|
515
|
+
id: sessionId,
|
|
516
|
+
projectPath: projectDir.projectPath,
|
|
517
|
+
summary: typeof firstLine?.content === 'string'
|
|
518
|
+
? firstLine.content.slice(0, 120)
|
|
519
|
+
: undefined,
|
|
520
|
+
createdAt: firstLine?.timestamp,
|
|
521
|
+
lastActivityAt: lastLine?.timestamp,
|
|
522
|
+
messageCount: lines.length,
|
|
523
|
+
status: firstLine?.status,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
entries.push({ id: sessionId, projectPath: projectDir.projectPath });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
entries.sort((a, b) => {
|
|
532
|
+
const da = a.lastActivityAt ?? a.createdAt ?? '';
|
|
533
|
+
const db = b.lastActivityAt ?? b.createdAt ?? '';
|
|
534
|
+
return db.localeCompare(da);
|
|
535
|
+
});
|
|
536
|
+
return entries;
|
|
537
|
+
}
|
|
375
538
|
// ── getSession ──
|
|
376
539
|
async getSession(sessionId, options = {}) {
|
|
377
540
|
try {
|
|
@@ -428,6 +591,124 @@ export class ClaudeAdapter {
|
|
|
428
591
|
return this.emptySessionResult(sessionId, err instanceof Error ? err.message : String(err));
|
|
429
592
|
}
|
|
430
593
|
}
|
|
594
|
+
// ── searchSessions(task 6.10) ──
|
|
595
|
+
/**
|
|
596
|
+
* 按可见文本字段搜索项目会话。命中 summary / customTitle / firstPrompt /
|
|
597
|
+
* cwd / projectName / projectPath(不区分大小写)。
|
|
598
|
+
*
|
|
599
|
+
* 实现为 collectSessionEntries + 内存过滤:daemon 侧结果集通常很小,
|
|
600
|
+
* 无需引擎原生搜索能力;与 listSessions 共享同一数据源保证一致性。
|
|
601
|
+
* cursor 为简单整数偏移字符串。
|
|
602
|
+
*/
|
|
603
|
+
async searchSessions(options) {
|
|
604
|
+
try {
|
|
605
|
+
const needle = options.query?.trim().toLowerCase();
|
|
606
|
+
if (!needle) {
|
|
607
|
+
return { sessions: [] };
|
|
608
|
+
}
|
|
609
|
+
const dir = options.dir?.trim();
|
|
610
|
+
const all = this.collectSessionEntries(dir);
|
|
611
|
+
const matched = all.filter((entry) => this.sessionEntryMatches(entry, needle));
|
|
612
|
+
const limit = options.limit ?? 50;
|
|
613
|
+
const start = this.cursorToOffset(options.cursor);
|
|
614
|
+
const page = matched.slice(start, start + limit);
|
|
615
|
+
const hasNext = start + limit < matched.length;
|
|
616
|
+
return {
|
|
617
|
+
sessions: page,
|
|
618
|
+
...(hasNext ? { nextCursor: String(start + limit) } : {}),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
catch (err) {
|
|
622
|
+
return { sessions: [], error: err instanceof Error ? err.message : String(err) };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
sessionEntryMatches(entry, needle) {
|
|
626
|
+
const fields = [
|
|
627
|
+
entry.summary,
|
|
628
|
+
entry.customTitle,
|
|
629
|
+
entry.firstPrompt,
|
|
630
|
+
entry.cwd,
|
|
631
|
+
entry.projectName,
|
|
632
|
+
entry.projectPath,
|
|
633
|
+
entry.gitBranch,
|
|
634
|
+
entry.tag,
|
|
635
|
+
];
|
|
636
|
+
return fields.some((value) => typeof value === 'string' && value.toLowerCase().includes(needle));
|
|
637
|
+
}
|
|
638
|
+
cursorToOffset(cursor) {
|
|
639
|
+
if (!cursor)
|
|
640
|
+
return 0;
|
|
641
|
+
const parsed = Number.parseInt(cursor, 10);
|
|
642
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
643
|
+
}
|
|
644
|
+
// ── deleteSession(task 6.10) ──
|
|
645
|
+
/**
|
|
646
|
+
* 幂等删除会话:删除 `~/.claude/projects/<dir>/<sessionId>.jsonl`,
|
|
647
|
+
* 并清理 `~/.claude/sessions/*.json` 索引中指向该 sessionId 的条目。
|
|
648
|
+
*
|
|
649
|
+
* - 文件不存在(或从未存在)→ `already_deleted`(幂等)。
|
|
650
|
+
* - 文件存在并删除成功 → `deleted`。
|
|
651
|
+
* - 删除失败(权限等)→ `rejected`(含 error)。
|
|
652
|
+
*
|
|
653
|
+
* 仅作用于 Engine 原生 Session 文件,不影响产品 conversation/task 记录(task 9.13)。
|
|
654
|
+
*/
|
|
655
|
+
async deleteSession(sessionId, _request) {
|
|
656
|
+
try {
|
|
657
|
+
const resolved = this.resolveSessionFile(sessionId);
|
|
658
|
+
if (!resolved || !existsSync(resolved.path)) {
|
|
659
|
+
// 索引条目仍可能残留:清理之,保持幂等语义
|
|
660
|
+
this.cleanSessionsIndex(sessionId);
|
|
661
|
+
return { status: 'already_deleted' };
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
unlinkSync(resolved.path);
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
return {
|
|
668
|
+
status: 'rejected',
|
|
669
|
+
error: `Failed to delete session file: ${err instanceof Error ? err.message : String(err)}`,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
this.cleanSessionsIndex(sessionId);
|
|
673
|
+
return { status: 'deleted' };
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
return {
|
|
677
|
+
status: 'rejected',
|
|
678
|
+
error: err instanceof Error ? err.message : String(err),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* 清理 `~/.claude/sessions/*.json` 索引中指向该 sessionId 的条目。
|
|
684
|
+
* 静默处理:索引不存在或解析失败不影响删除回执。
|
|
685
|
+
*/
|
|
686
|
+
cleanSessionsIndex(sessionId) {
|
|
687
|
+
try {
|
|
688
|
+
const sessionsDir = join(homedir(), '.claude', 'sessions');
|
|
689
|
+
if (!existsSync(sessionsDir))
|
|
690
|
+
return;
|
|
691
|
+
const files = readdirSync(sessionsDir, { withFileTypes: true });
|
|
692
|
+
for (const file of files) {
|
|
693
|
+
if (!file.isFile() || !file.name.endsWith('.json'))
|
|
694
|
+
continue;
|
|
695
|
+
const filePath = join(sessionsDir, file.name);
|
|
696
|
+
try {
|
|
697
|
+
const meta = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
698
|
+
if (meta?.sessionId !== sessionId)
|
|
699
|
+
continue;
|
|
700
|
+
// 该索引文件即为该会话的元数据;删除整个文件
|
|
701
|
+
unlinkSync(filePath);
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
/* skip malformed index entries */
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
/* index cleanup is best-effort */
|
|
710
|
+
}
|
|
711
|
+
}
|
|
431
712
|
// ── helpers for session queries ──
|
|
432
713
|
/** �?~/.claude/sessions/*.json 加载会话元数据索�?*/
|
|
433
714
|
loadSessionsIndex() {
|
|
@@ -482,6 +763,27 @@ export class ClaudeAdapter {
|
|
|
482
763
|
: undefined;
|
|
483
764
|
return this.stringifyClaudeContent(msg.content ?? nested?.content ?? '');
|
|
484
765
|
}
|
|
766
|
+
/**
|
|
767
|
+
* 从 Claude Agent SDK result 帧读取 is_error / subtype。
|
|
768
|
+
* 字段在帧顶层(query() 约定);个别版本可能挂在 object 形态的 result 上。
|
|
769
|
+
* 溢出仍 yield type:'result'(保留 sessionId / 可见正文),不得收成 type:'error'。
|
|
770
|
+
*/
|
|
771
|
+
extractSdkResultSignals(message) {
|
|
772
|
+
if (!message || typeof message !== 'object')
|
|
773
|
+
return { isError: false };
|
|
774
|
+
const frame = message;
|
|
775
|
+
const nested = frame.result && typeof frame.result === 'object' && !Array.isArray(frame.result)
|
|
776
|
+
? frame.result
|
|
777
|
+
: undefined;
|
|
778
|
+
const isError = frame.is_error === true || frame.isError === true
|
|
779
|
+
|| nested?.is_error === true || nested?.isError === true;
|
|
780
|
+
const subtype = typeof frame.subtype === 'string'
|
|
781
|
+
? frame.subtype
|
|
782
|
+
: typeof nested?.subtype === 'string'
|
|
783
|
+
? nested.subtype
|
|
784
|
+
: undefined;
|
|
785
|
+
return { isError, ...(subtype ? { subtype } : {}) };
|
|
786
|
+
}
|
|
485
787
|
stringifyClaudeContent(content) {
|
|
486
788
|
if (typeof content === 'string')
|
|
487
789
|
return content;
|
|
@@ -582,6 +884,7 @@ export class ClaudeAdapter {
|
|
|
582
884
|
generateMcpConfig(workDir, mcpToken) {
|
|
583
885
|
if (!this.mcpBridgeUrl)
|
|
584
886
|
return null;
|
|
887
|
+
validateMcpBridgeUrl(this.mcpBridgeUrl);
|
|
585
888
|
const effectiveToken = mcpToken ?? this.mcpApiKey;
|
|
586
889
|
const configDir = workDir ? join(workDir, '.claude') : join(homedir(), '.claude');
|
|
587
890
|
// 仅当使用 Agent �?token 且无 per-task token 时才使用缓存
|
|
@@ -702,5 +1005,106 @@ export class ClaudeAdapter {
|
|
|
702
1005
|
}
|
|
703
1006
|
return null;
|
|
704
1007
|
}
|
|
1008
|
+
// 搬自 run-claude-agent.mjs:38-40,491-511
|
|
1009
|
+
async *executeViaAgentSdk(task, signal) {
|
|
1010
|
+
const sdkPath = process.env.CLAUDE_AGENT_SDK_PATH
|
|
1011
|
+
|| '/home/node/.local/lib/node_modules/@anthropic-ai/claude-agent-sdk';
|
|
1012
|
+
const require = createRequire(import.meta.url);
|
|
1013
|
+
const sdk = require(sdkPath);
|
|
1014
|
+
const { query } = sdk;
|
|
1015
|
+
const budgeted = applyContextBudget({
|
|
1016
|
+
...task,
|
|
1017
|
+
systemPrompt: task.systemPrompt ?? task.claude?.systemPrompt,
|
|
1018
|
+
});
|
|
1019
|
+
if (budgeted.diagnostics) {
|
|
1020
|
+
const d = budgeted.diagnostics;
|
|
1021
|
+
yield {
|
|
1022
|
+
type: 'thinking',
|
|
1023
|
+
content: `[ContextBudget] input ${d.originalInputChars}→${d.finalInputChars} chars (${d.compression})`,
|
|
1024
|
+
engineEvent: {
|
|
1025
|
+
type: 'status',
|
|
1026
|
+
text: `[ContextBudget] input ${d.originalInputChars}→${d.finalInputChars} chars (${d.compression})`,
|
|
1027
|
+
},
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
1031
|
+
const permissionMode = resolvePermissionMode(task);
|
|
1032
|
+
const maxTurns = Math.min(task.claude?.maxTurns ?? 200, 500);
|
|
1033
|
+
const allowedTools = task.claude?.tools ?? ['Skill', 'Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'mcp__*'];
|
|
1034
|
+
const queryOptions = {
|
|
1035
|
+
cwd: task.workDir || process.cwd(),
|
|
1036
|
+
settingSources: ['user', 'project'],
|
|
1037
|
+
allowedTools,
|
|
1038
|
+
permissionMode,
|
|
1039
|
+
maxTurns,
|
|
1040
|
+
};
|
|
1041
|
+
if (task.mcpToken && this.mcpBridgeUrl) {
|
|
1042
|
+
validateMcpBridgeUrl(this.mcpBridgeUrl);
|
|
1043
|
+
queryOptions.mcpServers = {
|
|
1044
|
+
winmatrix: {
|
|
1045
|
+
type: 'http',
|
|
1046
|
+
url: this.mcpBridgeUrl,
|
|
1047
|
+
headers: { Authorization: `Bearer ${task.mcpToken}` },
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
queryOptions.strictMcpConfig = true;
|
|
1051
|
+
}
|
|
1052
|
+
if (task.resumeSession && task.sessionId)
|
|
1053
|
+
queryOptions.resume = task.sessionId;
|
|
1054
|
+
if (!isRoot)
|
|
1055
|
+
queryOptions.allowDangerouslySkipPermissions = true;
|
|
1056
|
+
const effectiveSystem = budgeted.systemPrompt ?? task.claude?.systemPrompt ?? task.systemPrompt;
|
|
1057
|
+
for await (const message of query({
|
|
1058
|
+
prompt: withSecurityPrefix(effectiveSystem
|
|
1059
|
+
? `${effectiveSystem}\n\n---\n\n${budgeted.input}`
|
|
1060
|
+
: budgeted.input),
|
|
1061
|
+
options: queryOptions,
|
|
1062
|
+
})) {
|
|
1063
|
+
if (signal?.aborted)
|
|
1064
|
+
return;
|
|
1065
|
+
if (message.type === 'result') {
|
|
1066
|
+
const sessionId = typeof message.session_id === 'string' ? message.session_id : undefined;
|
|
1067
|
+
const { isError, subtype } = this.extractSdkResultSignals(message);
|
|
1068
|
+
yield {
|
|
1069
|
+
type: 'result',
|
|
1070
|
+
content: this.stringifyClaudeContent(message.result ?? ''),
|
|
1071
|
+
...(sessionId ? { sessionId } : {}),
|
|
1072
|
+
...(isError ? { isError: true } : {}),
|
|
1073
|
+
...(subtype ? { sdkSubtype: subtype } : {}),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
else if (message.type === 'assistant' && message.message != null) {
|
|
1077
|
+
// SDK 的 message.message 是 Anthropic API Message 对象(content 为 block 数组),
|
|
1078
|
+
// 直接 String() 会得到 "[object Object]" 并污染下游 deliverable/回调正文。
|
|
1079
|
+
const text = this.extractAssistantMessageText(message.message);
|
|
1080
|
+
if (text) {
|
|
1081
|
+
yield { type: 'delta', content: text, engineEvent: { type: 'content_delta', text } };
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* assistant 帧 message.message(Anthropic Message 对象 / content 数组 / 字符串)
|
|
1088
|
+
* → content 内 text 块原文。thinking / tool_use / tool_result 块不属于交付文本,
|
|
1089
|
+
* 不混入(deliverable-tracker 只累积 Agent 文本输出)。
|
|
1090
|
+
*/
|
|
1091
|
+
extractAssistantMessageText(value) {
|
|
1092
|
+
if (typeof value === 'string')
|
|
1093
|
+
return value;
|
|
1094
|
+
if (!value || typeof value !== 'object')
|
|
1095
|
+
return '';
|
|
1096
|
+
const content = value.content;
|
|
1097
|
+
if (typeof content === 'string')
|
|
1098
|
+
return content;
|
|
1099
|
+
if (!Array.isArray(content))
|
|
1100
|
+
return '';
|
|
1101
|
+
return content
|
|
1102
|
+
.filter((block) => Boolean(block)
|
|
1103
|
+
&& typeof block === 'object'
|
|
1104
|
+
&& block.type === 'text'
|
|
1105
|
+
&& typeof block.text === 'string')
|
|
1106
|
+
.map((block) => block.text)
|
|
1107
|
+
.join('\n');
|
|
1108
|
+
}
|
|
705
1109
|
}
|
|
706
1110
|
//# sourceMappingURL=ClaudeAdapter.js.map
|