feat(invariants): require package-owned companions

This commit is contained in:
Tianyi Cui
2026-07-19 22:13:50 +08:00
parent 2c7822ddee
commit 433670a754
316 changed files with 5348 additions and 646 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"
@@ -26,12 +31,14 @@
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-loader-smoke": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",

View File

@@ -0,0 +1,30 @@
/**
* Generated invariant ownership companion for `@deepseek-ai/dsh-time-context`.
* Replace this file with package-owned checks while preserving its registration.
*
* @generated scripts/gen-package-invariants.ts
* @module @deepseek-ai/dsh-time-context/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-time-context'
/** Cordis companion plugin name. */
export const name = 'time-context-invariant'
/** Services required before the companion can register. */
export const inject = ['invariants']
/** Reserve this package's invariant ownership until it adds relational checks. */
const install: InvariantInstaller = () => {}
/**
* Register this package's 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))
/* jscpd:ignore-end */

View File

@@ -4,8 +4,7 @@ import Loader from '@cordisjs/plugin-loader'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { defineTool } from '@deepseek-ai/dsh-tools'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
@@ -83,7 +82,7 @@ async function fire(
step: number,
signal: AbortSignal = SIGNAL,
): Promise<void> {
await ctx.serial('agent/pre-step', agent, turn, step, signal)
await agentEvents(ctx, agent).serial('agent/pre-step', turn, step, signal)
}
function textResponse(text: string): StreamChunk[] {

View File

@@ -6,13 +6,32 @@
},
"include": ["src"],
"references": [
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../../vendor/schemastery" },
{ "path": "../../llm/llm" },
{ "path": "../../core/agent" },
{ "path": "../../core/system-prompt" },
{ "path": "../../core/agent" },
{ "path": "../../support/loader-smoke" }
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../llm/llm"
},
{
"path": "../../core/agent"
},
{
"path": "../../core/system-prompt"
},
{
"path": "../../core/agent"
},
{
"path": "../../support/loader-smoke"
},
{
"path": "../../support/invariants"
}
]
}

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-fs": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
@@ -39,6 +45,7 @@
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",

View File

@@ -0,0 +1,30 @@
/**
* Generated invariant ownership companion for `@deepseek-ai/dsh-workspace-context`.
* Replace this file with package-owned checks while preserving its registration.
*
* @generated scripts/gen-package-invariants.ts
* @module @deepseek-ai/dsh-workspace-context/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-workspace-context'
/** Cordis companion plugin name. */
export const name = 'workspace-context-invariant'
/** Services required before the companion can register. */
export const inject = ['invariants']
/** Reserve this package's invariant ownership until it adds relational checks. */
const install: InvariantInstaller = () => {}
/**
* Register this package's 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))
/* jscpd:ignore-end */

View File

@@ -7,8 +7,9 @@ import Loader from '@cordisjs/plugin-loader'
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
import LlmService, { CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent } from '@deepseek-ai/dsh-session'
import AgentRegistry, { type Agent, type HookContext } from '@deepseek-ai/dsh-agent'
import AgentRegistry, { agentEvents, type Agent, type HookContext } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import type {
FsDirEntry,
@@ -23,7 +24,12 @@ import type {
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import type { ToolExecution, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import type {
PostToolDecision,
ToolExecution,
ToolExecutionResult,
ToolExecutionToken,
} from '@deepseek-ai/dsh-tools'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import {
discoverBaselineInstructionFiles,
@@ -228,14 +234,31 @@ const composedPrefixes = new WeakMap<object, Message[]>()
async function composeBaselinePrefix(ctx: Context, agent: Agent): Promise<Message[]> {
const empty: Message[] = []
const prefix = await ctx.waterfall(
'agent/session-prefix', agent, empty, AbortSignal.timeout(1000),
const prefix = await agentEvents(ctx, agent).waterfall(
'agent/session-prefix', empty, AbortSignal.timeout(1000),
() => Promise.resolve(empty),
)
composedPrefixes.set(agent, prefix)
return prefix
}
function toolEventCarrier(ctx: Context, exec: ToolExecution) {
return scopeTarget(ctx.get('tools') ?? ctx as unknown as ToolRegistry, exec.agent)
}
function postExecute(
ctx: Context,
exec: ToolExecution,
result: Readonly<ToolExecutionResult>,
next: () => Promise<PostToolDecision>,
): Promise<PostToolDecision> {
return ctx.waterfall(toolEventCarrier(ctx, exec), 'tools/post-execute', exec, result, next)
}
function emitToolResult(ctx: Context, exec: ToolExecution, result: Readonly<ToolExecutionResult>): void {
ctx.emit(toolEventCarrier(ctx, exec), 'tools/result', exec, result)
}
function derivedText(agent: Agent): string {
return blocksText(composedPrefixes.get(agent)?.[0]?.content)
}
@@ -796,7 +819,7 @@ describe('workspace context request injection', () => {
try {
await ctx.plugin(workspaceContext, { maxBytes: 65536 })
const decision = await ctx.waterfall('tools/post-execute', stubToolExecution({
const decision = await postExecute(ctx, stubToolExecution({
callId: CallId('no-fs-post-execute'),
name: 'read',
arguments: { file_path: 'pkg/file.txt' },
@@ -843,7 +866,7 @@ describe('workspace context request injection', () => {
}
// A later PostToolUse-style policy blocks this otherwise-successful read.
const blocked = await ctx.waterfall('tools/post-execute', exec, result, async () => ({
const blocked = await postExecute(ctx, exec, result, async () => ({
kind: 'block' as const,
feedback: [{ type: 'text' as const, text: 'blocked by policy' }],
}))
@@ -857,7 +880,7 @@ describe('workspace context request injection', () => {
// The same read, when the downstream accepts, DOES surface the nested
// instructions — proving the block branch above is what suppressed them,
// and that the block did not consume the pending nested change.
const accepted = await ctx.waterfall('tools/post-execute', exec, result, async () => ({
const accepted = await postExecute(ctx, exec, result, async () => ({
kind: 'accept' as const,
}))
expect(accepted.kind).toBe('accept')
@@ -1169,8 +1192,9 @@ describe('workspace context request injection', () => {
const controller = new AbortController()
const reason = new Error('cancel prefix')
const empty: Message[] = []
const pending = ctx.waterfall(
'agent/session-prefix', stubAgent(root), empty, controller.signal,
const agent = stubAgent(root)
const pending = agentEvents(ctx, agent).waterfall(
'agent/session-prefix', empty, controller.signal,
() => Promise.resolve(empty),
)
@@ -1660,7 +1684,7 @@ describe('dynamic nested workspace context injection', () => {
signal: controller.signal,
})
const pending = ctx.waterfall('tools/post-execute', exec, {
const pending = postExecute(ctx, exec, {
content: [{ type: 'text', text: 'ok' }],
isError: false,
}, () => Promise.resolve({ kind: 'accept' as const }))
@@ -2387,12 +2411,12 @@ describe('dynamic nested workspace context injection', () => {
isError: false,
}
const failedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({
const failedStat = await postExecute(ctx, stubToolExecution({
callId: CallId('provider-stat-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
}), result, async () => ({ kind: 'accept' as const }))
fs.throwOnStat.clear()
fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'directory' })
const mismatchedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({
const mismatchedStat = await postExecute(ctx, stubToolExecution({
callId: CallId('provider-stat-mismatch'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
}), result, async () => ({ kind: 'accept' as const }))
@@ -2626,19 +2650,19 @@ describe('dynamic nested workspace context injection', () => {
const parent = Symbol('parent') as ToolExecutionToken
const plainResult = { callId: CallId('plain'), content: [], isError: false }
ctx.emit('tools/result', stubToolExecution({
emitToolResult(ctx, stubToolExecution({
callId: CallId('agentless-child'), name: 'read', arguments: {}, parent,
}), plainResult)
ctx.emit('tools/result', stubToolExecution({
emitToolResult(ctx, stubToolExecution({
callId: CallId('contextless-child'), name: 'read', arguments: {}, agent, parent,
}), { ...plainResult, additionalContexts: [{ content: [], source: { kind: 'plugin', plugin: 'workspace-context' } }] })
ctx.emit('tools/result', stubToolExecution({
emitToolResult(ctx, stubToolExecution({
callId: CallId('first-child'), name: 'read', arguments: {}, agent, parent,
}), { ...plainResult, additionalContexts: [workspaceChangeContext('first', 'one')] })
ctx.emit('tools/result', stubToolExecution({
emitToolResult(ctx, stubToolExecution({
callId: CallId('second-child'), name: 'read', arguments: {}, agent, parent,
}), { ...plainResult, additionalContexts: [workspaceChangeContext('second', 'two')] })
ctx.emit('tools/result', {
emitToolResult(ctx, {
...stubToolExecution({ callId: CallId('agentless-parent'), name: 'composite', arguments: {} }),
token: parent,
}, plainResult)
@@ -2674,7 +2698,7 @@ describe('dynamic nested workspace context injection', () => {
]
for (const item of cases) {
const decision = await ctx.waterfall('tools/post-execute', stubToolExecution({
const decision = await postExecute(ctx, stubToolExecution({
callId: CallId(`manual-${item.name}-${cases.indexOf(item)}`),
name: item.name,
arguments: item.arguments,

View File

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