Merge remote-tracking branch 'origin/master' into pr-265

# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/rfc/INDEX.md
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/loop.ts
#	packages/core/tools/README.md
#	packages/core/tools/src/index.ts
#	packages/core/tools/src/schema.ts
#	packages/ui/acp/src/index.ts
#	packages/ui/stdio-agent/README.md
This commit is contained in:
Dudu-0223
2026-07-14 20:49:54 +08:00
715 changed files with 21094 additions and 14133 deletions
+3 -9
View File
@@ -11,15 +11,9 @@ import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
/**
* Shared harness for the fs-tools with-key e2e: a minimal real agent stack (the
* DeepSeek adapter + the real fs provider + the read-before-write/edit policy +
* the model-facing read/write/edit tools). Lives outside the *.e2e.ts pattern so
* importing it never re-registers another file's tests.
*
* `fsCwd` is the local backend's default base; a per-session cwd (set via a
* session header) overrides it, but this harness creates agents without a
* session cwd, so the provider default IS the workspace. `persona` is the
* deployment persona (the system-prompt plugin's per-context config).
* Build the real fs-tool stack for with-key e2e tests. Agents have no session
* cwd, so `fsCwd` is their workspace; `persona` configures the deployment prompt.
* This helper lives outside the e2e glob so imports do not register tests.
*/
export async function fsHarness(fsCwd: string, persona = ''): Promise<Context> {
const ctx = new Context()
+10 -25
View File
@@ -1,16 +1,8 @@
/**
* Integration tests: the real local backend (`dsh-fs-local`) plus the model
* tools (`dsh-tool-fs`) as the executor, exercised through `ctx.tools.execute()`
* so nothing bypasses the tool registry. Two deployments:
*
* - DEFAULT — with the real `dsh-fs-policy` policy gate plugin: read-before-
* write/edit, version-guarded mutation, FS_NOT_OBSERVED for unread edits.
* - BARE — WITHOUT the policy plugin: every `fs/*` waterfall falls through to
* its undefined default, so write/edit are unconditional. This proves the
* tool carries no dependency on the policy plugin.
*
* These verify the WORLD — files are read back from disk and asserted
* byte-for-byte — not the tool's self-report.
* End-to-end tool-registry tests against the real local backend. The policy deployment verifies
* observed-state and guarded mutation; the bare deployment proves unconditional tools have no
* policy-service dependency. Assertions read files back byte-for-byte rather than trusting tool
* messages.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -28,9 +20,7 @@ import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
let dir: string
let ctx: Context
let fiber: Awaited<ReturnType<Context['plugin']>>
// A stable session object stands in for an agent session (the file-state
// owner). It carries a `header` (no `cwd`) so `sessionCwd(exec)` resolves to
// `undefined` and the backend falls back to its configured cwd (= `dir`).
// No header cwd: sessionCwd returns undefined and the provider's configured test dir applies.
const session = { header: {} }
let callCounter = 0
@@ -290,13 +280,9 @@ describe('bare provider (no dsh-fs-policy)', () => {
})
})
// --------------------------------------------------------------------------
// Per-session cwd: a relative file_path resolves against the CALLING session's
// workspace (`exec.agent.session.header.cwd`), NOT the backend's config.cwd —
// so an ACP editor's per-session dir wins, matching dsh-tool-bash. The regression
// this guards: before the seam fix the tool passed no cwd, so a relative write
// landed in config.cwd instead of the session dir.
// --------------------------------------------------------------------------
// Per-session cwd: a relative file_path resolves against the calling session's workspace
// (`exec.agent.session.header.cwd`), not the backend's config.cwd — so an ACP editor's
// per-session dir wins, matching dsh-tool-bash.
describe('per-session cwd', () => {
let sessionDir: string
beforeEach(async () => {
@@ -428,9 +414,8 @@ describe('signal, concurrency, and the fs/observed contract', () => {
})
it('a throwing fs/observed listener surfaces as isError, but the mutation already hit disk', async () => {
// fs/observed is a plain ctx.emit AFTER the write succeeded; a throwing
// listener cannot roll the write back — it only turns the tool result into
// isError. The file must still carry the written bytes.
// fs/observed is a plain ctx.emit after the write succeeded; a throwing listener cannot
// roll the write back — it only turns the tool result into isError.
ctx.on('fs/observed', () => { throw new Error('recording bug') })
const result = await callOwned('write', { file_path: 'w.txt', content: 'durable' })
expect(result.isError).toBe(true)
+7 -14
View File
@@ -1,11 +1,6 @@
/**
* Consumer-surface tests for the filesystem tools as the EXECUTOR. They run the
* REAL `@deepseek-ai/dsh-fs-policy` gate plugin (the genuine policy
* collaborator, per the prefer-the-real-implementation rule) over a fake
* `ctx.fs` provider, so they verify schemas, argument validation, result
* formatting, FsError→isError propagation, and that each tool dispatches the
* `fs/*` waterfalls + records observed-state through the gate (read authorizes a
* later edit) — not just that it moved bytes.
* Consumer-surface tests over a fake provider and the real policy collaborator: schemas,
* validation, formatting, typed errors, intent dispatch, and observation-driven authorization.
*/
import { describe, expect, it, vi } from 'vitest'
@@ -419,9 +414,8 @@ describe('tool-owned presentation (pure presentCall)', () => {
})
describe('result-time contextual diff (meta + presentResult)', () => {
// An edit records the applied contextual hunk on `tool/result` meta, and the
// tool's presentResult narrows it back into a `diff` result card the bridge
// renders. Drive execute end-to-end so the meta is the REAL computed hunk.
// An edit records the applied contextual hunk on `tool/result` meta, and the tool's
// presentResult narrows it back into a `diff` result card the bridge renders.
const withContext = 'a\nb\nc\nOLD\nd\ne\nf\n'
it('edit: execute attaches the applied hunk as meta { diffs }', async () => {
@@ -462,10 +456,9 @@ describe('result-time contextual diff (meta + presentResult)', () => {
})
it('write CREATE: no before-version → no meta, but presentResult still renders a whole-file diff card', async () => {
// A create has no prior content (no `meta`), yet the completed card must be a
// `diff` — an ACP tool_call_update.content REPLACES the call's content, so a
// non-diff result would clobber the pending new-file diff. The whole-file diff
// is derived from the args (oldText:null), replay-safe.
// A create has no prior content (no `meta`), yet the completed card must be a `diff` — an
// ACP tool_call_update.content REPLACES the call's content, so a non-diff result would
// clobber the pending new-file diff.
const { ctx } = await setup()
const session = { header: {} }
const result = await call(ctx, 'write', { file_path: 'new.txt', content: 'fresh\n' }, { session })