@steerable/agent-shell 0.6.21 → 0.6.24

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.
Files changed (48) hide show
  1. package/README.md +34 -0
  2. package/dist/auth/index.d.ts +38 -0
  3. package/dist/auth/index.js +20 -0
  4. package/dist/host/pack-assembly.d.ts +7 -7
  5. package/dist/host/runtime.d.ts +6 -2
  6. package/dist/host/runtime.js +54 -14
  7. package/dist/insights/flush.d.ts +5 -4
  8. package/dist/insights/flush.js +14 -15
  9. package/dist/insights/record.d.ts +6 -5
  10. package/dist/insights/record.js +9 -10
  11. package/dist/llm/index.d.ts +6 -2
  12. package/dist/llm/index.js +19 -9
  13. package/dist/local-backend/prompt-builder.d.ts +10 -4
  14. package/dist/local-backend/prompt-builder.js +13 -12
  15. package/dist/local-backend/router.d.ts +18 -4
  16. package/dist/local-backend/router.js +133 -98
  17. package/dist/local-backend/skills/00-identity/SKILL.md +1 -1
  18. package/dist/local-backend/task-service.d.ts +14 -13
  19. package/dist/local-backend/task-service.js +32 -29
  20. package/dist/local-backend/worktree-service.d.ts +1 -1
  21. package/dist/local-backend/worktree-service.js +6 -6
  22. package/dist/main.js +43 -26
  23. package/dist/server/http-server.d.ts +13 -1
  24. package/dist/server/http-server.js +37 -8
  25. package/dist/server/index.js +4 -90
  26. package/dist/server/start.d.ts +26 -0
  27. package/dist/server/start.js +111 -0
  28. package/dist/sidecar/boot.d.ts +3 -1
  29. package/dist/sidecar/boot.js +6 -7
  30. package/dist/sidecar/reverse-spawn.js +12 -4
  31. package/dist/sidecar/reverse-tools.d.ts +1 -1
  32. package/dist/sidecar/reverse-tools.js +1 -1
  33. package/dist/sidecar/storage-path.js +2 -3
  34. package/dist/storage/driver.d.ts +57 -0
  35. package/dist/storage/driver.js +104 -0
  36. package/dist/storage/index.d.ts +75 -68
  37. package/dist/storage/index.js +506 -301
  38. package/dist/storage/pack-migrations.d.ts +2 -4
  39. package/dist/storage/pack-migrations.js +2 -4
  40. package/dist/storage/scoped-store.d.ts +125 -0
  41. package/dist/storage/scoped-store.js +1 -0
  42. package/dist/storage/sqlite-driver.d.ts +16 -0
  43. package/dist/storage/sqlite-driver.js +135 -0
  44. package/dist/storage/write-lease-error.d.ts +11 -0
  45. package/dist/storage/write-lease-error.js +18 -0
  46. package/package.json +14 -5
  47. package/dist/storage/local-store-singleton.d.ts +0 -28
  48. package/dist/storage/local-store-singleton.js +0 -38
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @steerable/agent-shell
2
+
3
+ Product-neutral Electron and headless HTTP host for Steerable Framework.
4
+
5
+ ## BS composition
6
+
7
+ Products can register an `AuthProvider` from `@steerable/agent-shell/auth`
8
+ before starting the host. A registered provider authenticates every
9
+ `/api/v2/*` and `/host/*` request and supplies the request `Principal`.
10
+ Without a provider, the host retains its loopback Host check and
11
+ per-process Bearer token.
12
+
13
+ Use `startBsHost` from `@steerable/agent-shell/server/start` to embed the BS
14
+ host. Its options support host and port selection, a fixed Bearer token, and
15
+ ordered middleware. Middleware returns `pass` to continue or `handled` to
16
+ short-circuit. The returned handle owns shutdown of both HTTP and runtime
17
+ services.
18
+
19
+ ## Storage drivers
20
+
21
+ Storage drivers register during product composition and initialize after all
22
+ scenario-pack migrations and seeds are registered. `initializeStorage()` must
23
+ complete before `getScopedStore(scope)` or `getPackDbAccess(scope)` is used.
24
+ The default `SqliteStorageDriver` preserves the local database path, migration
25
+ sequence, seed behavior, and write lease.
26
+
27
+ Host repositories use the async `ScopedStore` API. Scenario packs receive
28
+ `PackDbAccess`, which carries `tenantId` and `userId` and does not expose the
29
+ driver connection. Because arbitrary SQL cannot be rewritten safely,
30
+ pack repositories must include both ownership columns in every statement;
31
+ the SQLite and MySQL access objects reject missing or mismatched ownership,
32
+ and pack boundary checks enforce the same requirement before runtime.
33
+
34
+ License: Apache-2.0.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Product-neutral authenticated identity attached to one local-backend request.
3
+ */
4
+ export interface Principal {
5
+ id: string;
6
+ tenantId: string;
7
+ displayName: string;
8
+ email: string | null;
9
+ roles: readonly string[];
10
+ isAdmin: boolean;
11
+ }
12
+ export type AuthDecision = {
13
+ ok: true;
14
+ principal: Principal;
15
+ } | {
16
+ ok: false;
17
+ status: 401 | 403;
18
+ body?: unknown;
19
+ };
20
+ /**
21
+ * Authentication extension point for BS deployments.
22
+ *
23
+ * Implementations own all interpretation and validation of request headers,
24
+ * including cookies and any identity assertions added by a trusted proxy.
25
+ */
26
+ export interface AuthProvider {
27
+ readonly id: string;
28
+ authenticate(headers: Readonly<Record<string, string | string[] | undefined>>): Promise<AuthDecision>;
29
+ describeSelf(principal: Principal): Promise<unknown>;
30
+ }
31
+ /**
32
+ * Registers the process-wide provider before the BS host starts.
33
+ *
34
+ * @returns A disposer that removes this provider if it is still active.
35
+ */
36
+ export declare function registerAuthProvider(provider: AuthProvider): () => void;
37
+ /** Returns the provider registered by the product composition root, if any. */
38
+ export declare function getAuthProvider(): AuthProvider | null;
@@ -0,0 +1,20 @@
1
+ let activeProvider = null;
2
+ /**
3
+ * Registers the process-wide provider before the BS host starts.
4
+ *
5
+ * @returns A disposer that removes this provider if it is still active.
6
+ */
7
+ export function registerAuthProvider(provider) {
8
+ if (activeProvider) {
9
+ throw new Error(`[auth] provider already registered: ${activeProvider.id}`);
10
+ }
11
+ activeProvider = provider;
12
+ return () => {
13
+ if (activeProvider === provider)
14
+ activeProvider = null;
15
+ };
16
+ }
17
+ /** Returns the provider registered by the product composition root, if any. */
18
+ export function getAuthProvider() {
19
+ return activeProvider;
20
+ }
@@ -14,12 +14,12 @@
14
14
  * 消费 handle.httpRoutes——包的入口面贡献由宿主循环驱动,宿主代码里
15
15
  * 不出现任何包名。
16
16
  */
17
- import type Database from 'better-sqlite3';
18
17
  import type { HttpRouteContribution, IpcContribution, ToolContribution } from '../scenario/pack.js';
18
+ import type { PackDbAccess } from '../storage/driver.js';
19
19
  /** 宿主注入包的装配能力面。包不 import 宿主装配模块,全部经回调注入。 */
20
20
  export interface PackAssemblyDeps {
21
- /** 宿主借出的同一个 SQLite 连接(包只读写自己声明的包前缀表)。 */
22
- readonly db: Database.Database;
21
+ /** Scope-bound database access; the underlying driver connection is hidden. */
22
+ readonly packDb: PackDbAccess;
23
23
  /** 模型可见工具面(与普通回合同源)。 */
24
24
  readonly listTools: () => {
25
25
  name: string;
@@ -27,10 +27,10 @@ export interface PackAssemblyDeps {
27
27
  inputSchema: unknown;
28
28
  }[];
29
29
  /** chatId → 绑定项目(无项目对话返回 null)。与 router.resolveChatProject 同源。 */
30
- readonly resolveChatProject: (chatId: string) => {
30
+ readonly resolveChatProject: (chatId: string) => Promise<{
31
31
  name: string;
32
32
  folderPath: string;
33
- } | null;
33
+ } | null>;
34
34
  /** 面向全部用户面的事件广播(CS=所有窗口 IPC,BS=SSE 总线)。 */
35
35
  readonly broadcast: (channel: string, payload: unknown) => void;
36
36
  /** 工具贡献注册口(toolRouter.registerToolContributions 的收窄面)。 */
@@ -46,7 +46,7 @@ export interface PackAssemblyDeps {
46
46
  totalTokens: number;
47
47
  cachedPromptTokens?: number;
48
48
  costUsd?: number | null;
49
- }) => void;
49
+ }) => Promise<void>;
50
50
  /** 行为洞察(与主对话同款 turn 事件)。 */
51
51
  readonly recordInsight: (input: {
52
52
  chatId: string;
@@ -57,7 +57,7 @@ export interface PackAssemblyDeps {
57
57
  toolNames?: unknown;
58
58
  userText?: string;
59
59
  assistantText?: string;
60
- }) => void;
60
+ }) => Promise<void>;
61
61
  /** 装配期日志行(进宿主日志)。 */
62
62
  readonly onLog: (line: string) => void;
63
63
  }
@@ -20,10 +20,13 @@ import { ProjectRegistry } from '../project-registry.js';
20
20
  import { LocalBackendRouter } from '../local-backend/router.js';
21
21
  import { WorktreeService } from '../local-backend/worktree-service.js';
22
22
  import { TaskService } from '../local-backend/task-service.js';
23
+ import { type TenantScope } from '../storage/driver.js';
23
24
  import { createApprovalBridge } from '../sidecar/reverse-approval.js';
24
25
  import { createAskUserBridge } from '../sidecar/reverse-ask-user.js';
25
26
  import { type PackAssemblyHandle } from './pack-assembly.js';
26
27
  export interface HostRuntimeOptions {
28
+ /** Storage ownership for this host process. Defaults to local personal use. */
29
+ scope?: TenantScope;
27
30
  /** 面向全部用户面的事件广播(CS=所有窗口 IPC,BS=SSE 总线)。 */
28
31
  broadcast: (channel: string, payload: unknown) => void;
29
32
  /**
@@ -42,6 +45,7 @@ export interface HostRuntimeOptions {
42
45
  taskSweepReason: string;
43
46
  }
44
47
  export interface HostRuntime {
48
+ store: import('../storage/scoped-store.js').ScopedStore;
45
49
  localExecutor: LocalExecutor;
46
50
  localScriptRegistry: LocalScriptRegistry;
47
51
  terminalManager: TerminalManager;
@@ -62,8 +66,8 @@ export interface HostRuntime {
62
66
  * 不阻塞调用方的就绪路径(sidecar python import 慢,竞速的回合由
63
67
  * router 回退处理)。
64
68
  */
65
- start(): void;
69
+ start(): Promise<void>;
66
70
  /** 关停:杀终端、停 mock、关 sidecar。 */
67
71
  shutdown(): Promise<void>;
68
72
  }
69
- export declare function createHostRuntime(options: HostRuntimeOptions): HostRuntime;
73
+ export declare function createHostRuntime(options: HostRuntimeOptions): Promise<HostRuntime>;
@@ -20,7 +20,7 @@ import { ProjectRegistry } from '../project-registry.js';
20
20
  import { LocalBackendRouter } from '../local-backend/router.js';
21
21
  import { WorktreeService } from '../local-backend/worktree-service.js';
22
22
  import { TaskService } from '../local-backend/task-service.js';
23
- import { localStore } from '../storage/index.js';
23
+ import { LOCAL_SCOPE, closeStorage, getPackDbAccess, getScopedStore, initializeStorage, } from '../storage/driver.js';
24
24
  import { createApprovalBridge } from '../sidecar/reverse-approval.js';
25
25
  import { createAskUserBridge } from '../sidecar/reverse-ask-user.js';
26
26
  import { startHostSidecar, shutdownHostSidecar } from '../sidecar/boot.js';
@@ -29,18 +29,52 @@ import { createJsonStore } from '../json-store.js';
29
29
  import { bindWorkspaceSkillRoots } from '../local-backend/skill-loader.js';
30
30
  import { getChatAttachmentsDir } from '../attachments.js';
31
31
  import { recordInsightTurn } from '../insights/record.js';
32
+ import { llmService } from '../llm/index.js';
32
33
  import { getPackAssemblies, } from './pack-assembly.js';
33
- export function createHostRuntime(options) {
34
+ export async function createHostRuntime(options) {
34
35
  const { broadcast, hasWindow, onLog } = options;
35
36
  const broadcastMain = options.broadcastMain ?? broadcast;
37
+ await initializeStorage();
38
+ const scope = options.scope ?? LOCAL_SCOPE;
39
+ const defaultStore = getScopedStore(scope);
40
+ let localBackendRouter;
41
+ const activeScope = () => localBackendRouter?.activeStore.scope ?? scope;
42
+ const packDb = {
43
+ get scope() {
44
+ return activeScope();
45
+ },
46
+ get(sql, params) {
47
+ return getPackDbAccess(activeScope()).get(sql, params);
48
+ },
49
+ all(sql, params) {
50
+ return getPackDbAccess(activeScope()).all(sql, params);
51
+ },
52
+ run(sql, params) {
53
+ return getPackDbAccess(activeScope()).run(sql, params);
54
+ },
55
+ exec(sql) {
56
+ return getPackDbAccess(activeScope()).exec(sql);
57
+ },
58
+ transaction(operation) {
59
+ return getPackDbAccess(activeScope()).transaction(() => operation(packDb));
60
+ },
61
+ };
36
62
  const localExecutor = new LocalExecutor();
37
63
  // 恢复设置界面里配置的"命令默认超时"(保存时由 local-backend router 即时
38
64
  // 生效,这里负责进程重启后的恢复)。
39
65
  {
40
- const persistedLlmSettings = localStore.getLlmSettings();
66
+ const persistedLlmSettings = await defaultStore.getLlmSettings();
41
67
  setDefaultExecTimeoutMs(persistedLlmSettings?.execTimeoutSeconds
42
68
  ? persistedLlmSettings.execTimeoutSeconds * 1000
43
69
  : null);
70
+ llmService.initialize(persistedLlmSettings ?? {
71
+ provider: 'ollama',
72
+ model: 'llama3.1:8b',
73
+ baseUrl: 'http://127.0.0.1:11434',
74
+ temperature: 0.3,
75
+ }, {
76
+ setLlmSettings: (settings) => defaultStore.setLlmSettings(settings),
77
+ });
44
78
  }
45
79
  const localScriptRegistry = new LocalScriptRegistry();
46
80
  const terminalManager = new TerminalManager();
@@ -78,8 +112,8 @@ export function createHostRuntime(options) {
78
112
  // 4.6a/4.6b:跨 turn 后台任务 + git worktree 隔离。任务流由宿主独立驱动
79
113
  // (不绑父 turn 生命周期,见 task-service.ts 模块头);终态经广播推到
80
114
  // 用户面任务面板。
81
- const resolveChatProjectRoot = (chatId) => {
82
- const projectId = localStore.getChat(chatId)?.projectId;
115
+ const resolveChatProjectRoot = async (chatId) => {
116
+ const projectId = (await defaultStore.getChat(chatId))?.projectId;
83
117
  if (!projectId)
84
118
  return null;
85
119
  const project = projectRegistry.get(projectId);
@@ -91,20 +125,19 @@ export function createHostRuntime(options) {
91
125
  const packHandles = new Map();
92
126
  {
93
127
  const packDeps = {
94
- db: localStore.getPackDb(),
128
+ packDb,
95
129
  listTools: () => toolRouter.listModelSchemas(),
96
130
  resolveChatProject: resolveChatProjectRoot,
97
131
  broadcast,
98
132
  registerTools: (tools) => toolRouter.registerToolContributions(tools),
99
- recordUsage: (input) => localStore.recordUsageEvent(input),
100
- recordInsight: (input) => recordInsightTurn(input),
133
+ recordUsage: (input) => (localBackendRouter?.activeStore ?? defaultStore).recordUsageEvent(input),
134
+ recordInsight: (input) => recordInsightTurn(localBackendRouter?.activeStore ?? defaultStore, input),
101
135
  onLog,
102
136
  };
103
137
  // 包迁移容错应用(3.2):包的 import 链可能经 llm/index 等模块提前
104
138
  // 触发 storage 单例构造(ESM 深度优先,active.ts 的注册体后于链上
105
139
  // 单例求值)——构造时注册表尚空,此处(装配前)补上已注册包迁移。
106
140
  // 幂等(按包 id 去重),注册表为空时零开销。
107
- localStore.applyPackMigrations();
108
141
  for (const [packId, assemble] of getPackAssemblies()) {
109
142
  const handle = assemble(packDeps);
110
143
  if (handle)
@@ -113,14 +146,18 @@ export function createHostRuntime(options) {
113
146
  }
114
147
  const worktreeService = new WorktreeService({ resolveProject: resolveChatProjectRoot });
115
148
  const taskService = new TaskService({
116
- store: localStore,
149
+ store: () => localBackendRouter?.activeStore ?? defaultStore,
117
150
  toolRouter,
118
151
  worktreeService,
119
152
  resolveChatProject: resolveChatProjectRoot,
120
153
  broadcast,
121
154
  });
122
155
  toolRouter.setTaskServices({ taskService, worktreeService });
123
- const localBackendRouter = new LocalBackendRouter(toolRouter, {
156
+ localBackendRouter = new LocalBackendRouter(toolRouter, {
157
+ store: defaultStore,
158
+ resolveStore: (principal) => principal
159
+ ? getScopedStore({ tenantId: principal.tenantId, userId: principal.id })
160
+ : defaultStore,
124
161
  taskService,
125
162
  broadcast: broadcastMain,
126
163
  });
@@ -137,6 +174,7 @@ export function createHostRuntime(options) {
137
174
  });
138
175
  let started = false;
139
176
  return {
177
+ store: defaultStore,
140
178
  localExecutor,
141
179
  localScriptRegistry,
142
180
  terminalManager,
@@ -150,13 +188,13 @@ export function createHostRuntime(options) {
150
188
  approvalBridge,
151
189
  askUserBridge,
152
190
  maybeExecInTerminal,
153
- start() {
191
+ async start() {
154
192
  if (started)
155
193
  return;
156
194
  started = true;
157
195
  // 4.6a:上次进程崩溃/强杀留下的 running 任务不是真相——任务流随进程
158
196
  // 一起死了,启动时落成 failed,任务面板据此显示"重启中断"。
159
- const sweptTasks = localStore.failRunningTasks(options.taskSweepReason);
197
+ const sweptTasks = await defaultStore.failRunningTasks(options.taskSweepReason);
160
198
  if (sweptTasks > 0) {
161
199
  onLog(`[task] swept ${sweptTasks} stale running task(s)`);
162
200
  }
@@ -164,8 +202,9 @@ export function createHostRuntime(options) {
164
202
  // Not awaited: sidecar boot (python import) must not delay readiness.
165
203
  // A turn that races the boot falls back for that turn only.
166
204
  void startHostSidecar({
205
+ store: defaultStore,
167
206
  toolRouter,
168
- resolveProjectRoot: (chatId) => localBackendRouter.resolveChatProject(chatId)?.folderPath ?? null,
207
+ resolveProjectRoot: async (chatId) => (await localBackendRouter.resolveChatProject(chatId))?.folderPath ?? null,
169
208
  // 项目模式下文件读写被围栏在项目目录内;会话附件目录是额外放行的
170
209
  // 只读根,保证用户上传的文件即使在项目会话里也能被 agent 读回。
171
210
  resolveAdditionalReadRoots: (chatId) => [getChatAttachmentsDir(chatId)],
@@ -201,6 +240,7 @@ export function createHostRuntime(options) {
201
240
  await handle.dispose?.();
202
241
  }
203
242
  await shutdownHostSidecar();
243
+ await closeStorage();
204
244
  },
205
245
  };
206
246
  }
@@ -1,10 +1,11 @@
1
+ import type { ScopedStore } from '../storage/scoped-store.js';
1
2
  export declare function insightsNetworkDisabled(): boolean;
2
- export declare function buildInsightsExportPayload(): Record<string, unknown>;
3
+ export declare function buildInsightsExportPayload(store: ScopedStore): Promise<Record<string, unknown>>;
3
4
  export declare function postInsightsJson(url: string, body: unknown): Promise<boolean>;
4
- export declare function flushInsightsOutbox(): Promise<{
5
+ export declare function flushInsightsOutbox(store: ScopedStore): Promise<{
5
6
  uploaded: number;
6
7
  skipped: number;
7
8
  failed: number;
8
9
  }>;
9
- export declare function uploadInsightsBundle(): Promise<boolean>;
10
- export declare function scheduleInsightsFlush(): void;
10
+ export declare function uploadInsightsBundle(store: ScopedStore): Promise<boolean>;
11
+ export declare function scheduleInsightsFlush(store: ScopedStore): void;
@@ -3,7 +3,6 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { getBrand } from '../brand.js';
5
5
  import { resolveInsightsApiBase, rowsEligibleForAutoUpload, } from '../storage/insights-settings.js';
6
- import { localStore } from '../storage/index.js';
7
6
  const FLUSH_TIMEOUT_MS = 8_000;
8
7
  const MAX_BATCH = 20;
9
8
  export function insightsNetworkDisabled() {
@@ -41,10 +40,10 @@ function clientMeta(settings) {
41
40
  appVersion: detectAppVersion(),
42
41
  };
43
42
  }
44
- export function buildInsightsExportPayload() {
45
- const settings = localStore.ensureInsightsSettings();
43
+ export async function buildInsightsExportPayload(store) {
44
+ const settings = await store.ensureInsightsSettings();
46
45
  return {
47
- ...localStore.exportInsightsBundle(),
46
+ ...await store.exportInsightsBundle(),
48
47
  ...clientMeta(settings),
49
48
  };
50
49
  }
@@ -84,12 +83,12 @@ function pathForKind(kind) {
84
83
  return '/api/v2/agent-insights/turns';
85
84
  return '/api/v2/agent-insights/profile';
86
85
  }
87
- export async function flushInsightsOutbox() {
86
+ export async function flushInsightsOutbox(store) {
88
87
  if (insightsNetworkDisabled()) {
89
88
  return { uploaded: 0, skipped: 0, failed: 0 };
90
89
  }
91
- const settings = localStore.ensureInsightsSettings();
92
- const pending = localStore.listInsightOutbox({ uploaded: false, limit: MAX_BATCH });
90
+ const settings = await store.ensureInsightsSettings();
91
+ const pending = await store.listInsightOutbox({ uploaded: false, limit: MAX_BATCH });
93
92
  const { upload, skipped } = rowsEligibleForAutoUpload(settings, pending);
94
93
  let uploaded = 0;
95
94
  let failed = 0;
@@ -105,22 +104,22 @@ export async function flushInsightsOutbox() {
105
104
  clientEventId: row.id,
106
105
  });
107
106
  if (ok) {
108
- localStore.markInsightUploaded(row.id);
107
+ await store.markInsightUploaded(row.id);
109
108
  uploaded += 1;
110
109
  }
111
110
  else {
112
- localStore.markInsightUploadError(row.id, 'upload_failed');
111
+ await store.markInsightUploadError(row.id, 'upload_failed');
113
112
  failed += 1;
114
113
  }
115
114
  }
116
115
  return { uploaded, skipped, failed };
117
116
  }
118
- export async function uploadInsightsBundle() {
117
+ export async function uploadInsightsBundle(store) {
119
118
  if (insightsNetworkDisabled()) {
120
119
  return false;
121
120
  }
122
- const settings = localStore.ensureInsightsSettings();
123
- const bundle = buildInsightsExportPayload();
121
+ const settings = await store.ensureInsightsSettings();
122
+ const bundle = await buildInsightsExportPayload(store);
124
123
  const base = resolveInsightsApiBase(settings, process.env.DEEPPATH_INSIGHTS_API_BASE);
125
124
  if (!base)
126
125
  return false;
@@ -128,12 +127,12 @@ export async function uploadInsightsBundle() {
128
127
  if (ok) {
129
128
  const records = Array.isArray(bundle.records) ? bundle.records : [];
130
129
  for (const row of records)
131
- localStore.markInsightUploaded(row.id);
130
+ await store.markInsightUploaded(row.id);
132
131
  }
133
132
  return ok;
134
133
  }
135
- export function scheduleInsightsFlush() {
136
- void flushInsightsOutbox().catch((err) => {
134
+ export function scheduleInsightsFlush(store) {
135
+ void flushInsightsOutbox(store).catch((err) => {
137
136
  console.warn('[insights] flush crashed', err);
138
137
  });
139
138
  }
@@ -1,5 +1,6 @@
1
- export declare function recordInsightEvent(eventName: string, properties?: Record<string, unknown>): void;
2
- export declare function recordInsightTurn(input: {
1
+ import type { ScopedStore } from '../storage/scoped-store.js';
2
+ export declare function recordInsightEvent(store: ScopedStore, eventName: string, properties?: Record<string, unknown>): Promise<void>;
3
+ export declare function recordInsightTurn(store: ScopedStore, input: {
3
4
  chatId: string;
4
5
  mode?: string;
5
6
  modelId?: string | null;
@@ -8,10 +9,10 @@ export declare function recordInsightTurn(input: {
8
9
  toolNames?: unknown;
9
10
  userText?: string;
10
11
  assistantText?: string;
11
- }): void;
12
- export declare function recordInsightProfile(profile: {
12
+ }): Promise<void>;
13
+ export declare function recordInsightProfile(store: ScopedStore, profile: {
13
14
  displayName?: string;
14
15
  email?: string;
15
16
  company?: string;
16
17
  note?: string;
17
- }): void;
18
+ }): Promise<void>;
@@ -1,18 +1,17 @@
1
1
  import { redactInsightText, toolNamesOnly } from '../storage/insights-redact.js';
2
- import { localStore } from '../storage/index.js';
3
2
  import { scheduleInsightsFlush } from './flush.js';
4
- export function recordInsightEvent(eventName, properties = {}) {
3
+ export async function recordInsightEvent(store, eventName, properties = {}) {
5
4
  try {
6
- localStore.enqueueInsight('event', { eventName, properties });
7
- scheduleInsightsFlush();
5
+ await store.enqueueInsight('event', { eventName, properties });
6
+ scheduleInsightsFlush(store);
8
7
  }
9
8
  catch (err) {
10
9
  console.warn('[insights] event enqueue failed', err);
11
10
  }
12
11
  }
13
- export function recordInsightTurn(input) {
12
+ export async function recordInsightTurn(store, input) {
14
13
  try {
15
- localStore.enqueueInsight('turn', {
14
+ await store.enqueueInsight('turn', {
16
15
  chatId: input.chatId,
17
16
  mode: input.mode === 'plan' ? 'plan' : 'agent',
18
17
  modelId: typeof input.modelId === 'string' ? input.modelId.slice(0, 128) : undefined,
@@ -22,21 +21,21 @@ export function recordInsightTurn(input) {
22
21
  userText: redactInsightText(input.userText, 4000),
23
22
  assistantText: redactInsightText(input.assistantText, 8000),
24
23
  });
25
- scheduleInsightsFlush();
24
+ scheduleInsightsFlush(store);
26
25
  }
27
26
  catch (err) {
28
27
  console.warn('[insights] turn enqueue failed', err);
29
28
  }
30
29
  }
31
- export function recordInsightProfile(profile) {
30
+ export async function recordInsightProfile(store, profile) {
32
31
  try {
33
- localStore.enqueueInsight('profile', {
32
+ await store.enqueueInsight('profile', {
34
33
  displayName: redactInsightText(profile.displayName, 128),
35
34
  email: redactInsightText(profile.email, 191),
36
35
  company: redactInsightText(profile.company, 191),
37
36
  note: redactInsightText(profile.note, 500),
38
37
  });
39
- scheduleInsightsFlush();
38
+ scheduleInsightsFlush(store);
40
39
  }
41
40
  catch (err) {
42
41
  console.warn('[insights] profile enqueue failed', err);
@@ -1,10 +1,14 @@
1
- import { type LlmSettings } from '../storage/index.js';
1
+ import type { LlmSettings, ScopedStore } from '../storage/index.js';
2
2
  import { getSidecarSupervisor, setSidecarSupervisor, setSidecarSupervisorPending, whenSidecarSupervisor } from '../sidecar/handle.js';
3
3
  export { getSidecarSupervisor, setSidecarSupervisor, setSidecarSupervisorPending, whenSidecarSupervisor };
4
4
  import type { LlmGenerateRequest, LlmGenerateResult, LlmGenerateStreamRequest } from './types.js';
5
5
  export declare class LlmService {
6
6
  private provider;
7
7
  private cachedConfigSignature;
8
+ private settings;
9
+ private store;
10
+ /** Initializes the synchronous runtime cache after storage has loaded. */
11
+ initialize(settings: LlmSettings, store: Pick<ScopedStore, 'setLlmSettings'>): void;
8
12
  listModels(): Promise<string[]>;
9
13
  generate(request: Omit<LlmGenerateRequest, 'model'> & {
10
14
  model?: string;
@@ -16,7 +20,7 @@ export declare class LlmService {
16
20
  model?: string;
17
21
  }): Promise<LlmGenerateResult>;
18
22
  getSettings(): LlmSettings;
19
- setSettings(next: LlmSettings): LlmSettings;
23
+ setSettings(next: LlmSettings, store?: Pick<ScopedStore, "setLlmSettings"> | null): Promise<LlmSettings>;
20
24
  private getProvider;
21
25
  }
22
26
  export declare const llmService: LlmService;
package/dist/llm/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { localStore } from '../storage/index.js';
2
1
  import { sidecarWireProvider } from '../storage/llm-settings.js';
3
2
  import { OllamaProvider } from './ollama.js';
4
3
  import { OpenAICompatProvider } from './openai-compat.js';
@@ -11,6 +10,15 @@ export { getSidecarSupervisor, setSidecarSupervisor, setSidecarSupervisorPending
11
10
  export class LlmService {
12
11
  provider = null;
13
12
  cachedConfigSignature = '';
13
+ settings = null;
14
+ store = null;
15
+ /** Initializes the synchronous runtime cache after storage has loaded. */
16
+ initialize(settings, store) {
17
+ this.settings = settings;
18
+ this.store = store;
19
+ this.provider = null;
20
+ this.cachedConfigSignature = '';
21
+ }
14
22
  async listModels() {
15
23
  const provider = this.getProvider();
16
24
  return provider.listModels();
@@ -51,15 +59,17 @@ export class LlmService {
51
59
  return result;
52
60
  }
53
61
  getSettings() {
54
- return localStore.getLlmSettings() || {
55
- provider: 'ollama',
56
- model: 'llama3.1:8b',
57
- baseUrl: 'http://127.0.0.1:11434',
58
- temperature: 0.3,
59
- };
62
+ if (!this.settings) {
63
+ throw new Error('LlmService must be initialized after storage');
64
+ }
65
+ return this.settings;
60
66
  }
61
- setSettings(next) {
62
- const saved = localStore.setLlmSettings(next);
67
+ async setSettings(next, store = this.store) {
68
+ if (!store) {
69
+ throw new Error('LlmService must be initialized after storage');
70
+ }
71
+ const saved = await store.setLlmSettings(next);
72
+ this.settings = saved;
63
73
  this.provider = null;
64
74
  this.cachedConfigSignature = '';
65
75
  return saved;
@@ -48,6 +48,11 @@ export interface BuildPromptOptions {
48
48
  * 仍然优先——模式级排除(plan 模式的执行类技能)不因勾选而失效。
49
49
  */
50
50
  pinnedSkillNames?: Iterable<string>;
51
+ /**
52
+ * 本轮自称。有绑定智能体时用智能体显示名,渲染技能正文 `{agentName}`;
53
+ * 未传则回落产品品牌 `brand.agentName`。
54
+ */
55
+ identityName?: string | null;
51
56
  }
52
57
  export interface BuiltPrompt {
53
58
  prompt: string;
@@ -84,11 +89,12 @@ export declare function conditionsFromTools(toolNames: Iterable<string>, autoHas
84
89
  */
85
90
  export declare function buildSystemPrompt(options?: BuildPromptOptions): Promise<BuiltPrompt>;
86
91
  /**
87
- * 品牌占位符变量:技能正文里的 `{agentName}` / `{brandDisplayName}` /
88
- * `{brandTagline}` 渲染为当前产品注入的品牌(brand.ts;未注入 = shell
89
- * 中性默认)。eager 系统提示词路径统一注入;包技能同理可用。
92
+ * 品牌/身份占位符:`{brandDisplayName}` / `{brandTagline}` 始终是产品品牌;
93
+ * `{agentName}` 有本轮智能体时用智能体显示名,否则回落 `brand.agentName`。
90
94
  */
91
- export declare function brandSkillVars(): Record<string, string>;
95
+ export declare function brandSkillVars(options?: {
96
+ identityName?: string | null;
97
+ }): Record<string, string>;
92
98
  /** 技能正文渲染:把 `{scripts}` 及额外占位符解析为绝对路径。 */
93
99
  export declare function renderSkillContent(m: SkillModule, vars?: Record<string, string>): string;
94
100
  /** 用户显式指定技能的优先级指令头,渲染在该技能内容之前。 */