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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sdk/security — HTTP 面三防线判定器与 token 生成(批 13e-1;10.4 回环钉死
|
|
3
|
+
* 条款同律复用引证 + 03 §10.6 差异面④⑤ 扩集谓词)。
|
|
4
|
+
*
|
|
5
|
+
* 纯函数件:判定器零 IO 零依赖(传输实装 13e-2 在 node:http 挂点位消费),
|
|
6
|
+
* 词面单源如下——
|
|
7
|
+
* - 回环白名单三形:`127.0.0.1` / `localhost` / `[::1]`(带可选 `:port`);
|
|
8
|
+
* - 非回环绑定形态 Host 扩集(差异面④):校验值域与配置绑定地址匹配——
|
|
9
|
+
* Host 主机部须恰等于绑定 host(大小写不敏感;IPv6 括号形同归一),
|
|
10
|
+
* **不接受任意 Host**(DNS rebinding 防线原义保留——attacker 域名 ≠ 绑定
|
|
11
|
+
* 地址字面即拒);回环绑定形态值域不变;
|
|
12
|
+
* - Origin 硬防线(10.4③):带 Origin 且非同源三形 = 403;**无 Origin 头
|
|
13
|
+
* 放行**(防线判浏览器跨源,不判非浏览器客户端——SDK 调用方恒无 Origin)。
|
|
14
|
+
*/
|
|
15
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
16
|
+
/** 回环白名单主机三形(10.4② 值域穷举字面——回环绑定形态恒用此值域) */
|
|
17
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);
|
|
18
|
+
/** 归一 Host 头主机部:剥可选 `:port`(IPv6 括号形内冒号不剥)+ 小写归一 */
|
|
19
|
+
export function hostPartOf(hostHeader) {
|
|
20
|
+
const lower = hostHeader.toLowerCase();
|
|
21
|
+
// 括号 IPv6 形 `[::1]:7860` —— 只剥闭括号后的端口段
|
|
22
|
+
if (lower.startsWith('[')) {
|
|
23
|
+
const close = lower.indexOf(']');
|
|
24
|
+
if (close !== -1)
|
|
25
|
+
return lower.slice(0, close + 1);
|
|
26
|
+
return lower;
|
|
27
|
+
}
|
|
28
|
+
const colon = lower.indexOf(':');
|
|
29
|
+
return colon === -1 ? lower : lower.slice(0, colon);
|
|
30
|
+
}
|
|
31
|
+
/** 判定 host 是否回环三形之一(`::1` 裸形归一 `[::1]` 同判) */
|
|
32
|
+
export function isLoopbackHost(host) {
|
|
33
|
+
const normalized = host.toLowerCase() === '::1' ? '[::1]' : host.toLowerCase();
|
|
34
|
+
return LOOPBACK_HOSTS.has(normalized);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Host 头白名单判定(三防线②——10.4 同律 + 差异面④ 扩集)。
|
|
38
|
+
* @param hostHeader 请求 Host 头原值(缺席/空串 = 拒——HTTP/1.1 Host 必在场)
|
|
39
|
+
* @param bindHost 配置绑定地址(回环三形或显式非回环地址)
|
|
40
|
+
*/
|
|
41
|
+
export function judgeHostHeader(hostHeader, bindHost) {
|
|
42
|
+
if (hostHeader === undefined || hostHeader === '') {
|
|
43
|
+
return { ok: false, reason: 'Host 头缺席(HTTP/1.1 必在场)' };
|
|
44
|
+
}
|
|
45
|
+
const part = hostPartOf(hostHeader);
|
|
46
|
+
if (isLoopbackHost(bindHost)) {
|
|
47
|
+
// 回环绑定形态:值域 = 回环穷举字面(差异面④「回环绑定形态值域不变」)
|
|
48
|
+
if (LOOPBACK_HOSTS.has(part))
|
|
49
|
+
return { ok: true };
|
|
50
|
+
return { ok: false, reason: `回环绑定形态只受回环 Host(收 ${part})` };
|
|
51
|
+
}
|
|
52
|
+
// 非回环绑定形态:扩集谓词 = Host 主机部恰等于绑定地址(地址族匹配的严格形)
|
|
53
|
+
const bindNormalized = bindHost.toLowerCase() === '::1' ? '[::1]' : bindHost.toLowerCase();
|
|
54
|
+
const bindBracketed = bindNormalized.includes(':') && !bindNormalized.startsWith('[') ? `[${bindNormalized}]` : bindNormalized;
|
|
55
|
+
if (part === bindNormalized || part === bindBracketed)
|
|
56
|
+
return { ok: true };
|
|
57
|
+
return { ok: false, reason: `非回环绑定形态只受绑定地址 Host(收 ${part} 期 ${bindNormalized})` };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Origin 硬防线判定(三防线③——10.4 同律)。同源三形 = 监听回环三形 +
|
|
61
|
+
* 监听端口(sdk 面 scheme 恒 http);**无 Origin 放行**(程序调用方)。
|
|
62
|
+
*/
|
|
63
|
+
export function originAllowed(origin, listenHost, listenPort) {
|
|
64
|
+
if (origin === undefined || origin === '')
|
|
65
|
+
return true; // 防线判浏览器跨源
|
|
66
|
+
// 回环绑定形态:同源三形 = 回环穷举(值域与 Host 白名单对称——10.4③)
|
|
67
|
+
const sameOrigin = new Set([
|
|
68
|
+
`http://127.0.0.1:${listenPort}`,
|
|
69
|
+
`http://localhost:${listenPort}`,
|
|
70
|
+
`http://[::1]:${listenPort}`,
|
|
71
|
+
]);
|
|
72
|
+
if (isLoopbackHost(listenHost) && sameOrigin.has(origin))
|
|
73
|
+
return true;
|
|
74
|
+
// 非回环绑定形态:同源 = scheme://绑定地址:端口 恰等(与 Host 扩集同族)
|
|
75
|
+
const host = listenHost.toLowerCase();
|
|
76
|
+
const bracketed = host === '::1' ? '[::1]' : host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
77
|
+
return origin === `http://${bracketed}:${listenPort}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 生成一次性 token(10.4 token 鉴权件本体保证同律:32 字节随机 hex、只存
|
|
81
|
+
* 内存不落盘——披露面归宿主形态)。
|
|
82
|
+
*/
|
|
83
|
+
export function generateToken() {
|
|
84
|
+
return randomBytes(32).toString('hex');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* token 常时验换(18a-1'——03 §10.6 路由扩展位 helper `verifyToken` 执法位):
|
|
88
|
+
* sha256 摘要恒长 + timingSafeEqual——任意长度输入先归一到 32 字节摘要再比对,
|
|
89
|
+
* 码位长度泄露面封死(`/v1/*` Bearer 判由此自字符串直比升常时比对——有意
|
|
90
|
+
* 行为增强注记,同条款)。
|
|
91
|
+
*/
|
|
92
|
+
export function verifyTokenConstantTime(actual, expected) {
|
|
93
|
+
const a = createHash('sha256').update(actual).digest();
|
|
94
|
+
const b = createHash('sha256').update(expected).digest();
|
|
95
|
+
return timingSafeEqual(a, b);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 启动断言(三防线①——差异面③ fail-closed 拒启律不豁免):非回环 TCP 绑定
|
|
99
|
+
* 必配预置凭证;违例即拒启(daemon 形退 2——07 §5 定名注)。收整个开面配置
|
|
100
|
+
* 形(socketPath 无关判定——只审 TCP 侧与凭证位)。TCP 侧单形/数组归一逐项
|
|
101
|
+
* 判(18a-1' 多监听并存——一票非回环无凭证即整面拒启)。
|
|
102
|
+
*/
|
|
103
|
+
export function judgeListenConfig(config) {
|
|
104
|
+
if (config.tcp === undefined)
|
|
105
|
+
return { ok: true };
|
|
106
|
+
const specs = Array.isArray(config.tcp) ? config.tcp : [config.tcp];
|
|
107
|
+
for (const spec of specs) {
|
|
108
|
+
if (!isLoopbackHost(spec.host) && (config.token === undefined || config.token === '')) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
reason: `非回环绑定(${spec.host})必配鉴权凭证 BERRY_AGENT_SDK_TOKEN——fail-closed 拒启(03 §10.6 差异面③)`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { ok: true };
|
|
116
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** HTTP 端点路由词面(03 §10.6 差异面①/批 13e 定形:程序调用面五 POST/GET + SSE 建立位) */
|
|
2
|
+
export const SDK_HTTP_ENDPOINTS = {
|
|
3
|
+
/** POST——prompt 新发(sessionId 缺席即新建) */
|
|
4
|
+
prompt: '/v1/prompt',
|
|
5
|
+
/** POST——interrupt(无应答档 HTTP 形 = 204 空体) */
|
|
6
|
+
interrupt: '/v1/interrupt',
|
|
7
|
+
/** POST——审批应答(跨入口竞速回执) */
|
|
8
|
+
decide: '/v1/decide',
|
|
9
|
+
/** POST——断线对账读面(since 窗口 (since, 高水位]) */
|
|
10
|
+
entries: '/v1/entries',
|
|
11
|
+
/** GET——会话清单 */
|
|
12
|
+
sessions: '/v1/sessions',
|
|
13
|
+
/** GET SSE——事件流建立位(即 hello 动词的 HTTP 承载:sessionId/after/noDelta 入查询参) */
|
|
14
|
+
events: '/v1/events',
|
|
15
|
+
};
|
|
16
|
+
/** 协议版本头名(HTTP 无连接级 hello 位——每请求携版本,不符即 400 SDK_PROTOCOL_MISMATCH) */
|
|
17
|
+
export const SDK_PROTOCOL_HEADER = 'x-sdk-protocol';
|
|
18
|
+
/** 鉴权头(10.4「监听 ⇒ 鉴权」恒在场——Bearer 形携 token) */
|
|
19
|
+
export const SDK_AUTH_HEADER = 'authorization';
|
|
20
|
+
/** 心跳看门狗节拍(10.4 同律:SSE 注释行 ping 每 30s + 写侧 90s 判死) */
|
|
21
|
+
export const SDK_SSE_PING_INTERVAL_MS = 30_000;
|
|
22
|
+
/** SSE 写侧看门狗判死窗(ping 写失败或写超时即 reap 连接——读侧判死不可实施) */
|
|
23
|
+
export const SDK_SSE_WRITE_TIMEOUT_MS = 90_000;
|
|
24
|
+
/* ---------------- 路由扩展位契约(18a-1';03 §10.6 路由扩展位段——2026-09-07 webui 插件化批) ---------------- */
|
|
25
|
+
/**
|
|
26
|
+
* open 档合法语义位闭集(射界枚举钉死——注册期白名单校验、越界即拒 fail-loud,
|
|
27
|
+
* 代码可执法非仅冷读审读):
|
|
28
|
+
* - `liveness`——探活(GET /api/health 形:零敏感面应答);
|
|
29
|
+
* - `auth-exchange`——鉴权换证(POST /api/auth 形:token 换 cookie 桥);
|
|
30
|
+
* - `static-shell`——静态壳(SPA 静态面 + fallback:壳先于鉴权必须可载)。
|
|
31
|
+
*/
|
|
32
|
+
export const SDK_ROUTE_OPEN_PURPOSES = ['liveness', 'auth-exchange', 'static-shell'];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** 内容腿预算(字节):60KiB——05 §1.2 步 4 单级设计(berry-agent 规范再设计,非 berry 两级) */
|
|
2
|
+
export const EVENT_BUDGET_BYTES = 60 * 1024;
|
|
3
|
+
/** errorMessage 腿小帽(字节):与 content 腿各自独立计帽——防同源双载叠加击穿(05 §1.1 assistant/message 表注) */
|
|
4
|
+
export const ERROR_MESSAGE_BUDGET_BYTES = 2 * 1024;
|
|
5
|
+
/** 字符串的护栏同尺体积(与 jsonBytes 同式:JSON.stringify 的转义后字节——两把尺同单位) */
|
|
6
|
+
export function escapedBytes(text) {
|
|
7
|
+
return Buffer.byteLength(JSON.stringify(text), 'utf8');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 字符串按「护栏同尺」字节预算截断(超预算加尾标记;不超原样返回)。
|
|
11
|
+
* 尾标记形态 = `…[truncated N chars]`(N = 截掉字符数——05 §1.2 步 4 字面)。
|
|
12
|
+
*/
|
|
13
|
+
export function budgetString(text, budget = EVENT_BUDGET_BYTES) {
|
|
14
|
+
if (escapedBytes(text) <= budget)
|
|
15
|
+
return text;
|
|
16
|
+
// 转义只增不减:原文截到 budget 字节是安全上界(转义后 ≤ 原文 × 截点)
|
|
17
|
+
let sliced = Buffer.from(text, 'utf8').subarray(0, budget).toString('utf8');
|
|
18
|
+
// 收敛循环:截段+尾标记的转义体积压进预算。按超限比例收缩(几何收敛——
|
|
19
|
+
// 控制字符 6x 形态一轮即近界),近界后逐字符兜底(subarray 可能切在多字节
|
|
20
|
+
// 字符中间,toString 对坏尾替换 U+FFFD——可接受且不影响收敛)
|
|
21
|
+
while (escapedBytes(sliced) + escapedBytes(marker(text.length - sliced.length)) > budget) {
|
|
22
|
+
sliced = sliced.slice(0, Math.max(sliced.length - 1, 0));
|
|
23
|
+
if (sliced.length === 0)
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
return sliced + marker(text.length - sliced.length);
|
|
27
|
+
}
|
|
28
|
+
/** 截断尾标记(N = 截掉的 UTF-16 码点数) */
|
|
29
|
+
function marker(truncatedChars) {
|
|
30
|
+
return `…[truncated ${truncatedChars} chars]`;
|
|
31
|
+
}
|
|
32
|
+
/** 单块体积(截断预算的计量单位;image 按 base64 字符串长度近似——base64 字母表无转义字符,转义前后等长) */
|
|
33
|
+
function blockBytes(block) {
|
|
34
|
+
switch (block.type) {
|
|
35
|
+
case 'text':
|
|
36
|
+
return escapedBytes(block.text);
|
|
37
|
+
case 'thinking':
|
|
38
|
+
return escapedBytes(block.thinking);
|
|
39
|
+
case 'image':
|
|
40
|
+
return block.data.length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 内容腿截断(块数组形态:逐块累加判断——多腿同源叠加击穿的防线;text/thinking
|
|
45
|
+
* 块按剩余预算截字节加尾标记,image 块放不下剩余预算时落 image-blob-dropped
|
|
46
|
+
* 占位——base64 动辄超帽,保留语义不保像素,05 §1.2 步 4)。
|
|
47
|
+
* 纯字符串形态(user 消息):整串按字节截。
|
|
48
|
+
*/
|
|
49
|
+
export function truncateContent(content, budget = EVENT_BUDGET_BYTES) {
|
|
50
|
+
if (typeof content === 'string') {
|
|
51
|
+
return budgetString(content, budget);
|
|
52
|
+
}
|
|
53
|
+
// 快路径:总字节在预算内直接通过(绝大多数消息零开销)
|
|
54
|
+
let total = 0;
|
|
55
|
+
for (const block of content) {
|
|
56
|
+
total += blockBytes(block);
|
|
57
|
+
}
|
|
58
|
+
if (total <= budget)
|
|
59
|
+
return content;
|
|
60
|
+
const out = [];
|
|
61
|
+
let remaining = budget;
|
|
62
|
+
for (const block of content) {
|
|
63
|
+
if (block.type === 'image') {
|
|
64
|
+
if (block.data.length <= remaining) {
|
|
65
|
+
// 预算容得下:保留像素(快路径外的少数小图)
|
|
66
|
+
out.push(block);
|
|
67
|
+
remaining -= block.data.length;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// 放不下 → 占位(事件日志是审计面非媒体库)
|
|
71
|
+
out.push({ type: 'text', text: '[image-blob-dropped: durable budget]' });
|
|
72
|
+
remaining = 0;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// text / thinking:按剩余预算截字节(度量与判定同尺——转义后体积)
|
|
76
|
+
const text = block.type === 'text' ? block.text : block.thinking;
|
|
77
|
+
const size = escapedBytes(text);
|
|
78
|
+
if (size <= remaining) {
|
|
79
|
+
out.push(block);
|
|
80
|
+
remaining -= size;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (remaining > 0) {
|
|
84
|
+
const marked = budgetString(text, remaining);
|
|
85
|
+
out.push(block.type === 'text' ? { type: 'text', text: marked } : { type: 'thinking', thinking: marked });
|
|
86
|
+
remaining = 0;
|
|
87
|
+
}
|
|
88
|
+
// 预算耗尽后的剩余 text/thinking 块整块丢弃(尾标记已声明截断事实)
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* append 流水线步 4 入口:按事件类型分派裁腿(单遍、只查不改原值)。
|
|
94
|
+
* 分派表(05 §1.2 步 4):
|
|
95
|
+
* - user/message / assistant/message:截 content(块数组逐块累加);
|
|
96
|
+
* - assistant/message 的 errorMessage 腿独立 2KiB 小帽;
|
|
97
|
+
* - tool/call:截 arguments(原始字符串);
|
|
98
|
+
* - tool/result:截 content;
|
|
99
|
+
* - 其余类型超帽:data 整体截断加尾标记(序列化为带尾标记的字符串——最后
|
|
100
|
+
* 手段的可观测降级,优先保可读)。
|
|
101
|
+
*/
|
|
102
|
+
export function applyEventBudget(type, data) {
|
|
103
|
+
if (data === null || typeof data !== 'object') {
|
|
104
|
+
// 原始值形态(含空对象快照类):整体体积由护栏语义覆盖,无腿可裁——原样过
|
|
105
|
+
return { data, truncated: false };
|
|
106
|
+
}
|
|
107
|
+
const record = data;
|
|
108
|
+
switch (type) {
|
|
109
|
+
case 'user/message':
|
|
110
|
+
case 'tool/result': {
|
|
111
|
+
const content = record.content;
|
|
112
|
+
if (typeof content === 'string' || Array.isArray(content)) {
|
|
113
|
+
const clipped = truncateContent(content);
|
|
114
|
+
return clipped === content
|
|
115
|
+
? { data, truncated: false }
|
|
116
|
+
: { data: { ...record, content: clipped }, truncated: true };
|
|
117
|
+
}
|
|
118
|
+
return { data, truncated: false };
|
|
119
|
+
}
|
|
120
|
+
case 'assistant/message': {
|
|
121
|
+
let changed = false;
|
|
122
|
+
const next = { ...record };
|
|
123
|
+
if (typeof record.content === 'string' || Array.isArray(record.content)) {
|
|
124
|
+
const clipped = truncateContent(record.content);
|
|
125
|
+
if (clipped !== record.content) {
|
|
126
|
+
next.content = clipped;
|
|
127
|
+
changed = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// errorMessage 腿独立小帽(与 content 各自计帽——同源双载防线)
|
|
131
|
+
if (typeof record.errorMessage === 'string') {
|
|
132
|
+
const clipped = budgetString(record.errorMessage, ERROR_MESSAGE_BUDGET_BYTES);
|
|
133
|
+
if (clipped !== record.errorMessage) {
|
|
134
|
+
next.errorMessage = clipped;
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { data: changed ? next : data, truncated: changed };
|
|
139
|
+
}
|
|
140
|
+
case 'tool/call': {
|
|
141
|
+
const args = record.arguments;
|
|
142
|
+
if (typeof args === 'string') {
|
|
143
|
+
const clipped = budgetString(args);
|
|
144
|
+
return clipped === args
|
|
145
|
+
? { data, truncated: false }
|
|
146
|
+
: { data: { ...record, arguments: clipped }, truncated: true };
|
|
147
|
+
}
|
|
148
|
+
return { data, truncated: false };
|
|
149
|
+
}
|
|
150
|
+
default: {
|
|
151
|
+
// 其余类型:data 整体截断(序列化字符串 + 尾标记——保留前段可读)
|
|
152
|
+
const serialized = JSON.stringify(data);
|
|
153
|
+
if (serialized === undefined || escapedBytes(serialized) <= EVENT_BUDGET_BYTES) {
|
|
154
|
+
return { data, truncated: false };
|
|
155
|
+
}
|
|
156
|
+
return { data: budgetString(serialized), truncated: true };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session 模块域错误码注册(05 篇会话与存储——append 流水线/遮蔽/导入/fork 各执法点)。
|
|
3
|
+
*
|
|
4
|
+
* contracts CORE_ERROR_CODES 已含三枚(第一批):SESSION_UNKNOWN_EVENT_TYPE
|
|
5
|
+
* (词汇检查)/ SESSION_EVENT_OVER_BUDGET(预算刀身份码——裁腿不抛、warn 落账
|
|
6
|
+
* 带码标识)/ SESSION_CORE_TYPE_FORBIDDEN(核心词身份双闸)。本文件补域内五枚。
|
|
7
|
+
*/
|
|
8
|
+
import { registerErrorCodes } from '../contracts/index.js';
|
|
9
|
+
registerErrorCodes([
|
|
10
|
+
// 05 §1.2 步 3:data 单遍校验——非法载荷 fail-loud(undefined 织进载荷/类实例/循环引用等)
|
|
11
|
+
{
|
|
12
|
+
code: 'SESSION_EVENT_DATA_INVALID',
|
|
13
|
+
module: 'session',
|
|
14
|
+
description: '事件 data 非法(非纯 JSON 值:undefined/function/symbol/bigint/NaN·Infinity/类实例/循环引用)',
|
|
15
|
+
},
|
|
16
|
+
// 05 §2.1 边缘纪律五条执法:appendWithSurfaceOp 与 compaction 内部同一码
|
|
17
|
+
{
|
|
18
|
+
code: 'SESSION_SURFACE_OP_INVALID',
|
|
19
|
+
module: 'session',
|
|
20
|
+
description: '遮蔽指令非法(区间越界/溯源不完整/切在 tool 配对中间/二次遮蔽/嵌套遮蔽)',
|
|
21
|
+
},
|
|
22
|
+
// 05 §5.1 身份闸:导入文件自描述不认识即拒载
|
|
23
|
+
{
|
|
24
|
+
code: 'SESSION_IMPORT_BAD_FORMAT',
|
|
25
|
+
module: 'session',
|
|
26
|
+
description: '导入文件格式不认识(自描述格式版本/导出器标识缺或未知)',
|
|
27
|
+
},
|
|
28
|
+
// 05 §5.1 洪水闸:会话增生限流(首版 100/分钟)——码名已裁(2026-09-05 拍板题 15:沿用此名,改名收益不抵扫引成本)
|
|
29
|
+
{
|
|
30
|
+
code: 'SESSION_SPAWN_RATE_LIMIT',
|
|
31
|
+
module: 'session',
|
|
32
|
+
description: '会话增生限流触发(单进程单位时间导入/fork 新建会话数超帽)',
|
|
33
|
+
},
|
|
34
|
+
// 05 §5.1:persist:false(内存会话)上调 forkSession/导入——fork 语义需要物理新会话
|
|
35
|
+
{
|
|
36
|
+
code: 'SESSION_PERSISTENCE_REQUIRED',
|
|
37
|
+
module: 'session',
|
|
38
|
+
description: '该操作需要持久化会话(内存会话不支持 fork/导入——静默降级为拷贝 = 语义谎言)',
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/** 新建空 fold 状态(增量缓存的起点;全量 fold 内部同用) */
|
|
2
|
+
export function createFoldState() {
|
|
3
|
+
return { messages: [], chars: 0, openAssistant: null, pendingCalls: new Map() };
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 单事件步进(就地修改 state)——全量 fold 与增量缓存共用的唯一转换函数。
|
|
7
|
+
* 遮蔽指令事件(携带 surfaceOp 的 compaction/surface 等)不在此处理:全量路
|
|
8
|
+
* 由 deriveMessages 以 occludedSeqs 预滤;增量路由 applyOcclusion 回溯摘除
|
|
9
|
+
* (见下——两路对遮蔽的处置不同但摘除语义单源于 occludedSeqs 的区间定义)。
|
|
10
|
+
*/
|
|
11
|
+
export function stepFold(state, event) {
|
|
12
|
+
switch (event.type) {
|
|
13
|
+
case 'user/message': {
|
|
14
|
+
// 遇用户输入先冲刷打开中的 assistant(消息序保真:assistant 在 user 前)
|
|
15
|
+
flushAssistant(state);
|
|
16
|
+
const data = event.data;
|
|
17
|
+
pushMessage(state, {
|
|
18
|
+
type: 'user',
|
|
19
|
+
seq: event.seq,
|
|
20
|
+
content: data.content,
|
|
21
|
+
...(data.source !== undefined ? { source: data.source } : {}),
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
case 'assistant/message': {
|
|
26
|
+
// 同一 turn 内多条 assistant/message:先冲刷上一条(罕见,容错处理)
|
|
27
|
+
flushAssistant(state);
|
|
28
|
+
const data = event.data;
|
|
29
|
+
state.openAssistant = {
|
|
30
|
+
seq: event.seq,
|
|
31
|
+
content: data.content,
|
|
32
|
+
usage: data.usage,
|
|
33
|
+
stopReason: data.stopReason,
|
|
34
|
+
...(data.errorMessage !== undefined ? { errorMessage: data.errorMessage } : {}),
|
|
35
|
+
toolCalls: [],
|
|
36
|
+
};
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
case 'tool/call': {
|
|
40
|
+
const data = event.data;
|
|
41
|
+
// 工具调用归属同响应的 assistant 消息;若无打开缓冲(日志残缺),补一个空缓冲兜底
|
|
42
|
+
if (!state.openAssistant) {
|
|
43
|
+
state.openAssistant = { seq: event.seq, content: [], toolCalls: [] };
|
|
44
|
+
}
|
|
45
|
+
state.openAssistant.toolCalls.push({
|
|
46
|
+
type: 'toolCall',
|
|
47
|
+
toolCallId: data.toolCallId,
|
|
48
|
+
toolName: data.name,
|
|
49
|
+
arguments: data.arguments,
|
|
50
|
+
});
|
|
51
|
+
state.pendingCalls.set(data.toolCallId, { name: data.name, arguments: data.arguments });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
case 'tool/result': {
|
|
55
|
+
// 工具结果消息排在 assistant(含其全部 toolCall 块)之后
|
|
56
|
+
flushAssistant(state);
|
|
57
|
+
const data = event.data;
|
|
58
|
+
const call = state.pendingCalls.get(data.toolCallId);
|
|
59
|
+
state.pendingCalls.delete(data.toolCallId);
|
|
60
|
+
pushMessage(state, {
|
|
61
|
+
type: 'toolResult',
|
|
62
|
+
seq: event.seq,
|
|
63
|
+
toolCallId: data.toolCallId,
|
|
64
|
+
toolName: call?.name ?? '',
|
|
65
|
+
...(call ? { arguments: call.arguments } : {}),
|
|
66
|
+
output: data.content,
|
|
67
|
+
isError: data.error !== undefined,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
// turn 边界 / request/header / todo/write / log-only 全部不产出消息:
|
|
73
|
+
// todo/write 的模型可见性走「跨 turn 每轮注入当前全表」通道(04 篇),
|
|
74
|
+
// 此处落日志供 UI 投影与注入器读取。
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** 折出消息入 state(chars 同步累加——可加性的写入点) */
|
|
79
|
+
function pushMessage(state, message) {
|
|
80
|
+
state.messages.push(message);
|
|
81
|
+
state.chars += JSON.stringify(message).length;
|
|
82
|
+
}
|
|
83
|
+
/** 冲刷打开中的 assistant 缓冲为一条投影消息(无缓冲则无事发生) */
|
|
84
|
+
function flushAssistant(state) {
|
|
85
|
+
if (!state.openAssistant) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const buf = state.openAssistant;
|
|
89
|
+
pushMessage(state, {
|
|
90
|
+
type: 'assistant',
|
|
91
|
+
seq: buf.seq,
|
|
92
|
+
content: buf.content,
|
|
93
|
+
toolCalls: [...buf.toolCalls],
|
|
94
|
+
usage: buf.usage,
|
|
95
|
+
stopReason: buf.stopReason,
|
|
96
|
+
...(buf.errorMessage !== undefined ? { errorMessage: buf.errorMessage } : {}),
|
|
97
|
+
});
|
|
98
|
+
state.openAssistant = null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 投影快照发布:拷贝 state.messages + 把 pending assistant 缓冲折成消息
|
|
102
|
+
* **追加进拷贝**——活态零改动(缓冲继续接收迟到 tool/call,下次推进自然冲刷
|
|
103
|
+
* 进活数组)。返回独立新数组,调用方可安全持有。
|
|
104
|
+
*/
|
|
105
|
+
export function snapshotProjection(state) {
|
|
106
|
+
const snapshot = [...state.messages];
|
|
107
|
+
if (state.openAssistant) {
|
|
108
|
+
const buf = state.openAssistant;
|
|
109
|
+
snapshot.push({
|
|
110
|
+
type: 'assistant',
|
|
111
|
+
seq: buf.seq,
|
|
112
|
+
content: buf.content,
|
|
113
|
+
toolCalls: [...buf.toolCalls],
|
|
114
|
+
usage: buf.usage,
|
|
115
|
+
stopReason: buf.stopReason,
|
|
116
|
+
...(buf.errorMessage !== undefined ? { errorMessage: buf.errorMessage } : {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return snapshot;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 增量遮蔽摘除(05 §3.1「append 加、遮蔽减」的减法腿):surfaceOp 指令事件
|
|
123
|
+
* append 后对活体 FoldState 的回溯处置——摘除 messages 中 seq ∈ [start,end]
|
|
124
|
+
* 的消息并回退 chars;pendingCalls / openAssistant 中锚在区间内的一并清理
|
|
125
|
+
* (边缘纪律保证遮蔽只落已闭合 turn,活缓冲理应为空——防御性自足)。
|
|
126
|
+
* 摘除消息的 chars 回退用同式 JSON.stringify——与 pushMessage 同一把尺。
|
|
127
|
+
*/
|
|
128
|
+
export function applyOcclusion(state, op) {
|
|
129
|
+
const inRange = (seq) => seq >= op.start && seq <= op.end;
|
|
130
|
+
if (state.messages.length > 0) {
|
|
131
|
+
const kept = [];
|
|
132
|
+
for (const message of state.messages) {
|
|
133
|
+
if (inRange(message.seq)) {
|
|
134
|
+
state.chars -= JSON.stringify(message).length;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
kept.push(message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
state.messages = kept;
|
|
141
|
+
}
|
|
142
|
+
if (state.openAssistant && inRange(state.openAssistant.seq)) {
|
|
143
|
+
state.openAssistant = null;
|
|
144
|
+
}
|
|
145
|
+
// pendingCalls 的 id 与事件 seq 的对应关系在 fold 内部无账——区间摘除后配对表
|
|
146
|
+
// 可能残留区间内 call 的条目(对应 tool/result 已摘,不会再被消费);残留
|
|
147
|
+
// 条目不影响投影正确性(只是死键),会话重建(fork/恢复)时自然清零。
|
|
148
|
+
}
|
|
149
|
+
/** 计算被遮蔽的 seq 集合:遍历所有 surfaceOp 的 [start,end] 区间取并集 */
|
|
150
|
+
export function occludedSeqs(events) {
|
|
151
|
+
const occluded = new Set();
|
|
152
|
+
for (const event of events) {
|
|
153
|
+
if (event.surfaceOp) {
|
|
154
|
+
for (let seq = event.surfaceOp.start; seq <= event.surfaceOp.end; seq++) {
|
|
155
|
+
occluded.add(seq);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return occluded;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 模型历史投影(05 §3.1):日志减被遮蔽节点后 fold 出 ProjectedMessage 序列。
|
|
163
|
+
* 纯函数——同输入同输出;未配对的 tool/call(正常路径不存在——恢复协议保证
|
|
164
|
+
* 闭合)容错丢弃:其 toolCall 块保留在 assistant 消息内,但不产出悬空 toolResult。
|
|
165
|
+
*/
|
|
166
|
+
export function deriveMessages(events) {
|
|
167
|
+
const occluded = occludedSeqs(events);
|
|
168
|
+
const state = createFoldState();
|
|
169
|
+
for (const event of events) {
|
|
170
|
+
if (!occluded.has(event.seq)) {
|
|
171
|
+
stepFold(state, event);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
flushAssistant(state);
|
|
175
|
+
return state.messages;
|
|
176
|
+
}
|
|
177
|
+
/** 投影字符数(阈值兜底判据的查询面——FoldState.chars 的只读读出) */
|
|
178
|
+
export function projectedJsonChars(state) {
|
|
179
|
+
return state.chars;
|
|
180
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 普通前缀拷贝:[0, upToSeq] 原样拷贝(seq 不重编——结构共享,data 已冻结
|
|
3
|
+
* 天然免拷贝)。end-seed 的 append 与 flush 屏障由调用方(SessionLog/host)编排。
|
|
4
|
+
* @param events 源日志(冻结态)
|
|
5
|
+
* @param upToSeq 前缀上界(含);调用方负责边界合法性(缺省 lastClosedBoundary)
|
|
6
|
+
*/
|
|
7
|
+
export function forkPrefix(events, upToSeq) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (let i = 0; i <= upToSeq && i < events.length; i++) {
|
|
10
|
+
out.push(events[i]);
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 中段前缀种子(第三形态 slice,05 §5.0——goal 续跑/导出再导入的「中段取种子」):
|
|
16
|
+
* 取 [fromSeq, toSeq] 切片并从 0 重编 seq(日志内 seq 连续性是硬约束);遮蔽
|
|
17
|
+
* 载体随种子走——surfaceOp 区间与 sourceEventSeqs 同步平移 fromSeq。
|
|
18
|
+
*
|
|
19
|
+
* 遮蔽跨界的保守语义(本实现补全,05 篇未细写):遮蔽区间完全落在切片外
|
|
20
|
+
* ([0, fromSeq))的指令事件整体丢弃(被遮蔽节点不在新日志内,指令失去对象);
|
|
21
|
+
* 跨切点的区间截断到切点(部分遮蔽——遮蔽者不可越界遮到不存在的前史);
|
|
22
|
+
* sourceEventSeqs 中指向切片前的引用过滤(溯源悬空不如溯源精简)。
|
|
23
|
+
*/
|
|
24
|
+
export function slicePrefix(events, fromSeq, toSeq) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (let i = fromSeq; i <= toSeq && i < events.length; i++) {
|
|
27
|
+
const event = events[i];
|
|
28
|
+
const op = event.surfaceOp;
|
|
29
|
+
if (!op) {
|
|
30
|
+
out.push(rewriteSeq(event, i - fromSeq));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// 遮蔽区间在新坐标系的位置
|
|
34
|
+
const newStart = Math.max(op.start - fromSeq, 0);
|
|
35
|
+
const newEnd = op.end - fromSeq;
|
|
36
|
+
if (newEnd < 0) {
|
|
37
|
+
// 区间整体在切片前——指令失去对象,丢弃(不拷贝本事件)
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const seqs = event.sourceEventSeqs?.map((seq) => seq - fromSeq).filter((seq) => seq >= 0);
|
|
41
|
+
out.push({
|
|
42
|
+
...rewriteSeq(event, i - fromSeq),
|
|
43
|
+
surfaceOp: { op: 'replace', start: newStart, end: newEnd },
|
|
44
|
+
...(seqs !== undefined ? { sourceEventSeqs: seqs } : {}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** 重编 seq 的事件拷贝(其余字段引用共享——data 冻结态不可变) */
|
|
50
|
+
function rewriteSeq(event, newSeq) {
|
|
51
|
+
return { ...event, seq: newSeq };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* ensureSeeded(05 §5.1):导入/fork 后新会话日志必须是合法种子前缀。
|
|
55
|
+
* 校验位 = **前缀尾条**(尾界标记在场且 seq 对齐 seedLength——end-seed 位于
|
|
56
|
+
* 前缀末尾而非开头,「以 end-seed 开头」字面不成立);裸拷贝不走正门即断言失败。
|
|
57
|
+
* 切片形态(无 end-seed 尾条)以 seedLength 锚定校验(尾条 seq 恰 = seedLength - 1)。
|
|
58
|
+
* @returns 布尔判据(调用方决定 fail-loud 形态:导入闸抛 SESSION_IMPORT_BAD_FORMAT、
|
|
59
|
+
* fork 断言直接炸)
|
|
60
|
+
*/
|
|
61
|
+
export function isSeededPrefix(events, seedLength, requireEndSeed) {
|
|
62
|
+
if (events.length !== seedLength)
|
|
63
|
+
return false;
|
|
64
|
+
if (seedLength === 0)
|
|
65
|
+
return !requireEndSeed; // 空前缀:仅无尾界要求时合法(新建会话)
|
|
66
|
+
const tail = events[seedLength - 1];
|
|
67
|
+
if (!tail || tail.seq !== seedLength - 1)
|
|
68
|
+
return false;
|
|
69
|
+
if (requireEndSeed && tail.type !== 'session/end-seed')
|
|
70
|
+
return false;
|
|
71
|
+
return true;
|
|
72
|
+
}
|