Files
dsh-context/src/shared/types.ts
T
bowenliang123 d75b539419 feat: system prompt prefix/suffix settings joined into every session
Two new per-user preferences, `promptPrefix`/`promptSuffix`, hold free text
joined at the very start / end of the harness's assembled system prompt for
all agents (subagents included) — a per-deployment persona or behavioral rule
riding the harness's own prompt registry instead of session-log surgery:

- src/host/promptInjection.ts registers two global prompt sections through a
  deferred `ctx.inject(['systemPrompt'], …)` (a composition without the
  registry degrades to a no-op; the registration unwinds with this plugin's
  fiber). Orders anchor on the harness's own section-order registry, so a
  central renumbering keeps the prefix first and the suffix last. Text is
  registered `interpolate: false`, so `{{...}}` in user text stays literal.
- The section text providers read the volatile config references the loader
  rewrites in place on preference edits, so an edit applies from the next
  turn without remounting the entry; new sessions carry the text from their
  first turn through the harness's own system-prompt projection.
- The settings card grows two free-text rows (local draft, committed on blur
  and on unmount; a host push never clobbers in-progress typing), zh/en
  strings, and the served-value sanitize/degrade path for the string fields.

Verified against a running harness: the committed text persists in the
profile layer, and a new session's system prompt starts with the prefix and
ends with the suffix. docs/compatibility.md documents the seams, and the
compat matrix probes them on both supported lines.
2026-10-11 13:55:14 +08:00

537 lines
22 KiB
TypeScript

/** Shared wire contract — the snapshot model exchanged between the Host and Client halves: the
* `view()` payloads of the `contextTimeline` / `contextHeaders` / `contextActivity` session projections. */
import type { ActivityState } from '../host/activity'
import type { HeadersState } from '../host/headers'
import type { TimelineState } from '../host/foldState'
// The registry package ROOT carries the `@deepseek-ai/cordis` Context augmentation
// (`sessionProjections` service); the `/types` subpath only declares the merge-extensible maps.
import type {} from '@deepseek-ai/dsh-session-projection'
import type {} from '@deepseek-ai/dsh-session-projection/types'
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionMap {
/** The plugin's timeline: slim head on the split generation, inline value on channel-less
* hosts; key absence = the host half is not composed. */
contextTimeline: ContextTimeline
/** Request-header content epochs — a separate unit so the hot timeline value stays lean;
* key absence = an older host (tokens only). */
contextHeaders: ContextHeaders
/** The per-day activity ledger behind the Context Insights heatmap; key absence = an older host (empty heatmap). */
contextActivity: ContextActivity
}
interface SessionProjectionStateMap {
contextTimeline: TimelineState
contextHeaders: HeadersState
contextActivity: ActivityState
}
}
/** The priced surface buckets. `skill` carries the skill-machinery content the harness injects:
* the `<available_skills>` digest, a `/name` invocation's instructions, and a `skill`-tool load. */
export type Category = 'user' | 'inject' | 'skill' | 'assistant' | 'tool'
/** One live system-prompt node; the harness models the prompt as a surface node, so its TEXT is
* fetched from the `system/message` event at `seq`. The effective figure is the LAST node with `tokens > 0`. */
export interface SystemPromptNode {
seq: number
time: number
tokens: number
}
/** Counts precomputed over the RETAINED request/event records; `steps` is the retained request-record count. */
export interface TimelineCounts {
turns: number
steps: number
injects: number
compactions: number
prunes: number
/** Distinct names among the retained events tagged `sub: 'skill'`; the `<available_skills>`
* digest has no name and never counts. Absent = 0. */
skills?: number
}
/** The newest retained request record's billing summary. */
export interface TimelineLast {
seq: number
total: number
prompt?: number
}
export interface ActivityDay {
/** Billed tokens from provider-reported usage that day (input + cache read/write + output); a
* settlement-less request counts only toward `requests`. */
tokens: number
/** Completed model calls (assistant settlements) that day. */
requests: number
/** The day's billed buckets keyed provider → model → pricing period, scoped to the day the
* requests INITIATED in. Absent = tokens-only bars. */
cost?: SessionCostUsage
/** Skill name → `{ n: loads, last: last-load instant in epoch ms }`; absent = an empty day. */
skills?: Record<string, { n: number; last: number }>
}
/** The per-session daily ledger: host-local day key (`YYYY-MM-DD`, shared/days.ts) → that day's
* billed volume; the Context Insights page merges every listed session's ledger. */
export interface ContextActivity {
days: Record<string, ActivityDay>
}
/** One available skill's catalog metadata, served by `/api/dsh-context/skills` (host/skills.ts); every field but `name` is best-effort. */
export interface SkillInfo {
/** Kebab-case identifier — the join key onto `ActivityDay.skills`. */
name: string
description: string
/** Absolute instruction file path, when the provider is filesystem-backed. */
path?: string
/** Discovery source bucket (e.g. 'project-agents', 'user-dsh', 'bundled'). */
source?: string
}
/** The durable `subagent/descriptor` identity of one agent session, as the Fleet detail route serves it. */
export interface FleetDescriptor {
mode: 'one-shot' | 'continuable'
label?: string
/** The `ctx.subagents` provider name that established the child ('spawn' / 'fork' / …). */
provider?: string
agentProvider?: string
agentModel?: string
persona?: string
}
/** One inter-agent message lifted from a session log: a team relay in the recipient's log
* (`agent-message`), a historical mailbox delivery (`team-message`), or the runtime's account of a
* child settling in the parent's log (`subagent-settled`). */
export interface FleetMessage {
seq: number
/** Event instant (epoch ms). */
time: number
kind: 'relay' | 'mailbox' | 'settled'
/** Sender session id, when the source attests one. */
from?: string
/** The historical mailbox's sender NAME (no id join needed). */
fromName?: string
/** Text excerpt, already capped by the route. */
text: string
}
/** One roster fact lifted from a lead session's durable `team/member` records — the projection
* strips these fields, so the Fleet detail route serves them for the member cards. */
export interface FleetMemberInfo {
name: string
/** The delegation's own role summary, authored at spawn. */
description: string
/** The `ctx.subagents` provider that established the member ('spawn' / 'fork'). */
provider?: string
context?: 'fresh' | 'fork'
}
/** The Fleet detail route's per-session payload (`/api/dsh-context/fleet`): the agent's delegation
* prompt, its descriptor composition, its latest reply, and the inter-agent traffic its own log
* attests. A LEAD session additionally serves its roster facts. */
export interface FleetDetail {
descriptor?: FleetDescriptor
/** The first non-injection user message — a child's delegation briefing, a root's first human prompt. */
initialPrompt?: string
/** The newest assistant text — the agent's latest report-back. */
lastReply?: { time: number; text: string }
/** Roster facts off `team/member` records (lead logs only), keyed by member name. */
roster?: FleetMemberInfo[]
/** Newest-first inter-agent messages found in this session's log. */
messages: FleetMessage[]
/** True when the route's caps cut the message list or a text. */
truncated: boolean
}
/** The display-preference vocabulary of the `dsh-context` entry — the ONE declaration shared by
* the Host Config schema (host/config.ts) and the Client's settings form (client/settings.ts). */
export type DefaultGranularity = 'step' | 'turn'
export type DefaultTrendMode = 'total' | 'delta'
/** The browser delta baseline: against the immediately preceding record, or the previous turn's last step. */
export type DefaultDeltaBase = 'step' | 'turn'
export type DefaultFileSort = 'count' | 'latest' | 'path'
export type DefaultToolSort = 'size' | 'count' | 'name'
export type DefaultPlacement = 'all' | 'tab' | 'sidebar'
export type InsightsEntry = 'show' | 'hide'
export type DefaultDurationCurve = 'show' | 'hide'
/** Whether the Fleet conversation tab is served. */
export type FleetTab = 'show' | 'hide'
export interface PluginSettings {
defaultPlacement: DefaultPlacement
defaultGranularity: DefaultGranularity
defaultTrendMode: DefaultTrendMode
defaultDeltaBase: DefaultDeltaBase
defaultToolSort: DefaultToolSort
defaultFileSort: DefaultFileSort
insightsEntry: InsightsEntry
defaultDurationCurve: DefaultDurationCurve
fleetTab: FleetTab
/** Free text joined ahead of the harness's assembled system prompt; empty disables. */
promptPrefix: string
/** Free text joined behind the harness's assembled system prompt; empty disables. */
promptSuffix: string
}
export type SettingsField = keyof PluginSettings
export interface Snapshot {
ok: boolean
/** Present ONLY when the running harness is below the supported baseline: the host folds
* nothing (all figures zero) and the client shows the upgrade gate. */
unsupported?: {
current: string
minimum: string
}
model?: string
provider?: string
contextWindow?: number
current: {
system: number
tools: number
user: number
inject: number
skill: number
assistant: number
tool: number
total: number
}
/** Image blocks in the CURRENT context (uploads plus nested tool-result images), summed over live surface nodes. Absent = 0. */
images?: number
/** Tool calls whose result is live (a `skill`-tool node keeps counting); in-flight and
* compacted/pruned-out calls are excluded. Absent = 0. */
toolCalls?: number
/** Whole-session tally of non-injection `user/message` plus answered `ask_user_question` — a
* running total over the COMPLETE log. Absent = 0. */
humanInputs?: number
/** Whole-session replies: one per assistant message carrying non-blank text (tool-only steps are not replies). Absent = 0. */
answers?: number
/** The newest own message as a one-line preview (first text block, whitespace collapsed, 80 chars max). Absent = hidden. */
lastUser?: string
/** Split-generation head fields — present exactly when the host serves the slim head;
* `detailRev` bumps whenever the detail collections change. */
counts?: TimelineCounts
last?: TimelineLast
detailRev?: number
/** The slim head serves these EMPTY (the collections ride the detail channel); the client swaps in the detail payload's collections. */
requests: RequestRecord[]
events: ContextEventRecord[]
/** Cumulative session-cost raw material (SessionCostUsage); absent until a request with a known model reports usage. */
cost?: SessionCostUsage
/** Whole-session timing totals (TimingTotals). Absent until the first step lifecycle completes. */
timing?: TimingTotals
/** The live system-prompt nodes, oldest first. Absent on rows folded before the field existed
* — the client then reads the header epoch's `systemTokens`. */
systems?: SystemPromptNode[]
/** The served live surface: the newest `maxNodes` tail PLUS every live inject/skill node older
* than the tail (they land first and are pinned). Seq-ordered, oldest first. */
nodes: SurfaceNode[]
/** Live nodes not served: the overflow beyond `maxNodes` minus the pinned inject/skill nodes. */
droppedNodes: number
/** Recently REMOVED surface nodes (compaction/prune shadows) stamped with `gone` (the replacing
* event's seq); a node belongs to request R when `seq < R.seq && (gone undefined || gone > R.seq)`. */
archive: SurfaceNode[]
/** Newest seq among the `droppedNodes` nodes not served; present only when droppedNodes > 0. */
surfaceFloor?: number
/** Newest `gone` among retention-dropped archive entries; steps below it may miss removed nodes. */
archiveFloor?: number
/** The fold-derived file-op log and trim floor: the INLINE value and detail payload, never the slim head. */
fileOps?: FileOpRecord[]
fileOpsFloor?: number
/** The timing strip's painted spans: INLINE value and detail payload only; absent on rows folded before the collection existed. */
spans?: TimingSpan[]
}
/** The on-demand DETAIL payload of the split generation: the heavy collections the slim head no
* longer carries (host/detail.ts). `rev` mirrors the head's `detailRev` and is the client's latest-wins cursor. */
export interface ContextTimelineDetail {
rev: number
/** The slim head at the SAME fold cut as the collections — a session listed cold has no
* `contextTimeline` row, so the Agent network ring reads this. Optional. */
head?: ContextTimeline
requests: RequestRecord[]
events: ContextEventRecord[]
nodes: SurfaceNode[]
droppedNodes: number
archive: SurfaceNode[]
surfaceFloor?: number
archiveFloor?: number
/** The file-op log (shared/fileOps.ts) over the FULL log; Code-Mode nested dispatches book on their parent run_code result (`parent`). */
fileOps?: FileOpRecord[]
/** The newest dropped op's seq when the op log trimmed (coverage honesty, same family as archiveFloor). */
fileOpsFloor?: number
/** The completed steps' time slices in log order, positioned by their real instants. Absent = no strip. */
spans?: TimingSpan[]
}
/** One executed file operation, folded host-side from the durable tool lifecycle (`tool/call` +
* `tool/result`, or a nested `tool/ptc-dispatch`); line deltas are estimates read off the call
* ARGUMENTS. `gone` is client-joined, not host-stamped. */
export interface FileOpRecord {
seq: number
/** The op's file; for a pathless search the searched PATTERN (`pattern: true`). */
path: string
kind: 'read' | 'write' | 'search'
tool: string
time?: number
err: boolean
added: number
removed: number
/** The searched pattern, with the include filter when one narrowed the call. */
detail?: string
/** Meta-attributed search op only: matched lines the result reported for this file. */
hits?: number
/** Read ops only: the exact 1-based window the result meta reported, else the `limit`-argument estimate (`est: true`). */
read?: { start: number; count: number } | { count: number; est: true }
/** Nested Code-Mode op only: the run_code result node the op ran under (the locate target). */
parent?: number
/** Nested Code-Mode op only: the run_code program's model-authored description. */
program?: string
/** The searched-pattern marker: `path` is a pattern, not a file — display must not relativize it. */
pattern?: true
/** Client-joined archive stamp (see the type note); absent on the wire. */
gone?: number
}
/** The `contextTimeline` projection's whole value; `ok` is always `true` here (kept for wire compatibility). */
export type ContextTimeline = Snapshot
/** The official token-meter `contextPressure` projection: the provider-anchored occupancy of the
* NEXT request; fields are independent last-wins records. Absent = the meter is not composed (the
* client then falls back to its derived anchor). */
export interface ContextPressure {
/** Provider-reported prompt size of the most recent request (input + cache). */
pressureTokens?: number
/** pressureTokens + heuristic surface movement since the sample (clamped ≥ 0). */
projectedTokens?: number
contextWindow?: number
}
/** The official token-meter `contextBreakdown` projection: the heuristic composition rows the chat
* ring's panel shows. Read directly so the card's counts stay identical to the panel's; the message
* bucket splits into the five message-bearing surface categories. Absent = an older harness. */
export interface ContextBreakdown {
systemTokens: number
toolsTokens: number
messageTokens: number
}
/** The official token-meter `tokenUsage` projection: durable cumulative provider-reported usage
* across the COMPLETE log; the four buckets are disjoint (reasoning sits inside `outputTokens`). */
export interface TokenUsage {
uncachedInputTokens: number
outputTokens: number
cacheReadTokens: number
cacheWriteTokens: number
}
/** One painted span of the timing card's session-time strip (a step's TTFT wait, decode block,
* tool-run window, or residue gap). `start`/`end` are the slice's REAL instants; the strip packs
* them gapless in log order, and `end` > `start` always. */
export interface TimingSpan {
/** The timing card's own slice vocabulary (same keys, same colors). */
kind: 'ttft' | 'reasoning' | 'text' | 'toolarg' | 'tools' | 'other'
start: number
end: number
}
/** One pricing bucket's cumulative billed tokens: a running total over the COMPLETE session log, never trimmed. */
export interface CostBucketTotals {
uncached: number
cacheRead: number
cacheWrite: number
output: number
}
export interface ToolTimingTotals {
calls: number
ms: number
}
/**
* Whole-session timing totals, host-folded from the durable step/tool lifecycle plus the call's
* first token (its embedded `assistant/message.data.stream`); running totals over the COMPLETE log.
*
* Milliseconds: `wallMs` sums whole steps; `ttftMs` the step-start → decode-start slice and `genMs`
* the decode-start → assistant-message slice (only over calls whose stream carried a token delta);
* `toolsMs` sums per-call tool durations (parallel calls each count). The decode window opens at the
* first OBSERVABLE instant — the first token, or an earlier `block-start` marker (a redacted
* reasoning block leaves no chunk): anchoring at the token would double-count the marker-tiled
* window past 100%.
*
* The generation split comes off the stream's `block-start` framing (`blockType`): `reasoningMs` /
* `textMs` / `toolArgMs` and their block counts, each marker owning the interval to the next.
* `speedTokens`/`speedMs` are the harness-parity throughput seat (first-token → assistant-message,
* calls carrying both); `speedMs` KEEPS the first-token anchor. `calls` counts folded assistant
* messages, `toolCalls` folded call/result pairs.
*/
export interface TimingTotals {
wallMs: number
ttftMs: number
genMs: number
reasoningMs?: number
reasoningBlocks?: number
textMs?: number
textBlocks?: number
toolArgMs?: number
toolArgBlocks?: number
speedTokens?: number
speedMs?: number
calls: number
toolsMs: number
toolCalls: number
/** Per-tool-name tallies behind the timing card's ranking (bounded). */
tools: Record<string, ToolTimingTotals>
}
/** One billed model's totals split by pricing period: providers without period pricing book
* everything under `peak`; DeepSeek bills `peak` at list price and `off` at half. */
export interface CostModelUsage {
peak?: CostBucketTotals
off?: CostBucketTotals
}
/** The session-cost raw material: cumulative provider-reported billed tokens keyed by the request
* envelope's DSH provider id ('' when none) then model id — the faces the Client's models.dev price book resolves. */
export interface SessionCostUsage {
[provider: string]: { [model: string]: CostModelUsage }
}
/** One model-visible message on the surface, with its heuristic token price. */
export interface SurfaceNode {
seq: number
time?: number
cat: Category
tokens: number
/** Image blocks inside this node's message (absent when zero). */
imgs?: number
/** Set only on `archive` entries: the seq of the replacement event that shadowed this node. */
gone?: number
form?: string
/** The producer identity the matching inject event names (host pricing.ts
* `injectionSourceName`); absent when unreadable or predating the stamp. */
name?: string
text?: string
tool?: string
err?: boolean
skill?: string
calls?: string[]
}
/** One answered model call (a step); consecutive records of one turn form it. */
export interface RequestRecord {
turn?: number
step?: number
time: number
seq: number
system: number
tools: number
user: number
inject: number
assistant: number
tool: number
total: number
prompt?: number
/** Skill-machinery tokens of this request (the `skill` composition category); absent on rows
* folded before the category existed (read as 0). */
skill?: number
/** Billed cache-read (served) prompt tokens — the hit-rate numerator against `prompt`; absent
* on older hosts, and zero is a real value. */
cacheRead?: number
output?: number
/** The step's ACTIVE ms (`step/start` → `step/end` minus in-step user waits: approval decisions
* and `ask_user_question`), stamped at `step/end`; absent while the step is open. */
activeMs?: number
/** Turn-mode aggregate marker, set by the Client's aggregateByTurn (the Host never sets it). */
stepCount?: number
/** Delta-mode signed net change, set by the Client's deltaOf (the Host never sets it). */
net?: number
}
export interface ContextEventRecord {
seq: number
time: number
kind: 'compaction' | 'prune' | 'inject' | 'model' | 'mode'
form?: string
tokens?: number
count?: number
sub?: string
name?: string
detail?: string
from?: string
to?: string
/** Turn/step of the request logged right BEFORE the event (host-stamped). */
fromTurn?: number
fromStep?: number
/** Turn/step of the request this event contributed to (host-stamped). */
turn?: number
step?: number
}
/** Sentinel `HeaderTool.plugin` value marking a tool whose provider could not be attributed (it was
* already registered when this plugin's attribution hook installed); no real plugin name collides. */
export const UNKNOWN_TOOL_SOURCE = '<unknown-plugin>'
/** One tool of a request-header epoch, with its display price. */
export interface HeaderTool {
name: string
tokens: number
/** The registering plugin's label when known: `mcp:<server>` for MCP tools, or the pinned
* first-party map; `UNKNOWN_TOOL_SOURCE` marks a boot-time tool, absent = no tag. */
plugin?: string
}
/** One request-header epoch's METADATA (epoch boundaries and token prices, in force from this seq
* until the next). The CONTENT is deliberately not projected — every list row and push frame would
* carry it — and is fetched on demand as a {@link HeaderEpochContent}. */
export interface HeaderRecord {
seq: number
time: number
/** The epoch's estimated system-prompt tokens; absent when it logged no system prompt. */
systemTokens?: number
tools: HeaderTool[]
}
/** The `contextHeaders` projection value: the bounded epoch list (newest last). */
export interface ContextHeaders {
headers: HeaderRecord[]
}
/** The fetched CONTENT of one request-header epoch, mapped client-side off the raw durable event
* (historyPage.ts); tool identity joins the metadata by `name`. */
export interface HeaderEpochContent {
system?: string
tools: Array<{
name: string
description?: string
schema?: unknown
}>
}
/** One currency's DeepSeek open-platform balance figures. */
export interface PlatformBalanceEntry {
/** The ISO code the platform reported (`CNY` / `USD`). */
currency: string
/** `granted` + `toppedUp`, derived rather than read from the platform's `total_balance`, which rounds independently of its parts. */
total: number
/** The not-expired granted (gift) balance. */
granted: number
toppedUp: number
}
/** The DeepSeek open-platform balance, served by `/api/dsh-context/balance`; `null` on the wire
* (nothing rendered) when unconfigured or the read fails. */
export interface PlatformBalance {
isAvailable: boolean
/** One entry per currency the account holds; at least one. */
balances: PlatformBalanceEntry[]
}