1
/** Scoped tool that declares filesystem deliveries in their owning Session. */2
import type { Context } from '@deepseek-ai/cordis'3
import z from '@deepseek-ai/schemastery'4
import { FsError } from '@deepseek-ai/dsh-fs'5
import { defineTool, type ToolExecution } from '@deepseek-ai/dsh-tools'6
import type {} from '@deepseek-ai/dsh-agent'7
import type {} from '@deepseek-ai/dsh-session-projection'8
import type { Session } from '@deepseek-ai/dsh-session'9
import type { PresentedFile } from './types.ts'11
/** Stable Loader identity. */12
export const name = 'tool-present'14
/** Per-call delivery limit. */15
export interface Config {16
/** Maximum number of files in one call. */17
maxFiles: number18
}20
/** Validated delivery limit. */21
export const Config: z<Config> = z.object({22
maxFiles: z.number().default(8),23
})25
/** Services used by the scoped delivery tool. */26
export const inject = ['tools', 'fs', 'sessionProjections']28
/**29
* Register present with durable file references in its tool result.30
* @param ctx - agent-scoped services.31
* @param config - maximum files per call.32
*/33
export function apply(ctx: Context, config: Config): void {34
if (!Number.isSafeInteger(config.maxFiles) || config.maxFiles < 1) {35
throw new Error('present requires a positive integer maxFiles')36
}37
const pending = new WeakMap<ToolExecution, { session: Session; turn: number; files: PresentedFile[] }>()38
ctx.tools.register(defineTool({39
name: 'present',40
description: 'Declare existing files as final deliverables for the user. '41
+ 'Use it when the user needs a separate file, especially Office documents, spreadsheets, and slide decks; '42
+ 'prefer your final response when that suffices. The user opens the current files; their contents are not copied.',43
parameters: {44
files: {45
type: 'array', required: true,46
// 4 is the recommended per-call count; `maxFiles` is the enforced ceiling above it.47
description: 'Usually the 1-2 most important deliverables; at most 4 per call.',48
items: {49
type: 'object', additionalProperties: false,50
properties: {51
path: { type: 'string', required: true, description: 'Path of an existing regular file. Relative paths use the Session working directory.' },52
description: { type: 'string', description: 'Brief description for the user.' },53
},54
},55
},56
},57
output: {58
schema: {59
type: 'object', additionalProperties: false,60
properties: {61
turn: { type: 'integer', required: true },62
files: {63
type: 'array', required: true,64
items: {65
type: 'object', additionalProperties: false,66
properties: {67
path: { type: 'string', required: true },68
description: { type: 'string' },69
},70
},71
},72
},73
},74
render: (_args, value) => [{ type: 'text', text: value.files.map(file => `Presented ${file.path}`).join('\n') }],75
},76
async execute(args, exec) {77
if (exec.agent === undefined) throw new Error('present requires an agent Session')78
const boundary = ctx.sessionProjections.stateOf(exec.agent.session, 'turnBoundary')79
if (boundary === undefined || boundary.openTurnStartSeq === null) throw new Error('present requires an open turn')80
if (args.files.length === 0 || args.files.length > config.maxFiles) throw new Error(`present accepts 1 to ${config.maxFiles} files`)81
const cwd = exec.agent.session.header.cwd82
if (cwd === undefined) throw new Error('present requires a workspace')83
const options = { cwd, signal: exec.signal }84
const files: PresentedFile[] = []85
for (const file of args.files) {86
if (file.path.trim().length === 0) throw new Error('present requires a non-empty file path')87
const entry = await ctx.fs.lstat(file.path, { cwd }, exec.signal)88
if (entry !== undefined && entry.type !== 'file') throw new Error(`Cannot present ${file.path}: not a regular file`)89
const target = await ctx.fs.resolve(file.path, options)90
const info = await ctx.fs.stat(target, exec.signal)91
if (info === undefined) throw new FsError(`Cannot present ${file.path}: file not found. Check the path, create the file if needed, and retry.`, 'FS_NOT_FOUND')92
if (info.type !== 'file') throw new Error(`Cannot present ${file.path}: not a regular file`)93
files.push({ ...file })94
}95
exec.signal.throwIfAborted()96
pending.set(exec, { session: exec.agent.session, turn: boundary.lastTurn, files })97
return { turn: boundary.lastTurn, files }98
},99
}))100
ctx.on('tools/result', (exec, result) => {101
const delivery = pending.get(exec)102
pending.delete(exec)103
if (delivery === undefined || result.isError) return104
const { session, turn, files } = delivery105
session.append('deliverables/presented', {106
turn, callId: exec.callId, files,107
})108
})109
}