diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index ab6c2a4598..76b19b8953 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 88a7d34b5ed72c1e7535d62f3b9163ea7d677756 -config-catalog.zh.md: f4ad394528dcde48a7c1c499acaece82ada894f4 +config-catalog.md: b7bc65cc2f2111ac549067f31518c18cf636c9b9 +config-catalog.zh.md: f934799dbab07d394732447ae2a4cd4184e10d61 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 88a7d34b5e..b7bc65cc2f 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1508,7 +1508,7 @@ export interface PresetSpec { Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsystems/sandbox.md) -Source: [`packages/interaction/permission-presets/src/index.ts:134`](../packages/interaction/permission-presets/src/index.ts) +Source: [`packages/interaction/permission-presets/src/index.ts:143`](../packages/interaction/permission-presets/src/index.ts) @@ -1992,7 +1992,7 @@ export interface Config { } ``` -Source: [`packages/session/session-title/src/index.ts:53`](../packages/session/session-title/src/index.ts) +Source: [`packages/session/session-title/src/index.ts:54`](../packages/session/session-title/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index f4ad394528..f934799dba 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1510,7 +1510,7 @@ export interface PresetSpec { 依赖:[`ApprovalPolicy`](subsystems/approval.zh.md) · [`SandboxMode`](subsystems/sandbox.zh.md) -来源:[`packages/interaction/permission-presets/src/index.ts:134`](../packages/interaction/permission-presets/src/index.ts) +来源:[`packages/interaction/permission-presets/src/index.ts:143`](../packages/interaction/permission-presets/src/index.ts) @@ -1994,7 +1994,7 @@ export interface Config { } ``` -来源:[`packages/session/session-title/src/index.ts:53`](../packages/session/session-title/src/index.ts) +来源:[`packages/session/session-title/src/index.ts:54`](../packages/session/session-title/src/index.ts) diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index c6569b3349..24e448b40d 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/event-producer-consumer.md -event-producer-consumer.md: 12efe17c1c02fa744c556996ed6e2bc9bf07deb9 -event-producer-consumer.zh.md: 108a8d1838cbd872137111735b35fe25e89303dc +event-producer-consumer.md: cb215c9e50902cac7eaa07f6bb294f798bcf848a +event-producer-consumer.zh.md: ccf1fb91259ac63e019c82d2b4d283f3213ab991 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 12efe17c1c..cb215c9e50 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -77,7 +77,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event string | Dispatchers | Listeners | | --- | --- | --- | -| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) | +| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `modules`, `webserver` | | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 108a8d1838..ccf1fb9125 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -79,7 +79,7 @@ | 事件字符串 | 派发方 | 监听方 | | --- | --- | --- | -| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) | +| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `modules`, `webserver` | | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index a94e12b812..7f40c918d7 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: 857d3783d57decf99596527ad3b8cc5f344be616 -persistence-catalog.zh.md: 591a3951f094d470b4522fe22250154a0d1cf293 +persistence-catalog.md: 29d07f674c2ebd054b0aafa47f729504807460bc +persistence-catalog.zh.md: 4c191e6f01a7513df2723502a88f3f973b7596e3 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 857d3783d5..29d07f674c 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -519,7 +519,7 @@ Source: [`packages/api/session-controller/src/types.ts:40`](../packages/api/sess 'permission/preset': { preset: string } ``` -Source: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/interaction/permission-presets/src/index.ts) +Source: [`packages/interaction/permission-presets/src/index.ts:53`](../packages/interaction/permission-presets/src/index.ts) ### `plan/*` @@ -662,7 +662,7 @@ Source: [`packages/core/session/src/types.ts:324`](../packages/core/session/src/ Types: [SessionTitleEventData](subsystems/session-title.md) -Source: [`packages/session/session-title/src/index.ts:74`](../packages/session/session-title/src/index.ts) +Source: [`packages/session/session-title/src/index.ts:75`](../packages/session/session-title/src/index.ts) diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 591a3951f0..4c191e6f01 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -521,7 +521,7 @@ export type SessionEvent = { 'permission/preset': { preset: string } ``` -来源:[`packages/interaction/permission-presets/src/index.ts:59`](../packages/interaction/permission-presets/src/index.ts) +来源:[`packages/interaction/permission-presets/src/index.ts:53`](../packages/interaction/permission-presets/src/index.ts) ### `plan/*` @@ -664,7 +664,7 @@ export type SessionEvent = { 类型:[SessionTitleEventData](subsystems/session-title.zh.md) -来源:[`packages/session/session-title/src/index.ts:74`](../packages/session/session-title/src/index.ts) +来源:[`packages/session/session-title/src/index.ts:75`](../packages/session/session-title/src/index.ts) diff --git a/docs/subsystems/sandbox.i18n.yaml b/docs/subsystems/sandbox.i18n.yaml index 6cec1e9dff..735c78927d 100644 --- a/docs/subsystems/sandbox.i18n.yaml +++ b/docs/subsystems/sandbox.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/sandbox.md -sandbox.md: 8afa1d2ef2673b240938f5c70c8f58f8e16c16df -sandbox.zh.md: 6ee68d7d26cdcaed6f62a27fecfc3604e4d4f7a5 +sandbox.md: 7160ac699a21a319c991ae70262a8bf61da86b0c +sandbox.zh.md: 6d2b10d63a0e180bc0fe39cc47930e66dee72466 diff --git a/docs/subsystems/sandbox.md b/docs/subsystems/sandbox.md index 8afa1d2ef2..7160ac699a 100644 --- a/docs/subsystems/sandbox.md +++ b/docs/subsystems/sandbox.md @@ -192,7 +192,7 @@ Source: [`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox ### `ctx.sandboxPolicy` — `SandboxPolicyService` -The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log. +The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability. ```ts cordis-catalog /** diff --git a/docs/subsystems/sandbox.zh.md b/docs/subsystems/sandbox.zh.md index 6ee68d7d26..6d2b10d63a 100644 --- a/docs/subsystems/sandbox.zh.md +++ b/docs/subsystems/sandbox.zh.md @@ -192,7 +192,7 @@ Source: [`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox ### `ctx.sandboxPolicy` — `SandboxPolicyService` -The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log. +The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability. ```ts cordis-catalog /** diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts index c551b83f58..8a1e7880c6 100644 --- a/packages/context/time-context/src/index.ts +++ b/packages/context/time-context/src/index.ts @@ -39,7 +39,7 @@ const timeContextStateSchema = zod.object({ }) /** Folded time-context readings. */ -export type TimeContextProjection = zod.infer +type TimeContextProjection = zod.infer /** The agent registry that owns pre-step processing. */ export const inject = ['agents', 'sessionProjections'] @@ -182,15 +182,13 @@ export function apply(ctx: Context, config: Config): void { const decision = await next() if (decision.kind === 'reject' || signal.aborted) return decision const now = Date.now() - const state = ctx.sessionProjections.stateOf(agent.session, 'timeContext') - if (state !== undefined && refreshIntervalMs !== undefined && refreshIntervalMs > 0) { + const state = ctx.sessionProjections.stateOf(agent.session, 'timeContext') as TimeContextProjection + if (refreshIntervalMs !== undefined && refreshIntervalMs > 0) { const lastInjection = state.lastInjectionTime if (lastInjection != null && now >= lastInjection && now - lastInjection < refreshIntervalMs) return decision } - /* v8 ignore next 2 -- time-context registers its own unit in apply, so the key is always present */ - if (state === undefined) return decision /* v8 ignore next 6 -- every later step follows a recorded injection in the same turn */ const previous = step === 1 ? state.lastMessageTime ?? undefined diff --git a/packages/context/tmux-context/src/index.ts b/packages/context/tmux-context/src/index.ts index cc4c136076..bfcec10247 100644 --- a/packages/context/tmux-context/src/index.ts +++ b/packages/context/tmux-context/src/index.ts @@ -241,7 +241,7 @@ export function apply(ctx: Context, config: Config): void { if (decision.kind === 'reject' || signal.aborted || step !== 1) return decision const bash = ctx.get('shell') if (bash === undefined) return decision - const previous = ctx.sessionProjections.stateOf(agent.session, 'tmuxContext') ?? null + const previous = ctx.sessionProjections.stateOf(agent.session, 'tmuxContext') as TmuxContextState if (refreshIntervalMs !== undefined && refreshIntervalMs > 0 && previous !== null) { const now = Date.now() if (now >= previous.time && now - previous.time < refreshIntervalMs) return decision diff --git a/packages/experimental/agent-team/src/invariant.ts b/packages/experimental/agent-team/src/invariant.ts index 90a2076bd6..dc5fd3ddc1 100644 --- a/packages/experimental/agent-team/src/invariant.ts +++ b/packages/experimental/agent-team/src/invariant.ts @@ -1,7 +1,13 @@ /** Agent Teams runtime invariant companion. */ import type { Context } from '@deepseek-ai/cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import { + isTeamEvent, + teamProjectionDefinition, + type TeamProjectionState, +} from './projection.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-experimental-agent-team' @@ -10,8 +16,20 @@ export const name = 'team-invariant' /** Invariant registry required by the companion. */ export const inject = ['invariants'] -/** No runtime invariant: the Team projection owns event decoding and relational state transitions. */ -const install: InvariantInstaller = () => {} +/** Validate candidate Team events against the projected committed prefix. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const [session, event] = args as [Session, SessionEvent] + /* v8 ignore next -- non-Team Session events have no Agent Teams invariant. */ + if (!isTeamEvent(event)) return + const state = ctx.sessionProjections.stateOf(session, 'agentTeam') as TeamProjectionState + const candidate = teamProjectionDefinition.apply(structuredClone(state), event) + if (candidate.failure !== undefined) { + fail(`session event ${event.seq} violates the Agent Teams stream: ${candidate.failure}`) + } + }, { global: true }) +}, { inject: ['sessionProjections'] }) /** Register the package invariant companion. */ export const apply = (ctx: Context): Promise<() => void> => diff --git a/packages/experimental/agent-team/src/projection.ts b/packages/experimental/agent-team/src/projection.ts index bb23ecc65e..0a873718e6 100644 --- a/packages/experimental/agent-team/src/projection.ts +++ b/packages/experimental/agent-team/src/projection.ts @@ -186,7 +186,7 @@ type TeamSessionEvent = SessionEvent * @param event - candidate Session event. * @returns whether the event has a Team-owned type. */ -function isTeamEvent(event: SessionEvent): event is TeamSessionEvent { +export function isTeamEvent(event: SessionEvent): event is TeamSessionEvent { return event.type === 'team/member' || event.type === 'team/task' || event.type === 'team/message/queued' @@ -220,8 +220,8 @@ function parseCurrentTeamEvent(event: TeamSessionEvent): TeamSessionEvent { } function applyProjectionEvent(state: TeamProjectionState, event: SessionEvent): void { - if (!isTeamEvent(event)) return if (state.failure !== undefined) return + if (!isTeamEvent(event)) return try { const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data) if (selector.teamId !== state.id) return diff --git a/packages/experimental/agent-team/tests/invariant.spec.ts b/packages/experimental/agent-team/tests/invariant.spec.ts new file mode 100644 index 0000000000..ba12a07c74 --- /dev/null +++ b/packages/experimental/agent-team/tests/invariant.spec.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import InvariantService, { InvariantError } from '@deepseek-ai/dsh-invariants' +import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import * as TeamInvariant from '../src/invariant.ts' +import { teamProjectionDefinition } from '../src/projection.ts' +import { TeamId, TeamTaskId } from '../src/types.ts' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + ctx.sessionProjections.register(teamProjectionDefinition) + await ctx.plugin(InvariantService, { enabled: true }) + await ctx.plugin(TeamInvariant) + return ctx +} + +describe('Agent Teams stream invariant', () => { + it('accepts provisioning and rejects a terminal member as the first edge', async () => { + const ctx = await setup() + const session = ctx.sessions.create(SessionId('team-invariant')) + const member = { + id: SessionId('team-invariant-child'), + name: 'worker', + description: 'worker responsibility', + provider: 'spawn', + context: 'fresh' as const, + phase: 'provisioning' as const, + } + expect(() => { + session.append('team/member', { version: 1, teamId: TeamId(session.id), member }) + }).not.toThrow() + + const invalid = ctx.sessions.create(SessionId('team-invariant-invalid')) + expect(() => { + invalid.append('team/member', { + version: 1, + teamId: TeamId(invalid.id), + member: { ...member, phase: 'active' }, + }) + }).toThrow(expect.objectContaining>({ + code: 'INVARIANT', + packageName: '@deepseek-ai/dsh-experimental-agent-team', + })) + expect(invalid.events).toEqual([]) + }) + + it('rejects an invalid task dependency before publication', async () => { + const ctx = await setup() + const session = ctx.sessions.create(SessionId('team-task-invariant')) + + expect(() => { + session.append('team/task', { + version: 1, + teamId: TeamId(session.id), + task: { + id: TeamTaskId('task-1'), + revision: 1, + subject: 'invalid dependency', + description: 'references a missing blocker', + status: 'pending', + blockedBy: [TeamTaskId('missing')], + writeScopes: [], + }, + }) + }).toThrow(expect.objectContaining>({ + code: 'INVARIANT', + packageName: '@deepseek-ai/dsh-experimental-agent-team', + })) + expect(session.events).toEqual([]) + }) +}) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 7ae4f780a5..df7c13bf5c 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1290,8 +1290,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { key: 'sandboxPolicy', - summary: 'The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log.', - description: 'The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log.', + summary: 'The sandbox-policy service (`ctx.sandboxPolicy`).', + description: 'The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session\'s mode log and immutable cwd travel together to every enforcing capability.', methods: [ { signature: 'readonly defaultMode: SandboxMode', diff --git a/packages/hooks/hooks-claude-code/src/index.ts b/packages/hooks/hooks-claude-code/src/index.ts index 4dfa3a58f1..e82b728a8e 100644 --- a/packages/hooks/hooks-claude-code/src/index.ts +++ b/packages/hooks/hooks-claude-code/src/index.ts @@ -12,7 +12,7 @@ import { readFileSync } from 'node:fs' import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' -import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' +import type { Agent, PreStepDecision, TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-session-projection' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' @@ -310,8 +310,8 @@ const SUBAGENT_TYPE = 'general-purpose' /** The last open turn number in the agent's log, or 0 without an agent. */ function lastTurn(ctx: Context, agent: Agent | undefined): number { if (!agent) return 0 - /* v8 ignore next -- agent-present hook points run inside AgentLoop, which owns this projection. */ - return ctx.sessionProjections.stateOf(agent.session, 'turnBoundary')?.lastTurn ?? 0 + const boundary = ctx.sessionProjections.stateOf(agent.session, 'turnBoundary') as TurnBoundaryProjection + return boundary.lastTurn } /** Flatten content blocks to the text a hook payload carries (the common case). */ diff --git a/packages/interaction/permission-presets/src/index.ts b/packages/interaction/permission-presets/src/index.ts index 5d6eef92f9..b6025fd98b 100644 --- a/packages/interaction/permission-presets/src/index.ts +++ b/packages/interaction/permission-presets/src/index.ts @@ -25,7 +25,7 @@ import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-sett // Type-only: resolves the optional projection and command children. import type {} from '@deepseek-ai/dsh-session-projection' import type {} from '@deepseek-ai/dsh-commands' -import type { KnobState, PermissionSelect, PresetOption } from './types.ts' +import type { PermissionSelect, PresetOption } from './types.ts' export type * from './types.ts' @@ -35,12 +35,6 @@ declare module '@deepseek-ai/cordis' { } } -/** Projection state for permission overrides and constructor-seed provenance. */ -interface PermissionProjectionState extends KnobState { - /** Whether the log contains a constructor-seed boundary. */ - seeded: boolean -} - declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionStateMap { /** Latest logged permission overrides and constructor-seed provenance. */ @@ -81,8 +75,26 @@ export const CUSTOM_PRESET = 'custom' /** Settings namespace carrying the default for future sessions. */ export const PERMISSION_SETTINGS_NAMESPACE = settingsNamespace('permission') -const permissionStateSchema: zod.ZodType = zod.object({ +/** + * The projection unit's knob state: the last seen value of each knob event, + * null before an override (composition defaults apply at view time). + */ +export interface KnobState { /** Last `permission/preset` payload, or null. */ + preset: string | null + /** Last `sandbox/mode` payload, or null. */ + sandbox: SandboxMode | null + /** Last `approval/policy` payload, or null. */ + approval: ApprovalPolicy | null +} + +/** Projection state for permission overrides and constructor-seed provenance. */ +interface PermissionProjectionState extends KnobState { + /** Whether the log contains a constructor-seed boundary. */ + seeded: boolean +} + +const permissionStateSchema: zod.ZodType = zod.object({ preset: zod.string().nullable(), sandbox: zod.union([ zod.literal('read-only'), @@ -96,9 +108,6 @@ const permissionStateSchema: zod.ZodType = zod.object /** State for the empty log: every knob at its composition default. */ const EMPTY_KNOBS: KnobState = { preset: null, sandbox: null, approval: null } -/** Projection state for a genuinely fresh session. */ -const EMPTY_PERMISSION_STATE: PermissionProjectionState = { ...EMPTY_KNOBS, seeded: false } - /** * One-event permission-state transition (the projection unit's `apply`). Unrelated * events return the same reference — the registry's change gate. @@ -118,7 +127,7 @@ function applyPermissionEvent( case 'approval/policy': return { ...state, approval: event.data.policy } case 'session/end-seed': - return state.seeded ? state : { ...state, seeded: true } + return { ...state, seeded: true } default: return state } @@ -230,7 +239,7 @@ export class PermissionPresetService extends Service { key: 'permissions', stateVersion: 2, stateSchema: permissionStateSchema, - init: () => EMPTY_PERMISSION_STATE, + init: () => ({ ...EMPTY_KNOBS, seeded: false }), apply: applyPermissionEvent, wire: { viewSchema: selectSchema, view: state => this.selectFor(state) }, }) diff --git a/packages/interaction/permission-presets/src/types.ts b/packages/interaction/permission-presets/src/types.ts index 9447ea5cb5..a978e3b5e8 100644 --- a/packages/interaction/permission-presets/src/types.ts +++ b/packages/interaction/permission-presets/src/types.ts @@ -2,15 +2,13 @@ * Pure types of the permission domain: the ONE home of the `permissions` * projection-key declaration plus its payload types, free of this package's * host-side value imports (cordis, schemastery). Two namespace projections - * serve it — the package root re-export for host consumers and `./client` for - * client aggregates — with zero content duplication. + * serve it — the package root re-export for host consumers, `./client` (the + * browser half-entry's re-export) for client aggregates — with zero content + * duplication. * * @module @deepseek-ai/dsh-permission-presets/types */ -import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' -import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval' - /** The select-option shape a presentation layer advertises for one preset (or for the derived `custom` state). */ export interface PresetOption { /** Stable option value: the table key, or `custom`. */ @@ -33,16 +31,6 @@ export interface PermissionSelect { currentValue: string } -/** Latest logged permission overrides. */ -export interface KnobState { - /** Last `permission/preset` payload, or null. */ - preset: string | null - /** Last `sandbox/mode` payload, or null. */ - sandbox: SandboxMode | null - /** Last `approval/policy` payload, or null. */ - approval: ApprovalPolicy | null -} - declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionMap { /** diff --git a/packages/llm/llm-retry/src/index.ts b/packages/llm/llm-retry/src/index.ts index a5d013ba29..89ffc14c40 100644 --- a/packages/llm/llm-retry/src/index.ts +++ b/packages/llm/llm-retry/src/index.ts @@ -217,8 +217,8 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna } const policyKey = retryPolicyKey(policy) - const retryState = ctx.sessionProjections.stateOf(agent.session, 'llmRetry') - const previous = retryState?.[retryStateKey(provider, policyKey)] + const retryState = ctx.sessionProjections.stateOf(agent.session, 'llmRetry') as LlmRetryState + const previous = retryState[retryStateKey(provider, policyKey)] const previousRetry = previous?.retry ?? 0 if (policy.mode === 'normal' && previousRetry >= policy.maxRetries) return next() const retry = previousRetry + 1 diff --git a/packages/sandbox/sandbox-policy/src/index.ts b/packages/sandbox/sandbox-policy/src/index.ts index 0c65356c5b..efc61572e6 100644 --- a/packages/sandbox/sandbox-policy/src/index.ts +++ b/packages/sandbox/sandbox-policy/src/index.ts @@ -85,12 +85,6 @@ export interface SandboxPolicyRequest { mode?: SandboxMode } -/** - * The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment - * default mode, fallback workspace root, and current request-time policy - * section. Tool layers call {@link resolve} for each execution so a session's - * mode log and immutable cwd travel together to every enforcing capability. - */ /** The sandbox-mode projection's state schema (state equals the public shape). */ const sandboxModeStateSchema = zod.union([ zod.literal('read-only'), @@ -106,7 +100,12 @@ declare module '@deepseek-ai/dsh-session-projection/types' { } } -/** The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log. */ +/** + * The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment + * default mode, fallback workspace root, and current request-time policy + * section. Tool layers call {@link resolve} for each execution so a session's + * mode log and immutable cwd travel together to every enforcing capability. + */ export class SandboxPolicyService extends Service { // Inline schema call: the config catalog walks `static Config` statically. static Config: z = z.object({ diff --git a/packages/session/session-title/src/index.ts b/packages/session/session-title/src/index.ts index a72f8baa6b..9c22e3c65f 100644 --- a/packages/session/session-title/src/index.ts +++ b/packages/session/session-title/src/index.ts @@ -7,6 +7,7 @@ import { Context, FiberState, Service, type Fiber } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { z as zod } from 'zod' import type { ZodType } from 'zod' +import type { Branded } from '@deepseek-ai/dsh-brand' import { assertNever, deepFreeze, isAgentLoopRequest } from '@deepseek-ai/dsh-llm' import type { GenerateOptions } from '@deepseek-ai/dsh-llm' import type { @@ -35,8 +36,8 @@ import type { TitleProjection, } from './types.ts' -/** Branded session-title provider identity. */ -export type SessionTitleProviderId = import('./types.ts').SessionTitleProviderId +/** Identifies one session-title provider registration. */ +export type SessionTitleProviderId = Branded<'SessionTitleProviderId'> /** * Brand a raw provider id. @@ -192,15 +193,12 @@ function assertPositiveInteger(name: keyof Config, value: number): void { } } -/** The title unit's folded state — the projection's full public face. */ -export type TitleUnitState = TitleProjection - /** * Convert title projection state into an immutable snapshot. * @param state - the title unit's folded state. * @returns the immutable snapshot. */ -export function titleSnapshotFromState(state: TitleUnitState): SessionTitleSnapshot { +function titleSnapshotFromState(state: TitleProjection): SessionTitleSnapshot { return deepFreeze({ title: state.title, messageSeqs: [...state.messageSeqs], @@ -272,7 +270,7 @@ const titleProjectionSchema = zod.object({ ]), eventSeq: zod.number().int().nonnegative(), updatedAt: zod.number(), -}).nullable() as unknown as ZodType +}).nullable() as unknown as ZodType const titleViewSchema: ZodType = zod.string().min(1).nullable() @@ -295,7 +293,7 @@ export const titleProjectionDefinition = { viewSchema: titleViewSchema, view: state => state?.title ?? null, }, -} satisfies ProjectionDefinition<'title', TitleUnitState | null> +} satisfies ProjectionDefinition<'title', TitleProjection | null> /** * Fold the latest title from a session log. @@ -303,7 +301,7 @@ export const titleProjectionDefinition = { * @returns the immutable latest title snapshot, or `undefined`. */ export function foldSessionTitle(events: readonly SessionEvent[]): SessionTitleSnapshot | undefined { - let state: TitleUnitState | null = titleProjectionDefinition.init() + let state: TitleProjection | null = titleProjectionDefinition.init() for (const event of events) state = titleProjectionDefinition.apply(state, event) return state === null ? undefined : titleSnapshotFromState(state) } @@ -400,8 +398,8 @@ export class SessionTitleService extends Service { * @returns latest title snapshot, or `undefined` before eligible input. */ get(session: Session): SessionTitleSnapshot | undefined { - const state = this.ctx.sessionProjections.stateOf(session, 'title') - return state === null || state === undefined ? undefined : titleSnapshotFromState(state) + const state = this.ctx.sessionProjections.stateOf(session, 'title') as TitleProjection | null + return state === null ? undefined : titleSnapshotFromState(state) } /** @@ -734,8 +732,7 @@ export class SessionTitleService extends Service { } private titleInputOf(session: Session): TitleInputState { - /* v8 ignore next -- session-title registers its own titleInput unit, so the key is always present */ - return this.ctx.sessionProjections.stateOf(session, 'titleInput') ?? EMPTY_TITLE_INPUT + return this.ctx.sessionProjections.stateOf(session, 'titleInput') as TitleInputState } /** Queue detached service work and retain it through service disposal. */ diff --git a/packages/subagent/tool-subagent/src/model-selection-state.ts b/packages/subagent/tool-subagent/src/model-selection-state.ts index 3ef592f5d7..d3b04b828b 100644 --- a/packages/subagent/tool-subagent/src/model-selection-state.ts +++ b/packages/subagent/tool-subagent/src/model-selection-state.ts @@ -46,7 +46,7 @@ export const subagentModelSelectionProjectionDefinition = { if (allowedModels.length === 0) { throw new Error('subagent/model-selection-policy requires at least one route') } - return allowedModels.map(route => ({ ...route })) + return allowedModels }, } satisfies ProjectionDefinition<'subagentModelSelectionPolicy', AllowedModelRoute[] | null>