@exaudeus/workrail 3.71.1 → 3.72.1
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/dist/cli-worktrain.js +4 -6
- package/dist/console-ui/assets/{index-CsX-nVV7.js → index-Yj9NHqbR.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/active-sessions.d.ts +17 -0
- package/dist/daemon/active-sessions.js +55 -0
- package/dist/daemon/context-loader.d.ts +32 -0
- package/dist/daemon/context-loader.js +34 -0
- package/dist/daemon/session-scope.d.ts +28 -0
- package/dist/daemon/session-scope.js +21 -0
- package/dist/daemon/tools/_shared.d.ts +38 -0
- package/dist/daemon/tools/_shared.js +101 -0
- package/dist/daemon/tools/bash.d.ts +3 -0
- package/dist/daemon/tools/bash.js +57 -0
- package/dist/daemon/tools/continue-workflow.d.ts +6 -0
- package/dist/daemon/tools/continue-workflow.js +208 -0
- package/dist/daemon/tools/file-tools.d.ts +6 -0
- package/dist/daemon/tools/file-tools.js +195 -0
- package/dist/daemon/tools/glob-grep.d.ts +4 -0
- package/dist/daemon/tools/glob-grep.js +172 -0
- package/dist/daemon/tools/report-issue.d.ts +3 -0
- package/dist/daemon/tools/report-issue.js +129 -0
- package/dist/daemon/tools/signal-coordinator.d.ts +4 -0
- package/dist/daemon/tools/signal-coordinator.js +105 -0
- package/dist/daemon/tools/spawn-agent.d.ts +6 -0
- package/dist/daemon/tools/spawn-agent.js +135 -0
- package/dist/daemon/turn-end/conversation-flusher.d.ts +4 -0
- package/dist/daemon/turn-end/conversation-flusher.js +8 -0
- package/dist/daemon/turn-end/detect-stuck.d.ts +2 -0
- package/dist/daemon/turn-end/detect-stuck.js +5 -0
- package/dist/daemon/turn-end/step-injector.d.ts +8 -0
- package/dist/daemon/turn-end/step-injector.js +10 -0
- package/dist/daemon/workflow-runner.d.ts +54 -29
- package/dist/daemon/workflow-runner.js +175 -989
- package/dist/infrastructure/storage/workflow-resolution.js +5 -6
- package/dist/manifest.json +161 -25
- package/dist/mcp/handlers/shared/request-workflow-reader.js +14 -0
- package/dist/trigger/coordinator-deps.d.ts +15 -0
- package/dist/trigger/coordinator-deps.js +322 -0
- package/dist/trigger/delivery-pipeline.d.ts +18 -0
- package/dist/trigger/delivery-pipeline.js +148 -0
- package/dist/trigger/dispatch-deduplicator.d.ts +6 -0
- package/dist/trigger/dispatch-deduplicator.js +24 -0
- package/dist/trigger/trigger-listener.d.ts +2 -3
- package/dist/trigger/trigger-listener.js +9 -276
- package/dist/trigger/trigger-router.d.ts +8 -7
- package/dist/trigger/trigger-router.js +19 -97
- package/dist/v2/usecases/console-routes.js +10 -2
- package/docs/ideas/backlog.md +82 -48
- package/package.json +1 -1
- package/workflows/wr.research.json +158 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.makeReadTool = makeReadTool;
|
|
37
|
+
exports.makeWriteTool = makeWriteTool;
|
|
38
|
+
exports.makeEditTool = makeEditTool;
|
|
39
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const _shared_js_1 = require("./_shared.js");
|
|
42
|
+
function makeReadTool(readFileState, schemas, sessionId, emitter, workrailSessionId) {
|
|
43
|
+
return {
|
|
44
|
+
name: 'Read',
|
|
45
|
+
description: 'Read the contents of a file at the given absolute path. ' +
|
|
46
|
+
'Content is returned in cat -n format: each line is prefixed with its 1-indexed line number and a tab character (e.g. "1\\tline one\\n2\\tline two"). ' +
|
|
47
|
+
'Use offset (0-indexed start line) and limit (max lines) to read a slice of a large file.',
|
|
48
|
+
inputSchema: schemas['ReadParams'],
|
|
49
|
+
label: 'Read',
|
|
50
|
+
execute: async (_toolCallId, params) => {
|
|
51
|
+
if (typeof params.filePath !== 'string' || !params.filePath)
|
|
52
|
+
throw new Error('Read: filePath must be a non-empty string');
|
|
53
|
+
const filePath = params.filePath;
|
|
54
|
+
if (sessionId)
|
|
55
|
+
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Read', summary: filePath.slice(0, 80), ...(0, _shared_js_1.withWorkrailSession)(workrailSessionId) });
|
|
56
|
+
const devPaths = ['/dev/stdin', '/dev/tty', '/dev/zero', '/dev/random', '/dev/full', '/dev/urandom'];
|
|
57
|
+
if (devPaths.some(d => filePath === d)) {
|
|
58
|
+
throw new Error(`Refusing to read device path: ${filePath}`);
|
|
59
|
+
}
|
|
60
|
+
const stat = await fs.stat(filePath);
|
|
61
|
+
const offset = params.offset ?? 0;
|
|
62
|
+
const limit = params.limit;
|
|
63
|
+
const isPaginated = params.offset !== undefined || params.limit !== undefined;
|
|
64
|
+
if (!isPaginated && stat.size > _shared_js_1.READ_SIZE_CAP_BYTES) {
|
|
65
|
+
throw new Error(`File is too large to read at once (${stat.size} bytes, cap is ${_shared_js_1.READ_SIZE_CAP_BYTES} bytes). ` +
|
|
66
|
+
`Use offset and limit parameters to read a specific range of lines.`);
|
|
67
|
+
}
|
|
68
|
+
const rawContent = await fs.readFile(filePath, 'utf8');
|
|
69
|
+
const allLines = rawContent.split('\n');
|
|
70
|
+
const isPartialView = offset !== 0 || limit != null;
|
|
71
|
+
const slicedLines = limit != null ? allLines.slice(offset, offset + limit) : allLines.slice(offset);
|
|
72
|
+
const startLine = offset;
|
|
73
|
+
const formatted = slicedLines.map((l, i) => `${startLine + i + 1}\t${l}`).join('\n');
|
|
74
|
+
readFileState.set(filePath, { content: rawContent, timestamp: stat.mtimeMs, isPartialView });
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: 'text', text: formatted }],
|
|
77
|
+
details: { filePath, totalLines: allLines.length, returnedLines: slicedLines.length, offset, isPartialView },
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function makeWriteTool(readFileState, schemas, sessionId, emitter, workrailSessionId) {
|
|
83
|
+
return {
|
|
84
|
+
name: 'Write',
|
|
85
|
+
description: 'Write content to a file at the given absolute path. Creates parent directories if needed. ' +
|
|
86
|
+
'For existing files: the file must have been read in this session and must not have changed on disk since then. ' +
|
|
87
|
+
'For new files (path does not exist): no prior read is required.',
|
|
88
|
+
inputSchema: schemas['WriteParams'],
|
|
89
|
+
label: 'Write',
|
|
90
|
+
execute: async (_toolCallId, params) => {
|
|
91
|
+
if (typeof params.filePath !== 'string' || !params.filePath)
|
|
92
|
+
throw new Error('Write: filePath must be a non-empty string');
|
|
93
|
+
if (typeof params.content !== 'string')
|
|
94
|
+
throw new Error('Write: content must be a string');
|
|
95
|
+
const filePath = params.filePath;
|
|
96
|
+
if (sessionId)
|
|
97
|
+
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Write', summary: filePath.slice(0, 80), ...(0, _shared_js_1.withWorkrailSession)(workrailSessionId) });
|
|
98
|
+
let existsOnDisk = false;
|
|
99
|
+
try {
|
|
100
|
+
await fs.access(filePath);
|
|
101
|
+
existsOnDisk = true;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
}
|
|
105
|
+
if (existsOnDisk) {
|
|
106
|
+
const state = readFileState.get(filePath);
|
|
107
|
+
if (!state) {
|
|
108
|
+
throw new Error(`File has not been read in this session. Call Read first before writing to it: ${filePath}`);
|
|
109
|
+
}
|
|
110
|
+
const stat = await fs.stat(filePath);
|
|
111
|
+
if (stat.mtimeMs !== state.timestamp) {
|
|
112
|
+
throw new Error(`File has been modified since it was read. Re-read before writing: ${filePath}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
116
|
+
await fs.writeFile(filePath, params.content, 'utf8');
|
|
117
|
+
const newStat = await fs.stat(filePath);
|
|
118
|
+
readFileState.set(filePath, { content: params.content, timestamp: newStat.mtimeMs, isPartialView: false });
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: 'text', text: `Written ${params.content.length} bytes to ${filePath}` }],
|
|
121
|
+
details: { filePath, length: params.content.length },
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function makeEditTool(workspacePath, readFileState, schemas, sessionId, emitter, workrailSessionId) {
|
|
127
|
+
return {
|
|
128
|
+
name: 'Edit',
|
|
129
|
+
description: 'Perform an exact string replacement in a file. ' +
|
|
130
|
+
'The file must have been read in this session via the Read tool. ' +
|
|
131
|
+
'By default, old_string must appear exactly once; use replace_all=true to replace all occurrences. ' +
|
|
132
|
+
'Do NOT include line-number prefixes (e.g. "1\\t") from Read output in old_string or new_string.',
|
|
133
|
+
inputSchema: schemas['EditParams'],
|
|
134
|
+
label: 'Edit',
|
|
135
|
+
execute: async (_toolCallId, params) => {
|
|
136
|
+
if (typeof params.file_path !== 'string' || !params.file_path)
|
|
137
|
+
throw new Error('Edit: file_path must be a non-empty string');
|
|
138
|
+
if (typeof params.old_string !== 'string')
|
|
139
|
+
throw new Error('Edit: old_string must be a string');
|
|
140
|
+
if (typeof params.new_string !== 'string')
|
|
141
|
+
throw new Error('Edit: new_string must be a string');
|
|
142
|
+
const rawFilePath = params.file_path;
|
|
143
|
+
const absoluteFilePath = path.isAbsolute(rawFilePath)
|
|
144
|
+
? rawFilePath
|
|
145
|
+
: path.join(workspacePath, rawFilePath);
|
|
146
|
+
if (!absoluteFilePath.startsWith(workspacePath)) {
|
|
147
|
+
throw new Error(`Edit target is outside the workspace: ${rawFilePath}`);
|
|
148
|
+
}
|
|
149
|
+
const filePath = absoluteFilePath;
|
|
150
|
+
const oldString = params.old_string;
|
|
151
|
+
const newString = params.new_string;
|
|
152
|
+
const replaceAll = params.replace_all ?? false;
|
|
153
|
+
if (sessionId)
|
|
154
|
+
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Edit', summary: filePath.slice(0, 80), ...(0, _shared_js_1.withWorkrailSession)(workrailSessionId) });
|
|
155
|
+
if (oldString === newString) {
|
|
156
|
+
throw new Error('old_string and new_string are identical. No edit needed.');
|
|
157
|
+
}
|
|
158
|
+
const state = readFileState.get(filePath);
|
|
159
|
+
if (!state) {
|
|
160
|
+
throw new Error(`File has not been read in this session. Call Read first before editing: ${filePath}`);
|
|
161
|
+
}
|
|
162
|
+
let stat;
|
|
163
|
+
try {
|
|
164
|
+
stat = await fs.stat(filePath);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
throw new Error(`File not found: ${filePath}. It may have been deleted after it was read.`);
|
|
168
|
+
}
|
|
169
|
+
if (stat.mtimeMs !== state.timestamp) {
|
|
170
|
+
throw new Error(`File has been modified since it was read. Re-read before editing: ${filePath}`);
|
|
171
|
+
}
|
|
172
|
+
const currentContent = await fs.readFile(filePath, 'utf8');
|
|
173
|
+
const actualString = (0, _shared_js_1.findActualString)(currentContent, oldString);
|
|
174
|
+
if (actualString === null) {
|
|
175
|
+
throw new Error(`String to replace not found in file. Make sure old_string exactly matches the file content ` +
|
|
176
|
+
`(do not include line-number prefixes from Read output): ${filePath}`);
|
|
177
|
+
}
|
|
178
|
+
const occurrences = currentContent.split(actualString).length - 1;
|
|
179
|
+
if (!replaceAll && occurrences > 1) {
|
|
180
|
+
throw new Error(`old_string appears ${occurrences} times in the file. ` +
|
|
181
|
+
`Provide a more specific string that matches exactly once, or set replace_all=true to replace all occurrences.`);
|
|
182
|
+
}
|
|
183
|
+
const updatedContent = replaceAll
|
|
184
|
+
? currentContent.split(actualString).join(newString)
|
|
185
|
+
: currentContent.replace(actualString, newString);
|
|
186
|
+
await fs.writeFile(filePath, updatedContent, 'utf8');
|
|
187
|
+
const newStat = await fs.stat(filePath);
|
|
188
|
+
readFileState.set(filePath, { content: updatedContent, timestamp: newStat.mtimeMs, isPartialView: false });
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: 'text', text: `The file ${filePath} has been updated successfully.` }],
|
|
191
|
+
details: { filePath, occurrencesReplaced: occurrences },
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentTool } from '../agent-loop.js';
|
|
2
|
+
import type { DaemonEventEmitter } from '../daemon-events.js';
|
|
3
|
+
export declare function makeGlobTool(workspacePath: string, schemas: Record<string, any>, sessionId?: string, emitter?: DaemonEventEmitter, workrailSessionId?: string | null): AgentTool;
|
|
4
|
+
export declare function makeGrepTool(workspacePath: string, schemas: Record<string, any>, sessionId?: string, emitter?: DaemonEventEmitter, workrailSessionId?: string | null): AgentTool;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.makeGlobTool = makeGlobTool;
|
|
37
|
+
exports.makeGrepTool = makeGrepTool;
|
|
38
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const node_child_process_1 = require("node:child_process");
|
|
41
|
+
const node_util_1 = require("node:util");
|
|
42
|
+
const tinyglobby_1 = require("tinyglobby");
|
|
43
|
+
const _shared_js_1 = require("./_shared.js");
|
|
44
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
45
|
+
function makeGlobTool(workspacePath, schemas, sessionId, emitter, workrailSessionId) {
|
|
46
|
+
return {
|
|
47
|
+
name: 'Glob',
|
|
48
|
+
description: 'Find files matching a glob pattern. Returns newline-separated relative file paths, sorted by modification time descending. ' +
|
|
49
|
+
'node_modules, .git, dist, and build directories are always excluded. ' +
|
|
50
|
+
'Results are capped at 100 files.',
|
|
51
|
+
inputSchema: schemas['GlobParams'],
|
|
52
|
+
label: 'Glob',
|
|
53
|
+
execute: async (_toolCallId, params) => {
|
|
54
|
+
if (typeof params.pattern !== 'string' || !params.pattern)
|
|
55
|
+
throw new Error('Glob: pattern must be a non-empty string');
|
|
56
|
+
const pattern = params.pattern;
|
|
57
|
+
const searchRoot = params.path ?? workspacePath;
|
|
58
|
+
if (sessionId)
|
|
59
|
+
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Glob', summary: pattern.slice(0, 80), ...(0, _shared_js_1.withWorkrailSession)(workrailSessionId) });
|
|
60
|
+
const GLOB_LIMIT = 100;
|
|
61
|
+
let paths;
|
|
62
|
+
try {
|
|
63
|
+
paths = await (0, tinyglobby_1.glob)(pattern, {
|
|
64
|
+
cwd: searchRoot,
|
|
65
|
+
ignore: _shared_js_1.GLOB_ALWAYS_EXCLUDE,
|
|
66
|
+
absolute: false,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
paths = [];
|
|
71
|
+
}
|
|
72
|
+
const withMtimes = await Promise.all(paths.map(async (p) => {
|
|
73
|
+
try {
|
|
74
|
+
const stat = await fs.stat(path.join(searchRoot, p));
|
|
75
|
+
return { p, mtime: stat.mtimeMs };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return { p, mtime: 0 };
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
withMtimes.sort((a, b) => b.mtime - a.mtime);
|
|
82
|
+
const sorted = withMtimes.map(x => x.p);
|
|
83
|
+
const truncated = sorted.length > GLOB_LIMIT;
|
|
84
|
+
const result = sorted.slice(0, GLOB_LIMIT);
|
|
85
|
+
let text = result.join('\n');
|
|
86
|
+
if (truncated) {
|
|
87
|
+
text += '\n[Results truncated at 100 files]';
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: 'text', text: text || '(no matches)' }],
|
|
91
|
+
details: { pattern, searchRoot, matchCount: sorted.length, truncated },
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function makeGrepTool(workspacePath, schemas, sessionId, emitter, workrailSessionId) {
|
|
97
|
+
return {
|
|
98
|
+
name: 'Grep',
|
|
99
|
+
description: 'Search file contents using ripgrep (rg). Fast regex search with optional context lines, file-type filtering, and case-insensitive mode. ' +
|
|
100
|
+
'output_mode: "files_with_matches" (default) returns only file paths; "content" returns matching lines; "count" returns match counts per file. ' +
|
|
101
|
+
'node_modules and .git are always excluded.',
|
|
102
|
+
inputSchema: schemas['GrepParams'],
|
|
103
|
+
label: 'Grep',
|
|
104
|
+
execute: async (_toolCallId, params) => {
|
|
105
|
+
if (typeof params.pattern !== 'string' || !params.pattern)
|
|
106
|
+
throw new Error('Grep: pattern must be a non-empty string');
|
|
107
|
+
const pattern = params.pattern;
|
|
108
|
+
const searchPath = params.path ?? workspacePath;
|
|
109
|
+
const outputMode = params.output_mode ?? 'files_with_matches';
|
|
110
|
+
const headLimit = params.head_limit ?? 250;
|
|
111
|
+
if (sessionId)
|
|
112
|
+
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Grep', summary: pattern.slice(0, 80), ...(0, _shared_js_1.withWorkrailSession)(workrailSessionId) });
|
|
113
|
+
const args = [
|
|
114
|
+
'--hidden',
|
|
115
|
+
'--glob', '!node_modules',
|
|
116
|
+
'--glob', '!.git',
|
|
117
|
+
'--max-columns', '500',
|
|
118
|
+
];
|
|
119
|
+
if (params['-i'])
|
|
120
|
+
args.push('-i');
|
|
121
|
+
if (params.glob) {
|
|
122
|
+
args.push('--glob', params.glob);
|
|
123
|
+
}
|
|
124
|
+
if (params.type) {
|
|
125
|
+
args.push('--type', params.type);
|
|
126
|
+
}
|
|
127
|
+
switch (outputMode) {
|
|
128
|
+
case 'files_with_matches':
|
|
129
|
+
args.push('--files-with-matches');
|
|
130
|
+
break;
|
|
131
|
+
case 'count':
|
|
132
|
+
args.push('--count');
|
|
133
|
+
break;
|
|
134
|
+
case 'content':
|
|
135
|
+
args.push('--vimgrep');
|
|
136
|
+
if (params.context != null) {
|
|
137
|
+
args.push('-C', String(params.context));
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
args.push('--', pattern, searchPath);
|
|
142
|
+
let stdout;
|
|
143
|
+
try {
|
|
144
|
+
const result = await execFileAsync('rg', args, { cwd: workspacePath, maxBuffer: 10 * 1024 * 1024 });
|
|
145
|
+
stdout = result.stdout;
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
const nodeErr = err;
|
|
149
|
+
if (nodeErr.code === 'ENOENT') {
|
|
150
|
+
throw new Error('ripgrep (rg) is not installed. Install it with: brew install ripgrep (macOS) or apt install ripgrep (Ubuntu/Debian).');
|
|
151
|
+
}
|
|
152
|
+
if (typeof nodeErr.code === 'number' && nodeErr.code === 1) {
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: 'text', text: '(no matches)' }],
|
|
155
|
+
details: { pattern, searchPath, outputMode },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`rg failed: ${nodeErr.message ?? String(err)}`);
|
|
159
|
+
}
|
|
160
|
+
const lines = stdout.split('\n').filter(l => l.length > 0);
|
|
161
|
+
const truncated = lines.length > headLimit;
|
|
162
|
+
let result = lines.slice(0, headLimit).join('\n');
|
|
163
|
+
if (truncated) {
|
|
164
|
+
result += `\n[Results truncated at ${headLimit} lines. Use a more specific pattern or increase head_limit.]`;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: 'text', text: result || '(no matches)' }],
|
|
168
|
+
details: { pattern, searchPath, outputMode, lineCount: lines.length, truncated },
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AgentTool } from '../agent-loop.js';
|
|
2
|
+
import type { DaemonEventEmitter } from '../daemon-events.js';
|
|
3
|
+
export declare function makeReportIssueTool(sessionId: string, emitter?: DaemonEventEmitter, workrailSessionId?: string | null, issuesDirOverride?: string, onIssueSummary?: (summary: string) => void): AgentTool;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.makeReportIssueTool = makeReportIssueTool;
|
|
37
|
+
const os = __importStar(require("node:os"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const _shared_js_1 = require("./_shared.js");
|
|
40
|
+
function makeReportIssueTool(sessionId, emitter, workrailSessionId, issuesDirOverride, onIssueSummary) {
|
|
41
|
+
const issuesDir = issuesDirOverride ?? path.join(os.homedir(), '.workrail', 'issues');
|
|
42
|
+
return {
|
|
43
|
+
name: 'report_issue',
|
|
44
|
+
description: "Record a structured issue, error, or unexpected behavior. Call this AND continue_workflow (unless fatal). " +
|
|
45
|
+
"Does not stop the session -- it creates a record for the auto-fix coordinator.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
kind: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
enum: ['tool_failure', 'blocked', 'unexpected_behavior', 'needs_human', 'self_correction'],
|
|
52
|
+
description: 'Category of issue being reported.',
|
|
53
|
+
},
|
|
54
|
+
severity: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
enum: ['info', 'warn', 'error', 'fatal'],
|
|
57
|
+
description: 'Severity level. Fatal means the session cannot continue productively.',
|
|
58
|
+
},
|
|
59
|
+
summary: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'One-line summary of the issue. Max 200 chars.',
|
|
62
|
+
maxLength: 200,
|
|
63
|
+
},
|
|
64
|
+
context: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'What you were trying to do when this issue occurred.',
|
|
67
|
+
},
|
|
68
|
+
toolName: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Name of the tool that failed or behaved unexpectedly, if applicable.',
|
|
71
|
+
},
|
|
72
|
+
command: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'The shell command or expression that caused the issue, if applicable.',
|
|
75
|
+
},
|
|
76
|
+
suggestedFix: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'A suggested fix or recovery action for the auto-fix coordinator.',
|
|
79
|
+
},
|
|
80
|
+
continueToken: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'The current continueToken, so the coordinator can resume this session.',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ['kind', 'severity', 'summary'],
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
},
|
|
88
|
+
label: 'report_issue',
|
|
89
|
+
execute: async (_toolCallId, params) => {
|
|
90
|
+
if (typeof params.kind !== 'string' || !params.kind)
|
|
91
|
+
throw new Error('report_issue: kind must be a non-empty string');
|
|
92
|
+
if (typeof params.severity !== 'string' || !params.severity)
|
|
93
|
+
throw new Error('report_issue: severity must be a non-empty string');
|
|
94
|
+
if (typeof params.summary !== 'string' || !params.summary)
|
|
95
|
+
throw new Error('report_issue: summary must be a non-empty string');
|
|
96
|
+
const record = {
|
|
97
|
+
sessionId,
|
|
98
|
+
kind: params.kind,
|
|
99
|
+
severity: params.severity,
|
|
100
|
+
summary: String(params.summary ?? '').slice(0, 200),
|
|
101
|
+
...(params.context !== undefined && { context: String(params.context) }),
|
|
102
|
+
...(params.toolName !== undefined && { toolName: String(params.toolName) }),
|
|
103
|
+
...(params.command !== undefined && { command: String(params.command) }),
|
|
104
|
+
...(params.suggestedFix !== undefined && { suggestedFix: String(params.suggestedFix) }),
|
|
105
|
+
...(params.continueToken !== undefined && { continueToken: String(params.continueToken) }),
|
|
106
|
+
};
|
|
107
|
+
void (0, _shared_js_1.appendIssueAsync)(issuesDir, sessionId, record).catch(() => {
|
|
108
|
+
});
|
|
109
|
+
emitter?.emit({
|
|
110
|
+
kind: 'issue_reported',
|
|
111
|
+
sessionId,
|
|
112
|
+
issueKind: record.kind,
|
|
113
|
+
severity: record.severity,
|
|
114
|
+
summary: record.summary,
|
|
115
|
+
...(record.continueToken !== undefined && { continueToken: record.continueToken }),
|
|
116
|
+
...(workrailSessionId != null ? { workrailSessionId } : {}),
|
|
117
|
+
});
|
|
118
|
+
onIssueSummary?.(record.summary);
|
|
119
|
+
const isFatal = record.severity === 'fatal';
|
|
120
|
+
const message = isFatal
|
|
121
|
+
? `FATAL issue recorded. Call continue_workflow with notes explaining the blocker, then the session will end.`
|
|
122
|
+
: `Issue recorded (severity=${record.severity}). Continue with your work unless this is fatal.`;
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: 'text', text: message }],
|
|
125
|
+
details: { sessionId, kind: record.kind, severity: record.severity },
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentTool } from '../agent-loop.js';
|
|
2
|
+
import type { DaemonEventEmitter } from '../daemon-events.js';
|
|
3
|
+
export declare const DAEMON_SIGNALS_DIR: string;
|
|
4
|
+
export declare function makeSignalCoordinatorTool(sessionId: string, emitter?: DaemonEventEmitter, workrailSessionId?: string | null, signalsDirOverride?: string): AgentTool;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DAEMON_SIGNALS_DIR = void 0;
|
|
37
|
+
exports.makeSignalCoordinatorTool = makeSignalCoordinatorTool;
|
|
38
|
+
const os = __importStar(require("node:os"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const node_crypto_1 = require("node:crypto");
|
|
41
|
+
const _shared_js_1 = require("./_shared.js");
|
|
42
|
+
exports.DAEMON_SIGNALS_DIR = path.join(os.homedir(), '.workrail', 'signals');
|
|
43
|
+
function makeSignalCoordinatorTool(sessionId, emitter, workrailSessionId, signalsDirOverride) {
|
|
44
|
+
const signalsDir = signalsDirOverride ?? exports.DAEMON_SIGNALS_DIR;
|
|
45
|
+
return {
|
|
46
|
+
name: 'signal_coordinator',
|
|
47
|
+
description: 'Emit a structured mid-session signal to the coordinator WITHOUT advancing the workflow step. ' +
|
|
48
|
+
'Use this to surface progress updates, intermediate findings, data requests, ' +
|
|
49
|
+
'approval requests, or blocking conditions while the session continues. ' +
|
|
50
|
+
'Always returns immediately -- fire-and-observe, never blocks. ' +
|
|
51
|
+
'Signal kinds: "progress" (heartbeat, no data needed), "finding" (intermediate result), ' +
|
|
52
|
+
'"data_needed" (request external data), "approval_needed" (request coordinator approval), ' +
|
|
53
|
+
'"blocked" (cannot continue without coordinator intervention).',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
signalKind: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
enum: ['progress', 'finding', 'data_needed', 'approval_needed', 'blocked'],
|
|
60
|
+
description: 'The kind of signal to emit.',
|
|
61
|
+
},
|
|
62
|
+
payload: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
additionalProperties: true,
|
|
65
|
+
description: 'Structured data accompanying the signal. Pass {} for progress signals.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: ['signalKind', 'payload'],
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
},
|
|
71
|
+
label: 'signal_coordinator',
|
|
72
|
+
execute: async (_toolCallId, params) => {
|
|
73
|
+
if (typeof params.signalKind !== 'string' || !params.signalKind)
|
|
74
|
+
throw new Error('signal_coordinator: signalKind must be a non-empty string');
|
|
75
|
+
const signalId = 'sig_' + (0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 8);
|
|
76
|
+
const signalKind = String(params.signalKind ?? 'progress');
|
|
77
|
+
const payload = (typeof params.payload === 'object' && params.payload !== null && !Array.isArray(params.payload))
|
|
78
|
+
? params.payload
|
|
79
|
+
: {};
|
|
80
|
+
console.log(`[WorkflowRunner] Tool: signal_coordinator sessionId=${sessionId} signalKind=${signalKind} signalId=${signalId}`);
|
|
81
|
+
const record = {
|
|
82
|
+
signalId,
|
|
83
|
+
sessionId,
|
|
84
|
+
...(workrailSessionId != null ? { workrailSessionId } : {}),
|
|
85
|
+
signalKind,
|
|
86
|
+
payload,
|
|
87
|
+
};
|
|
88
|
+
void (0, _shared_js_1.appendSignalAsync)(signalsDir, sessionId, record).catch(() => {
|
|
89
|
+
});
|
|
90
|
+
emitter?.emit({
|
|
91
|
+
kind: 'signal_emitted',
|
|
92
|
+
sessionId,
|
|
93
|
+
signalKind,
|
|
94
|
+
signalId,
|
|
95
|
+
payload,
|
|
96
|
+
...(workrailSessionId != null ? { workrailSessionId } : {}),
|
|
97
|
+
});
|
|
98
|
+
const result = { status: 'recorded', signalId };
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
101
|
+
details: result,
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentTool } from '../agent-loop.js';
|
|
2
|
+
import type { V2ToolContext } from '../../mcp/types.js';
|
|
3
|
+
import type { DaemonEventEmitter } from '../daemon-events.js';
|
|
4
|
+
import type { runWorkflow } from '../workflow-runner.js';
|
|
5
|
+
import type { ActiveSessionSet } from '../active-sessions.js';
|
|
6
|
+
export declare function makeSpawnAgentTool(sessionId: string, ctx: V2ToolContext, apiKey: string, thisWorkrailSessionId: string, currentDepth: number, maxDepth: number, runWorkflowFn: typeof runWorkflow, schemas: Record<string, any>, emitter?: DaemonEventEmitter, activeSessionSet?: ActiveSessionSet): AgentTool;
|