cortico 0.1.0 → 0.1.2
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/README.md +7 -14
- package/package.json +1 -3
- package/src/boot.ts +18 -0
- package/src/bot.ts +41 -6
- package/src/core/README.md +4 -6
- package/src/core/blobs.ts +23 -2
- package/src/core/config-schema.ts +1 -1
- package/src/core/config.ts +2 -16
- package/src/core/core.ts +15 -5
- package/src/core/loop.ts +55 -27
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +19 -3
- package/src/core/util.ts +17 -1
- package/src/deploy.ts +4 -1
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +23 -2
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +23 -9
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +32 -8
- package/src/providers/base.ts +3 -1
- package/src/providers/configuration.ts +2 -1
- package/src/providers/console/hub.ts +270 -0
- package/src/providers/console/settings.ts +8 -18
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +8 -6
- package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
- package/src/providers/llamacpp/console/server.ts +3 -1
- package/src/providers/llamacpp/index.ts +3 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/strings.ts +6 -6
- package/src/providers/name.ts +8 -0
- package/src/providers/openai-responses-compat/config.ts +13 -0
- package/src/providers/openai-responses-compat/console/client.ts +5 -0
- package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
- package/src/providers/openai-responses-compat/console/server.ts +127 -0
- package/src/providers/openai-responses-compat/index.ts +38 -13
- package/src/providers/openai-responses-compat/native.ts +9 -4
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/registry.ts +5 -0
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +11 -6
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
- package/src/web/client/console-pages/host.ts +24 -7
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- package/src/web/client/features/core/strings.ts +2 -2
- package/src/web/client/features/extensions/index.ts +276 -41
- package/src/web/client/features/extensions/strings.ts +84 -10
- package/src/web/client/features/feature.ts +2 -2
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +34 -11
- package/src/web/client/features/live/onboarding.ts +4 -1
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +20 -14
- package/src/web/client/features/live/timeline.ts +2 -1
- package/src/web/client/features/providers/detail.ts +262 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +173 -87
- package/src/web/client/features/providers/strings.ts +44 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +13 -0
- package/src/web/client/features/settings/index.ts +1 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/features/worlds/index.ts +1 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +28 -48
- package/src/web/client/shell/strings.ts +0 -22
- package/src/web/client/ui/icons.ts +14 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/console-pages.ts +1 -1
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +143 -26
- package/src/web/server.ts +233 -19
- package/src/web/shared/client-panel.ts +4 -1
- package/src/web/shared/console-protocol.ts +4 -1
- package/src/worlds/bilibili/README.md +1 -1
- package/src/worlds/bilibili/overlay/server.ts +4 -2
- package/src/worlds/minecraft/ADAPT.md +66 -0
- package/src/worlds/minecraft/README.md +69 -11
- package/src/worlds/minecraft/cell-facts.ts +226 -0
- package/src/worlds/minecraft/chests.ts +5 -0
- package/src/worlds/minecraft/containers.ts +325 -0
- package/src/worlds/minecraft/entity-facts.ts +31 -5
- package/src/worlds/minecraft/executor.ts +283 -10348
- package/src/worlds/minecraft/inventory.ts +268 -0
- package/src/worlds/minecraft/melee.ts +419 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
- package/src/worlds/minecraft/placed-ledger.ts +152 -0
- package/src/worlds/minecraft/placement.ts +1038 -0
- package/src/worlds/minecraft/receipt.ts +340 -0
- package/src/worlds/minecraft/skill-context.ts +358 -0
- package/src/worlds/minecraft/skills-build.ts +1169 -0
- package/src/worlds/minecraft/skills-container.ts +1343 -0
- package/src/worlds/minecraft/skills-craft.ts +333 -0
- package/src/worlds/minecraft/skills-dig.ts +624 -0
- package/src/worlds/minecraft/skills-gather.ts +1230 -0
- package/src/worlds/minecraft/skills-interact.ts +1559 -0
- package/src/worlds/minecraft/tools.ts +331 -0
- package/src/worlds/minecraft/travel.ts +763 -0
- package/src/worlds/minecraft/until.ts +75 -0
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +53 -7
- package/src/worlds/terminal/world.ts +5 -3
package/README.md
CHANGED
|
@@ -15,8 +15,7 @@ import type { EventEnvelope, WorldHost } from 'cortico/core/types.ts';
|
|
|
15
15
|
|
|
16
16
|
Inside a running instance those specifiers are resolved by the host's module hook to the
|
|
17
17
|
framework that instance runs. This package supplies the same files to the editor, the type
|
|
18
|
-
checker and the test runner
|
|
19
|
-
what it does: it has no entry point and starts nothing.
|
|
18
|
+
checker and the test runner. It has no entry point and starts nothing.
|
|
20
19
|
|
|
21
20
|
## Install
|
|
22
21
|
|
|
@@ -24,9 +23,9 @@ what it does: it has no entry point and starts nothing.
|
|
|
24
23
|
pnpm add -D cortico
|
|
25
24
|
```
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
your extension was written against.
|
|
26
|
+
Each extension kind has its own contract version — world 5, provider 5, bot 5 — and a package
|
|
27
|
+
declares its own as `"cortico": { "kind": "world", "api": 5 }` in its `package.json`. This
|
|
28
|
+
package's minor version tracks the framework, so pin the range your extension was written against.
|
|
30
29
|
|
|
31
30
|
`templates/extension/` in the repository holds a ready package for each kind, and
|
|
32
31
|
[docs/extensions.md](https://github.com/Pal-AI-Lab/Cortico/blob/main/docs/extensions.md) states
|
|
@@ -34,14 +33,8 @@ the packaging rules.
|
|
|
34
33
|
|
|
35
34
|
## Running a bot
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
git clone https://github.com/Pal-AI-Lab/Cortico.git
|
|
42
|
-
cd Cortico
|
|
43
|
-
corepack pnpm install
|
|
44
|
-
pnpm start
|
|
45
|
-
```
|
|
36
|
+
Deployments, bot packages and the web console live in a checkout of the repository, which is
|
|
37
|
+
where a bot runs from:
|
|
38
|
+
[Quick Start](https://github.com/Pal-AI-Lab/Cortico#quick-start).
|
|
46
39
|
|
|
47
40
|
MIT licensed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A TypeScript framework for event-driven, long-running persona bots",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,8 +36,6 @@
|
|
|
36
36
|
"@types/express": "^4.17.21",
|
|
37
37
|
"@types/ws": "^8.5.13",
|
|
38
38
|
"express": "^4.21.2",
|
|
39
|
-
"fflate": "0.8.3",
|
|
40
|
-
"tar-stream": "3.2.1",
|
|
41
39
|
"ws": "^8.18.0"
|
|
42
40
|
}
|
|
43
41
|
}
|
package/src/boot.ts
CHANGED
|
@@ -19,6 +19,14 @@ export function isSupervised(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
19
19
|
return env[SUPERVISED_ENV] === '1' || env[SUPERVISED_ENV] === 'true';
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 启动时是否暂停事件投递。每一轮受监管的启动拿到同一份环境和同一串参数,
|
|
24
|
+
* 所以这个值也是重启后的状态。
|
|
25
|
+
*/
|
|
26
|
+
export function startsPaused(env: NodeJS.ProcessEnv = process.env, argv: readonly string[] = process.argv): boolean {
|
|
27
|
+
return env.CORTICO_START_PAUSED === '1' || env.CORTICO_START_PAUSED === 'true' || argv.includes('--paused');
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
function notifyLauncher(message: { type: string; dataDir?: string }, env = process.env): void {
|
|
23
31
|
if (!isSupervised(env)) return;
|
|
24
32
|
process.send?.(message);
|
|
@@ -44,3 +52,13 @@ export function consumeBootFlags(dataDir: string): void {
|
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
}
|
|
55
|
+
|
|
56
|
+
export function listensOnEveryInterface(bound: string | null): boolean {
|
|
57
|
+
return bound === '0.0.0.0' || bound === '::';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 本机打得开的控制台地址:监听所有网卡时给回环地址,IPv6 加方括号。 */
|
|
61
|
+
export function consoleUrlOf(bound: string | null, port: number): string {
|
|
62
|
+
const host = bound === null || listensOnEveryInterface(bound) ? '127.0.0.1' : bound.includes(':') ? `[${bound}]` : bound;
|
|
63
|
+
return `http://${host}:${port}/`;
|
|
64
|
+
}
|
package/src/bot.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { coreConfigGroup } from './core/config.ts';
|
|
|
19
19
|
import { pick, resolveLanguage, type Language } from './core/language.ts';
|
|
20
20
|
import { updateJsonObject } from './config-file.ts';
|
|
21
21
|
import { ONBOARDING_FLAG_FILE } from './deploy.ts';
|
|
22
|
-
import { isSupervised, requestRestart } from './boot.ts';
|
|
22
|
+
import { isSupervised, requestRestart, startsPaused } from './boot.ts';
|
|
23
23
|
import { ExtensionManager, type ExtensionSet } from './extensions.ts';
|
|
24
24
|
import { WorldAssembly, type WorldDefinition, type WorldDeclaration, type WorldSection } from './world.ts';
|
|
25
25
|
import { Core, type WorldStopFailure } from './core/core.ts';
|
|
@@ -30,6 +30,7 @@ import { assembleSystemSegments, envPromptOverridePath, envPromptTemplateSource,
|
|
|
30
30
|
import { aggregateUsage } from './core/cost.ts';
|
|
31
31
|
import { nowIso, withDeadline } from './core/util.ts';
|
|
32
32
|
import { closeRun } from './core/run.ts';
|
|
33
|
+
import { ProviderHub } from './providers/console/hub.ts';
|
|
33
34
|
import { ProviderSettings } from './providers/console/settings.ts';
|
|
34
35
|
import { providerModules } from './providers/registry.ts';
|
|
35
36
|
import { readGroupValues, setByPath as setConfigPath } from './core/config-schema.ts';
|
|
@@ -51,6 +52,9 @@ import {
|
|
|
51
52
|
type ConsolePageContribution,
|
|
52
53
|
} from './web/shared/console-protocol.ts';
|
|
53
54
|
|
|
55
|
+
/** 控制台访问密码的密钥名;进程环境或部署 .env 里非空时优先于 web.password。 */
|
|
56
|
+
export const WEB_PASSWORD_SECRET = 'CORTICO_WEB_PASSWORD';
|
|
57
|
+
|
|
54
58
|
/** 启动器使用的 bot 包装配契约。 */
|
|
55
59
|
export interface BotDefinition<C extends CoreConfig = CoreConfig> {
|
|
56
60
|
/** 控制台页 id `persona:<id>` 与产物键取它;仓内包的 id 与目录同名。 */
|
|
@@ -166,7 +170,7 @@ const BOT_TEXT = {
|
|
|
166
170
|
},
|
|
167
171
|
session: {
|
|
168
172
|
label: '主session(当前对话上下文)',
|
|
169
|
-
note: '清除对话上下文并重新开场,保留 Memory
|
|
173
|
+
note: '清除对话上下文并重新开场,保留 Memory 和事件库。正在处理批次时等该批结束后执行',
|
|
170
174
|
stat: (records: number, ktok: number, size: string) => `${records}条 / ~${ktok}k tok / ${size}`,
|
|
171
175
|
cleared: 'session已清空重开(system前缀+开场消息)',
|
|
172
176
|
},
|
|
@@ -212,6 +216,12 @@ const BOT_TEXT = {
|
|
|
212
216
|
stat: (n: number) => `${n}条待投递`,
|
|
213
217
|
cleared: (n: number) => `已丢弃${n}条待投递事件`,
|
|
214
218
|
},
|
|
219
|
+
media: {
|
|
220
|
+
label: '附件库(事件与工具回执里的图片、音频)',
|
|
221
|
+
note: '删除全部附件文件,保留引用它们的事件与 session 记录;删掉的附件在上下文里只剩文字说明',
|
|
222
|
+
stat: (n: number, size: string) => `${n}份 / ${size}`,
|
|
223
|
+
cleared: (n: number) => `已删除${n}份附件`,
|
|
224
|
+
},
|
|
215
225
|
},
|
|
216
226
|
config: {
|
|
217
227
|
unknownGroup: (id: string) => `没有这一组配置: ${id}`,
|
|
@@ -260,7 +270,7 @@ const BOT_TEXT = {
|
|
|
260
270
|
},
|
|
261
271
|
session: {
|
|
262
272
|
label: 'Main session (current conversation context)',
|
|
263
|
-
note: 'Clears the conversation and reopens the session, keeping Memory and the event store.
|
|
273
|
+
note: 'Clears the conversation and reopens the session, keeping Memory and the event store. While a batch is in progress, runs after it finishes',
|
|
264
274
|
stat: (records: number, ktok: number, size: string) => `${records} records / ~${ktok}k tok / ${size}`,
|
|
265
275
|
cleared: 'Session cleared and reopened (system prefix + opening message)',
|
|
266
276
|
},
|
|
@@ -306,6 +316,12 @@ const BOT_TEXT = {
|
|
|
306
316
|
stat: (n: number) => `${n} pending`,
|
|
307
317
|
cleared: (n: number) => `Discarded ${n} pending events`,
|
|
308
318
|
},
|
|
319
|
+
media: {
|
|
320
|
+
label: 'Attachment store (images and audio from events and tool results)',
|
|
321
|
+
note: 'Deletes every attachment file and keeps the events and session records that reference them; a deleted attachment leaves only its text description in context',
|
|
322
|
+
stat: (n: number, size: string) => `${n} files / ${size}`,
|
|
323
|
+
cleared: (n: number) => `Deleted ${n} attachments`,
|
|
324
|
+
},
|
|
309
325
|
},
|
|
310
326
|
config: {
|
|
311
327
|
unknownGroup: (id: string) => `No such config group: ${id}`,
|
|
@@ -466,6 +482,19 @@ function deriveStorage<C extends CoreConfig>(core: Core<C>, dataDir: string, lan
|
|
|
466
482
|
// 保留延迟渲染项,其回调还负责复位 World 的排队状态。
|
|
467
483
|
clear: () => s.pending.cleared(core.discardPendingEvents()),
|
|
468
484
|
},
|
|
485
|
+
{
|
|
486
|
+
key: 'media',
|
|
487
|
+
label: s.media.label,
|
|
488
|
+
kind: 'disk',
|
|
489
|
+
location: 'data/media/',
|
|
490
|
+
danger: true,
|
|
491
|
+
note: s.media.note,
|
|
492
|
+
stat: () => {
|
|
493
|
+
const { count, bytes } = core.logBlobs.stat();
|
|
494
|
+
return s.media.stat(count, `${(bytes / 1024).toFixed(1)}KB`);
|
|
495
|
+
},
|
|
496
|
+
clear: () => s.media.cleared(core.logBlobs.clear()),
|
|
497
|
+
},
|
|
469
498
|
];
|
|
470
499
|
}
|
|
471
500
|
|
|
@@ -867,7 +896,7 @@ export function deriveConsolePageSources(
|
|
|
867
896
|
parts: { assembly: WorldAssembly; persona?: Persona },
|
|
868
897
|
/**
|
|
869
898
|
* bot 标识、展示名、Memory 名与配置组;配置组归入 Persona 页面。
|
|
870
|
-
*
|
|
899
|
+
* 展示名是这一台 bot 的名字(底栏头像旁边那个),Persona 页的标题另取 Persona 的类名。
|
|
871
900
|
*/
|
|
872
901
|
bot?: { id: string; label: string; memoryName?: string; configGroups?: readonly ConfigGroup[] },
|
|
873
902
|
|
|
@@ -990,6 +1019,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
990
1019
|
|
|
991
1020
|
// 共享端点配置位于部署根的 providers/;activeProvider 属于当前部署。
|
|
992
1021
|
const providerSettings = new ProviderSettings(cfg,core.providers,join(loaded.rootDir,'config.json'),loaded.providersDir ?? join(loaded.rootDir,'providers'));
|
|
1022
|
+
const providerHub = new ProviderHub(cfg, core.providers, providerSettings, join(loaded.rootDir, 'config.json'), loaded.providersDir ?? join(loaded.rootDir, 'providers'));
|
|
993
1023
|
const allConfigGroups = (language: Language) => [...configGroups(language),...providerSettings.groups(language)];
|
|
994
1024
|
const llmManagers = new Map<string,{stop():Promise<unknown>}>([['providers',{stop:()=>core.providers.stopAll()}]]);
|
|
995
1025
|
|
|
@@ -1058,6 +1088,9 @@ export function createBot<C extends CoreConfig>(
|
|
|
1058
1088
|
dataDir: loaded.dataDir,
|
|
1059
1089
|
botDir: loaded.rootDir,
|
|
1060
1090
|
language,
|
|
1091
|
+
...(cfg.web.host ? { host: cfg.web.host } : {}),
|
|
1092
|
+
allowedHosts: cfg.web.allowedHosts ?? [],
|
|
1093
|
+
password: loaded.secret(WEB_PASSWORD_SECRET) || (cfg.web.password ?? ''),
|
|
1061
1094
|
defaultScheme: cfg.web.theme,
|
|
1062
1095
|
sessions: core.sessions,
|
|
1063
1096
|
storage: consoleStorage,
|
|
@@ -1099,7 +1132,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
1099
1132
|
? (language) => contribution.consolePages!({ storage: consoleStorage(language), language })
|
|
1100
1133
|
: undefined,
|
|
1101
1134
|
)(),...providerSettings.sources()],
|
|
1102
|
-
|
|
1135
|
+
providers: providerHub,
|
|
1103
1136
|
worldVisibility: {
|
|
1104
1137
|
state: () => core.worldVisibility(),
|
|
1105
1138
|
set: (id, visible, language) => {
|
|
@@ -1127,6 +1160,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
1127
1160
|
return beginShutdown('控制台重启键', { closeWeb: false, exit: true, language });
|
|
1128
1161
|
},
|
|
1129
1162
|
supervised: isSupervised(),
|
|
1163
|
+
startsPaused: startsPaused(),
|
|
1130
1164
|
},
|
|
1131
1165
|
onboarding: {
|
|
1132
1166
|
dismiss: () => { try { unlinkSync(join(loaded.rootDir, ONBOARDING_FLAG_FILE)); } catch { /* 已经删过 */ } },
|
|
@@ -1166,6 +1200,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
1166
1200
|
},
|
|
1167
1201
|
getStatus: () => ({
|
|
1168
1202
|
displayName: cfg.displayName,
|
|
1203
|
+
modelConnection: providerHub.current(language),
|
|
1169
1204
|
startedAt,
|
|
1170
1205
|
loop: core.loop.getStatus(),
|
|
1171
1206
|
eventCount: core.store.latestCursor(),
|
|
@@ -1199,7 +1234,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
1199
1234
|
|
|
1200
1235
|
const orphans = Object.entries(cfg.providers).filter(([, entry]) => !providerModules.some((m) => m.id === entry.kind)).map(([name, entry]) => `${name}(kind=${entry.kind})`);
|
|
1201
1236
|
if (orphans.length) core.runlog.logger('provider').warn('端点条目没有对应的 Provider 模块,不可用', { orphans });
|
|
1202
|
-
void core.providers.start(cfg.activeProvider).catch(error=>core.runlog.logger('provider').error('Provider 启动失败',{error:String(error)}));
|
|
1237
|
+
if (cfg.activeProvider && cfg.providers[cfg.activeProvider]) void core.providers.start(cfg.activeProvider).catch(error=>core.runlog.logger('provider').error('Provider 启动失败',{error:String(error)}));
|
|
1203
1238
|
await parts.onStart?.({ core, loaded, port });
|
|
1204
1239
|
await core.start();
|
|
1205
1240
|
return { port };
|
package/src/core/README.md
CHANGED
|
@@ -51,14 +51,16 @@ piggyback 只入队,随后续唤醒一起投递。
|
|
|
51
51
|
更新后的值在下一次入队时参与计算。
|
|
52
52
|
|
|
53
53
|
投递水位 `lastDeliveredCursor` 持久化,队列不持久化。重启时补投水位之后的外部事件;
|
|
54
|
-
|
|
54
|
+
已被候选处理结果引用的原始归档不重复投递。内部事件仅在当次运行投递。清空分片或跳过损坏行
|
|
55
|
+
留下的游标空位没有可投递内容,不阻止水位推进。
|
|
55
56
|
|
|
56
57
|
## 主循环
|
|
57
58
|
|
|
58
59
|
每次唤醒处理一批事件,可进行多轮模型调用。`SessionDecl.rounds()` 提供
|
|
59
60
|
`{ soft, hard, softHint? }`:到 soft 轮时将 `softHint` 追加到最后一条工具回执;到 hard 轮时
|
|
60
61
|
记录 warn 并结束本批。`endsTurn` 工具和自然结束共用结束处理;本批结束时尚未处理的事件
|
|
61
|
-
|
|
62
|
+
退回总线,进入下一批。控制台的前缀重载与清空 session 在批次边界执行:正在处理批次时等该批
|
|
63
|
+
结束,空闲时立即;并发请求复用同一事务。
|
|
62
64
|
|
|
63
65
|
状态 0、429 或 5xx 的模型调用失败,可保留已记录的输出和工具回执,按 `ResubmitPolicy` 重试。
|
|
64
66
|
默认允许连续重试 2 次、每批最多 4 次,退避为 2 秒、10 秒;上下文超限、抢占、关机或轮数
|
|
@@ -77,12 +79,8 @@ handler 异常转为失败回执。流式生成时 `EagerDispatch` 可提前执
|
|
|
77
79
|
`Persona.onHandoff(snapshot, { hardTokens })` 返回 `{ tail, trim? }`。Core 重建 system 前缀,
|
|
78
80
|
按 `hardTokens − estimate(prefix)` 限制保留上下文,其中 prefix 包括 system 前缀和 Persona 的合成
|
|
79
81
|
开头(`sessionHead()`)。Core 校验工具调用配对,并重置 session。
|
|
80
|
-
合成开头每次请求现取,不写入持久 session;Core 不据此判定远端缓存是否命中。
|
|
81
82
|
阶段预算与保留比例由 Persona 决定,模型配置来自当前 provider。
|
|
82
83
|
|
|
83
|
-
存储清单使用 `StorageOwner` 标识 Core、Persona、Memory 与 World 的归属。
|
|
84
|
-
页面组织和 Memory 展示名称由装配层处理。
|
|
85
|
-
|
|
86
84
|
## 错误隔离
|
|
87
85
|
|
|
88
86
|
装配层将 World 构造失败限制在该 World。`start()` 抛错时不挂载;`stop()` 失败或超时会记录
|
package/src/core/blobs.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `mem:<后端标识>` 由 Persona 的 BlobStore 解析,后端决定标识格式。
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import type { BlobRef } from './types.ts';
|
|
10
10
|
|
|
@@ -58,7 +58,7 @@ export function withBlobLines(text: string, refs: readonly BlobRef[] | undefined
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* 字节按内容哈希存入 data/media
|
|
61
|
+
* 字节按内容哈希存入 data/media/,重复写入不增加副本。
|
|
62
62
|
* 读取接受完整句柄或唯一的十六进制前缀(至少 8 位)。
|
|
63
63
|
*/
|
|
64
64
|
export class LogBlobStore {
|
|
@@ -81,6 +81,27 @@ export class LogBlobStore {
|
|
|
81
81
|
return `${LOG_SCHEME}${name}`;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** 现存份数与占用字节;目录还没建时都是 0。 */
|
|
85
|
+
stat(): { count: number; bytes: number } {
|
|
86
|
+
if (!existsSync(this.dir)) return { count: 0, bytes: 0 };
|
|
87
|
+
const names = readdirSync(this.dir);
|
|
88
|
+
return { count: names.length, bytes: names.reduce((sum, name) => sum + statSync(join(this.dir, name)).size, 0) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 删掉目录里的全部附件,返回删掉的份数。附件那一行正文在落库时已写定
|
|
93
|
+
* (句柄、mime 与 fallbackText),此后 read() 返回 null,投递时不再附字节。
|
|
94
|
+
*/
|
|
95
|
+
clear(): number {
|
|
96
|
+
if (!existsSync(this.dir)) return 0;
|
|
97
|
+
let removed = 0;
|
|
98
|
+
for (const name of readdirSync(this.dir)) {
|
|
99
|
+
rmSync(join(this.dir, name), { force: true });
|
|
100
|
+
removed += 1;
|
|
101
|
+
}
|
|
102
|
+
return removed;
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
/** 按句柄取回;句柄不合形状、前缀不唯一或文件不在了返回 null。 */
|
|
85
106
|
read(handle: string): { bytes: Buffer; mime: string } | null {
|
|
86
107
|
const name = this.resolveName(handle);
|
|
@@ -12,7 +12,7 @@ import type { CoreConfig } from './types.ts';
|
|
|
12
12
|
import { pick, type Language } from './language.ts';
|
|
13
13
|
|
|
14
14
|
export interface ConfigProperty {
|
|
15
|
-
type: 'integer' | 'number' | 'boolean' | 'string' | 'array';
|
|
15
|
+
type: 'integer' | 'number' | 'boolean' | 'string' | 'array' | 'object';
|
|
16
16
|
title: string;
|
|
17
17
|
description?: string;
|
|
18
18
|
minimum?: number;
|
package/src/core/config.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Core 默认配置与深合并工具。Persona 和 World 参数由各自声明,部署合并顺序见 src/deploy.ts。 */
|
|
2
2
|
import type { CoreConfig } from './types.ts';
|
|
3
|
-
import type { PriceDefinition } from '../providers/pricebook.ts';
|
|
4
3
|
import type { ConfigGroup } from './config-schema.ts';
|
|
5
4
|
import { pick, type Language } from './language.ts';
|
|
6
5
|
|
|
@@ -186,21 +185,8 @@ export const CORE_DEFAULTS = {
|
|
|
186
185
|
/** 用于控制台标题和终端消息的发送方名称。 */
|
|
187
186
|
displayName: 'Cortico Bot',
|
|
188
187
|
timezone: 'Asia/Shanghai',
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
deepseek: {
|
|
192
|
-
kind: 'openai-responses-compat' as const,
|
|
193
|
-
baseUrl: 'https://api.deepseek.com',
|
|
194
|
-
secret: 'DEEPSEEK_API_KEY',
|
|
195
|
-
spec: { model: 'deepseek-flash', thinking: false },
|
|
196
|
-
// 默认价目为零;操作者可在控制台填写实际价格。
|
|
197
|
-
pricing: [{
|
|
198
|
-
models: ['*'], currency: 'USD', basis: 'marginal', source: 'console',
|
|
199
|
-
rules: [{ meter: 'cachedInput', perMillion: 0 }, { meter: 'uncachedInput', perMillion: 0 }, { meter: 'output', perMillion: 0 }],
|
|
200
|
-
}] as PriceDefinition[],
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
activeProvider: 'deepseek',
|
|
188
|
+
providers: {} as Record<string, import('./types.ts').LLMProviderEntry>,
|
|
189
|
+
activeProvider: '',
|
|
204
190
|
web: { port: 7777, theme: 'mint' },
|
|
205
191
|
paths: { memory: 'memory', data: 'data' },
|
|
206
192
|
batching: { quietGapMs: 2500, minBatchAgeMs: 0, maxBatchAgeMs: 15000, maxBatchSize: 100 },
|
package/src/core/core.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { SessionLog } from './session.ts';
|
|
|
32
32
|
import { CoreState } from './state.ts';
|
|
33
33
|
import { ProviderRegistry } from '../providers/registry.ts';
|
|
34
34
|
import { runForkLoop } from './fork.ts';
|
|
35
|
-
import { providerModule } from '../providers/registry.ts';
|
|
35
|
+
import { providerModule, providerModules } from '../providers/registry.ts';
|
|
36
36
|
import { LogBlobStore, blobScheme, mimeOfHandle, withBlobLines } from './blobs.ts';
|
|
37
37
|
import { TimerStore } from './timers.ts';
|
|
38
38
|
import { SessionTracker, type SessionHandle } from './sessions.ts';
|
|
@@ -289,15 +289,19 @@ export class Core<C extends CoreConfig = CoreConfig> {
|
|
|
289
289
|
|
|
290
290
|
/** 按当前活跃端点读取模型上下文事实。 */
|
|
291
291
|
private contextFacts(): ContextFacts {
|
|
292
|
-
const module = () =>
|
|
292
|
+
const module = () => {
|
|
293
|
+
const entry = this.config.providers[this.config.activeProvider];
|
|
294
|
+
return entry ? providerModules.find(module => module.id === entry.kind) : undefined;
|
|
295
|
+
};
|
|
293
296
|
return {
|
|
294
297
|
hardTokens: () => {
|
|
298
|
+
if (!module() || !this.config.providers[this.config.activeProvider]?.spec) return null;
|
|
295
299
|
const spec = this.activeSpec();
|
|
296
300
|
const window = this.contextWindowOf(spec);
|
|
297
301
|
return window === undefined ? null : Math.max(0, window - (spec.maxTokens ?? 0));
|
|
298
302
|
},
|
|
299
|
-
estimateTokens: (records) => module()
|
|
300
|
-
contextOverflow: (error) => module()
|
|
303
|
+
estimateTokens: (records) => module()?.estimateTokens?.(records, this.activeSpec()) ?? estimateMessagesTokens(records),
|
|
304
|
+
contextOverflow: (error) => module()?.contextOverflow?.(error) ?? false,
|
|
301
305
|
};
|
|
302
306
|
}
|
|
303
307
|
|
|
@@ -587,7 +591,13 @@ export class Core<C extends CoreConfig = CoreConfig> {
|
|
|
587
591
|
const dataDir = this.loaded.dataDir;
|
|
588
592
|
if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
|
|
589
593
|
// 构造期已加载状态;此处再次 load 会覆盖装配后、启动前的修改。
|
|
590
|
-
this.session.load();
|
|
594
|
+
const repair = this.session.load();
|
|
595
|
+
if (repair) {
|
|
596
|
+
this.log.warn(
|
|
597
|
+
repair.kind === 'torn-tail' ? 'session 末行未写完,已截掉' : 'session 末行缺换行,已补上',
|
|
598
|
+
{ ...repair, file: 'session-main.jsonl' },
|
|
599
|
+
);
|
|
600
|
+
}
|
|
591
601
|
for (const mod of this.worlds) {
|
|
592
602
|
try {
|
|
593
603
|
await mod.start(this.makeHost(mod));
|
package/src/core/loop.ts
CHANGED
|
@@ -245,8 +245,9 @@ export class MainLoop {
|
|
|
245
245
|
/** 截断与前缀重载共用的维护串行链,避免两个 session.reset 互相覆盖。 */
|
|
246
246
|
private maintenanceChain: Promise<void> = Promise.resolve();
|
|
247
247
|
private prefixReloadPromise: Promise<void> | null = null;
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
private clearSessionPromise: Promise<void> | null = null;
|
|
249
|
+
/** 在一轮处理中请求的前缀重载与清空,等自然回合边界再释放。 */
|
|
250
|
+
private releaseAtBoundary: Array<() => void> = [];
|
|
250
251
|
/** 当前正在处理一个事件批;手动交接必须等到该批自然结束,不能重置半轮session。 */
|
|
251
252
|
private processingBatch = false;
|
|
252
253
|
private handoffRequested = false;
|
|
@@ -722,13 +723,15 @@ export class MainLoop {
|
|
|
722
723
|
const latest = store.latestCursor();
|
|
723
724
|
for (let c = top + 1; c <= latest; c++) {
|
|
724
725
|
const next = store.get(c);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
726
|
+
// 清空分片或跳过损坏行留下的空位没有可投递内容,不阻止水位推进。
|
|
727
|
+
if (next) {
|
|
728
|
+
// 未处理的 archive-only 项必须保留在水位之后,重启才会补投。
|
|
729
|
+
// 已处理项由 settledArchives 标识。
|
|
730
|
+
const skippable = next.origin === 'internal'
|
|
731
|
+
|| (next.contextDelivery === 'archive-only' && this.settledArchives.has(c));
|
|
732
|
+
// 集合记录存储位置;装载期重排保证 next.cursor === c。
|
|
733
|
+
if (!skippable && !this.deliveredCursors.has(c)) break;
|
|
734
|
+
}
|
|
732
735
|
this.deliveredCursors.delete(c);
|
|
733
736
|
this.settledArchives.delete(c);
|
|
734
737
|
top = c;
|
|
@@ -904,8 +907,8 @@ export class MainLoop {
|
|
|
904
907
|
} finally {
|
|
905
908
|
this.processingBatch = false;
|
|
906
909
|
}
|
|
907
|
-
// 异常退出由 stop
|
|
908
|
-
await this.
|
|
910
|
+
// 异常退出由 stop 释放排队请求;只有正常批次边界执行重载与清空。
|
|
911
|
+
await this.flushBoundaryMaintenance();
|
|
909
912
|
}
|
|
910
913
|
} finally {
|
|
911
914
|
this.stop();
|
|
@@ -933,7 +936,11 @@ export class MainLoop {
|
|
|
933
936
|
for (let round = 1; ; round++) {
|
|
934
937
|
if (!this.active(generation)) return;
|
|
935
938
|
this.roundsLastBatch = round;
|
|
936
|
-
|
|
939
|
+
let spec: ModelSpec;
|
|
940
|
+
try { spec = this.d.spec(); } catch (error) {
|
|
941
|
+
log.warn('模型配置不可用', { error: String(error) });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
937
944
|
// 后续轮输入超限时结束本批,由批末检查执行交接。
|
|
938
945
|
// 首轮仍处理本批新投递的事件;上一批的容量检查已在批末执行。
|
|
939
946
|
if (round > 1) {
|
|
@@ -1425,9 +1432,25 @@ export class MainLoop {
|
|
|
1425
1432
|
return rebuildTail(paired, budget, estimate);
|
|
1426
1433
|
}
|
|
1427
1434
|
|
|
1428
|
-
/**
|
|
1429
|
-
|
|
1435
|
+
/**
|
|
1436
|
+
* 清空主 session,重建 system 前缀并调用 Persona 开场钩子;事件库保留。
|
|
1437
|
+
* 正在处理批次时等到该批自然结束,不重置半轮 session;并发请求复用同一 Promise。
|
|
1438
|
+
*/
|
|
1439
|
+
clearSession(): Promise<void> {
|
|
1430
1440
|
const generation = this.generation;
|
|
1441
|
+
if (!this.active(generation)) return Promise.resolve();
|
|
1442
|
+
if (this.clearSessionPromise) return this.clearSessionPromise;
|
|
1443
|
+
let tracked: Promise<void>;
|
|
1444
|
+
tracked = this.safeBoundary()
|
|
1445
|
+
.then(() => this.enqueueMaintenance(() => this.performClearSession(generation), generation))
|
|
1446
|
+
.finally(() => {
|
|
1447
|
+
if (this.clearSessionPromise === tracked) this.clearSessionPromise = null;
|
|
1448
|
+
});
|
|
1449
|
+
this.clearSessionPromise = tracked;
|
|
1450
|
+
return tracked;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
private async performClearSession(generation: number): Promise<void> {
|
|
1431
1454
|
if (!this.active(generation)) return;
|
|
1432
1455
|
const { session, log } = this.d;
|
|
1433
1456
|
const sysMsg = await this.buildSystem();
|
|
@@ -1447,11 +1470,8 @@ export class MainLoop {
|
|
|
1447
1470
|
const generation = this.generation;
|
|
1448
1471
|
if (!this.active(generation)) return Promise.resolve();
|
|
1449
1472
|
if (this.prefixReloadPromise) return this.prefixReloadPromise;
|
|
1450
|
-
const safeBoundary = this.processingBatch
|
|
1451
|
-
? new Promise<void>((resolve) => { this.releasePrefixReload = resolve; })
|
|
1452
|
-
: Promise.resolve();
|
|
1453
1473
|
let tracked: Promise<void>;
|
|
1454
|
-
tracked = safeBoundary
|
|
1474
|
+
tracked = this.safeBoundary()
|
|
1455
1475
|
.then(() => this.enqueueMaintenance(() => this.performSystemPrefixReload(generation), generation))
|
|
1456
1476
|
.finally(() => {
|
|
1457
1477
|
if (this.prefixReloadPromise === tracked) this.prefixReloadPromise = null;
|
|
@@ -1460,12 +1480,22 @@ export class MainLoop {
|
|
|
1460
1480
|
return tracked;
|
|
1461
1481
|
}
|
|
1462
1482
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (!
|
|
1466
|
-
this.
|
|
1467
|
-
|
|
1468
|
-
|
|
1483
|
+
/** 空闲时立即;正在处理批次时等 run() 在批末释放,或 stop() 释放。 */
|
|
1484
|
+
private safeBoundary(): Promise<void> {
|
|
1485
|
+
if (!this.processingBatch) return Promise.resolve();
|
|
1486
|
+
return new Promise<void>((resolve) => { this.releaseAtBoundary.push(resolve); });
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
private releaseBoundary(): void {
|
|
1490
|
+
const waiting = this.releaseAtBoundary;
|
|
1491
|
+
this.releaseAtBoundary = [];
|
|
1492
|
+
for (const release of waiting) release();
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
private async flushBoundaryMaintenance(): Promise<boolean> {
|
|
1496
|
+
if (this.releaseAtBoundary.length === 0) return false;
|
|
1497
|
+
this.releaseBoundary();
|
|
1498
|
+
await Promise.all([this.prefixReloadPromise, this.clearSessionPromise]);
|
|
1469
1499
|
return true;
|
|
1470
1500
|
}
|
|
1471
1501
|
|
|
@@ -1721,9 +1751,7 @@ export class MainLoop {
|
|
|
1721
1751
|
this.backoffWake?.();
|
|
1722
1752
|
if (!this.shutdown.signal.aborted) this.shutdown.abort(new Error('core 正在关机'));
|
|
1723
1753
|
this.handoffRequested = false;
|
|
1724
|
-
|
|
1725
|
-
this.releasePrefixReload = null;
|
|
1726
|
-
releasePrefixReload?.();
|
|
1754
|
+
this.releaseBoundary();
|
|
1727
1755
|
this.stopFn?.();
|
|
1728
1756
|
const round = this.currentRound;
|
|
1729
1757
|
if (round && !round.controller.signal.aborted) {
|
package/src/core/session.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Session context is append-only between lifecycle resets. */
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import type { ContextRecord } from '../protocol/open-responses/context.ts';
|
|
4
|
-
import { ContextLog } from '../protocol/open-responses/context-log.ts';
|
|
4
|
+
import { ContextLog, type ContextLoadRepair } from '../protocol/open-responses/context-log.ts';
|
|
5
5
|
import { estimateMessagesTokens } from './util.ts';
|
|
6
6
|
|
|
7
7
|
|
|
@@ -38,7 +38,8 @@ export class SessionLog {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/** 返回对未写完末行的修复;其他损坏行抛错。 */
|
|
42
|
+
load(): ContextLoadRepair | null { return this.context.load(); }
|
|
42
43
|
|
|
43
44
|
/** 整体替换:先写临时文件,再 rename 覆盖目标文件。 */
|
|
44
45
|
reset(messages: readonly ContextRecord[]): void {
|
package/src/core/timers.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 通用持久定时器:到点回调持有方、跨重启恢复。载荷不透明——
|
|
3
3
|
* 闹钟的备注、阻断、关键词这些语义归持有它的Persona。
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, renameSync,
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import type { Logger, TimerEntry, TimersApi } from './types.ts';
|
|
8
8
|
import { nullLogger, shortId } from './util.ts';
|
|
@@ -47,7 +47,8 @@ export class TimerStore implements TimersApi {
|
|
|
47
47
|
const entry: TimerEntry = { id: shortId('wake_'), atIso, payload };
|
|
48
48
|
this.entries.push(entry);
|
|
49
49
|
this.save();
|
|
50
|
-
|
|
50
|
+
// 已到期的也等 set 返回后再回调,持有方先拿到 id。
|
|
51
|
+
if (this.started) this.arm(entry, 'next-tick');
|
|
51
52
|
return { ok: true, id: entry.id };
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -76,9 +77,10 @@ export class TimerStore implements TimersApi {
|
|
|
76
77
|
return n;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
/** 已到期的条目按 whenDue 立即回调或下一个宏任务回调;start() 用前者。 */
|
|
81
|
+
private arm(entry: TimerEntry, whenDue: 'now' | 'next-tick' = 'now'): void {
|
|
80
82
|
const delay = Date.parse(entry.atIso) - Date.now();
|
|
81
|
-
if (delay <= 0) {
|
|
83
|
+
if (delay <= 0 && whenDue === 'now') {
|
|
82
84
|
this.fire(entry.id);
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
@@ -88,7 +90,7 @@ export class TimerStore implements TimersApi {
|
|
|
88
90
|
// 超长延迟按 MAX_TIMEOUT 分段重新 arm。
|
|
89
91
|
this.timers.set(entry.id, setTimeout(() => this.arm(entry), MAX_TIMEOUT));
|
|
90
92
|
} else {
|
|
91
|
-
this.timers.set(entry.id, setTimeout(() => this.fire(entry.id), delay));
|
|
93
|
+
this.timers.set(entry.id, setTimeout(() => this.fire(entry.id), Math.max(0, delay)));
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -123,7 +125,7 @@ export class TimerStore implements TimersApi {
|
|
|
123
125
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
124
126
|
const tmp = this.file + '.tmp';
|
|
125
127
|
writeFileSync(tmp, JSON.stringify(this.entries, null, 2), 'utf8');
|
|
126
|
-
|
|
128
|
+
// 通过 rename 原子覆盖,不先删除,避免崩溃后定时器全部丢失。
|
|
127
129
|
renameSync(tmp, this.file);
|
|
128
130
|
}
|
|
129
131
|
}
|