mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge latest master into tool JSON schema DSL
# Conflicts: # examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md # examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md # examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md # examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# tui-agent
|
||||
|
||||
The full-screen interactive coding agent: DeepSeek V4, local bash and filesystem tools, compaction, subagents, workflows and fresh-agent Ralph iteration, `todo_write`, timeout/spill policy, and [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo).
|
||||
The full-screen interactive coding agent: DeepSeek V4, local bash and filesystem tools, compaction, subagents, workflows and fresh-agent Ralph iteration, `todo_write`, plan mode (`/plan` enters and `exit_plan_mode` reviews the exit), timeout/spill policy, and [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo).
|
||||
|
||||
## Run it
|
||||
|
||||
@@ -10,7 +10,7 @@ pnpm run demo:tui
|
||||
|
||||
The command needs `DEEPSEEK_API_KEY` in the environment or gitignored repository-root `.env`. Set `RESUME_SESSION_ID` to reopen a persisted conversation under `./.sessions`.
|
||||
|
||||
The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and the latest todo list. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model <model>` and `/model <provider>/<model>` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options.
|
||||
The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and the latest todo list. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/plan` selects plan mode for the next step; `/plan <message>` also submits the message into that step. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model <model>` and `/model <provider>/<model>` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options.
|
||||
|
||||
Run `pnpm run demo:code-mode tui` for the Code Mode overlay.
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ flowchart LR
|
||||
cfg --> plugin_tui_tool_ralph
|
||||
plugin_tui_tool_todo["tool-todo<br/>@deepseek-ai/dsh-tool-todo"]
|
||||
cfg --> plugin_tui_tool_todo
|
||||
plugin_tui_plan_mode["plan-mode<br/>@deepseek-ai/dsh-plan-mode"]
|
||||
cfg --> plugin_tui_plan_mode
|
||||
plugin_tui_fs_local["fs-local<br/>@deepseek-ai/dsh-fs-local"]
|
||||
cfg --> plugin_tui_fs_local
|
||||
plugin_tui_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"]
|
||||
@@ -81,6 +83,7 @@ flowchart LR
|
||||
| `tool-workflow` | `@deepseek-ai/dsh-tool-workflow` |
|
||||
| `tool-ralph` | `@deepseek-ai/dsh-tool-ralph` |
|
||||
| `tool-todo` | `@deepseek-ai/dsh-tool-todo` |
|
||||
| `plan-mode` | `@deepseek-ai/dsh-plan-mode` |
|
||||
| `fs-local` | `@deepseek-ai/dsh-fs-local` |
|
||||
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
|
||||
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |
|
||||
|
||||
@@ -88,6 +88,25 @@
|
||||
- id: tool-todo
|
||||
name: '@deepseek-ai/dsh-tool-todo'
|
||||
|
||||
# Plan mode gives the TUI a plugin-owned /plan [message] command; the exit
|
||||
# review rides the TUI's user-interaction provider.
|
||||
- id: plan-mode
|
||||
name: '@deepseek-ai/dsh-plan-mode'
|
||||
config:
|
||||
section: |
|
||||
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
||||
|
||||
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
||||
|
||||
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed only to keep the request shape stable. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
||||
|
||||
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
||||
|
||||
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
||||
|
||||
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
||||
|
||||
|
||||
- id: fs-local
|
||||
name: '@deepseek-ai/dsh-fs-local'
|
||||
config:
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
- id: token-meter
|
||||
name: '@deepseek-ai/dsh-token-meter'
|
||||
|
||||
- id: plan-mode
|
||||
name: '@deepseek-ai/dsh-plan-mode'
|
||||
config:
|
||||
section: 'Stay in plan mode for this scripted TUI test.'
|
||||
|
||||
- id: tui-agent
|
||||
name: '@deepseek-ai/dsh-tui-demo'
|
||||
config:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Reply with exactly the word: — DSH TUI snapshot"
|
||||
cursor hidden column=1 viewportRow=28 bufferRow=28
|
||||
cursor hidden column=1 viewportRow=30 bufferRow=30
|
||||
buffer
|
||||
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
|
||||
style 0-99 fg=bright-blue
|
||||
@@ -21,50 +21,53 @@ buffer
|
||||
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
style 0-99 fg=bright-blue
|
||||
5| <blank>
|
||||
6| "▌ "
|
||||
6| " Entering plan mode (applies from the next step). "
|
||||
style 1-48 fg=bright-black
|
||||
7| <blank>
|
||||
8| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
7| "▌ You "
|
||||
9| "▌ You "
|
||||
style 0-0 fg=bright-blue
|
||||
style 2-4 fg=bright-blue bold
|
||||
8| "▌ Reply with exactly the word: ONE. No tools. "
|
||||
10| "▌ Reply with exactly the word: ONE. No tools. "
|
||||
style 0-0 fg=bright-blue
|
||||
9| "▌ "
|
||||
11| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
10| <blank>
|
||||
11| " Reasoning "
|
||||
12| <blank>
|
||||
13| " Reasoning "
|
||||
style 1-9 fg=bright-black italic
|
||||
12| " The user wants me to reply with exactly the word \"ONE\" and use no tools. "
|
||||
14| " The user wants me to reply with exactly the word \"ONE\" and use no tools. "
|
||||
style 1-72 fg=bright-black italic
|
||||
13| <blank>
|
||||
14| " Assistant "
|
||||
15| <blank>
|
||||
16| " Assistant "
|
||||
style 1-9 fg=bright-magenta bold
|
||||
15| " ONE "
|
||||
16| <blank>
|
||||
17| "▌ "
|
||||
17| " ONE "
|
||||
18| <blank>
|
||||
19| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
18| "▌ You "
|
||||
20| "▌ You "
|
||||
style 0-0 fg=bright-blue
|
||||
style 2-4 fg=bright-blue bold
|
||||
19| "▌ Reply with exactly the word: TWO. No tools. "
|
||||
21| "▌ Reply with exactly the word: TWO. No tools. "
|
||||
style 0-0 fg=bright-blue
|
||||
20| "▌ "
|
||||
22| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
21| <blank>
|
||||
22| " Reasoning "
|
||||
23| <blank>
|
||||
24| " Reasoning "
|
||||
style 1-9 fg=bright-black italic
|
||||
23| " The user wants me to reply with exactly the word \"TWO\" and no tools. "
|
||||
25| " The user wants me to reply with exactly the word \"TWO\" and no tools. "
|
||||
style 1-68 fg=bright-black italic
|
||||
24| <blank>
|
||||
25| " Assistant "
|
||||
26| <blank>
|
||||
27| " Assistant "
|
||||
style 1-9 fg=bright-magenta bold
|
||||
26| " TWO "
|
||||
27| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
28| " "
|
||||
style 1-1 inverse
|
||||
28| " TWO "
|
||||
29| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
30| "/tmp/dsh-tui-snapshot-multi-tu ↑2.9k ↓41 3% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
30| " "
|
||||
style 1-1 inverse
|
||||
31| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
32| "/tmp/dsh-tui-snapshot-multi-tu ↑2.9k ↓41 3% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-29 dim
|
||||
style 42-99 dim
|
||||
31-35| <blank>
|
||||
33-35| <blank>
|
||||
|
||||
@@ -9,7 +9,7 @@ const scriptedConfigPath = fileURLToPath(new URL('./fixtures/tui-scripted.cordis
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
||||
it('boots pi-tui, renders the configured banner, accepts /exit, and restores the terminal', async () => {
|
||||
it('boots pi-tui, enters plan mode through /plan, accepts /exit, and restores the terminal', async () => {
|
||||
const output = await runTuiPtySmoke({
|
||||
label: 'tui-agent boot',
|
||||
tempDirPrefix: 'tui-agent-smoke-',
|
||||
@@ -17,10 +17,14 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call' },
|
||||
actions: [{ waitFor: 'TUI agent ready.', send: '/exit\r' }],
|
||||
actions: [
|
||||
{ waitFor: 'TUI agent ready.', send: '/plan\r' },
|
||||
{ waitFor: 'Entering plan mode (applies from the next step).', send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).toContain('DEEPSEEK')
|
||||
expect(output).toContain('TUI agent ready.')
|
||||
expect(output).toContain('Entering plan mode (applies from the next step).')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
@@ -34,12 +38,13 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
||||
actions: [
|
||||
{ waitFor: 'scripted TUI ready.', send: '/model\r' },
|
||||
{ waitFor: 'Select model', send: '\x1b[B\r' },
|
||||
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: 'exercise the TUI\r' },
|
||||
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: '/plan exercise the TUI\r' },
|
||||
{ waitFor: 'How should the scripted run proceed?', send: '\r' },
|
||||
{ waitFor: 'Decision received. Scripted TUI run complete.', send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).toContain('I need one decision before I continue.')
|
||||
expect(output).toContain('Entering plan mode (applies from the next step).')
|
||||
expect(output).toContain(String.raw`\x1b]2;MODEL_CONTROLLED\x07`)
|
||||
expect(output).toContain(String.raw`\x1b[999CMODEL_CURSOR`)
|
||||
expect(output).toContain(String.raw`\x9b31mMODEL_C1`)
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
|
||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { installLlmReplay, parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
||||
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -45,6 +46,7 @@ interface Scenario {
|
||||
expectedTools: string[]
|
||||
expectedEventCounts?: Record<string, number>
|
||||
childSessions?: number
|
||||
enterPlanMode?: boolean
|
||||
recorded: boolean
|
||||
seedWorkspace?: boolean
|
||||
}
|
||||
@@ -54,6 +56,8 @@ const SCENARIOS: Scenario[] = [
|
||||
name: 'multi-turn-conversation',
|
||||
composition: 'native',
|
||||
expectedTools: [],
|
||||
expectedEventCounts: { 'plan/mode': 1 },
|
||||
enterPlanMode: true,
|
||||
recorded: true,
|
||||
},
|
||||
{
|
||||
@@ -204,6 +208,9 @@ async function mountScenarioContext(
|
||||
await ctx.plugin(ToolWorkflow)
|
||||
await ctx.plugin(ToolRalph)
|
||||
await ctx.plugin(CommandService)
|
||||
if (scenario.enterPlanMode === true) {
|
||||
await ctx.plugin(PlanModeService, { section: 'Snapshot plan mode instructions.' })
|
||||
}
|
||||
if (scenario.composition === 'code' || scenario.composition === 'advanced') {
|
||||
await ctx.plugin(WorkerCodeRuntime, {})
|
||||
}
|
||||
@@ -268,7 +275,17 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
|
||||
})
|
||||
await settleTerminal(terminal)
|
||||
|
||||
for (const prompt of prompts) {
|
||||
let remainingPrompts = prompts
|
||||
if (scenario.enterPlanMode === true) {
|
||||
const firstPrompt = prompts[0]!
|
||||
terminal.send(`/plan ${firstPrompt}`)
|
||||
terminal.send('\r')
|
||||
await agent.whenIdle()
|
||||
await settleTerminal(terminal)
|
||||
remainingPrompts = prompts.slice(1)
|
||||
}
|
||||
|
||||
for (const prompt of remainingPrompts) {
|
||||
terminal.send(prompt)
|
||||
terminal.send('\r')
|
||||
await agent.whenIdle()
|
||||
@@ -280,6 +297,18 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
|
||||
for (const [type, count] of Object.entries(scenario.expectedEventCounts ?? {})) {
|
||||
expect(events.filter(event => event.type === type), `${scenario.name} must emit ${type}`).toHaveLength(count)
|
||||
}
|
||||
if (scenario.enterPlanMode === true) {
|
||||
expect(ctx.planMode.get(agent)).toEqual({ active: true })
|
||||
const planMode = events.find(event => event.type === 'plan/mode')
|
||||
const firstHeader = events.find(event => event.type === 'request/header')
|
||||
if (planMode === undefined || firstHeader === undefined) {
|
||||
throw new Error('plan-mode command snapshot needs plan/mode before its first request/header')
|
||||
}
|
||||
expect(planMode.seq).toBeLessThan(firstHeader.seq)
|
||||
expect(firstHeader.data.header.system).toContain('Snapshot plan mode instructions.')
|
||||
const firstMessage = events.find(event => event.type === 'user/message')
|
||||
expect(firstMessage?.data.content).toEqual([{ type: 'text', text: prompts[0] }])
|
||||
}
|
||||
expect(events.filter(event => event.type === 'tool/result').every(event => !event.data.isError)).toBe(true)
|
||||
expect(events.filter(event => event.type === 'turn/end').every(event => event.data.reason.kind !== 'error')).toBe(true)
|
||||
if (scenario.name === 'dynamic-workflow' || scenario.name === 'cordis-dynamic-toolchain') {
|
||||
|
||||
Reference in New Issue
Block a user