cortico 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/package.json +43 -0
- package/src/boot.ts +46 -0
- package/src/bot.ts +1342 -0
- package/src/config-file.ts +41 -0
- package/src/core/README.md +92 -0
- package/src/core/billing.ts +26 -0
- package/src/core/blobs.ts +106 -0
- package/src/core/bus.ts +311 -0
- package/src/core/config-schema.ts +201 -0
- package/src/core/config.ts +262 -0
- package/src/core/core.ts +699 -0
- package/src/core/cost.ts +244 -0
- package/src/core/event-store.ts +304 -0
- package/src/core/fork.ts +113 -0
- package/src/core/generation.ts +118 -0
- package/src/core/instance-lock.ts +215 -0
- package/src/core/ipc-logger.ts +105 -0
- package/src/core/language.ts +62 -0
- package/src/core/log-context.ts +38 -0
- package/src/core/loop.ts +1858 -0
- package/src/core/markers.ts +48 -0
- package/src/core/prefix.ts +121 -0
- package/src/core/run.ts +95 -0
- package/src/core/secrets.ts +18 -0
- package/src/core/session.ts +56 -0
- package/src/core/sessions.ts +261 -0
- package/src/core/state.ts +104 -0
- package/src/core/template.ts +72 -0
- package/src/core/timers.ts +129 -0
- package/src/core/tool-log.ts +135 -0
- package/src/core/transcript.ts +92 -0
- package/src/core/truncate.ts +121 -0
- package/src/core/types.ts +1140 -0
- package/src/core/usage-log.ts +79 -0
- package/src/core/util.ts +386 -0
- package/src/deploy-listing.ts +160 -0
- package/src/deploy.ts +162 -0
- package/src/extensions/README.md +72 -0
- package/src/extensions/dry-mount.ts +390 -0
- package/src/extensions/manifest.ts +153 -0
- package/src/extensions/runtime.ts +42 -0
- package/src/extensions.ts +519 -0
- package/src/launcher.ts +285 -0
- package/src/paths.ts +155 -0
- package/src/protocol/open-responses/LICENSE +201 -0
- package/src/protocol/open-responses/README.md +14 -0
- package/src/protocol/open-responses/context-helpers.ts +27 -0
- package/src/protocol/open-responses/context-log.ts +59 -0
- package/src/protocol/open-responses/context.ts +89 -0
- package/src/protocol/open-responses/generated.ts +115 -0
- package/src/protocol/open-responses/index.ts +33 -0
- package/src/protocol/open-responses/openapi.json +4230 -0
- package/src/protocol/open-responses/stream.ts +146 -0
- package/src/protocol/open-responses/tokens.ts +31 -0
- package/src/providers/README.md +86 -0
- package/src/providers/base.ts +111 -0
- package/src/providers/configuration.ts +97 -0
- package/src/providers/console/config.ts +21 -0
- package/src/providers/console/settings.ts +443 -0
- package/src/providers/console/strings.ts +74 -0
- package/src/providers/console/types.ts +11 -0
- package/src/providers/llamacpp/archive.ts +148 -0
- package/src/providers/llamacpp/catalog.ts +247 -0
- package/src/providers/llamacpp/config.ts +36 -0
- package/src/providers/llamacpp/console/client.ts +6 -0
- package/src/providers/llamacpp/console/models-panel.ts +120 -0
- package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
- package/src/providers/llamacpp/console/server.ts +153 -0
- package/src/providers/llamacpp/index.ts +95 -0
- package/src/providers/llamacpp/native.ts +51 -0
- package/src/providers/llamacpp/options.ts +117 -0
- package/src/providers/llamacpp/runtime-store.ts +139 -0
- package/src/providers/llamacpp/runtime.ts +214 -0
- package/src/providers/llamacpp/server.ts +286 -0
- package/src/providers/llamacpp/strings.ts +230 -0
- package/src/providers/openai-responses-compat/config.ts +20 -0
- package/src/providers/openai-responses-compat/index.ts +89 -0
- package/src/providers/openai-responses-compat/native.ts +127 -0
- package/src/providers/openai-responses-compat/strings.ts +19 -0
- package/src/providers/pricebook.ts +75 -0
- package/src/providers/registry.ts +137 -0
- package/src/providers/strings.ts +71 -0
- package/src/providers/transport/chat.ts +156 -0
- package/src/providers/transport/errors.ts +48 -0
- package/src/providers/transport/history.ts +68 -0
- package/src/providers/transport/native-input.ts +83 -0
- package/src/providers/transport/native-types.ts +13 -0
- package/src/providers/transport/response-assembly.ts +191 -0
- package/src/providers/transport/response-http.ts +196 -0
- package/src/providers/transport/response-meters.ts +27 -0
- package/src/providers/transport/responses-input.ts +56 -0
- package/src/web/README.md +84 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
- package/src/web/client/console-pages/builtins.ts +10 -0
- package/src/web/client/console-pages/context.ts +135 -0
- package/src/web/client/console-pages/host.ts +633 -0
- package/src/web/client/console-pages/loader.ts +132 -0
- package/src/web/client/console-pages/strings.ts +90 -0
- package/src/web/client/console-pages/tools/strings.ts +49 -0
- package/src/web/client/console-pages/tools/view.ts +215 -0
- package/src/web/client/core/api.ts +200 -0
- package/src/web/client/core/language.ts +56 -0
- package/src/web/client/core/lifecycle.ts +132 -0
- package/src/web/client/core/router.ts +199 -0
- package/src/web/client/core/stream.ts +157 -0
- package/src/web/client/core/websocket.ts +82 -0
- package/src/web/client/features/appearance/index.ts +333 -0
- package/src/web/client/features/appearance/strings.ts +87 -0
- package/src/web/client/features/config/index.ts +11 -0
- package/src/web/client/features/config/strings.ts +41 -0
- package/src/web/client/features/config/view.ts +428 -0
- package/src/web/client/features/core/events.ts +197 -0
- package/src/web/client/features/core/index.ts +254 -0
- package/src/web/client/features/core/run.ts +56 -0
- package/src/web/client/features/core/runlog.ts +214 -0
- package/src/web/client/features/core/sessions.ts +53 -0
- package/src/web/client/features/core/strings.ts +157 -0
- package/src/web/client/features/extensions/index.ts +422 -0
- package/src/web/client/features/extensions/strings.ts +164 -0
- package/src/web/client/features/feature.ts +93 -0
- package/src/web/client/features/live/context.ts +280 -0
- package/src/web/client/features/live/fork.ts +156 -0
- package/src/web/client/features/live/index.ts +392 -0
- package/src/web/client/features/live/onboarding.ts +113 -0
- package/src/web/client/features/live/protocol.ts +122 -0
- package/src/web/client/features/live/sessions.ts +71 -0
- package/src/web/client/features/live/status.ts +68 -0
- package/src/web/client/features/live/strings.ts +252 -0
- package/src/web/client/features/live/timeline.ts +590 -0
- package/src/web/client/features/prompts/editor.ts +292 -0
- package/src/web/client/features/prompts/index.ts +346 -0
- package/src/web/client/features/prompts/strings.ts +136 -0
- package/src/web/client/features/prompts/view.ts +256 -0
- package/src/web/client/features/providers/index.ts +100 -0
- package/src/web/client/features/providers/strings.ts +23 -0
- package/src/web/client/features/settings/general.ts +28 -0
- package/src/web/client/features/settings/index.ts +82 -0
- package/src/web/client/features/settings/strings.ts +33 -0
- package/src/web/client/features/storage/index.ts +1 -0
- package/src/web/client/features/storage/strings.ts +45 -0
- package/src/web/client/features/storage/view.ts +154 -0
- package/src/web/client/features/usage/chart.ts +486 -0
- package/src/web/client/features/usage/index.ts +407 -0
- package/src/web/client/features/usage/labels.ts +46 -0
- package/src/web/client/features/usage/range.ts +36 -0
- package/src/web/client/features/usage/state.ts +37 -0
- package/src/web/client/features/usage/strings.ts +191 -0
- package/src/web/client/features/usage/tooltip.ts +86 -0
- package/src/web/client/features/usage/types.ts +96 -0
- package/src/web/client/features/worlds/index.ts +424 -0
- package/src/web/client/features/worlds/strings.ts +121 -0
- package/src/web/client/main.ts +273 -0
- package/src/web/client/shell/avatar.ts +233 -0
- package/src/web/client/shell/index.ts +549 -0
- package/src/web/client/shell/strings.ts +89 -0
- package/src/web/client/theme/handoff.ts +43 -0
- package/src/web/client/theme/palette.ts +109 -0
- package/src/web/client/theme/registry.ts +130 -0
- package/src/web/client/theme/storage.ts +72 -0
- package/src/web/client/theme/strings.ts +123 -0
- package/src/web/client/theme/studio.ts +366 -0
- package/src/web/client/ui/actions.ts +109 -0
- package/src/web/client/ui/data.ts +185 -0
- package/src/web/client/ui/dom.ts +23 -0
- package/src/web/client/ui/fields.ts +191 -0
- package/src/web/client/ui/format.ts +75 -0
- package/src/web/client/ui/icons.ts +183 -0
- package/src/web/client/ui/images.ts +91 -0
- package/src/web/client/ui/index.ts +89 -0
- package/src/web/client/ui/lamp.ts +125 -0
- package/src/web/client/ui/log.ts +98 -0
- package/src/web/client/ui/overlay.ts +184 -0
- package/src/web/client/ui/page.ts +8 -0
- package/src/web/client/ui/prompt-input.tsx +311 -0
- package/src/web/client/ui/sheet.ts +131 -0
- package/src/web/client/ui/strings.ts +71 -0
- package/src/web/console-pages.ts +367 -0
- package/src/web/files.ts +107 -0
- package/src/web/path-picker.ts +296 -0
- package/src/web/public/index.html +17 -0
- package/src/web/public/styles.css +1715 -0
- package/src/web/server.ts +1883 -0
- package/src/web/shared/client-panel.ts +603 -0
- package/src/web/shared/console-protocol.ts +560 -0
- package/src/web/shared/css.d.ts +1 -0
- package/src/web/shared/path-picker.ts +23 -0
- package/src/web/shared/theme.ts +256 -0
- package/src/web/theme-store.ts +37 -0
- package/src/world.ts +449 -0
- package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
- package/src/worlds/bilibili/README.md +176 -0
- package/src/worlds/bilibili/audience-admission.ts +995 -0
- package/src/worlds/bilibili/client.ts +528 -0
- package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
- package/src/worlds/bilibili/config.ts +185 -0
- package/src/worlds/bilibili/console/client.ts +202 -0
- package/src/worlds/bilibili/definition.ts +35 -0
- package/src/worlds/bilibili/gift-frame.ts +131 -0
- package/src/worlds/bilibili/normalize.ts +452 -0
- package/src/worlds/bilibili/overlay/announcement.ts +76 -0
- package/src/worlds/bilibili/overlay/assets.ts +82 -0
- package/src/worlds/bilibili/overlay/model.ts +494 -0
- package/src/worlds/bilibili/overlay/project.ts +202 -0
- package/src/worlds/bilibili/overlay/server.ts +286 -0
- package/src/worlds/bilibili/overlay/types.ts +217 -0
- package/src/worlds/bilibili/overlay/web/app.js +690 -0
- package/src/worlds/bilibili/overlay/web/editor.css +297 -0
- package/src/worlds/bilibili/overlay/web/editor.html +102 -0
- package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
- package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
- package/src/worlds/bilibili/overlay/web/styles.css +258 -0
- package/src/worlds/bilibili/protobuf.ts +123 -0
- package/src/worlds/bilibili/wire.ts +94 -0
- package/src/worlds/bilibili/world.ts +1389 -0
- package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
- package/src/worlds/console-fixture/console/client.ts +72 -0
- package/src/worlds/console-fixture/world.ts +73 -0
- package/src/worlds/index.ts +15 -0
- package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
- package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
- package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
- package/src/worlds/minecraft/README.md +917 -0
- package/src/worlds/minecraft/blueprint-plan.ts +915 -0
- package/src/worlds/minecraft/blueprint-registry.ts +566 -0
- package/src/worlds/minecraft/blueprint-repair.ts +236 -0
- package/src/worlds/minecraft/blueprint-resource.ts +272 -0
- package/src/worlds/minecraft/blueprint.ts +492 -0
- package/src/worlds/minecraft/body-lease.ts +340 -0
- package/src/worlds/minecraft/bridge.ts +749 -0
- package/src/worlds/minecraft/check.ts +651 -0
- package/src/worlds/minecraft/chests.ts +292 -0
- package/src/worlds/minecraft/client-launch.ts +272 -0
- package/src/worlds/minecraft/client-options.ts +85 -0
- package/src/worlds/minecraft/client-skins.ts +164 -0
- package/src/worlds/minecraft/client-window.ps1 +91 -0
- package/src/worlds/minecraft/client.ts +423 -0
- package/src/worlds/minecraft/combat-context.ts +109 -0
- package/src/worlds/minecraft/combat.ts +1552 -0
- package/src/worlds/minecraft/config.ts +499 -0
- package/src/worlds/minecraft/console/access.ts +218 -0
- package/src/worlds/minecraft/console/client.ts +244 -0
- package/src/worlds/minecraft/console/log.ts +110 -0
- package/src/worlds/minecraft/console/mount.ts +420 -0
- package/src/worlds/minecraft/console/skin.ts +257 -0
- package/src/worlds/minecraft/console/style.css +154 -0
- package/src/worlds/minecraft/console/world.ts +449 -0
- package/src/worlds/minecraft/deaths.ts +135 -0
- package/src/worlds/minecraft/definition.ts +18 -0
- package/src/worlds/minecraft/engine-child.ts +274 -0
- package/src/worlds/minecraft/engine-ipc.ts +98 -0
- package/src/worlds/minecraft/entity-facts.ts +132 -0
- package/src/worlds/minecraft/escape.ts +407 -0
- package/src/worlds/minecraft/executor.ts +14494 -0
- package/src/worlds/minecraft/explored.ts +156 -0
- package/src/worlds/minecraft/geometry.ts +205 -0
- package/src/worlds/minecraft/goal-plan.ts +647 -0
- package/src/worlds/minecraft/item-break.ts +126 -0
- package/src/worlds/minecraft/item-facts.ts +146 -0
- package/src/worlds/minecraft/item-pick.ts +67 -0
- package/src/worlds/minecraft/level-dat.ts +176 -0
- package/src/worlds/minecraft/log.ts +132 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
- package/src/worlds/minecraft/names.ts +377 -0
- package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
- package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
- package/src/worlds/minecraft/piglin.ts +49 -0
- package/src/worlds/minecraft/policy.ts +547 -0
- package/src/worlds/minecraft/precheck.ts +655 -0
- package/src/worlds/minecraft/proxy.ts +445 -0
- package/src/worlds/minecraft/ranged.ts +635 -0
- package/src/worlds/minecraft/readouts.ts +102 -0
- package/src/worlds/minecraft/round.ts +26 -0
- package/src/worlds/minecraft/search-observation.ts +92 -0
- package/src/worlds/minecraft/server-config.ts +467 -0
- package/src/worlds/minecraft/server.ts +658 -0
- package/src/worlds/minecraft/show.ts +84 -0
- package/src/worlds/minecraft/skills.ts +1763 -0
- package/src/worlds/minecraft/terrain.ts +1735 -0
- package/src/worlds/minecraft/window.ts +96 -0
- package/src/worlds/minecraft/works.ts +179 -0
- package/src/worlds/minecraft/world.ts +6438 -0
- package/src/worlds/qq/ENV_PROMPT.md +5 -0
- package/src/worlds/qq/config.ts +108 -0
- package/src/worlds/qq/console/client.ts +126 -0
- package/src/worlds/qq/console/events.ts +155 -0
- package/src/worlds/qq/console/gate.ts +232 -0
- package/src/worlds/qq/console/roster.ts +203 -0
- package/src/worlds/qq/console/style.css +65 -0
- package/src/worlds/qq/conversation.ts +28 -0
- package/src/worlds/qq/definition.ts +62 -0
- package/src/worlds/qq/driver.ts +375 -0
- package/src/worlds/qq/history-tools.ts +209 -0
- package/src/worlds/qq/normalize.ts +323 -0
- package/src/worlds/qq/qface-map.ts +282 -0
- package/src/worlds/qq/vision-prompt.ts +27 -0
- package/src/worlds/qq/vision.ts +642 -0
- package/src/worlds/qq/vlm.ts +142 -0
- package/src/worlds/qq/world.ts +1503 -0
- package/src/worlds/terminal/ENV_PROMPT.md +11 -0
- package/src/worlds/terminal/config.ts +9 -0
- package/src/worlds/terminal/definition.ts +14 -0
- package/src/worlds/terminal/world.ts +700 -0
- package/src/worlds/websearch/ENV_PROMPT.md +1 -0
- package/src/worlds/websearch/brave-client.ts +261 -0
- package/src/worlds/websearch/config.ts +75 -0
- package/src/worlds/websearch/definition.ts +20 -0
- package/src/worlds/websearch/world.ts +171 -0
package/src/core/cost.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/** Aggregate observed consumption and immutable Provider charges. */
|
|
2
|
+
import type { UsageRecord } from './types.ts';
|
|
3
|
+
import { billingBalances, recordCharges, type BillingBalance } from './billing.ts';
|
|
4
|
+
|
|
5
|
+
/** 已解析的具体时间粒度(桶宽) */
|
|
6
|
+
export type UsageBucketUnit = 'minute' | 'hour' | 'day' | 'week' | 'month';
|
|
7
|
+
/** 请求粒度:具体粒度或 'auto'(按范围跨度自适应) */
|
|
8
|
+
export type UsageBucketOption = UsageBucketUnit | 'auto';
|
|
9
|
+
export type UsageGroupBy = 'role' | 'model' | 'none';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 用量与费用合计,供时间序列及角色、模型分组使用。
|
|
13
|
+
* cost = costCacheHit + costCacheMiss + costOutput + costOther。
|
|
14
|
+
* reasoningTokens 是 completionTokens 的子集;费用采用记录内的 charges。
|
|
15
|
+
*/
|
|
16
|
+
export interface UsageAccum {
|
|
17
|
+
calls: number;
|
|
18
|
+
promptTokens: number;
|
|
19
|
+
completionTokens: number;
|
|
20
|
+
cacheHitTokens: number;
|
|
21
|
+
cacheMissTokens: number;
|
|
22
|
+
reasoningTokens: number;
|
|
23
|
+
cost: number;
|
|
24
|
+
costCacheHit: number;
|
|
25
|
+
costCacheMiss: number;
|
|
26
|
+
costOutput: number;
|
|
27
|
+
costOther: number;
|
|
28
|
+
unclassifiedInputTokens: number;
|
|
29
|
+
pricedCalls: number;
|
|
30
|
+
unpricedCalls: number;
|
|
31
|
+
unknownUsageCalls: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UsageSeriesPoint extends UsageAccum {
|
|
35
|
+
/** 本地时区前缀,如 2026-07-19T14:30、2026-07-19 或 2026-07。 */
|
|
36
|
+
bucket: string;
|
|
37
|
+
/** 本桶内按角色的子拆分(键=角色);用于堆叠"按角色"视图 */
|
|
38
|
+
byRole: Record<string, UsageAccum>;
|
|
39
|
+
/** 本桶内按模型的子拆分(键=模型);用于堆叠"按模型"视图 */
|
|
40
|
+
byModel: Record<string, UsageAccum>;
|
|
41
|
+
/** 按角色与模型交叉分组,键为 byRoleModel[role][model]。 */
|
|
42
|
+
byRoleModel: Record<string, Record<string, UsageAccum>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 按 role / model 分组的合计 */
|
|
46
|
+
export interface UsageGroupStat extends UsageAccum {
|
|
47
|
+
key: string;
|
|
48
|
+
/** 声明方提供的 UsageRecord.label;与 key 相同时省略。缺失时显示 key,不推断角色名称。 */
|
|
49
|
+
label?: string;
|
|
50
|
+
cacheHitRate: number | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface UsageAggregate {
|
|
54
|
+
currency: string;
|
|
55
|
+
basis: 'marginal' | 'equivalent';
|
|
56
|
+
balances: BillingBalance[];
|
|
57
|
+
successful: UsageAccum;
|
|
58
|
+
byInstance: UsageGroupStat[];
|
|
59
|
+
/** 实际使用的粒度(auto 会被解析成具体值) */
|
|
60
|
+
bucket: UsageBucketUnit;
|
|
61
|
+
from: string | null;
|
|
62
|
+
to: string | null;
|
|
63
|
+
/** 时间序列(按 bucket 升序) */
|
|
64
|
+
series: UsageSeriesPoint[];
|
|
65
|
+
/** 全量合计 */
|
|
66
|
+
totals: UsageGroupStat;
|
|
67
|
+
/** 按角色分组(cost 降序) */
|
|
68
|
+
byRole: UsageGroupStat[];
|
|
69
|
+
/** 按模型分组(cost 降序) */
|
|
70
|
+
byModel: UsageGroupStat[];
|
|
71
|
+
/** Failed, aborted and discarded attempts are also included in totals. */
|
|
72
|
+
failed: UsageAccum;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function zero(): UsageAccum {
|
|
76
|
+
return {
|
|
77
|
+
calls: 0, promptTokens: 0, completionTokens: 0,
|
|
78
|
+
cacheHitTokens: 0, cacheMissTokens: 0, reasoningTokens: 0,
|
|
79
|
+
cost: 0, costCacheHit: 0, costCacheMiss: 0, costOutput: 0, costOther: 0, unclassifiedInputTokens: 0,
|
|
80
|
+
pricedCalls: 0, unpricedCalls: 0, unknownUsageCalls: 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type MeasuredCost = { cacheHit: number; cacheMiss: number; output: number; total: number; priced: boolean };
|
|
85
|
+
|
|
86
|
+
function add(a: UsageAccum, r: UsageRecord, c: MeasuredCost): void {
|
|
87
|
+
a.calls += 1;
|
|
88
|
+
if (c.priced) a.pricedCalls++; else a.unpricedCalls++;
|
|
89
|
+
if (r.attempt && (r.attempt.meters.input === null || r.attempt.meters.output === null || r.attempt.meters.cachedInput === null)) a.unknownUsageCalls++;
|
|
90
|
+
a.promptTokens += r.promptTokens;
|
|
91
|
+
a.completionTokens += r.completionTokens;
|
|
92
|
+
a.cacheHitTokens += r.cacheHitTokens;
|
|
93
|
+
a.cacheMissTokens += r.cacheMissTokens;
|
|
94
|
+
a.reasoningTokens += r.reasoningTokens;
|
|
95
|
+
a.cost += c.total;
|
|
96
|
+
a.costCacheHit += c.cacheHit;
|
|
97
|
+
a.costCacheMiss += c.cacheMiss;
|
|
98
|
+
a.costOutput += c.output;
|
|
99
|
+
a.costOther += c.total - c.cacheHit - c.cacheMiss - c.output;
|
|
100
|
+
a.unclassifiedInputTokens += Math.max(0, r.promptTokens - r.cacheHitTokens - r.cacheMissTokens);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function upsert(m: Map<string, UsageAccum>, key: string, r: UsageRecord, c: MeasuredCost): void {
|
|
104
|
+
let a = m.get(key);
|
|
105
|
+
if (!a) { a = zero(); m.set(key, a); }
|
|
106
|
+
add(a, r, c);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mapToObj(m: Map<string, UsageAccum>): Record<string, UsageAccum> {
|
|
110
|
+
const o: Record<string, UsageAccum> = {};
|
|
111
|
+
for (const [k, v] of m) o[k] = v;
|
|
112
|
+
return o;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function nestedMapToObj(m: Map<string, Map<string, UsageAccum>>): Record<string, Record<string, UsageAccum>> {
|
|
116
|
+
const o: Record<string, Record<string, UsageAccum>> = {};
|
|
117
|
+
for (const [k, inner] of m) o[k] = mapToObj(inner);
|
|
118
|
+
return o;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function statOf(key: string, a: UsageAccum, label?: string): UsageGroupStat {
|
|
122
|
+
const out: UsageGroupStat = { key, ...a, cacheHitRate: cacheHitRate(a) };
|
|
123
|
+
if (label && label !== key) out.label = label;
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 一天 = 该日期(YYYY-MM-DD)所在 ISO 周的周一(用 UTC 计算,避免时区漂移) */
|
|
128
|
+
function weekStart(day: string): string {
|
|
129
|
+
const [y, m, d] = day.split('-').map(Number);
|
|
130
|
+
const dt = new Date(Date.UTC(y || 1970, (m || 1) - 1, d || 1));
|
|
131
|
+
const dow = (dt.getUTCDay() + 6) % 7; // 周一=0 … 周日=6
|
|
132
|
+
dt.setUTCDate(dt.getUTCDate() - dow);
|
|
133
|
+
return dt.toISOString().slice(0, 10);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 取某条记录在给定粒度下的桶键(本地时区 ISO 前缀直接切,周单独算) */
|
|
137
|
+
function bucketKeyOf(ts: string, unit: UsageBucketUnit): string {
|
|
138
|
+
switch (unit) {
|
|
139
|
+
case 'minute': return ts.slice(0, 16); // YYYY-MM-DDTHH:MM
|
|
140
|
+
case 'hour': return ts.slice(0, 13); // YYYY-MM-DDTHH
|
|
141
|
+
case 'day': return ts.slice(0, 10); // YYYY-MM-DD
|
|
142
|
+
case 'week': return weekStart(ts.slice(0, 10));
|
|
143
|
+
case 'month': return ts.slice(0, 7); // YYYY-MM
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** from/to(YYYY-MM-DD,含端)之间的整天跨度(含首尾);任一为空→NaN */
|
|
148
|
+
function daySpan(from?: string | null, to?: string | null): number {
|
|
149
|
+
if (!from || !to) return NaN;
|
|
150
|
+
const a = Date.parse(from + 'T00:00:00Z');
|
|
151
|
+
const b = Date.parse(to + 'T00:00:00Z');
|
|
152
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return NaN;
|
|
153
|
+
return Math.abs(b - a) / 86400000 + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 自动粒度:跨度 ≤2 天用小时,≤62 天用天,≤366 天用周,其余用月。
|
|
158
|
+
* 缺少 from/to 时用天;分钟需显式选择。
|
|
159
|
+
*/
|
|
160
|
+
export function resolveBucket(from?: string | null, to?: string | null): UsageBucketUnit {
|
|
161
|
+
const span = daySpan(from, to);
|
|
162
|
+
if (Number.isNaN(span)) return 'day';
|
|
163
|
+
if (span <= 2) return 'hour';
|
|
164
|
+
if (span <= 62) return 'day';
|
|
165
|
+
if (span <= 366) return 'week';
|
|
166
|
+
return 'month';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 按包含端点的日期范围筛选,按时间、角色和模型聚合用量。
|
|
171
|
+
* 费用从记录内的 charges 按币种和计价基础选择,再按 meter 归入四类成本。
|
|
172
|
+
*/
|
|
173
|
+
export function aggregateUsage(
|
|
174
|
+
records: UsageRecord[],
|
|
175
|
+
opts: { from?: string | null; to?: string | null; bucket: UsageBucketOption; currency?: string; basis?: 'marginal' | 'equivalent' },
|
|
176
|
+
): UsageAggregate {
|
|
177
|
+
const { from = null, to = null } = opts;
|
|
178
|
+
const unit: UsageBucketUnit = opts.bucket === 'auto' ? resolveBucket(from, to) : opts.bucket;
|
|
179
|
+
const series = new Map<string, { total: UsageAccum; roles: Map<string, UsageAccum>; models: Map<string, UsageAccum>; roleModels: Map<string, Map<string, UsageAccum>> }>();
|
|
180
|
+
const roles = new Map<string, UsageAccum>();
|
|
181
|
+
/** 同一 role 的 label 使用最后一条记录提供的值。 */
|
|
182
|
+
const roleLabels = new Map<string, string>();
|
|
183
|
+
const models = new Map<string, UsageAccum>();
|
|
184
|
+
const totals = zero();
|
|
185
|
+
const failed = zero();
|
|
186
|
+
const successful = zero();
|
|
187
|
+
const instances = new Map<string, UsageAccum>();
|
|
188
|
+
const selected = records.filter(r => r && typeof r.ts === 'string' && (!from || r.ts.slice(0,10) >= from) && (!to || r.ts.slice(0,10) <= to));
|
|
189
|
+
const balances = billingBalances(selected);
|
|
190
|
+
const basis = opts.basis ?? 'marginal';
|
|
191
|
+
const currency = opts.currency ?? balances.find(balance => balance.basis === basis)?.currency ?? 'USD';
|
|
192
|
+
|
|
193
|
+
for (const r of selected) {
|
|
194
|
+
const charge = recordCharges(r).find(charge => charge.quote.currency === currency && charge.quote.basis === basis);
|
|
195
|
+
const cost: MeasuredCost = { cacheHit: 0, cacheMiss: 0, output: 0, total: charge?.knownAmount ?? 0, priced: charge?.amount != null };
|
|
196
|
+
for (const line of charge?.lines ?? []) {
|
|
197
|
+
if (line.meter === 'cachedInput') cost.cacheHit += line.amount ?? 0;
|
|
198
|
+
if (line.meter === 'uncachedInput' || line.meter === 'input') cost.cacheMiss += line.amount ?? 0;
|
|
199
|
+
if (line.meter === 'output') cost.output += line.amount ?? 0;
|
|
200
|
+
}
|
|
201
|
+
const outcome = r.attempt?.outcome;
|
|
202
|
+
if (r.outcome !== undefined || (outcome !== undefined && outcome !== 'completed' && outcome !== 'incomplete')) add(failed, r, cost);
|
|
203
|
+
else if (r.attempt?.purpose !== 'diagnostic') add(successful, r, cost);
|
|
204
|
+
upsert(instances, r.attempt?.origin.instance ?? 'unknown', r, cost);
|
|
205
|
+
const role = r.role ?? 'unknown';
|
|
206
|
+
const model = r.model || 'unknown';
|
|
207
|
+
const bk = bucketKeyOf(r.ts, unit);
|
|
208
|
+
let bucketAcc = series.get(bk);
|
|
209
|
+
if (!bucketAcc) { bucketAcc = { total: zero(), roles: new Map(), models: new Map(), roleModels: new Map() }; series.set(bk, bucketAcc); }
|
|
210
|
+
add(bucketAcc.total, r, cost);
|
|
211
|
+
upsert(bucketAcc.roles, role, r, cost);
|
|
212
|
+
upsert(bucketAcc.models, model, r, cost);
|
|
213
|
+
let rm = bucketAcc.roleModels.get(role);
|
|
214
|
+
if (!rm) { rm = new Map(); bucketAcc.roleModels.set(role, rm); }
|
|
215
|
+
upsert(rm, model, r, cost);
|
|
216
|
+
upsert(roles, role, r, cost);
|
|
217
|
+
if (r.label) roleLabels.set(role, r.label);
|
|
218
|
+
upsert(models, model, r, cost);
|
|
219
|
+
add(totals, r, cost);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const seriesOut: UsageSeriesPoint[] = [...series.entries()]
|
|
223
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
224
|
+
.map(([b, acc]) => ({ bucket: b, ...acc.total, byRole: mapToObj(acc.roles), byModel: mapToObj(acc.models), byRoleModel: nestedMapToObj(acc.roleModels) }));
|
|
225
|
+
const groupOut = (m: Map<string, UsageAccum>, labels?: Map<string, string>): UsageGroupStat[] =>
|
|
226
|
+
[...m.entries()].map(([k, a]) => statOf(k, a, labels?.get(k))).sort((x, y) => y.cost - x.cost);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
currency, basis, balances, successful, byInstance: groupOut(instances),
|
|
230
|
+
bucket: unit,
|
|
231
|
+
from,
|
|
232
|
+
to,
|
|
233
|
+
series: seriesOut,
|
|
234
|
+
totals: statOf('total', totals),
|
|
235
|
+
byRole: groupOut(roles, roleLabels),
|
|
236
|
+
byModel: groupOut(models),
|
|
237
|
+
failed,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function cacheHitRate(u: { cacheHitTokens: number; cacheMissTokens: number }): number | null {
|
|
242
|
+
const total = u.cacheHitTokens + u.cacheMissTokens;
|
|
243
|
+
return total > 0 ? u.cacheHitTokens / total : null;
|
|
244
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 事件库:按 run 分片的 JSONL,`data/runs/<run>/events.jsonl` 一行一个 EventEnvelope。
|
|
3
|
+
* cursor 跨 run 全局单调:开机时从已有分片的末行续号。当前 run 常驻内存,更早的
|
|
4
|
+
* 分片按 cursor / ts 区间按需装载;range / around / grep 跨分片作答。
|
|
5
|
+
*/
|
|
6
|
+
import { appendFileSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import type {
|
|
9
|
+
EventEnvelope,
|
|
10
|
+
EventGrepHit,
|
|
11
|
+
EventGrepQuery,
|
|
12
|
+
EventRangeQuery,
|
|
13
|
+
EventStore,
|
|
14
|
+
Logger,
|
|
15
|
+
} from './types.ts';
|
|
16
|
+
import { listRuns, runsDirOf } from './run.ts';
|
|
17
|
+
import { nullLogger } from './util.ts';
|
|
18
|
+
|
|
19
|
+
export interface JsonlEventStoreOptions {
|
|
20
|
+
dataDir: string;
|
|
21
|
+
/** 当前 run id;新事件写进它的分片 */
|
|
22
|
+
run: string;
|
|
23
|
+
log?: Logger;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Segment {
|
|
27
|
+
run: string;
|
|
28
|
+
file: string;
|
|
29
|
+
first: number;
|
|
30
|
+
last: number;
|
|
31
|
+
firstTs: string;
|
|
32
|
+
lastTs: string;
|
|
33
|
+
/** 装载后的记录,按 cursor 升序 */
|
|
34
|
+
events: EventEnvelope[] | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const EDGE_BYTES = 64 * 1024;
|
|
38
|
+
const EVENTS_FILE = 'events.jsonl';
|
|
39
|
+
|
|
40
|
+
function parseLine(line: string): EventEnvelope | null {
|
|
41
|
+
try {
|
|
42
|
+
const e = JSON.parse(line) as EventEnvelope;
|
|
43
|
+
return Number.isInteger(e.cursor) && typeof e.ts === 'string' ? e : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 不整读文件:头尾各取一段,拿首末两条的 cursor 与 ts。 */
|
|
50
|
+
function readEdges(file: string): { first: EventEnvelope; last: EventEnvelope } | null {
|
|
51
|
+
const fd = openSync(file, 'r');
|
|
52
|
+
try {
|
|
53
|
+
const size = fstatSync(fd).size;
|
|
54
|
+
if (size === 0) return null;
|
|
55
|
+
const headLen = Math.min(size, EDGE_BYTES);
|
|
56
|
+
const head = Buffer.alloc(headLen);
|
|
57
|
+
readSync(fd, head, 0, headLen, 0);
|
|
58
|
+
const headLines = head.toString('utf8').split('\n');
|
|
59
|
+
const first = headLines.map((l) => l.trim()).filter(Boolean).map(parseLine).find((e) => e !== null);
|
|
60
|
+
if (!first) return null;
|
|
61
|
+
const tailStart = Math.max(0, size - EDGE_BYTES);
|
|
62
|
+
const tail = Buffer.alloc(size - tailStart);
|
|
63
|
+
readSync(fd, tail, 0, size - tailStart, tailStart);
|
|
64
|
+
const tailLines = tail.toString('utf8').split('\n');
|
|
65
|
+
if (tailStart > 0) tailLines.shift();
|
|
66
|
+
const last = tailLines.map((l) => l.trim()).filter(Boolean).map(parseLine).reverse().find((e) => e !== null);
|
|
67
|
+
return { first, last: last ?? first };
|
|
68
|
+
} finally {
|
|
69
|
+
closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function lowerBound(events: EventEnvelope[], cursor: number): number {
|
|
74
|
+
let lo = 0;
|
|
75
|
+
let hi = events.length;
|
|
76
|
+
while (lo < hi) {
|
|
77
|
+
const mid = (lo + hi) >> 1;
|
|
78
|
+
if (events[mid].cursor < cursor) lo = mid + 1;
|
|
79
|
+
else hi = mid;
|
|
80
|
+
}
|
|
81
|
+
return lo;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class JsonlEventStore implements EventStore {
|
|
85
|
+
private readonly log: Logger;
|
|
86
|
+
private readonly run: string;
|
|
87
|
+
private readonly segments: Segment[] = [];
|
|
88
|
+
private readonly current: Segment;
|
|
89
|
+
/** 下一条事件拿到的 cursor */
|
|
90
|
+
private next = 1;
|
|
91
|
+
/**
|
|
92
|
+
* 本实例记录的分片字节数;写入前的大小变化可能来自其他写入方。
|
|
93
|
+
* 用 stat 比较大小,避免每次追加都读取全部文件。
|
|
94
|
+
*/
|
|
95
|
+
private expectedSize = 0;
|
|
96
|
+
/** 文件大小不符的错误只报告一次。 */
|
|
97
|
+
private concurrencyReported = false;
|
|
98
|
+
private appendListeners: Array<(e: EventEnvelope) => void> = [];
|
|
99
|
+
|
|
100
|
+
constructor(opts: JsonlEventStoreOptions) {
|
|
101
|
+
this.log = opts.log ?? nullLogger();
|
|
102
|
+
this.run = opts.run;
|
|
103
|
+
const runsDir = runsDirOf(opts.dataDir);
|
|
104
|
+
for (const id of listRuns(opts.dataDir)) {
|
|
105
|
+
if (id === this.run) continue;
|
|
106
|
+
const file = join(runsDir, id, EVENTS_FILE);
|
|
107
|
+
if (!existsSync(file)) continue;
|
|
108
|
+
let edges: { first: EventEnvelope; last: EventEnvelope } | null;
|
|
109
|
+
try {
|
|
110
|
+
edges = readEdges(file);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
this.log.warn('事件分片读不出头尾,跳过', { run: id, err: error });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!edges) continue;
|
|
116
|
+
this.segments.push({
|
|
117
|
+
run: id, file, first: edges.first.cursor, last: edges.last.cursor,
|
|
118
|
+
firstTs: edges.first.ts, lastTs: edges.last.ts, events: null,
|
|
119
|
+
});
|
|
120
|
+
this.next = Math.max(this.next, edges.last.cursor + 1);
|
|
121
|
+
}
|
|
122
|
+
const currentFile = join(runsDir, this.run, EVENTS_FILE);
|
|
123
|
+
mkdirSync(join(runsDir, this.run), { recursive: true });
|
|
124
|
+
this.current = { run: this.run, file: currentFile, first: 0, last: 0, firstTs: '', lastTs: '', events: [] };
|
|
125
|
+
if (existsSync(currentFile)) {
|
|
126
|
+
const raw = readFileSync(currentFile, 'utf8');
|
|
127
|
+
this.expectedSize = Buffer.byteLength(raw, 'utf8');
|
|
128
|
+
this.current.events = this.parseAll(raw, this.run);
|
|
129
|
+
const events = this.current.events;
|
|
130
|
+
if (events.length) {
|
|
131
|
+
this.current.first = events[0].cursor;
|
|
132
|
+
this.current.last = events[events.length - 1].cursor;
|
|
133
|
+
this.current.firstTs = events[0].ts;
|
|
134
|
+
this.current.lastTs = events[events.length - 1].ts;
|
|
135
|
+
this.next = Math.max(this.next, this.current.last + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.segments.push(this.current);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private parseAll(raw: string, run: string): EventEnvelope[] {
|
|
142
|
+
const out: EventEnvelope[] = [];
|
|
143
|
+
let skipped = 0;
|
|
144
|
+
for (const line of raw.split('\n')) {
|
|
145
|
+
const t = line.trim();
|
|
146
|
+
if (!t) continue;
|
|
147
|
+
const e = parseLine(t);
|
|
148
|
+
if (!e) { skipped++; continue; }
|
|
149
|
+
if (out.length && e.cursor <= out[out.length - 1].cursor) { skipped++; continue; }
|
|
150
|
+
out.push(e);
|
|
151
|
+
}
|
|
152
|
+
if (skipped) this.log.warn('事件分片有损坏或乱序行,已跳过', { run, skipped });
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private load(segment: Segment): EventEnvelope[] {
|
|
157
|
+
if (segment.events) return segment.events;
|
|
158
|
+
let raw = '';
|
|
159
|
+
try {
|
|
160
|
+
raw = readFileSync(segment.file, 'utf8');
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.log.warn('事件分片读取失败', { run: segment.run, err: error });
|
|
163
|
+
}
|
|
164
|
+
segment.events = this.parseAll(raw, segment.run);
|
|
165
|
+
return segment.events;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
append(e: Omit<EventEnvelope, 'cursor'>): EventEnvelope {
|
|
169
|
+
this.checkExclusiveWrite();
|
|
170
|
+
const envelope: EventEnvelope = { ...e, cursor: this.next++, run: this.run };
|
|
171
|
+
const events = this.current.events!;
|
|
172
|
+
events.push(envelope);
|
|
173
|
+
if (events.length === 1) { this.current.first = envelope.cursor; this.current.firstTs = envelope.ts; }
|
|
174
|
+
this.current.last = envelope.cursor;
|
|
175
|
+
this.current.lastTs = envelope.ts;
|
|
176
|
+
const line = JSON.stringify(envelope) + '\n';
|
|
177
|
+
appendFileSync(this.current.file, line, 'utf8');
|
|
178
|
+
this.expectedSize += Buffer.byteLength(line, 'utf8');
|
|
179
|
+
for (const cb of this.appendListeners) {
|
|
180
|
+
try { cb(envelope); } catch { /* 观察者异常不影响主流程 */ }
|
|
181
|
+
}
|
|
182
|
+
return envelope;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 大小与本实例记录不符时报告文件变化,仍继续写入;独占检查由 instanceLock 负责。 */
|
|
186
|
+
private checkExclusiveWrite(): void {
|
|
187
|
+
if (this.concurrencyReported) return;
|
|
188
|
+
let onDisk = 0;
|
|
189
|
+
try {
|
|
190
|
+
onDisk = existsSync(this.current.file) ? statSync(this.current.file).size : 0;
|
|
191
|
+
} catch {
|
|
192
|
+
return; // 读不到文件状态不影响写入
|
|
193
|
+
}
|
|
194
|
+
if (onDisk === this.expectedSize) return;
|
|
195
|
+
this.concurrencyReported = true;
|
|
196
|
+
this.log.error('事件库被本进程之外的写入改动过,疑似另一个实例在写同一个事件库', {
|
|
197
|
+
expectedSize: this.expectedSize,
|
|
198
|
+
onDiskSize: onDisk,
|
|
199
|
+
file: this.current.file,
|
|
200
|
+
});
|
|
201
|
+
this.expectedSize = onDisk;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 事件追加订阅,供控制台实时观察。 */
|
|
205
|
+
onAppend(cb: (e: EventEnvelope) => void): void {
|
|
206
|
+
this.appendListeners.push(cb);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 当前 run 分片里的条数 */
|
|
210
|
+
currentCount(): number {
|
|
211
|
+
return this.current.events!.length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
get(cursor: number): EventEnvelope | undefined {
|
|
215
|
+
if (!Number.isInteger(cursor) || cursor < 1) return undefined;
|
|
216
|
+
const segment = this.segments.find((s) => s.first <= cursor && cursor <= s.last);
|
|
217
|
+
if (!segment) return undefined;
|
|
218
|
+
const events = this.load(segment);
|
|
219
|
+
const i = lowerBound(events, cursor);
|
|
220
|
+
return events[i]?.cursor === cursor ? events[i] : undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
latestCursor(): number {
|
|
224
|
+
return this.next - 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private segmentsIn(q: { fromCursor?: number; toCursor?: number; fromTs?: string; toTs?: string }): Segment[] {
|
|
228
|
+
return this.segments.filter((s) => {
|
|
229
|
+
if (s.last === 0) return s === this.current;
|
|
230
|
+
if (q.fromCursor !== undefined && s.last < q.fromCursor) return false;
|
|
231
|
+
if (q.toCursor !== undefined && s.first > q.toCursor) return false;
|
|
232
|
+
if (q.fromTs !== undefined && s.lastTs < q.fromTs) return false;
|
|
233
|
+
if (q.toTs !== undefined && s.firstTs > q.toTs) return false;
|
|
234
|
+
return true;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
range(q: EventRangeQuery): EventEnvelope[] {
|
|
239
|
+
const matched: EventEnvelope[] = [];
|
|
240
|
+
for (const segment of this.segmentsIn(q)) {
|
|
241
|
+
const events = this.load(segment);
|
|
242
|
+
const start = q.fromCursor !== undefined ? lowerBound(events, q.fromCursor) : 0;
|
|
243
|
+
for (let i = start; i < events.length; i++) {
|
|
244
|
+
const e = events[i];
|
|
245
|
+
if (q.toCursor !== undefined && e.cursor > q.toCursor) break;
|
|
246
|
+
if (q.fromTs !== undefined && e.ts < q.fromTs) continue;
|
|
247
|
+
if (q.toTs !== undefined && e.ts > q.toTs) continue;
|
|
248
|
+
if (q.senderKey !== undefined && e.senderKey !== q.senderKey) continue;
|
|
249
|
+
if (q.source !== undefined && e.source !== q.source) continue;
|
|
250
|
+
if (q.origin !== undefined && e.origin !== q.origin) continue;
|
|
251
|
+
matched.push(e);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// limit从区间尾部取(最近优先),返回仍按游标升序
|
|
255
|
+
if (q.limit !== undefined && q.limit >= 0 && matched.length > q.limit) {
|
|
256
|
+
return matched.slice(matched.length - q.limit);
|
|
257
|
+
}
|
|
258
|
+
return matched;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
around(cursor: number, before: number, after: number): EventEnvelope[] {
|
|
262
|
+
const latest = this.latestCursor();
|
|
263
|
+
if (latest === 0) return [];
|
|
264
|
+
const center = Math.min(Math.max(cursor, 1), latest);
|
|
265
|
+
return this.range({ fromCursor: center - Math.max(0, before), toCursor: center + Math.max(0, after) });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
grep(q: EventGrepQuery): EventGrepHit[] {
|
|
269
|
+
const kw = q.keyword.toLowerCase();
|
|
270
|
+
const ctx = Math.max(0, q.context);
|
|
271
|
+
const hits: EventGrepHit[] = [];
|
|
272
|
+
for (const segment of this.segmentsIn(q)) {
|
|
273
|
+
for (const e of this.load(segment)) {
|
|
274
|
+
if (q.senderKey !== undefined && e.senderKey !== q.senderKey) continue;
|
|
275
|
+
if (q.source !== undefined && e.source !== q.source) continue;
|
|
276
|
+
if (q.origin !== undefined && e.origin !== q.origin) continue;
|
|
277
|
+
if (q.fromTs !== undefined && e.ts < q.fromTs) continue;
|
|
278
|
+
if (q.toTs !== undefined && e.ts > q.toTs) continue;
|
|
279
|
+
if (!e.text.toLowerCase().includes(kw)) continue;
|
|
280
|
+
// 邻近记录不经过筛选;重叠窗口保持分开。
|
|
281
|
+
hits.push({ hitCursor: e.cursor, events: this.around(e.cursor, ctx, ctx) });
|
|
282
|
+
if (q.limit !== undefined && hits.length >= q.limit) return hits;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return hits;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 清空当前 run 的分片(web 运维动作):删文件与内存里这一段,游标不回退,
|
|
290
|
+
* 更早的 run 不受影响。返回清掉的条数。
|
|
291
|
+
*/
|
|
292
|
+
clear(): number {
|
|
293
|
+
const n = this.current.events!.length;
|
|
294
|
+
this.current.events = [];
|
|
295
|
+
this.current.first = 0;
|
|
296
|
+
this.current.last = 0;
|
|
297
|
+
this.current.firstTs = '';
|
|
298
|
+
this.current.lastTs = '';
|
|
299
|
+
if (existsSync(this.current.file)) rmSync(this.current.file);
|
|
300
|
+
this.expectedSize = 0;
|
|
301
|
+
this.log.warn('当前 run 的事件分片已清空', { cleared: n, run: this.run });
|
|
302
|
+
return n;
|
|
303
|
+
}
|
|
304
|
+
}
|
package/src/core/fork.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Logger, ModelSpec, ToolCallContext, ToolDef } from './types.ts';
|
|
2
|
+
import type { SessionHandle } from './sessions.ts';
|
|
3
|
+
import { message, functionResult, responseRecords, type ContextRecord } from '../protocol/open-responses/context.ts';
|
|
4
|
+
import { responseRequest, textOf } from '../protocol/open-responses/context-helpers.ts';
|
|
5
|
+
import { ResponseAccumulator } from '../protocol/open-responses/stream.ts';
|
|
6
|
+
import { GenerationError, type ResponseClient } from './generation.ts';
|
|
7
|
+
import {
|
|
8
|
+
NOT_EXECUTED_BARRIER,
|
|
9
|
+
NOT_EXECUTED_INCOMPLETE,
|
|
10
|
+
NOT_EXECUTED_THREAD_ENDED,
|
|
11
|
+
forkUnknownTool,
|
|
12
|
+
toolFailed,
|
|
13
|
+
} from './markers.ts';
|
|
14
|
+
|
|
15
|
+
export interface ForkLoopOptions {
|
|
16
|
+
id: string;
|
|
17
|
+
llm: ResponseClient;
|
|
18
|
+
spec: ModelSpec;
|
|
19
|
+
messages: ContextRecord[];
|
|
20
|
+
tools: ToolDef[];
|
|
21
|
+
maxRounds: number;
|
|
22
|
+
softRounds?: number;
|
|
23
|
+
log: Logger;
|
|
24
|
+
stopWhen?: () => boolean;
|
|
25
|
+
wrapUpHint?: string;
|
|
26
|
+
capNote?: string;
|
|
27
|
+
nudge?: { when: (lastContent: string) => boolean; message: string };
|
|
28
|
+
track?: SessionHandle;
|
|
29
|
+
observeMessages?: (messages: ContextRecord[]) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The caller hands the fork a client already bound to one provider instance; the Item sequence is retained across tool rounds. */
|
|
33
|
+
export async function runForkLoop(opts: ForkLoopOptions): Promise<string> {
|
|
34
|
+
const { id, llm, spec, tools, maxRounds, log } = opts;
|
|
35
|
+
const messages = [...opts.messages];
|
|
36
|
+
const observed = [...messages];
|
|
37
|
+
opts.observeMessages?.(observed);
|
|
38
|
+
const ctx: ToolCallContext = { role: id, log };
|
|
39
|
+
const schemas = tools.map(({ name, description, parameters }) => ({ name, description, parameters }));
|
|
40
|
+
const wrapUpAt = Math.min(Math.max(1, opts.softRounds ?? maxRounds - 1), Math.max(1, maxRounds - 1));
|
|
41
|
+
const wrapUpHint = opts.wrapUpHint ?? 'Wrap up: give your conclusion next round, no more tool calls.';
|
|
42
|
+
let lastContent = '';
|
|
43
|
+
const runRound = async (round: number): Promise<'done' | 'continue'> => {
|
|
44
|
+
let draft = new ResponseAccumulator();
|
|
45
|
+
const settledLength = observed.length;
|
|
46
|
+
let generated;
|
|
47
|
+
try {
|
|
48
|
+
generated = await llm.respond(responseRequest(spec, messages, schemas), {
|
|
49
|
+
role: id, sessionId: opts.track?.id, context: messages, nativeSpec: spec,
|
|
50
|
+
onEvent: event => {
|
|
51
|
+
if (event.type === 'response.created') draft = new ResponseAccumulator();
|
|
52
|
+
draft.accept(event);
|
|
53
|
+
const snapshot = draft.snapshot();
|
|
54
|
+
if (snapshot) {
|
|
55
|
+
observed.splice(settledLength, observed.length - settledLength, ...snapshot.output.map(item => ({ version: 2 as const, item, context: { responseId: snapshot.id } })));
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof GenerationError) opts.track?.recordAttempts(error.attempts, observed);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const output = responseRecords(generated.response, generated.origin);
|
|
64
|
+
messages.push(...output);
|
|
65
|
+
observed.splice(settledLength, observed.length - settledLength, ...output);
|
|
66
|
+
opts.track?.recordAttempts(generated.attempts, observed);
|
|
67
|
+
const text = output.filter(entry => entry.item.type === 'message').map(textOf).join('');
|
|
68
|
+
if (text) lastContent = text;
|
|
69
|
+
const calls = generated.response.output.filter(item => item.type === 'function_call');
|
|
70
|
+
if (!calls.length) return 'done';
|
|
71
|
+
let barrier = false;
|
|
72
|
+
for (const [index, call] of calls.entries()) {
|
|
73
|
+
const def = tools.find(tool => tool.name === call.name);
|
|
74
|
+
let out: string;
|
|
75
|
+
let ended = false;
|
|
76
|
+
if (barrier) out = NOT_EXECUTED_BARRIER;
|
|
77
|
+
else if (call.status !== 'completed') { out = NOT_EXECUTED_INCOMPLETE; barrier = true; }
|
|
78
|
+
else if (!def) out = forkUnknownTool(call.name);
|
|
79
|
+
else {
|
|
80
|
+
try {
|
|
81
|
+
const args = JSON.parse(call.arguments || '{}') as Record<string, unknown>;
|
|
82
|
+
const result = await def.handler(args, { ...ctx, callId: call.call_id });
|
|
83
|
+
out = typeof result === 'string' ? result : result.text;
|
|
84
|
+
ended = def.endsTurn === true;
|
|
85
|
+
} catch (error) { out = toolFailed(error instanceof Error ? error.message : String(error)); }
|
|
86
|
+
if (def.barrierAfter) barrier = true;
|
|
87
|
+
}
|
|
88
|
+
if (round === wrapUpAt) out += `\n[system] ${wrapUpHint}`;
|
|
89
|
+
const receipt = functionResult(call.call_id, out);
|
|
90
|
+
messages.push(receipt); observed.push(receipt);
|
|
91
|
+
if (ended || opts.stopWhen?.()) {
|
|
92
|
+
for (const skipped of calls.slice(index + 1)) {
|
|
93
|
+
const receipt = functionResult(skipped.call_id, NOT_EXECUTED_THREAD_ENDED);
|
|
94
|
+
messages.push(receipt); observed.push(receipt);
|
|
95
|
+
}
|
|
96
|
+
return 'done';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return 'continue';
|
|
100
|
+
};
|
|
101
|
+
let finished = false;
|
|
102
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
103
|
+
if (await runRound(round) === 'done') { finished = true; break; }
|
|
104
|
+
}
|
|
105
|
+
if (!finished) log.warn('工具循环达到硬上限,取最后内容为结果', { maxRounds });
|
|
106
|
+
if (opts.nudge && !opts.stopWhen?.() && opts.nudge.when(lastContent)) {
|
|
107
|
+
const reminder = message('user', opts.nudge.message);
|
|
108
|
+
messages.push(reminder); observed.push(reminder);
|
|
109
|
+
await runRound(maxRounds);
|
|
110
|
+
}
|
|
111
|
+
if (!finished && opts.capNote) lastContent = lastContent ? `${lastContent}\n\n${opts.capNote}` : opts.capNote;
|
|
112
|
+
return lastContent;
|
|
113
|
+
}
|