refactor(web): consume owner remote events directly
This commit is contained in:
@@ -41,7 +41,7 @@ The child records the joined id on its own durable header ([`dsh-subagent`](../.
|
||||
|
||||
The creation header names the preset a session STARTED with; `resolveSessionPreset(session)` names the one it RUNS. They differ whenever a blank session switched, so every reconstruction path — the summary a picker reads, a resume, a fork — resolves rather than reading the header.
|
||||
|
||||
The header stays frozen because it is a creation fact. A switch is an `agent-preset/selected` session event appended after the swap commits, which is what the model-visible ⟺ logged rule requires: the preset decides the tool schemas and prompt sections the model sees, so it has to be reconstructable from the log. Reading the header alone would rebuild a switched session under the composition it was created with, replaying history the new tool set cannot act on — the exact hazard the blank-only lock exists to prevent.
|
||||
The header stays frozen because it is a creation fact. A switch is an `agent-preset/selected` session event appended after the swap commits, which is what the model-visible ⟺ logged rule requires: the preset decides the tool schemas and prompt sections the model sees, so it has to be reconstructable from the log. The service re-emits that committed fact as the non-scoped cordis event `agent-preset/selected(sessionId, agentPreset)` declared by the client-safe `./types` export, allowing remote consumers to invalidate session-derived state without importing Host runtime types. Reading the header alone would rebuild a switched session under the composition it was created with, replaying history the new tool set cannot act on — the exact hazard the blank-only lock exists to prevent.
|
||||
|
||||
### Switching a blank agent
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ subagent 的子 agent 通过 `composeFrom()` 加入其父方的常驻组装,
|
||||
|
||||
创建头部记录的是会话**以什么开始**,`resolveSessionPreset(session)` 给出的才是它**实际运行的**。空白会话一旦切换过,两者就不同,因此所有重建路径——选择器读取的摘要、resume、fork——都走解析,而非直接读头部。
|
||||
|
||||
头部保持冻结,因为它是创建期事实。切换以 `agent-preset/selected` 会话事件记录,在替换提交之后追加;这正是 model-visible ⟺ logged 规则的要求:preset 决定模型看到的工具 schema 与提示词段落,因此必须能从日志重建。只读头部会让切换过的会话按创建时的组装重建,从而重放新工具集无法执行的历史——这正是「仅空白可切」那道锁要防的危险。
|
||||
头部保持冻结,因为它是创建期事实。切换以 `agent-preset/selected` 会话事件记录,在替换提交之后追加;这正是 model-visible ⟺ logged 规则的要求:preset 决定模型看到的工具 schema 与提示词段落,因此必须能从日志重建。服务会把这项已提交事实重新发为不带 scope 的 cordis 事件 `agent-preset/selected(sessionId, agentPreset)`,其声明位于 client-safe 的 `./types` 出口,使远端消费方无需导入 Host 运行时类型即可让会话派生状态失效。只读头部会让切换过的会话按创建时的组装重建,从而重放新工具集无法执行的历史——这正是「仅空白可切」那道锁要防的危险。
|
||||
|
||||
### 切换空白 agent
|
||||
|
||||
|
||||
@@ -22,12 +22,17 @@
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./lib/types/types.d.ts",
|
||||
"default": "./lib/types/types.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.js",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
|
||||
@@ -17,7 +17,7 @@ import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
|
||||
import { expandHomePath } from '@deepseek-ai/dsh-paths'
|
||||
import { METADATA_FILE, renderPresetMetadata } from './metadata.ts'
|
||||
import { PRESET_ID, type AgentPreset, type PresetRoot } from './types.ts'
|
||||
import { PRESET_ID, type AgentPreset, type PresetRoot } from './preset.ts'
|
||||
|
||||
/** A preset id that cannot be used as a directory name under a root. */
|
||||
export class InvalidPresetIdError extends Error {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { load } from 'js-yaml'
|
||||
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { expandHomePath } from '@deepseek-ai/dsh-paths'
|
||||
import { readPresetMetadata } from './metadata.ts'
|
||||
import { PRESET_ID, type AgentPreset, type PresetRoot } from './types.ts'
|
||||
import { PRESET_ID, type AgentPreset, type PresetRoot } from './preset.ts'
|
||||
|
||||
/** The composition file that makes a directory a preset. */
|
||||
export const COMPOSITION_FILE = 'agent.cordis.yml'
|
||||
|
||||
@@ -32,7 +32,8 @@ import { discoverPresets } from './discovery.ts'
|
||||
import { copyComposition, deleteComposition, readComposition } from './authoring.ts'
|
||||
import { mountPreset, serviceForAgent, standingMountFor } from './mount.ts'
|
||||
import { PresetExistsError } from './authoring.ts'
|
||||
import { PresetMountError, UnknownPresetError, type AgentPreset, type Config } from './types.ts'
|
||||
import { PresetMountError, UnknownPresetError, type AgentPreset, type Config } from './preset.ts'
|
||||
import type {} from './types.ts'
|
||||
|
||||
/** Settings namespace carrying the user's chosen default preset. */
|
||||
export const SETTINGS_NAMESPACE = 'agent-presets'
|
||||
@@ -61,8 +62,8 @@ export {
|
||||
PresetNotWritableError, readComposition, writableRoot,
|
||||
} from './authoring.ts'
|
||||
export { resolveSessionPreset, type PresetBearingSession } from './session.ts'
|
||||
export { PresetMountError, UnknownPresetError } from './types.ts'
|
||||
export type { AgentPreset, Config, PresetRoot, PresetTrust } from './types.ts'
|
||||
export { PresetMountError, UnknownPresetError } from './preset.ts'
|
||||
export type { AgentPreset, Config, PresetRoot, PresetTrust } from './preset.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
@@ -154,6 +155,13 @@ export class AgentPresets extends Service {
|
||||
+ '(join through AgentPresets.mount() or composeFrom() in the agent factory setup)',
|
||||
)
|
||||
})
|
||||
|
||||
// The durable record is the commit point. Its public notification carries
|
||||
// only the stable identity needed by clients, never the live Session.
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (event.type !== 'agent-preset/selected') return
|
||||
ctx.emit('agent-preset/selected', session.id, event.data.agentPreset)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Context, type Fiber } from '@deepseek-ai/cordis'
|
||||
import { Include } from '@deepseek-ai/cordis-plugin-include'
|
||||
import type { EntryTree } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { scopeOf, scopeParentOf, type ScopeKey } from '@deepseek-ai/dsh-scope'
|
||||
import { PresetMountError, type AgentPreset } from './types.ts'
|
||||
import { PresetMountError, type AgentPreset } from './preset.ts'
|
||||
|
||||
/** What one mounted subtree publishes about itself for the audit to read. */
|
||||
interface MountedTree {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/** Agent-preset vocabulary shared by discovery, mounting, and consumers. */
|
||||
|
||||
/**
|
||||
* Where a preset's composition came from. A `system` preset ships with the
|
||||
* deployment; a `user` preset was authored locally, by a person or by an
|
||||
* agent, and therefore carries the same trust as shell access.
|
||||
*/
|
||||
export type PresetTrust = 'system' | 'user'
|
||||
|
||||
/**
|
||||
* Ids a preset directory may use.
|
||||
*
|
||||
* The id becomes a path segment, so this is a containment boundary rather than
|
||||
* a style rule: `..`, a separator, or an absolute-looking name would place the
|
||||
* composition outside the root the deployment authorised. Discovery shares it:
|
||||
* a directory whose name no copy could ever claim is not a preset slot.
|
||||
*/
|
||||
export const PRESET_ID = /^[a-z0-9][a-z0-9-]*$/
|
||||
|
||||
/** One preset directory that carries a mountable agent composition. */
|
||||
export interface AgentPreset {
|
||||
/** Stable identifier; the preset directory's name. */
|
||||
readonly id: string
|
||||
/** Trust recorded from the root this preset was discovered under. */
|
||||
readonly trust: PresetTrust
|
||||
/** Absolute path of the preset's agent composition file. */
|
||||
readonly path: string
|
||||
/** Display name from the preset's own metadata; absent falls back to {@link id}. */
|
||||
readonly name?: string
|
||||
/** One sentence on what this preset is for, when it published one. */
|
||||
readonly description?: string
|
||||
/** Declared position within its group; absent sorts after those that declare one. */
|
||||
readonly order?: number
|
||||
/**
|
||||
* Why this preset cannot compose a session, absent when it can. A broken
|
||||
* preset stays on the roster — hiding it would leave its directory blocking
|
||||
* the id with nothing to see or delete — but every mounting path refuses it
|
||||
* up front with this reason instead of failing deep inside the loader.
|
||||
*/
|
||||
readonly broken?: string
|
||||
}
|
||||
|
||||
/** One directory scanned for preset subdirectories. */
|
||||
export interface PresetRoot {
|
||||
/** Directory holding one subdirectory per preset; a leading `~` expands. */
|
||||
path: string
|
||||
/** Trust recorded on every preset discovered under this root. */
|
||||
trust: PresetTrust
|
||||
}
|
||||
|
||||
/** Plugin config: which preset is the default, and where presets live. */
|
||||
export interface Config {
|
||||
/** Preset id mounted when a caller names none. Missing at mount time fails loud. */
|
||||
default: string
|
||||
/** Scanned roots in precedence order; an earlier root wins a duplicate id. */
|
||||
roots: PresetRoot[]
|
||||
}
|
||||
|
||||
/**
|
||||
* No configured root supplies the requested preset.
|
||||
*
|
||||
* Separate from a mount failure because the two mean different things to a
|
||||
* caller: an unknown id is a bad request, while an unusable composition is a
|
||||
* broken preset the deployment must fix.
|
||||
*/
|
||||
export class UnknownPresetError extends Error {
|
||||
constructor(
|
||||
/** The id that was requested. */
|
||||
readonly presetId: string,
|
||||
/** Ids the roster does supply, for the caller to offer instead. */
|
||||
readonly available: readonly string[],
|
||||
) {
|
||||
super(`agent-presets: preset "${presetId}" not found (available: ${available.join(', ') || 'none'})`)
|
||||
}
|
||||
}
|
||||
|
||||
/** A preset exists but its composition cannot be installed. */
|
||||
export class PresetMountError extends Error {
|
||||
constructor(
|
||||
/** The preset whose composition failed. */
|
||||
readonly presetId: string,
|
||||
/** Why it failed, without this package's own message prefix. */
|
||||
readonly reason: string,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(`agent-presets: preset "${presetId}" failed to mount: ${reason}`, options)
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,17 @@
|
||||
/** Agent-preset vocabulary shared by discovery, mounting, and consumers. @module @deepseek-ai/dsh-agent-presets/types */
|
||||
/** Client-safe event declarations owned by the agent-preset domain. */
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
|
||||
/**
|
||||
* Where a preset's composition came from. A `system` preset ships with the
|
||||
* deployment; a `user` preset was authored locally, by a person or by an
|
||||
* agent, and therefore carries the same trust as shell access.
|
||||
*/
|
||||
export type PresetTrust = 'system' | 'user'
|
||||
|
||||
/**
|
||||
* Ids a preset directory may use.
|
||||
*
|
||||
* The id becomes a path segment, so this is a containment boundary rather than
|
||||
* a style rule: `..`, a separator, or an absolute-looking name would place the
|
||||
* composition outside the root the deployment authorised. Discovery shares it:
|
||||
* a directory whose name no copy could ever claim is not a preset slot.
|
||||
*/
|
||||
export const PRESET_ID = /^[a-z0-9][a-z0-9-]*$/
|
||||
|
||||
/** One preset directory that carries a mountable agent composition. */
|
||||
export interface AgentPreset {
|
||||
/** Stable identifier; the preset directory's name. */
|
||||
readonly id: string
|
||||
/** Trust recorded from the root this preset was discovered under. */
|
||||
readonly trust: PresetTrust
|
||||
/** Absolute path of the preset's agent composition file. */
|
||||
readonly path: string
|
||||
/** Display name from the preset's own metadata; absent falls back to {@link id}. */
|
||||
readonly name?: string
|
||||
/** One sentence on what this preset is for, when it published one. */
|
||||
readonly description?: string
|
||||
/** Declared position within its group; absent sorts after those that declare one. */
|
||||
readonly order?: number
|
||||
/**
|
||||
* Why this preset cannot compose a session, absent when it can. A broken
|
||||
* preset stays on the roster — hiding it would leave its directory blocking
|
||||
* the id with nothing to see or delete — but every mounting path refuses it
|
||||
* up front with this reason instead of failing deep inside the loader.
|
||||
*/
|
||||
readonly broken?: string
|
||||
}
|
||||
|
||||
/** One directory scanned for preset subdirectories. */
|
||||
export interface PresetRoot {
|
||||
/** Directory holding one subdirectory per preset; a leading `~` expands. */
|
||||
path: string
|
||||
/** Trust recorded on every preset discovered under this root. */
|
||||
trust: PresetTrust
|
||||
}
|
||||
|
||||
/** Plugin config: which preset is the default, and where presets live. */
|
||||
export interface Config {
|
||||
/** Preset id mounted when a caller names none. Missing at mount time fails loud. */
|
||||
default: string
|
||||
/** Scanned roots in precedence order; an earlier root wins a duplicate id. */
|
||||
roots: PresetRoot[]
|
||||
}
|
||||
|
||||
/**
|
||||
* No configured root supplies the requested preset.
|
||||
*
|
||||
* Separate from a mount failure because the two mean different things to a
|
||||
* caller: an unknown id is a bad request, while an unusable composition is a
|
||||
* broken preset the deployment must fix.
|
||||
*/
|
||||
export class UnknownPresetError extends Error {
|
||||
constructor(
|
||||
/** The id that was requested. */
|
||||
readonly presetId: string,
|
||||
/** Ids the roster does supply, for the caller to offer instead. */
|
||||
readonly available: readonly string[],
|
||||
) {
|
||||
super(`agent-presets: preset "${presetId}" not found (available: ${available.join(', ') || 'none'})`)
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Events {
|
||||
/**
|
||||
* One session committed a different agent preset to its durable log.
|
||||
* Consumers invalidate only state derived from that session's composition.
|
||||
* @mode emit
|
||||
* @param sessionId - the session whose composition changed.
|
||||
* @param agentPreset - the preset recorded by the committed selection.
|
||||
*/
|
||||
'agent-preset/selected'(sessionId: SessionId, agentPreset: string): void
|
||||
}
|
||||
}
|
||||
|
||||
/** A preset exists but its composition cannot be installed. */
|
||||
export class PresetMountError extends Error {
|
||||
constructor(
|
||||
/** The preset whose composition failed. */
|
||||
readonly presetId: string,
|
||||
/** Why it failed, without this package's own message prefix. */
|
||||
readonly reason: string,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(`agent-presets: preset "${presetId}" failed to mount: ${reason}`, options)
|
||||
}
|
||||
}
|
||||
export {}
|
||||
|
||||
Reference in New Issue
Block a user