bd7fb31ae3
The fs tools rendered as generic cards (title = tool name, raw file content) in
an ACP editor. Give them tool-owned presentation like bash/subagent have:
- read → title "Read <path>", kind read, offset/limit as rawInput
- write → title "Write <path>", kind edit
- edit → title "Edit <path>", kind edit, a clipped old→new rawInput summary
Add a provider-neutral `locations: { path, line? }[]` to ToolCallPresentation —
the files a call reads/modifies — so a capable editor can follow along / jump to
the file (read carries its offset as the line). The ACP bridge forwards it onto
the wire `tool_call` (ResolvedCallPresentation + call() + the tool_call build in
streamSessionEventUpdate). This flips the `locations` cell in the ACP feature
matrix to supported. The SDK already carries `tool_call.locations`
(ToolCallLocation `{ path, line? }`), so no ACP types leak into dsh-tools.
presentResult is intentionally omitted: it only receives `{ content, isError }`,
not the write/edit outcome, so titling by create-vs-overwrite or replacement
count would mean parsing the model-facing text — the static title stays.
Tests: pure presentCall assertions for all three tools incl. locations and the
edit rawInput clip; a bridge test drives the REAL fs tools through ToolPresenter
and asserts locations reaches the wire tool_call (proven to fail without the
forwarding line). New withFs harness option + dsh-fs devDeps on dsh-acp.
74 lines
3.6 KiB
TypeScript
74 lines
3.6 KiB
TypeScript
/**
|
|
* The model-facing `write` tool: create or fully replace a UTF-8 text file. The
|
|
* tool is the executor: it dispatches the `fs/write-intent` waterfall to
|
|
* obtain the optional version guard, calls `ctx.fs.writeText` directly, and
|
|
* emits `fs/observed`. The default thunk returns `undefined` (unconditional
|
|
* create-or-overwrite — the bare provider); a policy plugin
|
|
* (`@deepseek-ai/dsh-fs-policy`) occupies the single decision slot and
|
|
* returns `createIfAbsent`/`replaceIfVersion` instead. The tool stats ZERO
|
|
* times either way.
|
|
*
|
|
* @module @deepseek-ai/dsh-tool-fs/src/write
|
|
*/
|
|
|
|
import type { Context } from 'cordis'
|
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
import type { FsWriteOutcome } from '@deepseek-ai/dsh-fs'
|
|
import type {} from '@deepseek-ai/dsh-fs'
|
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
import { sessionCwd } from './session-cwd.ts'
|
|
|
|
/** Validate value constraints the schema DSL can't express. */
|
|
export function parseWriteArgs(args: { file_path: string; content: string }): { filePath: string; content: string } {
|
|
if (args.file_path.trim().length === 0) throw new Error('file_path must be a non-empty string')
|
|
return { filePath: args.file_path, content: args.content }
|
|
}
|
|
|
|
/** Format a write outcome as one model-facing text block body. */
|
|
export function formatWriteOutput(displayPath: string, outcome: FsWriteOutcome): string {
|
|
const verb = outcome.operation === 'create' ? 'Created' : 'Updated'
|
|
return `<path>${displayPath}</path>
|
|
<type>file</type>
|
|
<content>
|
|
${verb} file
|
|
</content>`
|
|
}
|
|
|
|
/** Register the `write` tool and its system-prompt guidance. */
|
|
export function applyWriteTool(ctx: Context): void {
|
|
ctx.systemPrompt.section({
|
|
name: 'tool:write',
|
|
order: 101,
|
|
text: 'Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.',
|
|
})
|
|
|
|
ctx.tools.register(defineTool({
|
|
name: 'write',
|
|
description: 'Create or fully replace a UTF-8 text file.',
|
|
parameters: {
|
|
file_path: { type: 'string', required: true, description: 'Path to write, resolved by the filesystem backend.' },
|
|
content: { type: 'string', required: true, description: 'Full UTF-8 text content to write.' },
|
|
},
|
|
async execute(args, exec): Promise<ContentBlock[]> {
|
|
const input = parseWriteArgs(args)
|
|
const cwd = sessionCwd(exec)
|
|
const target = await ctx.fs.resolve(input.filePath, cwd !== undefined ? { cwd } : undefined)
|
|
// Single-slot decision: the policy plugin produces createIfAbsent/
|
|
// replaceIfVersion; the bare default is undefined (unconditional). No stat.
|
|
const intent = await ctx.waterfall('fs/write-intent', target, exec, () => undefined)
|
|
const outcome = await ctx.fs.writeText(target, input.content, intent, exec.signal)
|
|
// Record the observed version (a no-op when no policy plugin listens).
|
|
ctx.emit('fs/observed', target, outcome.version, exec)
|
|
return [{ type: 'text', text: formatWriteOutput(target.displayPath, outcome) }]
|
|
},
|
|
// Pure display: `edit` kind (an editor treats create/replace as an edit) and
|
|
// a location so the UI can follow along to the written file. The create-vs-
|
|
// overwrite fact lives in the model-facing result text; `presentResult` only
|
|
// sees `{ content, isError }` (not the outcome), so the title stays static.
|
|
presentCall(args) {
|
|
return { title: `Write ${args.file_path}`, kind: 'edit', locations: [{ path: args.file_path }] }
|
|
},
|
|
}))
|
|
}
|