Merge origin/master: scope-aware fusion of the tools/execute seam, session-prefix, and tool-cordis
Master brought 50 commits (the tool-cordis group, dsh-code-runtime + worker, the tools/execute around-dispatch seam + timeout-policy, repeat-tool-guard, agent/session-prefix, the ui reorganization). Beyond the ten textual conflicts, the merge reconciles master's new seams with this branch's scoped-registration world: - tools/execute (new waterfall around core dispatch): dispatched with the SAME exec.agent carrier as the pre/post waterfalls — an agent.ctx wrapper times/retries only its own agent's calls — and its base thunk resolves the tool through the caller's visible view (get(exec.name, exec.agent)), so a scoped/shadowed tool dispatches and a restricted-away global stays UNKNOWN_TOOL. Declared this: Scoped<ToolRegistry> with the scope-filtered doc sentence; invariants table + verify-scoped-dispatch pin it (21 events). - agent/session-prefix (new waterfall, once per loop instance): composed via the fused agentEvents dispatcher (scope-filtered like every agent-subject event), declared this: Scoped<Agent>, table-pinned. agent/pre-step keeps master's new sessionPrefix parameter with this branch's Scoped this. - timeout-policy reads the budget through the caller's visible view (get(exec.name, exec.agent)): a scoped tool's own timeoutMs governs its calls; a global name-twin's budget is never misapplied to a shadowing per-agent variant. - tool-cordis: cordis_inspect's tools section lists the CALLING agent's view (its description promises "what you can call"); the sandbox tool façade's reads resolve through the mount's own scope, mirroring where its register lands writes; sandboxRegisterTool's return type carries the exact-disposer union honestly. dsh-scope declared as peer+dev with the project reference. - doc-sync chain unions master's verify-cordis-api with this branch's verify-scoped-dispatch; the generated catalogs, event matrix (the zero-dispatcher guard passes over master's new events), module graph, and the cordis api-catalog are regenerated on the merged surface. Full gate sequence green on the merged tree: typecheck, lint, per-file 100% coverage (2668 tests), snapshots (38), doc-sync, module graph, build, hygiene, demo smoke.
This commit is contained in:
@@ -13,15 +13,23 @@
|
||||
*/
|
||||
|
||||
import { callConfigEquals } from '@deepseek-ai/dsh-llm'
|
||||
import type { LlmCallConfig, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import type { LlmCallConfig, Message, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import type { EpochHeader, SessionEvent, SystemDelta, ToolsDelta } from './types.ts'
|
||||
|
||||
/** The `request/header-delta` payload shape: each present field amends the folded header. */
|
||||
type HeaderDelta = {
|
||||
system?: SystemDelta
|
||||
tools?: ToolsDelta
|
||||
config?: LlmCallConfig
|
||||
messagePrefix?: Message[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a header to canonical form: an empty system prompt and an empty
|
||||
* tool list become ABSENT fields, matching how requests are built (both
|
||||
* request-build spreads skip empty values). Diff, fold, and comparison all
|
||||
* operate on canonical headers, so "no system prompt" has exactly one
|
||||
* representation.
|
||||
* Normalize a header to canonical form: an empty system prompt, an empty
|
||||
* tool list, and an empty session prefix become ABSENT fields, matching how
|
||||
* requests are built (the request-build spreads skip empty values). Diff,
|
||||
* fold, and comparison all operate on canonical headers, so "no system
|
||||
* prompt" (and "no session prefix") has exactly one representation.
|
||||
* @param header - the header to normalize (not mutated).
|
||||
* @returns the canonical header.
|
||||
*/
|
||||
@@ -30,6 +38,7 @@ export function canonicalHeader(header: EpochHeader): EpochHeader {
|
||||
config: header.config,
|
||||
...header.system !== undefined && header.system.length > 0 ? { system: header.system } : {},
|
||||
...header.tools !== undefined && header.tools.length > 0 ? { tools: header.tools } : {},
|
||||
...header.messagePrefix !== undefined && header.messagePrefix.length > 0 ? { messagePrefix: header.messagePrefix } : {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,37 +118,46 @@ function applyTools(prev: readonly ToolSchema[], delta: ToolsDelta): ToolSchema[
|
||||
* writer's round-trip guard runs (`applyHeaderDelta(prev, delta)` must equal
|
||||
* the intended header) and the loop runs to skip logging an unchanged header.
|
||||
* Tools compare per-schema IN ORDER (canonical JSON), so a pure reordering is
|
||||
* correctly unequal.
|
||||
* correctly unequal; the session prefix compares as canonical JSON (both
|
||||
* sides come from the same build path, so key order matches when the values
|
||||
* do).
|
||||
* @param a - one canonical header.
|
||||
* @param b - the other.
|
||||
* @returns whether config, system, and tools (in order) all match.
|
||||
* @returns whether config, system, tools (in order), and the session prefix all match.
|
||||
*/
|
||||
export function headerEquals(a: EpochHeader, b: EpochHeader): boolean {
|
||||
if (!callConfigEquals(a.config, b.config) || a.system !== b.system) return false
|
||||
if (!sameMessages(a.messagePrefix, b.messagePrefix)) 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))
|
||||
}
|
||||
|
||||
/** Canonical JSON equality over session-prefix arrays; absence equals the empty array. */
|
||||
function sameMessages(a: readonly Message[] | undefined, b: readonly Message[] | undefined): boolean {
|
||||
return JSON.stringify(a ?? []) === JSON.stringify(b ?? [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the `request/header-delta` payload between two canonical headers,
|
||||
* or undefined when they are equal. The caller MUST round-trip the result
|
||||
* ({@link applyHeaderDelta} on `prev` deep-equals `next`) before logging it —
|
||||
* the encoding cannot express every change (a pure tool reordering) — and
|
||||
* fall back to a full `request/header` snapshot when the check fails.
|
||||
* The session prefix is replaced whole (small advisory content, not worth
|
||||
* diffing); an empty replacement array encodes the transition to "none".
|
||||
* @param prev - the folded header the log currently implies.
|
||||
* @param next - the header the next request will actually use.
|
||||
* @returns the delta payload, or undefined when nothing changed.
|
||||
*/
|
||||
export function diffHeader(
|
||||
prev: EpochHeader, next: EpochHeader,
|
||||
): { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig } | undefined {
|
||||
const delta: { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig } = {}
|
||||
export function diffHeader(prev: EpochHeader, next: EpochHeader): HeaderDelta | undefined {
|
||||
const delta: HeaderDelta = {}
|
||||
if (prev.system !== next.system) delta.system = diffSystem(prev.system, next.system)
|
||||
const prevTools = prev.tools ?? []
|
||||
const nextTools = next.tools ?? []
|
||||
if (JSON.stringify(prevTools) !== JSON.stringify(nextTools)) delta.tools = diffTools(prevTools, nextTools)
|
||||
if (!callConfigEquals(prev.config, next.config)) delta.config = next.config
|
||||
if (!sameMessages(prev.messagePrefix, next.messagePrefix)) delta.messagePrefix = next.messagePrefix ?? []
|
||||
return Object.keys(delta).length > 0 ? delta : undefined
|
||||
}
|
||||
|
||||
@@ -151,15 +169,15 @@ export function diffHeader(
|
||||
* @param delta - the logged delta payload.
|
||||
* @returns the canonical header after the delta.
|
||||
*/
|
||||
export function applyHeaderDelta(
|
||||
prev: EpochHeader, delta: { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig },
|
||||
): EpochHeader {
|
||||
export function applyHeaderDelta(prev: EpochHeader, delta: HeaderDelta): EpochHeader {
|
||||
const system = delta.system !== undefined ? applySystem(prev.system, delta.system) : prev.system
|
||||
const tools = delta.tools !== undefined ? applyTools(prev.tools ?? [], delta.tools) : prev.tools
|
||||
const messagePrefix = delta.messagePrefix ?? prev.messagePrefix
|
||||
return canonicalHeader({
|
||||
config: delta.config ?? prev.config,
|
||||
...system !== undefined ? { system } : {},
|
||||
...tools !== undefined ? { tools } : {},
|
||||
...messagePrefix !== undefined ? { messagePrefix } : {},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
import type { CallId, ContentBlock, LlmCallConfig, MessageSource, StreamChunk, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import type { CallId, ContentBlock, LlmCallConfig, Message, MessageSource, StreamChunk, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
/** Identifies one session in the store (and its persistence artifacts). */
|
||||
export type SessionId = Branded<'SessionId'>
|
||||
@@ -183,14 +183,15 @@ export interface TodoItem {
|
||||
}
|
||||
|
||||
/**
|
||||
* The request header: everything about an LLM request besides its message
|
||||
* content — the call configuration plus the rendered system prompt and tool
|
||||
* schemas. Logged session state (the reconstructability RFC): a
|
||||
* The request header: everything about an LLM request besides its derived
|
||||
* message history — the call configuration plus the rendered system prompt,
|
||||
* tool schemas, and the session prefix. Logged session state (the
|
||||
* reconstructability RFC): a
|
||||
* {@link SessionEventMap} `request/header` snapshot installs one, a
|
||||
* `request/header-delta` amends it, and folding those events over the log
|
||||
* (`foldRequestHeader`) reconstructs the header any request was built under.
|
||||
* Canonical form: an empty system prompt and an empty tool list are ABSENT
|
||||
* fields, matching how requests are built.
|
||||
* Canonical form: an empty system prompt, an empty tool list, and an empty
|
||||
* prefix are ABSENT fields, matching how requests are built.
|
||||
*/
|
||||
export interface EpochHeader {
|
||||
/** The conversation's call configuration (model + sampling scalars). */
|
||||
@@ -199,6 +200,14 @@ export interface EpochHeader {
|
||||
system?: string
|
||||
/** Assembled tool schemas; absent for a tool-less request. */
|
||||
tools?: ToolSchema[]
|
||||
/**
|
||||
* The session prefix: request-only messages sent BEFORE the entire derived
|
||||
* history (the `agent/session-prefix` waterfall's product, composed once
|
||||
* per loop instance and reused for every request it sends). Not session
|
||||
* history — `deriveMessages()` never returns it — so the header is its
|
||||
* only durable record; absent when the instance composed none.
|
||||
*/
|
||||
messagePrefix?: Message[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,15 +365,21 @@ export interface SessionEventMap {
|
||||
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
|
||||
/**
|
||||
* Amendment to the folded {@link EpochHeader}: at least one of a
|
||||
* {@link SystemDelta}, a {@link ToolsDelta}, or a whole replacement
|
||||
* {@link LlmCallConfig} (four scalars — not worth diffing). Appended by the
|
||||
* {@link SystemDelta}, a {@link ToolsDelta}, a whole replacement
|
||||
* {@link LlmCallConfig} (four scalars — not worth diffing), or a whole
|
||||
* replacement session prefix (`messagePrefix` — small advisory content,
|
||||
* replaced whole; an EMPTY array encodes the transition to "none",
|
||||
* mirroring the canonical form's absent field — the loop never produces
|
||||
* one in practice: the prefix is composed once per instance and anchored
|
||||
* by that instance's snapshot, so this arm exists for codec totality).
|
||||
* Appended by the
|
||||
* loop inside the step, before dispatch, when the header for this request
|
||||
* differs from the fold of the log so far; the writer verifies
|
||||
* `applyHeaderDelta(previous, delta)` reproduces the new header exactly and
|
||||
* falls back to a `'fallback'` `request/header` snapshot when it cannot, so
|
||||
* a logged delta ALWAYS round-trips. NOT a {@link SurfaceEventType}.
|
||||
*/
|
||||
'request/header-delta': { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig }
|
||||
'request/header-delta': { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig; messagePrefix?: Message[] }
|
||||
}
|
||||
|
||||
/** The appendable event-type keys of {@link SessionEventMap}, plugin-merged extensions included. */
|
||||
|
||||
Reference in New Issue
Block a user