mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
# Conflicts: # .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml # .agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.i18n.yaml # .agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml # .agents/notes/implemented/feature/2026-07-22-docked-web-goal-bar.i18n.yaml # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/core.md # docs/core-data-structures/core.zh.md # docs/event-producer-consumer.md # docs/module-graph.md # docs/persistence-catalog.md # examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl # examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl # examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl # examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl # examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl # examples/acp-agent/tests/snapshots/workspace-context/session.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/headless-agent/tests/snapshots/pty-tools/session.jsonl # examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl # examples/jsonrpc-agent/tests/snapshots/subagent-spawn/notifications.expected.jsonl # examples/jsonrpc-agent/tests/snapshots/subagent-spawn/session.1.jsonl # packages/acp/acp/README.i18n.yaml # packages/acp/acp/src/index.ts # packages/client/connection/src/client/index.ts # packages/client/runtime/README.i18n.yaml # packages/client/runtime/README.md # packages/client/runtime/README.zh.md # packages/client/runtime/src/client/sessions/conversation.ts # packages/client/runtime/src/client/sessions/session.ts # packages/client/runtime/tests/queue-store.spec.ts # packages/client/ui-conversation/src/client/input/contract.ts # packages/client/ui-goal/README.i18n.yaml # packages/compact/compact-basic/src/index.ts # packages/context/time-context/tests/time-context.spec.ts # packages/context/tmux-context/tests/tmux-context.spec.ts # packages/context/workspace-context/tests/workspace-context.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-loop/README.i18n.yaml # packages/core/agent-loop/README.md # packages/core/agent-loop/README.zh.md # packages/core/agent-loop/src/agent.ts # packages/core/agent-loop/tests/agent.spec.ts # packages/core/agent-loop/tests/loop.spec.ts # packages/core/agent/README.i18n.yaml # packages/core/agent/README.md # packages/core/agent/README.zh.md # packages/core/agent/src/types.ts # packages/core/session/README.i18n.yaml # packages/core/session/README.md # packages/core/session/README.zh.md # packages/goal/command-goal/tests/command-goal.spec.ts # packages/goal/goal/tests/goal.spec.ts # packages/goal/tool-goal/README.i18n.yaml # packages/goal/tool-goal/README.md # packages/goal/tool-goal/README.zh.md # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/events.schema.ts # packages/host/apiproxy/src/api/events.ts # packages/host/apiproxy/src/api/rpc.ts # packages/host/apiproxy/tests/api-proxy-commands.spec.ts # packages/host/apiproxy/tests/api-proxy-projections.spec.ts # packages/host/apiproxy/tests/rpc-schemas.spec.ts # packages/pty/pty-local/tests/index.spec.ts # packages/pty/pty-local/tests/local.spec.ts # packages/pty/tool-pty/tests/loader-composition.spec.ts # packages/pty/tool-pty/tests/tools.spec.ts # packages/session-persistence/session-checkpoint-policy/src/index.ts # packages/session-persistence/session-persistence/tests/coordinator-contract.ts # packages/skill/tool-skill/tests/tool-skill.spec.ts # packages/subagent/subagent-inprocess/README.i18n.yaml # packages/subagent/subagent-inprocess/README.md # packages/subagent/subagent-inprocess/README.zh.md # packages/subagent/subagent-inprocess/src/index.ts # packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts # packages/support/acp-snapshot/src/harness.ts # packages/ui/tui/tests/tui.spec.ts
158 lines
6.1 KiB
TypeScript
158 lines
6.1 KiB
TypeScript
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import Loader from '@cordisjs/plugin-loader'
|
|
import Include from '@cordisjs/plugin-include'
|
|
import { CallId } from '@deepseek-ai/dsh-llm'
|
|
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
|
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
import PtyService from '@deepseek-ai/dsh-pty'
|
|
import * as PtyLocal from '@deepseek-ai/dsh-pty-local'
|
|
import SandboxProvider from '@deepseek-ai/dsh-sandbox'
|
|
import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
|
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
|
|
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
|
import * as ToolBashPersistent from '@deepseek-ai/dsh-tool-bash-persistent'
|
|
|
|
let root: string | undefined
|
|
let context: Context | undefined
|
|
|
|
afterEach(async () => {
|
|
await context?.fiber.dispose()
|
|
context = undefined
|
|
if (root !== undefined) await rm(root, { recursive: true, force: true })
|
|
root = undefined
|
|
})
|
|
|
|
class PassthroughSandbox extends SandboxProvider {
|
|
confine(argv: readonly string[], _policy: SandboxPolicy): ConfinedArgv {
|
|
return { argv: [...argv], enforcement: 'full', denialSignatures: [], runnerFailureSignatures: [] }
|
|
}
|
|
}
|
|
|
|
function agent(ctx: Context, cwd: string): Agent {
|
|
const id = SessionId('persistent-bash-loader-agent')
|
|
const scope = ctx.plugin(() => {})
|
|
const session = new Session(id, [], { version: 0, id, createdAt: 0, cwd })
|
|
const value: Agent = {
|
|
id,
|
|
options: {},
|
|
session,
|
|
inbox: new Inbox(session, { inserted: () => {}, discarded: () => {} }),
|
|
status: 'idle',
|
|
ctx: scope.ctx,
|
|
send: () => {},
|
|
followup: () => {},
|
|
steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
|
|
inject: () => {},
|
|
cancel() {},
|
|
whenIdle: () => Promise.resolve(),
|
|
}
|
|
ctx.agents.register(value)
|
|
return value
|
|
}
|
|
|
|
function text(result: { content: { type: string; text?: string }[] }): string {
|
|
return result.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
|
}
|
|
|
|
const suite = process.platform === 'linux' || process.platform === 'darwin' ? describe : describe.skip
|
|
|
|
suite('persistent Bash through a real cordis.yml Loader composition', () => {
|
|
it('preserves cwd and environment across calls', async () => {
|
|
root = await mkdtemp(join(tmpdir(), 'dsh-persistent-bash-loader-'))
|
|
const configPath = join(root, 'cordis.yml')
|
|
await writeFile(configPath, [
|
|
"- name: '@deepseek-ai/dsh-agent'",
|
|
"- name: '@deepseek-ai/dsh-system-prompt'",
|
|
"- name: '@deepseek-ai/dsh-tools'",
|
|
"- name: '@deepseek-ai/dsh-pty'",
|
|
"- name: '@deepseek-ai/dsh-test-sandbox'",
|
|
"- name: '@deepseek-ai/dsh-sandbox-policy'",
|
|
' config:',
|
|
' mode: danger-full-access',
|
|
` workspaceRoot: ${JSON.stringify(root)}`,
|
|
"- name: '@deepseek-ai/dsh-pty-local'",
|
|
' config:',
|
|
' pollIntervalMs: 10',
|
|
' exactProbeAfterMs: 20',
|
|
' idleSilenceMs: 100',
|
|
' handoffGraceMs: 100',
|
|
' scrollbackLines: 20000',
|
|
' timeoutMs: 2000',
|
|
' disposeGraceMs: 500',
|
|
"- name: '@deepseek-ai/dsh-tool-bash-persistent'",
|
|
' config:',
|
|
' timeoutMs: 5000',
|
|
'',
|
|
].join('\n'))
|
|
|
|
context = new Context()
|
|
context.baseUrl = pathToFileURL(root).href + '/'
|
|
await context.plugin(Loader)
|
|
context.loader.builtins.include = Include
|
|
const modules = new Map<string, unknown>([
|
|
['@deepseek-ai/dsh-agent', AgentRegistry],
|
|
['@deepseek-ai/dsh-system-prompt', SystemPrompt],
|
|
['@deepseek-ai/dsh-tools', ToolRegistry],
|
|
['@deepseek-ai/dsh-pty', PtyService],
|
|
['@deepseek-ai/dsh-test-sandbox', PassthroughSandbox],
|
|
['@deepseek-ai/dsh-sandbox-policy', SandboxPolicyService],
|
|
['@deepseek-ai/dsh-pty-local', PtyLocal],
|
|
['@deepseek-ai/dsh-tool-bash-persistent', ToolBashPersistent],
|
|
])
|
|
context.loader.internal = {
|
|
version: 'v2',
|
|
async import(specifier: string) {
|
|
if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
|
|
return modules.get(specifier)
|
|
},
|
|
} as unknown as NonNullable<typeof context.loader.internal>
|
|
await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
|
|
await context.loader.await()
|
|
|
|
const owner = agent(context, root)
|
|
const signal = new AbortController().signal
|
|
const execute = (id: string, command: string) => context!.tools.execute({
|
|
signal,
|
|
callId: CallId(id),
|
|
name: 'bash',
|
|
arguments: { command },
|
|
agent: owner,
|
|
})
|
|
|
|
expect(context.tools.schemas().map(schema => schema.name)).toEqual(['bash'])
|
|
await execute('state', 'export KEEP=loader; mkdir -p nested; cd nested')
|
|
const observed = text(await execute('observe', 'printf "cwd=%s keep=%s\\n" "$PWD" "$KEEP"'))
|
|
expect(observed).toContain(`cwd=${join(root, 'nested')} keep=loader`)
|
|
expect(observed).not.toContain('DSH_PERSISTENT_BASH')
|
|
|
|
const multiline = text(await execute(
|
|
'multiline',
|
|
'value="line one"\nprintf "%s:%s\\n" "$value" "it\'s fine"',
|
|
))
|
|
expect(multiline).toBe("line one:it's fine")
|
|
expect(multiline).not.toContain('DSH_PERSISTENT_BASH')
|
|
|
|
const heredoc = text(await execute(
|
|
'heredoc',
|
|
"cat <<'EOF'\nalpha\nbeta\nEOF",
|
|
))
|
|
expect(heredoc).toBe('alpha\nbeta')
|
|
|
|
const large = text(await execute('large-output', 'seq 1 12050'))
|
|
expect(large.startsWith('1\n2\n3\n')).toBe(true)
|
|
expect(large).toContain('<response clipped>')
|
|
expect(large).not.toContain('beginning of this command output was dropped')
|
|
|
|
const exited = text(await execute('exit', 'exit'))
|
|
expect(exited).toContain('next bash call starts from the workspace')
|
|
expect(text(await execute('after-exit', 'printf "%s\\n" "$PWD"'))).toBe(root)
|
|
}, 20_000)
|
|
})
|