botholomew 0.12.3 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -68
- package/package.json +3 -3
- package/src/chat/agent.ts +42 -82
- package/src/chat/session.ts +29 -25
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +177 -926
- package/src/commands/db.ts +9 -13
- package/src/commands/init.ts +4 -1
- package/src/commands/nuke.ts +57 -90
- package/src/commands/schedule.ts +103 -124
- package/src/commands/skill.ts +2 -2
- package/src/commands/task.ts +86 -95
- package/src/commands/thread.ts +107 -112
- package/src/commands/worker.ts +88 -88
- package/src/constants.ts +93 -16
- package/src/context/capabilities.ts +10 -10
- package/src/context/fetcher.ts +9 -10
- package/src/context/reindex.ts +189 -0
- package/src/context/store.ts +630 -0
- package/src/db/doctor.ts +1 -8
- package/src/db/embeddings.ts +227 -175
- package/src/db/sql/19-disk_backed_index.sql +36 -0
- package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
- package/src/fs/atomic.ts +217 -0
- package/src/fs/compat.ts +86 -0
- package/src/fs/sandbox.ts +279 -0
- package/src/init/index.ts +69 -52
- package/src/init/templates.ts +1 -1
- package/src/mcpx/client.ts +1 -1
- package/src/schedules/schema.ts +19 -0
- package/src/schedules/store.ts +296 -0
- package/src/skills/commands.ts +1 -3
- package/src/tasks/schema.ts +47 -0
- package/src/tasks/store.ts +486 -0
- package/src/threads/store.ts +559 -0
- package/src/tools/capabilities/refresh.ts +42 -21
- package/src/tools/context/pipe.ts +15 -71
- package/src/tools/context/update-beliefs.ts +3 -3
- package/src/tools/context/update-goals.ts +3 -3
- package/src/tools/dir/create.ts +26 -23
- package/src/tools/dir/size.ts +46 -17
- package/src/tools/dir/tree.ts +73 -279
- package/src/tools/file/copy.ts +50 -24
- package/src/tools/file/count-lines.ts +34 -10
- package/src/tools/file/delete.ts +44 -23
- package/src/tools/file/edit.ts +39 -14
- package/src/tools/file/exists.ts +12 -26
- package/src/tools/file/info.ts +25 -85
- package/src/tools/file/move.ts +39 -24
- package/src/tools/file/read.ts +32 -80
- package/src/tools/file/write.ts +14 -91
- package/src/tools/registry.ts +3 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schedule/list.ts +7 -3
- package/src/tools/search/fuse.ts +12 -33
- package/src/tools/search/index.ts +36 -43
- package/src/tools/search/regexp.ts +29 -17
- package/src/tools/search/semantic.ts +137 -51
- package/src/tools/skill/delete.ts +1 -1
- package/src/tools/skill/list.ts +1 -1
- package/src/tools/skill/write.ts +1 -1
- package/src/tools/task/create.ts +41 -16
- package/src/tools/task/delete.ts +3 -3
- package/src/tools/task/list.ts +6 -3
- package/src/tools/task/update.ts +31 -9
- package/src/tools/task/view.ts +6 -6
- package/src/tools/thread/list.ts +2 -2
- package/src/tools/thread/search.ts +208 -0
- package/src/tools/thread/view.ts +50 -5
- package/src/tools/worker/spawn.ts +28 -14
- package/src/tui/App.tsx +12 -19
- package/src/tui/components/ContextPanel.tsx +83 -316
- package/src/tui/components/SchedulePanel.tsx +34 -48
- package/src/tui/components/StatusBar.tsx +15 -15
- package/src/tui/components/TaskPanel.tsx +34 -38
- package/src/tui/components/ThreadPanel.tsx +29 -38
- package/src/tui/components/WorkerPanel.tsx +21 -19
- package/src/tui/markdown.ts +2 -8
- package/src/types/file-imports.d.ts +9 -0
- package/src/utils/title.ts +5 -7
- package/src/utils/v7-date.ts +47 -0
- package/src/worker/heartbeat.ts +46 -24
- package/src/worker/index.ts +13 -15
- package/src/worker/llm.ts +30 -37
- package/src/worker/prompt.ts +19 -41
- package/src/worker/schedules.ts +48 -69
- package/src/worker/spawn.ts +11 -11
- package/src/worker/tick.ts +39 -43
- package/src/workers/store.ts +247 -0
- package/src/commands/tools.ts +0 -367
- package/src/context/describer.ts +0 -140
- package/src/context/drives.ts +0 -110
- package/src/context/ingest.ts +0 -162
- package/src/context/refresh.ts +0 -183
- package/src/db/context.ts +0 -637
- package/src/db/daemon-state.ts +0 -6
- package/src/db/reembed.ts +0 -113
- package/src/db/schedules.ts +0 -213
- package/src/db/tasks.ts +0 -347
- package/src/db/threads.ts +0 -276
- package/src/db/workers.ts +0 -212
- package/src/tools/context/list-drives.ts +0 -36
- package/src/tools/context/refresh.ts +0 -165
- package/src/tools/context/search.ts +0 -54
package/src/tools/task/create.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { TASK_PRIORITIES } from "../../tasks/schema.ts";
|
|
3
|
+
import { CircularDependencyError, createTask } from "../../tasks/store.ts";
|
|
3
4
|
import { logger } from "../../utils/logger.ts";
|
|
4
5
|
import type { ToolDefinition } from "../tool.ts";
|
|
5
6
|
|
|
@@ -21,13 +22,21 @@ const inputSchema = z.object({
|
|
|
21
22
|
.array(z.string())
|
|
22
23
|
.optional()
|
|
23
24
|
.describe("IDs of tasks that must complete first"),
|
|
25
|
+
context_paths: z
|
|
26
|
+
.array(z.string())
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
"Project-relative paths under context/ that the task should reference",
|
|
30
|
+
),
|
|
24
31
|
});
|
|
25
32
|
|
|
26
33
|
const outputSchema = z.object({
|
|
27
|
-
id: z.string(),
|
|
28
|
-
name: z.string(),
|
|
34
|
+
id: z.string().nullable(),
|
|
35
|
+
name: z.string().nullable(),
|
|
29
36
|
message: z.string(),
|
|
30
37
|
is_error: z.boolean(),
|
|
38
|
+
error_type: z.string().optional(),
|
|
39
|
+
next_action_hint: z.string().optional(),
|
|
31
40
|
});
|
|
32
41
|
|
|
33
42
|
export const createTaskTool = {
|
|
@@ -38,18 +47,34 @@ export const createTaskTool = {
|
|
|
38
47
|
inputSchema,
|
|
39
48
|
outputSchema,
|
|
40
49
|
execute: async (input, ctx) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
try {
|
|
51
|
+
const newTask = await createTask(ctx.projectDir, {
|
|
52
|
+
name: input.name,
|
|
53
|
+
description: input.description,
|
|
54
|
+
priority: input.priority,
|
|
55
|
+
blocked_by: input.blocked_by,
|
|
56
|
+
context_paths: input.context_paths,
|
|
57
|
+
});
|
|
58
|
+
logger.info(`Created subtask: ${newTask.name} (${newTask.id})`);
|
|
59
|
+
return {
|
|
60
|
+
id: newTask.id,
|
|
61
|
+
name: newTask.name,
|
|
62
|
+
message: `Created task "${newTask.name}" with ID ${newTask.id}`,
|
|
63
|
+
is_error: false,
|
|
64
|
+
};
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err instanceof CircularDependencyError) {
|
|
67
|
+
return {
|
|
68
|
+
id: null,
|
|
69
|
+
name: null,
|
|
70
|
+
message: err.message,
|
|
71
|
+
is_error: true,
|
|
72
|
+
error_type: "circular_dependency",
|
|
73
|
+
next_action_hint:
|
|
74
|
+
"Pick blockers that don't transitively depend on this task. `list_tasks` + `view_task` show the existing dependency graph.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
54
79
|
},
|
|
55
80
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/task/delete.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { deleteTask, getTask } from "../../
|
|
2
|
+
import { deleteTask, getTask } from "../../tasks/store.ts";
|
|
3
3
|
import { logger } from "../../utils/logger.ts";
|
|
4
4
|
import type { ToolDefinition } from "../tool.ts";
|
|
5
5
|
|
|
@@ -21,7 +21,7 @@ export const deleteTaskTool = {
|
|
|
21
21
|
inputSchema,
|
|
22
22
|
outputSchema,
|
|
23
23
|
execute: async (input, ctx) => {
|
|
24
|
-
const existing = await getTask(ctx.
|
|
24
|
+
const existing = await getTask(ctx.projectDir, input.id);
|
|
25
25
|
if (!existing) {
|
|
26
26
|
return {
|
|
27
27
|
deleted_id: null,
|
|
@@ -36,7 +36,7 @@ export const deleteTaskTool = {
|
|
|
36
36
|
is_error: true,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
const ok = await deleteTask(ctx.
|
|
39
|
+
const ok = await deleteTask(ctx.projectDir, input.id);
|
|
40
40
|
if (!ok) {
|
|
41
41
|
return {
|
|
42
42
|
deleted_id: null,
|
package/src/tools/task/list.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { TASK_PRIORITIES, TASK_STATUSES } from "../../tasks/schema.ts";
|
|
3
|
+
import { listTasks } from "../../tasks/store.ts";
|
|
3
4
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
5
|
|
|
5
6
|
const inputSchema = z.object({
|
|
6
7
|
status: z.enum(TASK_STATUSES).optional().describe("Filter by status"),
|
|
7
8
|
priority: z.enum(TASK_PRIORITIES).optional().describe("Filter by priority"),
|
|
8
9
|
limit: z.number().optional().describe("Max number of tasks to return"),
|
|
10
|
+
offset: z.number().optional().describe("Skip first N tasks"),
|
|
9
11
|
});
|
|
10
12
|
|
|
11
13
|
const outputSchema = z.object({
|
|
@@ -31,10 +33,11 @@ export const listTasksTool = {
|
|
|
31
33
|
inputSchema,
|
|
32
34
|
outputSchema,
|
|
33
35
|
execute: async (input, ctx) => {
|
|
34
|
-
const tasks = await listTasks(ctx.
|
|
36
|
+
const tasks = await listTasks(ctx.projectDir, {
|
|
35
37
|
status: input.status,
|
|
36
38
|
priority: input.priority,
|
|
37
39
|
limit: input.limit,
|
|
40
|
+
offset: input.offset,
|
|
38
41
|
});
|
|
39
42
|
return {
|
|
40
43
|
tasks: tasks.map((t) => ({
|
|
@@ -44,7 +47,7 @@ export const listTasksTool = {
|
|
|
44
47
|
priority: t.priority,
|
|
45
48
|
description: t.description,
|
|
46
49
|
output: t.output,
|
|
47
|
-
created_at: t.created_at
|
|
50
|
+
created_at: t.created_at,
|
|
48
51
|
})),
|
|
49
52
|
count: tasks.length,
|
|
50
53
|
is_error: false,
|
package/src/tools/task/update.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { TASK_PRIORITIES } from "../../tasks/schema.ts";
|
|
3
|
+
import {
|
|
4
|
+
CircularDependencyError,
|
|
5
|
+
getTask,
|
|
6
|
+
updateTask,
|
|
7
|
+
} from "../../tasks/store.ts";
|
|
3
8
|
import { logger } from "../../utils/logger.ts";
|
|
4
9
|
import type { ToolDefinition } from "../tool.ts";
|
|
5
10
|
|
|
@@ -28,6 +33,8 @@ const outputSchema = z.object({
|
|
|
28
33
|
.nullable(),
|
|
29
34
|
message: z.string(),
|
|
30
35
|
is_error: z.boolean(),
|
|
36
|
+
error_type: z.string().optional(),
|
|
37
|
+
next_action_hint: z.string().optional(),
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
export const updateTaskTool = {
|
|
@@ -38,7 +45,7 @@ export const updateTaskTool = {
|
|
|
38
45
|
inputSchema,
|
|
39
46
|
outputSchema,
|
|
40
47
|
execute: async (input, ctx) => {
|
|
41
|
-
const existing = await getTask(ctx.
|
|
48
|
+
const existing = await getTask(ctx.projectDir, input.id);
|
|
42
49
|
if (!existing) {
|
|
43
50
|
return {
|
|
44
51
|
task: null,
|
|
@@ -54,12 +61,27 @@ export const updateTaskTool = {
|
|
|
54
61
|
};
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
let updated: Awaited<ReturnType<typeof updateTask>>;
|
|
65
|
+
try {
|
|
66
|
+
updated = await updateTask(ctx.projectDir, input.id, {
|
|
67
|
+
name: input.name,
|
|
68
|
+
description: input.description,
|
|
69
|
+
priority: input.priority,
|
|
70
|
+
blocked_by: input.blocked_by,
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err instanceof CircularDependencyError) {
|
|
74
|
+
return {
|
|
75
|
+
task: null,
|
|
76
|
+
message: err.message,
|
|
77
|
+
is_error: true,
|
|
78
|
+
error_type: "circular_dependency",
|
|
79
|
+
next_action_hint:
|
|
80
|
+
"Pick blockers that don't transitively depend on this task.",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
63
85
|
|
|
64
86
|
if (!updated) {
|
|
65
87
|
return {
|
|
@@ -78,7 +100,7 @@ export const updateTaskTool = {
|
|
|
78
100
|
status: updated.status,
|
|
79
101
|
priority: updated.priority,
|
|
80
102
|
blocked_by: updated.blocked_by,
|
|
81
|
-
updated_at: updated.updated_at
|
|
103
|
+
updated_at: updated.updated_at,
|
|
82
104
|
},
|
|
83
105
|
message: `Updated task "${updated.name}"`,
|
|
84
106
|
is_error: false,
|
package/src/tools/task/view.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getTask } from "../../
|
|
2
|
+
import { getTask } from "../../tasks/store.ts";
|
|
3
3
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
4
|
|
|
5
5
|
const inputSchema = z.object({
|
|
@@ -18,7 +18,7 @@ const outputSchema = z.object({
|
|
|
18
18
|
output: z.string().nullable(),
|
|
19
19
|
claimed_by: z.string().nullable(),
|
|
20
20
|
blocked_by: z.array(z.string()),
|
|
21
|
-
|
|
21
|
+
context_paths: z.array(z.string()),
|
|
22
22
|
created_at: z.string(),
|
|
23
23
|
updated_at: z.string(),
|
|
24
24
|
})
|
|
@@ -33,7 +33,7 @@ export const viewTaskTool = {
|
|
|
33
33
|
inputSchema,
|
|
34
34
|
outputSchema,
|
|
35
35
|
execute: async (input, ctx) => {
|
|
36
|
-
const task = await getTask(ctx.
|
|
36
|
+
const task = await getTask(ctx.projectDir, input.id);
|
|
37
37
|
if (!task) return { task: null, is_error: true };
|
|
38
38
|
return {
|
|
39
39
|
task: {
|
|
@@ -46,9 +46,9 @@ export const viewTaskTool = {
|
|
|
46
46
|
output: task.output,
|
|
47
47
|
claimed_by: task.claimed_by,
|
|
48
48
|
blocked_by: task.blocked_by,
|
|
49
|
-
|
|
50
|
-
created_at: task.created_at
|
|
51
|
-
updated_at: task.updated_at
|
|
49
|
+
context_paths: task.context_paths,
|
|
50
|
+
created_at: task.created_at,
|
|
51
|
+
updated_at: task.updated_at,
|
|
52
52
|
},
|
|
53
53
|
is_error: false,
|
|
54
54
|
};
|
package/src/tools/thread/list.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { listThreads } from "../../
|
|
2
|
+
import { listThreads } from "../../threads/store.ts";
|
|
3
3
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
4
|
|
|
5
5
|
const inputSchema = z.object({
|
|
@@ -32,7 +32,7 @@ export const listThreadsTool = {
|
|
|
32
32
|
inputSchema,
|
|
33
33
|
outputSchema,
|
|
34
34
|
execute: async (input, ctx) => {
|
|
35
|
-
const threads = await listThreads(ctx.
|
|
35
|
+
const threads = await listThreads(ctx.projectDir, {
|
|
36
36
|
type: input.type,
|
|
37
37
|
limit: input.limit,
|
|
38
38
|
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
getThread,
|
|
4
|
+
type Interaction,
|
|
5
|
+
type InteractionKind,
|
|
6
|
+
type InteractionRole,
|
|
7
|
+
listThreads,
|
|
8
|
+
type Thread,
|
|
9
|
+
} from "../../threads/store.ts";
|
|
10
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
11
|
+
|
|
12
|
+
const ROLES = ["user", "assistant", "system", "tool"] as const;
|
|
13
|
+
const KINDS = [
|
|
14
|
+
"message",
|
|
15
|
+
"thinking",
|
|
16
|
+
"tool_use",
|
|
17
|
+
"tool_result",
|
|
18
|
+
"context_update",
|
|
19
|
+
"status_change",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
const SNIPPET_MAX = 240;
|
|
23
|
+
|
|
24
|
+
const inputSchema = z.object({
|
|
25
|
+
pattern: z
|
|
26
|
+
.string()
|
|
27
|
+
.describe(
|
|
28
|
+
"Regex pattern matched against each interaction's `content`. Use a plain substring (it's a regex, but plain text Just Works).",
|
|
29
|
+
),
|
|
30
|
+
ignore_case: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.default(true)
|
|
34
|
+
.describe("Case-insensitive regex (default true)."),
|
|
35
|
+
role: z
|
|
36
|
+
.enum(ROLES)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe(
|
|
39
|
+
"Restrict matches to a single role (user/assistant/system/tool).",
|
|
40
|
+
),
|
|
41
|
+
kind: z
|
|
42
|
+
.enum(KINDS)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe(
|
|
45
|
+
"Restrict matches to a single interaction kind (message/tool_use/tool_result/etc).",
|
|
46
|
+
),
|
|
47
|
+
thread_type: z
|
|
48
|
+
.enum(["worker_tick", "chat_session"])
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Restrict to chat sessions or worker-tick threads."),
|
|
51
|
+
since: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("ISO date — only consider threads started on or after this."),
|
|
55
|
+
until: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("ISO date — only consider threads started on or before this."),
|
|
59
|
+
max_results: z
|
|
60
|
+
.number()
|
|
61
|
+
.int()
|
|
62
|
+
.positive()
|
|
63
|
+
.optional()
|
|
64
|
+
.default(20)
|
|
65
|
+
.describe("Maximum number of hits to return across all threads."),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const HitSchema = z.object({
|
|
69
|
+
thread_id: z.string(),
|
|
70
|
+
thread_title: z.string(),
|
|
71
|
+
thread_type: z.string(),
|
|
72
|
+
sequence: z
|
|
73
|
+
.number()
|
|
74
|
+
.describe(
|
|
75
|
+
"1-based sequence of the matching interaction in the thread. Plug this into `view_thread({ id, offset: sequence-1, limit: 5 })` to read context around the hit.",
|
|
76
|
+
),
|
|
77
|
+
role: z.string(),
|
|
78
|
+
kind: z.string(),
|
|
79
|
+
content_snippet: z.string(),
|
|
80
|
+
created_at: z.string(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const outputSchema = z.object({
|
|
84
|
+
matches: z.array(HitSchema),
|
|
85
|
+
threads_scanned: z.number(),
|
|
86
|
+
is_error: z.boolean(),
|
|
87
|
+
error_type: z.string().optional(),
|
|
88
|
+
message: z.string().optional(),
|
|
89
|
+
next_action_hint: z.string().optional(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const searchThreadsTool = {
|
|
93
|
+
name: "search_threads",
|
|
94
|
+
description:
|
|
95
|
+
"[[ bash equivalent command: grep -r ]] Search past conversations (chat sessions and worker ticks) for a regex match. Returns hits with `(thread_id, sequence)` pairs — pass them to `view_thread` to read context around the match.",
|
|
96
|
+
group: "thread",
|
|
97
|
+
inputSchema,
|
|
98
|
+
outputSchema,
|
|
99
|
+
execute: async (input, ctx) => {
|
|
100
|
+
let regex: RegExp;
|
|
101
|
+
try {
|
|
102
|
+
regex = new RegExp(input.pattern, input.ignore_case ? "i" : "");
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
matches: [],
|
|
106
|
+
threads_scanned: 0,
|
|
107
|
+
is_error: true,
|
|
108
|
+
error_type: "invalid_regex",
|
|
109
|
+
message: `Could not compile pattern: ${err instanceof Error ? err.message : String(err)}`,
|
|
110
|
+
next_action_hint:
|
|
111
|
+
"Double-check the regex; remember `.` is a metacharacter — escape it as `\\.` for a literal dot.",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const sinceMs = input.since
|
|
116
|
+
? Date.parse(input.since)
|
|
117
|
+
: Number.NEGATIVE_INFINITY;
|
|
118
|
+
const untilMs = input.until
|
|
119
|
+
? Date.parse(input.until)
|
|
120
|
+
: Number.POSITIVE_INFINITY;
|
|
121
|
+
|
|
122
|
+
let threads: Thread[];
|
|
123
|
+
try {
|
|
124
|
+
threads = await listThreads(ctx.projectDir, {
|
|
125
|
+
type: input.thread_type,
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return {
|
|
129
|
+
matches: [],
|
|
130
|
+
threads_scanned: 0,
|
|
131
|
+
is_error: true,
|
|
132
|
+
error_type: "list_failed",
|
|
133
|
+
message: `Failed to enumerate threads: ${err instanceof Error ? err.message : String(err)}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type Hit = z.infer<typeof HitSchema>;
|
|
138
|
+
const matches: Hit[] = [];
|
|
139
|
+
let scanned = 0;
|
|
140
|
+
|
|
141
|
+
for (const t of threads) {
|
|
142
|
+
const startedMs = t.started_at.getTime();
|
|
143
|
+
if (startedMs < sinceMs || startedMs > untilMs) continue;
|
|
144
|
+
const data = await getThread(ctx.projectDir, t.id);
|
|
145
|
+
if (!data) continue;
|
|
146
|
+
scanned++;
|
|
147
|
+
for (const ix of data.interactions) {
|
|
148
|
+
if (input.role && ix.role !== input.role) continue;
|
|
149
|
+
if (input.kind && ix.kind !== input.kind) continue;
|
|
150
|
+
if (!matchInteraction(ix, regex)) continue;
|
|
151
|
+
matches.push({
|
|
152
|
+
thread_id: t.id,
|
|
153
|
+
thread_title: t.title || "(untitled)",
|
|
154
|
+
thread_type: t.type,
|
|
155
|
+
sequence: ix.sequence,
|
|
156
|
+
role: ix.role,
|
|
157
|
+
kind: ix.kind,
|
|
158
|
+
content_snippet: snippetForMatch(ix.content, regex),
|
|
159
|
+
created_at: ix.created_at.toISOString(),
|
|
160
|
+
});
|
|
161
|
+
if (matches.length >= input.max_results) break;
|
|
162
|
+
}
|
|
163
|
+
if (matches.length >= input.max_results) break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
matches,
|
|
168
|
+
threads_scanned: scanned,
|
|
169
|
+
is_error: false,
|
|
170
|
+
next_action_hint:
|
|
171
|
+
matches.length === 0
|
|
172
|
+
? `No hits in ${scanned} thread(s). Try a broader pattern or remove role/kind filters.`
|
|
173
|
+
: `Pass any (thread_id, sequence) into view_thread({ id: thread_id, offset: sequence - 1, limit: 5 }) to read surrounding context.`,
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
177
|
+
|
|
178
|
+
function matchInteraction(ix: Interaction, regex: RegExp): boolean {
|
|
179
|
+
// We treat the user-visible content as the primary haystack, but a
|
|
180
|
+
// tool_use interaction's content is just "Calling <name>" — fall through
|
|
181
|
+
// to the tool name + JSON args so a search for an exact tool argument
|
|
182
|
+
// still finds the call.
|
|
183
|
+
if (regex.test(ix.content)) return true;
|
|
184
|
+
if (ix.tool_name && regex.test(ix.tool_name)) return true;
|
|
185
|
+
if (ix.tool_input && regex.test(ix.tool_input)) return true;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Pick a short window around the first regex match so the agent gets enough
|
|
191
|
+
* context to know whether the hit is relevant without paging the whole
|
|
192
|
+
* interaction. Falls back to the head when the match index isn't available.
|
|
193
|
+
*/
|
|
194
|
+
function snippetForMatch(content: string, regex: RegExp): string {
|
|
195
|
+
const m = regex.exec(content);
|
|
196
|
+
if (!m) return content.slice(0, SNIPPET_MAX);
|
|
197
|
+
const idx = m.index;
|
|
198
|
+
const start = Math.max(0, idx - 60);
|
|
199
|
+
const end = Math.min(content.length, idx + SNIPPET_MAX - 60);
|
|
200
|
+
let snippet = content.slice(start, end);
|
|
201
|
+
if (start > 0) snippet = `…${snippet}`;
|
|
202
|
+
if (end < content.length) snippet = `${snippet}…`;
|
|
203
|
+
return snippet;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Keep the role/kind unions exported for tests that want to type-pin filters.
|
|
207
|
+
export type SearchThreadsRole = InteractionRole;
|
|
208
|
+
export type SearchThreadsKind = InteractionKind;
|
package/src/tools/thread/view.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getThread } from "../../
|
|
2
|
+
import { getThread } from "../../threads/store.ts";
|
|
3
3
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
4
|
|
|
5
5
|
const inputSchema = z.object({
|
|
6
6
|
id: z.string().describe("Thread ID to view"),
|
|
7
|
+
offset: z
|
|
8
|
+
.number()
|
|
9
|
+
.int()
|
|
10
|
+
.nonnegative()
|
|
11
|
+
.optional()
|
|
12
|
+
.default(0)
|
|
13
|
+
.describe(
|
|
14
|
+
"1-based sequence to start from (skip earlier interactions). Use with `limit` to paginate long threads.",
|
|
15
|
+
),
|
|
16
|
+
limit: z
|
|
17
|
+
.number()
|
|
18
|
+
.int()
|
|
19
|
+
.positive()
|
|
20
|
+
.optional()
|
|
21
|
+
.default(50)
|
|
22
|
+
.describe(
|
|
23
|
+
"Max interactions to return in this page. Default 50 keeps long threads from blowing the LLM context window.",
|
|
24
|
+
),
|
|
7
25
|
});
|
|
8
26
|
|
|
9
27
|
const outputSchema = z.object({
|
|
@@ -28,19 +46,39 @@ const outputSchema = z.object({
|
|
|
28
46
|
created_at: z.string(),
|
|
29
47
|
}),
|
|
30
48
|
),
|
|
49
|
+
total_interactions: z.number(),
|
|
50
|
+
offset: z.number(),
|
|
51
|
+
limit: z.number(),
|
|
52
|
+
has_more: z.boolean(),
|
|
31
53
|
is_error: z.boolean(),
|
|
54
|
+
next_action_hint: z.string().optional(),
|
|
32
55
|
});
|
|
33
56
|
|
|
34
57
|
export const viewThreadTool = {
|
|
35
58
|
name: "view_thread",
|
|
36
59
|
description:
|
|
37
|
-
"View a thread and its
|
|
60
|
+
"View a thread's metadata and a paginated slice of its interactions. Pass `offset` (sequence to start from) and `limit` to walk a long thread without flooding the context window. `search_threads` returns `(thread_id, sequence)` pairs you can plug into `offset` to jump straight to a hit.",
|
|
38
61
|
group: "thread",
|
|
39
62
|
inputSchema,
|
|
40
63
|
outputSchema,
|
|
41
64
|
execute: async (input, ctx) => {
|
|
42
|
-
const result = await getThread(ctx.
|
|
43
|
-
if (!result)
|
|
65
|
+
const result = await getThread(ctx.projectDir, input.id);
|
|
66
|
+
if (!result) {
|
|
67
|
+
return {
|
|
68
|
+
thread: null,
|
|
69
|
+
interactions: [],
|
|
70
|
+
total_interactions: 0,
|
|
71
|
+
offset: input.offset,
|
|
72
|
+
limit: input.limit,
|
|
73
|
+
has_more: false,
|
|
74
|
+
is_error: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const total = result.interactions.length;
|
|
78
|
+
const start = Math.min(input.offset, total);
|
|
79
|
+
const end = Math.min(start + input.limit, total);
|
|
80
|
+
const page = result.interactions.slice(start, end);
|
|
81
|
+
const hasMore = end < total;
|
|
44
82
|
return {
|
|
45
83
|
thread: {
|
|
46
84
|
id: result.thread.id,
|
|
@@ -50,7 +88,7 @@ export const viewThreadTool = {
|
|
|
50
88
|
started_at: result.thread.started_at.toISOString(),
|
|
51
89
|
ended_at: result.thread.ended_at?.toISOString() ?? null,
|
|
52
90
|
},
|
|
53
|
-
interactions:
|
|
91
|
+
interactions: page.map((i) => ({
|
|
54
92
|
id: i.id,
|
|
55
93
|
sequence: i.sequence,
|
|
56
94
|
role: i.role,
|
|
@@ -59,6 +97,13 @@ export const viewThreadTool = {
|
|
|
59
97
|
tool_name: i.tool_name,
|
|
60
98
|
created_at: i.created_at.toISOString(),
|
|
61
99
|
})),
|
|
100
|
+
total_interactions: total,
|
|
101
|
+
offset: start,
|
|
102
|
+
limit: input.limit,
|
|
103
|
+
has_more: hasMore,
|
|
104
|
+
next_action_hint: hasMore
|
|
105
|
+
? `Call view_thread again with offset=${end} to see the next page.`
|
|
106
|
+
: undefined,
|
|
62
107
|
is_error: false,
|
|
63
108
|
};
|
|
64
109
|
},
|
|
@@ -18,10 +18,12 @@ const inputSchema = z.object({
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const outputSchema = z.object({
|
|
21
|
-
worker_pid: z.number(),
|
|
21
|
+
worker_pid: z.number().nullable(),
|
|
22
22
|
mode: z.enum(["once", "persist"]),
|
|
23
23
|
message: z.string(),
|
|
24
24
|
is_error: z.boolean(),
|
|
25
|
+
error_type: z.string().optional(),
|
|
26
|
+
next_action_hint: z.string().optional(),
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
export const spawnWorkerTool = {
|
|
@@ -33,18 +35,30 @@ export const spawnWorkerTool = {
|
|
|
33
35
|
outputSchema,
|
|
34
36
|
execute: async (input, ctx) => {
|
|
35
37
|
const mode = input.persist ? "persist" : "once";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
try {
|
|
39
|
+
const { pid } = await spawnWorker(ctx.projectDir, {
|
|
40
|
+
mode,
|
|
41
|
+
taskId: input.task_id,
|
|
42
|
+
});
|
|
43
|
+
const target = input.task_id
|
|
44
|
+
? `task ${input.task_id}`
|
|
45
|
+
: "next eligible task";
|
|
46
|
+
return {
|
|
47
|
+
worker_pid: pid,
|
|
48
|
+
mode,
|
|
49
|
+
message: `Spawned ${mode} worker (pid ${pid}) for ${target}.`,
|
|
50
|
+
is_error: false,
|
|
51
|
+
};
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
worker_pid: null,
|
|
55
|
+
mode,
|
|
56
|
+
message: err instanceof Error ? err.message : String(err),
|
|
57
|
+
is_error: true,
|
|
58
|
+
error_type: "spawn_failed",
|
|
59
|
+
next_action_hint:
|
|
60
|
+
"Bun must be on PATH for the spawned child to launch. Confirm with `which bun` from the same shell that runs the agent.",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
49
63
|
},
|
|
50
64
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|