oxe-cc 1.5.1 → 1.7.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/AGENTS.md +1 -1
- package/CHANGELOG.md +45 -0
- package/README.md +19 -15
- package/bin/lib/oxe-agent-install.cjs +125 -24
- package/bin/lib/oxe-dashboard.cjs +21 -5
- package/bin/lib/oxe-project-health.cjs +120 -42
- package/bin/lib/oxe-release.cjs +77 -4
- package/bin/oxe-cc.js +155 -78
- package/commands/oxe/debug.md +6 -1
- package/commands/oxe/discuss.md +7 -2
- package/commands/oxe/execute.md +7 -2
- package/commands/oxe/plan-agent.md +7 -2
- package/commands/oxe/plan.md +7 -2
- package/commands/oxe/scan.md +6 -1
- package/commands/oxe/spec.md +6 -1
- package/commands/oxe/verify.md +6 -1
- package/docs/CONTENT-MIGRATION-AUDIT.md +49 -0
- package/docs/RELEASE-READINESS.md +8 -0
- package/docs/RUNTIME-SMOKE-MATRIX.md +9 -2
- package/lib/runtime/compiler/graph-compiler.js +32 -0
- package/lib/runtime/context/context-pack-builder.d.ts +15 -0
- package/lib/runtime/context/context-pack-builder.js +78 -0
- package/lib/runtime/events/catalog.d.ts +1 -1
- package/lib/runtime/events/catalog.js +5 -0
- package/lib/runtime/executor/action-tool-map.d.ts +3 -0
- package/lib/runtime/executor/action-tool-map.js +41 -0
- package/lib/runtime/executor/built-in-tools.d.ts +8 -0
- package/lib/runtime/executor/built-in-tools.js +267 -0
- package/lib/runtime/executor/index.d.ts +6 -0
- package/lib/runtime/executor/index.js +12 -0
- package/lib/runtime/executor/llm-task-executor.d.ts +29 -0
- package/lib/runtime/executor/llm-task-executor.js +138 -0
- package/lib/runtime/executor/node-prompt-builder.d.ts +3 -0
- package/lib/runtime/executor/node-prompt-builder.js +36 -0
- package/lib/runtime/executor/stream-completion.d.ts +38 -0
- package/lib/runtime/executor/stream-completion.js +105 -0
- package/lib/runtime/index.d.ts +1 -0
- package/lib/runtime/index.js +2 -0
- package/lib/runtime/models/failure.d.ts +5 -0
- package/lib/runtime/models/failure.js +2 -0
- package/lib/runtime/plugins/capability-adapter.d.ts +9 -0
- package/lib/runtime/plugins/capability-adapter.js +111 -8
- package/lib/runtime/plugins/plugin-abi.d.ts +8 -0
- package/lib/runtime/plugins/plugin-registry.d.ts +2 -1
- package/lib/runtime/plugins/plugin-registry.js +6 -1
- package/lib/runtime/reducers/run-state-reducer.js +39 -2
- package/lib/runtime/scheduler/scheduler.d.ts +14 -2
- package/lib/runtime/scheduler/scheduler.js +131 -11
- package/lib/runtime/verification/verification-manifest.d.ts +5 -2
- package/lib/sdk/index.cjs +10 -5
- package/lib/sdk/index.d.ts +21 -10
- package/oxe/agents/oxe-assumptions-analyzer.md +136 -0
- package/oxe/agents/oxe-codebase-mapper.md +142 -0
- package/oxe/agents/oxe-debugger.md +145 -0
- package/oxe/agents/oxe-executor.md +139 -0
- package/oxe/agents/oxe-integration-checker.md +142 -0
- package/oxe/agents/oxe-plan-checker.md +143 -0
- package/oxe/agents/oxe-planner.md +151 -0
- package/oxe/agents/oxe-research-synthesizer.md +146 -0
- package/oxe/agents/oxe-researcher.md +163 -0
- package/oxe/agents/oxe-ui-auditor.md +151 -0
- package/oxe/agents/oxe-ui-checker.md +157 -0
- package/oxe/agents/oxe-ui-researcher.md +179 -0
- package/oxe/agents/oxe-validation-auditor.md +154 -0
- package/oxe/agents/oxe-verifier.md +132 -0
- package/oxe/personas/README.md +91 -39
- package/oxe/personas/architect.md +149 -37
- package/oxe/personas/db-specialist.md +149 -36
- package/oxe/personas/debugger.md +155 -38
- package/oxe/personas/executor.md +164 -38
- package/oxe/personas/planner.md +165 -36
- package/oxe/personas/researcher.md +148 -35
- package/oxe/personas/ui-specialist.md +164 -36
- package/oxe/personas/verifier.md +174 -39
- package/oxe/templates/CONFIG.md +3 -3
- package/oxe/templates/EXECUTION-RUNTIME.template.md +1 -1
- package/oxe/templates/FIXTURE-PACK.template.json +29 -22
- package/oxe/templates/FIXTURE-PACK.template.md +20 -11
- package/oxe/templates/IMPLEMENTATION-PACK.template.json +55 -39
- package/oxe/templates/IMPLEMENTATION-PACK.template.md +28 -16
- package/oxe/templates/INVESTIGATION.template.md +38 -38
- package/oxe/templates/PLAN.template.md +63 -32
- package/oxe/templates/REFERENCE-ANCHORS.template.md +18 -14
- package/oxe/templates/RESEARCH.template.md +11 -11
- package/oxe/templates/SPEC.template.md +6 -6
- package/oxe/templates/SUMMARY.template.md +33 -3
- package/oxe/templates/config.template.json +1 -1
- package/oxe/workflows/debug.md +9 -7
- package/oxe/workflows/execute.md +31 -28
- package/oxe/workflows/forensics.md +5 -3
- package/oxe/workflows/milestone.md +12 -12
- package/oxe/workflows/next.md +1 -1
- package/oxe/workflows/plan.md +409 -132
- package/oxe/workflows/references/adaptive-discovery.md +27 -27
- package/oxe/workflows/references/flow-robustness-contract.md +80 -80
- package/oxe/workflows/references/session-path-resolution.md +71 -71
- package/oxe/workflows/references/workflow-runtime-contracts.json +127 -127
- package/oxe/workflows/scan.md +355 -69
- package/oxe/workflows/spec.md +302 -9
- package/oxe/workflows/ui-review.md +5 -4
- package/oxe/workflows/ui-spec.md +4 -3
- package/oxe/workflows/verify.md +12 -9
- package/oxe/workflows/workstream.md +16 -16
- package/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/runtime/src/compiler/graph-compiler.ts +40 -0
- package/packages/runtime/src/context/context-pack-builder.ts +80 -0
- package/packages/runtime/src/events/catalog.ts +5 -0
- package/packages/runtime/src/executor/action-tool-map.ts +46 -0
- package/packages/runtime/src/executor/built-in-tools.ts +276 -0
- package/packages/runtime/src/executor/index.ts +6 -0
- package/packages/runtime/src/executor/llm-task-executor.ts +194 -0
- package/packages/runtime/src/executor/node-prompt-builder.ts +45 -0
- package/packages/runtime/src/executor/stream-completion.ts +145 -0
- package/packages/runtime/src/index.ts +3 -0
- package/packages/runtime/src/models/failure.ts +11 -0
- package/packages/runtime/src/plugins/capability-adapter.ts +117 -10
- package/packages/runtime/src/plugins/plugin-abi.ts +9 -0
- package/packages/runtime/src/plugins/plugin-registry.ts +10 -1
- package/packages/runtime/src/reducers/run-state-reducer.ts +59 -2
- package/packages/runtime/src/scheduler/scheduler.ts +152 -14
- package/packages/runtime/src/verification/verification-manifest.ts +12 -8
- package/vscode-extension/oxe-agents-1.6.0.vsix +0 -0
- package/vscode-extension/oxe-agents-1.7.0.vsix +0 -0
- package/vscode-extension/package.json +1 -1
|
@@ -237,6 +237,86 @@ export class ContextPackBuilder {
|
|
|
237
237
|
return this.build(workItem, state, [], new Map(), lessons);
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Remove artifacts with lowest relevance until the pack fits within targetTokens.
|
|
242
|
+
* Artifacts already sorted by relevance; we trim the tail.
|
|
243
|
+
*/
|
|
244
|
+
compact(pack: ContextPack, targetTokens: number): ContextPack {
|
|
245
|
+
if (estimateTokens(pack.artifacts.map((a) => a.content).join('\n')) <= targetTokens) {
|
|
246
|
+
return pack;
|
|
247
|
+
}
|
|
248
|
+
const sorted = [...pack.artifacts].sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
249
|
+
const trimmed: ContextArtifact[] = [];
|
|
250
|
+
let used = 0;
|
|
251
|
+
for (const artifact of sorted) {
|
|
252
|
+
const t = estimateTokens(artifact.content);
|
|
253
|
+
if (used + t > targetTokens) break;
|
|
254
|
+
trimmed.push(artifact);
|
|
255
|
+
used += t;
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
...pack,
|
|
259
|
+
artifacts: trimmed,
|
|
260
|
+
redundancy_removed: pack.redundancy_removed + (pack.artifacts.length - trimmed.length),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Merge groups of similar artifacts (cosine similarity >= threshold) into single
|
|
266
|
+
* combined artifacts to reduce redundancy without discarding information entirely.
|
|
267
|
+
*/
|
|
268
|
+
microCompact(artifacts: ContextArtifact[], similarityThreshold = 0.7): ContextArtifact[] {
|
|
269
|
+
const merged: ContextArtifact[] = [];
|
|
270
|
+
const used = new Set<number>();
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < artifacts.length; i++) {
|
|
273
|
+
if (used.has(i)) continue;
|
|
274
|
+
const group: ContextArtifact[] = [artifacts[i]];
|
|
275
|
+
for (let j = i + 1; j < artifacts.length; j++) {
|
|
276
|
+
if (!used.has(j) && cosineSimilarity(artifacts[i], artifacts[j]) >= similarityThreshold) {
|
|
277
|
+
group.push(artifacts[j]);
|
|
278
|
+
used.add(j);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
used.add(i);
|
|
282
|
+
if (group.length === 1) {
|
|
283
|
+
merged.push(group[0]);
|
|
284
|
+
} else {
|
|
285
|
+
const combinedContent = group
|
|
286
|
+
.map((a) => a.content)
|
|
287
|
+
.join('\n---\n')
|
|
288
|
+
.slice(0, 4000);
|
|
289
|
+
merged.push({
|
|
290
|
+
id: group[0].id,
|
|
291
|
+
kind: group[0].kind,
|
|
292
|
+
content: combinedContent,
|
|
293
|
+
relevanceScore: group.reduce((s, a) => s + a.relevanceScore, 0) / group.length,
|
|
294
|
+
tags: [...new Set(group.flatMap((a) => a.tags))],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return merged;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Automatically compact a pack to fit within hardLimitTokens.
|
|
303
|
+
* First applies microCompact (lossless merging), then compact (trimming by relevance).
|
|
304
|
+
*/
|
|
305
|
+
autoCompact(pack: ContextPack, hardLimitTokens: number): ContextPack {
|
|
306
|
+
const currentTokens = estimateTokens(pack.artifacts.map((a) => a.content).join('\n'));
|
|
307
|
+
if (currentTokens <= hardLimitTokens) return pack;
|
|
308
|
+
|
|
309
|
+
const microCompacted = this.microCompact(pack.artifacts);
|
|
310
|
+
const removedByMicro = pack.artifacts.length - microCompacted.length;
|
|
311
|
+
|
|
312
|
+
const interim: ContextPack = {
|
|
313
|
+
...pack,
|
|
314
|
+
artifacts: microCompacted,
|
|
315
|
+
redundancy_removed: pack.redundancy_removed + removedByMicro,
|
|
316
|
+
};
|
|
317
|
+
return this.compact(interim, hardLimitTokens);
|
|
318
|
+
}
|
|
319
|
+
|
|
240
320
|
/**
|
|
241
321
|
* Filter artifacts to those whose path-like tags are within mutation_scope.
|
|
242
322
|
* L0/L1 tiers apply the filter; L2/L3 skip it (full access).
|
|
@@ -20,6 +20,11 @@ export const EVENT_TYPES = [
|
|
|
20
20
|
'RunCompleted',
|
|
21
21
|
'RetroPublished',
|
|
22
22
|
'LessonPromoted',
|
|
23
|
+
'RunAborted',
|
|
24
|
+
'RollbackExecuted',
|
|
25
|
+
'RollbackFailed',
|
|
26
|
+
'TaskErrorBoundaryTripped',
|
|
27
|
+
'WorkspaceDisposeFailed',
|
|
23
28
|
] as const;
|
|
24
29
|
|
|
25
30
|
export type EventType = (typeof EVENT_TYPES)[number];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Action } from '../compiler/graph-compiler';
|
|
2
|
+
import { ALL_BUILT_IN_SCHEMAS, BUILT_IN_TOOLS } from './built-in-tools';
|
|
3
|
+
import type { ToolSchema } from './stream-completion';
|
|
4
|
+
|
|
5
|
+
const READ_TOOLS: ToolSchema[] = [
|
|
6
|
+
BUILT_IN_TOOLS.read_file.schema,
|
|
7
|
+
BUILT_IN_TOOLS.glob.schema,
|
|
8
|
+
BUILT_IN_TOOLS.grep.schema,
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const PATCH_TOOLS: ToolSchema[] = [
|
|
12
|
+
BUILT_IN_TOOLS.read_file.schema,
|
|
13
|
+
BUILT_IN_TOOLS.write_file.schema,
|
|
14
|
+
BUILT_IN_TOOLS.patch_file.schema,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const RUN_TOOLS: ToolSchema[] = [BUILT_IN_TOOLS.run_command.schema];
|
|
18
|
+
|
|
19
|
+
const EVIDENCE_TOOLS: ToolSchema[] = [
|
|
20
|
+
BUILT_IN_TOOLS.read_file.schema,
|
|
21
|
+
BUILT_IN_TOOLS.glob.schema,
|
|
22
|
+
BUILT_IN_TOOLS.run_command.schema,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const ACTION_TOOL_MAP: Record<Action['type'], ToolSchema[]> = {
|
|
26
|
+
read_code: READ_TOOLS,
|
|
27
|
+
generate_patch: PATCH_TOOLS,
|
|
28
|
+
run_tests: RUN_TOOLS,
|
|
29
|
+
run_lint: RUN_TOOLS,
|
|
30
|
+
collect_evidence: EVIDENCE_TOOLS,
|
|
31
|
+
custom: ALL_BUILT_IN_SCHEMAS,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function selectToolsForActions(actions: Action[]): ToolSchema[] {
|
|
35
|
+
const seen = new Set<string>();
|
|
36
|
+
const result: ToolSchema[] = [];
|
|
37
|
+
for (const action of actions) {
|
|
38
|
+
for (const tool of ACTION_TOOL_MAP[action.type] ?? ALL_BUILT_IN_SCHEMAS) {
|
|
39
|
+
if (!seen.has(tool.function.name)) {
|
|
40
|
+
seen.add(tool.function.name);
|
|
41
|
+
result.push(tool);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import type { ToolSchema } from './stream-completion';
|
|
5
|
+
|
|
6
|
+
export interface BuiltInToolHandler {
|
|
7
|
+
schema: ToolSchema;
|
|
8
|
+
idempotent: boolean;
|
|
9
|
+
execute(args: Record<string, unknown>, cwd: string): Promise<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ─── read_file ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const readFile: BuiltInToolHandler = {
|
|
15
|
+
idempotent: true,
|
|
16
|
+
schema: {
|
|
17
|
+
type: 'function',
|
|
18
|
+
function: {
|
|
19
|
+
name: 'read_file',
|
|
20
|
+
description: 'Read the contents of a file',
|
|
21
|
+
parameters: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
path: { type: 'string', description: 'File path relative to workspace root' },
|
|
25
|
+
offset: { type: 'integer', description: 'Line to start reading from (1-based)' },
|
|
26
|
+
limit: { type: 'integer', description: 'Maximum number of lines to read' },
|
|
27
|
+
},
|
|
28
|
+
required: ['path'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
async execute(args, cwd) {
|
|
33
|
+
const filePath = path.resolve(cwd, String(args.path));
|
|
34
|
+
if (!fs.existsSync(filePath)) return `Error: file not found: ${args.path}`;
|
|
35
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
36
|
+
const lines = content.split('\n');
|
|
37
|
+
const offset = typeof args.offset === 'number' ? args.offset - 1 : 0;
|
|
38
|
+
const limit = typeof args.limit === 'number' ? args.limit : lines.length;
|
|
39
|
+
return lines.slice(offset, offset + limit).join('\n');
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ─── write_file ───────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const writeFile: BuiltInToolHandler = {
|
|
46
|
+
idempotent: false,
|
|
47
|
+
schema: {
|
|
48
|
+
type: 'function',
|
|
49
|
+
function: {
|
|
50
|
+
name: 'write_file',
|
|
51
|
+
description: 'Write content to a file (creates or overwrites)',
|
|
52
|
+
parameters: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
path: { type: 'string', description: 'File path relative to workspace root' },
|
|
56
|
+
content: { type: 'string', description: 'Content to write' },
|
|
57
|
+
},
|
|
58
|
+
required: ['path', 'content'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
async execute(args, cwd) {
|
|
63
|
+
const filePath = path.resolve(cwd, String(args.path));
|
|
64
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
65
|
+
fs.writeFileSync(filePath, String(args.content), 'utf8');
|
|
66
|
+
return `Written ${String(args.content).length} bytes to ${args.path}`;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ─── patch_file ───────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const patchFile: BuiltInToolHandler = {
|
|
73
|
+
idempotent: false,
|
|
74
|
+
schema: {
|
|
75
|
+
type: 'function',
|
|
76
|
+
function: {
|
|
77
|
+
name: 'patch_file',
|
|
78
|
+
description: 'Replace an exact string in a file with new content',
|
|
79
|
+
parameters: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
path: { type: 'string', description: 'File path relative to workspace root' },
|
|
83
|
+
old_string: { type: 'string', description: 'Exact string to replace' },
|
|
84
|
+
new_string: { type: 'string', description: 'Replacement string' },
|
|
85
|
+
},
|
|
86
|
+
required: ['path', 'old_string', 'new_string'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
async execute(args, cwd) {
|
|
91
|
+
const filePath = path.resolve(cwd, String(args.path));
|
|
92
|
+
if (!fs.existsSync(filePath)) return `Error: file not found: ${args.path}`;
|
|
93
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
94
|
+
const oldStr = String(args.old_string);
|
|
95
|
+
if (!content.includes(oldStr)) return `Error: old_string not found in ${args.path}`;
|
|
96
|
+
const updated = content.replace(oldStr, String(args.new_string));
|
|
97
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
98
|
+
return `Patched ${args.path}`;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ─── glob ─────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
const glob: BuiltInToolHandler = {
|
|
105
|
+
idempotent: true,
|
|
106
|
+
schema: {
|
|
107
|
+
type: 'function',
|
|
108
|
+
function: {
|
|
109
|
+
name: 'glob',
|
|
110
|
+
description: 'List files matching a glob pattern within the workspace',
|
|
111
|
+
parameters: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
pattern: { type: 'string', description: 'Glob pattern, e.g. src/**/*.ts' },
|
|
115
|
+
},
|
|
116
|
+
required: ['pattern'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
async execute(args, cwd) {
|
|
121
|
+
const results = globSync(String(args.pattern), cwd);
|
|
122
|
+
return results.length ? results.join('\n') : '(no files matched)';
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function globSync(pattern: string, root: string): string[] {
|
|
127
|
+
const results: string[] = [];
|
|
128
|
+
const parts = pattern.split('/');
|
|
129
|
+
function walk(dir: string, remaining: string[]): void {
|
|
130
|
+
if (!fs.existsSync(dir)) return;
|
|
131
|
+
const [head, ...tail] = remaining;
|
|
132
|
+
if (!head) return;
|
|
133
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const matches = micromatch(entry.name, head);
|
|
136
|
+
if (!matches) continue;
|
|
137
|
+
const full = path.join(dir, entry.name);
|
|
138
|
+
if (tail.length === 0) {
|
|
139
|
+
results.push(path.relative(root, full).replace(/\\/g, '/'));
|
|
140
|
+
} else if (entry.isDirectory()) {
|
|
141
|
+
if (head === '**') {
|
|
142
|
+
walk(full, remaining);
|
|
143
|
+
walk(full, tail);
|
|
144
|
+
} else {
|
|
145
|
+
walk(full, tail);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
walk(root, parts);
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function micromatch(name: string, pattern: string): boolean {
|
|
155
|
+
if (pattern === '**') return true;
|
|
156
|
+
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]') + '$');
|
|
157
|
+
return regex.test(name);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── grep ─────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
const grep: BuiltInToolHandler = {
|
|
163
|
+
idempotent: true,
|
|
164
|
+
schema: {
|
|
165
|
+
type: 'function',
|
|
166
|
+
function: {
|
|
167
|
+
name: 'grep',
|
|
168
|
+
description: 'Search for a regex pattern in files',
|
|
169
|
+
parameters: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
pattern: { type: 'string', description: 'Regex pattern to search for' },
|
|
173
|
+
path: { type: 'string', description: 'File or directory to search in (relative to workspace)' },
|
|
174
|
+
},
|
|
175
|
+
required: ['pattern'],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
async execute(args, cwd) {
|
|
180
|
+
const searchRoot = args.path ? path.resolve(cwd, String(args.path)) : cwd;
|
|
181
|
+
const regex = new RegExp(String(args.pattern));
|
|
182
|
+
const results: string[] = [];
|
|
183
|
+
grepDir(searchRoot, regex, cwd, results);
|
|
184
|
+
return results.length ? results.slice(0, 200).join('\n') : '(no matches)';
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
function grepDir(dir: string, regex: RegExp, root: string, out: string[]): void {
|
|
189
|
+
if (!fs.existsSync(dir)) return;
|
|
190
|
+
const stat = fs.statSync(dir);
|
|
191
|
+
if (stat.isFile()) {
|
|
192
|
+
grepFile(dir, regex, root, out);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
196
|
+
const full = path.join(dir, entry.name);
|
|
197
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
198
|
+
grepDir(full, regex, root, out);
|
|
199
|
+
} else if (entry.isFile()) {
|
|
200
|
+
grepFile(full, regex, root, out);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function grepFile(filePath: string, regex: RegExp, root: string, out: string[]): void {
|
|
206
|
+
try {
|
|
207
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
208
|
+
content.split('\n').forEach((line, i) => {
|
|
209
|
+
if (regex.test(line)) {
|
|
210
|
+
out.push(`${path.relative(root, filePath).replace(/\\/g, '/')}:${i + 1}: ${line}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
} catch { /* skip binary or unreadable files */ }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── run_command ──────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
const runCommand: BuiltInToolHandler = {
|
|
219
|
+
idempotent: false,
|
|
220
|
+
schema: {
|
|
221
|
+
type: 'function',
|
|
222
|
+
function: {
|
|
223
|
+
name: 'run_command',
|
|
224
|
+
description: 'Run a shell command in the workspace directory',
|
|
225
|
+
parameters: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
command: { type: 'string', description: 'Shell command to execute' },
|
|
229
|
+
timeout_ms: { type: 'integer', description: 'Timeout in milliseconds (default 30000)' },
|
|
230
|
+
},
|
|
231
|
+
required: ['command'],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
async execute(args, cwd) {
|
|
236
|
+
const command = String(args.command);
|
|
237
|
+
const timeoutMs = typeof args.timeout_ms === 'number' ? args.timeout_ms : 30_000;
|
|
238
|
+
return runShell(command, cwd, timeoutMs);
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
function runShell(command: string, cwd: string, timeoutMs: number): Promise<string> {
|
|
243
|
+
return new Promise((resolve) => {
|
|
244
|
+
const isWin = process.platform === 'win32';
|
|
245
|
+
const shell = isWin ? 'cmd' : 'sh';
|
|
246
|
+
const shellArgs = isWin ? ['/c', command] : ['-c', command];
|
|
247
|
+
const proc = spawn(shell, shellArgs, { cwd, stdio: 'pipe' });
|
|
248
|
+
const out: Buffer[] = [];
|
|
249
|
+
let timedOut = false;
|
|
250
|
+
|
|
251
|
+
const timer = setTimeout(() => { timedOut = true; proc.kill('SIGTERM'); }, timeoutMs);
|
|
252
|
+
|
|
253
|
+
proc.stdout.on('data', (c: Buffer) => out.push(c));
|
|
254
|
+
proc.stderr.on('data', (c: Buffer) => out.push(c));
|
|
255
|
+
proc.on('close', (code) => {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
const text = Buffer.concat(out).toString('utf8').trim();
|
|
258
|
+
if (timedOut) resolve(`[timed out after ${timeoutMs}ms]\n${text}`);
|
|
259
|
+
else resolve(code === 0 ? text || '(no output)' : `[exit ${code}]\n${text}`);
|
|
260
|
+
});
|
|
261
|
+
proc.on('error', (err) => { clearTimeout(timer); resolve(`[error] ${err}`); });
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
export const BUILT_IN_TOOLS: Record<string, BuiltInToolHandler> = {
|
|
268
|
+
read_file: readFile,
|
|
269
|
+
write_file: writeFile,
|
|
270
|
+
patch_file: patchFile,
|
|
271
|
+
glob,
|
|
272
|
+
grep,
|
|
273
|
+
run_command: runCommand,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export const ALL_BUILT_IN_SCHEMAS: ToolSchema[] = Object.values(BUILT_IN_TOOLS).map((t) => t.schema);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { LlmTaskExecutor } from './llm-task-executor';
|
|
2
|
+
export type { LlmProviderConfig, LlmExecutorEvent, LlmExecutorEventType } from './llm-task-executor';
|
|
3
|
+
export { buildNodePrompt } from './node-prompt-builder';
|
|
4
|
+
export { selectToolsForActions } from './action-tool-map';
|
|
5
|
+
export { BUILT_IN_TOOLS, ALL_BUILT_IN_SCHEMAS } from './built-in-tools';
|
|
6
|
+
export type { BuiltInToolHandler } from './built-in-tools';
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { GraphNode } from '../compiler/graph-compiler';
|
|
2
|
+
import type { WorkspaceLease } from '../models/workspace';
|
|
3
|
+
import type { TaskExecutor, TaskResult } from '../scheduler/scheduler';
|
|
4
|
+
import type { PluginRegistry } from '../plugins/plugin-registry';
|
|
5
|
+
import { streamCompletion } from './stream-completion';
|
|
6
|
+
import type { ChatMessage, ToolCall, ToolSchema } from './stream-completion';
|
|
7
|
+
import { BUILT_IN_TOOLS } from './built-in-tools';
|
|
8
|
+
import { selectToolsForActions } from './action-tool-map';
|
|
9
|
+
import { buildNodePrompt } from './node-prompt-builder';
|
|
10
|
+
|
|
11
|
+
export interface LlmProviderConfig {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
model: string;
|
|
15
|
+
maxTokens?: number;
|
|
16
|
+
maxTurns?: number;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
systemPrompt?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type LlmExecutorEventType =
|
|
22
|
+
| 'turn_start'
|
|
23
|
+
| 'turn_complete'
|
|
24
|
+
| 'tool_call'
|
|
25
|
+
| 'tool_result'
|
|
26
|
+
| 'executor_complete';
|
|
27
|
+
|
|
28
|
+
export interface LlmExecutorEvent {
|
|
29
|
+
type: LlmExecutorEventType;
|
|
30
|
+
nodeId: string;
|
|
31
|
+
attempt: number;
|
|
32
|
+
detail?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_SYSTEM_PROMPT =
|
|
36
|
+
'You are a precise software engineering agent. Use the tools provided to complete the task. ' +
|
|
37
|
+
'When the task is done, summarize what was accomplished in your final message without calling any tools.';
|
|
38
|
+
|
|
39
|
+
export class LlmTaskExecutor implements TaskExecutor {
|
|
40
|
+
constructor(
|
|
41
|
+
private readonly provider: LlmProviderConfig,
|
|
42
|
+
private readonly registry?: PluginRegistry,
|
|
43
|
+
private readonly onProgress?: (event: LlmExecutorEvent) => void,
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
async execute(
|
|
47
|
+
node: GraphNode,
|
|
48
|
+
lease: WorkspaceLease,
|
|
49
|
+
runId: string,
|
|
50
|
+
attempt: number,
|
|
51
|
+
): Promise<TaskResult> {
|
|
52
|
+
const prompt = buildNodePrompt(node, lease, runId, attempt);
|
|
53
|
+
const tools = selectToolsForActions(node.actions);
|
|
54
|
+
const cwd = lease.root_path;
|
|
55
|
+
const maxTurns = this.provider.maxTurns ?? 10;
|
|
56
|
+
const systemPrompt = this.provider.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
57
|
+
|
|
58
|
+
const messages: ChatMessage[] = [
|
|
59
|
+
{ role: 'system', content: systemPrompt },
|
|
60
|
+
{ role: 'user', content: prompt },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
let finalOutput = '';
|
|
64
|
+
const evidencePaths: string[] = [];
|
|
65
|
+
|
|
66
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
67
|
+
this.emit({ type: 'turn_start', nodeId: node.id, attempt, detail: { turn } });
|
|
68
|
+
|
|
69
|
+
let response;
|
|
70
|
+
try {
|
|
71
|
+
response = await streamCompletion({
|
|
72
|
+
baseUrl: this.provider.baseUrl,
|
|
73
|
+
apiKey: this.provider.apiKey,
|
|
74
|
+
model: this.provider.model,
|
|
75
|
+
messages,
|
|
76
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
77
|
+
maxTokens: this.provider.maxTokens,
|
|
78
|
+
timeoutMs: this.provider.timeoutMs,
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return { success: false, failure_class: 'env', evidence: evidencePaths, output: String(err) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.emit({ type: 'turn_complete', nodeId: node.id, attempt, detail: { turn, finish_reason: response.finish_reason } });
|
|
85
|
+
|
|
86
|
+
if (response.content) finalOutput = response.content;
|
|
87
|
+
|
|
88
|
+
if (!response.tool_calls.length) break;
|
|
89
|
+
|
|
90
|
+
messages.push({
|
|
91
|
+
role: 'assistant',
|
|
92
|
+
content: response.content || null,
|
|
93
|
+
tool_calls: response.tool_calls,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Partition: idempotent tools run concurrently, mutations run serially
|
|
97
|
+
const [concurrent, serial] = partitionToolCalls(response.tool_calls, tools);
|
|
98
|
+
|
|
99
|
+
const concurrentResults = await Promise.all(
|
|
100
|
+
concurrent.map((tc) => this.invokeToolCall(tc, cwd, node, evidencePaths)),
|
|
101
|
+
);
|
|
102
|
+
const serialResults: ChatMessage[] = [];
|
|
103
|
+
for (const tc of serial) {
|
|
104
|
+
serialResults.push(await this.invokeToolCall(tc, cwd, node, evidencePaths));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
messages.push(...concurrentResults, ...serialResults);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
success: true,
|
|
112
|
+
failure_class: null,
|
|
113
|
+
evidence: evidencePaths,
|
|
114
|
+
output: finalOutput,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async invokeToolCall(
|
|
119
|
+
tc: ToolCall,
|
|
120
|
+
cwd: string,
|
|
121
|
+
node: GraphNode,
|
|
122
|
+
evidencePaths: string[],
|
|
123
|
+
): Promise<ChatMessage> {
|
|
124
|
+
this.emit({ type: 'tool_call', nodeId: node.id, attempt: 0, detail: { tool: tc.function.name } });
|
|
125
|
+
|
|
126
|
+
let args: Record<string, unknown> = {};
|
|
127
|
+
try {
|
|
128
|
+
args = JSON.parse(tc.function.arguments || '{}');
|
|
129
|
+
} catch {
|
|
130
|
+
// malformed args — pass empty
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let result: string;
|
|
134
|
+
const builtIn = BUILT_IN_TOOLS[tc.function.name];
|
|
135
|
+
if (builtIn) {
|
|
136
|
+
try {
|
|
137
|
+
result = await builtIn.execute(args, cwd);
|
|
138
|
+
if (tc.function.name === 'write_file' || tc.function.name === 'patch_file') {
|
|
139
|
+
if (typeof args.path === 'string') evidencePaths.push(args.path);
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
result = `[tool error] ${err}`;
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
// Delegate to plugin registry
|
|
146
|
+
const provider = this.registry?.toolProviderFor(tc.function.name);
|
|
147
|
+
if (provider) {
|
|
148
|
+
try {
|
|
149
|
+
const res = await provider.invoke({
|
|
150
|
+
action_type: tc.function.name,
|
|
151
|
+
work_item_id: node.id,
|
|
152
|
+
run_id: '',
|
|
153
|
+
attempt_id: '',
|
|
154
|
+
params: args,
|
|
155
|
+
workspace_root: cwd,
|
|
156
|
+
});
|
|
157
|
+
result = res.output || (res.success ? 'done' : res.error ?? 'failed');
|
|
158
|
+
evidencePaths.push(...res.evidence_paths);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
result = `[plugin error] ${err}`;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
result = `[unknown tool: ${tc.function.name}]`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.emit({ type: 'tool_result', nodeId: node.id, attempt: 0, detail: { tool: tc.function.name, length: result.length } });
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
role: 'tool',
|
|
171
|
+
tool_call_id: tc.id,
|
|
172
|
+
name: tc.function.name,
|
|
173
|
+
content: result,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private emit(event: LlmExecutorEvent): void {
|
|
178
|
+
this.onProgress?.(event);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function partitionToolCalls(
|
|
183
|
+
toolCalls: ToolCall[],
|
|
184
|
+
schemas: ToolSchema[],
|
|
185
|
+
): [ToolCall[], ToolCall[]] {
|
|
186
|
+
const idempotentNames = new Set(
|
|
187
|
+
schemas
|
|
188
|
+
.map((s) => s.function.name)
|
|
189
|
+
.filter((name) => BUILT_IN_TOOLS[name]?.idempotent ?? true),
|
|
190
|
+
);
|
|
191
|
+
const concurrent = toolCalls.filter((tc) => idempotentNames.has(tc.function.name));
|
|
192
|
+
const serial = toolCalls.filter((tc) => !idempotentNames.has(tc.function.name));
|
|
193
|
+
return [concurrent, serial];
|
|
194
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { GraphNode } from '../compiler/graph-compiler';
|
|
2
|
+
import type { WorkspaceLease } from '../models/workspace';
|
|
3
|
+
|
|
4
|
+
export function buildNodePrompt(
|
|
5
|
+
node: GraphNode,
|
|
6
|
+
lease: WorkspaceLease,
|
|
7
|
+
runId: string,
|
|
8
|
+
attempt: number,
|
|
9
|
+
): string {
|
|
10
|
+
const lines: string[] = [
|
|
11
|
+
`# Tarefa: ${node.title}`,
|
|
12
|
+
'',
|
|
13
|
+
`**Run:** ${runId} | **Attempt:** ${attempt}`,
|
|
14
|
+
`**Workspace:** ${lease.root_path}`,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
if (node.mutation_scope.length > 0) {
|
|
18
|
+
lines.push(`**Escopo de mutação:** ${node.mutation_scope.join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (node.actions.length > 0) {
|
|
22
|
+
lines.push('', '## Ações requeridas');
|
|
23
|
+
for (const action of node.actions) {
|
|
24
|
+
let line = `- ${action.type}`;
|
|
25
|
+
if (action.command) line += `: \`${action.command}\``;
|
|
26
|
+
if (action.targets?.length) line += ` em ${action.targets.join(', ')}`;
|
|
27
|
+
lines.push(line);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (node.verify.must_pass.length > 0) {
|
|
32
|
+
lines.push('', '## Critérios de aceite');
|
|
33
|
+
for (const criterion of node.verify.must_pass) {
|
|
34
|
+
lines.push(`- ${criterion}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (node.verify.command) {
|
|
39
|
+
lines.push('', `**Verificação:** \`${node.verify.command}\``);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lines.push('', 'Execute as ações acima usando as ferramentas disponíveis e confirme o resultado.');
|
|
43
|
+
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|