Merge remote-tracking branch 'origin/master' into feat/plan-mode

Master arrivals re-grafted onto the plan-mode surfaces:
- Package invariant runtime contracts: every package owns ./invariant —
  added the dsh-mode companion (mode/set payload validation: non-empty
  trimmed name), its unit spec, exports/peer wiring, and the mode group in
  the tsconfig invariant paths glob.
- Interception waterfalls gained an AbortSignal parameter
  (agent/prompt-submit, agent/turn-continuation) and ToolExecutionInput.signal
  became required: listeners and test dispatch updated; the exit tool now
  forwards exec.signal unconditionally.
- TUI question dialog: master's position/unanswered header layout kept,
  the plan-review detail block re-grafted between question and answers.
- LSP capability family arrived: lsp tool joins the catalog pin, dsh-lsp
  examples deps beside dsh-mode; lsp-definition snapshot refreshed with
  the mode command and modes advertisement.
- docs/architecture.md and packages/README.md regenerated tables re-grafted
  (ctx.modes row, mode package row) and condensed within word ceilings.
- examples/acp-agent cordis.yml: kept the deployment-owned plan instructions
  (incl. the conversational-agreement sentence) beside master's routed-model
  compaction comment; snapshot expected outputs refreshed keyless and noise
  fixtures restored.
This commit is contained in:
kingwl
2026-07-22 02:13:39 +08:00
773 changed files with 24980 additions and 4428 deletions

View File

@@ -11,11 +11,16 @@
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
@@ -24,6 +29,7 @@
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-commands": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
@@ -40,6 +46,7 @@
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-code-runtime": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",

View File

@@ -231,7 +231,7 @@ export class ModesService extends Service {
// flushed mode therefore lands before the prompt that should reflect it.
// Contained: policy must never block a prompt or turn; onBoundary can throw
// only when session.append rejects during teardown.
ctx.on('agent/prompt-submit', (agent, _content, _source, next) => {
ctx.on('agent/prompt-submit', (agent, _content, _source, _signal, next) => {
try {
this.onBoundary(agent)
} catch (error) {
@@ -239,7 +239,7 @@ export class ModesService extends Service {
}
return next()
})
ctx.on('agent/turn-continuation', (agent, _turn, _decision, next) => {
ctx.on('agent/turn-continuation', (agent, _turn, _decision, _signal, next) => {
try {
this.onBoundary(agent)
} catch (error) {
@@ -335,7 +335,7 @@ export class ModesService extends Service {
],
}],
agent,
...exec.signal ? { signal: exec.signal } : {},
signal: exec.signal,
})
const reviewItems = answer.answers.filter(entry => entry.id === 'plan-review')
const item = reviewItems.length === 1 ? reviewItems[0] : undefined

View File

@@ -0,0 +1,48 @@
/** Package-owned durable mode-stream invariants. @module @deepseek-ai/dsh-mode/invariant */
import type { Context } from 'cordis'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-mode'
/** Cordis companion plugin name. */
export const name = 'mode-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* Validate one `mode/set` payload before it reaches the durable log: the mode
* is a non-empty bare name (config-declared vocabulary, not an opaque id), so
* an empty or non-string value can only be a writer bug — folding it would
* silently select the default mode while the log claims otherwise.
*/
function validateEvent(event: SessionEvent, fail: InvariantFailure): void {
if (event.type !== 'mode/set') return
const mode = (event.data as { mode?: unknown }).mode
if (typeof mode !== 'string' || mode.trim() === '' || mode.trim() !== mode) {
fail(`mode/set carries invalid mode ${JSON.stringify(mode)}; expected a non-empty trimmed name`)
}
}
/* jscpd:ignore-start -- package companions share replay and dispatch plumbing */
/** Install validation for loaded and newly appended mode selections. */
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
for (const session of ctx.sessions.list()) {
for (const event of session.events) validateEvent(event, fail)
}
ctx.on('internal/dispatch', (_mode, eventName, args) => {
if (eventName !== 'session/event') return
const event = (args as [Session, SessionEvent])[1]
validateEvent(event, fail)
}, { global: true })
}, { inject: ['sessions'] })
/* jscpd:ignore-end */
/**
* Register the mode invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))

View File

@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import SessionStore, { type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
import * as ModeInvariant from '@deepseek-ai/dsh-mode/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService, { enabled: true })
await ctx.plugin(ModeInvariant)
return ctx
}
function event(mode: unknown): SessionEvent {
return { type: 'mode/set', seq: 0, time: 0, data: { mode } } as SessionEvent
}
describe('mode stream invariants', () => {
it('accepts a plain non-empty trimmed mode name', async () => {
const ctx = await setup()
expect(() => { ctx.emit('session/event', {} as Session, event('plan')) }).not.toThrow()
expect(() => { ctx.emit('session/event', {} as Session, event('default')) }).not.toThrow()
})
it.each([
[42, /invalid mode 42/],
['', /invalid mode ""/],
[' plan ', /invalid mode " plan "/],
])('rejects an invalid durable mode selection', async (mode, message) => {
const ctx = await setup()
expect(() => { ctx.emit('session/event', {} as Session, event(mode)) }).toThrow(message)
})
it('ignores unrelated dispatches and session events', async () => {
const ctx = await setup()
expect(() => {
ctx.emit('tools/change')
ctx.emit('session/event', {} as Session, {
type: 'turn/start', seq: 0, time: 0, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
})
}).not.toThrow()
})
it('rejects an invalid existing selection on late registration', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
ctx.sessions.create().append('mode/set', { mode: '' })
await ctx.plugin(InvariantService, { enabled: true })
await expect(ctx.plugin(ModeInvariant).then(() => undefined)).rejects.toThrow(/invalid mode ""/)
})
})

View File

@@ -63,11 +63,11 @@ async function boundary(ctx: Context, agent: Agent & { session: Session }, type:
const events = agentEvents(ctx, agent)
if (type === 'turn/start') {
agent.session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
await events.waterfall('agent/prompt-submit', [{ type: 'text', text: 'boundary probe' }], { kind: 'user' }, () => Promise.resolve({ kind: 'allow' }))
await events.waterfall('agent/prompt-submit', [{ type: 'text', text: 'boundary probe' }], { kind: 'user' }, new AbortController().signal, () => Promise.resolve({ kind: 'allow' }))
return
}
agent.session.append('step/end', { turn: 1, step: 1 })
await events.waterfall('agent/turn-continuation', 1, { action: 'stop' }, () => Promise.resolve({ action: 'stop' }))
await events.waterfall('agent/turn-continuation', 1, { action: 'stop' }, new AbortController().signal, () => Promise.resolve({ action: 'stop' }))
}
/** Dispatch the closed-step recovery seam with one terminal decision. */
@@ -116,6 +116,7 @@ function execute(ctx: Context, name: string, agent?: Agent) {
callId: CallId(`call-${++callCounter}`),
name,
arguments: {},
signal: new AbortController().signal,
...agent ? { agent } : {},
})
}
@@ -586,6 +587,7 @@ describe('exit_plan_mode', () => {
callId: CallId(`call-exit-${++callCounter}`),
name: EXIT_PLAN_MODE,
arguments: { plan },
signal: new AbortController().signal,
...agent ? { agent } : {},
})
}
@@ -690,6 +692,7 @@ describe('exit_plan_mode', () => {
callId: CallId(`call-exit-${++callCounter}`),
name: RUN_CODE_NAME,
arguments: { code: `return await tools.${EXIT_PLAN_MODE}({ plan: ${JSON.stringify(plan)} })` },
signal: new AbortController().signal,
agent,
})

View File

@@ -31,6 +31,9 @@
},
{
"path": "../../ui/commands"
},
{
"path": "../../support/invariants"
}
]
}