@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
|
@@ -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. */
|
|
@@ -544,6 +545,17 @@ export interface SidecarStreamChunk {
|
|
|
544
545
|
kind: string;
|
|
545
546
|
[key: string]: unknown;
|
|
546
547
|
};
|
|
548
|
+
/**
|
|
549
|
+
* The sidecar abandoned the attempt it was streaming and re-issued the
|
|
550
|
+
* request. Deltas received before this point belong to a reply that will
|
|
551
|
+
* never be completed, so the host withdraws them from the display rather
|
|
552
|
+
* than appending the retry's text after them.
|
|
553
|
+
*/
|
|
554
|
+
restart?: {
|
|
555
|
+
reason: string;
|
|
556
|
+
round?: number;
|
|
557
|
+
attempt?: number;
|
|
558
|
+
};
|
|
547
559
|
finishReason?: string;
|
|
548
560
|
usage?: {
|
|
549
561
|
promptTokens: number;
|
|
@@ -579,6 +591,22 @@ export interface SidecarStreamError {
|
|
|
579
591
|
traceId?: string;
|
|
580
592
|
kind: string;
|
|
581
593
|
message: string;
|
|
594
|
+
/**
|
|
595
|
+
* Provider classification (`transport` / `auth` / `rate_limit` / `server` /
|
|
596
|
+
* `context_overflow` / `invalid_request` / `unknown`). Absent from older
|
|
597
|
+
* sidecars and from non-provider failures; treat missing as `unknown` rather
|
|
598
|
+
* than parsing the message.
|
|
599
|
+
*/
|
|
600
|
+
errorKind?: string;
|
|
601
|
+
/**
|
|
602
|
+
* Proxy the model request went through, credentials stripped. Absent means
|
|
603
|
+
* the request went direct or the sidecar predates this field.
|
|
604
|
+
*/
|
|
605
|
+
proxy?: string;
|
|
606
|
+
/** Provider attempts spent on the failing request, including the last. */
|
|
607
|
+
attempts?: number;
|
|
608
|
+
/** HTTP status when the provider answered with one. */
|
|
609
|
+
statusCode?: number;
|
|
582
610
|
}
|
|
583
611
|
/**
|
|
584
612
|
* `agent.child` notification payload (child-agent lifecycle), demuxed by
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type TelemetrySettings } from './telemetry-settings.js';
|
|
|
4
4
|
import { type WebSearchSettings } from './web-search-settings.js';
|
|
5
5
|
import { type InsightKind, type InsightsProfile, type InsightsSettings, type InsightsSettingsPatch } from './insights-settings.js';
|
|
6
6
|
import { type UsageSummary } from './usage-summary.js';
|
|
7
|
+
import type { ReliabilitySummary } from './reliability-summary.js';
|
|
7
8
|
import type { TenantScope } from './driver.js';
|
|
8
9
|
import type { ScopedStore } from './scoped-store.js';
|
|
9
10
|
export type { UsageModelBucket, UsageSummary } from './usage-summary.js';
|
|
@@ -326,7 +327,32 @@ export declare class SqliteScopedStore implements ScopedStore {
|
|
|
326
327
|
totalTokens: number;
|
|
327
328
|
cachedPromptTokens?: number;
|
|
328
329
|
costUsd?: number | null;
|
|
330
|
+
/** How the turn ended; omitted keeps the historical 'completed' default. */
|
|
331
|
+
outcome?: 'completed' | 'failed' | 'cancelled';
|
|
332
|
+
/** Provider classification, recorded only for `failed`. */
|
|
333
|
+
failureKind?: string | null;
|
|
334
|
+
/** Model requests spent on the turn, retries included. */
|
|
335
|
+
attempts?: number | null;
|
|
336
|
+
/** Whether any request in the turn was retried. */
|
|
337
|
+
retried?: boolean;
|
|
338
|
+
/** Whether any request went direct instead of through the proxy. */
|
|
339
|
+
usedDirect?: boolean;
|
|
329
340
|
}): Promise<void>;
|
|
341
|
+
/**
|
|
342
|
+
* Reliability summary for the last `sinceDays` days.
|
|
343
|
+
*
|
|
344
|
+
* Splits the three terminal states apart because they answer different
|
|
345
|
+
* questions: a failed turn is something to fix, a cancelled one is the user's
|
|
346
|
+
* decision and must not inflate the failure rate, and a retried turn that
|
|
347
|
+
* completed is the measure of whether retrying is doing its job. Turns
|
|
348
|
+
* recorded before these columns existed default to `completed`, which is the
|
|
349
|
+
* only honest reading — their outcome was not observed.
|
|
350
|
+
*
|
|
351
|
+
* @param sinceDays Window length in days, ending now.
|
|
352
|
+
* @returns Counts per outcome, failures broken down by provider
|
|
353
|
+
* classification, and how many completed turns had retried.
|
|
354
|
+
*/
|
|
355
|
+
getReliabilitySummary(sinceDays?: number): Promise<ReliabilitySummary>;
|
|
330
356
|
/**
|
|
331
357
|
* 聚合最近 `sinceDays` 天的用量:按 model 分桶( token 合计 + 成本合计),
|
|
332
358
|
* 外加总计。成本只统计有单价的模型(cost_usd 非 NULL);无单价模型的
|
package/dist/storage/index.js
CHANGED
|
@@ -335,6 +335,20 @@ export class SqliteScopedStore {
|
|
|
335
335
|
this.ensureColumn('tasks', 'owner_instance_id', 'TEXT');
|
|
336
336
|
this.ensureColumn('insights_outbox', 'claimed_by', 'TEXT');
|
|
337
337
|
this.ensureColumn('insights_outbox', 'claimed_at', 'TEXT');
|
|
338
|
+
// 网络失败统计(二期 R4)。一次回合一行,与 token 用量共用一张表:
|
|
339
|
+
// 两者都是「这一回合发生了什么」的事实,分成两张表只会让复盘时
|
|
340
|
+
// 要 join。
|
|
341
|
+
//
|
|
342
|
+
// outcome 取值 completed / failed / cancelled。cancelled 是用户主动
|
|
343
|
+
// 停止,不算失败——界面与统计都不该把它和网络故障混为一谈。
|
|
344
|
+
this.ensureColumn('usage_events', 'outcome', `TEXT NOT NULL DEFAULT 'completed'`);
|
|
345
|
+
// 失败类别(provider 分类:transport / auth / rate_limit / …)。
|
|
346
|
+
this.ensureColumn('usage_events', 'failure_kind', 'TEXT');
|
|
347
|
+
// 本回合一共发出了几次模型请求(含重试)。
|
|
348
|
+
this.ensureColumn('usage_events', 'attempts', 'INTEGER');
|
|
349
|
+
// 是否发生过重试,以及是否改走过直连。
|
|
350
|
+
this.ensureColumn('usage_events', 'retried', 'INTEGER NOT NULL DEFAULT 0');
|
|
351
|
+
this.ensureColumn('usage_events', 'used_direct', 'INTEGER NOT NULL DEFAULT 0');
|
|
338
352
|
// 智能体技能面(增量迁移):load_all_skills = 无视触发条件全量加载。
|
|
339
353
|
// 此前这个行为是 router 里 `chatAgentId === 'all-round-assistant'` 的
|
|
340
354
|
// 硬编码,自建智能体拿不到;建列时一次性回填给内置「智能助手」,之后
|
|
@@ -1124,9 +1138,59 @@ export class SqliteScopedStore {
|
|
|
1124
1138
|
async recordUsageEvent(input) {
|
|
1125
1139
|
this.db.prepare(`
|
|
1126
1140
|
INSERT INTO usage_events
|
|
1127
|
-
(tenant_id, user_id, id, chat_id, kind, provider, model, prompt_tokens, completion_tokens, total_tokens, cached_prompt_tokens, cost_usd, created_at)
|
|
1128
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1129
|
-
`).run(this.scope.tenantId, this.scope.userId, randomUUID(), input.chatId ?? null, input.kind, input.provider ?? null, input.model ?? null, Math.max(0, Math.floor(input.promptTokens)), Math.max(0, Math.floor(input.completionTokens)), Math.max(0, Math.floor(input.totalTokens)), Math.max(0, Math.floor(input.cachedPromptTokens ?? 0)), input.costUsd ?? null, new Date().toISOString());
|
|
1141
|
+
(tenant_id, user_id, id, chat_id, kind, provider, model, prompt_tokens, completion_tokens, total_tokens, cached_prompt_tokens, cost_usd, created_at, outcome, failure_kind, attempts, retried, used_direct)
|
|
1142
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1143
|
+
`).run(this.scope.tenantId, this.scope.userId, randomUUID(), input.chatId ?? null, input.kind, input.provider ?? null, input.model ?? null, Math.max(0, Math.floor(input.promptTokens)), Math.max(0, Math.floor(input.completionTokens)), Math.max(0, Math.floor(input.totalTokens)), Math.max(0, Math.floor(input.cachedPromptTokens ?? 0)), input.costUsd ?? null, new Date().toISOString(), input.outcome ?? 'completed', input.failureKind ?? null, input.attempts ?? null, input.retried ? 1 : 0, input.usedDirect ? 1 : 0);
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Reliability summary for the last `sinceDays` days.
|
|
1147
|
+
*
|
|
1148
|
+
* Splits the three terminal states apart because they answer different
|
|
1149
|
+
* questions: a failed turn is something to fix, a cancelled one is the user's
|
|
1150
|
+
* decision and must not inflate the failure rate, and a retried turn that
|
|
1151
|
+
* completed is the measure of whether retrying is doing its job. Turns
|
|
1152
|
+
* recorded before these columns existed default to `completed`, which is the
|
|
1153
|
+
* only honest reading — their outcome was not observed.
|
|
1154
|
+
*
|
|
1155
|
+
* @param sinceDays Window length in days, ending now.
|
|
1156
|
+
* @returns Counts per outcome, failures broken down by provider
|
|
1157
|
+
* classification, and how many completed turns had retried.
|
|
1158
|
+
*/
|
|
1159
|
+
async getReliabilitySummary(sinceDays = 7) {
|
|
1160
|
+
const since = new Date(Date.now() - sinceDays * 24 * 60 * 60 * 1000).toISOString();
|
|
1161
|
+
const totals = this.db
|
|
1162
|
+
.prepare(`SELECT
|
|
1163
|
+
COUNT(*) AS total,
|
|
1164
|
+
SUM(CASE WHEN outcome = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
1165
|
+
SUM(CASE WHEN outcome = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
|
|
1166
|
+
SUM(CASE WHEN outcome = 'completed' AND retried = 1 THEN 1 ELSE 0 END) AS recovered,
|
|
1167
|
+
SUM(CASE WHEN used_direct = 1 THEN 1 ELSE 0 END) AS used_direct
|
|
1168
|
+
FROM usage_events
|
|
1169
|
+
WHERE tenant_id = ? AND user_id = ? AND created_at >= ?`)
|
|
1170
|
+
.get(this.scope.tenantId, this.scope.userId, since);
|
|
1171
|
+
const byKind = this.db
|
|
1172
|
+
.prepare(
|
|
1173
|
+
// Group by the expression, not the output alias: a bare `GROUP BY kind`
|
|
1174
|
+
// resolves to a source column when one exists, which silently merges
|
|
1175
|
+
// every row into the wrong bucket.
|
|
1176
|
+
`SELECT COALESCE(failure_kind, 'unknown') AS kind, COUNT(*) AS count
|
|
1177
|
+
FROM usage_events
|
|
1178
|
+
WHERE tenant_id = ? AND user_id = ? AND created_at >= ?
|
|
1179
|
+
AND outcome = 'failed'
|
|
1180
|
+
GROUP BY COALESCE(failure_kind, 'unknown')
|
|
1181
|
+
ORDER BY count DESC, kind`)
|
|
1182
|
+
.all(this.scope.tenantId, this.scope.userId, since);
|
|
1183
|
+
return {
|
|
1184
|
+
sinceDays,
|
|
1185
|
+
total: totals.total ?? 0,
|
|
1186
|
+
failed: totals.failed ?? 0,
|
|
1187
|
+
cancelled: totals.cancelled ?? 0,
|
|
1188
|
+
/** Completed turns that had retried at least once. */
|
|
1189
|
+
recoveredByRetry: totals.recovered ?? 0,
|
|
1190
|
+
/** Turns where the model request went direct instead of via a proxy. */
|
|
1191
|
+
usedDirect: totals.used_direct ?? 0,
|
|
1192
|
+
failuresByKind: byKind.map((row) => ({ kind: row.kind, count: row.count })),
|
|
1193
|
+
};
|
|
1130
1194
|
}
|
|
1131
1195
|
/**
|
|
1132
1196
|
* 聚合最近 `sinceDays` 天的用量:按 model 分桶( token 合计 + 成本合计),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 二期 R4 —— 网络失败的统计口径(纯逻辑)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `usage-summary.ts` 同样的拆分理由:`LocalStore` 顶层加载的
|
|
5
|
+
* better-sqlite3 native 模块与 vitest 运行时二进制不兼容,SQL 留在
|
|
6
|
+
* store 里,口径与派生量放这里,可以直接单测。
|
|
7
|
+
*
|
|
8
|
+
* 三个终态分开数,因为它们回答不同的问题:
|
|
9
|
+
*
|
|
10
|
+
* - `failed` 是要修的东西。失败率的分母用它。
|
|
11
|
+
* - `cancelled` 是用户自己的决定,**不算失败**。把停止算成失败,会让
|
|
12
|
+
* 「网络在变差」和「用户等得不耐烦」看起来一样。
|
|
13
|
+
* - `recoveredByRetry` 是重试有没有用的直接度量:重试过、最终成功了
|
|
14
|
+
* 的回合数。它低不代表重试没价值(可能是网络好),但配合失败数看就
|
|
15
|
+
* 能判断 R3 的策略是否合适。
|
|
16
|
+
*
|
|
17
|
+
* 早于这三列存在之前写入的行,`outcome` 取默认值 `completed`——那时候
|
|
18
|
+
* 也没观测过终态,按完成计是唯一诚实的读法。
|
|
19
|
+
*/
|
|
20
|
+
/** Provider failure classification, as `LLMError.kind` names them. */
|
|
21
|
+
export type FailureKind = 'transport' | 'rate_limit' | 'server' | 'auth' | 'invalid_request' | 'context_overflow' | 'unknown';
|
|
22
|
+
/** One failure category with its count. */
|
|
23
|
+
export interface FailureKindCount {
|
|
24
|
+
kind: string;
|
|
25
|
+
count: number;
|
|
26
|
+
}
|
|
27
|
+
/** Reliability numbers for one window. */
|
|
28
|
+
export interface ReliabilitySummary {
|
|
29
|
+
sinceDays: number;
|
|
30
|
+
/** Every recorded turn in the window, whatever its outcome. */
|
|
31
|
+
total: number;
|
|
32
|
+
failed: number;
|
|
33
|
+
cancelled: number;
|
|
34
|
+
/** Completed turns in which at least one model request was retried. */
|
|
35
|
+
recoveredByRetry: number;
|
|
36
|
+
/** Turns whose model request went direct instead of through a proxy. */
|
|
37
|
+
usedDirect: number;
|
|
38
|
+
/** Failure counts per provider classification, largest first. */
|
|
39
|
+
failuresByKind: FailureKindCount[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Share of turns that failed, as a number in [0, 1].
|
|
43
|
+
*
|
|
44
|
+
* Cancelled turns are excluded from the denominator: the user stopping is not
|
|
45
|
+
* evidence about the network. Returns null when nothing was observed, so a
|
|
46
|
+
* reader can tell "no failures" from "no data" — a dashboard that shows 0%
|
|
47
|
+
* on an empty window invites the wrong conclusion.
|
|
48
|
+
*/
|
|
49
|
+
export declare function failureRate(summary: ReliabilitySummary): number | null;
|
|
50
|
+
/**
|
|
51
|
+
* The network-attributable share of failures, in [0, 1], or null when there
|
|
52
|
+
* were none. `transport` is the kind this phase's work targets; the rest are
|
|
53
|
+
* configuration (`auth`), throttling, or the provider's own errors.
|
|
54
|
+
*/
|
|
55
|
+
export declare function networkFailureShare(summary: ReliabilitySummary): number | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 二期 R4 —— 网络失败的统计口径(纯逻辑)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `usage-summary.ts` 同样的拆分理由:`LocalStore` 顶层加载的
|
|
5
|
+
* better-sqlite3 native 模块与 vitest 运行时二进制不兼容,SQL 留在
|
|
6
|
+
* store 里,口径与派生量放这里,可以直接单测。
|
|
7
|
+
*
|
|
8
|
+
* 三个终态分开数,因为它们回答不同的问题:
|
|
9
|
+
*
|
|
10
|
+
* - `failed` 是要修的东西。失败率的分母用它。
|
|
11
|
+
* - `cancelled` 是用户自己的决定,**不算失败**。把停止算成失败,会让
|
|
12
|
+
* 「网络在变差」和「用户等得不耐烦」看起来一样。
|
|
13
|
+
* - `recoveredByRetry` 是重试有没有用的直接度量:重试过、最终成功了
|
|
14
|
+
* 的回合数。它低不代表重试没价值(可能是网络好),但配合失败数看就
|
|
15
|
+
* 能判断 R3 的策略是否合适。
|
|
16
|
+
*
|
|
17
|
+
* 早于这三列存在之前写入的行,`outcome` 取默认值 `completed`——那时候
|
|
18
|
+
* 也没观测过终态,按完成计是唯一诚实的读法。
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Share of turns that failed, as a number in [0, 1].
|
|
22
|
+
*
|
|
23
|
+
* Cancelled turns are excluded from the denominator: the user stopping is not
|
|
24
|
+
* evidence about the network. Returns null when nothing was observed, so a
|
|
25
|
+
* reader can tell "no failures" from "no data" — a dashboard that shows 0%
|
|
26
|
+
* on an empty window invites the wrong conclusion.
|
|
27
|
+
*/
|
|
28
|
+
export function failureRate(summary) {
|
|
29
|
+
const observed = summary.total - summary.cancelled;
|
|
30
|
+
if (observed <= 0)
|
|
31
|
+
return null;
|
|
32
|
+
return summary.failed / observed;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The network-attributable share of failures, in [0, 1], or null when there
|
|
36
|
+
* were none. `transport` is the kind this phase's work targets; the rest are
|
|
37
|
+
* configuration (`auth`), throttling, or the provider's own errors.
|
|
38
|
+
*/
|
|
39
|
+
export function networkFailureShare(summary) {
|
|
40
|
+
if (summary.failed <= 0)
|
|
41
|
+
return null;
|
|
42
|
+
const transport = summary.failuresByKind.find((row) => row.kind === 'transport');
|
|
43
|
+
return (transport?.count ?? 0) / summary.failed;
|
|
44
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { InsightKind, InsightsSettings, InsightsSettingsPatch } from './ins
|
|
|
3
3
|
import type { LlmSettings } from './llm-settings.js';
|
|
4
4
|
import type { TelemetrySettings } from './telemetry-settings.js';
|
|
5
5
|
import type { UsageSummary } from './usage-summary.js';
|
|
6
|
+
import type { ReliabilitySummary } from './reliability-summary.js';
|
|
6
7
|
import type { WebSearchSettings } from './web-search-settings.js';
|
|
7
8
|
import type { TenantScope } from './driver.js';
|
|
8
9
|
/** Explicit asynchronous storage API bound to one tenant and user. */
|
|
@@ -79,8 +80,14 @@ export interface ScopedStore {
|
|
|
79
80
|
totalTokens: number;
|
|
80
81
|
cachedPromptTokens?: number;
|
|
81
82
|
costUsd?: number | null;
|
|
83
|
+
outcome?: 'completed' | 'failed' | 'cancelled';
|
|
84
|
+
failureKind?: string | null;
|
|
85
|
+
attempts?: number | null;
|
|
86
|
+
retried?: boolean;
|
|
87
|
+
usedDirect?: boolean;
|
|
82
88
|
}): Promise<void>;
|
|
83
89
|
getUsageSummary(sinceDays?: number): Promise<UsageSummary>;
|
|
90
|
+
getReliabilitySummary(sinceDays?: number): Promise<ReliabilitySummary>;
|
|
84
91
|
getLlmSettings(): Promise<LlmSettings | null>;
|
|
85
92
|
setLlmSettings(settings: LlmSettings): Promise<LlmSettings>;
|
|
86
93
|
getTelemetrySettings(): Promise<TelemetrySettings | null>;
|
|
@@ -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.
|
|
3
|
+
"version": "0.8.0",
|
|
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.
|
|
57
|
-
"@steerable/agent-protocol": "0.
|
|
58
|
-
"@steerable/pack-sdk": "0.
|
|
56
|
+
"@steerable/agent-harness": "0.8.0",
|
|
57
|
+
"@steerable/agent-protocol": "0.8.0",
|
|
58
|
+
"@steerable/pack-sdk": "0.8.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/better-sqlite3": "^7.6.13",
|