@codeany/open-agent-sdk 0.1.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.
Files changed (57) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +388 -0
  4. package/examples/01-simple-query.ts +43 -0
  5. package/examples/02-multi-tool.ts +44 -0
  6. package/examples/03-multi-turn.ts +39 -0
  7. package/examples/04-prompt-api.ts +29 -0
  8. package/examples/05-custom-system-prompt.ts +26 -0
  9. package/examples/06-mcp-server.ts +49 -0
  10. package/examples/07-custom-tools.ts +87 -0
  11. package/examples/08-official-api-compat.ts +38 -0
  12. package/examples/09-subagents.ts +48 -0
  13. package/examples/10-permissions.ts +40 -0
  14. package/examples/11-custom-mcp-tools.ts +101 -0
  15. package/examples/web/index.html +365 -0
  16. package/examples/web/server.ts +157 -0
  17. package/package.json +60 -0
  18. package/src/agent.ts +425 -0
  19. package/src/engine.ts +520 -0
  20. package/src/hooks.ts +261 -0
  21. package/src/index.ts +376 -0
  22. package/src/mcp/client.ts +150 -0
  23. package/src/sdk-mcp-server.ts +78 -0
  24. package/src/session.ts +227 -0
  25. package/src/tool-helper.ts +127 -0
  26. package/src/tools/agent-tool.ts +153 -0
  27. package/src/tools/ask-user.ts +79 -0
  28. package/src/tools/bash.ts +75 -0
  29. package/src/tools/config-tool.ts +89 -0
  30. package/src/tools/cron-tools.ts +153 -0
  31. package/src/tools/edit.ts +74 -0
  32. package/src/tools/glob.ts +77 -0
  33. package/src/tools/grep.ts +168 -0
  34. package/src/tools/index.ts +232 -0
  35. package/src/tools/lsp-tool.ts +163 -0
  36. package/src/tools/mcp-resource-tools.ts +125 -0
  37. package/src/tools/notebook-edit.ts +93 -0
  38. package/src/tools/plan-tools.ts +88 -0
  39. package/src/tools/read.ts +73 -0
  40. package/src/tools/send-message.ts +96 -0
  41. package/src/tools/task-tools.ts +290 -0
  42. package/src/tools/team-tools.ts +128 -0
  43. package/src/tools/todo-tool.ts +112 -0
  44. package/src/tools/tool-search.ts +87 -0
  45. package/src/tools/types.ts +62 -0
  46. package/src/tools/web-fetch.ts +66 -0
  47. package/src/tools/web-search.ts +86 -0
  48. package/src/tools/worktree-tools.ts +140 -0
  49. package/src/tools/write.ts +42 -0
  50. package/src/types.ts +459 -0
  51. package/src/utils/compact.ts +206 -0
  52. package/src/utils/context.ts +191 -0
  53. package/src/utils/fileCache.ts +148 -0
  54. package/src/utils/messages.ts +196 -0
  55. package/src/utils/retry.ts +140 -0
  56. package/src/utils/tokens.ts +122 -0
  57. package/tsconfig.json +19 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * NotebookEditTool - Edit Jupyter notebooks
3
+ */
4
+
5
+ import { readFile, writeFile } from 'fs/promises'
6
+ import { resolve } from 'path'
7
+ import { defineTool } from './types.js'
8
+
9
+ export const NotebookEditTool = defineTool({
10
+ name: 'NotebookEdit',
11
+ description: 'Edit Jupyter notebook (.ipynb) cells. Can insert, replace, or delete cells.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ file_path: {
16
+ type: 'string',
17
+ description: 'Path to the .ipynb file',
18
+ },
19
+ command: {
20
+ type: 'string',
21
+ enum: ['insert', 'replace', 'delete'],
22
+ description: 'The edit operation to perform',
23
+ },
24
+ cell_number: {
25
+ type: 'number',
26
+ description: 'Cell index (0-based) to operate on',
27
+ },
28
+ cell_type: {
29
+ type: 'string',
30
+ enum: ['code', 'markdown'],
31
+ description: 'Type of cell (for insert/replace)',
32
+ },
33
+ source: {
34
+ type: 'string',
35
+ description: 'Cell content (for insert/replace)',
36
+ },
37
+ },
38
+ required: ['file_path', 'command', 'cell_number'],
39
+ },
40
+ isReadOnly: false,
41
+ isConcurrencySafe: false,
42
+ async call(input, context) {
43
+ const filePath = resolve(context.cwd, input.file_path)
44
+
45
+ try {
46
+ const content = await readFile(filePath, 'utf-8')
47
+ const notebook = JSON.parse(content)
48
+
49
+ if (!notebook.cells || !Array.isArray(notebook.cells)) {
50
+ return { data: 'Error: Invalid notebook format', is_error: true }
51
+ }
52
+
53
+ const { command, cell_number, cell_type, source } = input
54
+
55
+ switch (command) {
56
+ case 'insert': {
57
+ const newCell = {
58
+ cell_type: cell_type || 'code',
59
+ source: (source || '').split('\n').map((l: string, i: number, arr: string[]) =>
60
+ i < arr.length - 1 ? l + '\n' : l
61
+ ),
62
+ metadata: {},
63
+ ...(cell_type !== 'markdown' ? { outputs: [], execution_count: null } : {}),
64
+ }
65
+ notebook.cells.splice(cell_number, 0, newCell)
66
+ break
67
+ }
68
+ case 'replace': {
69
+ if (cell_number >= notebook.cells.length) {
70
+ return { data: `Error: Cell ${cell_number} does not exist`, is_error: true }
71
+ }
72
+ notebook.cells[cell_number].source = (source || '').split('\n').map(
73
+ (l: string, i: number, arr: string[]) => i < arr.length - 1 ? l + '\n' : l
74
+ )
75
+ if (cell_type) notebook.cells[cell_number].cell_type = cell_type
76
+ break
77
+ }
78
+ case 'delete': {
79
+ if (cell_number >= notebook.cells.length) {
80
+ return { data: `Error: Cell ${cell_number} does not exist`, is_error: true }
81
+ }
82
+ notebook.cells.splice(cell_number, 1)
83
+ break
84
+ }
85
+ }
86
+
87
+ await writeFile(filePath, JSON.stringify(notebook, null, 1), 'utf-8')
88
+ return `Notebook ${command}: cell ${cell_number} in ${filePath}`
89
+ } catch (err: any) {
90
+ return { data: `Error: ${err.message}`, is_error: true }
91
+ }
92
+ },
93
+ })
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Plan Mode Tools
3
+ *
4
+ * EnterPlanMode / ExitPlanMode - Structured planning workflow.
5
+ * Allows the agent to enter a design/planning phase before execution.
6
+ */
7
+
8
+ import type { ToolDefinition, ToolResult } from '../types.js'
9
+
10
+ // Track plan mode state
11
+ let planModeActive = false
12
+ let currentPlan: string | null = null
13
+
14
+ export function isPlanModeActive(): boolean {
15
+ return planModeActive
16
+ }
17
+
18
+ export function getCurrentPlan(): string | null {
19
+ return currentPlan
20
+ }
21
+
22
+ export const EnterPlanModeTool: ToolDefinition = {
23
+ name: 'EnterPlanMode',
24
+ description: 'Enter plan/design mode for complex tasks. In plan mode, the agent focuses on designing the approach before executing.',
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {},
28
+ },
29
+ isReadOnly: () => false,
30
+ isConcurrencySafe: () => false,
31
+ isEnabled: () => true,
32
+ async prompt() { return 'Enter plan mode for structured planning.' },
33
+ async call(): Promise<ToolResult> {
34
+ if (planModeActive) {
35
+ return {
36
+ type: 'tool_result',
37
+ tool_use_id: '',
38
+ content: 'Already in plan mode.',
39
+ }
40
+ }
41
+
42
+ planModeActive = true
43
+ currentPlan = null
44
+
45
+ return {
46
+ type: 'tool_result',
47
+ tool_use_id: '',
48
+ content: 'Entered plan mode. Design your approach before executing. Use ExitPlanMode when the plan is ready.',
49
+ }
50
+ },
51
+ }
52
+
53
+ export const ExitPlanModeTool: ToolDefinition = {
54
+ name: 'ExitPlanMode',
55
+ description: 'Exit plan mode with a completed plan. The plan will be recorded and execution can proceed.',
56
+ inputSchema: {
57
+ type: 'object',
58
+ properties: {
59
+ plan: { type: 'string', description: 'The completed plan' },
60
+ approved: { type: 'boolean', description: 'Whether the plan is approved for execution' },
61
+ },
62
+ },
63
+ isReadOnly: () => false,
64
+ isConcurrencySafe: () => false,
65
+ isEnabled: () => true,
66
+ async prompt() { return 'Exit plan mode with a completed plan.' },
67
+ async call(input: any): Promise<ToolResult> {
68
+ if (!planModeActive) {
69
+ return {
70
+ type: 'tool_result',
71
+ tool_use_id: '',
72
+ content: 'Not in plan mode.',
73
+ is_error: true,
74
+ }
75
+ }
76
+
77
+ planModeActive = false
78
+ currentPlan = input.plan || null
79
+
80
+ const status = input.approved !== false ? 'approved' : 'pending approval'
81
+
82
+ return {
83
+ type: 'tool_result',
84
+ tool_use_id: '',
85
+ content: `Plan mode exited. Plan status: ${status}.${currentPlan ? `\n\nPlan:\n${currentPlan}` : ''}`,
86
+ }
87
+ },
88
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * FileReadTool - Read file contents with line numbers
3
+ */
4
+
5
+ import { readFile, stat } from 'fs/promises'
6
+ import { resolve } from 'path'
7
+ import { defineTool } from './types.js'
8
+
9
+ export const FileReadTool = defineTool({
10
+ name: 'Read',
11
+ description: 'Read a file from the filesystem. Returns content with line numbers. Supports text files, images (returns visual content), and PDFs.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ file_path: {
16
+ type: 'string',
17
+ description: 'The absolute path to the file to read',
18
+ },
19
+ offset: {
20
+ type: 'number',
21
+ description: 'Line number to start reading from (0-based)',
22
+ },
23
+ limit: {
24
+ type: 'number',
25
+ description: 'Maximum number of lines to read',
26
+ },
27
+ },
28
+ required: ['file_path'],
29
+ },
30
+ isReadOnly: true,
31
+ isConcurrencySafe: true,
32
+ async call(input, context) {
33
+ const filePath = resolve(context.cwd, input.file_path)
34
+
35
+ try {
36
+ const fileStat = await stat(filePath)
37
+ if (fileStat.isDirectory()) {
38
+ return { data: `Error: ${filePath} is a directory, not a file. Use Bash with 'ls' to list directory contents.`, is_error: true }
39
+ }
40
+
41
+ // Check for binary/image files
42
+ const ext = filePath.split('.').pop()?.toLowerCase()
43
+ if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'].includes(ext || '')) {
44
+ return `[Image file: ${filePath} (${fileStat.size} bytes)]`
45
+ }
46
+
47
+ const content = await readFile(filePath, 'utf-8')
48
+ const lines = content.split('\n')
49
+
50
+ const offset = input.offset || 0
51
+ const limit = input.limit || 2000
52
+ const selectedLines = lines.slice(offset, offset + limit)
53
+
54
+ // Format with line numbers (cat -n style)
55
+ const numbered = selectedLines.map((line: string, i: number) => {
56
+ const lineNum = offset + i + 1
57
+ return `${lineNum}\t${line}`
58
+ }).join('\n')
59
+
60
+ let result = numbered
61
+ if (lines.length > offset + limit) {
62
+ result += `\n\n(${lines.length - offset - limit} more lines not shown)`
63
+ }
64
+
65
+ return result || '(empty file)'
66
+ } catch (err: any) {
67
+ if (err.code === 'ENOENT') {
68
+ return { data: `Error: File not found: ${filePath}`, is_error: true }
69
+ }
70
+ return { data: `Error reading file: ${err.message}`, is_error: true }
71
+ }
72
+ },
73
+ })
@@ -0,0 +1,96 @@
1
+ /**
2
+ * SendMessageTool - Inter-agent messaging
3
+ *
4
+ * Supports plain text and structured protocol messages
5
+ * between teammates in a multi-agent setup.
6
+ */
7
+
8
+ import type { ToolDefinition, ToolResult } from '../types.js'
9
+
10
+ /**
11
+ * Message inbox for inter-agent communication.
12
+ */
13
+ export interface AgentMessage {
14
+ from: string
15
+ to: string
16
+ content: string
17
+ timestamp: string
18
+ type: 'text' | 'shutdown_request' | 'shutdown_response' | 'plan_approval_response'
19
+ }
20
+
21
+ const mailboxes = new Map<string, AgentMessage[]>()
22
+
23
+ /**
24
+ * Read messages from a mailbox.
25
+ */
26
+ export function readMailbox(agentName: string): AgentMessage[] {
27
+ const messages = mailboxes.get(agentName) || []
28
+ mailboxes.set(agentName, []) // Clear after reading
29
+ return messages
30
+ }
31
+
32
+ /**
33
+ * Write to a mailbox.
34
+ */
35
+ export function writeToMailbox(agentName: string, message: AgentMessage): void {
36
+ const messages = mailboxes.get(agentName) || []
37
+ messages.push(message)
38
+ mailboxes.set(agentName, messages)
39
+ }
40
+
41
+ /**
42
+ * Clear all mailboxes.
43
+ */
44
+ export function clearMailboxes(): void {
45
+ mailboxes.clear()
46
+ }
47
+
48
+ export const SendMessageTool: ToolDefinition = {
49
+ name: 'SendMessage',
50
+ description: 'Send a message to another agent or teammate. Supports plain text and structured protocol messages.',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ to: { type: 'string', description: 'Recipient agent name or ID. Use "*" for broadcast.' },
55
+ content: { type: 'string', description: 'Message content' },
56
+ type: {
57
+ type: 'string',
58
+ enum: ['text', 'shutdown_request', 'shutdown_response', 'plan_approval_response'],
59
+ description: 'Message type (default: text)',
60
+ },
61
+ },
62
+ required: ['to', 'content'],
63
+ },
64
+ isReadOnly: () => false,
65
+ isConcurrencySafe: () => true,
66
+ isEnabled: () => true,
67
+ async prompt() { return 'Send a message to another agent.' },
68
+ async call(input: any): Promise<ToolResult> {
69
+ const message: AgentMessage = {
70
+ from: 'self',
71
+ to: input.to,
72
+ content: input.content,
73
+ timestamp: new Date().toISOString(),
74
+ type: input.type || 'text',
75
+ }
76
+
77
+ if (input.to === '*') {
78
+ // Broadcast to all known mailboxes
79
+ for (const [name] of mailboxes) {
80
+ writeToMailbox(name, { ...message, to: name })
81
+ }
82
+ return {
83
+ type: 'tool_result',
84
+ tool_use_id: '',
85
+ content: `Message broadcast to all agents`,
86
+ }
87
+ }
88
+
89
+ writeToMailbox(input.to, message)
90
+ return {
91
+ type: 'tool_result',
92
+ tool_use_id: '',
93
+ content: `Message sent to ${input.to}`,
94
+ }
95
+ },
96
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Task Management Tools
3
+ *
4
+ * TaskCreate, TaskList, TaskUpdate, TaskGet, TaskStop, TaskOutput
5
+ *
6
+ * Provides in-memory task tracking for agent coordination.
7
+ * Tasks persist across turns within a session.
8
+ */
9
+
10
+ import type { ToolDefinition, ToolContext, ToolResult } from '../types.js'
11
+
12
+ /**
13
+ * Task status.
14
+ */
15
+ export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled'
16
+
17
+ /**
18
+ * Task entry.
19
+ */
20
+ export interface Task {
21
+ id: string
22
+ subject: string
23
+ description?: string
24
+ status: TaskStatus
25
+ owner?: string
26
+ createdAt: string
27
+ updatedAt: string
28
+ output?: string
29
+ blockedBy?: string[]
30
+ blocks?: string[]
31
+ metadata?: Record<string, unknown>
32
+ }
33
+
34
+ /**
35
+ * Global task store (shared across tools in a session).
36
+ */
37
+ const taskStore = new Map<string, Task>()
38
+
39
+ let taskCounter = 0
40
+
41
+ /**
42
+ * Get all tasks.
43
+ */
44
+ export function getAllTasks(): Task[] {
45
+ return Array.from(taskStore.values())
46
+ }
47
+
48
+ /**
49
+ * Get a task by ID.
50
+ */
51
+ export function getTask(id: string): Task | undefined {
52
+ return taskStore.get(id)
53
+ }
54
+
55
+ /**
56
+ * Clear all tasks (for session reset).
57
+ */
58
+ export function clearTasks(): void {
59
+ taskStore.clear()
60
+ taskCounter = 0
61
+ }
62
+
63
+ // ============================================================================
64
+ // TaskCreateTool
65
+ // ============================================================================
66
+
67
+ export const TaskCreateTool: ToolDefinition = {
68
+ name: 'TaskCreate',
69
+ description: 'Create a new task for tracking work progress. Tasks help organize multi-step operations.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ subject: { type: 'string', description: 'Short task title' },
74
+ description: { type: 'string', description: 'Detailed task description' },
75
+ owner: { type: 'string', description: 'Task owner/assignee' },
76
+ status: { type: 'string', enum: ['pending', 'in_progress'], description: 'Initial status' },
77
+ },
78
+ required: ['subject'],
79
+ },
80
+ isReadOnly: () => false,
81
+ isConcurrencySafe: () => true,
82
+ isEnabled: () => true,
83
+ async prompt() { return 'Create a task for tracking progress.' },
84
+ async call(input: any): Promise<ToolResult> {
85
+ const id = `task_${++taskCounter}`
86
+ const task: Task = {
87
+ id,
88
+ subject: input.subject,
89
+ description: input.description,
90
+ status: input.status || 'pending',
91
+ owner: input.owner,
92
+ createdAt: new Date().toISOString(),
93
+ updatedAt: new Date().toISOString(),
94
+ }
95
+ taskStore.set(id, task)
96
+
97
+ return {
98
+ type: 'tool_result',
99
+ tool_use_id: '',
100
+ content: `Task created: ${id} - "${task.subject}" (${task.status})`,
101
+ }
102
+ },
103
+ }
104
+
105
+ // ============================================================================
106
+ // TaskListTool
107
+ // ============================================================================
108
+
109
+ export const TaskListTool: ToolDefinition = {
110
+ name: 'TaskList',
111
+ description: 'List all tasks with their status, ownership, and dependencies.',
112
+ inputSchema: {
113
+ type: 'object',
114
+ properties: {
115
+ status: { type: 'string', description: 'Filter by status' },
116
+ owner: { type: 'string', description: 'Filter by owner' },
117
+ },
118
+ },
119
+ isReadOnly: () => true,
120
+ isConcurrencySafe: () => true,
121
+ isEnabled: () => true,
122
+ async prompt() { return 'List tasks.' },
123
+ async call(input: any): Promise<ToolResult> {
124
+ let tasks = getAllTasks()
125
+
126
+ if (input.status) {
127
+ tasks = tasks.filter(t => t.status === input.status)
128
+ }
129
+ if (input.owner) {
130
+ tasks = tasks.filter(t => t.owner === input.owner)
131
+ }
132
+
133
+ if (tasks.length === 0) {
134
+ return { type: 'tool_result', tool_use_id: '', content: 'No tasks found.' }
135
+ }
136
+
137
+ const lines = tasks.map(t =>
138
+ `[${t.id}] ${t.status.toUpperCase()} - ${t.subject}${t.owner ? ` (owner: ${t.owner})` : ''}`
139
+ )
140
+
141
+ return {
142
+ type: 'tool_result',
143
+ tool_use_id: '',
144
+ content: lines.join('\n'),
145
+ }
146
+ },
147
+ }
148
+
149
+ // ============================================================================
150
+ // TaskUpdateTool
151
+ // ============================================================================
152
+
153
+ export const TaskUpdateTool: ToolDefinition = {
154
+ name: 'TaskUpdate',
155
+ description: 'Update a task\'s status, description, or other properties.',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ id: { type: 'string', description: 'Task ID' },
160
+ status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'failed', 'cancelled'] },
161
+ description: { type: 'string', description: 'Updated description' },
162
+ owner: { type: 'string', description: 'New owner' },
163
+ output: { type: 'string', description: 'Task output/result' },
164
+ },
165
+ required: ['id'],
166
+ },
167
+ isReadOnly: () => false,
168
+ isConcurrencySafe: () => true,
169
+ isEnabled: () => true,
170
+ async prompt() { return 'Update a task.' },
171
+ async call(input: any): Promise<ToolResult> {
172
+ const task = taskStore.get(input.id)
173
+ if (!task) {
174
+ return { type: 'tool_result', tool_use_id: '', content: `Task not found: ${input.id}`, is_error: true }
175
+ }
176
+
177
+ if (input.status) task.status = input.status
178
+ if (input.description) task.description = input.description
179
+ if (input.owner) task.owner = input.owner
180
+ if (input.output) task.output = input.output
181
+ task.updatedAt = new Date().toISOString()
182
+
183
+ return {
184
+ type: 'tool_result',
185
+ tool_use_id: '',
186
+ content: `Task updated: ${task.id} - ${task.status} - "${task.subject}"`,
187
+ }
188
+ },
189
+ }
190
+
191
+ // ============================================================================
192
+ // TaskGetTool
193
+ // ============================================================================
194
+
195
+ export const TaskGetTool: ToolDefinition = {
196
+ name: 'TaskGet',
197
+ description: 'Get full details of a specific task.',
198
+ inputSchema: {
199
+ type: 'object',
200
+ properties: {
201
+ id: { type: 'string', description: 'Task ID' },
202
+ },
203
+ required: ['id'],
204
+ },
205
+ isReadOnly: () => true,
206
+ isConcurrencySafe: () => true,
207
+ isEnabled: () => true,
208
+ async prompt() { return 'Get task details.' },
209
+ async call(input: any): Promise<ToolResult> {
210
+ const task = taskStore.get(input.id)
211
+ if (!task) {
212
+ return { type: 'tool_result', tool_use_id: '', content: `Task not found: ${input.id}`, is_error: true }
213
+ }
214
+
215
+ return {
216
+ type: 'tool_result',
217
+ tool_use_id: '',
218
+ content: JSON.stringify(task, null, 2),
219
+ }
220
+ },
221
+ }
222
+
223
+ // ============================================================================
224
+ // TaskStopTool
225
+ // ============================================================================
226
+
227
+ export const TaskStopTool: ToolDefinition = {
228
+ name: 'TaskStop',
229
+ description: 'Stop/cancel a running task.',
230
+ inputSchema: {
231
+ type: 'object',
232
+ properties: {
233
+ id: { type: 'string', description: 'Task ID to stop' },
234
+ reason: { type: 'string', description: 'Reason for stopping' },
235
+ },
236
+ required: ['id'],
237
+ },
238
+ isReadOnly: () => false,
239
+ isConcurrencySafe: () => true,
240
+ isEnabled: () => true,
241
+ async prompt() { return 'Stop a task.' },
242
+ async call(input: any): Promise<ToolResult> {
243
+ const task = taskStore.get(input.id)
244
+ if (!task) {
245
+ return { type: 'tool_result', tool_use_id: '', content: `Task not found: ${input.id}`, is_error: true }
246
+ }
247
+
248
+ task.status = 'cancelled'
249
+ task.updatedAt = new Date().toISOString()
250
+ if (input.reason) task.output = `Stopped: ${input.reason}`
251
+
252
+ return {
253
+ type: 'tool_result',
254
+ tool_use_id: '',
255
+ content: `Task stopped: ${task.id}`,
256
+ }
257
+ },
258
+ }
259
+
260
+ // ============================================================================
261
+ // TaskOutputTool
262
+ // ============================================================================
263
+
264
+ export const TaskOutputTool: ToolDefinition = {
265
+ name: 'TaskOutput',
266
+ description: 'Get the output/result of a task.',
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: {
270
+ id: { type: 'string', description: 'Task ID' },
271
+ },
272
+ required: ['id'],
273
+ },
274
+ isReadOnly: () => true,
275
+ isConcurrencySafe: () => true,
276
+ isEnabled: () => true,
277
+ async prompt() { return 'Get task output.' },
278
+ async call(input: any): Promise<ToolResult> {
279
+ const task = taskStore.get(input.id)
280
+ if (!task) {
281
+ return { type: 'tool_result', tool_use_id: '', content: `Task not found: ${input.id}`, is_error: true }
282
+ }
283
+
284
+ return {
285
+ type: 'tool_result',
286
+ tool_use_id: '',
287
+ content: task.output || '(no output yet)',
288
+ }
289
+ },
290
+ }