Merge master into worktree-windows-runtime
This commit is contained in:
@@ -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"
|
||||
@@ -23,6 +28,7 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-fs": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -31,6 +37,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
}
|
||||
|
||||
30
packages/fs/fs-local/src/invariant.ts
Normal file
30
packages/fs/fs-local/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-fs-local`.
|
||||
* @module @deepseek-ai/dsh-fs-local/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-fs-local'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'fs-local-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
||||
* beyond contracts enforced at its owning seam.
|
||||
*/
|
||||
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 */
|
||||
@@ -6,10 +6,23 @@
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../../../vendor/cosmokit" },
|
||||
{ "path": "../../../vendor/cordis" },
|
||||
{ "path": "../../../vendor/schemastery" },
|
||||
{ "path": "../../llm/llm" },
|
||||
{ "path": "../fs" }
|
||||
{
|
||||
"path": "../../../vendor/cosmokit"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/schemastery"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
{
|
||||
"path": "../fs"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -23,10 +28,12 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-fs": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
}
|
||||
|
||||
30
packages/fs/fs-policy/src/invariant.ts
Normal file
30
packages/fs/fs-policy/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-fs-policy`.
|
||||
* @module @deepseek-ai/dsh-fs-policy/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-fs-policy'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'fs-policy-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
||||
* beyond contracts enforced at its owning seam.
|
||||
*/
|
||||
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 */
|
||||
@@ -6,9 +6,20 @@
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../../../vendor/cosmokit" },
|
||||
{ "path": "../../../vendor/cordis" },
|
||||
{ "path": "../../llm/llm" },
|
||||
{ "path": "../fs" }
|
||||
{
|
||||
"path": "../../../vendor/cosmokit"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
{
|
||||
"path": "../fs"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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-fs": "^0.0.1",
|
||||
"@deepseek-ai/dsh-fs-local": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
@@ -31,6 +37,7 @@
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
|
||||
27
packages/fs/fs-sandbox/src/invariant.ts
Normal file
27
packages/fs/fs-sandbox/src/invariant.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-fs-sandbox`.
|
||||
* @module @deepseek-ai/dsh-fs-sandbox/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-fs-sandbox'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'fs-sandbox-invariant'
|
||||
/** Services required before the companion can register. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/** No runtime invariant: this stateless adapter delegates policy and filesystem relations to their owning seams. */
|
||||
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 */
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../sandbox/sandbox-policy"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -23,12 +28,14 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-brand": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
|
||||
39
packages/fs/fs/src/invariant.ts
Normal file
39
packages/fs/fs/src/invariant.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/** Package-owned filesystem event-data invariants. @module @deepseek-ai/dsh-fs/invariant */
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
import type { FsTarget, FsVersion } from './types.ts'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-fs'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'fs-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/** Assert that an event carries a usable opaque target identity. */
|
||||
function validateTarget(target: FsTarget, fail: (message: string) => never): void {
|
||||
if (target.targetKey.length === 0) fail('filesystem event targetKey must be non-empty')
|
||||
if (target.displayPath.length === 0) fail('filesystem event displayPath must be non-empty')
|
||||
}
|
||||
|
||||
/** Install checks over the filesystem decision and observation event stream. */
|
||||
const install: InvariantInstaller = (ctx, fail) => {
|
||||
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
||||
if (eventName !== 'fs/write-intent'
|
||||
&& eventName !== 'fs/edit-intent'
|
||||
&& eventName !== 'fs/observed') return
|
||||
validateTarget(args[0] as FsTarget, fail)
|
||||
if (eventName === 'fs/observed' && (args[1] as FsVersion).length === 0) {
|
||||
fail('fs/observed version must be non-empty')
|
||||
}
|
||||
}, { global: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the filesystem 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))
|
||||
44
packages/fs/fs/tests/invariant.spec.ts
Normal file
44
packages/fs/fs/tests/invariant.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
|
||||
import type { FsTarget } from '@deepseek-ai/dsh-fs'
|
||||
import * as FsInvariant from '@deepseek-ai/dsh-fs/invariant'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
async function setup(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(InvariantService)
|
||||
await ctx.plugin(FsInvariant)
|
||||
return ctx
|
||||
}
|
||||
|
||||
const target = (key = 'file:1', displayPath = 'file.txt'): FsTarget => ({
|
||||
targetKey: FsTargetKey(key),
|
||||
displayPath,
|
||||
})
|
||||
|
||||
describe('filesystem invariants', () => {
|
||||
it('accepts decision and observation events with usable identities', async () => {
|
||||
const ctx = await setup()
|
||||
await expect(ctx.waterfall(
|
||||
ctx as never, 'fs/write-intent', target(), undefined,
|
||||
() => Promise.resolve(undefined),
|
||||
)).resolves.toBeUndefined()
|
||||
await expect(ctx.waterfall(
|
||||
ctx as never, 'fs/edit-intent', target(), undefined,
|
||||
() => Promise.resolve(undefined),
|
||||
)).resolves.toBeUndefined()
|
||||
expect(() => { ctx.emit('fs/observed', target(), FsVersion('v1'), undefined) }).not.toThrow()
|
||||
expect(() => { ctx.emit('tools/change') }).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects empty target and version identities', async () => {
|
||||
const ctx = await setup()
|
||||
expect(() => { ctx.emit('fs/observed', target(''), FsVersion('v1'), undefined) })
|
||||
.toThrow(/targetKey must be non-empty/)
|
||||
expect(() => { ctx.emit('fs/observed', target('file:1', ''), FsVersion('v1'), undefined) })
|
||||
.toThrow(/displayPath must be non-empty/)
|
||||
expect(() => { ctx.emit('fs/observed', target(), FsVersion(''), undefined) })
|
||||
.toThrow(/version must be non-empty/)
|
||||
})
|
||||
})
|
||||
@@ -6,10 +6,23 @@
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../../../vendor/cosmokit" },
|
||||
{ "path": "../../../vendor/cordis" },
|
||||
{ "path": "../../util/brand" },
|
||||
{ "path": "../../llm/llm" },
|
||||
{ "path": "../../sandbox/sandbox" }
|
||||
{
|
||||
"path": "../../../vendor/cosmokit"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../util/brand"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../sandbox/sandbox"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,6 +31,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-retention": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
@@ -38,6 +44,7 @@
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-retention": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
|
||||
30
packages/fs/tool-fs-search/src/invariant.ts
Normal file
30
packages/fs/tool-fs-search/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-fs-search`.
|
||||
* @module @deepseek-ai/dsh-tool-fs-search/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-fs-search'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'tool-fs-search-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution
|
||||
* relations are owned by the capability seam it calls.
|
||||
*/
|
||||
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 */
|
||||
@@ -162,7 +162,7 @@ export async function runRipgrep(
|
||||
command,
|
||||
stdoutMaxBytes: rawOutputMaxBytes,
|
||||
...cwd !== undefined ? { workdir: cwd } : {},
|
||||
...exec.signal ? { signal: exec.signal } : {},
|
||||
signal: exec.signal,
|
||||
})
|
||||
let result: BashRunResult
|
||||
try {
|
||||
|
||||
@@ -16,10 +16,12 @@ import { join } from 'node:path'
|
||||
import { Context } from 'cordis'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
|
||||
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
||||
import * as ToolFsSearch from '@deepseek-ai/dsh-tool-fs-search'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
const hasRg = spawnSync('rg', ['--version'], { encoding: 'utf8' }).status === 0
|
||||
|
||||
let dir: string
|
||||
@@ -28,6 +30,7 @@ let ctx: Context
|
||||
let callCounter = 0
|
||||
function call(name: string, args: unknown, agentObj?: object) {
|
||||
return ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId(`it-${++callCounter}`),
|
||||
name,
|
||||
arguments: args,
|
||||
@@ -165,8 +168,8 @@ describe.skipIf(!hasRg)('search tools over the real bash executor + real rg', ()
|
||||
})
|
||||
})
|
||||
|
||||
describe('bash-start infrastructure failures stay in the SEARCH_* taxonomy', () => {
|
||||
it('a pre-aborted exec.signal (real executor rejects before spawn) is SEARCH_ABORTED', async () => {
|
||||
describe('pre-dispatch cancellation and bash-start failures', () => {
|
||||
it('a pre-aborted registry call is ABORTED_BEFORE_DISPATCH', async () => {
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
const result = await ctx.tools.execute({
|
||||
@@ -176,7 +179,7 @@ describe.skipIf(!hasRg)('search tools over the real bash executor + real rg', ()
|
||||
signal: controller.signal,
|
||||
})
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.error).toMatchObject({ name: 'SearchError', code: 'SEARCH_ABORTED' })
|
||||
expect(result.error).toMatchObject({ name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH })
|
||||
})
|
||||
|
||||
it('an unusable session cwd (spawn failure) is SEARCH_FAILED', async () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Context } from 'cordis'
|
||||
import { join } from 'node:path'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
|
||||
import { BashExecutor } from '@deepseek-ai/dsh-bash'
|
||||
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash'
|
||||
import { SpillLocator, SpillStore } from '@deepseek-ai/dsh-spill'
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
toWorkdirRelative,
|
||||
} from '@deepseek-ai/dsh-tool-fs-search'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
const RG_PROBE_COMMAND = 'command -v rg >/dev/null 2>&1'
|
||||
|
||||
/** A successful run result over the given stdout; overrides script the failure shapes. */
|
||||
@@ -60,6 +61,7 @@ class FakeBash extends BashExecutor {
|
||||
requests: BashExecRequest[] = []
|
||||
specs: BashExecSpec[] = []
|
||||
startCalls = 0
|
||||
forwardSignal = true
|
||||
probeResult: BashRunResult = runResult('')
|
||||
probeError?: Error
|
||||
handler: (spec: BashExecSpec) => BashRunResult = () => runResult('')
|
||||
@@ -72,7 +74,7 @@ class FakeBash extends BashExecutor {
|
||||
workdir: request.workdir ?? '/work',
|
||||
timeoutMs: request.timeoutMs ?? 60_000,
|
||||
stdoutMaxBytes: request.stdoutMaxBytes ?? 64_000,
|
||||
signal: request.signal,
|
||||
...this.forwardSignal ? { signal: request.signal } : {},
|
||||
sandboxMode: request.sandboxMode,
|
||||
}
|
||||
}
|
||||
@@ -148,6 +150,7 @@ const agent = (cwd?: string) => ({ session: { header: { id: 'session-1', ...cwd
|
||||
let callCounter = 0
|
||||
function call(ctx: Context, name: string, args: unknown, options: { agent?: object; signal?: AbortSignal } = {}) {
|
||||
return ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId(`call-${++callCounter}`),
|
||||
name,
|
||||
arguments: args,
|
||||
@@ -303,16 +306,13 @@ describe('workdir derivation and signal forwarding', () => {
|
||||
expect(bash.requests[1]).not.toHaveProperty('workdir')
|
||||
})
|
||||
|
||||
it('forwards exec.signal into the bash spec (the abort reaches the backend)', async () => {
|
||||
it('forwards exec.signal into the bash spec', async () => {
|
||||
const { ctx, bash } = await setup()
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
bash.handler = spec => runResult('', { aborted: spec.signal?.aborted === true })
|
||||
bash.handler = () => runResult('')
|
||||
const result = await call(ctx, 'grep', { pattern: 'x' }, { signal: controller.signal })
|
||||
expect(bash.specs[0]?.signal).toBe(controller.signal)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.error).toMatchObject({ name: 'SearchError', code: 'SEARCH_ABORTED' })
|
||||
expect(text(result)).toContain('aborted')
|
||||
expect(result.isError).toBe(false)
|
||||
})
|
||||
|
||||
it('reports the bash executor timeout as SEARCH_ABORTED with the budget', async () => {
|
||||
@@ -324,20 +324,46 @@ describe('workdir derivation and signal forwarding', () => {
|
||||
expect(text(result)).toContain('timed out after 1234ms')
|
||||
})
|
||||
|
||||
it('translates a run() rejection under a pre-aborted signal into SEARCH_ABORTED', async () => {
|
||||
// The seam contract: run() REJECTS for a pre-aborted signal (it never
|
||||
// spawns). The plain rejection must not escape the SEARCH_* taxonomy.
|
||||
it('skips a pre-aborted registry call before run()', async () => {
|
||||
const { ctx, bash } = await setup()
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
bash.handler = () => { throw new Error('aborted before spawn') }
|
||||
const result = await call(ctx, 'grep', { pattern: 'x' }, { signal: controller.signal })
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.error).toMatchObject({ name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH })
|
||||
expect(bash.specs).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('translates a run() rejection after the forwarded signal aborts', async () => {
|
||||
const { ctx, bash } = await setup()
|
||||
const controller = new AbortController()
|
||||
bash.handler = () => {
|
||||
controller.abort('cancel search')
|
||||
throw new Error('executor stopped on abort')
|
||||
}
|
||||
|
||||
const result = await call(ctx, 'grep', { pattern: 'x' }, { signal: controller.signal })
|
||||
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.error).toMatchObject({ name: 'SearchError', code: 'SEARCH_ABORTED' })
|
||||
expect(text(result)).toContain('aborted before completion')
|
||||
})
|
||||
|
||||
it('translates an aborted executor result after dispatch starts', async () => {
|
||||
const { ctx, bash } = await setup()
|
||||
bash.handler = () => runResult('', { aborted: true, exitCode: null })
|
||||
|
||||
const result = await call(ctx, 'glob', { pattern: '*' })
|
||||
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.error).toMatchObject({ name: 'SearchError', code: 'SEARCH_ABORTED' })
|
||||
expect(text(result)).toContain('aborted before completion')
|
||||
})
|
||||
|
||||
it('translates a run() rejection without an abort (unusable workdir) into SEARCH_FAILED', async () => {
|
||||
const { ctx, bash } = await setup()
|
||||
bash.forwardSignal = false
|
||||
bash.handler = () => { throw new Error('spawn bash ENOENT') }
|
||||
const result = await call(ctx, 'glob', { pattern: '*' })
|
||||
expect(result.isError).toBe(true)
|
||||
|
||||
@@ -6,15 +6,38 @@
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../../../vendor/cosmokit" },
|
||||
{ "path": "../../../vendor/cordis" },
|
||||
{ "path": "../../../vendor/schemastery" },
|
||||
{ "path": "../../util/retention" },
|
||||
{ "path": "../../llm/llm" },
|
||||
{ "path": "../../core/session" },
|
||||
{ "path": "../../core/tools" },
|
||||
{ "path": "../../core/system-prompt" },
|
||||
{ "path": "../../bash/bash" },
|
||||
{ "path": "../../spill/spill" }
|
||||
{
|
||||
"path": "../../../vendor/cosmokit"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/schemastery"
|
||||
},
|
||||
{
|
||||
"path": "../../util/retention"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
{
|
||||
"path": "../../core/tools"
|
||||
},
|
||||
{
|
||||
"path": "../../core/system-prompt"
|
||||
},
|
||||
{
|
||||
"path": "../../bash/bash"
|
||||
},
|
||||
{
|
||||
"path": "../../spill/spill"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -27,6 +32,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-fs": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
|
||||
@@ -40,9 +46,10 @@
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
|
||||
30
packages/fs/tool-fs/src/invariant.ts
Normal file
30
packages/fs/tool-fs/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-fs`.
|
||||
* @module @deepseek-ai/dsh-tool-fs/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-fs'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'tool-fs-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution
|
||||
* relations are owned by the capability seam it calls.
|
||||
*/
|
||||
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 */
|
||||
@@ -106,7 +106,7 @@ export class FsSandboxSurface {
|
||||
agent: exec.agent,
|
||||
callId: exec.callId,
|
||||
toolName,
|
||||
...exec.signal ? { signal: exec.signal } : {},
|
||||
signal: exec.signal,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,6 @@ export function sessionResolveOptions(exec: ToolExecution): { cwd?: string; sign
|
||||
const cwd = sessionCwd(exec)
|
||||
return {
|
||||
...cwd !== undefined ? { cwd } : {},
|
||||
...exec.signal !== undefined ? { signal: exec.signal } : {},
|
||||
signal: exec.signal,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import { join } from 'node:path'
|
||||
import { Context } from 'cordis'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
|
||||
import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
|
||||
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
|
||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
let dir: string
|
||||
let ctx: Context
|
||||
let fiber: Awaited<ReturnType<Context['plugin']>>
|
||||
@@ -26,6 +28,7 @@ const session = { header: {} }
|
||||
let callCounter = 0
|
||||
function call(name: string, args: unknown) {
|
||||
return ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId(`call-${++callCounter}`),
|
||||
name,
|
||||
arguments: args,
|
||||
@@ -299,6 +302,7 @@ describe('per-session cwd', () => {
|
||||
|
||||
const callIn = (sessionObj: object, name: string, args: unknown) =>
|
||||
ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId(`call-${++callCounter}`),
|
||||
name,
|
||||
arguments: args,
|
||||
@@ -344,26 +348,25 @@ describe('signal, concurrency, and the fs/observed contract', () => {
|
||||
const callSig = (signal: AbortSignal, name: string, args: unknown) =>
|
||||
ctx.tools.execute({ callId: CallId(`c-${++callCounter}`), name, arguments: args, agent: { session } as never, signal })
|
||||
const callOwned = (name: string, args: unknown) =>
|
||||
ctx.tools.execute({ callId: CallId(`c-${++callCounter}`), name, arguments: args, agent: { session } as never })
|
||||
ctx.tools.execute({ signal: testToolSignal, callId: CallId(`c-${++callCounter}`), name, arguments: args, agent: { session } as never })
|
||||
|
||||
it('a pre-aborted signal makes read/write/edit return isError FS_ABORTED', async () => {
|
||||
it('a pre-aborted registry call skips read/write/edit with ABORTED_BEFORE_DISPATCH', async () => {
|
||||
await writeFile(join(dir, 'a.txt'), 'hello')
|
||||
const read = await callSig(AbortSignal.abort(), 'read', { file_path: 'a.txt' })
|
||||
expect(read.isError).toBe(true)
|
||||
expect(read.error).toMatchObject({ code: 'FS_ABORTED' })
|
||||
expect(read.error).toMatchObject({ name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH })
|
||||
|
||||
const write = await callSig(AbortSignal.abort(), 'write', { file_path: 'new.txt', content: 'x' })
|
||||
expect(write.isError).toBe(true)
|
||||
expect(write.error).toMatchObject({ code: 'FS_ABORTED' })
|
||||
expect(write.error).toMatchObject({ name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH })
|
||||
await expect(readFile(join(dir, 'new.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
|
||||
// Read first (un-aborted, SAME session owner) so the edit clears the
|
||||
// observation gate; then the aborted edit fails on the signal, not on
|
||||
// FS_NOT_OBSERVED.
|
||||
// observation gate; then the registry skips the aborted edit before its body.
|
||||
expect((await callOwned('read', { file_path: 'a.txt' })).isError).toBe(false)
|
||||
const edit = await callSig(AbortSignal.abort(), 'edit', { file_path: 'a.txt', old_string: 'hello', new_string: 'bye' })
|
||||
expect(edit.isError).toBe(true)
|
||||
expect(edit.error).toMatchObject({ code: 'FS_ABORTED' })
|
||||
expect(edit.error).toMatchObject({ name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH })
|
||||
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('hello') // unchanged
|
||||
})
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import type { FileReadOutcome } from '../src/read-render.ts'
|
||||
import ApprovalService from '@deepseek-ai/dsh-user-approval'
|
||||
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
/** An in-memory fake provider; a test can arm a rejection on any primitive. */
|
||||
class FakeFs extends FileSystem {
|
||||
files = new Map<string, string>()
|
||||
@@ -93,6 +95,7 @@ async function setup() {
|
||||
let callCounter = 0
|
||||
function call(ctx: Context, name: string, args: unknown, agent?: object) {
|
||||
return ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId(`call-${++callCounter}`),
|
||||
name,
|
||||
arguments: args,
|
||||
@@ -112,11 +115,11 @@ describe('registration', () => {
|
||||
|
||||
it('declares read parallel-safe while write/edit remain exclusive', async () => {
|
||||
const { ctx } = await setup()
|
||||
expect(ctx.tools.executionMode({ callId: CallId('read-safe'), name: 'read', arguments: { file_path: 'a.txt' } }))
|
||||
expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('read-safe'), name: 'read', arguments: { file_path: 'a.txt' } }))
|
||||
.toEqual({ kind: 'parallel' })
|
||||
expect(ctx.tools.executionMode({ callId: CallId('write-exclusive'), name: 'write', arguments: { file_path: 'a.txt', content: 'x' } }))
|
||||
expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('write-exclusive'), name: 'write', arguments: { file_path: 'a.txt', content: 'x' } }))
|
||||
.toEqual({ kind: 'exclusive' })
|
||||
expect(ctx.tools.executionMode({ callId: CallId('edit-exclusive'), name: 'edit', arguments: { file_path: 'a.txt', old_string: 'x', new_string: 'y' } }))
|
||||
expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('edit-exclusive'), name: 'edit', arguments: { file_path: 'a.txt', old_string: 'x', new_string: 'y' } }))
|
||||
.toEqual({ kind: 'exclusive' })
|
||||
})
|
||||
|
||||
|
||||
@@ -6,16 +6,41 @@
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../../../vendor/cosmokit" },
|
||||
{ "path": "../../../vendor/cordis" },
|
||||
{ "path": "../../../vendor/schemastery" },
|
||||
{ "path": "../../llm/llm" },
|
||||
{ "path": "../../core/tools" },
|
||||
{ "path": "../../core/system-prompt" },
|
||||
{ "path": "../fs" },
|
||||
{ "path": "../fs-policy" },
|
||||
{ "path": "../../sandbox/sandbox" },
|
||||
{ "path": "../../sandbox/sandbox-policy" },
|
||||
{ "path": "../../ui/user-approval" }
|
||||
{
|
||||
"path": "../../../vendor/cosmokit"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/schemastery"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
{
|
||||
"path": "../../core/tools"
|
||||
},
|
||||
{
|
||||
"path": "../../core/system-prompt"
|
||||
},
|
||||
{
|
||||
"path": "../fs"
|
||||
},
|
||||
{
|
||||
"path": "../fs-policy"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../sandbox/sandbox"
|
||||
},
|
||||
{
|
||||
"path": "../../sandbox/sandbox-policy"
|
||||
},
|
||||
{
|
||||
"path": "../../ui/user-approval"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user