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,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导入四闸(05 篇 §5.1——金样导入/外部会话导入的统一门)。
|
|
3
|
+
*
|
|
4
|
+
* 身份闸(自描述格式)→ 词汇闸(逐事件过注册表)→ 配对闸(离线 closer 预检)
|
|
5
|
+
* → 洪水闸(会话增生限流)。四闸全过才允许建会话;洪水闸是进程态限速器,
|
|
6
|
+
* 其余三闸是文件内容的纯校验。
|
|
7
|
+
*/
|
|
8
|
+
import { BaseError, isKnownEventType } from '../contracts/index.js';
|
|
9
|
+
/** 本实现认识的格式身份与支持版本(身份闸判据单源) */
|
|
10
|
+
const KNOWN_FORMAT = 'berry-agent/session';
|
|
11
|
+
const SUPPORTED_VERSION = 1;
|
|
12
|
+
/**
|
|
13
|
+
* 身份闸 + 解析:导入文件文本(JSONL——首行 _meta,其后每行一条 SessionEvent)。
|
|
14
|
+
* 不认识的自描述拒载(SESSION_IMPORT_BAD_FORMAT,fail-loud 宁拒勿吞);
|
|
15
|
+
* 行级 JSON 解析失败同码(撕裂导出文件不可静默截半)。
|
|
16
|
+
*/
|
|
17
|
+
export function parseImportFile(text) {
|
|
18
|
+
const lines = text.split('\n').filter((line) => line.trim().length > 0);
|
|
19
|
+
if (lines.length === 0) {
|
|
20
|
+
throw new BaseError('SESSION_IMPORT_BAD_FORMAT', '导入文件为空(缺 _meta 自描述首行)');
|
|
21
|
+
}
|
|
22
|
+
let meta;
|
|
23
|
+
try {
|
|
24
|
+
meta = JSON.parse(lines[0]);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
throw new BaseError('SESSION_IMPORT_BAD_FORMAT', `_meta 首行非 JSON:${String(err)}`);
|
|
28
|
+
}
|
|
29
|
+
const header = meta;
|
|
30
|
+
if (!header || typeof header !== 'object' || header.format !== KNOWN_FORMAT || header.version !== SUPPORTED_VERSION) {
|
|
31
|
+
throw new BaseError('SESSION_IMPORT_BAD_FORMAT', `导入文件自描述不认识:format=${String(header?.format)} version=${String(header?.version)}(本实现认识 ${KNOWN_FORMAT} v${SUPPORTED_VERSION})`);
|
|
32
|
+
}
|
|
33
|
+
const events = [];
|
|
34
|
+
for (let i = 1; i < lines.length; i++) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(lines[i]);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
throw new BaseError('SESSION_IMPORT_BAD_FORMAT', `第 ${i + 1} 行非 JSON(撕裂文件):${String(err)}`);
|
|
41
|
+
}
|
|
42
|
+
const event = parsed;
|
|
43
|
+
if (!event || typeof event !== 'object' || typeof event.type !== 'string' || typeof event.seq !== 'number') {
|
|
44
|
+
throw new BaseError('SESSION_IMPORT_BAD_FORMAT', `第 ${i + 1} 行非 SessionEvent 信封(缺 type/seq)`);
|
|
45
|
+
}
|
|
46
|
+
events.push(event);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
meta: { format: KNOWN_FORMAT, version: SUPPORTED_VERSION },
|
|
50
|
+
events,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 词汇闸:逐事件类型过词汇注册表——未知类型且未标 ignorable 的拒整批
|
|
55
|
+
* (宁拒勿吞;ignorable 事件向前兼容放行)。
|
|
56
|
+
*/
|
|
57
|
+
export function vocabularyGate(events) {
|
|
58
|
+
for (const event of events) {
|
|
59
|
+
if (isKnownEventType(event.type))
|
|
60
|
+
continue;
|
|
61
|
+
if (event.ignorable === true)
|
|
62
|
+
continue;
|
|
63
|
+
throw new BaseError('SESSION_UNKNOWN_EVENT_TYPE', `导入事件 seq#${event.seq} 类型 ${event.type} 未注册且未标 ignorable(拒整批)`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 配对闸:turn/tool 配对完整性预检——导入前离线跑 closer 语义的反面:
|
|
68
|
+
* 「缺闭合」可合成收形(recoverClosers 能处置);「多余闭合」(end 无 start /
|
|
69
|
+
* result 无 call / seq 断号)无法合成——拒载。seq 连续性同检(firstSeqBreak)。
|
|
70
|
+
*/
|
|
71
|
+
export function pairingGate(events) {
|
|
72
|
+
const breakAt = (() => {
|
|
73
|
+
for (let i = 0; i < events.length; i++) {
|
|
74
|
+
if (events[i].seq !== i)
|
|
75
|
+
return i;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
})();
|
|
79
|
+
if (breakAt !== null) {
|
|
80
|
+
throw new BaseError('SESSION_IMPORT_BAD_FORMAT', `导入事件 seq 不连续:位置 ${breakAt} 期望 seq ${breakAt} 实得 ${events[breakAt].seq}`);
|
|
81
|
+
}
|
|
82
|
+
let turnDepth = 0;
|
|
83
|
+
const seenCalls = new Set();
|
|
84
|
+
for (const event of events) {
|
|
85
|
+
switch (event.type) {
|
|
86
|
+
case 'turn/start':
|
|
87
|
+
turnDepth += 1;
|
|
88
|
+
break;
|
|
89
|
+
case 'turn/end':
|
|
90
|
+
turnDepth -= 1;
|
|
91
|
+
if (turnDepth < 0) {
|
|
92
|
+
throw new BaseError('SESSION_IMPORT_BAD_FORMAT', `导入事件 seq#${event.seq} turn/end 无对应 start(多余闭合不可合成)`);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case 'tool/call': {
|
|
96
|
+
const id = event.data?.toolCallId;
|
|
97
|
+
if (typeof id === 'string')
|
|
98
|
+
seenCalls.add(id);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case 'tool/result': {
|
|
102
|
+
const id = event.data?.toolCallId;
|
|
103
|
+
if (typeof id === 'string' && !seenCalls.has(id)) {
|
|
104
|
+
throw new BaseError('SESSION_IMPORT_BAD_FORMAT', `导入事件 seq#${event.seq} tool/result 无前置 call(${id}——多余闭合不可合成)`);
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
default:
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 洪水闸(会话增生限流):单进程滑动时间窗内导入/fork 新建会话数限速——防
|
|
115
|
+
* 失控脚本(插件 bug 或用户脚本)以 fork 洪水填库。首版 100/分钟(05 §5.1)。
|
|
116
|
+
* 超限抛 SESSION_SPAWN_RATE_LIMIT。时钟注入(测试假钟);窗口态纯内存。
|
|
117
|
+
*/
|
|
118
|
+
export class SessionSpawnLimiter {
|
|
119
|
+
windowMs;
|
|
120
|
+
max;
|
|
121
|
+
clock;
|
|
122
|
+
/** 窗口内新建时间戳(滑动窗账本) */
|
|
123
|
+
stamps = [];
|
|
124
|
+
constructor(options) {
|
|
125
|
+
this.windowMs = options?.windowMs ?? 60_000;
|
|
126
|
+
this.max = options?.max ?? 100;
|
|
127
|
+
this.clock = options?.clock ?? (() => Date.now());
|
|
128
|
+
}
|
|
129
|
+
/** 尝试占一个名额(超限抛;成功即记账) */
|
|
130
|
+
acquire() {
|
|
131
|
+
const now = this.clock();
|
|
132
|
+
// 清窗:只留窗口内时间戳
|
|
133
|
+
while (this.stamps.length > 0 && now - this.stamps[0] >= this.windowMs) {
|
|
134
|
+
this.stamps.shift();
|
|
135
|
+
}
|
|
136
|
+
if (this.stamps.length >= this.max) {
|
|
137
|
+
throw new BaseError('SESSION_SPAWN_RATE_LIMIT', `会话增生限流:窗口 ${this.windowMs}ms 内新建已达 ${this.max}(防 fork 洪水填库)`);
|
|
138
|
+
}
|
|
139
|
+
this.stamps.push(now);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** 便捷面:导入文件全闸校验(身份 → 词汇 → 配对;洪水闸由调用方在建会话位点执法) */
|
|
143
|
+
export function runImportGates(text) {
|
|
144
|
+
const parsed = parseImportFile(text);
|
|
145
|
+
vocabularyGate(parsed.events);
|
|
146
|
+
pairingGate(parsed.events);
|
|
147
|
+
return parsed;
|
|
148
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session 模块公开面(L1 会话事件日志——02 §2.2 席 9;机制真源 05 篇)。
|
|
3
|
+
*
|
|
4
|
+
* 本批落码:append 七步流水线(词汇/信封/快照冻结/预算刀/尾部追加/入队回调)
|
|
5
|
+
* + surfaceOp 遮蔽(appendWithSurfaceOp 正门 + 边缘纪律执法)+ 投影(derive/
|
|
6
|
+
* FoldState 增量/occludedSeqs)+ 崩溃恢复(recoverClosers 合成)+ fork 前缀
|
|
7
|
+
* 种子(forkPrefix/slicePrefix/ensureSeeded)+ 导入四闸。
|
|
8
|
+
* 物理落盘(write-behind 批落/SQLite)在 persist(L2,依赖本模块);ctx.sessions
|
|
9
|
+
* 只读服务面随 host 装配批接线。
|
|
10
|
+
*/
|
|
11
|
+
import './codes.js';
|
|
12
|
+
export { SessionLog } from './session.js';
|
|
13
|
+
export * from './snapshot.js';
|
|
14
|
+
export * from './budget.js';
|
|
15
|
+
export { createFoldState, stepFold, snapshotProjection, applyOcclusion, occludedSeqs, deriveMessages, projectedJsonChars, } from './derive.js';
|
|
16
|
+
export { recoverClosers, firstSeqBreak, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './recover.js';
|
|
17
|
+
export { forkPrefix, slicePrefix, isSeededPrefix } from './fork.js';
|
|
18
|
+
export { parseImportFile, vocabularyGate, pairingGate, SessionSpawnLimiter, runImportGates } from './import-gates.js';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** 孤儿 tool/call 的占位语义码(05 §4——按「崩溃前是否已开始执行」二分) */
|
|
2
|
+
export const TOOL_NOT_STARTED = 'TOOL_NOT_STARTED';
|
|
3
|
+
export const TOOL_OUTCOME_UNKNOWN = 'TOOL_OUTCOME_UNKNOWN';
|
|
4
|
+
/**
|
|
5
|
+
* 配对扫描 + closer 合成(单遍从头扫——05 §4 步 5「配对完整性是全日志属性,
|
|
6
|
+
* 不设窗口;增量恢复优化不做,正确性面前不省这点」):
|
|
7
|
+
* - 孤儿 tool/call(有 call 无 result):合成 tool/result——执行证据 =
|
|
8
|
+
* 守门 gate/decision 是否在场(在场 = 已开始执行 OUTCOME_UNKNOWN / 缺席 =
|
|
9
|
+
* NOT_STARTED)。gate/decision 按全日志 toolCallId 索引(决策可能晚于崩溃
|
|
10
|
+
* 前最后一刻才落——全日志扫描是保守完备面);
|
|
11
|
+
* - 未闭合压缩对(compaction/start 无 end):合成 compaction/end
|
|
12
|
+
* (reason='aborted')——孤 start 与孤儿摘要(摘要已落、遮蔽指令未落)
|
|
13
|
+
* 两子形态都收形;孤儿摘要是合法 surface 事件无需处置(按普通
|
|
14
|
+
* user/message 同视);
|
|
15
|
+
* - 未闭合 turn:合成 turn/end(reason='interrupted')——嵌套场景按深度
|
|
16
|
+
* 补 N 条(05 §4 步 4 深度计数)。
|
|
17
|
+
* 合成序:result 们 → compaction/end 们 → turn/end 们(日志合法性:turn
|
|
18
|
+
* 闭合必后于其内一切内容)。
|
|
19
|
+
*/
|
|
20
|
+
export function recoverClosers(events) {
|
|
21
|
+
// gate/decision 的 toolCallId 索引(执行证据位——按 data.toolCallId 字段)
|
|
22
|
+
const gatedCallIds = new Set();
|
|
23
|
+
// tool 配对状态:call 出现序记录 id;result 到达即摘
|
|
24
|
+
const pendingCallIds = [];
|
|
25
|
+
const settledCallIds = new Set();
|
|
26
|
+
// 压缩对状态:start 计数(end 到达减计;嵌套压缩不合法但容错计数)
|
|
27
|
+
let compactionDepth = 0;
|
|
28
|
+
// turn 深度计数
|
|
29
|
+
let turnDepth = 0;
|
|
30
|
+
for (const event of events) {
|
|
31
|
+
switch (event.type) {
|
|
32
|
+
case 'tool/call': {
|
|
33
|
+
const id = event.data?.toolCallId;
|
|
34
|
+
if (typeof id === 'string')
|
|
35
|
+
pendingCallIds.push(id);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case 'tool/result': {
|
|
39
|
+
const id = event.data?.toolCallId;
|
|
40
|
+
if (typeof id === 'string')
|
|
41
|
+
settledCallIds.add(id);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'gate/decision': {
|
|
45
|
+
const id = event.data?.toolCallId;
|
|
46
|
+
if (typeof id === 'string')
|
|
47
|
+
gatedCallIds.add(id);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case 'compaction/start':
|
|
51
|
+
compactionDepth += 1;
|
|
52
|
+
break;
|
|
53
|
+
case 'compaction/end':
|
|
54
|
+
if (compactionDepth > 0)
|
|
55
|
+
compactionDepth -= 1;
|
|
56
|
+
break;
|
|
57
|
+
case 'turn/start':
|
|
58
|
+
turnDepth += 1;
|
|
59
|
+
break;
|
|
60
|
+
case 'turn/end':
|
|
61
|
+
if (turnDepth > 0)
|
|
62
|
+
turnDepth -= 1;
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// time 基准:最后一条真实事件的 time(日志空则 0——空日志无 closer 可言,防御位)
|
|
69
|
+
const lastTime = events.length > 0 ? events[events.length - 1].time : 0;
|
|
70
|
+
const drafts = [];
|
|
71
|
+
// ① 孤儿 tool/call → 合成 tool/result(对模型诚实占位——防 provider 拒绝未配对 tool_use)
|
|
72
|
+
for (const id of pendingCallIds) {
|
|
73
|
+
if (settledCallIds.has(id))
|
|
74
|
+
continue;
|
|
75
|
+
const outcome = gatedCallIds.has(id) ? TOOL_OUTCOME_UNKNOWN : TOOL_NOT_STARTED;
|
|
76
|
+
drafts.push({
|
|
77
|
+
type: 'tool/result',
|
|
78
|
+
data: { toolCallId: id, content: `${outcome}: session recovered before completion`, error: true },
|
|
79
|
+
time: lastTime,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// ② 未闭合压缩对 → 按深度合成 compaction/end(reason='aborted'——视为未发生,下次触发重做)
|
|
83
|
+
for (let i = 0; i < compactionDepth; i++) {
|
|
84
|
+
drafts.push({ type: 'compaction/end', data: { reason: 'aborted' }, time: lastTime });
|
|
85
|
+
}
|
|
86
|
+
// ③ 未闭合 turn → 按深度补 N 条 turn/end(reason='interrupted')
|
|
87
|
+
for (let i = 0; i < turnDepth; i++) {
|
|
88
|
+
drafts.push({ type: 'turn/end', data: { reason: 'interrupted' }, time: lastTime });
|
|
89
|
+
}
|
|
90
|
+
return drafts;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 日志完整性预检(05 §4 步 1 的纯函数半边——seq 连续性;尾部残行 JSON 检测
|
|
94
|
+
* 属物理层读侧,persist 落码时接线)。撕裂尾截断由调用方按本函数结果处置。
|
|
95
|
+
* @returns 首个断点 seq(该位置事件 seq ≠ 期望值)——null = 连续
|
|
96
|
+
*/
|
|
97
|
+
export function firstSeqBreak(events) {
|
|
98
|
+
for (let i = 0; i < events.length; i++) {
|
|
99
|
+
if (events[i].seq !== i)
|
|
100
|
+
return i;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话事件日志(05 篇 §1-§3——SessionLog:append 流水线/遮蔽校验/投影增量/读原语)。
|
|
3
|
+
*
|
|
4
|
+
* 纯逻辑层:物理落盘经构造注入的 onAppend 回调(write-behind 入队面——persist
|
|
5
|
+
* 实现注入,append 热路径零 I/O);缺省回调 = 纯内存会话(CLI :memory: 诊断形态)。
|
|
6
|
+
* seq 连续性由「只在 append 尾部追加」结构保证(无独立序号分配器)。
|
|
7
|
+
*/
|
|
8
|
+
import { BaseError, getEventTypeMeta, isKnownEventType, } from '../contracts/index.js';
|
|
9
|
+
import { applyEventBudget } from './budget.js';
|
|
10
|
+
import { applyOcclusion, createFoldState, snapshotProjection, stepFold } from './derive.js';
|
|
11
|
+
import { deepFreeze, snapshotJsonValue } from './snapshot.js';
|
|
12
|
+
/**
|
|
13
|
+
* 会话事件日志实例(per-session)。三类写入口:
|
|
14
|
+
* - append:普通追加(七步流水线);
|
|
15
|
+
* - appendSynthetic:恢复合成收形(time 复用最后真实事件——确定性标记);
|
|
16
|
+
* - appendWithSurfaceOp:装载面改投影历史的唯一正门(边缘纪律五条执法)。
|
|
17
|
+
*/
|
|
18
|
+
export class SessionLog {
|
|
19
|
+
sessionId;
|
|
20
|
+
lineage;
|
|
21
|
+
/** 日志本体(append-only;data 冻结态——外部拿到也改不动) */
|
|
22
|
+
log = [];
|
|
23
|
+
/** 增量投影状态(FoldState——投影的缓存而非第二事实源,可全量重放对账) */
|
|
24
|
+
fold = createFoldState();
|
|
25
|
+
onAppend;
|
|
26
|
+
clock;
|
|
27
|
+
warn;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.sessionId = options.sessionId;
|
|
30
|
+
this.lineage = options.lineage;
|
|
31
|
+
this.onAppend = options.onAppend ?? (() => undefined);
|
|
32
|
+
this.clock = options.clock ?? (() => Date.now());
|
|
33
|
+
this.warn = options.warn ?? ((message) => console.error(message));
|
|
34
|
+
if (options.seed && options.seed.length > 0) {
|
|
35
|
+
// 种子前缀:重放折叠(投影增量与日志同建);seq 连续性顺带断言
|
|
36
|
+
for (const event of options.seed) {
|
|
37
|
+
if (event.seq !== this.log.length) {
|
|
38
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `种子前缀 seq 不连续:期望 ${this.log.length} 实得 ${event.seq}(裸拷贝不走正门)`);
|
|
39
|
+
}
|
|
40
|
+
this.log.push(event);
|
|
41
|
+
stepFold(this.fold, event);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* append 七步流水线(05 §1.2):
|
|
47
|
+
* ① 词汇检查(SESSION_UNKNOWN_EVENT_TYPE fail-loud)→ ② 信封注入(seq =
|
|
48
|
+
* log.length / time = clock)→ ③ data 单遍校验 + 快照拷贝 → ④ 预算刀裁腿
|
|
49
|
+
* (60KiB 内容帽 + errorMessage 2KiB 小帽;截断 warn 落账带码名义)→ ⑤ 事件
|
|
50
|
+
* 目录断言(运行测试面——两账当前合一于词汇注册表,07 篇门禁接线时拆静态
|
|
51
|
+
* 目录账,本步留结构位)→ ⑥ 尾部追加 → ⑦ write-behind 入队回调。
|
|
52
|
+
*/
|
|
53
|
+
append(type, data, options) {
|
|
54
|
+
// ① 词汇检查
|
|
55
|
+
if (!isKnownEventType(type)) {
|
|
56
|
+
throw new BaseError('SESSION_UNKNOWN_EVENT_TYPE', `事件类型 ${type} 未注册(词汇注册表单源——先经核心表或 registerEventType)`);
|
|
57
|
+
}
|
|
58
|
+
// ③-a 单遍校验 + 快照拷贝(非法载荷 fail-loud)
|
|
59
|
+
const snapshot = snapshotJsonValue(data, 'data');
|
|
60
|
+
// ④ 预算刀(裁腿产新对象;原 data 不动——纯函数面)
|
|
61
|
+
const budgeted = applyEventBudget(type, snapshot);
|
|
62
|
+
if (budgeted.truncated) {
|
|
63
|
+
this.warn(`[SESSION_EVENT_OVER_BUDGET] ${this.sessionId} seq#${this.log.length} ${type} 超预算截断降级`);
|
|
64
|
+
}
|
|
65
|
+
// ③-b 冻结(裁后终态冻结——「写入后不可变」;步序上冻结在裁后语义等价)
|
|
66
|
+
const frozen = deepFreeze(budgeted.data);
|
|
67
|
+
// ② 信封注入
|
|
68
|
+
const event = {
|
|
69
|
+
type,
|
|
70
|
+
seq: this.log.length,
|
|
71
|
+
time: options?.time ?? this.clock(),
|
|
72
|
+
data: frozen,
|
|
73
|
+
...(options?.ignorable !== undefined ? { ignorable: options.ignorable } : {}),
|
|
74
|
+
...(options?.surfaceOp ? { surfaceOp: options.surfaceOp } : {}),
|
|
75
|
+
...(options?.sourceEventSeqs ? { sourceEventSeqs: [...options.sourceEventSeqs] } : {}),
|
|
76
|
+
};
|
|
77
|
+
// ⑥ 尾部追加 + 投影增量步进
|
|
78
|
+
this.log.push(event);
|
|
79
|
+
stepFold(this.fold, event);
|
|
80
|
+
// ⑦ write-behind 入队
|
|
81
|
+
this.onAppend(event);
|
|
82
|
+
return event;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 恢复合成收形(05 §4):recoverClosers 产出的草稿经此入口 append——time
|
|
86
|
+
* 复用最后真实事件(确定性标记的合成面)。其余流水线与 append 同(词汇/
|
|
87
|
+
* 校验/冻结——合成物同权入日志)。
|
|
88
|
+
*/
|
|
89
|
+
appendSynthetic(draft) {
|
|
90
|
+
return this.append(draft.type, draft.data, { time: draft.time });
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 装载面改投影历史的唯一正门(05 §2.1 appendWithSurfaceOp / §2.4 retry 形):
|
|
94
|
+
* 与 compaction 内部同一校验同一码(SESSION_SURFACE_OP_INVALID)——不是绕道口。
|
|
95
|
+
* 两形在载体上分流(05 §2.4 retry 区间合法规约):载体 = `llm/retry` 走
|
|
96
|
+
* retry 形分支(三判据换轨——起点=错误 assistant / call 尸体豁免 /
|
|
97
|
+
* 尾=高水位+含 turn/end);其余载体走通用形。共用前置四条两形同执法
|
|
98
|
+
* (成对写序由调用方编排——compaction 五步骨架):
|
|
99
|
+
* - 区间合法 + 溯源完整(sourceEventSeqs 覆盖区间全部 seq 且只引用更早 seq);
|
|
100
|
+
* - 不遮带 surfaceOp 的事件(防嵌套遮蔽);
|
|
101
|
+
* - 同一位置不二次遮蔽(与既有遮蔽区间不相交);
|
|
102
|
+
* 通用形另执法两条:
|
|
103
|
+
* - 不遮进行中 turn(tool 配对完整性表检查——区间内每个 call 的配对 result
|
|
104
|
+
* 也在区间内、反之亦然;无对可切、放行);
|
|
105
|
+
* - 区间起点对齐 turn 边界(start = 0 / 首条为 turn/start / 紧接上次遮蔽终点)。
|
|
106
|
+
*/
|
|
107
|
+
appendWithSurfaceOp(type, data, surfaceOp, sourceEventSeqs) {
|
|
108
|
+
this.validateSurfaceOp(type, data, surfaceOp, sourceEventSeqs);
|
|
109
|
+
const event = this.append(type, data, { surfaceOp, sourceEventSeqs });
|
|
110
|
+
// 增量遮蔽摘除(chars 减法腿)——指令事件自身在区间外(半开区间语义,
|
|
111
|
+
// 「遮蔽者不可被自己遮蔽」由不遮带 surfaceOp 事件条保证)
|
|
112
|
+
applyOcclusion(this.fold, surfaceOp);
|
|
113
|
+
return event;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 遮蔽指令校验(边缘纪律执法——appendWithSurfaceOp 与 compaction 内部共用)。
|
|
117
|
+
* 两形分流(05 §2.4):载体 `llm/retry` 走 retry 形(三判据换轨,通用形
|
|
118
|
+
* 的起点对齐与 call 侧配对两判据对其豁免);其余载体走通用形。共用前置
|
|
119
|
+
* 四条(区间合法 / 溯源完整 / 防嵌套 / 不二次遮蔽)在分流前同执法。
|
|
120
|
+
*/
|
|
121
|
+
validateSurfaceOp(type, data, op, sourceEventSeqs) {
|
|
122
|
+
if (!(Number.isInteger(op.start) &&
|
|
123
|
+
Number.isInteger(op.end) &&
|
|
124
|
+
op.start >= 0 &&
|
|
125
|
+
op.start <= op.end &&
|
|
126
|
+
op.end < this.log.length)) {
|
|
127
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `遮蔽区间非法:[${op.start},${op.end}](当前日志长度 ${this.log.length})`);
|
|
128
|
+
}
|
|
129
|
+
// 溯源完整:sourceEventSeqs 须覆盖 [start,end] 全部 seq,且只能引用更早 seq
|
|
130
|
+
const nextSeq = this.log.length;
|
|
131
|
+
const required = new Set();
|
|
132
|
+
for (let seq = op.start; seq <= op.end; seq++)
|
|
133
|
+
required.add(seq);
|
|
134
|
+
for (const seq of sourceEventSeqs ?? []) {
|
|
135
|
+
if (!(Number.isInteger(seq) && seq >= 0 && seq < nextSeq)) {
|
|
136
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `溯源引用了非法 seq:${seq}(须为 0..${nextSeq - 1})`);
|
|
137
|
+
}
|
|
138
|
+
required.delete(seq);
|
|
139
|
+
}
|
|
140
|
+
if (required.size > 0) {
|
|
141
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `溯源不完整:sourceEventSeqs 未覆盖被遮蔽区间 seq ${[...required].sort((a, b) => a - b).join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
// 防嵌套 + 不二次遮蔽:区间内事件不得携带 surfaceOp,且与既有遮蔽区间不相交
|
|
144
|
+
const occluded = new Set();
|
|
145
|
+
for (const event of this.log) {
|
|
146
|
+
if (event.surfaceOp) {
|
|
147
|
+
for (let seq = event.surfaceOp.start; seq <= event.surfaceOp.end; seq++)
|
|
148
|
+
occluded.add(seq);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (let seq = op.start; seq <= op.end; seq++) {
|
|
152
|
+
if (occluded.has(seq)) {
|
|
153
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `seq ${seq} 已被遮蔽(不二次遮蔽/不嵌套遮蔽)`);
|
|
154
|
+
}
|
|
155
|
+
if (this.log[seq].surfaceOp) {
|
|
156
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `seq ${seq} 自身携带 surfaceOp(遮蔽者不可被遮蔽——防嵌套)`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// tool 配对位置表(两形的配对执法共用):按 toolCallId 建 call/result
|
|
160
|
+
// 全日志位置表
|
|
161
|
+
const callAt = new Map();
|
|
162
|
+
const resultAt = new Map();
|
|
163
|
+
for (let i = 0; i < this.log.length; i++) {
|
|
164
|
+
const data = this.log[i].data;
|
|
165
|
+
const id = data && typeof data === 'object' && typeof data.toolCallId === 'string' ? data.toolCallId : undefined;
|
|
166
|
+
if (id === undefined)
|
|
167
|
+
continue;
|
|
168
|
+
if (this.log[i].type === 'tool/call')
|
|
169
|
+
callAt.set(id, i);
|
|
170
|
+
else if (this.log[i].type === 'tool/result')
|
|
171
|
+
resultAt.set(id, i);
|
|
172
|
+
}
|
|
173
|
+
const inRange = (seq) => seq >= op.start && seq <= op.end;
|
|
174
|
+
// —— retry 形分支(05 §2.4 retry 区间合法规约:载体限定 llm/retry,三判据换轨)——
|
|
175
|
+
if (type === 'llm/retry') {
|
|
176
|
+
// 相位限定:surfaceOp 只许 phase=scheduled 携带(aborted/exhausted 是
|
|
177
|
+
// 事后事实事件,不携遮蔽指令)
|
|
178
|
+
const phase = data?.phase;
|
|
179
|
+
if (phase !== 'scheduled') {
|
|
180
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `llm/retry 携带 surfaceOp 须 phase=scheduled(当前 ${String(phase)})`);
|
|
181
|
+
}
|
|
182
|
+
// 起点判据:区间首条为 stopReason=error 的 assistant/message——失败 turn
|
|
183
|
+
// 的尸体起点不是 turn 边界(通用形起点对齐对本形豁免)
|
|
184
|
+
const first = this.log[op.start];
|
|
185
|
+
const firstData = first.data;
|
|
186
|
+
if (first.type !== 'assistant/message' || firstData?.stopReason !== 'error') {
|
|
187
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `retry 遮蔽区间起点须为 stopReason=error 的 assistant/message(seq ${op.start} 是 ${first.type})`);
|
|
188
|
+
}
|
|
189
|
+
// 尾=高水位:区间尾为追加时点日志末条 seq——盖住失败 turn 一切残余
|
|
190
|
+
// (伴生尸体与垫底的 turn/end、零笔 llm/usage)
|
|
191
|
+
if (op.end !== this.log.length - 1) {
|
|
192
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `retry 遮蔽区间尾须为日志高水位 ${this.log.length - 1}(当前 ${op.end})`);
|
|
193
|
+
}
|
|
194
|
+
// 区间含 turn/end:未结算 turn 不可遮(turn 已收形是重试判定的前置)
|
|
195
|
+
let settled = false;
|
|
196
|
+
for (let seq = op.start; seq <= op.end; seq++) {
|
|
197
|
+
if (this.log[seq].type === 'turn/end') {
|
|
198
|
+
settled = true;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!settled) {
|
|
203
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', 'retry 遮蔽区间内未含 turn/end(未结算 turn 不可遮)');
|
|
204
|
+
}
|
|
205
|
+
// 配对执法(单向):result 侧照旧——区间内 result 的配对 call 不得在
|
|
206
|
+
// 区间外(起点不得切在配对中间);call 侧豁免——区间内未配对 tool/call
|
|
207
|
+
// 是流中断尸体,恰是要盖住的对象
|
|
208
|
+
for (const [id, resultSeq] of resultAt) {
|
|
209
|
+
const callSeq = callAt.get(id);
|
|
210
|
+
if (inRange(resultSeq) && callSeq !== undefined && !inRange(callSeq)) {
|
|
211
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `tool 配对被切断:result@${resultSeq}(${id})的 call@${callSeq} 在区间外`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// —— 通用形:tool 配对完整性(不遮进行中 turn——切点永不落在配对中间)——
|
|
217
|
+
// 区间内每个 call 的配对 result 必在区间内、区间内每个 result 的配对
|
|
218
|
+
// call 必在区间内
|
|
219
|
+
for (const [id, callSeq] of callAt) {
|
|
220
|
+
const resultSeq = resultAt.get(id);
|
|
221
|
+
if (inRange(callSeq) && resultSeq !== undefined && !inRange(resultSeq)) {
|
|
222
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `tool 配对被切断:call@${callSeq}(${id})的 result@${resultSeq} 在区间外`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const [id, resultSeq] of resultAt) {
|
|
226
|
+
const callSeq = callAt.get(id);
|
|
227
|
+
if (inRange(resultSeq) && callSeq !== undefined && !inRange(callSeq)) {
|
|
228
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `tool 配对被切断:result@${resultSeq}(${id})的 call@${callSeq} 在区间外`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// 区间起点对齐 turn 单元边界:start = 0 / 首条为 turn/start(合成形)/
|
|
232
|
+
// start-1 为 turn/end(真实 driver 形——提交先落 user/message 后起 turn,
|
|
233
|
+
// 紧随闭合 turn 的切点即次 turn 单元首位;U4-3 装配批集成勘正——原两形
|
|
234
|
+
// 在真实日志形下永假致阈值路不可落账)/ 紧接上次遮蔽终点(连续压缩切点
|
|
235
|
+
// ——判据是既存遮蔽区间终点恰为 start-1,全日志扫描;prev 事件自身携带
|
|
236
|
+
// surfaceOp 的旧判据是死码:被遮蔽节点永不带遮蔽)
|
|
237
|
+
if (op.start !== 0) {
|
|
238
|
+
const first = this.log[op.start];
|
|
239
|
+
const afterTurnEnd = this.log[op.start - 1]?.type === 'turn/end';
|
|
240
|
+
const afterOcclusion = this.log.some((event) => event.surfaceOp?.end === op.start - 1);
|
|
241
|
+
if (first.type !== 'turn/start' && !afterTurnEnd && !afterOcclusion) {
|
|
242
|
+
throw new BaseError('SESSION_SURFACE_OP_INVALID', `区间起点 ${op.start} 未对齐 turn 边界(首条 ${first.type};须 start=0 / turn/start / 紧随 turn/end / 紧接上次遮蔽终点)`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** 日志只读视图(外部不可 push——冻结数组视图语义由约定 + append-only 纪律保) */
|
|
247
|
+
events() {
|
|
248
|
+
return this.log;
|
|
249
|
+
}
|
|
250
|
+
/** 按类型过滤读(含 fromSeq 窗口参数)——插件只读面 ctx.sessions 的底层原语(05 §3.2) */
|
|
251
|
+
eventsOfType(type, opts) {
|
|
252
|
+
const from = opts?.fromSeq ?? 0;
|
|
253
|
+
return this.log.filter((event) => event.type === type && event.seq >= from);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 最后一条完整 turn 边界的 seq(05 §3.2——「进行中 turn」判定的单源:工具
|
|
257
|
+
* 配对、遮蔽边界、fork 边界共用)。词级过滤下标不得冒充位置——返回值必是
|
|
258
|
+
* 真实事件 seq;无闭合 turn 返回 -1。
|
|
259
|
+
*/
|
|
260
|
+
lastClosedBoundary() {
|
|
261
|
+
for (let i = this.log.length - 1; i >= 0; i--) {
|
|
262
|
+
if (this.log[i].type === 'turn/end')
|
|
263
|
+
return this.log[i].seq;
|
|
264
|
+
}
|
|
265
|
+
return -1;
|
|
266
|
+
}
|
|
267
|
+
/** 投影快照(活缓冲折入拷贝尾——调用方可安全持有的独立数组) */
|
|
268
|
+
projection() {
|
|
269
|
+
return snapshotProjection(this.fold);
|
|
270
|
+
}
|
|
271
|
+
/** 投影字符数(阈值兜底判据查询面——FoldState.chars 读出) */
|
|
272
|
+
projectedChars() {
|
|
273
|
+
return this.fold.chars;
|
|
274
|
+
}
|
|
275
|
+
/** 事件类型元信息查询透传(诊断面——类别/归属消费) */
|
|
276
|
+
metaOf(type) {
|
|
277
|
+
return getEventTypeMeta(type);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 事件 data 快照与冻结(05 篇 §1.2 append 流水线步 3——JSON 快照冻结)。
|
|
3
|
+
*
|
|
4
|
+
* 单遍校验 + 拷贝:递归读一次即产出快照副本(getter 双读免疫),同时确保
|
|
5
|
+
* 结果是纯 JSON 值(可被 SQLite 与任何读侧无损序列化)。deepFreeze 冻结后
|
|
6
|
+
* 任何持有者都改不动事件日志里的数据——不可变性靠语言机制,不靠约定。
|
|
7
|
+
*/
|
|
8
|
+
import { BaseError } from '../contracts/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* 单遍 JSON 校验 + 快照拷贝。
|
|
11
|
+
* 仅接受 null / boolean / number(有限) / string / 纯对象 / 数组;
|
|
12
|
+
* undefined / function / symbol / bigint / NaN·Infinity / 类实例(Date、Map 等)
|
|
13
|
+
* / 循环引用一律拒绝(SESSION_EVENT_DATA_INVALID)。
|
|
14
|
+
* @param value 待校验的载荷
|
|
15
|
+
* @param path 诊断用字段路径(顶层传 'data'——报错可读定位到 'data.items[2]')
|
|
16
|
+
* @returns 深拷贝快照(与原值再无引用共享)
|
|
17
|
+
*/
|
|
18
|
+
export function snapshotJsonValue(value, path) {
|
|
19
|
+
return snap(value, path, new Set());
|
|
20
|
+
}
|
|
21
|
+
/** 递归实现:ancestors 记录当前路径上的祖先对象,用于循环引用检测 */
|
|
22
|
+
function snap(value, path, ancestors) {
|
|
23
|
+
// 原始值:null/boolean/string 直接过;number 必须有限(JSON.stringify 会把 NaN/Infinity 变 null——静默变性不可接受)
|
|
24
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === 'number') {
|
|
28
|
+
if (!Number.isFinite(value)) {
|
|
29
|
+
throw dataInvalid(path, 'number 必须有限(NaN/Infinity 不可序列化)');
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
// undefined 单列:成因句帮定位——可选链取值/条件展开把 undefined 织进载荷是
|
|
34
|
+
// 最常见病灶;JSON.stringify 对 undefined 属性静默丢键、稀疏数组变 null,
|
|
35
|
+
// durable 事件不可走「序列化时碰运气」路线
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
throw dataInvalid(path, 'undefined(展开对象携带了未定义字段——检查可选链/条件展开)');
|
|
38
|
+
}
|
|
39
|
+
// 显式拒绝的非 JSON 类型
|
|
40
|
+
if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'bigint') {
|
|
41
|
+
throw dataInvalid(path, `不允许的值类型:${typeof value}`);
|
|
42
|
+
}
|
|
43
|
+
if (typeof value !== 'object') {
|
|
44
|
+
throw dataInvalid(path, `不允许的值类型:${typeof value}`);
|
|
45
|
+
}
|
|
46
|
+
if (ancestors.has(value)) {
|
|
47
|
+
throw dataInvalid(path, '循环引用');
|
|
48
|
+
}
|
|
49
|
+
// 类实例(Date/Map/Set/自定义类)原型非 Object/Array 原型——拒绝(先转纯 JSON 结构再落账)
|
|
50
|
+
const proto = Object.getPrototypeOf(value);
|
|
51
|
+
const isPlainObject = proto === Object.prototype || proto === null;
|
|
52
|
+
const isPlainArray = Array.isArray(value) && proto === Array.prototype;
|
|
53
|
+
if (!isPlainObject && !isPlainArray) {
|
|
54
|
+
throw dataInvalid(path, '只接受纯对象/数组(类实例请先转纯 JSON 结构)');
|
|
55
|
+
}
|
|
56
|
+
ancestors.add(value);
|
|
57
|
+
try {
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
const out = new Array(value.length);
|
|
60
|
+
for (let i = 0; i < value.length; i++) {
|
|
61
|
+
out[i] = snap(value[i], `${path}[${i}]`, ancestors);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const key of Object.keys(value)) {
|
|
67
|
+
// 每个属性只读一次(snap 内完成校验+拷贝),属性描述符(getter 等)不会二次触发
|
|
68
|
+
out[key] = snap(value[key], `${path}.${key}`, ancestors);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
ancestors.delete(value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** 统一的非法载荷报错构造 */
|
|
77
|
+
function dataInvalid(path, why) {
|
|
78
|
+
return new BaseError('SESSION_EVENT_DATA_INVALID', `事件 data 非法于 ${path}:${why}`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 深冻结(返回同引用):递归 Object.freeze 数组与纯对象。
|
|
82
|
+
* 冻结的是快照副本,外部原对象不受影响;配合快照实现「写入后不可变」。
|
|
83
|
+
* 已访问节点用 WeakSet 跳过,防环形结构死循环(快照虽拒绝循环引用,本函数
|
|
84
|
+
* 亦服务于 fork 前缀等已知纯 JSON 面——防御性自足)。
|
|
85
|
+
*/
|
|
86
|
+
export function deepFreeze(value) {
|
|
87
|
+
freezeWalk(value, new WeakSet());
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function freezeWalk(value, seen) {
|
|
91
|
+
if (value === null || typeof value !== 'object') {
|
|
92
|
+
return; // 原始值无需冻结
|
|
93
|
+
}
|
|
94
|
+
const obj = value;
|
|
95
|
+
if (seen.has(obj)) {
|
|
96
|
+
return; // 已访问(共享引用),跳过
|
|
97
|
+
}
|
|
98
|
+
seen.add(obj);
|
|
99
|
+
Object.freeze(obj);
|
|
100
|
+
for (const key of Object.keys(obj)) {
|
|
101
|
+
freezeWalk(obj[key], seen);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** 序列化字节数(体积度量用;UTF-8 真实字节,中英文案一致对待。值域已由 snapshotJsonValue 保证纯 JSON) */
|
|
105
|
+
export function jsonBytes(value) {
|
|
106
|
+
return Buffer.byteLength(JSON.stringify(value) ?? 'null', 'utf8');
|
|
107
|
+
}
|