This commit is contained in:
@@ -12,16 +12,20 @@
|
||||
*
|
||||
* Scoping is enforced by the bound `ctx.taskConversation` IO: comments come from
|
||||
* `listLocalTaskComments(taskId)` (WHERE task_id = ?), and the transcript path is
|
||||
* bound by the worker ONLY to a per-task `runtimeDir` — never the space-shared
|
||||
* `workspacePath/logs` fallback — so a tool call can never read another task's
|
||||
* logs. When the transcript path is unset the transcript source yields nothing
|
||||
* (comments still work); when the whole IO is unset (subtasks / gitea-issue runs)
|
||||
* the tools degrade to a friendly non-error message.
|
||||
* bound by the worker to a location a tool call can never use to read another
|
||||
* task's logs. For a top-level local task that is the per-task `runtimeDir`
|
||||
* (never the space-shared `workspacePath/logs` fallback). A sub-task run instead
|
||||
* gets a transcript-only IO (no comments — see `makeTranscriptOnlyConversationIO`)
|
||||
* bound to the sub-task's OWN isolated workspace transcript. When the transcript
|
||||
* path is unset the transcript source yields nothing (comments still work); when
|
||||
* the whole IO is unset (gitea-issue runs) the tools degrade to a friendly
|
||||
* non-error message.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { ToolDef } from '../../llm/openai-compat.js';
|
||||
import type { ToolContext, ToolResult, TaskConversationComment } from './core.js';
|
||||
import { excerpt, tokenize } from './text-excerpt.js';
|
||||
|
||||
const SEARCH_DEF: ToolDef = {
|
||||
type: 'function',
|
||||
@@ -32,7 +36,7 @@ const SEARCH_DEF: ToolDef = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: '検索キーワード (部分一致・大文字小文字無視)。' },
|
||||
query: { type: 'string', description: '検索キーワード。スペース区切りで複数指定すると AND 検索 (すべての語を含む発言にヒット)。部分一致・大文字小文字無視。' },
|
||||
source: { type: 'string', enum: ['comments', 'transcript', 'both'], description: '検索対象。既定 both。' },
|
||||
author: { type: 'string', enum: ['user', 'agent', 'system'], description: '発言者で絞り込む。' },
|
||||
kind: { type: 'string', description: 'コメント種別で絞り込む (request/comment/interjection/result/ask/progress/handoff)。transcript には適用されない。' },
|
||||
@@ -105,23 +109,6 @@ function loadTranscript(path: string | undefined): TranscriptEntry[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
function collapse(s: string): string {
|
||||
return s.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
/** Compact excerpt centered on the first query hit, bounded to `max` chars. */
|
||||
function excerpt(body: string, query: string, max: number): string {
|
||||
const flat = collapse(body);
|
||||
if (flat.length <= max && !query) return flat;
|
||||
const idx = query ? flat.toLowerCase().indexOf(query.toLowerCase()) : 0;
|
||||
if (idx < 0 || flat.length <= max) {
|
||||
return flat.length <= max ? flat : flat.slice(0, max - 1) + '…';
|
||||
}
|
||||
const start = Math.max(0, idx - 40);
|
||||
const end = Math.min(flat.length, start + max);
|
||||
return (start > 0 ? '…' : '') + flat.slice(start, end) + (end < flat.length ? '…' : '');
|
||||
}
|
||||
|
||||
function clampLimit(v: unknown, def: number, max: number): number {
|
||||
const n = typeof v === 'number' && Number.isFinite(v) ? Math.floor(v) : def;
|
||||
return Math.min(Math.max(n, 1), max);
|
||||
@@ -144,7 +131,11 @@ async function search(input: Record<string, unknown>, ctx: ToolContext): Promise
|
||||
const author = input['author'] as Author | undefined;
|
||||
const kind = typeof input['kind'] === 'string' ? (input['kind'] as string) : undefined;
|
||||
const limit = clampLimit(input['limit'], 10, 50);
|
||||
const q = query.toLowerCase();
|
||||
const tokens = tokenize(query);
|
||||
const matches = (body: string): boolean => {
|
||||
const lower = body.toLowerCase();
|
||||
return tokens.every((t) => lower.includes(t));
|
||||
};
|
||||
|
||||
type Hit = { ref: string; header: string; body: string };
|
||||
const hits: Hit[] = [];
|
||||
@@ -154,7 +145,7 @@ async function search(input: Record<string, unknown>, ctx: ToolContext): Promise
|
||||
for (const c of comments) {
|
||||
if (author && c.author !== author) continue;
|
||||
if (kind && c.kind !== kind) continue;
|
||||
if (!c.body.toLowerCase().includes(q)) continue;
|
||||
if (!matches(c.body)) continue;
|
||||
hits.push({ ref: `comment:${c.id}`, header: `${c.author}/${c.kind} ${c.createdAt}`, body: c.body });
|
||||
}
|
||||
}
|
||||
@@ -165,7 +156,7 @@ async function search(input: Record<string, unknown>, ctx: ToolContext): Promise
|
||||
if (!kind) {
|
||||
for (const e of loadTranscript(io.transcriptPath)) {
|
||||
if (author && e.author !== author) continue;
|
||||
if (!e.content.toLowerCase().includes(q)) continue;
|
||||
if (!matches(e.content)) continue;
|
||||
hits.push({ ref: `transcript:${e.index}`, header: e.author, body: e.content });
|
||||
}
|
||||
}
|
||||
@@ -179,7 +170,7 @@ async function search(input: Record<string, unknown>, ctx: ToolContext): Promise
|
||||
const lines = [`## Conversation Search Results (query: "${query}", ${hits.length} hit(s)${hits.length > limit ? `, showing ${limit}` : ''})`, ''];
|
||||
for (const h of shown) {
|
||||
lines.push(`- ${h.ref} ${h.header}`);
|
||||
lines.push(` ${excerpt(h.body, query, EXCERPT_MAX)}`);
|
||||
lines.push(` ${excerpt(h.body, tokens, EXCERPT_MAX)}`);
|
||||
lines.push('');
|
||||
}
|
||||
lines.push('前後の文脈は ReadTaskConversation({ ref }) で確認できます。');
|
||||
@@ -209,7 +200,7 @@ async function read(input: Record<string, unknown>, ctx: ToolContext): Promise<T
|
||||
for (const c of slice) {
|
||||
const marker = c.id === id ? ' ←' : '';
|
||||
lines.push(`- comment:${c.id} ${c.author}/${c.kind} ${c.createdAt}${marker}`);
|
||||
lines.push(` ${excerpt(c.body, '', READ_EXCERPT_MAX)}`);
|
||||
lines.push(` ${excerpt(c.body, [], READ_EXCERPT_MAX)}`);
|
||||
}
|
||||
return { output: lines.join('\n'), isError: false };
|
||||
}
|
||||
@@ -222,7 +213,7 @@ async function read(input: Record<string, unknown>, ctx: ToolContext): Promise<T
|
||||
for (const e of slice) {
|
||||
const marker = e.index === id ? ' ←' : '';
|
||||
lines.push(`- transcript:${e.index} ${e.author}${marker}`);
|
||||
lines.push(` ${excerpt(e.content, '', READ_EXCERPT_MAX)}`);
|
||||
lines.push(` ${excerpt(e.content, [], READ_EXCERPT_MAX)}`);
|
||||
}
|
||||
return { output: lines.join('\n'), isError: false };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user