berry-agent 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obs — 服务面(03 §10.8 增量摄取/告警条款的编舞位)。
|
|
3
|
+
*
|
|
4
|
+
* refresh() 单入口三段:①增量摄取(水位−1h 重叠窗扫事件流 → 脏桶整窗重查
|
|
5
|
+
* 重算替换——幂等自愈)②闭日物化(日桶只在日闭合后从小时桶聚合落账,
|
|
6
|
+
* 重算幂等)③告警评估(只通知不执法 + hasAudience 前置不耗冷却)。
|
|
7
|
+
* 已知边界(如实注记,不虚构保证):时钟回拨超 1h 重叠窗的旧事件不回补
|
|
8
|
+
* ——单写者 append-only 事件流的 time 单调性使其实际不发生。
|
|
9
|
+
*
|
|
10
|
+
* 聚合呈现两维分立(07 §4.2):本件数据面只有会话客观态聚合;用户已读
|
|
11
|
+
* 游标不入观测库(住通道侧本地状态)。
|
|
12
|
+
*/
|
|
13
|
+
import { BaseError } from '../contracts/index.js';
|
|
14
|
+
import { openRollupDatabase } from './db.js';
|
|
15
|
+
import { aggregateHours, bucketClosed, DAY_MS, dayBucketMs, HOUR_MS, hourBucketMs } from './rollup.js';
|
|
16
|
+
/** 摄取水位 obs_meta 键(上次扫描推进到的 now 毫秒) */
|
|
17
|
+
const META_WATERMARK = 'ingest_watermark_ms';
|
|
18
|
+
/** 重叠窗(迟到事件/时钟回拨自愈面——1 小时) */
|
|
19
|
+
const OVERLAP_MS = HOUR_MS;
|
|
20
|
+
/** 告警冷却缺省(规则小时粒度——冷却缺省与粒度对齐) */
|
|
21
|
+
const DEFAULT_COOLDOWN_MS = HOUR_MS;
|
|
22
|
+
/** 扫描页参数(与 Store.queryEvents 硬帽一致;页数护栏防坏游标死循环) */
|
|
23
|
+
const SCAN_PAGE_LIMIT = 10_000;
|
|
24
|
+
const SCAN_PAGE_GUARD = 1_000;
|
|
25
|
+
/** 查询行帽(缺省 100、硬帽 1000——模型消费面) */
|
|
26
|
+
const QUERY_LIMIT_DEFAULT = 100;
|
|
27
|
+
const QUERY_LIMIT_MAX = 1_000;
|
|
28
|
+
/**
|
|
29
|
+
* LIKE 前缀转义(03 §10.8 u-1 RP5 尾通配——u-4 落码):族前缀按字面匹配,
|
|
30
|
+
* `\` `%` `_` 三元前置反斜杠——LIKE 元字符不放大匹配面(未转义的 `_` 会
|
|
31
|
+
* 单字符通配误中,如 `plug_n/*` 误中 `plugAn/x`,破坏族语义)。
|
|
32
|
+
*/
|
|
33
|
+
function escapeLikePrefix(prefix) {
|
|
34
|
+
return prefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
35
|
+
}
|
|
36
|
+
/** createObsService 工厂(装载面/测试唯一入口) */
|
|
37
|
+
export function createObsService(deps) {
|
|
38
|
+
return new ObsServiceImpl(deps);
|
|
39
|
+
}
|
|
40
|
+
/** 服务实现(类私有——外界只经 ObsService 面) */
|
|
41
|
+
class ObsServiceImpl {
|
|
42
|
+
db;
|
|
43
|
+
events;
|
|
44
|
+
notify;
|
|
45
|
+
audience;
|
|
46
|
+
alerts;
|
|
47
|
+
clock;
|
|
48
|
+
warn;
|
|
49
|
+
timer;
|
|
50
|
+
disposed = false;
|
|
51
|
+
constructor(deps) {
|
|
52
|
+
// 开库即抛 OBS_DB_OPEN_FAILED(fail-loud——装载面降级跳件消费)
|
|
53
|
+
this.db = openRollupDatabase(deps.dbPath, deps.warn);
|
|
54
|
+
this.events = deps.events;
|
|
55
|
+
this.notify = deps.notify;
|
|
56
|
+
this.audience = deps.audience;
|
|
57
|
+
this.alerts = deps.alerts ?? [];
|
|
58
|
+
this.clock = deps.clock ?? (() => Date.now());
|
|
59
|
+
this.warn = deps.warn ?? ((message) => console.error(message));
|
|
60
|
+
const refreshMs = deps.refreshMs ?? 60_000;
|
|
61
|
+
if (refreshMs > 0) {
|
|
62
|
+
// 自驱挂钟(obs deps 无 scheduler 边——件内自持合法;dispose 注销。
|
|
63
|
+
// 定时腿异常不炸进程:吞进 warn,下一拍重试)
|
|
64
|
+
this.timer = setInterval(() => {
|
|
65
|
+
try {
|
|
66
|
+
this.refresh();
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
this.warn(`[obs] 自驱 refresh 失败(下一拍重试):${err instanceof Error ? err.message : String(err)}`);
|
|
70
|
+
}
|
|
71
|
+
}, refreshMs);
|
|
72
|
+
// unref:观测挂钟不锚进程生命周期(诊断形装载本件后进程可自然退
|
|
73
|
+
// ——dump-config/plugins-list :memory: 同构纪律;长驻形不受影响,
|
|
74
|
+
// 收口恒走 dispose 注销)
|
|
75
|
+
this.timer.unref?.();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
refresh() {
|
|
79
|
+
this.ensureOpen();
|
|
80
|
+
const now = this.clock();
|
|
81
|
+
const prevWatermark = this.readMetaNumber(META_WATERMARK) ?? 0;
|
|
82
|
+
// ① 增量摄取:水位−1h 重叠窗、窗起点下取整到小时边界——窗内每桶被
|
|
83
|
+
// 完整重扫,产物即各桶完整真值(当前桶覆盖到 now 为止,恰为「事件
|
|
84
|
+
// 至此」的正确计数;迟到在 1h 窗内的旧桶下一拍整窗重算自愈)
|
|
85
|
+
const scanSince = hourBucketMs(Math.max(0, prevWatermark - OVERLAP_MS));
|
|
86
|
+
const aggregation = this.scanEvents(scanSince, now);
|
|
87
|
+
// ②③ 同事务落账(摄取替换 + 闭日物化 + 水位推进——单事务幂等)
|
|
88
|
+
const write = this.db.transaction(() => {
|
|
89
|
+
this.replaceHourBuckets(aggregation);
|
|
90
|
+
this.materializeClosedDays(aggregation.touchedBuckets, prevWatermark, now);
|
|
91
|
+
// 水位只进不退(时钟回拨不回吃扫描窗——下拍以旧水位+重叠窗续扫)
|
|
92
|
+
this.setMeta(META_WATERMARK, String(Math.max(prevWatermark, now)));
|
|
93
|
+
});
|
|
94
|
+
write();
|
|
95
|
+
// 告警评估挂 refresh 尾(03 §10.8——只通知不执法)
|
|
96
|
+
this.evaluateAlerts(now);
|
|
97
|
+
}
|
|
98
|
+
query(input) {
|
|
99
|
+
this.ensureOpen();
|
|
100
|
+
const granularity = input.granularity === 'day' ? 'day' : 'hour';
|
|
101
|
+
const metric = input.metric === 'usage' ? 'usage' : 'events';
|
|
102
|
+
const limit = Math.min(Math.max(1, input.limit ?? QUERY_LIMIT_DEFAULT), QUERY_LIMIT_MAX);
|
|
103
|
+
// 窗口对齐到桶边界(from/to 都是「含」语义)
|
|
104
|
+
const span = granularity === 'day' ? DAY_MS : HOUR_MS;
|
|
105
|
+
const from = input.from !== undefined ? Math.floor(input.from / span) * span : undefined;
|
|
106
|
+
const to = input.to !== undefined ? Math.floor(input.to / span) * span : undefined;
|
|
107
|
+
if (metric === 'usage') {
|
|
108
|
+
const table = granularity === 'day' ? 'usage_day' : 'usage_hour';
|
|
109
|
+
const clauses = [];
|
|
110
|
+
const params = [];
|
|
111
|
+
if (from !== undefined) {
|
|
112
|
+
clauses.push('bucket >= ?');
|
|
113
|
+
params.push(from);
|
|
114
|
+
}
|
|
115
|
+
if (to !== undefined) {
|
|
116
|
+
clauses.push('bucket <= ?');
|
|
117
|
+
params.push(to);
|
|
118
|
+
}
|
|
119
|
+
params.push(limit);
|
|
120
|
+
const rows = this.db
|
|
121
|
+
.prepare(`SELECT bucket, calls, input, output, cache_read, cache_write, cache_write_1h, reasoning
|
|
122
|
+
FROM ${table}${clauses.length > 0 ? ` WHERE ${clauses.join(' AND ')}` : ''}
|
|
123
|
+
ORDER BY bucket ASC LIMIT ?`)
|
|
124
|
+
.all(...params);
|
|
125
|
+
return rows.map((row) => this.toUsageRow(row, table));
|
|
126
|
+
}
|
|
127
|
+
const table = granularity === 'day' ? 'events_day' : 'events_hour';
|
|
128
|
+
const clauses = [];
|
|
129
|
+
const params = [];
|
|
130
|
+
if (from !== undefined) {
|
|
131
|
+
clauses.push('bucket >= ?');
|
|
132
|
+
params.push(from);
|
|
133
|
+
}
|
|
134
|
+
if (to !== undefined) {
|
|
135
|
+
clauses.push('bucket <= ?');
|
|
136
|
+
params.push(to);
|
|
137
|
+
}
|
|
138
|
+
if (input.eventType !== undefined) {
|
|
139
|
+
// 尾通配形(03 §10.8 u-1 RP5 定形——u-4 落码扩形):`<族前缀>/*` 一次
|
|
140
|
+
// 取全族计数(如 compaction/*——四词族一次可达);语法限定尾 `/*` 一形
|
|
141
|
+
// 非任意 glob。LIKE ESCAPE '\' + 前缀转义保字面;hour/day 两表同路
|
|
142
|
+
// (table 变量切换——本分支单码点覆盖两粒度)。非尾通配的含 '*' 形
|
|
143
|
+
// 落精确匹配零行(诚实空不炸——工具面 schema pattern 段已前置拒,
|
|
144
|
+
// 此处兜底即防御纵深)
|
|
145
|
+
if (input.eventType.endsWith('/*')) {
|
|
146
|
+
clauses.push("event_type LIKE ? ESCAPE '\\'");
|
|
147
|
+
params.push(`${escapeLikePrefix(input.eventType.slice(0, -1))}%`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
clauses.push('event_type = ?');
|
|
151
|
+
params.push(input.eventType);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
params.push(limit);
|
|
155
|
+
const rows = this.db
|
|
156
|
+
.prepare(`SELECT bucket, event_type, count FROM ${table}${clauses.length > 0 ? ` WHERE ${clauses.join(' AND ')}` : ''} ORDER BY bucket ASC, event_type ASC LIMIT ?`)
|
|
157
|
+
.all(...params);
|
|
158
|
+
return rows.map((row) => this.toEventsRow(row, table));
|
|
159
|
+
}
|
|
160
|
+
dispose() {
|
|
161
|
+
if (this.disposed)
|
|
162
|
+
return;
|
|
163
|
+
this.disposed = true;
|
|
164
|
+
if (this.timer !== undefined)
|
|
165
|
+
clearInterval(this.timer);
|
|
166
|
+
this.db.close();
|
|
167
|
+
}
|
|
168
|
+
/* ---------------- 私有腿 ---------------- */
|
|
169
|
+
/** dispose 后拒用(fail-loud——归 OBS_DB_OPEN_FAILED 族:库已闭) */
|
|
170
|
+
ensureOpen() {
|
|
171
|
+
if (this.disposed) {
|
|
172
|
+
throw new BaseError('OBS_DB_OPEN_FAILED', 'obs 服务已 dispose——refresh/query 拒用');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** 页游标遍历事件流(untilMs 含 now——不吞 now 时刻事件) */
|
|
176
|
+
scanEvents(scanSince, now) {
|
|
177
|
+
const collected = [];
|
|
178
|
+
let cursor = null;
|
|
179
|
+
for (let page = 0; page < SCAN_PAGE_GUARD; page++) {
|
|
180
|
+
const result = this.events.queryEvents({
|
|
181
|
+
sinceMs: scanSince,
|
|
182
|
+
untilMs: now,
|
|
183
|
+
limit: SCAN_PAGE_LIMIT,
|
|
184
|
+
...(cursor !== null ? { cursor } : {}),
|
|
185
|
+
});
|
|
186
|
+
collected.push(...result.events);
|
|
187
|
+
if (result.nextCursor === null)
|
|
188
|
+
break;
|
|
189
|
+
cursor = result.nextCursor;
|
|
190
|
+
}
|
|
191
|
+
return aggregateHours(collected);
|
|
192
|
+
}
|
|
193
|
+
/** 脏桶整体替换(DELETE+INSERT——幂等自愈的执法位) */
|
|
194
|
+
replaceHourBuckets(aggregation) {
|
|
195
|
+
const delEvents = this.db.prepare('DELETE FROM events_hour WHERE bucket = ?');
|
|
196
|
+
const delUsage = this.db.prepare('DELETE FROM usage_hour WHERE bucket = ?');
|
|
197
|
+
const insEvents = this.db.prepare('INSERT INTO events_hour (bucket, event_type, count) VALUES (?, ?, ?)');
|
|
198
|
+
const insUsage = this.db.prepare('INSERT INTO usage_hour (bucket, calls, input, output, cache_read, cache_write, cache_write_1h, reasoning) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
|
199
|
+
for (const bucket of aggregation.touchedBuckets) {
|
|
200
|
+
delEvents.run(bucket);
|
|
201
|
+
delUsage.run(bucket);
|
|
202
|
+
const byType = aggregation.counts.get(bucket);
|
|
203
|
+
if (byType) {
|
|
204
|
+
for (const [eventType, count] of byType)
|
|
205
|
+
insEvents.run(bucket, eventType, count);
|
|
206
|
+
}
|
|
207
|
+
const usage = aggregation.usage.get(bucket);
|
|
208
|
+
if (usage) {
|
|
209
|
+
insUsage.run(bucket, usage.calls, usage.input, usage.output, usage.cacheRead, usage.cacheWrite, usage.cacheWrite1h, usage.reasoning);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 闭日物化:候选 = 本拍触达桶的日 + 水位以来跨过的日(末拍后闭日的桶
|
|
215
|
+
* 无新事件也要物化)。只做闭日(bucketClosed——当前日在飞不落),从
|
|
216
|
+
* 小时桶表聚合重算(DELETE+INSERT 幂等)。
|
|
217
|
+
*/
|
|
218
|
+
materializeClosedDays(touchedHourBuckets, prevWatermark, now) {
|
|
219
|
+
const candidates = new Set();
|
|
220
|
+
for (const bucket of touchedHourBuckets)
|
|
221
|
+
candidates.add(dayBucketMs(bucket));
|
|
222
|
+
if (prevWatermark > 0) {
|
|
223
|
+
// 水位以来跨过的日(首拍 prevWatermark=0 不走全史迭代——触达桶的日
|
|
224
|
+
// 已覆盖有数据的日)
|
|
225
|
+
for (let day = dayBucketMs(prevWatermark); day <= dayBucketMs(now); day += DAY_MS) {
|
|
226
|
+
candidates.add(day);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const delEvents = this.db.prepare('DELETE FROM events_day WHERE bucket = ?');
|
|
230
|
+
const delUsage = this.db.prepare('DELETE FROM usage_day WHERE bucket = ?');
|
|
231
|
+
const insEvents = this.db.prepare('INSERT INTO events_day (bucket, event_type, count) VALUES (?, ?, ?)');
|
|
232
|
+
const selEvents = this.db.prepare('SELECT event_type, SUM(count) AS count FROM events_hour WHERE bucket >= ? AND bucket < ? GROUP BY event_type');
|
|
233
|
+
const insUsage = this.db.prepare('INSERT INTO usage_day (bucket, calls, input, output, cache_read, cache_write, cache_write_1h, reasoning) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
|
234
|
+
const selUsage = this.db.prepare(`SELECT SUM(calls) AS calls, SUM(input) AS input, SUM(output) AS output, SUM(cache_read) AS cache_read,
|
|
235
|
+
SUM(cache_write) AS cache_write, SUM(cache_write_1h) AS cache_write_1h, SUM(reasoning) AS reasoning
|
|
236
|
+
FROM usage_hour WHERE bucket >= ? AND bucket < ?`);
|
|
237
|
+
for (const day of candidates) {
|
|
238
|
+
if (!bucketClosed(day, now, DAY_MS))
|
|
239
|
+
continue;
|
|
240
|
+
delEvents.run(day);
|
|
241
|
+
delUsage.run(day);
|
|
242
|
+
for (const row of selEvents.all(day, day + DAY_MS)) {
|
|
243
|
+
const typed = row;
|
|
244
|
+
const eventType = typeof typed.event_type === 'string' ? typed.event_type : '(unknown)';
|
|
245
|
+
const count = typeof typed.count === 'number' && Number.isFinite(typed.count) ? typed.count : 0;
|
|
246
|
+
insEvents.run(day, eventType, count);
|
|
247
|
+
}
|
|
248
|
+
const usage = selUsage.get(day, day + DAY_MS);
|
|
249
|
+
// 全天零行时 SUM 全 NULL——不落 usage_day 行(无调用日无行)
|
|
250
|
+
if (usage && typeof usage.calls === 'number') {
|
|
251
|
+
const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
|
|
252
|
+
insUsage.run(day, usage.calls, num(usage.input), num(usage.output), num(usage.cache_read), num(usage.cache_write), num(usage.cache_write_1h), num(usage.reasoning));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** 告警评估(refresh 尾;hasAudience 前置——无观众整跳且不耗冷却) */
|
|
257
|
+
evaluateAlerts(now) {
|
|
258
|
+
if (this.alerts.length === 0)
|
|
259
|
+
return;
|
|
260
|
+
// 观众探针前置(03 §10.8):false 时跳过整次评估——冷却不烧、观众回场
|
|
261
|
+
// 仍可响(07 §4.3 无头不耗告警冷却原句语义)
|
|
262
|
+
if (!this.audience.hasAudience())
|
|
263
|
+
return;
|
|
264
|
+
const bucket = hourBucketMs(now);
|
|
265
|
+
const row = this.db.prepare('SELECT input, output FROM usage_hour WHERE bucket = ?').get(bucket);
|
|
266
|
+
const mainBillable = row && typeof row.input === 'number' && typeof row.output === 'number' ? row.input + row.output : 0;
|
|
267
|
+
this.alerts.forEach((rule, index) => {
|
|
268
|
+
// v1 单规则种:未知种跳过 + warn(配置校验主责在装载面——此处防御
|
|
269
|
+
// 不炸宿主);坏阈值(非正数)同理跳过
|
|
270
|
+
if (rule.kind !== 'token_spend_hourly') {
|
|
271
|
+
this.warn(`[obs] 跳过未知告警规则种:${String(rule.kind)}(规则 #${index})`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (!(rule.thresholdTokens > 0)) {
|
|
275
|
+
this.warn(`[obs] 跳过坏阈值告警规则(thresholdTokens=${String(rule.thresholdTokens)},规则 #${index})`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (mainBillable < rule.thresholdTokens)
|
|
279
|
+
return;
|
|
280
|
+
// 冷却判定(per-规则 lastFiredAt;notify 失败不烧冷却——下一拍重试)
|
|
281
|
+
const cooldownMs = rule.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
282
|
+
const key = `alert:${index}:lastFiredAt`;
|
|
283
|
+
const lastFiredAt = this.readMetaNumber(key) ?? 0;
|
|
284
|
+
if (now - lastFiredAt < cooldownMs)
|
|
285
|
+
return;
|
|
286
|
+
try {
|
|
287
|
+
// ObsNotifyFace 面(词面独立——notify(message, opts) 单方法)
|
|
288
|
+
this.notify.notify(`[obs] 当前小时 token 花销 ${mainBillable} 已超阈值 ${rule.thresholdTokens}(主计费桶 input+output;cache 桶不计)`, { level: 'warn' });
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
this.warn(`[obs] 告警通知失败(不烧冷却,下一拍重试):${err instanceof Error ? err.message : String(err)}`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.setMeta(key, String(now));
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/** obs_meta 读(字符串值;缺席 null) */
|
|
298
|
+
readMeta(key) {
|
|
299
|
+
const row = this.db.prepare('SELECT value FROM obs_meta WHERE key = ?').get(key);
|
|
300
|
+
return row && typeof row.value === 'string' ? row.value : null;
|
|
301
|
+
}
|
|
302
|
+
/** obs_meta 数值读(坏值折 null——水位/冷却只信合法数) */
|
|
303
|
+
readMetaNumber(key) {
|
|
304
|
+
const raw = this.readMeta(key);
|
|
305
|
+
if (raw === null)
|
|
306
|
+
return null;
|
|
307
|
+
const value = Number(raw);
|
|
308
|
+
return Number.isFinite(value) ? value : null;
|
|
309
|
+
}
|
|
310
|
+
/** obs_meta 写(upsert) */
|
|
311
|
+
setMeta(key, value) {
|
|
312
|
+
this.db
|
|
313
|
+
.prepare('INSERT INTO obs_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
|
|
314
|
+
.run(key, value);
|
|
315
|
+
}
|
|
316
|
+
/** 事件行拒误读校验(坏行 = OBS_ROLLUP_CORRUPT——派生物宁弃读不误读) */
|
|
317
|
+
toEventsRow(row, table) {
|
|
318
|
+
const typed = row;
|
|
319
|
+
if (typeof typed.bucket !== 'number' ||
|
|
320
|
+
!Number.isInteger(typed.bucket) ||
|
|
321
|
+
typed.bucket < 0 ||
|
|
322
|
+
typeof typed.event_type !== 'string' ||
|
|
323
|
+
typeof typed.count !== 'number' ||
|
|
324
|
+
!Number.isFinite(typed.count) ||
|
|
325
|
+
typed.count < 0) {
|
|
326
|
+
throw new BaseError('OBS_ROLLUP_CORRUPT', `${table} 命中坏行(拒误读——重算即修复):${JSON.stringify(row)}`);
|
|
327
|
+
}
|
|
328
|
+
return { bucket: typed.bucket, eventType: typed.event_type, count: typed.count };
|
|
329
|
+
}
|
|
330
|
+
/** 用量行拒误读校验(同上律——七列全数且非负) */
|
|
331
|
+
toUsageRow(row, table) {
|
|
332
|
+
const typed = row;
|
|
333
|
+
const fields = ['bucket', 'calls', 'input', 'output', 'cache_read', 'cache_write', 'cache_write_1h', 'reasoning'];
|
|
334
|
+
for (const field of fields) {
|
|
335
|
+
const value = typed[field];
|
|
336
|
+
if (typeof value !== 'number' ||
|
|
337
|
+
!Number.isFinite(value) ||
|
|
338
|
+
value < 0 ||
|
|
339
|
+
(field === 'bucket' && !Number.isInteger(value))) {
|
|
340
|
+
throw new BaseError('OBS_ROLLUP_CORRUPT', `${table} 命中坏行(拒误读——重算即修复):${JSON.stringify(row)}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
bucket: typed.bucket,
|
|
345
|
+
calls: typed.calls,
|
|
346
|
+
input: typed.input,
|
|
347
|
+
output: typed.output,
|
|
348
|
+
cacheRead: typed.cache_read,
|
|
349
|
+
cacheWrite: typed.cache_write,
|
|
350
|
+
cacheWrite1h: typed.cache_write_1h,
|
|
351
|
+
reasoning: typed.reasoning,
|
|
352
|
+
// 命中率派生列(RP5——查询期派生零 rollup 列):桶内聚合比值;分母零
|
|
353
|
+
// = 无 token 流 → null(诚实缺席非 0%)
|
|
354
|
+
hitRate: (() => {
|
|
355
|
+
const input = typed.input;
|
|
356
|
+
const cacheRead = typed.cache_read;
|
|
357
|
+
const cacheWrite = typed.cache_write;
|
|
358
|
+
const denominator = input + cacheRead + cacheWrite;
|
|
359
|
+
return denominator > 0 ? cacheRead / denominator : null;
|
|
360
|
+
})(),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obs — 会话维工具族(03 §10.8 会话维扩展——e-2 观测腿 + e-3 环境自感;
|
|
3
|
+
* 宿主固定工具族四件 `session_list` / `session_read` / `session_trace` /
|
|
4
|
+
* `session_status`,`session_` 前缀保留字〔03 §2.7〕的宿主侧落位)。
|
|
5
|
+
*
|
|
6
|
+
* **可见性分轴执法**(03 §10.8——观测轴):
|
|
7
|
+
* - 树内(self 同 id / parent_id 链同根——SessionView.isSameTree 单源)
|
|
8
|
+
* 零开门白给;
|
|
9
|
+
* - 跨树/全会话维枚举与读走高危面 `sessions.observe-cross` 门检
|
|
10
|
+
* (03 §4.6 v1 首批第五枚——adjudicateCapabilityDoor;拒 =
|
|
11
|
+
* SESSION_OBSERVE_DENIED〔02 §5.3 e-2 补登〕);门开后逐次
|
|
12
|
+
* capability/used 审计 seam(05 §1.1——真发射位 U3 批已接线
|
|
13
|
+
* audit_events)。
|
|
14
|
+
* - session_list 的门未开形 = 只列树内会话(枚举本身即跨树信息——
|
|
15
|
+
* 「枚举与读」同门同码防旁路)。
|
|
16
|
+
*
|
|
17
|
+
* **行为律带出**(立题档 §六「问当事人 vs 查档案」——oh-my-pi 先例:
|
|
18
|
+
* 判断隔壁会话在做什么优先直接向当事会话发消息,档案查询归本工具族;
|
|
19
|
+
* 提示词层面带出不立条款):写进 session_read/session_trace description。
|
|
20
|
+
*
|
|
21
|
+
* **e-3 环境自感**(普查候选 E——03 §10.8 session_status 条并入注):
|
|
22
|
+
* session_status 扩工具清单(整形后有效可见集)/ 能力门态快照(闭门
|
|
23
|
+
* 附诊断 reason——与执行时拒绝 message 同源)/ 负面能力声明文案
|
|
24
|
+
* (hermes「你没有 X——不要承诺」防幻觉负向清单)三段;数据经 env
|
|
25
|
+
* 窄面注入(装配现场事实非 durable 派生——与 SessionView 分立)。
|
|
26
|
+
*
|
|
27
|
+
* 恒挂载律(03 §2.2 第十一面同款):工具族模型可见清单恒在,不随门开合
|
|
28
|
+
* 动态挂载——门检在 execute 内执法。per-session 闭包由装配位构造
|
|
29
|
+
* (callerSessionId 注入——todoTool 换装 seam 同构先例)。
|
|
30
|
+
*/
|
|
31
|
+
import { BaseError } from '../contracts/index.js';
|
|
32
|
+
import { adjudicateCapabilityDoor } from '../contracts/api.js';
|
|
33
|
+
import { Type } from 'typebox';
|
|
34
|
+
/** 观测轴高危面名(03 §4.6 v1 首批第五枚——拉取/订阅两形态一枚统摄) */
|
|
35
|
+
export const OBSERVE_CROSS_CAPABILITY = 'sessions.observe-cross';
|
|
36
|
+
/** createSessionTools 工厂(装载位/测试唯一入口)——四件定义数组 */
|
|
37
|
+
export function createSessionTools(deps) {
|
|
38
|
+
const { view, callerSessionId } = deps;
|
|
39
|
+
/** 跨树门检 + 开门后审计(执法序:树内白给前置由调用侧判,此处只管跨树档) */
|
|
40
|
+
const enforceCrossTreeDoor = (verb, targetSessionId) => {
|
|
41
|
+
const verdict = adjudicateCapabilityDoor(deps.getOpens(), OBSERVE_CROSS_CAPABILITY);
|
|
42
|
+
if (!verdict.ok) {
|
|
43
|
+
throw new BaseError('SESSION_OBSERVE_DENIED', `${verdict.message}(调用方会话 ${callerSessionId}${targetSessionId !== undefined ? `,目标 ${targetSessionId}` : ''})`);
|
|
44
|
+
}
|
|
45
|
+
// 开门后逐次审计(core: 豁免面不适用于模型工具道——caller 恒为会话模型)
|
|
46
|
+
deps.onCapabilityUsed?.({
|
|
47
|
+
capability: OBSERVE_CROSS_CAPABILITY,
|
|
48
|
+
callerSessionId,
|
|
49
|
+
verb,
|
|
50
|
+
...(targetSessionId !== undefined ? { targetSessionId } : {}),
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
/** 树内判定(self 同 id 特例天然含于 isSameTree) */
|
|
54
|
+
const inOwnTree = (targetSessionId) => view.isSameTree(targetSessionId, callerSessionId);
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
name: 'session_list',
|
|
58
|
+
description: '列出本进程在管会话清单(id/标题/血缘 origin·parentId/在飞粗状态/近次模型/updatedAt)。' +
|
|
59
|
+
'默认只列本会话血缘树内会话;跨树/全会话维枚举需高危面 sessions.observe-cross 开门(未开门时跨树会话不呈现)。' +
|
|
60
|
+
'在飞粗状态由事件流尾条推导(idle = 回合闭合 / running = 回合进行中 / waiting-approval = 等待用户审批 / paused = 预算停靠——预算帽尽停靠、回充唤醒)。',
|
|
61
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
62
|
+
effect: 'read',
|
|
63
|
+
execute: async () => guard(async () => {
|
|
64
|
+
const summaries = view.listSessions();
|
|
65
|
+
// 可见性分轴:门未开只列树内(含 self);门开全列——先探门再裁
|
|
66
|
+
const door = adjudicateCapabilityDoor(deps.getOpens(), OBSERVE_CROSS_CAPABILITY);
|
|
67
|
+
const visible = door.ok ? summaries : summaries.filter((row) => inOwnTree(row.id));
|
|
68
|
+
if (door.ok) {
|
|
69
|
+
deps.onCapabilityUsed?.({ capability: OBSERVE_CROSS_CAPABILITY, callerSessionId, verb: 'session_list' });
|
|
70
|
+
}
|
|
71
|
+
if (visible.length === 0) {
|
|
72
|
+
return { content: [{ type: 'text', text: '(无可见会话——进程内无在管会话,或跨树会话未开门不呈现)' }] };
|
|
73
|
+
}
|
|
74
|
+
const header = 'id title origin parent live model updatedAt';
|
|
75
|
+
const lines = visible.map(summaryLine);
|
|
76
|
+
return { content: [{ type: 'text', text: [header, ...lines].join('\n') }] };
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'session_read',
|
|
81
|
+
description: '读取指定会话事件流尾部窗口(有帽有界——缺省 50 条、上限 200 条,含事件类型/序号/摘要)。' +
|
|
82
|
+
'树内会话(本会话及其血缘树)直接可读;跨树会话需高危面 sessions.observe-cross 开门。' +
|
|
83
|
+
'行为律:判断隔壁会话当前在做什么,优先直接向当事会话发消息确认(对话协作同人类);' +
|
|
84
|
+
'本工具面向历史档案核查与事后回溯。',
|
|
85
|
+
parameters: Type.Object({
|
|
86
|
+
sessionId: Type.String({ description: '目标会话 id' }),
|
|
87
|
+
limit: Type.Optional(Type.Number({ description: '尾窗条数(缺省 50、硬帽 200)' })),
|
|
88
|
+
}, { additionalProperties: false }),
|
|
89
|
+
effect: 'read',
|
|
90
|
+
execute: async (args) => guard(async () => {
|
|
91
|
+
// schema 段已校验(typebox String)——execute 层直断窄读
|
|
92
|
+
const sessionId = args.sessionId;
|
|
93
|
+
if (!inOwnTree(sessionId))
|
|
94
|
+
enforceCrossTreeDoor('session_read', sessionId);
|
|
95
|
+
const window = typeof args.limit === 'number' ? view.readTail(sessionId, { limit: args.limit }) : view.readTail(sessionId);
|
|
96
|
+
if (!window.exists) {
|
|
97
|
+
return { content: [{ type: 'text', text: `(目标会话 ${sessionId} 无对应行——查无此档)` }] };
|
|
98
|
+
}
|
|
99
|
+
if (window.items.length === 0) {
|
|
100
|
+
return { content: [{ type: 'text', text: '(目标会话零事件——空档案)' }] };
|
|
101
|
+
}
|
|
102
|
+
const lines = window.items.map((item) => `${item.seq} ${item.time} ${item.type} ${item.summary}`);
|
|
103
|
+
return { content: [{ type: 'text', text: ['seq time type summary', ...lines].join('\n') }] };
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'session_trace',
|
|
108
|
+
description: '查询指定会话当前进行态(在飞粗状态 + 当前回合尾窗 20 条 + 在飞工具清单)。' +
|
|
109
|
+
'树内会话直接可查;跨树会话需高危面 sessions.observe-cross 开门。' +
|
|
110
|
+
'行为律:判断隔壁会话当前在做什么,优先直接向当事会话发消息确认;本工具面向状态核查。',
|
|
111
|
+
parameters: Type.Object({ sessionId: Type.String({ description: '目标会话 id' }) }, { additionalProperties: false }),
|
|
112
|
+
effect: 'read',
|
|
113
|
+
execute: async (args) => guard(async () => {
|
|
114
|
+
const sessionId = args.sessionId; // schema 段已校验——直断窄读
|
|
115
|
+
if (!inOwnTree(sessionId))
|
|
116
|
+
enforceCrossTreeDoor('session_trace', sessionId);
|
|
117
|
+
const report = view.trace(sessionId);
|
|
118
|
+
if (!report.exists) {
|
|
119
|
+
return { content: [{ type: 'text', text: `(目标会话 ${sessionId} 无对应行——查无此档)` }] };
|
|
120
|
+
}
|
|
121
|
+
const head = `live=${report.live} inflight=${report.inflightTools.length > 0 ? report.inflightTools.join(',') : '(无)'}`;
|
|
122
|
+
const lines = report.recent.map((item) => `${item.seq} ${item.time} ${item.type} ${item.summary}`);
|
|
123
|
+
return { content: [{ type: 'text', text: [head, 'seq time type summary', ...lines].join('\n') }] };
|
|
124
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'session_status',
|
|
128
|
+
description: '查询自身坐标与环境自感(「我是谁在哪、有什么、没有什么」):本会话 id、血缘 origin 与父会话、' +
|
|
129
|
+
'工作区根、在飞粗状态、近次模型,以及可用工具清单(整形后有效可见集)、能力门态快照(高危面开/闭与' +
|
|
130
|
+
'闭门理由)、负面能力声明(未开门面/无工作区等「不要承诺」负向清单——防幻觉)、工具策略表快照' +
|
|
131
|
+
'(allow/deny 条目 + 整名族干跑裁决——装配期快照,/approval explain 为活体试解析)。' +
|
|
132
|
+
'零参数——目标恒为本会话(树内 self 档,零开门)。',
|
|
133
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
134
|
+
effect: 'read',
|
|
135
|
+
execute: async () => guard(async () => {
|
|
136
|
+
const status = view.selfStatus(callerSessionId);
|
|
137
|
+
const lines = [
|
|
138
|
+
`sessionId=${status.sessionId}`,
|
|
139
|
+
`origin=${status.origin}`,
|
|
140
|
+
`parent=${status.parentId ?? '(根会话)'}`,
|
|
141
|
+
`workspaceRoot=${status.workspaceRoot ?? '(无工作区)'}`,
|
|
142
|
+
`live=${status.live}`,
|
|
143
|
+
`model=${status.model ?? '(未发起请求)'}`,
|
|
144
|
+
];
|
|
145
|
+
// e-3 环境自感三段(env 缺席 = 基础坐标档——段整体不呈现,诚实降级不虚构)
|
|
146
|
+
if (deps.env !== undefined) {
|
|
147
|
+
const listing = deps.env.listTools();
|
|
148
|
+
lines.push(`tools(${listing.length})=${listing.map((entry) => entry.name).join(', ')}`);
|
|
149
|
+
const doors = deps.env.doorStates();
|
|
150
|
+
lines.push('capability-doors:');
|
|
151
|
+
for (const door of doors) {
|
|
152
|
+
lines.push(doorLine(door));
|
|
153
|
+
}
|
|
154
|
+
const negatives = negativeLines(doors, status.workspaceRoot);
|
|
155
|
+
lines.push('negative-capabilities:', ...negatives);
|
|
156
|
+
// ap-3 第四段:工具策略表(toolPolicy 缺席 = 段不呈现——与 doorStates
|
|
157
|
+
// 同缺席语义;装配期快照诚实,/approval entries/explain 才是活体面)
|
|
158
|
+
const policy = deps.env.toolPolicy?.();
|
|
159
|
+
if (policy !== undefined) {
|
|
160
|
+
lines.push(...policyLines(policy));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
/** 门态行(能力自省快照——闭门附诊断 reason:与执行时拒绝 message 同源,先查后用) */
|
|
169
|
+
function doorLine(door) {
|
|
170
|
+
const state = door.open ? 'open' : 'closed';
|
|
171
|
+
if (door.open)
|
|
172
|
+
return ` ${door.capability}=${state}(${door.scope})`;
|
|
173
|
+
return ` ${door.capability}=${state}(${door.reason ?? '未开门'})——${door.scope}`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 负面能力声明文案(hermes「你没有 X——不要承诺」防幻觉负向清单;
|
|
177
|
+
* 普查候选 E D4 缺缝③)。v1 两锚:闭门面(对应操作将被拒)+ 无工作区
|
|
178
|
+
* (文件工具结构性不可用)。全开 + 有工作区 = 显式「无缺失」收口行
|
|
179
|
+
* (负向清单的价值在显式闭合——「查过且无」不等于「没查」)。
|
|
180
|
+
*/
|
|
181
|
+
function negativeLines(doors, workspaceRoot) {
|
|
182
|
+
const lines = [];
|
|
183
|
+
for (const door of doors) {
|
|
184
|
+
if (!door.open)
|
|
185
|
+
lines.push(` - ${door.capability} 未开门——${door.scope}将被拒,不要承诺对应操作`);
|
|
186
|
+
}
|
|
187
|
+
if (workspaceRoot === undefined) {
|
|
188
|
+
lines.push(' - 本会话无工作区根——文件读写工具不可用,不要承诺文件操作');
|
|
189
|
+
}
|
|
190
|
+
if (lines.length === 0)
|
|
191
|
+
lines.push(' (无——当前能力面无缺失声明)');
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
/** 清单行文本(列对齐——模型消费面) */
|
|
195
|
+
function summaryLine(row) {
|
|
196
|
+
return [
|
|
197
|
+
row.id,
|
|
198
|
+
row.title ?? '(无标题)',
|
|
199
|
+
row.origin,
|
|
200
|
+
row.parentId ?? '-',
|
|
201
|
+
row.live,
|
|
202
|
+
row.model ?? '-',
|
|
203
|
+
String(row.updatedAt),
|
|
204
|
+
].join(' ');
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 实参绑定族工具名(write/edit/bash——条目命中依赖调用实参〔路径/命令〕,
|
|
208
|
+
* 整名干跑恒 miss 不可判;字面 = host 工具词汇非 safety 域逻辑——obs 零
|
|
209
|
+
* safety 依赖〔DAG〕,此处仅作呈现分族,判定真源在注入的 dryRun 闭包)。
|
|
210
|
+
*/
|
|
211
|
+
const ARG_BOUND_TOOLS = new Set(['write', 'edit', 'bash']);
|
|
212
|
+
/**
|
|
213
|
+
* 工具策略表段渲染(ap-3 第四段——03 §10.8 ap-3 定形注):① 条目清单全列
|
|
214
|
+
* (装配期快照 + 载体路径注明);② 整名族条目经 dryRun(守门行同一
|
|
215
|
+
* matchToolPolicy 注入)干跑呈行——deny/allow 命中含条目序与 reason,无
|
|
216
|
+
* 命中不占行;③ 实参绑定族(fs/bash)计数注记指路 /approval explain——
|
|
217
|
+
* 不假报无命中(诚实分形)。
|
|
218
|
+
*/
|
|
219
|
+
function policyLines(policy) {
|
|
220
|
+
const lines = [`tool-policy(${policy.entries.length}) [装配期快照 path=${policy.path}]:`];
|
|
221
|
+
if (policy.entries.length === 0) {
|
|
222
|
+
lines.push(' (空——无任何条目)');
|
|
223
|
+
return lines;
|
|
224
|
+
}
|
|
225
|
+
for (const entry of policy.entries) {
|
|
226
|
+
const bits = [`[${entry.index}]`, `tool=${entry.tool}`];
|
|
227
|
+
if (entry.pattern !== undefined)
|
|
228
|
+
bits.push(`pattern=${entry.pattern}`);
|
|
229
|
+
bits.push(`decision=${entry.decision}`);
|
|
230
|
+
if (entry.effect !== undefined)
|
|
231
|
+
bits.push(`effect=${entry.effect}`);
|
|
232
|
+
if (entry.reason !== undefined)
|
|
233
|
+
bits.push(`reason=${entry.reason}`);
|
|
234
|
+
if (entry.expiresAt !== undefined)
|
|
235
|
+
bits.push(`expiresAt=${new Date(entry.expiresAt).toISOString()}`);
|
|
236
|
+
lines.push(` ${bits.join(' ')}`);
|
|
237
|
+
}
|
|
238
|
+
// ② 整名族干跑(每工具三档并列;同条目命中的档合并呈一行「全档」)
|
|
239
|
+
const wholeNameTools = [...new Set(policy.entries.filter((e) => !ARG_BOUND_TOOLS.has(e.tool)).map((e) => e.tool))];
|
|
240
|
+
const argBoundCount = policy.entries.filter((e) => ARG_BOUND_TOOLS.has(e.tool)).length;
|
|
241
|
+
if (wholeNameTools.length > 0) {
|
|
242
|
+
lines.push(' 整名族干跑(装配期快照 + 守门行同一判定函数):');
|
|
243
|
+
for (const tool of wholeNameTools) {
|
|
244
|
+
const hits = ['read', 'write', 'exec']
|
|
245
|
+
.map((effect) => ({ effect, hit: policy.dryRun(tool, effect) }))
|
|
246
|
+
.filter((h) => h.hit !== undefined);
|
|
247
|
+
if (hits.length === 0)
|
|
248
|
+
continue; // 无命中不占行(如 allow 条目已过期)
|
|
249
|
+
const sameEntry = hits.every((h) => h.hit.index === hits[0].hit.index);
|
|
250
|
+
const verdict = (hit) => hit.decision === 'deny'
|
|
251
|
+
? `policy-deny:${hit.index}(硬拒——任何面不可翻转)`
|
|
252
|
+
: `policy-allow:${hit.index}(免问放行)`;
|
|
253
|
+
if (sameEntry) {
|
|
254
|
+
const tiers = hits.length === 3 ? '全档' : hits.map((h) => h.effect).join('/');
|
|
255
|
+
lines.push(` ${tool}: ${tiers}命中 ${verdict(hits[0].hit)}`);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
for (const { effect, hit } of hits)
|
|
259
|
+
lines.push(` ${tool}: ${effect}档命中 ${verdict(hit)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// ③ 实参绑定族计数注记(不假报无命中——指路活体试解析)
|
|
264
|
+
if (argBoundCount > 0) {
|
|
265
|
+
lines.push(` (另 ${argBoundCount} 条 write/edit/bash 族条目逐调用裁决——命中依赖路径/命令实参,/approval explain <tool> [pattern] 试解析)`);
|
|
266
|
+
}
|
|
267
|
+
return lines;
|
|
268
|
+
}
|
|
269
|
+
/** 统一异常编码(03 §2.3——BaseError 携码前置披露;obs_query 同款先例) */
|
|
270
|
+
async function guard(run) {
|
|
271
|
+
try {
|
|
272
|
+
return await run();
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
const code = error instanceof BaseError ? error.code : undefined;
|
|
276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
277
|
+
return {
|
|
278
|
+
content: [{ type: 'text', text: code ? `[${code}] ${message}` : message }],
|
|
279
|
+
isError: true,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|