/** * Model-facing, workspace-authorized session-history search and read tools. * * @module @deepseek-ai/dsh-tool-session-query */ import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' import { defineTool } from '@deepseek-ai/dsh-tools' import type {} from '@deepseek-ai/dsh-system-prompt' import { toolInput } from './input.ts' import { operations } from './operations.ts' import { presentation } from './presentation.ts' /** Cordis plugin name used by Loader diagnostics. */ export const name = 'tool-session-query' /** Capability services required by the model-facing consumer. */ export const inject = ['tools', 'systemPrompt', 'sessionQuery'] /** Default maximum number of authorized search hits returned by one call. */ export const DEFAULT_MAX_SEARCH_RESULTS = 100 /** Default cooperative deadline for either full-text search tool. */ export const DEFAULT_SEARCH_TIMEOUT_MS = 30_000 /** Deployment-owned search count and timeout bounds. */ export interface Config { /** Maximum authorized hits returned by one search call. Defaults to 100. */ maxSearchResults?: number /** Cooperative full-text search deadline in milliseconds. Defaults to 30000. */ searchTimeoutMs?: number } /** Schemastery config for Loader defaults and generated configuration docs. */ export const Config: z = z.object({ maxSearchResults: z.number().step(1).min(1).default(DEFAULT_MAX_SEARCH_RESULTS), searchTimeoutMs: z.number().step(1).min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_SEARCH_TIMEOUT_MS), }) interface ResolvedConfig { readonly maxSearchResults: number readonly searchTimeoutMs: number } const TEXT_OUTPUT = { schema: { type: 'string' as const }, render: (_args: unknown, value: string) => [{ type: 'text' as const, text: value }], } const PROMPT_TEXT = 'Use session_search to find relevant work from prior sessions, or session_event_search to search earlier ' + 'events in one session. Search results are cursor-free and workspace-scoped. Follow a useful hit with ' + 'session_trace, session_event_trace, or session_event_read when you need lineage, relationships, or exact data.' /** Register all five tools and their shared model guidance. */ export function apply(ctx: Context, config: Config): void { const resolved = resolveConfig(config) ctx.systemPrompt.section({ name: 'tool:session-query', order: 113, text: PROMPT_TEXT, }) ctx.tools.register(defineTool({ name: 'session_search', description: 'Search prior sessions in the caller workspace and return the strongest matching event from each session.', parameters: toolInput.sessionSearchParameters, output: TEXT_OUTPUT, timeoutMs: resolved.searchTimeoutMs, execute: (args, exec) => operations.executeSessionSearch(ctx, args, exec, resolved.maxSearchResults), presentCall: presentation.presentSessionSearchCall, })) ctx.tools.register(defineTool({ name: 'session_event_search', description: 'Search prior events in one authorized session; the current session excludes the step performing this call.', parameters: toolInput.eventSearchParameters, output: TEXT_OUTPUT, timeoutMs: resolved.searchTimeoutMs, execute: (args, exec) => operations.executeEventSearch(ctx, args, exec, resolved.maxSearchResults), presentCall: presentation.presentEventSearchCall, })) ctx.tools.register(defineTool({ name: 'session_trace', description: 'Read the authorized session lineage around one session, including complete visible ancestor and descendant relationships.', parameters: toolInput.targetSessionParameter, output: TEXT_OUTPUT, isConcurrencySafe: () => true, execute: (args, exec) => operations.executeSessionTrace(ctx, args, exec), presentCall: presentation.presentSessionTraceCall, })) ctx.tools.register(defineTool({ name: 'session_event_trace', description: 'Read every direct replacement and relationship to a cited source event for one event in an authorized session.', parameters: { ...toolInput.targetSessionParameter, seq: { type: 'integer', required: true, description: 'Target event sequence number.' }, }, output: TEXT_OUTPUT, isConcurrencySafe: () => true, execute: (args, exec) => operations.executeEventTrace(ctx, args, exec), presentCall: args => presentation.presentEventTargetCall('Trace event', args), })) ctx.tools.register(defineTool({ name: 'session_event_read', description: 'Read one full unabridged event and optional neighboring raw-event summaries from an authorized session.', parameters: { ...toolInput.targetSessionParameter, seq: { type: 'integer', required: true, description: 'Target event sequence number.' }, before: { type: 'integer', description: 'Number of preceding raw events to summarize. Omit for none.' }, after: { type: 'integer', description: 'Number of following raw events to summarize. Omit for none.' }, }, output: TEXT_OUTPUT, isConcurrencySafe: () => true, execute: (args, exec) => operations.executeEventRead(ctx, args, exec), presentCall: args => presentation.presentEventTargetCall('Read event', args), })) } function resolveConfig(config: Config): ResolvedConfig { const maxSearchResults = config.maxSearchResults ?? DEFAULT_MAX_SEARCH_RESULTS const searchTimeoutMs = config.searchTimeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS if (!Number.isSafeInteger(maxSearchResults) || maxSearchResults < 1) { throw new TypeError('tool-session-query: maxSearchResults must be a positive safe integer') } if (!Number.isInteger(searchTimeoutMs) || searchTimeoutMs < 1 || searchTimeoutMs > MAX_TIMER_DELAY_MS) { throw new TypeError( `tool-session-query: searchTimeoutMs must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`, ) } return { maxSearchResults, searchTimeoutMs } }