Merge pinned master into status bar projection

This commit is contained in:
Hypatia May
2026-07-31 09:28:52 +08:00
831 changed files with 23503 additions and 4750 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/session/README.md
README.md: 3c1f5531a2b301825959acd7be5e6cd115898bf0
README.zh.md: 28552c45d1de17f87f93948e272b6bee364494c2
README.md: 9c7d41901e6fb0133fff0e210260e5310a025f75
README.zh.md: ca1292289901a09b83f9b0a794fa4edc9754b1da

View File

@@ -64,7 +64,7 @@ Providers stream token-sized deltas, so a raw log stores hundreds of `assistant/
### Request-header reconstruction (`request-header.ts`)
`request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. See the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
`request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. Its optional `adapterDefaults` map marks effective `reasoningEffort` or `maxTokens` values materialized by exact-model resolution, allowing the next request proposal to distinguish them from explicit conversation settings. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. See the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
`request/context` records registration-bound metadata for the route a request resolved to, appended inside its step beside `request/header` and only when the provider, model, or capacity differs from the previous record. `session.requestContext()` folds the latest one incrementally, mirroring `requestHeader()`. Capacity stays OUT of `EpochHeader` on purpose: it is adapter metadata describing a route, not an input the request was built from, so it must not enter request reconstruction or header equality — a capacity change is not a header `change`. A route whose adapter advertises no capacity is still recorded with `contextWindow` absent, clearing any older known capacity.

View File

@@ -64,7 +64,7 @@
### 请求头重建(`request-header.ts`
`request/header` 记录非历史请求封装的完整规范快照,其原因为 `initial``resume``change``foldRequestHeader()` 选择最新快照;旧版增量事件和已移除的 `fallback` 原因会被拒绝。详见[可重建请求 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
`request/header` 记录非历史请求封装的完整规范快照,其原因为 `initial``resume``change`其可选 `adapterDefaults` 映射会标记由精确模型解析填入的生效 `reasoningEffort``maxTokens` 值,使下一次请求提议能够将它们与显式对话设置区分开。`foldRequestHeader()` 选择最新快照;旧版增量事件和已移除的 `fallback` 原因会被拒绝。详见[可重建请求 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
`request/context` 记录请求所解析到的路由的、绑定注册项的元数据,在其所属步骤内紧随 `request/header` 追加,且仅在提供方、模型或容量与上一条记录不同时追加。`session.requestContext()` 以增量方式归并最新一条,与 `requestHeader()` 保持一致。容量刻意不进入 `EpochHeader`:它是描述路由的适配器元数据,不是构建该请求所依据的输入,因此绝不可进入请求重建或请求头相等性判断:容量变化不构成请求头 `change`。适配器不公布容量的路由仍会被记录,但 `contextWindow` 字段缺失,从而清除较早的已知容量。

View File

@@ -201,13 +201,18 @@ function assertCurrentLlmShape(event: Record<string, unknown>, index: number): v
: undefined
if (event['type'] === 'request/header') {
const header = record?.['header']
const config = typeof header === 'object' && header !== null ? (header as Record<string, unknown>)['config'] : undefined
const headerRecord = typeof header === 'object' && header !== null && !Array.isArray(header)
? header as Record<string, unknown>
: undefined
const config = headerRecord?.['config']
if (!hasProviderModel(config)) throw new Error(`seed request/header at index ${index} lacks provider/model`)
const reasoningEffort = (config as Record<string, unknown>)['reasoningEffort']
const configRecord = config as Record<string, unknown>
const reasoningEffort = configRecord['reasoningEffort']
if (reasoningEffort !== undefined
&& (typeof reasoningEffort !== 'string' || reasoningEffort.length === 0)) {
throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`)
}
assertAdapterDefaults(headerRecord?.['adapterDefaults'], configRecord, index)
}
const type = event['type']
if (type !== 'user/message' && type !== 'assistant/message'
@@ -215,6 +220,26 @@ function assertCurrentLlmShape(event: Record<string, unknown>, index: number): v
assertMessageEventShape(event, `seed ${type} at index ${index}`)
}
/** Validate adapter-default provenance imported from a durable request header. */
function assertAdapterDefaults(
value: unknown,
config: Record<string, unknown>,
index: number,
): void {
if (value === undefined) return
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`)
}
const defaults = value as Record<string, unknown>
const allowed = new Set(['reasoningEffort', 'maxTokens'])
if (Object.keys(defaults).some(key => !allowed.has(key))
|| Object.values(defaults).some(marker => marker !== true)
|| defaults['reasoningEffort'] === true && config['reasoningEffort'] === undefined
|| defaults['maxTokens'] === true && config['maxTokens'] === undefined) {
throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`)
}
}
/** Validate only the event-specific invariants needed to safely replay a message. */
function assertMessageEventShape(event: Record<string, unknown>, subject: string): void {
const type = event['type']

View File

@@ -19,8 +19,12 @@ import type { EpochHeader, SessionEvent } from './types.ts'
* @returns the canonical header.
*/
export function canonicalHeader(header: EpochHeader): EpochHeader {
const adapterDefaults = header.adapterDefaults
return {
config: header.config,
...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true
? { adapterDefaults }
: {},
...header.system !== undefined && header.system.length > 0 ? { system: header.system } : {},
...header.tools !== undefined && header.tools.length > 0 ? { tools: header.tools } : {},
}
@@ -38,7 +42,12 @@ function sameSchema(a: ToolSchema, b: ToolSchema): boolean {
* @returns whether config, system, and tools all match.
*/
export function headerEquals(a: EpochHeader, b: EpochHeader): boolean {
if (!callConfigEquals(a.config, b.config) || a.system !== b.system) return false
if (
!callConfigEquals(a.config, b.config)
|| a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort
|| a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens
|| a.system !== b.system
) return false
const at = a.tools ?? []
const bt = b.tools ?? []
return at.length === bt.length && at.every((tool, i) => sameSchema(tool, bt[i] as ToolSchema))

View File

@@ -3,6 +3,7 @@ import type {
AssistantMessage,
CallId,
LlmCallConfig,
LlmCallConfigAdapterDefaults,
LlmFailure,
MessageSource,
StreamChunk,
@@ -163,6 +164,8 @@ export interface TodoItem {
export interface EpochHeader {
/** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
config: LlmCallConfig
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
adapterDefaults?: LlmCallConfigAdapterDefaults
/** Rendered system prompt text; absent for a system-less request. */
system?: string
/** Assembled tool schemas; absent for a tool-less request. */

View File

@@ -14,9 +14,24 @@ function tool(name: string, description = 'd'): ToolSchema {
describe('canonicalHeader', () => {
it('normalizes empty optional fields to absence and preserves populated fields', () => {
expect(canonicalHeader({ config: CONFIG, system: '', tools: [] })).toEqual({ config: CONFIG })
const full = canonicalHeader({ config: CONFIG, system: 's', tools: [tool('a')] })
expect(full).toEqual({ config: CONFIG, system: 's', tools: [tool('a')] })
expect(canonicalHeader({
config: CONFIG,
adapterDefaults: {},
system: '',
tools: [],
})).toEqual({ config: CONFIG })
const full = canonicalHeader({
config: { ...CONFIG, maxTokens: 256_000 },
adapterDefaults: { maxTokens: true },
system: 's',
tools: [tool('a')],
})
expect(full).toEqual({
config: { ...CONFIG, maxTokens: 256_000 },
adapterDefaults: { maxTokens: true },
system: 's',
tools: [tool('a')],
})
})
})
@@ -30,6 +45,14 @@ describe('headerEquals', () => {
...base,
config: { ...base.config, reasoningEffort: ReasoningEffortId('high') },
})).toBe(false)
expect(headerEquals(
{ ...base, config: { ...base.config, maxTokens: 256_000 } },
{
...base,
config: { ...base.config, maxTokens: 256_000 },
adapterDefaults: { maxTokens: true },
},
)).toBe(false)
expect(headerEquals(base, { ...base, system: 'other' })).toBe(false)
expect(headerEquals(base, { ...base, tools: [] })).toBe(false)
expect(headerEquals(base, { ...base, tools: [tool('a', 'changed')] })).toBe(false)

View File

@@ -403,6 +403,40 @@ describe('Session', () => {
}
})
it('round-trips adapter-default provenance and rejects invalid durable values', () => {
const valid = {
type: 'request/header',
seq: 0,
time: 1,
data: {
header: {
config: {
provider: 'mock',
model: 'model',
maxTokens: 256_000,
},
adapterDefaults: { maxTokens: true },
},
reason: 'initial',
},
} as const
expect(new Session(SessionId('adapter-defaults'), [valid]).events[0]).toEqual(valid)
for (const adapterDefaults of [
null,
[],
{ unknown: true },
{ maxTokens: false },
{ reasoningEffort: true },
]) {
const invalid = structuredClone(valid) as unknown as SessionEvent
if (invalid.type !== 'request/header') throw new Error('test fixture must be a request header')
invalid.data.header.adapterDefaults = adapterDefaults as never
expect(() => new Session(SessionId('invalid-adapter-defaults'), [invalid]))
.toThrow('seed request/header at index 0 has invalid adapterDefaults')
}
})
it('isolates the log from mutation through a derived message (append-only contract)', () => {
const session = new Session(SessionId('s4'))
session.append('user/message', createUserMessage({