@steerable/agent-shell 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +91 -0
- package/contracts/tool-contract.json +326 -0
- package/dist/attachments.d.ts +41 -0
- package/dist/attachments.js +147 -0
- package/dist/brand.d.ts +24 -0
- package/dist/brand.js +92 -0
- package/dist/host/http-routes.d.ts +21 -0
- package/dist/host/http-routes.js +55 -0
- package/dist/host/ipc.d.ts +11 -0
- package/dist/host/ipc.js +20 -0
- package/dist/host/pack-assembly.d.ts +86 -0
- package/dist/host/pack-assembly.js +32 -0
- package/dist/host/runtime.d.ts +69 -0
- package/dist/host/runtime.js +207 -0
- package/dist/host/visible-terminal-exec.d.ts +21 -0
- package/dist/host/visible-terminal-exec.js +151 -0
- package/dist/hosted-web-search.d.ts +13 -0
- package/dist/hosted-web-search.js +82 -0
- package/dist/image-attachment.d.ts +39 -0
- package/dist/image-attachment.js +133 -0
- package/dist/insights/flush.d.ts +10 -0
- package/dist/insights/flush.js +139 -0
- package/dist/insights/record.d.ts +17 -0
- package/dist/insights/record.js +44 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.js +22 -0
- package/dist/llm/index.d.ts +23 -0
- package/dist/llm/index.js +105 -0
- package/dist/llm/ollama.d.ts +23 -0
- package/dist/llm/ollama.js +242 -0
- package/dist/llm/openai-compat.d.ts +20 -0
- package/dist/llm/openai-compat.js +199 -0
- package/dist/llm/sidecar-provider.d.ts +37 -0
- package/dist/llm/sidecar-provider.js +163 -0
- package/dist/llm/tool-choice.d.ts +35 -0
- package/dist/llm/tool-choice.js +85 -0
- package/dist/llm/types.d.ts +122 -0
- package/dist/llm/types.js +1 -0
- package/dist/local-backend/agent-capability.d.ts +101 -0
- package/dist/local-backend/agent-capability.js +174 -0
- package/dist/local-backend/ai-title.d.ts +43 -0
- package/dist/local-backend/ai-title.js +173 -0
- package/dist/local-backend/auto-continue-helper.d.ts +80 -0
- package/dist/local-backend/auto-continue-helper.js +83 -0
- package/dist/local-backend/branch-helper.d.ts +24 -0
- package/dist/local-backend/branch-helper.js +27 -0
- package/dist/local-backend/context-compactor.d.ts +81 -0
- package/dist/local-backend/context-compactor.js +213 -0
- package/dist/local-backend/coreloop-stream.d.ts +245 -0
- package/dist/local-backend/coreloop-stream.js +277 -0
- package/dist/local-backend/deferred-detector.d.ts +15 -0
- package/dist/local-backend/deferred-detector.js +124 -0
- package/dist/local-backend/history-helper.d.ts +30 -0
- package/dist/local-backend/history-helper.js +34 -0
- package/dist/local-backend/interrupted-helper.d.ts +29 -0
- package/dist/local-backend/interrupted-helper.js +25 -0
- package/dist/local-backend/live-stream.d.ts +36 -0
- package/dist/local-backend/live-stream.js +23 -0
- package/dist/local-backend/llm-diagnose.d.ts +37 -0
- package/dist/local-backend/llm-diagnose.js +284 -0
- package/dist/local-backend/message-triggers.d.ts +27 -0
- package/dist/local-backend/message-triggers.js +67 -0
- package/dist/local-backend/pack-backend-routes.d.ts +31 -0
- package/dist/local-backend/pack-backend-routes.js +62 -0
- package/dist/local-backend/pack-turn-hooks.d.ts +37 -0
- package/dist/local-backend/pack-turn-hooks.js +67 -0
- package/dist/local-backend/prompt-builder.d.ts +101 -0
- package/dist/local-backend/prompt-builder.js +246 -0
- package/dist/local-backend/regenerate-helper.d.ts +62 -0
- package/dist/local-backend/regenerate-helper.js +75 -0
- package/dist/local-backend/router.d.ts +175 -0
- package/dist/local-backend/router.js +3139 -0
- package/dist/local-backend/skill-install.d.ts +19 -0
- package/dist/local-backend/skill-install.js +71 -0
- package/dist/local-backend/skill-loader.d.ts +92 -0
- package/dist/local-backend/skill-loader.js +146 -0
- package/dist/local-backend/skills/00-identity/SKILL.md +32 -0
- package/dist/local-backend/skills/10-goal/SKILL.md +59 -0
- package/dist/local-backend/skills/11-loop/SKILL.md +71 -0
- package/dist/local-backend/skills/12-create-skill/SKILL.md +88 -0
- package/dist/local-backend/skills/70-plan-mode/SKILL.md +58 -0
- package/dist/local-backend/skills/80-tool-usage/SKILL.md +70 -0
- package/dist/local-backend/skills/81-anti-deferred/SKILL.md +53 -0
- package/dist/local-backend/skills/82-data-grounding/SKILL.md +56 -0
- package/dist/local-backend/skills/85-local-exec/SKILL.md +86 -0
- package/dist/local-backend/skills/86-proactive-coding/SKILL.md +51 -0
- package/dist/local-backend/subagent-profiles.d.ts +30 -0
- package/dist/local-backend/subagent-profiles.js +74 -0
- package/dist/local-backend/task-process.d.ts +12 -0
- package/dist/local-backend/task-process.js +176 -0
- package/dist/local-backend/task-service.d.ts +135 -0
- package/dist/local-backend/task-service.js +565 -0
- package/dist/local-backend/turn-duration.d.ts +2 -0
- package/dist/local-backend/turn-duration.js +9 -0
- package/dist/local-backend/turn-timeline.d.ts +16 -0
- package/dist/local-backend/turn-timeline.js +42 -0
- package/dist/local-backend/worktree-service.d.ts +84 -0
- package/dist/local-backend/worktree-service.js +243 -0
- package/dist/local-edit.d.ts +48 -0
- package/dist/local-edit.js +44 -0
- package/dist/local-executor.d.ts +255 -0
- package/dist/local-executor.js +881 -0
- package/dist/local-script-registry.d.ts +28 -0
- package/dist/local-script-registry.js +63 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.js +12 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +855 -0
- package/dist/mcp-executor.d.ts +45 -0
- package/dist/mcp-executor.js +241 -0
- package/dist/mcp-server-registry.d.ts +104 -0
- package/dist/mcp-server-registry.js +234 -0
- package/dist/preload-default.d.ts +1 -0
- package/dist/preload-default.js +9 -0
- package/dist/preload.cjs +395 -0
- package/dist/preload.d.ts +20 -0
- package/dist/preload.js +411 -0
- package/dist/product-config.d.ts +43 -0
- package/dist/product-config.js +26 -0
- package/dist/project-registry.d.ts +55 -0
- package/dist/project-registry.js +106 -0
- package/dist/project-rules.d.ts +15 -0
- package/dist/project-rules.js +102 -0
- package/dist/runtime.d.ts +62 -0
- package/dist/runtime.js +217 -0
- package/dist/scenario/pack.d.ts +8 -0
- package/dist/scenario/pack.js +1 -0
- package/dist/scenario/registry.d.ts +24 -0
- package/dist/scenario/registry.js +31 -0
- package/dist/server/http-server.d.ts +39 -0
- package/dist/server/http-server.js +361 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +107 -0
- package/dist/server/sse-bus.d.ts +14 -0
- package/dist/server/sse-bus.js +31 -0
- package/dist/shell-adapt.d.ts +21 -0
- package/dist/shell-adapt.js +104 -0
- package/dist/sidecar/boot.d.ts +36 -0
- package/dist/sidecar/boot.js +343 -0
- package/dist/sidecar/egress-hint.d.ts +15 -0
- package/dist/sidecar/egress-hint.js +46 -0
- package/dist/sidecar/egress-proxy.d.ts +183 -0
- package/dist/sidecar/egress-proxy.js +419 -0
- package/dist/sidecar/errors.d.ts +22 -0
- package/dist/sidecar/errors.js +38 -0
- package/dist/sidecar/exec-sandbox.d.ts +48 -0
- package/dist/sidecar/exec-sandbox.js +94 -0
- package/dist/sidecar/handle.d.ts +32 -0
- package/dist/sidecar/handle.js +53 -0
- package/dist/sidecar/index.d.ts +14 -0
- package/dist/sidecar/index.js +13 -0
- package/dist/sidecar/proxy-detect.d.ts +50 -0
- package/dist/sidecar/proxy-detect.js +182 -0
- package/dist/sidecar/reverse-approval.d.ts +55 -0
- package/dist/sidecar/reverse-approval.js +86 -0
- package/dist/sidecar/reverse-ask-user.d.ts +34 -0
- package/dist/sidecar/reverse-ask-user.js +59 -0
- package/dist/sidecar/reverse-spawn.d.ts +19 -0
- package/dist/sidecar/reverse-spawn.js +161 -0
- package/dist/sidecar/reverse-tools.d.ts +29 -0
- package/dist/sidecar/reverse-tools.js +106 -0
- package/dist/sidecar/safety-patterns.d.ts +41 -0
- package/dist/sidecar/safety-patterns.js +157 -0
- package/dist/sidecar/storage-path.d.ts +14 -0
- package/dist/sidecar/storage-path.js +35 -0
- package/dist/sidecar/supervisor.d.ts +218 -0
- package/dist/sidecar/supervisor.js +932 -0
- package/dist/sidecar/types.d.ts +601 -0
- package/dist/sidecar/types.js +1 -0
- package/dist/single-instance.d.ts +11 -0
- package/dist/single-instance.js +21 -0
- package/dist/storage/empty-chats.d.ts +9 -0
- package/dist/storage/empty-chats.js +16 -0
- package/dist/storage/index.d.ts +373 -0
- package/dist/storage/index.js +1158 -0
- package/dist/storage/insights-redact.d.ts +2 -0
- package/dist/storage/insights-redact.js +30 -0
- package/dist/storage/insights-settings.d.ts +53 -0
- package/dist/storage/insights-settings.js +92 -0
- package/dist/storage/llm-settings.d.ts +120 -0
- package/dist/storage/llm-settings.js +233 -0
- package/dist/storage/local-store-singleton.d.ts +28 -0
- package/dist/storage/local-store-singleton.js +38 -0
- package/dist/storage/message-order.d.ts +25 -0
- package/dist/storage/message-order.js +27 -0
- package/dist/storage/pack-migrations.d.ts +22 -0
- package/dist/storage/pack-migrations.js +24 -0
- package/dist/storage/pack-seeds.d.ts +36 -0
- package/dist/storage/pack-seeds.js +42 -0
- package/dist/storage/telemetry-settings.d.ts +38 -0
- package/dist/storage/telemetry-settings.js +59 -0
- package/dist/storage/usage-summary.d.ts +55 -0
- package/dist/storage/usage-summary.js +38 -0
- package/dist/storage/web-search-settings.d.ts +38 -0
- package/dist/storage/web-search-settings.js +74 -0
- package/dist/storage/write-lease.d.ts +26 -0
- package/dist/storage/write-lease.js +74 -0
- package/dist/terminal-manager.d.ts +83 -0
- package/dist/terminal-manager.js +506 -0
- package/dist/tool-router.d.ts +228 -0
- package/dist/tool-router.js +930 -0
- package/dist/tool-search-rank.d.ts +42 -0
- package/dist/tool-search-rank.js +96 -0
- package/package.json +67 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A4: drive one chat turn through the sidecar-hosted Python CoreLoop.
|
|
3
|
+
*
|
|
4
|
+
* This is the default chat path (since 2026-08-26): the sidecar owns
|
|
5
|
+
* think→act→observe, and tool calls come back over the reverse channel
|
|
6
|
+
* (`tool.invoke`, registered in `main.ts`). This module only translates the
|
|
7
|
+
* wire notifications into the existing SSE contract — the renderer, preload
|
|
8
|
+
* and IPC layers are untouched.
|
|
9
|
+
*
|
|
10
|
+
* This is the only chat path (the TS loop was deleted 2026-08-26). If the
|
|
11
|
+
* sidecar failed to start, the router fails loud with a 503 — there is no
|
|
12
|
+
* in-process fallback loop anymore. `STEERABLE_USE_SIDECAR=0` still
|
|
13
|
+
* disables the sidecar process itself (auxiliary LLM calls then fall back
|
|
14
|
+
* to the in-proc providers), but chat requires the sidecar.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Active CoreLoop turns by chatId — lets the main process route a mid-turn
|
|
18
|
+
* steer (user typed while the turn runs) to the right sidecar stream without
|
|
19
|
+
* threading the sidecar streamId up through the router's call chain.
|
|
20
|
+
*/
|
|
21
|
+
const activeCoreLoopStreams = new Map();
|
|
22
|
+
// How long to wait for the sidecar's cancelled stream.done (carries the
|
|
23
|
+
// traceId) after a user abort before settling with a bare AbortError.
|
|
24
|
+
const CANCEL_GRACE_MS = 10_000;
|
|
25
|
+
/** Sidecar streamId of the chat's running CoreLoop turn, if any. */
|
|
26
|
+
export function getActiveCoreLoopStreamId(chatId) {
|
|
27
|
+
return activeCoreLoopStreams.get(chatId);
|
|
28
|
+
}
|
|
29
|
+
const WEEKDAYS_ZH = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
30
|
+
/**
|
|
31
|
+
* 构建本轮的 world-state 节(Wave 2 起;W6-7b 从只有 time 扩到多节)。
|
|
32
|
+
*
|
|
33
|
+
* time 节分钟精度是刻意的:模型不需要秒,而快照在同一分钟内稳定,sidecar
|
|
34
|
+
* 的 merge-patch diff 对连续轮次就是零 token 空转;跨分钟只追加一个几
|
|
35
|
+
* token 的尾部补丁,系统提示词前缀保持字节稳定(cache 净收益)。其余各节
|
|
36
|
+
* (mode/permissions/skills)同理——慢变,变了才补一个小补丁。
|
|
37
|
+
*/
|
|
38
|
+
export function buildWorldState(input = {}) {
|
|
39
|
+
const now = input.now ?? new Date();
|
|
40
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
41
|
+
const local = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` +
|
|
42
|
+
`T${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
43
|
+
const state = {
|
|
44
|
+
time: {
|
|
45
|
+
local,
|
|
46
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
47
|
+
weekday: WEEKDAYS_ZH[now.getDay()],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
if (input.mode) {
|
|
51
|
+
state.mode = { name: input.mode };
|
|
52
|
+
}
|
|
53
|
+
if (input.permissions) {
|
|
54
|
+
state.permissions = {
|
|
55
|
+
approval: input.permissions.approval,
|
|
56
|
+
sandbox: {
|
|
57
|
+
enabled: input.permissions.sandbox.enabled,
|
|
58
|
+
writableRoots: input.permissions.sandbox.writableRoots,
|
|
59
|
+
network: input.permissions.sandbox.network,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (input.skills) {
|
|
64
|
+
state.skills = {
|
|
65
|
+
conditions: input.skills.conditions,
|
|
66
|
+
exclude: input.skills.exclude,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (input.extra) {
|
|
70
|
+
Object.assign(state, input.extra);
|
|
71
|
+
}
|
|
72
|
+
return state;
|
|
73
|
+
}
|
|
74
|
+
export async function streamCoreLoopTurn(options) {
|
|
75
|
+
const { supervisor, signal } = options;
|
|
76
|
+
if (signal?.aborted) {
|
|
77
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
78
|
+
}
|
|
79
|
+
const request = {
|
|
80
|
+
provider: options.provider,
|
|
81
|
+
model: options.model,
|
|
82
|
+
baseUrl: options.baseUrl,
|
|
83
|
+
apiKey: options.apiKey,
|
|
84
|
+
temperature: options.temperature,
|
|
85
|
+
systemPrompt: options.systemPrompt,
|
|
86
|
+
compat: options.compat,
|
|
87
|
+
presets: options.presets,
|
|
88
|
+
...(options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}),
|
|
89
|
+
chatId: options.chatId,
|
|
90
|
+
recordId: options.recordId,
|
|
91
|
+
resume: options.resume,
|
|
92
|
+
useCoreLoop: true,
|
|
93
|
+
// Stream every CoreLoop round's display text so the UI can interleave
|
|
94
|
+
// reasoning, tool cards, and replies in call order. Retry drafts still
|
|
95
|
+
// live in the durable record/trace; the renderer places each round's
|
|
96
|
+
// narration next to the tools that followed it.
|
|
97
|
+
contentMode: 'all',
|
|
98
|
+
toolsViaHost: true,
|
|
99
|
+
streamRawChunks: options.streamRawChunks,
|
|
100
|
+
// W8: advertise the sidecar-hosted ask_user tool; the desktop answers
|
|
101
|
+
// `ask_user.request` reverse calls with the renderer question card
|
|
102
|
+
// (AskUserModalHost). Under toolsViaHost the sidecar intercepts
|
|
103
|
+
// ask_user locally, so it never reaches the host's tool.invoke.
|
|
104
|
+
// 4.6a: background task streams opt out (options.askUser === false).
|
|
105
|
+
askUser: options.askUser ?? true,
|
|
106
|
+
toolContext: options.toolContext,
|
|
107
|
+
// Parity with the TS loop: the four anti-hallucination guards (data-need
|
|
108
|
+
// routing, deferred/claimed retry, grounding judge, narration round) run
|
|
109
|
+
// as CoreLoop hooks on the sidecar side.
|
|
110
|
+
antiHallucination: true,
|
|
111
|
+
skills: options.skills,
|
|
112
|
+
worldState: options.worldState,
|
|
113
|
+
execSandbox: options.execSandbox,
|
|
114
|
+
approval: options.approval,
|
|
115
|
+
orchestration: options.orchestration,
|
|
116
|
+
subagent: options.subagent,
|
|
117
|
+
budgetTokens: options.budgetTokens,
|
|
118
|
+
messages: options.messages.map((m) => {
|
|
119
|
+
// W6-3: a message carrying images is sent as structured `parts` (the
|
|
120
|
+
// sidecar treats `parts` as authoritative, `content` as its text
|
|
121
|
+
// projection). Text-only messages keep the legacy `content` shorthand.
|
|
122
|
+
const base = {
|
|
123
|
+
role: m.role,
|
|
124
|
+
content: m.content,
|
|
125
|
+
name: m.name,
|
|
126
|
+
toolCallId: m.toolCallId,
|
|
127
|
+
};
|
|
128
|
+
if (m.images && m.images.length > 0) {
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
parts: [
|
|
132
|
+
{ type: 'text', text: m.content },
|
|
133
|
+
...m.images.map((img) => ({
|
|
134
|
+
type: 'image',
|
|
135
|
+
data: img.data,
|
|
136
|
+
mediaType: img.mediaType,
|
|
137
|
+
})),
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return base;
|
|
142
|
+
}),
|
|
143
|
+
tools: options.tools.map((t) => ({
|
|
144
|
+
type: 'function',
|
|
145
|
+
function: {
|
|
146
|
+
name: t.name,
|
|
147
|
+
description: t.description,
|
|
148
|
+
parameters: t.inputSchema ?? { type: 'object', properties: {} },
|
|
149
|
+
},
|
|
150
|
+
})),
|
|
151
|
+
};
|
|
152
|
+
// Track in-flight tool calls so results can be paired with their arguments
|
|
153
|
+
// (the start notification carries them, the result notification does not).
|
|
154
|
+
const pendingCalls = new Map();
|
|
155
|
+
return await new Promise((resolve, reject) => {
|
|
156
|
+
let streamIdRef = null;
|
|
157
|
+
let settled = false;
|
|
158
|
+
let cancelling = false;
|
|
159
|
+
let abortGraceTimer = null;
|
|
160
|
+
const onAbort = () => {
|
|
161
|
+
if (settled || cancelling)
|
|
162
|
+
return;
|
|
163
|
+
cancelling = true;
|
|
164
|
+
if (streamIdRef) {
|
|
165
|
+
void supervisor.cancelChat(streamIdRef);
|
|
166
|
+
}
|
|
167
|
+
// Don't reject yet: the sidecar answers the cancel with a stream.done
|
|
168
|
+
// carrying status 'cancelled' AND the traceId — resolving on it lets
|
|
169
|
+
// the router persist the partial trace (cancelled turns are dogfood
|
|
170
|
+
// signal too). Only if the done never arrives do we settle bare.
|
|
171
|
+
abortGraceTimer = setTimeout(() => {
|
|
172
|
+
if (settled)
|
|
173
|
+
return;
|
|
174
|
+
settled = true;
|
|
175
|
+
cleanup();
|
|
176
|
+
reject(new DOMException('The operation was aborted.', 'AbortError'));
|
|
177
|
+
}, CANCEL_GRACE_MS);
|
|
178
|
+
};
|
|
179
|
+
signal?.addEventListener('abort', onAbort);
|
|
180
|
+
const cleanup = () => {
|
|
181
|
+
signal?.removeEventListener('abort', onAbort);
|
|
182
|
+
if (abortGraceTimer)
|
|
183
|
+
clearTimeout(abortGraceTimer);
|
|
184
|
+
activeCoreLoopStreams.delete(options.chatId);
|
|
185
|
+
};
|
|
186
|
+
void supervisor
|
|
187
|
+
.streamChat(request, {
|
|
188
|
+
onChunk: (chunk) => {
|
|
189
|
+
// The user cancelled — swallow any in-flight deltas while we wait
|
|
190
|
+
// for the sidecar's cancelled done.
|
|
191
|
+
if (cancelling)
|
|
192
|
+
return;
|
|
193
|
+
if (chunk.delta) {
|
|
194
|
+
options.onText(chunk.delta);
|
|
195
|
+
}
|
|
196
|
+
if (chunk.reasoningDelta) {
|
|
197
|
+
options.onReasoning?.(chunk.reasoningDelta);
|
|
198
|
+
}
|
|
199
|
+
if (chunk.toolCall) {
|
|
200
|
+
pendingCalls.set(chunk.toolCall.id, {
|
|
201
|
+
name: chunk.toolCall.name,
|
|
202
|
+
arguments: chunk.toolCall.arguments ?? {},
|
|
203
|
+
});
|
|
204
|
+
options.onToolStart?.({
|
|
205
|
+
id: chunk.toolCall.id,
|
|
206
|
+
tool: chunk.toolCall.name,
|
|
207
|
+
arguments: chunk.toolCall.arguments ?? {},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (chunk.toolResult) {
|
|
211
|
+
const started = pendingCalls.get(chunk.toolResult.id);
|
|
212
|
+
pendingCalls.delete(chunk.toolResult.id);
|
|
213
|
+
options.onToolAction?.({
|
|
214
|
+
id: chunk.toolResult.id,
|
|
215
|
+
tool: chunk.toolResult.name,
|
|
216
|
+
arguments: started?.arguments ?? {},
|
|
217
|
+
success: chunk.toolResult.success,
|
|
218
|
+
result: chunk.toolResult.resultPreview,
|
|
219
|
+
error: chunk.toolResult.error,
|
|
220
|
+
durationMs: chunk.toolResult.durationMs,
|
|
221
|
+
sandbox: chunk.toolResult.sandbox,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (chunk.notice) {
|
|
225
|
+
options.onNotice?.(chunk.notice.kind, chunk.notice);
|
|
226
|
+
}
|
|
227
|
+
if (chunk.rawChunk) {
|
|
228
|
+
options.onRawChunk?.(chunk.rawChunk);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
onDone: (done) => {
|
|
232
|
+
if (settled)
|
|
233
|
+
return;
|
|
234
|
+
settled = true;
|
|
235
|
+
cleanup();
|
|
236
|
+
resolve({
|
|
237
|
+
status: done.status ?? (done.cancelled ? 'cancelled' : done.ok ? 'completed' : 'failed'),
|
|
238
|
+
reason: done.reason ?? (done.cancelled ? 'aborted_by_user' : undefined),
|
|
239
|
+
traceId: done.traceId,
|
|
240
|
+
usage: done.usage,
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
onChildEvent: (event) => {
|
|
244
|
+
if (settled || cancelling)
|
|
245
|
+
return;
|
|
246
|
+
options.onChildEvent?.(event);
|
|
247
|
+
},
|
|
248
|
+
onError: (err) => {
|
|
249
|
+
if (settled)
|
|
250
|
+
return;
|
|
251
|
+
settled = true;
|
|
252
|
+
cleanup();
|
|
253
|
+
// Carry the sidecar trace id on the rejection so the router can
|
|
254
|
+
// persist the partial trace — failed turns are the ones dogfooding
|
|
255
|
+
// most needs to inspect.
|
|
256
|
+
const failure = new Error(`coreloop stream failed: ${err.kind}: ${err.message}`);
|
|
257
|
+
if (err.traceId)
|
|
258
|
+
failure.traceId = err.traceId;
|
|
259
|
+
reject(failure);
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
.then((streamId) => {
|
|
263
|
+
streamIdRef = streamId;
|
|
264
|
+
activeCoreLoopStreams.set(options.chatId, streamId);
|
|
265
|
+
options.onStreamId?.(streamId);
|
|
266
|
+
if (signal?.aborted && !settled)
|
|
267
|
+
onAbort();
|
|
268
|
+
})
|
|
269
|
+
.catch((err) => {
|
|
270
|
+
if (settled)
|
|
271
|
+
return;
|
|
272
|
+
settled = true;
|
|
273
|
+
cleanup();
|
|
274
|
+
reject(err);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects "deferred-execution hallucination" — the failure mode where the LLM
|
|
3
|
+
* narrates an upcoming action (e.g. "现在轮询结果。" / "I'll now poll…") but
|
|
4
|
+
* does NOT actually emit the corresponding tool_call, leaving the stream
|
|
5
|
+
* dangling.
|
|
6
|
+
*
|
|
7
|
+
* The in-turn retry machinery that used to act on this signal moved to the
|
|
8
|
+
* sidecar's Python AntiHallucinationHooks (2026-08-26, TS loop deleted). The
|
|
9
|
+
* surviving caller is `detectToolDenialInHistory` in the router: deferred-
|
|
10
|
+
* execution residue in *history* counts as prompt pollution and triggers the
|
|
11
|
+
* tool reality-check preamble on later turns.
|
|
12
|
+
*
|
|
13
|
+
* Pure function (no `this`, no I/O) for unit-testability.
|
|
14
|
+
*/
|
|
15
|
+
export declare function detectDeferredExecution(text: string): boolean;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects "deferred-execution hallucination" — the failure mode where the LLM
|
|
3
|
+
* narrates an upcoming action (e.g. "现在轮询结果。" / "I'll now poll…") but
|
|
4
|
+
* does NOT actually emit the corresponding tool_call, leaving the stream
|
|
5
|
+
* dangling.
|
|
6
|
+
*
|
|
7
|
+
* The in-turn retry machinery that used to act on this signal moved to the
|
|
8
|
+
* sidecar's Python AntiHallucinationHooks (2026-08-26, TS loop deleted). The
|
|
9
|
+
* surviving caller is `detectToolDenialInHistory` in the router: deferred-
|
|
10
|
+
* execution residue in *history* counts as prompt pollution and triggers the
|
|
11
|
+
* tool reality-check preamble on later turns.
|
|
12
|
+
*
|
|
13
|
+
* Pure function (no `this`, no I/O) for unit-testability.
|
|
14
|
+
*/
|
|
15
|
+
const INTENT_VERBS = [
|
|
16
|
+
// 行动类
|
|
17
|
+
'执行', '调用', '运行', '发起', '开始', '继续',
|
|
18
|
+
// 读类
|
|
19
|
+
'搜索', '查找', '查询', '检查', '获取', '读取', '查看', '列出',
|
|
20
|
+
// 探查类
|
|
21
|
+
'探查', '探测', '处理', '试一下', '试试',
|
|
22
|
+
// 异步 follow-up 类(看到这些词意味着模型答应了"再调一次工具")
|
|
23
|
+
'轮询', '等待', '等结果', '等待结果', '查看结果', '看结果',
|
|
24
|
+
'跟进', '跟踪', '监控', '确认结果',
|
|
25
|
+
];
|
|
26
|
+
const VERB_ALT = INTENT_VERBS.join('|');
|
|
27
|
+
// 宽松主语(用于初筛 hasIntent)。允许 '然后' / '下一步' 这些过渡词;rule ③
|
|
28
|
+
// 用更严格的子集避免误伤过去时陈述。
|
|
29
|
+
const LOOSE_SUBJECTS = [
|
|
30
|
+
'现在', '接下来', '下一步', '然后',
|
|
31
|
+
// '我' 必须带未来时态修饰词。
|
|
32
|
+
'我(将|会|要|马上|立刻|想|想要|准备|现在|即将)', '我来', '我去',
|
|
33
|
+
'马上', '即将', '正要', '正在', '正',
|
|
34
|
+
'准备', '即刻',
|
|
35
|
+
];
|
|
36
|
+
const LOOSE_SUBJECT_ALT = LOOSE_SUBJECTS.join('|');
|
|
37
|
+
const CN_INTENT_LOOSE = new RegExp(`(${LOOSE_SUBJECT_ALT})[^\\n。!?]{0,10}(${VERB_ALT})`);
|
|
38
|
+
// 严格主语集合:只接受真正暗示"未来 / 马上"语态的词。
|
|
39
|
+
// - 不含 '然后' / '下一步':这些既可能是计划也可能是回顾,rule ③ 会过紧。
|
|
40
|
+
// - 不含 '正' / '继续':歧义太大。
|
|
41
|
+
const STRICT_FUTURE_SUBJECTS = [
|
|
42
|
+
'现在', '接下来', '马上', '即将', '正要', '正在', '准备', '即刻',
|
|
43
|
+
'我(将|会|要|马上|立刻|想|想要|准备|现在|即将)', '我来', '我去',
|
|
44
|
+
];
|
|
45
|
+
const STRICT_SUBJECT_ALT = STRICT_FUTURE_SUBJECTS.join('|');
|
|
46
|
+
// rule ③ 专用:subject ─ {0~10 字非句号}(**不含**了/过/完,否则中间夹个
|
|
47
|
+
// 过去时标记就该放过)─ verb ─ **lookahead**:verb 后面不能立刻接 了/过/完/毕
|
|
48
|
+
const CN_INTENT_STRICT = new RegExp(`(${STRICT_SUBJECT_ALT})[^\\n。!?了过完]{0,10}(${VERB_ALT})(?![了过完毕])`);
|
|
49
|
+
// 英文:把动词的词干提出来,允许 -ing / -ed / -s 后缀;也允许 modal 和 verb
|
|
50
|
+
// 之间夹一个 adverb("I'll now poll" / "I'll just go and poll" 这种)。
|
|
51
|
+
const EN_VERB_STEM = '(?:execute|call|run|invoke|search|query|fetch|read|list|poll|wait|follow\\s*up)';
|
|
52
|
+
const EN_VERB = `${EN_VERB_STEM}(?:ing|ed|s)?`;
|
|
53
|
+
const EN_ADVERB = '(?:now|just|then|first|go ahead and|now please)';
|
|
54
|
+
const EN_PATTERNS = [
|
|
55
|
+
new RegExp(`now\\s+${EN_VERB}\\b`, 'i'),
|
|
56
|
+
new RegExp(`let me\\s+(?:${EN_ADVERB}\\s+)?${EN_VERB}\\b`, 'i'),
|
|
57
|
+
new RegExp(`(?:i'?ll|i will|i'?m going to|i'?m about to)\\s+(?:${EN_ADVERB}\\s+)?${EN_VERB}\\b`, 'i'),
|
|
58
|
+
new RegExp(`(?:preparing|about)\\s+to\\s+${EN_VERB}\\b`, 'i'),
|
|
59
|
+
];
|
|
60
|
+
// ─── 澄清 / 条件式承诺收尾 ──────────────────────────────────────────────
|
|
61
|
+
// 真实误判 case(2026-07-05 日志):模型收尾写
|
|
62
|
+
// "告诉我你现在想做什么,我来帮你执行。"
|
|
63
|
+
// "告诉我文件路径和需求,我立刻执行。"
|
|
64
|
+
// 这是在向用户征询输入、承诺"拿到信息后再执行"——合法的对话终点(等用户),
|
|
65
|
+
// 不是 deferred-execution。旧规则 ③ 命中"我来…执行"强制重试,模型换个说法
|
|
66
|
+
// 还是澄清式收尾,再次误判,两轮重试直接把 token 预算烧爆。
|
|
67
|
+
//
|
|
68
|
+
// 判定:征询/条件标记出现在意图动词**之前**("告诉我 X,我就执行"),或整句
|
|
69
|
+
// 只有征询没有意图动词("你可以告诉我:")。标记在动词之后不豁免——
|
|
70
|
+
// "我现在执行,如果失败会重试。"仍然算 deferred。
|
|
71
|
+
const CONDITIONAL_OFFER_MARKER = new RegExp('(告诉我|告知我|请提供|请给出|请发|发给我|你可以|您可以|如果|若(你|您)|只要|一旦|' +
|
|
72
|
+
'等(你|您)|需要(你|您)|你(想|要|希望|确认)|您(想|要|希望|确认)|' +
|
|
73
|
+
'let me know|tell me|if you|once you|provide|send me|share)', 'i');
|
|
74
|
+
function isConditionalOffer(sentence) {
|
|
75
|
+
if (!sentence)
|
|
76
|
+
return false;
|
|
77
|
+
const marker = CONDITIONAL_OFFER_MARKER.exec(sentence);
|
|
78
|
+
if (!marker)
|
|
79
|
+
return false;
|
|
80
|
+
const verb = new RegExp(VERB_ALT).exec(sentence);
|
|
81
|
+
if (!verb)
|
|
82
|
+
return true;
|
|
83
|
+
return marker.index < verb.index;
|
|
84
|
+
}
|
|
85
|
+
export function detectDeferredExecution(text) {
|
|
86
|
+
if (!text)
|
|
87
|
+
return false;
|
|
88
|
+
const trimmed = text.trim();
|
|
89
|
+
// 6 字符门槛:避免极端短句("好的")触发,又能放过"现在轮询" 4 字 + 标点。
|
|
90
|
+
if (trimmed.length < 6)
|
|
91
|
+
return false;
|
|
92
|
+
const hasIntent = CN_INTENT_LOOSE.test(trimmed) || EN_PATTERNS.some((p) => p.test(trimmed));
|
|
93
|
+
// 收尾标点:省略号 / 半角省略号 / 全角省略号 / 冒号 — 都意味着话没说完
|
|
94
|
+
const trailingEllipsis = /(\.{3,}|…|……)\s*$/.test(trimmed);
|
|
95
|
+
const trailingColon = /[::]\s*$/.test(trimmed);
|
|
96
|
+
const lastSentence = trimmed.split(/[\n。!?!?]/).filter(Boolean).pop()?.trim() ?? '';
|
|
97
|
+
const lastEllipsis = /(\.{3,}|…|……)\s*$/.test(lastSentence);
|
|
98
|
+
const lastColon = /[::]\s*$/.test(lastSentence);
|
|
99
|
+
const lastIntentLoose = CN_INTENT_LOOSE.test(lastSentence) ||
|
|
100
|
+
EN_PATTERNS.some((p) => p.test(lastSentence));
|
|
101
|
+
const lastIntentStrict = CN_INTENT_STRICT.test(lastSentence) ||
|
|
102
|
+
EN_PATTERNS.some((p) => p.test(lastSentence));
|
|
103
|
+
const looksOpenEnded = trailingEllipsis || trailingColon || lastEllipsis || lastColon;
|
|
104
|
+
// 澄清式收尾豁免:最后一句是"给我信息 → 我再执行"的条件承诺,
|
|
105
|
+
// 模型在等用户输入,不是把执行"说"完就跑。
|
|
106
|
+
const lastConditionalOffer = isConditionalOffer(lastSentence);
|
|
107
|
+
// ① 意图词 + 任一开放式收尾:高置信,直接判 deferred
|
|
108
|
+
if (hasIntent && looksOpenEnded && !lastConditionalOffer)
|
|
109
|
+
return true;
|
|
110
|
+
// ② 最后一句既有意图词又有省略号 / 冒号:高置信
|
|
111
|
+
if (lastIntentLoose && (lastEllipsis || lastColon) && !lastConditionalOffer)
|
|
112
|
+
return true;
|
|
113
|
+
// ③ **最后一句**用**严格**未来主语 + 意图动词、且 verb 后不接过去时
|
|
114
|
+
// 标记 (了/过/完/毕) → 即使尾巴是句号也算 deferred。
|
|
115
|
+
// 历史 bug 复现:"任务已排队……现在轮询结果。" 用句号收尾骗过旧规则。
|
|
116
|
+
// 严格主语保证不误伤 "然后调用了 X" 这种回顾叙述。
|
|
117
|
+
if (lastIntentStrict && !lastConditionalOffer)
|
|
118
|
+
return true;
|
|
119
|
+
// ④ 纯省略号收尾且整段很短(< 400 字):保守兜底;防止"很长的最终结论 …
|
|
120
|
+
// 末尾哲学省略号"被误判
|
|
121
|
+
if ((trailingEllipsis || lastEllipsis) && trimmed.length < 400)
|
|
122
|
+
return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper for the conversation-history assembly in `router.ts`.
|
|
3
|
+
*
|
|
4
|
+
* The send path inserts the current user message into the store *before*
|
|
5
|
+
* building the model context, so the history window must drop that copy to
|
|
6
|
+
* avoid the model seeing it twice (it is appended explicitly as the final
|
|
7
|
+
* user message). The drop must be by id, not by tail position: the previous
|
|
8
|
+
* assistant reply persists asynchronously at stream end (createdAt = write
|
|
9
|
+
* time), so a fast follow-up message lands *before* it and the tail becomes
|
|
10
|
+
* `[..., u_new, a_prev]` — a positional pop then misses the duplicate
|
|
11
|
+
* (observed in the 2026-08-28 E2E request recording: every multi-turn user
|
|
12
|
+
* message was injected twice).
|
|
13
|
+
*
|
|
14
|
+
* The regenerate path inserts nothing; its truncation guarantees the
|
|
15
|
+
* triggering user message is the tail, so it keeps the positional pop.
|
|
16
|
+
*
|
|
17
|
+
* This module is pure so it can be unit-tested under plain Node/vitest —
|
|
18
|
+
* `router.ts` itself imports `better-sqlite3` via `storage`, which is
|
|
19
|
+
* compiled against Electron's ABI and can't load outside Electron.
|
|
20
|
+
*/
|
|
21
|
+
export interface HistoryMessageLike {
|
|
22
|
+
id: string;
|
|
23
|
+
role: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @param historyAsc Chat history, chronologically ascending (oldest first).
|
|
27
|
+
* @param currentUserMessageId Id of the just-inserted user message on the
|
|
28
|
+
* send path; `undefined` on the regenerate path.
|
|
29
|
+
*/
|
|
30
|
+
export declare function dropCurrentUserMessage<T extends HistoryMessageLike>(historyAsc: T[], currentUserMessageId: string | undefined): T[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper for the conversation-history assembly in `router.ts`.
|
|
3
|
+
*
|
|
4
|
+
* The send path inserts the current user message into the store *before*
|
|
5
|
+
* building the model context, so the history window must drop that copy to
|
|
6
|
+
* avoid the model seeing it twice (it is appended explicitly as the final
|
|
7
|
+
* user message). The drop must be by id, not by tail position: the previous
|
|
8
|
+
* assistant reply persists asynchronously at stream end (createdAt = write
|
|
9
|
+
* time), so a fast follow-up message lands *before* it and the tail becomes
|
|
10
|
+
* `[..., u_new, a_prev]` — a positional pop then misses the duplicate
|
|
11
|
+
* (observed in the 2026-08-28 E2E request recording: every multi-turn user
|
|
12
|
+
* message was injected twice).
|
|
13
|
+
*
|
|
14
|
+
* The regenerate path inserts nothing; its truncation guarantees the
|
|
15
|
+
* triggering user message is the tail, so it keeps the positional pop.
|
|
16
|
+
*
|
|
17
|
+
* This module is pure so it can be unit-tested under plain Node/vitest —
|
|
18
|
+
* `router.ts` itself imports `better-sqlite3` via `storage`, which is
|
|
19
|
+
* compiled against Electron's ABI and can't load outside Electron.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* @param historyAsc Chat history, chronologically ascending (oldest first).
|
|
23
|
+
* @param currentUserMessageId Id of the just-inserted user message on the
|
|
24
|
+
* send path; `undefined` on the regenerate path.
|
|
25
|
+
*/
|
|
26
|
+
export function dropCurrentUserMessage(historyAsc, currentUserMessageId) {
|
|
27
|
+
if (currentUserMessageId) {
|
|
28
|
+
return historyAsc.filter((item) => item.id !== currentUserMessageId);
|
|
29
|
+
}
|
|
30
|
+
if (historyAsc.length > 0 && historyAsc[historyAsc.length - 1].role === 'user') {
|
|
31
|
+
return historyAsc.slice(0, -1);
|
|
32
|
+
}
|
|
33
|
+
return historyAsc;
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W7-1: pure helper for the interrupted-turn contract in `router.ts`.
|
|
3
|
+
*
|
|
4
|
+
* A turn whose process died mid-stream (crash / kill / lost stream) never
|
|
5
|
+
* reaches the code that persists `completionStatus`, so the crash signature
|
|
6
|
+
* is the surviving `turn_active:{chatId}` marker in `settings_kv` — written
|
|
7
|
+
* before the stream starts, cleared only after the terminal assistant
|
|
8
|
+
* message persists. User-cancelled and failed turns are settled states the
|
|
9
|
+
* live process DID record: their marker is cleared and their assistant
|
|
10
|
+
* message carries an explicit `completionStatus`, so neither is reported
|
|
11
|
+
* here. Pure so it is unit-testable — `storage/index.ts` can't load under
|
|
12
|
+
* plain Node/vitest (`better-sqlite3` is compiled against Electron's ABI).
|
|
13
|
+
*/
|
|
14
|
+
export interface InterruptedTurnInput {
|
|
15
|
+
/** The `turn_active` marker survived — a turn began and never settled. */
|
|
16
|
+
turnActive: boolean;
|
|
17
|
+
/** A stream for this chat is live in THIS process right now. */
|
|
18
|
+
streamActive: boolean;
|
|
19
|
+
/** Role of the chat's newest persisted message, or null on an empty chat. */
|
|
20
|
+
lastMessageRole: string | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Interrupted ⟺ marker present, no live stream, and the store does not
|
|
24
|
+
* already end on an assistant reply. The role check dismisses the stale
|
|
25
|
+
* marker left by a crash between the assistant write and the marker clear
|
|
26
|
+
* (adjacent statements, but the clear is deliberately last): the reply's
|
|
27
|
+
* presence proves the turn settled.
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectInterruptedTurn(input: InterruptedTurnInput): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W7-1: pure helper for the interrupted-turn contract in `router.ts`.
|
|
3
|
+
*
|
|
4
|
+
* A turn whose process died mid-stream (crash / kill / lost stream) never
|
|
5
|
+
* reaches the code that persists `completionStatus`, so the crash signature
|
|
6
|
+
* is the surviving `turn_active:{chatId}` marker in `settings_kv` — written
|
|
7
|
+
* before the stream starts, cleared only after the terminal assistant
|
|
8
|
+
* message persists. User-cancelled and failed turns are settled states the
|
|
9
|
+
* live process DID record: their marker is cleared and their assistant
|
|
10
|
+
* message carries an explicit `completionStatus`, so neither is reported
|
|
11
|
+
* here. Pure so it is unit-testable — `storage/index.ts` can't load under
|
|
12
|
+
* plain Node/vitest (`better-sqlite3` is compiled against Electron's ABI).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Interrupted ⟺ marker present, no live stream, and the store does not
|
|
16
|
+
* already end on an assistant reply. The role check dismisses the stale
|
|
17
|
+
* marker left by a crash between the assistant write and the marker clear
|
|
18
|
+
* (adjacent statements, but the clear is deliberately last): the reply's
|
|
19
|
+
* presence proves the turn settled.
|
|
20
|
+
*/
|
|
21
|
+
export function detectInterruptedTurn(input) {
|
|
22
|
+
if (!input.turnActive || input.streamActive)
|
|
23
|
+
return false;
|
|
24
|
+
return input.lastMessageRole !== 'assistant';
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 运行中回合的「实时快照」注册表 —— 让 renderer 在切走再切回时能把正在
|
|
3
|
+
* 流式的回合重新接上(显示运行状态 + 已产出的部分内容 / 工具卡片 / 时间线)。
|
|
4
|
+
*
|
|
5
|
+
* 背景:AgentChatView 卸载时不再取消回合(回合不因切页而暂停),但该回合
|
|
6
|
+
* 的 SSE 事件只会流向当初发起 fetch 的那个组件;切回后的新组件拿不到这些
|
|
7
|
+
* 事件,只能靠轮询本注册表把「运行中 + 部分产出」恢复出来。
|
|
8
|
+
*
|
|
9
|
+
* 设计:注册表只存「正在运行」的回合。router 的 handleCoreLoopTurn 在
|
|
10
|
+
* 流开始前 register、每个产出点更新 content、结束(落库后)立即 remove。
|
|
11
|
+
* executedActions / timeline / children 三个字段存的是 router 里同名的
|
|
12
|
+
* 活数组引用(appendTimelineDelta / syncTimelineTools / onToolAction 都
|
|
13
|
+
* 原地 mutate),因此 GET 序列化时自然读到最新状态,无需逐事件拷贝。
|
|
14
|
+
*/
|
|
15
|
+
import type { PersistedTurnBlock } from './turn-timeline.js';
|
|
16
|
+
export interface LiveStreamState {
|
|
17
|
+
chatId: string;
|
|
18
|
+
status: 'running' | 'completed' | 'cancelled' | 'failed';
|
|
19
|
+
/** 已产出的助手文本(部分)。 */
|
|
20
|
+
content: string;
|
|
21
|
+
/** 已产出的工具调用卡片(原地 mutate 的活数组)。 */
|
|
22
|
+
executedActions: Array<Record<string, unknown>>;
|
|
23
|
+
/** 调用顺序时间线(原地 mutate 的活数组)。 */
|
|
24
|
+
timeline: PersistedTurnBlock[];
|
|
25
|
+
/** 编排子代理生命周期事件(按到达顺序累积)。 */
|
|
26
|
+
children: Array<Record<string, unknown>>;
|
|
27
|
+
}
|
|
28
|
+
export declare function registerLiveStream(chatId: string, refs: {
|
|
29
|
+
executedActions: Array<Record<string, unknown>>;
|
|
30
|
+
timeline: PersistedTurnBlock[];
|
|
31
|
+
children: Array<Record<string, unknown>>;
|
|
32
|
+
}): LiveStreamState;
|
|
33
|
+
export declare function getLiveStream(chatId: string): LiveStreamState | undefined;
|
|
34
|
+
export declare function removeLiveStream(chatId: string): void;
|
|
35
|
+
/** 供测试 / 诊断使用。 */
|
|
36
|
+
export declare function liveStreamCount(): number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const liveStreams = new Map();
|
|
2
|
+
export function registerLiveStream(chatId, refs) {
|
|
3
|
+
const state = {
|
|
4
|
+
chatId,
|
|
5
|
+
status: 'running',
|
|
6
|
+
content: '',
|
|
7
|
+
executedActions: refs.executedActions,
|
|
8
|
+
timeline: refs.timeline,
|
|
9
|
+
children: refs.children,
|
|
10
|
+
};
|
|
11
|
+
liveStreams.set(chatId, state);
|
|
12
|
+
return state;
|
|
13
|
+
}
|
|
14
|
+
export function getLiveStream(chatId) {
|
|
15
|
+
return liveStreams.get(chatId);
|
|
16
|
+
}
|
|
17
|
+
export function removeLiveStream(chatId) {
|
|
18
|
+
liveStreams.delete(chatId);
|
|
19
|
+
}
|
|
20
|
+
/** 供测试 / 诊断使用。 */
|
|
21
|
+
export function liveStreamCount() {
|
|
22
|
+
return liveStreams.size;
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W-llm-diagnose: LLM link diagnosis orchestration.
|
|
3
|
+
*
|
|
4
|
+
* The settings page's "诊断" button runs a battery of probes against the
|
|
5
|
+
* configured provider: DNS resolution, TCP connect, TLS handshake, HTTP
|
|
6
|
+
* GET /models, and a minimal chat completion. Each probe is timed and
|
|
7
|
+
* classified so the UI can render a per-step verdict.
|
|
8
|
+
*
|
|
9
|
+
* The diagnosis runs in the host process (not the sidecar) because it
|
|
10
|
+
* needs to observe the *host's* network path — the sidecar's sandboxed
|
|
11
|
+
* view would hide proxy misconfigurations that the user needs to see.
|
|
12
|
+
*/
|
|
13
|
+
export interface DiagnoseStep {
|
|
14
|
+
name: string;
|
|
15
|
+
ok: boolean;
|
|
16
|
+
durationMs: number;
|
|
17
|
+
detail: string;
|
|
18
|
+
}
|
|
19
|
+
export interface DiagnoseResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
steps: DiagnoseStep[];
|
|
22
|
+
/** Ambient proxy endpoints detected on the host (env + system). */
|
|
23
|
+
ambientProxies: string[];
|
|
24
|
+
/** User-facing hint when a step fails, translated from the raw error. */
|
|
25
|
+
hint: string | null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Run the full diagnosis battery against `baseUrl` with `apiKey`.
|
|
29
|
+
* `model` is used for the chat-completion probe; when omitted the
|
|
30
|
+
* battery stops after the HTTP probe.
|
|
31
|
+
*/
|
|
32
|
+
export declare function diagnoseLlmConnection(options: {
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
model?: string;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}): Promise<DiagnoseResult>;
|