mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
refactor(agent-presets): expose browser operations through Remote
This commit is contained in:
@@ -8,11 +8,11 @@ import { dirname } from 'node:path'
|
|||||||
import type { Context } from '@deepseek-ai/cordis'
|
import type { Context } from '@deepseek-ai/cordis'
|
||||||
import type { ModelSelection } from '@deepseek-ai/dsh-agent'
|
import type { ModelSelection } from '@deepseek-ai/dsh-agent'
|
||||||
import type {} from '@deepseek-ai/dsh-agent-presets/types'
|
import type {} from '@deepseek-ai/dsh-agent-presets/types'
|
||||||
import type { Session, SessionId } from '@deepseek-ai/dsh-session'
|
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||||
import { SubagentError } from '@deepseek-ai/dsh-subagent'
|
import { SubagentError } from '@deepseek-ai/dsh-subagent'
|
||||||
import { isUserInvocable } from '@deepseek-ai/dsh-skill'
|
import { isUserInvocable } from '@deepseek-ai/dsh-skill'
|
||||||
import {
|
import {
|
||||||
InvalidPresetIdError, PresetExistsError, PresetMountError,
|
InvalidPresetIdError, PresetExistsError,
|
||||||
PresetNotWritableError, UnknownPresetError,
|
PresetNotWritableError, UnknownPresetError,
|
||||||
} from '@deepseek-ai/dsh-agent-presets'
|
} from '@deepseek-ai/dsh-agent-presets'
|
||||||
import type {
|
import type {
|
||||||
@@ -80,42 +80,6 @@ function err<T>(request: RpcRequest<unknown>, error: RpcError): RpcResponse<T> {
|
|||||||
return { rpcId: request.rpcId, result: { ok: false, error } }
|
return { rpcId: request.rpcId, result: { ok: false, error } }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Map an agent-preset selection failure onto its stable RPC refusal, or leave
|
|
||||||
* unrelated failures to the caller.
|
|
||||||
* @param request - the request being answered.
|
|
||||||
* @param error - the thrown value.
|
|
||||||
* @returns the refusal, or undefined when the caller should keep handling.
|
|
||||||
*/
|
|
||||||
function presetFailure(request: RpcRequest<unknown>, error: unknown): RpcResponse<never> | undefined {
|
|
||||||
if (error instanceof UnknownPresetError) {
|
|
||||||
return err(request, {
|
|
||||||
code: 'agent-preset-not-found',
|
|
||||||
message: error.message,
|
|
||||||
details: { agentPreset: error.presetId, available: [...error.available] },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (error instanceof PresetMountError) {
|
|
||||||
return err(request, {
|
|
||||||
code: 'agent-preset-invalid',
|
|
||||||
message: error.message,
|
|
||||||
details: { agentPreset: error.presetId, reason: error.reason },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the session's conversation has started: no turn has run yet (a
|
|
||||||
* turn is one model-loop execution). Standalone plugin events — command
|
|
||||||
* lifecycle records, plan/mode, titles, goals — never open a turn, so
|
|
||||||
* running `/plan` or `/goal` on a fresh session keeps it blank
|
|
||||||
* (list-hidden, reusable).
|
|
||||||
*/
|
|
||||||
function sessionBlank(session: Session): boolean {
|
|
||||||
return !session.events.some(event => event.type === 'turn/start')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Map a browse-primitive failure onto the wire error vocabulary (unknown throws stay internal). */
|
/** Map a browse-primitive failure onto the wire error vocabulary (unknown throws stay internal). */
|
||||||
function directoryError(error: unknown): RpcError {
|
function directoryError(error: unknown): RpcError {
|
||||||
if (error instanceof DirectoryPickerError) {
|
if (error instanceof DirectoryPickerError) {
|
||||||
@@ -195,14 +159,6 @@ function projectionsUnavailableError(): RpcError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The requested preset differs from the one this session already runs.
|
|
||||||
*
|
|
||||||
* A session's composition is fixed at creation: its history was produced under
|
|
||||||
* that preset's tools, so adopting the identity under a different one would
|
|
||||||
* replay tool calls the rebuilt agent cannot make. Naming a different preset
|
|
||||||
* is therefore a caller error rather than a switch.
|
|
||||||
*/
|
|
||||||
/** The roster is absent: this deployment composes no agent presets at all. */
|
/** The roster is absent: this deployment composes no agent presets at all. */
|
||||||
function noRoster(agentPreset: string): RpcError {
|
function noRoster(agentPreset: string): RpcError {
|
||||||
return {
|
return {
|
||||||
@@ -239,17 +195,6 @@ function presetError(agentPreset: string, error: unknown): RpcError {
|
|||||||
export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiProxy {
|
export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiProxy {
|
||||||
const sessionExportCompressionLevel = defaults.sessionExportCompressionLevel
|
const sessionExportCompressionLevel = defaults.sessionExportCompressionLevel
|
||||||
?? DEFAULT_SESSION_LOG_COMPRESSION_LEVEL
|
?? DEFAULT_SESSION_LOG_COMPRESSION_LEVEL
|
||||||
/**
|
|
||||||
* Serializes `agentPreset.select` per session. Two concurrent selects both
|
|
||||||
* pass the blank check, and the second `unmountPresetFor` then finds nothing
|
|
||||||
* to unmount because the first already removed the record — leaving two
|
|
||||||
* compositions registered into one agent layer. The client's `busy` flag is
|
|
||||||
* not enforcement: the wire is reachable directly.
|
|
||||||
*/
|
|
||||||
const presetSwitches = new Map<SessionId, Promise<unknown>>()
|
|
||||||
const agentFor = (sessionId: SessionId) =>
|
|
||||||
ctx.sessionController.resolveAgent(sessionId)
|
|
||||||
|
|
||||||
/** Resolve a Session's live or standing preset scope without resuming it. */
|
/** Resolve a Session's live or standing preset scope without resuming it. */
|
||||||
async function sessionScopeFor(
|
async function sessionScopeFor(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
@@ -580,112 +525,8 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
|||||||
},
|
},
|
||||||
|
|
||||||
agentPresets: {
|
agentPresets: {
|
||||||
// A deployment with no roster answers with an empty list rather than an
|
// Only the desktop opener remains here: the roster, selection, and
|
||||||
// error: composing no presets is a valid deployment, and the browser
|
// authoring calls are the AgentPresets service's own Remote namespace.
|
||||||
// simply offers no choice.
|
|
||||||
async list(request) {
|
|
||||||
const presets = ctx.get('agentPresets')
|
|
||||||
if (presets === undefined) return ok(request, { presets: [], authorable: false, hasDocument: false })
|
|
||||||
const defaultId = presets.defaultId
|
|
||||||
return ok(request, {
|
|
||||||
presets: (await presets.list()).map(preset => ({
|
|
||||||
id: preset.id,
|
|
||||||
trust: preset.trust,
|
|
||||||
isDefault: preset.id === defaultId,
|
|
||||||
...preset.name === undefined ? {} : { name: preset.name },
|
|
||||||
...preset.description === undefined ? {} : { description: preset.description },
|
|
||||||
...preset.broken === undefined ? {} : { broken: preset.broken },
|
|
||||||
})),
|
|
||||||
authorable: presets.authorable,
|
|
||||||
hasDocument: canOpenPaths(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// Recomposing is limited to a blank session because a started
|
|
||||||
// conversation's history was produced under its preset's tools; the
|
|
||||||
// agent and the session survive, only the composition is swapped.
|
|
||||||
async select(request) {
|
|
||||||
const { sessionId, agentPreset } = request.payload
|
|
||||||
const presets = ctx.get('agentPresets')
|
|
||||||
if (presets === undefined) {
|
|
||||||
return err(request, {
|
|
||||||
code: 'agent-preset-not-found',
|
|
||||||
message: 'this deployment composes no agent presets',
|
|
||||||
details: { agentPreset, available: [] },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const found = await agentFor(sessionId)
|
|
||||||
if ('error' in found) return err(request, found.error)
|
|
||||||
const { agent } = found
|
|
||||||
const swap = async (): Promise<RpcResponse<{ agentPreset: string }>> => {
|
|
||||||
// Re-read inside the queue: an earlier switch may have run, and a
|
|
||||||
// conversation may have started, since this request arrived.
|
|
||||||
if (!sessionBlank(agent.session)) {
|
|
||||||
return err(request, {
|
|
||||||
code: 'agent-preset-locked',
|
|
||||||
message: `session "${sessionId}" has already started; its agent preset is fixed`,
|
|
||||||
details: { sessionId, agentPreset },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const preset = await presets.recompose(agent.ctx, agentPreset)
|
|
||||||
// Recorded only after the swap committed: the log states what the
|
|
||||||
// agent runs, and a rejected mount leaves the previous composition.
|
|
||||||
agent.session.append('agent-preset/selected', { agentPreset: preset.id })
|
|
||||||
return ok(request, { agentPreset: preset.id })
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const refused = presetFailure(request, error)
|
|
||||||
if (refused !== undefined) return refused
|
|
||||||
return err(request, {
|
|
||||||
code: 'internal',
|
|
||||||
message: `failed to select agent preset "${agentPreset}": ${String(error)}`,
|
|
||||||
details: {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const queued = presetSwitches.get(sessionId) ?? Promise.resolve()
|
|
||||||
const turn = queued.then(swap)
|
|
||||||
presetSwitches.set(sessionId, turn.catch(() => undefined))
|
|
||||||
try {
|
|
||||||
return await turn
|
|
||||||
} finally {
|
|
||||||
if (presetSwitches.get(sessionId) === turn) presetSwitches.delete(sessionId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// A composition names the plugins a session runs, so reading one is
|
|
||||||
// reconnaissance, and copy/remove/openDocument manage the roster and
|
|
||||||
// drive the host desktop. Connection authenticates the complete API.
|
|
||||||
async read(request) {
|
|
||||||
const { agentPreset } = request.payload
|
|
||||||
const presets = ctx.get('agentPresets')
|
|
||||||
if (presets === undefined) return err(request, noRoster(agentPreset))
|
|
||||||
try {
|
|
||||||
const preset = await presets.resolve(agentPreset)
|
|
||||||
return ok(request, {
|
|
||||||
agentPreset: preset.id,
|
|
||||||
trust: preset.trust,
|
|
||||||
content: await presets.read(preset.id),
|
|
||||||
...preset.name === undefined ? {} : { name: preset.name },
|
|
||||||
...preset.description === undefined ? {} : { description: preset.description },
|
|
||||||
})
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return err(request, presetError(agentPreset, error))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async copy(request) {
|
|
||||||
const { from, agentPreset, name } = request.payload
|
|
||||||
const presets = ctx.get('agentPresets')
|
|
||||||
if (presets === undefined) return err(request, noRoster(agentPreset))
|
|
||||||
try {
|
|
||||||
await presets.copy(from, agentPreset, name)
|
|
||||||
return ok(request, { agentPreset })
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return err(request, presetError(agentPreset, error))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async openDocument(request, signal) {
|
async openDocument(request, signal) {
|
||||||
const { agentPreset } = request.payload
|
const { agentPreset } = request.payload
|
||||||
const presets = ctx.get('agentPresets')
|
const presets = ctx.get('agentPresets')
|
||||||
@@ -708,18 +549,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
|||||||
return err(request, presetError(agentPreset, error))
|
return err(request, presetError(agentPreset, error))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async remove(request) {
|
|
||||||
const { agentPreset } = request.payload
|
|
||||||
const presets = ctx.get('agentPresets')
|
|
||||||
if (presets === undefined) return err(request, noRoster(agentPreset))
|
|
||||||
try {
|
|
||||||
await presets.remove(agentPreset)
|
|
||||||
return ok(request, {})
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return err(request, presetError(agentPreset, error))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
skills: {
|
skills: {
|
||||||
|
|||||||
@@ -1,71 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* agent-presets domain zod schemas (names derived from map keys:
|
* agent-presets domain zod schemas (names derived from map keys:
|
||||||
* agentPresetListRequestSchema / agentPresetListValueSchema).
|
* agentPresetOpenDocumentRequestSchema / agentPresetOpenDocumentValueSchema).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
|
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
|
||||||
import type { Wire } from './rpc.schema.ts'
|
import type { Wire } from './rpc.schema.ts'
|
||||||
import { sessionIdSchema } from './ids.schema.ts'
|
|
||||||
import type { AgentPresetEntry } from './agent-presets.ts'
|
|
||||||
|
|
||||||
/** AgentPresetEntry row of agentPreset.list. */
|
|
||||||
export const agentPresetEntrySchema = z.object({
|
|
||||||
id: z.string().min(1),
|
|
||||||
trust: z.union([z.literal('system'), z.literal('user')]),
|
|
||||||
isDefault: z.boolean(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
broken: z.string().min(1).optional(),
|
|
||||||
}) satisfies z.ZodType<Wire<AgentPresetEntry>>
|
|
||||||
|
|
||||||
/** agentPreset.list request payload. */
|
|
||||||
export const agentPresetListRequestSchema = z.object({
|
|
||||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.list'>>>
|
|
||||||
|
|
||||||
/** agentPreset.list response value. */
|
|
||||||
export const agentPresetListValueSchema = z.object({
|
|
||||||
presets: z.array(agentPresetEntrySchema),
|
|
||||||
authorable: z.boolean(),
|
|
||||||
hasDocument: z.boolean(),
|
|
||||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.list'>>>
|
|
||||||
|
|
||||||
/** agentPreset.select request payload. */
|
|
||||||
export const agentPresetSelectRequestSchema = z.object({
|
|
||||||
sessionId: sessionIdSchema,
|
|
||||||
agentPreset: z.string().min(1),
|
|
||||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.select'>>>
|
|
||||||
|
|
||||||
/** agentPreset.select response value. */
|
|
||||||
export const agentPresetSelectValueSchema = z.object({
|
|
||||||
agentPreset: z.string(),
|
|
||||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.select'>>>
|
|
||||||
|
|
||||||
/** agentPreset.read request payload. */
|
|
||||||
export const agentPresetReadRequestSchema = z.object({
|
|
||||||
agentPreset: z.string().min(1),
|
|
||||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.read'>>>
|
|
||||||
|
|
||||||
/** agentPreset.read response value. */
|
|
||||||
export const agentPresetReadValueSchema = z.object({
|
|
||||||
agentPreset: z.string(),
|
|
||||||
trust: z.union([z.literal('system'), z.literal('user')]),
|
|
||||||
content: z.string(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.read'>>>
|
|
||||||
|
|
||||||
/** agentPreset.copy request payload. */
|
|
||||||
export const agentPresetCopyRequestSchema = z.object({
|
|
||||||
from: z.string().min(1),
|
|
||||||
agentPreset: z.string().min(1),
|
|
||||||
name: z.string().optional(),
|
|
||||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.copy'>>>
|
|
||||||
|
|
||||||
/** agentPreset.copy response value. */
|
|
||||||
export const agentPresetCopyValueSchema = z.object({
|
|
||||||
agentPreset: z.string(),
|
|
||||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.copy'>>>
|
|
||||||
|
|
||||||
/** agentPreset.openDocument request payload. */
|
/** agentPreset.openDocument request payload. */
|
||||||
export const agentPresetOpenDocumentRequestSchema = z.object({
|
export const agentPresetOpenDocumentRequestSchema = z.object({
|
||||||
@@ -77,12 +17,3 @@ export const agentPresetOpenDocumentValueSchema = z.union([
|
|||||||
z.object({ opened: z.literal(true) }),
|
z.object({ opened: z.literal(true) }),
|
||||||
z.object({ opened: z.literal(false), path: z.string() }),
|
z.object({ opened: z.literal(false), path: z.string() }),
|
||||||
]) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.openDocument'>>>
|
]) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.openDocument'>>>
|
||||||
|
|
||||||
/** agentPreset.remove request payload. */
|
|
||||||
export const agentPresetRemoveRequestSchema = z.object({
|
|
||||||
agentPreset: z.string().min(1),
|
|
||||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.remove'>>>
|
|
||||||
|
|
||||||
/** agentPreset.remove response value. */
|
|
||||||
export const agentPresetRemoveValueSchema = z.object({
|
|
||||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.remove'>>>
|
|
||||||
|
|||||||
@@ -1,116 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* agent-presets domain contract: the roster a browser offers when starting a
|
* agent-presets domain contract: handing one preset's directory to the
|
||||||
* session, plus the authoring calls behind it.
|
* platform opener, which is the only agent-preset call still carried here.
|
||||||
*
|
*
|
||||||
* A composition names the plugins a session runs, so reading one is
|
* The roster and its authoring calls are the AgentPresets service's own Remote
|
||||||
* reconnaissance; although authoring is copy-only (no caller supplies
|
* namespace. This one stays because the opener is a Host desktop integration
|
||||||
* composition text or a path), copying and deleting still rearrange what the
|
* rather than a preset operation.
|
||||||
* deployment offers. Connection authenticates these calls with the complete
|
|
||||||
* Host API rather than assigning a separate method tier.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
||||||
import type { RpcRequest, RpcResponse } from './rpc.ts'
|
import type { RpcRequest, RpcResponse } from './rpc.ts'
|
||||||
|
|
||||||
/** One preset the deployment can compose a session's agent from. */
|
|
||||||
export interface AgentPresetEntry {
|
|
||||||
/** Stable identifier, also the display name until presets carry metadata. */
|
|
||||||
readonly id: string
|
|
||||||
/**
|
|
||||||
* Whether the preset ships with the deployment or was authored locally.
|
|
||||||
* A `user` preset is exactly as privileged as the plugins it names, so a
|
|
||||||
* surface offering one should say so rather than present it as vetted.
|
|
||||||
*/
|
|
||||||
readonly trust: 'system' | 'user'
|
|
||||||
/** Whether a session that names no preset gets this one. */
|
|
||||||
readonly isDefault: boolean
|
|
||||||
/**
|
|
||||||
* Display name the preset published, absent when it published none. A
|
|
||||||
* surface falls back to {@link id}; it is never a second identity, and it
|
|
||||||
* never decides trust — a locally authored preset cannot name itself into
|
|
||||||
* the shipped set.
|
|
||||||
*/
|
|
||||||
readonly name?: string
|
|
||||||
/** One sentence on what the preset is for, when it published one. */
|
|
||||||
readonly description?: string
|
|
||||||
/**
|
|
||||||
* Why this preset cannot compose a session, absent when it can. A broken
|
|
||||||
* preset stays listed — its directory still occupies the id, so a surface
|
|
||||||
* must be able to show and delete it — but offering it for selection would
|
|
||||||
* only defer this reason to a failed session start.
|
|
||||||
*/
|
|
||||||
readonly broken?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** agent-preset-domain unary methods (the map key agentPreset.* of RpcMethodMap). */
|
/** agent-preset-domain unary methods (the map key agentPreset.* of RpcMethodMap). */
|
||||||
export interface AgentPresetsApi {
|
export interface AgentPresetsApi {
|
||||||
/**
|
|
||||||
* Lists every preset the deployment currently supplies, in root-precedence
|
|
||||||
* order — the roots as configured, each root's own presets sorted by id,
|
|
||||||
* and the first root to supply an id wins. The order is not globally
|
|
||||||
* sorted: a user root's preset sits in that root's block, not among the
|
|
||||||
* shipped ids.
|
|
||||||
* An empty roster means the deployment composes no presets at all, and
|
|
||||||
* every session shares the host composition. `authorable` reports whether
|
|
||||||
* the deployment configures a root new presets can be written to, and
|
|
||||||
* `hasDocument` whether `openDocument` can hand a preset directory to a
|
|
||||||
* native opener — both deployment facts rather than per-preset ones, and
|
|
||||||
* neither exposes a Host path.
|
|
||||||
*/
|
|
||||||
list(request: RpcRequest<{}>):
|
|
||||||
Promise<RpcResponse<{ presets: readonly AgentPresetEntry[]; authorable: boolean; hasDocument: boolean }>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recompose one session's agent from a different preset.
|
|
||||||
*
|
|
||||||
* Allowed only while the session is blank — no turn has run. Once a
|
|
||||||
* conversation starts, its history was produced under that preset's tools,
|
|
||||||
* and swapping them would leave logged tool calls the new composition cannot
|
|
||||||
* make; the attempt answers `agent-preset-locked`.
|
|
||||||
*/
|
|
||||||
select(request: RpcRequest<{ sessionId: SessionId; agentPreset: string }>):
|
|
||||||
Promise<RpcResponse<{ agentPreset: string }>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read one preset's composition text, for the read-only viewer.
|
|
||||||
*
|
|
||||||
* Privileged: a composition names the plugins a session runs, so reading
|
|
||||||
* one is reconnaissance.
|
|
||||||
*/
|
|
||||||
read(request: RpcRequest<{ agentPreset: string }>):
|
|
||||||
Promise<RpcResponse<{
|
|
||||||
agentPreset: string
|
|
||||||
trust: 'system' | 'user'
|
|
||||||
content: string
|
|
||||||
name?: string
|
|
||||||
description?: string
|
|
||||||
}>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a locally authored preset by copying an existing one whole.
|
|
||||||
*
|
|
||||||
* The only authoring write. No composition text and no path crosses the
|
|
||||||
* wire: `from` and `agentPreset` are ids the Host resolves against its own
|
|
||||||
* roots, so a copy is exactly as loadable as its source and grants nothing
|
|
||||||
* the roster did not already carry. The copy keeps the source's description
|
|
||||||
* (the file is the author's to edit afterwards) but not its name — `name`
|
|
||||||
* here or the id fallback is what distinguishes the rows.
|
|
||||||
*/
|
|
||||||
copy(request: RpcRequest<{ from: string; agentPreset: string; name?: string }>):
|
|
||||||
Promise<RpcResponse<{ agentPreset: string }>>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hand one locally authored preset's DIRECTORY to the platform opener, for
|
* Hand one locally authored preset's DIRECTORY to the platform opener, for
|
||||||
* editing the files, which are the only composition editor. The request
|
* editing the files, which are the only composition editor. The request
|
||||||
* carries an id, never a path — the Host resolves it — so no browser
|
* carries an id, never a path — the Host resolves it — so no browser
|
||||||
* payload can select an arbitrary filesystem target. Where the deployment
|
* payload can select an arbitrary filesystem target. Where the deployment
|
||||||
* has no native opener (`hasDocument: false` on `list`), the reply carries
|
* has no native opener (`canOpenPath: false` on `host.describe`), the reply
|
||||||
* the resolved directory for the surface to show as text instead. Shipped
|
* carries the resolved directory for the surface to show as text instead.
|
||||||
* presets are refused: their install is not the user's to manage.
|
* Shipped presets are refused: their install is not the user's to manage.
|
||||||
*/
|
*/
|
||||||
openDocument(request: RpcRequest<{ agentPreset: string }>, signal: AbortSignal):
|
openDocument(request: RpcRequest<{ agentPreset: string }>, signal: AbortSignal):
|
||||||
Promise<RpcResponse<{ opened: true } | { opened: false; path: string }>>
|
Promise<RpcResponse<{ opened: true } | { opened: false; path: string }>>
|
||||||
|
|
||||||
/** Delete a locally authored preset. Shipped presets are refused. */
|
|
||||||
remove(request: RpcRequest<{ agentPreset: string }>): Promise<RpcResponse<{}>>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export type {
|
|||||||
SubagentPromptReceipt, SubagentsApi,
|
SubagentPromptReceipt, SubagentsApi,
|
||||||
} from './subagents.ts'
|
} from './subagents.ts'
|
||||||
export type { SkillsApi, SkillEntry } from './skills.ts'
|
export type { SkillsApi, SkillEntry } from './skills.ts'
|
||||||
export type { AgentPresetsApi, AgentPresetEntry } from './agent-presets.ts'
|
export type { AgentPresetsApi } from './agent-presets.ts'
|
||||||
export type { SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView } from './settings.ts'
|
export type { SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView } from './settings.ts'
|
||||||
export type { CredentialsApi, CredentialView } from './credentials.ts'
|
export type { CredentialsApi, CredentialView } from './credentials.ts'
|
||||||
export type { ConfigurableProviderView, DiscoveredModelView, LlmApi } from './llm.ts'
|
export type { ConfigurableProviderView, DiscoveredModelView, LlmApi } from './llm.ts'
|
||||||
|
|||||||
@@ -27,12 +27,7 @@ export interface RpcMethodMap {
|
|||||||
'host.createDirectory': HostApi['createDirectory']
|
'host.createDirectory': HostApi['createDirectory']
|
||||||
'host.openPath': HostApi['openPath']
|
'host.openPath': HostApi['openPath']
|
||||||
'skill.list': SkillsApi['list']
|
'skill.list': SkillsApi['list']
|
||||||
'agentPreset.list': AgentPresetsApi['list']
|
|
||||||
'agentPreset.select': AgentPresetsApi['select']
|
|
||||||
'agentPreset.read': AgentPresetsApi['read']
|
|
||||||
'agentPreset.copy': AgentPresetsApi['copy']
|
|
||||||
'agentPreset.openDocument': AgentPresetsApi['openDocument']
|
'agentPreset.openDocument': AgentPresetsApi['openDocument']
|
||||||
'agentPreset.remove': AgentPresetsApi['remove']
|
|
||||||
'settings.describe': SettingsApi['describe']
|
'settings.describe': SettingsApi['describe']
|
||||||
'settings.openDocument': SettingsApi['openDocument']
|
'settings.openDocument': SettingsApi['openDocument']
|
||||||
'settings.update': SettingsApi['update']
|
'settings.update': SettingsApi['update']
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ import {
|
|||||||
} from '../api/host.schema.ts'
|
} from '../api/host.schema.ts'
|
||||||
import { skillListValueSchema } from '../api/skills.schema.ts'
|
import { skillListValueSchema } from '../api/skills.schema.ts'
|
||||||
import {
|
import {
|
||||||
agentPresetCopyValueSchema, agentPresetListValueSchema, agentPresetOpenDocumentValueSchema,
|
agentPresetOpenDocumentValueSchema,
|
||||||
agentPresetReadValueSchema, agentPresetRemoveValueSchema, agentPresetSelectValueSchema,
|
|
||||||
} from '../api/agent-presets.schema.ts'
|
} from '../api/agent-presets.schema.ts'
|
||||||
import {
|
import {
|
||||||
settingsDescribeValueSchema, settingsMutateValueSchema, settingsOpenDocumentValueSchema,
|
settingsDescribeValueSchema, settingsMutateValueSchema, settingsOpenDocumentValueSchema,
|
||||||
@@ -64,12 +63,7 @@ export interface IApiClient {
|
|||||||
list(payload: RequestPayload<'skill.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'skill.list'>>>
|
list(payload: RequestPayload<'skill.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'skill.list'>>>
|
||||||
}
|
}
|
||||||
agentPresets: {
|
agentPresets: {
|
||||||
list(payload: RequestPayload<'agentPreset.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.list'>>>
|
|
||||||
select(payload: RequestPayload<'agentPreset.select'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.select'>>>
|
|
||||||
read(payload: RequestPayload<'agentPreset.read'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.read'>>>
|
|
||||||
copy(payload: RequestPayload<'agentPreset.copy'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.copy'>>>
|
|
||||||
openDocument(payload: RequestPayload<'agentPreset.openDocument'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.openDocument'>>>
|
openDocument(payload: RequestPayload<'agentPreset.openDocument'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.openDocument'>>>
|
||||||
remove(payload: RequestPayload<'agentPreset.remove'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.remove'>>>
|
|
||||||
}
|
}
|
||||||
settings: {
|
settings: {
|
||||||
describe(payload: RequestPayload<'settings.describe'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'settings.describe'>>>
|
describe(payload: RequestPayload<'settings.describe'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'settings.describe'>>>
|
||||||
@@ -104,12 +98,7 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
|
|||||||
'host.createDirectory': hostCreateDirectoryValueSchema,
|
'host.createDirectory': hostCreateDirectoryValueSchema,
|
||||||
'host.openPath': hostOpenPathValueSchema,
|
'host.openPath': hostOpenPathValueSchema,
|
||||||
'skill.list': skillListValueSchema,
|
'skill.list': skillListValueSchema,
|
||||||
'agentPreset.list': agentPresetListValueSchema,
|
|
||||||
'agentPreset.select': agentPresetSelectValueSchema,
|
|
||||||
'agentPreset.read': agentPresetReadValueSchema,
|
|
||||||
'agentPreset.copy': agentPresetCopyValueSchema,
|
|
||||||
'agentPreset.openDocument': agentPresetOpenDocumentValueSchema,
|
'agentPreset.openDocument': agentPresetOpenDocumentValueSchema,
|
||||||
'agentPreset.remove': agentPresetRemoveValueSchema,
|
|
||||||
'settings.describe': settingsDescribeValueSchema,
|
'settings.describe': settingsDescribeValueSchema,
|
||||||
'settings.openDocument': settingsOpenDocumentValueSchema,
|
'settings.openDocument': settingsOpenDocumentValueSchema,
|
||||||
'settings.update': settingsUpdateValueSchema,
|
'settings.update': settingsUpdateValueSchema,
|
||||||
@@ -279,12 +268,7 @@ export abstract class AbstractApiClient implements IApiClient {
|
|||||||
// the whole gateway, and with it the host `Context` merges, into every
|
// the whole gateway, and with it the host `Context` merges, into every
|
||||||
// Client program that imports this carrier.
|
// Client program that imports this carrier.
|
||||||
readonly agentPresets: IApiClient['agentPresets'] = {
|
readonly agentPresets: IApiClient['agentPresets'] = {
|
||||||
list: (payload, signal) => this.callUnary('agentPreset.list', payload, signal),
|
|
||||||
select: (payload, signal) => this.callUnary('agentPreset.select', payload, signal),
|
|
||||||
read: (payload, signal) => this.callUnary('agentPreset.read', payload, signal),
|
|
||||||
copy: (payload, signal) => this.callUnary('agentPreset.copy', payload, signal),
|
|
||||||
openDocument: (payload, signal) => this.callUnary('agentPreset.openDocument', payload, signal),
|
openDocument: (payload, signal) => this.callUnary('agentPreset.openDocument', payload, signal),
|
||||||
remove: (payload, signal) => this.callUnary('agentPreset.remove', payload, signal),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly settings: IApiClient['settings'] = {
|
readonly settings: IApiClient['settings'] = {
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import {
|
|||||||
} from '../api/host.schema.ts'
|
} from '../api/host.schema.ts'
|
||||||
import { skillListRequestSchema } from '../api/skills.schema.ts'
|
import { skillListRequestSchema } from '../api/skills.schema.ts'
|
||||||
import {
|
import {
|
||||||
agentPresetCopyRequestSchema, agentPresetListRequestSchema, agentPresetOpenDocumentRequestSchema,
|
agentPresetOpenDocumentRequestSchema,
|
||||||
agentPresetReadRequestSchema, agentPresetRemoveRequestSchema, agentPresetSelectRequestSchema,
|
|
||||||
} from '../api/agent-presets.schema.ts'
|
} from '../api/agent-presets.schema.ts'
|
||||||
import {
|
import {
|
||||||
settingsDescribeRequestSchema, settingsMutateRequestSchema, settingsOpenDocumentRequestSchema,
|
settingsDescribeRequestSchema, settingsMutateRequestSchema, settingsOpenDocumentRequestSchema,
|
||||||
@@ -64,12 +63,7 @@ const UNARY_ROUTES: UnaryRoutes = {
|
|||||||
'host.createDirectory': { schema: hostCreateDirectoryRequestSchema, invoke: (api, r) => api.host.createDirectory(r) },
|
'host.createDirectory': { schema: hostCreateDirectoryRequestSchema, invoke: (api, r) => api.host.createDirectory(r) },
|
||||||
'host.openPath': { schema: hostOpenPathRequestSchema, invoke: (api, r, signal) => api.host.openPath(r, signal) },
|
'host.openPath': { schema: hostOpenPathRequestSchema, invoke: (api, r, signal) => api.host.openPath(r, signal) },
|
||||||
'skill.list': { schema: skillListRequestSchema, invoke: (api, r) => api.skills.list(r) },
|
'skill.list': { schema: skillListRequestSchema, invoke: (api, r) => api.skills.list(r) },
|
||||||
'agentPreset.list': { schema: agentPresetListRequestSchema, invoke: (api, r) => api.agentPresets.list(r) },
|
|
||||||
'agentPreset.select': { schema: agentPresetSelectRequestSchema, invoke: (api, r) => api.agentPresets.select(r) },
|
|
||||||
'agentPreset.read': { schema: agentPresetReadRequestSchema, invoke: (api, r) => api.agentPresets.read(r) },
|
|
||||||
'agentPreset.copy': { schema: agentPresetCopyRequestSchema, invoke: (api, r) => api.agentPresets.copy(r) },
|
|
||||||
'agentPreset.openDocument': { schema: agentPresetOpenDocumentRequestSchema, invoke: (api, r, signal) => api.agentPresets.openDocument(r, signal) },
|
'agentPreset.openDocument': { schema: agentPresetOpenDocumentRequestSchema, invoke: (api, r, signal) => api.agentPresets.openDocument(r, signal) },
|
||||||
'agentPreset.remove': { schema: agentPresetRemoveRequestSchema, invoke: (api, r) => api.agentPresets.remove(r) },
|
|
||||||
'settings.describe': { schema: settingsDescribeRequestSchema, invoke: (api, r) => api.settings.describe(r) },
|
'settings.describe': { schema: settingsDescribeRequestSchema, invoke: (api, r) => api.settings.describe(r) },
|
||||||
'settings.openDocument': { schema: settingsOpenDocumentRequestSchema, invoke: (api, r, signal) => api.settings.openDocument(r, signal) },
|
'settings.openDocument': { schema: settingsOpenDocumentRequestSchema, invoke: (api, r, signal) => api.settings.openDocument(r, signal) },
|
||||||
'settings.update': { schema: settingsUpdateRequestSchema, invoke: (api, r) => api.settings.update(r) },
|
'settings.update': { schema: settingsUpdateRequestSchema, invoke: (api, r) => api.settings.update(r) },
|
||||||
|
|||||||
@@ -26,6 +26,14 @@
|
|||||||
"types": "./lib/types/types.d.ts",
|
"types": "./lib/types/types.d.ts",
|
||||||
"default": "./lib/types/types.js"
|
"default": "./lib/types/types.js"
|
||||||
},
|
},
|
||||||
|
"./typert": {
|
||||||
|
"types": "./lib/typert.host.d.ts",
|
||||||
|
"default": "./lib/typert.host.js"
|
||||||
|
},
|
||||||
|
"./remote": {
|
||||||
|
"types": "./lib/typert.remote-client.d.ts",
|
||||||
|
"default": "./lib/typert.remote-client.js"
|
||||||
|
},
|
||||||
"./src/*": "./src/*",
|
"./src/*": "./src/*",
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
@@ -34,7 +42,11 @@
|
|||||||
"lib/invariant.js",
|
"lib/invariant.js",
|
||||||
"presets",
|
"presets",
|
||||||
"lib/types/**/*.js",
|
"lib/types/**/*.js",
|
||||||
"lib/types/**/*.d.ts"
|
"lib/types/**/*.d.ts",
|
||||||
|
"lib/typert.host.js",
|
||||||
|
"lib/typert.host.d.ts",
|
||||||
|
"lib/typert.remote-client.js",
|
||||||
|
"lib/typert.remote-client.d.ts"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -50,6 +62,7 @@
|
|||||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||||
"@deepseek-ai/cordis": "workspace:^"
|
"@deepseek-ai/cordis": "workspace:^"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -73,6 +86,7 @@
|
|||||||
"@deepseek-ai/dsh-settings-file": "workspace:^",
|
"@deepseek-ai/dsh-settings-file": "workspace:^",
|
||||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||||
"@deepseek-ai/cordis": "workspace:^"
|
"@deepseek-ai/cordis": "workspace:^"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,27 +22,96 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { stat } from 'node:fs/promises'
|
import { stat } from 'node:fs/promises'
|
||||||
import { Context, Service } from '@deepseek-ai/cordis'
|
import { Context } from '@deepseek-ai/cordis'
|
||||||
import z from '@deepseek-ai/schemastery'
|
import z from '@deepseek-ai/schemastery'
|
||||||
|
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||||
import { bindScopeParent, createScope, scopeOf, type Scope, type ScopeKey, type ScopeParentBinding } from '@deepseek-ai/dsh-scope'
|
import { bindScopeParent, createScope, scopeOf, type Scope, type ScopeKey, type ScopeParentBinding } from '@deepseek-ai/dsh-scope'
|
||||||
// Type-only: resolves the `agent/created` lifecycle event this service watches.
|
// Type-only: resolves the `agent/created` lifecycle event this service watches.
|
||||||
import type {} from '@deepseek-ai/dsh-agent'
|
import type {} from '@deepseek-ai/dsh-agent'
|
||||||
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||||
|
import type { AgentPresetDocument, AgentPresetErrorDetailsMap, AgentPresetRoster } from './types.ts'
|
||||||
import type {} from '@deepseek-ai/dsh-session-projection'
|
import type {} from '@deepseek-ai/dsh-session-projection'
|
||||||
// Type-only: resolves the registry notification emitted after scope reparenting.
|
// Type-only: resolves the registry notification emitted after scope reparenting.
|
||||||
import type {} from '@deepseek-ai/dsh-tools'
|
import type {} from '@deepseek-ai/dsh-tools'
|
||||||
import { settingsNamespace, type SettingsScope, type default as SettingsService } from '@deepseek-ai/dsh-settings'
|
import { settingsNamespace, type SettingsScope, type default as SettingsService } from '@deepseek-ai/dsh-settings'
|
||||||
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
||||||
import { discoverPresets, SHIPPED_PRESET_ROOT, USER_PRESET_DIR } from './discovery.ts'
|
import { discoverPresets, SHIPPED_PRESET_ROOT, USER_PRESET_DIR } from './discovery.ts'
|
||||||
import { copyComposition, deleteComposition, readComposition } from './authoring.ts'
|
import {
|
||||||
|
copyComposition, deleteComposition, readComposition,
|
||||||
|
InvalidPresetIdError, PresetExistsError, PresetNotWritableError,
|
||||||
|
} from './authoring.ts'
|
||||||
import { mountPreset, serviceForAgent, standingMountFor } from './mount.ts'
|
import { mountPreset, serviceForAgent, standingMountFor } from './mount.ts'
|
||||||
import { PresetExistsError } from './authoring.ts'
|
import {
|
||||||
import { PresetMountError, UnknownPresetError, type AgentPreset, type Config, type PresetRoot } from './preset.ts'
|
PresetLockedError, PresetMountError, UnknownPresetError,
|
||||||
|
type AgentPreset, type Config, type PresetRoot,
|
||||||
|
} from './preset.ts'
|
||||||
import { agentPresetProjectionDefinition } from './session.ts'
|
import { agentPresetProjectionDefinition } from './session.ts'
|
||||||
export type * from './types.ts'
|
export type * from './types.ts'
|
||||||
|
|
||||||
/** Settings namespace carrying the user's chosen default preset. */
|
/** Settings namespace carrying the user's chosen default preset. */
|
||||||
export const SETTINGS_NAMESPACE = 'agent-presets'
|
export const SETTINGS_NAMESPACE = 'agent-presets'
|
||||||
|
|
||||||
|
/** Construct one typed preset failure for the Remote carrier. */
|
||||||
|
function remotePresetFailure<Code extends keyof AgentPresetErrorDetailsMap>(
|
||||||
|
code: Code,
|
||||||
|
message: string,
|
||||||
|
details: AgentPresetErrorDetailsMap[Code],
|
||||||
|
): TypertRemoteFailure {
|
||||||
|
return new TypertRemoteFailure({ code, message, details })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map one preset rejection to its stable Remote code and details. */
|
||||||
|
function presetFailure(error: unknown, agentPreset: string): TypertRemoteFailure | undefined {
|
||||||
|
if (error instanceof UnknownPresetError) {
|
||||||
|
return remotePresetFailure(
|
||||||
|
'agent-preset-not-found',
|
||||||
|
error.message,
|
||||||
|
{ agentPreset: error.presetId, available: [...error.available] },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (error instanceof PresetMountError) {
|
||||||
|
return remotePresetFailure(
|
||||||
|
'agent-preset-invalid',
|
||||||
|
error.message,
|
||||||
|
{ agentPreset: error.presetId, reason: error.reason },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (error instanceof InvalidPresetIdError || error instanceof PresetExistsError) {
|
||||||
|
return remotePresetFailure(
|
||||||
|
'agent-preset-invalid',
|
||||||
|
error.message,
|
||||||
|
{ agentPreset: error.presetId, reason: error.message },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (error instanceof PresetNotWritableError) {
|
||||||
|
return remotePresetFailure(
|
||||||
|
'agent-preset-read-only',
|
||||||
|
error.message,
|
||||||
|
{ agentPreset, reason: error.message },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (error instanceof PresetLockedError) {
|
||||||
|
return remotePresetFailure(
|
||||||
|
'agent-preset-locked',
|
||||||
|
`session "${error.sessionId}" has already started; its agent preset is fixed`,
|
||||||
|
{ sessionId: error.sessionId, agentPreset: error.presetId },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Refuse an empty preset id before invoking a domain operation. */
|
||||||
|
function validatePresetId(value: string, field: 'agentPreset' | 'from'): void {
|
||||||
|
if (value.length === 0) {
|
||||||
|
throw remotePresetFailure('bad-request', `${field} must be a non-empty string`, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Throw the stable preset failure or the caller's operation-specific fallback. */
|
||||||
|
function rejectPreset(error: unknown, agentPreset: string, fallbackMessage: string): never {
|
||||||
|
throw presetFailure(error, agentPreset) ?? remotePresetFailure('internal', fallbackMessage, {})
|
||||||
|
}
|
||||||
|
|
||||||
/** The user-writable slice of this plugin's config. */
|
/** The user-writable slice of this plugin's config. */
|
||||||
export interface AgentPresetSettings {
|
export interface AgentPresetSettings {
|
||||||
/** Preset mounted when a session names none. */
|
/** Preset mounted when a session names none. */
|
||||||
@@ -67,7 +136,7 @@ export {
|
|||||||
PresetNotWritableError, readComposition, writableRoot,
|
PresetNotWritableError, readComposition, writableRoot,
|
||||||
} from './authoring.ts'
|
} from './authoring.ts'
|
||||||
export { agentPresetProjectionDefinition } from './session.ts'
|
export { agentPresetProjectionDefinition } from './session.ts'
|
||||||
export { PresetMountError, UnknownPresetError } from './preset.ts'
|
export { PresetLockedError, PresetMountError, UnknownPresetError } from './preset.ts'
|
||||||
export type { AgentPreset, Config, PresetRoot, PresetTrust } from './preset.ts'
|
export type { AgentPreset, Config, PresetRoot, PresetTrust } from './preset.ts'
|
||||||
|
|
||||||
declare module '@deepseek-ai/cordis' {
|
declare module '@deepseek-ai/cordis' {
|
||||||
@@ -83,7 +152,7 @@ declare module '@deepseek-ai/cordis' {
|
|||||||
* call so a preset authored while the process runs is visible immediately,
|
* call so a preset authored while the process runs is visible immediately,
|
||||||
* and a preset deleted underneath a picker disappears from the next read.
|
* and a preset deleted underneath a picker disappears from the next read.
|
||||||
*/
|
*/
|
||||||
export class AgentPresets extends Service {
|
export class AgentPresets extends TypertRemoteService {
|
||||||
static inject = ['loader']
|
static inject = ['loader']
|
||||||
|
|
||||||
/** Runtime schema for the preset roster. */
|
/** Runtime schema for the preset roster. */
|
||||||
@@ -213,6 +282,30 @@ export class AgentPresets extends Service {
|
|||||||
return await discoverPresets(this.resolvedRoots)
|
return await discoverPresets(this.resolvedRoots)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The roster off the Host: {@link list} projected to path-free rows, with
|
||||||
|
* the default marked and this deployment's authoring capability beside it.
|
||||||
|
*
|
||||||
|
* Whether a client can open a preset's directory is the Host's own opener
|
||||||
|
* capability, not a roster property — a caller needing both joins them.
|
||||||
|
* @returns the rows and the authoring capability.
|
||||||
|
*/
|
||||||
|
@Remote('list')
|
||||||
|
async remoteExportList(): Promise<AgentPresetRoster> {
|
||||||
|
const defaultId = this.defaultId
|
||||||
|
return {
|
||||||
|
presets: (await this.list()).map(preset => ({
|
||||||
|
id: preset.id,
|
||||||
|
trust: preset.trust,
|
||||||
|
isDefault: preset.id === defaultId,
|
||||||
|
...preset.name === undefined ? {} : { name: preset.name },
|
||||||
|
...preset.description === undefined ? {} : { description: preset.description },
|
||||||
|
...preset.broken === undefined ? {} : { broken: preset.broken },
|
||||||
|
})),
|
||||||
|
authorable: this.authorable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve one preset by id.
|
* Resolve one preset by id.
|
||||||
*
|
*
|
||||||
@@ -376,6 +469,30 @@ export class AgentPresets extends Service {
|
|||||||
return await readComposition(await this.resolve(id))
|
return await readComposition(await this.resolve(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One preset's composition text with the roster row it belongs to.
|
||||||
|
* @param agentPreset - the preset id.
|
||||||
|
* @returns the composition beside its trust and published metadata.
|
||||||
|
* @throws {TypertRemoteFailure} `bad-request` for an empty id, or
|
||||||
|
* `agent-preset-not-found` when no configured root supplies it.
|
||||||
|
*/
|
||||||
|
@Remote('read')
|
||||||
|
async readDocument(agentPreset: string): Promise<AgentPresetDocument> {
|
||||||
|
validatePresetId(agentPreset, 'agentPreset')
|
||||||
|
try {
|
||||||
|
const preset = await this.resolve(agentPreset)
|
||||||
|
return {
|
||||||
|
agentPreset: preset.id,
|
||||||
|
trust: preset.trust,
|
||||||
|
content: await this.read(preset.id),
|
||||||
|
...preset.name === undefined ? {} : { name: preset.name },
|
||||||
|
...preset.description === undefined ? {} : { description: preset.description },
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
rejectPreset(error, agentPreset, `agent preset "${agentPreset}": ${String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a locally authored preset by copying an existing one whole.
|
* Create a locally authored preset by copying an existing one whole.
|
||||||
*
|
*
|
||||||
@@ -406,8 +523,29 @@ export class AgentPresets extends Service {
|
|||||||
this.standing.delete(id)
|
this.standing.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy one preset through the Remote API.
|
||||||
|
* @param from - the source preset id.
|
||||||
|
* @param id - the new preset id.
|
||||||
|
* @param name - the copy's optional display name.
|
||||||
|
* @returns once the copy is stored.
|
||||||
|
* @throws {TypertRemoteFailure} with the corresponding stable preset code
|
||||||
|
* and details when the copy is refused.
|
||||||
|
*/
|
||||||
|
@Remote('copy')
|
||||||
|
async remoteExportCopy(from: string, id: string, name?: string): Promise<void> {
|
||||||
|
validatePresetId(from, 'from')
|
||||||
|
validatePresetId(id, 'agentPreset')
|
||||||
|
try {
|
||||||
|
await this.copy(from, id, name)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
rejectPreset(error, id, `agent preset "${id}": ${String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a locally authored preset.
|
* Delete a locally authored preset.
|
||||||
|
*
|
||||||
* @param id - the preset id.
|
* @param id - the preset id.
|
||||||
* @throws when the preset is unknown or ships with the deployment.
|
* @throws when the preset is unknown or ships with the deployment.
|
||||||
*/
|
*/
|
||||||
@@ -429,6 +567,23 @@ export class AgentPresets extends Service {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete one preset through the Remote API.
|
||||||
|
* @param id - the preset id.
|
||||||
|
* @returns once the preset is deleted.
|
||||||
|
* @throws {TypertRemoteFailure} with the corresponding stable preset code
|
||||||
|
* and details when deletion is refused.
|
||||||
|
*/
|
||||||
|
@Remote('deletePreset')
|
||||||
|
async remoteExportDelete(id: string): Promise<void> {
|
||||||
|
validatePresetId(id, 'agentPreset')
|
||||||
|
try {
|
||||||
|
await this.remove(id)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
rejectPreset(error, id, `agent preset "${id}": ${String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One agent's instance of a service its preset mounted.
|
* One agent's instance of a service its preset mounted.
|
||||||
*
|
*
|
||||||
@@ -495,6 +650,58 @@ export class AgentPresets extends Service {
|
|||||||
return preset
|
return preset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes {@link select} per session. Two concurrent selects would both
|
||||||
|
* pass the blank check, and the second re-link would then find the record
|
||||||
|
* the first already replaced — leaving two compositions registered into one
|
||||||
|
* agent layer. A client's `busy` flag is not enforcement: the wire is
|
||||||
|
* reachable directly.
|
||||||
|
*
|
||||||
|
* Entries hold a failure-swallowing guard rather than the turn itself, so a
|
||||||
|
* refused switch does not reject the next caller's chain.
|
||||||
|
*/
|
||||||
|
private readonly switches = new Map<string, Promise<unknown>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose a blank session's agent from a different preset and record it.
|
||||||
|
* @param agent - the session's live agent, resolved from the wire identity.
|
||||||
|
* @param agentPreset - the preset to compose the agent from instead.
|
||||||
|
* @returns the preset id that was recorded.
|
||||||
|
* @throws {TypertRemoteFailure} with `bad-request`, `agent-preset-locked`,
|
||||||
|
* `agent-preset-not-found`, or `agent-preset-invalid` when refused.
|
||||||
|
*/
|
||||||
|
@Remote('select')
|
||||||
|
async select(agent: Agent, agentPreset: string): Promise<string> {
|
||||||
|
validatePresetId(agentPreset, 'agentPreset')
|
||||||
|
const queued = this.switches.get(agent.id) ?? Promise.resolve()
|
||||||
|
const turn = queued.then(() => this.swap(agent, agentPreset))
|
||||||
|
const guard = turn.catch(() => undefined)
|
||||||
|
this.switches.set(agent.id, guard)
|
||||||
|
try {
|
||||||
|
return await turn
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return rejectPreset(error, agentPreset, `failed to select agent preset "${agentPreset}": ${String(error)}`)
|
||||||
|
} finally {
|
||||||
|
if (this.switches.get(agent.id) === guard) this.switches.delete(agent.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One queued switch: re-check, recompose, then record what the agent runs. */
|
||||||
|
private async swap(agent: Agent, agentPreset: string): Promise<string> {
|
||||||
|
// Re-read inside the queue: an earlier switch may have run, and a
|
||||||
|
// conversation may have started, since this call was queued. A turn is one
|
||||||
|
// model-loop execution; standalone plugin events never open one, so a
|
||||||
|
// session that has only run commands is still blank.
|
||||||
|
if (agent.session.events.some(event => event.type === 'turn/start')) {
|
||||||
|
throw new PresetLockedError(agent.id, agentPreset)
|
||||||
|
}
|
||||||
|
const preset = await this.recompose(agent.ctx, agentPreset)
|
||||||
|
// Recorded only after the swap committed: the log states what the agent
|
||||||
|
// runs, and a rejected mount leaves the previous composition.
|
||||||
|
agent.session.append('agent-preset/selected', { agentPreset: preset.id })
|
||||||
|
return preset.id
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The standing scope key of one preset, for a host reader with no agent.
|
* The standing scope key of one preset, for a host reader with no agent.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/** Agent-preset vocabulary shared by discovery, mounting, and consumers. */
|
/** Agent-preset vocabulary shared by discovery, mounting, and consumers. */
|
||||||
|
|
||||||
|
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where a preset's composition came from. A `system` preset ships with the
|
* 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
|
* deployment; a `user` preset was authored locally, by a person or by an
|
||||||
@@ -87,6 +89,22 @@ export class UnknownPresetError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The session's composition is fixed: its conversation has started, so its
|
||||||
|
* history was produced under the preset it runs and swapping the composition
|
||||||
|
* would leave logged tool calls the new one cannot make.
|
||||||
|
*/
|
||||||
|
export class PresetLockedError extends Error {
|
||||||
|
constructor(
|
||||||
|
/** The session whose composition is already fixed. */
|
||||||
|
readonly sessionId: SessionId,
|
||||||
|
/** The preset that was refused. */
|
||||||
|
readonly presetId: string,
|
||||||
|
) {
|
||||||
|
super(`agent-presets: session "${sessionId}" has already started; its agent preset is fixed`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** A preset exists but its composition cannot be installed. */
|
/** A preset exists but its composition cannot be installed. */
|
||||||
export class PresetMountError extends Error {
|
export class PresetMountError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,5 +1,74 @@
|
|||||||
/** Client-safe event declarations owned by the agent-preset domain. */
|
/** Client-safe payloads and event declarations owned by the agent-preset domain. */
|
||||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||||
|
import type { PresetTrust } from './preset.ts'
|
||||||
|
|
||||||
|
export type { PresetTrust } from './preset.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One roster row as a client reads it. Path-free: a preset is addressed by id
|
||||||
|
* everywhere off the Host, and the composition's location is the Host's own.
|
||||||
|
*/
|
||||||
|
export interface AgentPresetRow {
|
||||||
|
/** Stable identifier; also the label's fallback. */
|
||||||
|
readonly id: string
|
||||||
|
/** Trust of the root this preset was discovered under. */
|
||||||
|
readonly trust: PresetTrust
|
||||||
|
/** Whether a session naming no preset composes this one. */
|
||||||
|
readonly isDefault: boolean
|
||||||
|
/** Display name the preset published. */
|
||||||
|
readonly name?: string
|
||||||
|
/** One sentence on what this preset is for. */
|
||||||
|
readonly description?: string
|
||||||
|
/** Why this preset cannot compose a session; absent when it can. */
|
||||||
|
readonly broken?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The roster one deployment currently supplies, with its authoring capability. */
|
||||||
|
export interface AgentPresetRoster {
|
||||||
|
/** Every preset the configured roots supply, first-root-wins per id. */
|
||||||
|
readonly presets: readonly AgentPresetRow[]
|
||||||
|
/** Whether this deployment has a root locally authored presets go to. */
|
||||||
|
readonly authorable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stable details for agent-preset failures returned by the Remote namespace. */
|
||||||
|
export interface AgentPresetErrorDetailsMap {
|
||||||
|
/** A required preset id is empty. */
|
||||||
|
'bad-request': Record<never, never>
|
||||||
|
/** No configured root supplies the requested id. */
|
||||||
|
'agent-preset-not-found': { readonly agentPreset: string; readonly available: readonly string[] }
|
||||||
|
/** The id is unusable, already taken, or its composition cannot be installed. */
|
||||||
|
'agent-preset-invalid': { readonly agentPreset: string; readonly reason: string }
|
||||||
|
/** The preset ships with the deployment and is not the user's to change. */
|
||||||
|
'agent-preset-read-only': { readonly agentPreset: string; readonly reason: string }
|
||||||
|
/** The session's conversation has started, so its composition is fixed. */
|
||||||
|
'agent-preset-locked': { readonly sessionId: SessionId; readonly agentPreset: string }
|
||||||
|
/** The preset operation failed without a caller-actionable classification. */
|
||||||
|
internal: Record<never, never>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One agent-preset refusal as a client reads it. */
|
||||||
|
export type AgentPresetError = {
|
||||||
|
[Code in keyof AgentPresetErrorDetailsMap]: {
|
||||||
|
readonly code: Code
|
||||||
|
readonly message: string
|
||||||
|
readonly details: AgentPresetErrorDetailsMap[Code]
|
||||||
|
}
|
||||||
|
}[keyof AgentPresetErrorDetailsMap]
|
||||||
|
|
||||||
|
/** One preset's composition text beside the row it belongs to. */
|
||||||
|
export interface AgentPresetDocument {
|
||||||
|
/** The preset the composition belongs to. */
|
||||||
|
readonly agentPreset: string
|
||||||
|
/** Trust of the root this preset was discovered under. */
|
||||||
|
readonly trust: PresetTrust
|
||||||
|
/** The composition exactly as stored. */
|
||||||
|
readonly content: string
|
||||||
|
/** Display name the preset published. */
|
||||||
|
readonly name?: string
|
||||||
|
/** One sentence on what this preset is for. */
|
||||||
|
readonly description?: string
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@deepseek-ai/dsh-session-projection/types' {
|
declare module '@deepseek-ai/dsh-session-projection/types' {
|
||||||
interface SessionProjectionStateMap {
|
interface SessionProjectionStateMap {
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
{
|
{
|
||||||
"path": "../../core/tools"
|
"path": "../../core/tools"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "../../typert/protocol"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../../settings/settings"
|
"path": "../../settings/settings"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+3
@@ -6649,6 +6649,9 @@ importers:
|
|||||||
'@deepseek-ai/dsh-tools':
|
'@deepseek-ai/dsh-tools':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../core/tools
|
version: link:../../core/tools
|
||||||
|
'@deepseek-ai/dsh-typert-protocol':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../typert/protocol
|
||||||
|
|
||||||
packages/preset/persona:
|
packages/preset/persona:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user