@steerable/agent-shell 0.7.2 → 0.7.4
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 +1 -0
- package/dist/local-backend/live-stream.d.ts +10 -1
- package/dist/local-backend/live-stream.js +30 -0
- package/dist/local-backend/router.d.ts +4 -0
- package/dist/local-backend/router.js +13 -2
- 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 +2 -1
- 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(),
|
|
@@ -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,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ScopedStore } from '../storage/scoped-store.js';
|
|
2
2
|
import { ToolRouter } from '../tool-router.js';
|
|
3
|
+
import { type ResolvedAsk } from '../sidecar/reverse-ask-user.js';
|
|
3
4
|
import type { TaskService } from './task-service.js';
|
|
4
5
|
import { type Principal } from '../auth/index.js';
|
|
5
6
|
export interface LocalBackendRequest {
|
|
@@ -60,12 +61,15 @@ export declare class LocalBackendRouter {
|
|
|
60
61
|
private readonly broadcast;
|
|
61
62
|
/** 4.6a/4.6c:任务面板的路由入口(列表 + 合并/丢弃)。未注入时任务路由 503。 */
|
|
62
63
|
private readonly taskService;
|
|
64
|
+
private takeAskReplay;
|
|
63
65
|
constructor(toolRouter: ToolRouter, options: {
|
|
64
66
|
store: ScopedStore;
|
|
65
67
|
resolveStore?: (principal?: Principal) => ScopedStore;
|
|
66
68
|
broadcast?: LocalBackendBroadcast;
|
|
67
69
|
taskService?: TaskService;
|
|
68
70
|
});
|
|
71
|
+
/** The host records each answered ask; the following tool row stores that text. */
|
|
72
|
+
setAskReplay(take: (chatId: string) => ResolvedAsk | undefined): void;
|
|
69
73
|
private get store();
|
|
70
74
|
/** Store selected for the active request, or the host default outside one. */
|
|
71
75
|
get activeStore(): ScopedStore;
|
|
@@ -27,6 +27,7 @@ import { sanitizeCompatOverrides, sanitizePresetsChoice, sanitizeLlmProvider, sa
|
|
|
27
27
|
import { setDefaultExecTimeoutMs } from '../local-executor.js';
|
|
28
28
|
import { allowEgressForBaseUrl, getActiveEgressBroker, getEgressPosture } from '../sidecar/egress-proxy.js';
|
|
29
29
|
import { buildExecSandbox, parseExecPolicy } from '../sidecar/exec-sandbox.js';
|
|
30
|
+
import { storedAskResult } from '../sidecar/reverse-ask-user.js';
|
|
30
31
|
import { clampExecPolicy } from '../host-tools.js';
|
|
31
32
|
import { getResolvedHostTools, isProductLlmLocked, resolveRuntimeLlmSettings, resolveTurnApproval, resolveTurnChatMode, resolveTurnOrchestration, } from '../host-tools-runtime.js';
|
|
32
33
|
import { SidecarSupervisor } from '../sidecar/index.js';
|
|
@@ -320,6 +321,7 @@ export class LocalBackendRouter {
|
|
|
320
321
|
broadcast;
|
|
321
322
|
/** 4.6a/4.6c:任务面板的路由入口(列表 + 合并/丢弃)。未注入时任务路由 503。 */
|
|
322
323
|
taskService;
|
|
324
|
+
takeAskReplay = null;
|
|
323
325
|
constructor(toolRouter, options) {
|
|
324
326
|
this.toolRouter = toolRouter;
|
|
325
327
|
this.defaultStore = options.store;
|
|
@@ -331,6 +333,10 @@ export class LocalBackendRouter {
|
|
|
331
333
|
this.broadcast?.('goal-changed', { chatId, goal });
|
|
332
334
|
});
|
|
333
335
|
}
|
|
336
|
+
/** The host records each answered ask; the following tool row stores that text. */
|
|
337
|
+
setAskReplay(take) {
|
|
338
|
+
this.takeAskReplay = take;
|
|
339
|
+
}
|
|
334
340
|
get store() {
|
|
335
341
|
return this.storeContext.getStore() ?? this.defaultStore;
|
|
336
342
|
}
|
|
@@ -3674,6 +3680,9 @@ export class LocalBackendRouter {
|
|
|
3674
3680
|
// 实时快照:executedActions / timeline / children 传引用(原地 mutate),
|
|
3675
3681
|
// content 由 onText 逐段更新。切走再切回的 renderer 靠 GET /live-stream 读它。
|
|
3676
3682
|
const live = registerLiveStream(chatId, { executedActions, timeline, children });
|
|
3683
|
+
live.notify = () => {
|
|
3684
|
+
emit(this.sseData({ type: 'executed_actions', actions: [...executedActions] }));
|
|
3685
|
+
};
|
|
3677
3686
|
// 场景包的回合观察器(如识别包技能调用并广播产物更新)。
|
|
3678
3687
|
const packObservers = beginPackTurnObservers({ chatId, broadcast: this.broadcast ?? undefined });
|
|
3679
3688
|
// 回合产物文件列表(回合收尾时收集):扫描根与 exec 沙箱可写根同源
|
|
@@ -3817,13 +3826,15 @@ export class LocalBackendRouter {
|
|
|
3817
3826
|
const idx = action.id
|
|
3818
3827
|
? executedActions.findIndex((row) => row.id === action.id)
|
|
3819
3828
|
: -1;
|
|
3829
|
+
const replay = action.tool === 'ask_user' ? this.takeAskReplay?.(args.chatId) : undefined;
|
|
3830
|
+
const result = replay ? storedAskResult(replay) : action.result;
|
|
3820
3831
|
const row = {
|
|
3821
3832
|
id: action.id,
|
|
3822
3833
|
tool: action.tool,
|
|
3823
3834
|
mode: chatMode,
|
|
3824
3835
|
arguments: action.arguments,
|
|
3825
|
-
view: presentToolCall(action.tool, action.arguments,
|
|
3826
|
-
result
|
|
3836
|
+
view: presentToolCall(action.tool, action.arguments, result),
|
|
3837
|
+
result,
|
|
3827
3838
|
success: action.success,
|
|
3828
3839
|
error: action.error,
|
|
3829
3840
|
durationMs: action.durationMs,
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Fail-closed by construction: no window, no listener, a renderer error,
|
|
7
7
|
* or an invalid reply all become `deny_once` — never a hang, never an
|
|
8
|
-
* auto-allow.
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
8
|
+
* auto-allow. A command on the dangerous-command list is the exception:
|
|
9
|
+
* the bridge returns `allow_once` without a card so the executor can
|
|
10
|
+
* reject it with the blocked-command error. An explicit `timeoutMs` on
|
|
11
|
+
* the approval config still fails closed as `timed_out`; the desktop
|
|
12
|
+
* omits it, so the prompt waits until the user decides.
|
|
11
13
|
*
|
|
12
14
|
* The renderer answers via the `approval:decide` invoke with the requestId
|
|
13
15
|
* echoed back; decisions are validated against the algebra's kind set
|
|
@@ -68,6 +70,19 @@ export interface ApprovalBridgeDeps {
|
|
|
68
70
|
arguments: Record<string, unknown>;
|
|
69
71
|
chatId?: string;
|
|
70
72
|
}) => boolean | 'project' | 'workspace' | Promise<boolean | 'project' | 'workspace'>;
|
|
73
|
+
/**
|
|
74
|
+
* Pattern that will block this command in the executor.
|
|
75
|
+
* When set, the bridge does not show a card.
|
|
76
|
+
*/
|
|
77
|
+
blockedCommand?: (toolName: string, args: Record<string, unknown>) => string | null;
|
|
78
|
+
/**
|
|
79
|
+
* An allowed network_egress grant is not a tool result. The host stores it
|
|
80
|
+
* on the running turn so the row survives reopening the chat.
|
|
81
|
+
*/
|
|
82
|
+
recordEgressGrant?: (prompt: ApprovalPromptRequest, decision: {
|
|
83
|
+
kind: ApprovalDecisionKind;
|
|
84
|
+
reason: string;
|
|
85
|
+
}) => void;
|
|
71
86
|
}
|
|
72
87
|
export interface ApprovalBridge {
|
|
73
88
|
/** Reverse-channel handler for the sidecar's `approval.request`. */
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Fail-closed by construction: no window, no listener, a renderer error,
|
|
7
7
|
* or an invalid reply all become `deny_once` — never a hang, never an
|
|
8
|
-
* auto-allow.
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
8
|
+
* auto-allow. A command on the dangerous-command list is the exception:
|
|
9
|
+
* the bridge returns `allow_once` without a card so the executor can
|
|
10
|
+
* reject it with the blocked-command error. An explicit `timeoutMs` on
|
|
11
|
+
* the approval config still fails closed as `timed_out`; the desktop
|
|
12
|
+
* omits it, so the prompt waits until the user decides.
|
|
11
13
|
*
|
|
12
14
|
* The renderer answers via the `approval:decide` invoke with the requestId
|
|
13
15
|
* echoed back; decisions are validated against the algebra's kind set
|
|
@@ -38,15 +40,19 @@ export function createApprovalBridge(deps) {
|
|
|
38
40
|
if (!toolName) {
|
|
39
41
|
return { kind: 'deny_once', reason: 'malformed approval request (no toolName)' };
|
|
40
42
|
}
|
|
43
|
+
const args = p.arguments && typeof p.arguments === 'object'
|
|
44
|
+
? p.arguments
|
|
45
|
+
: {};
|
|
46
|
+
if (deps.blockedCommand?.(toolName, args)) {
|
|
47
|
+
deps.onLog?.(`approval: ${toolName} matches the dangerous-command list; no card`);
|
|
48
|
+
return { kind: 'allow_once', reason: '' };
|
|
49
|
+
}
|
|
41
50
|
if (!deps.hasWindow()) {
|
|
42
51
|
deps.onLog?.(`approval: no renderer window; denying ${toolName} once`);
|
|
43
52
|
return deny('no renderer window available to approve the call');
|
|
44
53
|
}
|
|
45
54
|
const requestId = randomUUID();
|
|
46
55
|
const chatId = typeof p.chatId === 'string' && p.chatId ? p.chatId : undefined;
|
|
47
|
-
const args = p.arguments && typeof p.arguments === 'object'
|
|
48
|
-
? p.arguments
|
|
49
|
-
: {};
|
|
50
56
|
const category = typeof p.category === 'string' ? p.category : toolName;
|
|
51
57
|
const categoryLabel = deps.categoryLabel?.(toolName, category);
|
|
52
58
|
const fence = deps.outsideWorkspace
|
|
@@ -91,10 +97,17 @@ export function createApprovalBridge(deps) {
|
|
|
91
97
|
entry.resolve(deny(`renderer returned an invalid decision kind: ${String(p.kind)}`));
|
|
92
98
|
return { ok: true };
|
|
93
99
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const reason = typeof p.reason === 'string' ? p.reason : '';
|
|
101
|
+
entry.resolve({ kind, reason });
|
|
102
|
+
if (entry.prompt.toolName === 'network_egress'
|
|
103
|
+
&& (kind === 'allow_once' || kind === 'allow_for_session' || kind === 'allow_always')) {
|
|
104
|
+
try {
|
|
105
|
+
deps.recordEgressGrant?.(entry.prompt, { kind, reason });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
deps.onLog?.(`approval: failed to store egress grant: ${err instanceof Error ? err.message : String(err)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
98
111
|
return { ok: true };
|
|
99
112
|
},
|
|
100
113
|
};
|
|
@@ -19,6 +19,13 @@ export interface AskUserPromptRequest {
|
|
|
19
19
|
/** 发起提问的对话。renderer 只在这条会话的输入框展示卡片。 */
|
|
20
20
|
chatId?: string;
|
|
21
21
|
}
|
|
22
|
+
/** One answered ask, kept so the tool row can store the full text. */
|
|
23
|
+
export interface ResolvedAsk {
|
|
24
|
+
intro: string;
|
|
25
|
+
questions: Array<Record<string, unknown>>;
|
|
26
|
+
answers: Record<string, string | string[]>;
|
|
27
|
+
unattended?: boolean;
|
|
28
|
+
}
|
|
22
29
|
export interface AskUserBridgeDeps {
|
|
23
30
|
broadcast: (channel: 'ask-user:request', payload: AskUserPromptRequest) => void;
|
|
24
31
|
/** 是否有能应答的窗口/浏览器连接;没有则立即空答。 */
|
|
@@ -34,5 +41,17 @@ export interface AskUserBridge {
|
|
|
34
41
|
answer: (payload: unknown) => {
|
|
35
42
|
ok: boolean;
|
|
36
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* The next answered ask for this chat. The tool result that follows only
|
|
46
|
+
* carries a short preview, so the turn stores this instead.
|
|
47
|
+
*/
|
|
48
|
+
takeResolved: (chatId: string) => ResolvedAsk | undefined;
|
|
37
49
|
}
|
|
50
|
+
/** The tool-row result stored in place of the truncated preview. */
|
|
51
|
+
export declare function storedAskResult(ask: ResolvedAsk): {
|
|
52
|
+
intro: string;
|
|
53
|
+
questions: Array<Record<string, unknown>>;
|
|
54
|
+
answers: Record<string, string | string[]>;
|
|
55
|
+
unattended?: true;
|
|
56
|
+
};
|
|
38
57
|
export declare function createAskUserBridge(deps: AskUserBridgeDeps): AskUserBridge;
|
|
@@ -11,25 +11,59 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { randomUUID } from 'node:crypto';
|
|
13
13
|
const EMPTY_REPLY = { answers: {} };
|
|
14
|
+
function rememberAsk(resolved, chatId, intro, questions, answers, unattended) {
|
|
15
|
+
if (questions.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
const key = chatId ?? '';
|
|
18
|
+
const queue = resolved.get(key) ?? [];
|
|
19
|
+
queue.push({
|
|
20
|
+
intro,
|
|
21
|
+
questions,
|
|
22
|
+
answers,
|
|
23
|
+
...(unattended ? { unattended: true } : {}),
|
|
24
|
+
});
|
|
25
|
+
resolved.set(key, queue);
|
|
26
|
+
}
|
|
27
|
+
function shiftAsk(resolved, key) {
|
|
28
|
+
const queue = resolved.get(key);
|
|
29
|
+
if (!queue || queue.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
const next = queue.shift();
|
|
32
|
+
if (queue.length === 0)
|
|
33
|
+
resolved.delete(key);
|
|
34
|
+
return next;
|
|
35
|
+
}
|
|
36
|
+
/** The tool-row result stored in place of the truncated preview. */
|
|
37
|
+
export function storedAskResult(ask) {
|
|
38
|
+
return {
|
|
39
|
+
intro: ask.intro,
|
|
40
|
+
questions: ask.questions,
|
|
41
|
+
answers: ask.answers,
|
|
42
|
+
...(ask.unattended ? { unattended: true } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
14
45
|
export function createAskUserBridge(deps) {
|
|
15
46
|
const pending = new Map();
|
|
47
|
+
const resolved = new Map();
|
|
16
48
|
return {
|
|
17
49
|
handler: (params) => {
|
|
18
50
|
const p = (params ?? {});
|
|
19
51
|
const questions = Array.isArray(p.questions) ? p.questions : [];
|
|
20
52
|
const chatId = typeof p.chatId === 'string' && p.chatId ? p.chatId : undefined;
|
|
53
|
+
const intro = typeof p.intro === 'string' ? p.intro : '';
|
|
21
54
|
if (questions.length === 0) {
|
|
22
55
|
deps.onLog?.('ask_user: malformed request (no questions); answering empty');
|
|
23
56
|
return Promise.resolve({ ...EMPTY_REPLY });
|
|
24
57
|
}
|
|
25
58
|
if (!deps.hasWindow()) {
|
|
26
59
|
deps.onLog?.('ask_user: no renderer window; answering empty');
|
|
60
|
+
rememberAsk(resolved, chatId, intro, questions, {}, false);
|
|
27
61
|
return Promise.resolve({ ...EMPTY_REPLY });
|
|
28
62
|
}
|
|
29
63
|
const requestId = randomUUID();
|
|
30
64
|
const prompt = {
|
|
31
65
|
requestId,
|
|
32
|
-
intro
|
|
66
|
+
intro,
|
|
33
67
|
questions: questions,
|
|
34
68
|
...(chatId ? { chatId } : {}),
|
|
35
69
|
};
|
|
@@ -41,6 +75,7 @@ export function createAskUserBridge(deps) {
|
|
|
41
75
|
catch (err) {
|
|
42
76
|
pending.delete(requestId);
|
|
43
77
|
deps.onLog?.(`ask_user: broadcast failed: ${String(err)}`);
|
|
78
|
+
rememberAsk(resolved, chatId, intro, prompt.questions, {}, false);
|
|
44
79
|
resolve({ ...EMPTY_REPLY });
|
|
45
80
|
}
|
|
46
81
|
});
|
|
@@ -55,8 +90,10 @@ export function createAskUserBridge(deps) {
|
|
|
55
90
|
const answers = p.answers && typeof p.answers === 'object' && !Array.isArray(p.answers)
|
|
56
91
|
? p.answers
|
|
57
92
|
: {};
|
|
93
|
+
rememberAsk(resolved, entry.prompt.chatId, entry.prompt.intro, entry.prompt.questions, answers, p.unattended === true);
|
|
58
94
|
entry.resolve({ answers });
|
|
59
95
|
return { ok: true };
|
|
60
96
|
},
|
|
97
|
+
takeResolved: (chatId) => shiftAsk(resolved, chatId) ?? (chatId ? shiftAsk(resolved, '') : undefined),
|
|
61
98
|
};
|
|
62
99
|
}
|
package/dist/sidecar/types.d.ts
CHANGED
|
@@ -531,12 +531,13 @@ export interface SidecarStreamChunk {
|
|
|
531
531
|
backend?: string;
|
|
532
532
|
enforcement: string;
|
|
533
533
|
};
|
|
534
|
-
/** Session-allow reuse. `cached` means this call did not show a card. */
|
|
534
|
+
/** Session-allow reuse. `cached` means this call did not show a card. `auto` is an unattended allow. */
|
|
535
535
|
approval?: {
|
|
536
536
|
kind?: string;
|
|
537
537
|
category?: string;
|
|
538
538
|
cached?: boolean;
|
|
539
539
|
waitMs?: number;
|
|
540
|
+
auto?: boolean;
|
|
540
541
|
};
|
|
541
542
|
};
|
|
542
543
|
/** CoreLoop notices: soft_timeout / budget_exhausted / round_end / hook_action. */
|
|
@@ -38,6 +38,24 @@ function dataOf(result) {
|
|
|
38
38
|
function failed(result) {
|
|
39
39
|
return Boolean(result && typeof result === 'object' && result.success === false);
|
|
40
40
|
}
|
|
41
|
+
function askTitle(args, result) {
|
|
42
|
+
return clip(firstQuestion(args) || firstQuestion(result) || str(args, 'intro') || str(args, 'question')) || '提问';
|
|
43
|
+
}
|
|
44
|
+
function firstQuestion(value) {
|
|
45
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
46
|
+
return '';
|
|
47
|
+
const questions = value.questions;
|
|
48
|
+
if (!Array.isArray(questions))
|
|
49
|
+
return '';
|
|
50
|
+
for (const question of questions) {
|
|
51
|
+
if (!question || typeof question !== 'object' || Array.isArray(question))
|
|
52
|
+
continue;
|
|
53
|
+
const text = str(question, 'text').trim();
|
|
54
|
+
if (text)
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
41
59
|
function commandTitle(args) {
|
|
42
60
|
return clip(str(args, 'command')) || 'shell';
|
|
43
61
|
}
|
|
@@ -230,7 +248,7 @@ const PRESENTERS = {
|
|
|
230
248
|
ask_user: {
|
|
231
249
|
card: 'generic',
|
|
232
250
|
kind: 'other',
|
|
233
|
-
title: (args
|
|
251
|
+
title: (args, result) => askTitle(args, result),
|
|
234
252
|
},
|
|
235
253
|
skill: { card: 'generic', kind: 'read', title: (args) => str(args, 'name') || 'skill' },
|
|
236
254
|
delegate_subagent: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Steerable framework — product-neutral Node HostRuntime and headless HTTP server shared by Tauri desktop and browser deployments; scenario packs extend it through @steerable/pack-sdk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"jpeg-js": "^0.4.4",
|
|
54
54
|
"node-pty": "^1.1.0",
|
|
55
55
|
"pngjs": "^7.0.0",
|
|
56
|
-
"@steerable/agent-harness": "0.7.
|
|
57
|
-
"@steerable/agent-protocol": "0.7.
|
|
58
|
-
"@steerable/pack-sdk": "0.7.
|
|
56
|
+
"@steerable/agent-harness": "0.7.4",
|
|
57
|
+
"@steerable/agent-protocol": "0.7.4",
|
|
58
|
+
"@steerable/pack-sdk": "0.7.4"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/better-sqlite3": "^7.6.13",
|