@steerable/agent-shell 0.7.2 → 0.8.0
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/host/runtime.js +24 -1
- package/dist/local-backend/coreloop-stream.d.ts +25 -0
- package/dist/local-backend/coreloop-stream.js +11 -1
- package/dist/local-backend/live-stream.d.ts +10 -1
- package/dist/local-backend/live-stream.js +30 -0
- package/dist/local-backend/resumable-turn.d.ts +48 -0
- package/dist/local-backend/resumable-turn.js +53 -0
- package/dist/local-backend/router.d.ts +43 -3
- package/dist/local-backend/router.js +240 -29
- package/dist/local-backend/turn-timeline.d.ts +14 -0
- package/dist/local-backend/turn-timeline.js +21 -0
- package/dist/product-config.d.ts +17 -0
- package/dist/product-config.js +9 -0
- package/dist/sidecar/reverse-approval.d.ts +18 -3
- package/dist/sidecar/reverse-approval.js +23 -10
- package/dist/sidecar/reverse-ask-user.d.ts +19 -0
- package/dist/sidecar/reverse-ask-user.js +38 -1
- package/dist/sidecar/types.d.ts +29 -1
- package/dist/storage/index.d.ts +26 -0
- package/dist/storage/index.js +67 -3
- package/dist/storage/reliability-summary.d.ts +55 -0
- package/dist/storage/reliability-summary.js +44 -0
- package/dist/storage/scoped-store.d.ts +7 -0
- package/dist/tool-presentation.js +19 -1
- package/package.json +4 -4
package/dist/host/runtime.js
CHANGED
|
@@ -21,6 +21,7 @@ import { mcpExecutor } from '../mcp-executor.js';
|
|
|
21
21
|
import { ProjectRegistry } from '../project-registry.js';
|
|
22
22
|
import { getProductConfig } from '../product-config.js';
|
|
23
23
|
import { LocalBackendRouter } from '../local-backend/router.js';
|
|
24
|
+
import { getLiveStream, recordGrantedEgress } from '../local-backend/live-stream.js';
|
|
24
25
|
import { WorktreeService } from '../local-backend/worktree-service.js';
|
|
25
26
|
import { TaskService } from '../local-backend/task-service.js';
|
|
26
27
|
import { LoopPtyMonitor } from '../local-backend/loop-pty-monitor.js';
|
|
@@ -224,6 +225,27 @@ export async function createHostRuntime(options) {
|
|
|
224
225
|
...(declaredLabel ? { declaredLabel } : {}),
|
|
225
226
|
});
|
|
226
227
|
},
|
|
228
|
+
recordEgressGrant: (prompt, decision) => {
|
|
229
|
+
if (!prompt.chatId) {
|
|
230
|
+
onLog('approval: egress grant has no chatId; not stored');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const live = getLiveStream(prompt.chatId);
|
|
234
|
+
if (!live || live.status !== 'running')
|
|
235
|
+
return;
|
|
236
|
+
recordGrantedEgress(live, {
|
|
237
|
+
requestId: prompt.requestId,
|
|
238
|
+
arguments: prompt.arguments,
|
|
239
|
+
kind: decision.kind,
|
|
240
|
+
reason: decision.reason,
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
blockedCommand: (toolName, args) => {
|
|
244
|
+
if (toolName !== 'local_exec_shell')
|
|
245
|
+
return null;
|
|
246
|
+
const command = typeof args.command === 'string' ? args.command : '';
|
|
247
|
+
return localExecutor.detectDangerousCommand(command);
|
|
248
|
+
},
|
|
227
249
|
outsideWorkspace: async ({ toolName, arguments: args, chatId }) => {
|
|
228
250
|
if (!chatId || !localBackendRouter)
|
|
229
251
|
return false;
|
|
@@ -246,6 +268,7 @@ export async function createHostRuntime(options) {
|
|
|
246
268
|
hasWindow,
|
|
247
269
|
onLog: (line) => onLog(line),
|
|
248
270
|
});
|
|
271
|
+
localBackendRouter.setAskReplay((chatId) => askUserBridge.takeResolved(chatId));
|
|
249
272
|
let started = false;
|
|
250
273
|
let instanceLease = null;
|
|
251
274
|
let warmTimer = null;
|
|
@@ -293,7 +316,7 @@ export async function createHostRuntime(options) {
|
|
|
293
316
|
...(project?.sourceFolders ?? []),
|
|
294
317
|
...collectPackExecWritableRoots(chatId),
|
|
295
318
|
// 技能根:workflows/references/templates 是只读参考,模型必须能读
|
|
296
|
-
//
|
|
319
|
+
// (技能包里的版式说明曾因读不到而被拒)。
|
|
297
320
|
...listSkillRoots(),
|
|
298
321
|
// spill 根:工具输出过大时 sidecar 落到这里,模型要读回全文。
|
|
299
322
|
...spillReadRoots(),
|
|
@@ -37,8 +37,26 @@ export interface CoreLoopToolAction {
|
|
|
37
37
|
category?: string;
|
|
38
38
|
cached?: boolean;
|
|
39
39
|
waitMs?: number;
|
|
40
|
+
auto?: boolean;
|
|
40
41
|
};
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* What the sidecar knew about a failed model request, beyond its text.
|
|
45
|
+
*
|
|
46
|
+
* The host renders user-facing copy per `errorKind` and never parses
|
|
47
|
+
* `message` for it: the provider taxonomy is structured data, and message
|
|
48
|
+
* text is for the diagnostics panel.
|
|
49
|
+
*/
|
|
50
|
+
export interface TurnFailureInfo {
|
|
51
|
+
/** Provider classification; `undefined` when the sidecar did not send one. */
|
|
52
|
+
errorKind?: string;
|
|
53
|
+
/** Proxy the request used, credentials stripped; `undefined` means direct. */
|
|
54
|
+
proxy?: string;
|
|
55
|
+
/** Provider attempts spent on the failing request, including the last. */
|
|
56
|
+
attempts?: number;
|
|
57
|
+
/** HTTP status when the provider answered with one. */
|
|
58
|
+
statusCode?: number;
|
|
59
|
+
}
|
|
42
60
|
export interface CoreLoopTurnOutcome {
|
|
43
61
|
status: string;
|
|
44
62
|
reason?: string;
|
|
@@ -203,6 +221,13 @@ export interface StreamCoreLoopTurnOptions {
|
|
|
203
221
|
}) => void;
|
|
204
222
|
/** Fired when a tool finishes on the host (for tool-card rendering). */
|
|
205
223
|
onToolAction?: (action: CoreLoopToolAction) => void;
|
|
224
|
+
/**
|
|
225
|
+
* Fired when the sidecar abandons the attempt it was streaming and starts
|
|
226
|
+
* the request again (a retry after partial output). Everything emitted via
|
|
227
|
+
* `onText` / `onReasoning` since the last restart belongs to a reply the
|
|
228
|
+
* user will never see in full and must be withdrawn from the display.
|
|
229
|
+
*/
|
|
230
|
+
onRestart?: () => void;
|
|
206
231
|
/** soft_timeout / budget_exhausted notices from the loop. */
|
|
207
232
|
onNotice?: (kind: string, notice?: {
|
|
208
233
|
kind: string;
|
|
@@ -307,6 +307,9 @@ export async function streamCoreLoopTurn(options) {
|
|
|
307
307
|
// for the sidecar's cancelled done.
|
|
308
308
|
if (cancelling)
|
|
309
309
|
return;
|
|
310
|
+
if (chunk.restart) {
|
|
311
|
+
options.onRestart?.();
|
|
312
|
+
}
|
|
310
313
|
if (chunk.delta) {
|
|
311
314
|
options.onText(chunk.delta);
|
|
312
315
|
}
|
|
@@ -373,8 +376,15 @@ export async function streamCoreLoopTurn(options) {
|
|
|
373
376
|
// persist the partial trace — failed turns are the ones dogfooding
|
|
374
377
|
// most needs to inspect.
|
|
375
378
|
const failure = new Error(`coreloop stream failed: ${err.kind}: ${err.message}`);
|
|
379
|
+
const annotated = failure;
|
|
376
380
|
if (err.traceId)
|
|
377
|
-
|
|
381
|
+
annotated.traceId = err.traceId;
|
|
382
|
+
annotated.failureInfo = {
|
|
383
|
+
errorKind: err.errorKind,
|
|
384
|
+
proxy: err.proxy,
|
|
385
|
+
attempts: err.attempts,
|
|
386
|
+
statusCode: err.statusCode,
|
|
387
|
+
};
|
|
378
388
|
reject(failure);
|
|
379
389
|
},
|
|
380
390
|
})
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 活数组引用(appendTimelineDelta / syncTimelineTools / onToolAction 都
|
|
13
13
|
* 原地 mutate),因此 GET 序列化时自然读到最新状态,无需逐事件拷贝。
|
|
14
14
|
*/
|
|
15
|
-
import type
|
|
15
|
+
import { type PersistedTurnBlock } from './turn-timeline.js';
|
|
16
16
|
export interface LiveStreamState {
|
|
17
17
|
chatId: string;
|
|
18
18
|
status: 'running' | 'completed' | 'cancelled' | 'failed';
|
|
@@ -24,7 +24,16 @@ export interface LiveStreamState {
|
|
|
24
24
|
timeline: PersistedTurnBlock[];
|
|
25
25
|
/** 编排子代理生命周期事件(按到达顺序累积)。 */
|
|
26
26
|
children: Array<Record<string, unknown>>;
|
|
27
|
+
/** 出网放行写入 executedActions 后通知正在看这一轮的界面。 */
|
|
28
|
+
notify?: () => void;
|
|
27
29
|
}
|
|
30
|
+
/** 把一次已允许的出网放行记进正在运行的回合。这不是工具结果,模型看不到。 */
|
|
31
|
+
export declare function recordGrantedEgress(live: LiveStreamState, grant: {
|
|
32
|
+
requestId: string;
|
|
33
|
+
arguments: Record<string, unknown>;
|
|
34
|
+
kind: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
}): void;
|
|
28
37
|
export declare function registerLiveStream(chatId: string, refs: {
|
|
29
38
|
executedActions: Array<Record<string, unknown>>;
|
|
30
39
|
timeline: PersistedTurnBlock[];
|
|
@@ -1,4 +1,34 @@
|
|
|
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 { syncTimelineTools } from './turn-timeline.js';
|
|
1
16
|
const liveStreams = new Map();
|
|
17
|
+
/** 把一次已允许的出网放行记进正在运行的回合。这不是工具结果,模型看不到。 */
|
|
18
|
+
export function recordGrantedEgress(live, grant) {
|
|
19
|
+
live.executedActions.push({
|
|
20
|
+
id: `egress-${grant.requestId}`,
|
|
21
|
+
tool: 'network_egress',
|
|
22
|
+
arguments: grant.arguments,
|
|
23
|
+
success: true,
|
|
24
|
+
approval: {
|
|
25
|
+
kind: grant.kind,
|
|
26
|
+
...(grant.reason === 'unattended' ? { auto: true } : {}),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
syncTimelineTools(live.timeline, live.executedActions);
|
|
30
|
+
live.notify?.();
|
|
31
|
+
}
|
|
2
32
|
export function registerLiveStream(chatId, refs) {
|
|
3
33
|
const state = {
|
|
4
34
|
chatId,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which turns the `resume` channel may continue.
|
|
3
|
+
*
|
|
4
|
+
* `resume` replays the durable record's projection as the loop seed, so it can
|
|
5
|
+
* only continue a turn the record still ends mid-way through. Three settled
|
|
6
|
+
* states reach this decision, and they are not the same question:
|
|
7
|
+
*
|
|
8
|
+
* - **Interrupted** (W7-1): the process died, so the turn_active marker
|
|
9
|
+
* survived and no terminal assistant message was written. The user message
|
|
10
|
+
* is the record's tail; continuing finishes the request that never got an
|
|
11
|
+
* answer.
|
|
12
|
+
* - **Failed**: the turn's last model request exhausted its retries. The
|
|
13
|
+
* process is alive and settled the turn, so the marker is gone and the
|
|
14
|
+
* assistant message carries `completionStatus: 'failed'`. Everything
|
|
15
|
+
* completed before the failure is in the record — tools included — so
|
|
16
|
+
* continuing asks the model for the next step without repeating side
|
|
17
|
+
* effects.
|
|
18
|
+
* - **Cancelled**: the user stopped it. Continuing is allowed for the same
|
|
19
|
+
* reason as a failure, and deliberately: the usual cause is impatience with
|
|
20
|
+
* a retry, not a decision to abandon the task.
|
|
21
|
+
*
|
|
22
|
+
* A turn that completed normally is none of these. Resuming it would append a
|
|
23
|
+
* reply the user never asked for, which is why this is a whitelist: an
|
|
24
|
+
* unrecognised state refuses.
|
|
25
|
+
*/
|
|
26
|
+
/** Why a turn can be continued, as far as the store can tell. */
|
|
27
|
+
export type ResumableReason = 'interrupted' | 'failed' | 'cancelled';
|
|
28
|
+
export interface ResumableTurnInput {
|
|
29
|
+
/** The `turn_active` marker survived: a turn began and never settled. */
|
|
30
|
+
turnActive: boolean;
|
|
31
|
+
/** A stream for this chat is live in THIS process right now. */
|
|
32
|
+
streamActive: boolean;
|
|
33
|
+
/** Role of the chat's newest persisted message, or null on an empty chat. */
|
|
34
|
+
lastMessageRole: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* `completionStatus` of the last assistant message when the store ends on
|
|
37
|
+
* one. Absent when the newest message is not an assistant reply, or when the
|
|
38
|
+
* metadata predates the field.
|
|
39
|
+
*/
|
|
40
|
+
lastAssistantStatus?: string | null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns why the chat's last turn may be continued, or null when it may not.
|
|
44
|
+
*
|
|
45
|
+
* A live stream always refuses: the turn is running, and a second writer on
|
|
46
|
+
* the same record would interleave two tails.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resumableTurn(input: ResumableTurnInput): ResumableReason | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which turns the `resume` channel may continue.
|
|
3
|
+
*
|
|
4
|
+
* `resume` replays the durable record's projection as the loop seed, so it can
|
|
5
|
+
* only continue a turn the record still ends mid-way through. Three settled
|
|
6
|
+
* states reach this decision, and they are not the same question:
|
|
7
|
+
*
|
|
8
|
+
* - **Interrupted** (W7-1): the process died, so the turn_active marker
|
|
9
|
+
* survived and no terminal assistant message was written. The user message
|
|
10
|
+
* is the record's tail; continuing finishes the request that never got an
|
|
11
|
+
* answer.
|
|
12
|
+
* - **Failed**: the turn's last model request exhausted its retries. The
|
|
13
|
+
* process is alive and settled the turn, so the marker is gone and the
|
|
14
|
+
* assistant message carries `completionStatus: 'failed'`. Everything
|
|
15
|
+
* completed before the failure is in the record — tools included — so
|
|
16
|
+
* continuing asks the model for the next step without repeating side
|
|
17
|
+
* effects.
|
|
18
|
+
* - **Cancelled**: the user stopped it. Continuing is allowed for the same
|
|
19
|
+
* reason as a failure, and deliberately: the usual cause is impatience with
|
|
20
|
+
* a retry, not a decision to abandon the task.
|
|
21
|
+
*
|
|
22
|
+
* A turn that completed normally is none of these. Resuming it would append a
|
|
23
|
+
* reply the user never asked for, which is why this is a whitelist: an
|
|
24
|
+
* unrecognised state refuses.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Returns why the chat's last turn may be continued, or null when it may not.
|
|
28
|
+
*
|
|
29
|
+
* A live stream always refuses: the turn is running, and a second writer on
|
|
30
|
+
* the same record would interleave two tails.
|
|
31
|
+
*/
|
|
32
|
+
export function resumableTurn(input) {
|
|
33
|
+
if (input.streamActive)
|
|
34
|
+
return null;
|
|
35
|
+
if (input.turnActive) {
|
|
36
|
+
// A surviving marker with an assistant tail is the crash-between-write-
|
|
37
|
+
// and-clear window; the reply proves the turn settled, so it is not
|
|
38
|
+
// interrupted — but a failed reply may still be worth continuing.
|
|
39
|
+
return input.lastMessageRole === 'assistant'
|
|
40
|
+
? statusReason(input.lastAssistantStatus)
|
|
41
|
+
: 'interrupted';
|
|
42
|
+
}
|
|
43
|
+
if (input.lastMessageRole !== 'assistant')
|
|
44
|
+
return null;
|
|
45
|
+
return statusReason(input.lastAssistantStatus);
|
|
46
|
+
}
|
|
47
|
+
function statusReason(status) {
|
|
48
|
+
if (status === 'failed')
|
|
49
|
+
return 'failed';
|
|
50
|
+
if (status === 'cancelled')
|
|
51
|
+
return 'cancelled';
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type TurnFailureInfo } from './coreloop-stream.js';
|
|
1
2
|
import type { ScopedStore } from '../storage/scoped-store.js';
|
|
2
3
|
import { ToolRouter } from '../tool-router.js';
|
|
4
|
+
import { type ResolvedAsk } from '../sidecar/reverse-ask-user.js';
|
|
3
5
|
import type { TaskService } from './task-service.js';
|
|
4
6
|
import { type Principal } from '../auth/index.js';
|
|
5
7
|
export interface LocalBackendRequest {
|
|
@@ -34,10 +36,14 @@ export interface WakeResult {
|
|
|
34
36
|
status?: number;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
+
* Reads the structured failure detail off a rejected stream.
|
|
40
|
+
*
|
|
41
|
+
* `streamCoreLoopTurn` annotates its rejection; anything else (an abort, a
|
|
42
|
+
* router-level error) carries none, which the renderer shows as its generic
|
|
43
|
+
* card. Returns undefined rather than an empty object so the persisted
|
|
44
|
+
* metadata omits the field entirely.
|
|
39
45
|
*/
|
|
40
|
-
export declare function
|
|
46
|
+
export declare function readTurnFailureInfo(err: unknown): TurnFailureInfo | undefined;
|
|
41
47
|
export interface StreamOptions {
|
|
42
48
|
/**
|
|
43
49
|
* 取消信号。renderer 主动 Stop、窗口被销毁、应用退出时由 main 进程 abort。
|
|
@@ -46,6 +52,37 @@ export interface StreamOptions {
|
|
|
46
52
|
*/
|
|
47
53
|
signal?: AbortSignal;
|
|
48
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Extracts the fields a "reconnecting" indicator needs from a retry notice.
|
|
57
|
+
*
|
|
58
|
+
* The loop announces every retry as a `hook_action`; only attempts on the
|
|
59
|
+
* same model request are a wait the user notices, so the round and delay are
|
|
60
|
+
* carried through. Returns null for any other hook action so the caller can
|
|
61
|
+
* keep its round-boundary handling unchanged.
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* `completionStatus` of a persisted assistant message, or null.
|
|
65
|
+
*
|
|
66
|
+
* Only the terminal assistant message's status decides whether Continue is
|
|
67
|
+
* offered, so this reads the field and leaves the meaning to
|
|
68
|
+
* `resumableTurn`. A message without metadata (older builds) yields null,
|
|
69
|
+
* which refuses — resuming a turn whose end state is unknown could append a
|
|
70
|
+
* reply to a conversation that already finished.
|
|
71
|
+
*/
|
|
72
|
+
export declare function readCompletionStatus(message: {
|
|
73
|
+
messageMetadata?: string | null;
|
|
74
|
+
} | undefined): string | null;
|
|
75
|
+
export declare function readRetryNotice(notice: {
|
|
76
|
+
kind?: string;
|
|
77
|
+
action?: unknown;
|
|
78
|
+
delayMs?: unknown;
|
|
79
|
+
round?: unknown;
|
|
80
|
+
reason?: unknown;
|
|
81
|
+
} | undefined): {
|
|
82
|
+
round?: number;
|
|
83
|
+
delayMs?: number;
|
|
84
|
+
reason?: string;
|
|
85
|
+
} | null;
|
|
49
86
|
/**
|
|
50
87
|
* Out-of-band 事件推送回调。用于把"在请求生命周期之外"才出结果的事件(例如
|
|
51
88
|
* 后台 AI 标题生成)异步广播到所有 renderer。main.ts 注入 `webContents.send`
|
|
@@ -60,12 +97,15 @@ export declare class LocalBackendRouter {
|
|
|
60
97
|
private readonly broadcast;
|
|
61
98
|
/** 4.6a/4.6c:任务面板的路由入口(列表 + 合并/丢弃)。未注入时任务路由 503。 */
|
|
62
99
|
private readonly taskService;
|
|
100
|
+
private takeAskReplay;
|
|
63
101
|
constructor(toolRouter: ToolRouter, options: {
|
|
64
102
|
store: ScopedStore;
|
|
65
103
|
resolveStore?: (principal?: Principal) => ScopedStore;
|
|
66
104
|
broadcast?: LocalBackendBroadcast;
|
|
67
105
|
taskService?: TaskService;
|
|
68
106
|
});
|
|
107
|
+
/** The host records each answered ask; the following tool row stores that text. */
|
|
108
|
+
setAskReplay(take: (chatId: string) => ResolvedAsk | undefined): void;
|
|
69
109
|
private get store();
|
|
70
110
|
/** Store selected for the active request, or the host default outside one. */
|
|
71
111
|
get activeStore(): ScopedStore;
|