feat(examples): add one-shot CLI demo

This commit is contained in:
Tianyi Cui
2026-07-15 21:21:24 +08:00
parent b045b553a9
commit b78cdbcd51
37 changed files with 1600 additions and 28 deletions

View File

@@ -0,0 +1,43 @@
import { readdir } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
describe('coding-agent one-shot CLI keyless smoke', () => {
it('boots the real Loader tree, runs a real bash tool round trip, and persists the turn', async () => {
let persisted = false
const { stdout, stderr } = await runLoaderSmoke({
label: 'coding-agent CLI',
tempDirPrefix: 'coding-cli-smoke-',
binScript,
configPath,
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'prove the tool path'],
tsconfigPath,
inspect: async (cwd) => {
const files = await readdir(cwd, { recursive: true })
persisted = files.some(file => file.endsWith('.jsonl'))
},
})
const lines = stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
const events = lines.slice(0, -1).map(line => line['event'] as SessionEvent)
const result = lines.at(-1)
expect(stderr).toBe('')
expect(events.some(event => event.type === 'tool/call' && event.data.name === 'bash')).toBe(true)
const toolResult = events.find(event => event.type === 'tool/result')
expect(JSON.stringify(toolResult)).toContain('CLI_TOOL_ROUND_TRIP')
expect(result).toMatchObject({
type: 'result',
success: true,
turn: 1,
reason: { kind: 'completed' },
usage: { inputTokens: 18, outputTokens: 8, cacheReadTokens: 2, reasoningTokens: 1 },
})
expect(String(result?.['result'])).toContain('CLI_TOOL_ROUND_TRIP')
expect(persisted).toBe(true)
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})

View File

@@ -0,0 +1,33 @@
import { readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cli.cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const hasKey = Boolean(process.env.DEEPSEEK_API_KEY)
describe.skipIf(!hasKey)('coding-agent one-shot CLI with real model', () => {
it('modifies a temporary workspace and verifies the file outside the agent', async () => {
let verified = ''
const { stdout } = await runLoaderSmoke({
label: 'coding-agent CLI real model',
tempDirPrefix: 'coding-cli-real-',
binScript,
configPath,
binArgs: [
'--config',
configPath,
'Read task.txt, replace its complete contents with exactly "value=after" followed by a newline, read it again, and report briefly.',
],
tsconfigPath,
processTimeoutMs: 120_000,
prepare: cwd => writeFile(join(cwd, 'task.txt'), 'value=before\n'),
inspect: async (cwd) => { verified = await readFile(join(cwd, 'task.txt'), 'utf8') },
})
expect(verified).toBe('value=after\n')
expect(stdout.trim().length).toBeGreaterThan(0)
}, 135_000)
})

View File

@@ -0,0 +1,37 @@
import type { Context } from 'cordis'
import { CallId, LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
/** Keyless coding smoke adapter: one real bash call followed by a final answer. */
class CliMockAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const toolResult = options.messages.at(-1)?.content.find(block => block.type === 'tool-result')
if (toolResult === undefined) {
const args = JSON.stringify({ command: 'printf CLI_TOOL_ROUND_TRIP', description: 'Prove the CLI tool round trip.' })
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
yield { type: 'tool-call-delta', index: 0, id: CallId('cli-smoke-call'), name: 'bash', argumentsDelta: args }
yield { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('cli-smoke-call'), name: 'bash', arguments: args } }
yield { type: 'usage', usage: { inputTokens: 11, outputTokens: 3, cacheReadTokens: 2 } }
yield { type: 'finish', reason: { kind: 'tool-calls' } }
return
}
const toolText = toolResult.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
const reply = `CLI tool round trip complete: ${toolText.trim()}`
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'text-delta', index: 0, text: reply }
yield { type: 'block-end', index: 0, block: { type: 'text', text: reply } }
yield { type: 'usage', usage: { inputTokens: 7, outputTokens: 5, reasoningTokens: 1 } }
yield { type: 'finish', reason: { kind: 'stop' } }
}
}
export const name = 'cli-mock-llm'
export const inject = ['llm']
/** Register the keyless `cli-mock` adapter. */
export function apply(ctx: Context): void {
ctx.llm.registerAdapter(['cli-mock'], new CliMockAdapter())
}

View File

@@ -0,0 +1,24 @@
- id: cli-mock-llm
name: './cli-mock-llm.ts'
- id: base
name: '@cordisjs/plugin-include'
config:
path: ../../cordis.yml
patches:
- id: hmr
name: '@cordisjs/plugin-hmr'
disabled: true
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-demo'
disabled: true
- insert:
- id: cli-agent
name: '@deepseek-ai/dsh-cli-demo'
config:
model: cli-mock
persistenceRoot: './.sessions'
persona: 'Keyless CLI smoke.'