@sdods/agents 0.2.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/LICENSE +17 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/adapter/claude-code.d.ts +29 -0
- package/dist/adapter/claude-code.js +195 -0
- package/dist/adapter/claude.d.ts +42 -0
- package/dist/adapter/claude.js +216 -0
- package/dist/adapter/cli-common.d.ts +46 -0
- package/dist/adapter/cli-common.js +145 -0
- package/dist/adapter/codex.d.ts +31 -0
- package/dist/adapter/codex.js +252 -0
- package/dist/adapter/factory.d.ts +41 -0
- package/dist/adapter/factory.js +102 -0
- package/dist/adapter/fake.d.ts +39 -0
- package/dist/adapter/fake.js +91 -0
- package/dist/adapter/index.d.ts +11 -0
- package/dist/adapter/index.js +11 -0
- package/dist/adapter/json-schema.d.ts +11 -0
- package/dist/adapter/json-schema.js +17 -0
- package/dist/adapter/mcp-bridge.d.ts +35 -0
- package/dist/adapter/mcp-bridge.js +122 -0
- package/dist/adapter/ollama.d.ts +62 -0
- package/dist/adapter/ollama.js +272 -0
- package/dist/adapter/openai-compat.d.ts +44 -0
- package/dist/adapter/openai-compat.js +200 -0
- package/dist/adapter/profile.d.ts +54 -0
- package/dist/adapter/profile.js +42 -0
- package/dist/adapter/tool-loop.d.ts +64 -0
- package/dist/adapter/tool-loop.js +192 -0
- package/dist/adapter/types.d.ts +109 -0
- package/dist/adapter/types.js +20 -0
- package/dist/claude-code/install.d.ts +36 -0
- package/dist/claude-code/install.js +183 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jobs/runner.d.ts +80 -0
- package/dist/jobs/runner.js +197 -0
- package/dist/roles/index.d.ts +50 -0
- package/dist/roles/index.js +125 -0
- package/dist/roles/steps.d.ts +28 -0
- package/dist/roles/steps.js +75 -0
- package/package.json +50 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { renderPrompt, ROLE_PROMPTS } from '@sdods/mcp/prompts';
|
|
2
|
+
import { toAgentSdkTools } from '@sdods/mcp';
|
|
3
|
+
export { ROLE_PROMPTS };
|
|
4
|
+
const COMMON_READ = [
|
|
5
|
+
'project_',
|
|
6
|
+
'workspace_',
|
|
7
|
+
'process_',
|
|
8
|
+
'feature_list',
|
|
9
|
+
'feature_read',
|
|
10
|
+
'feature_parse',
|
|
11
|
+
'feature_lint',
|
|
12
|
+
'step_',
|
|
13
|
+
'run_get',
|
|
14
|
+
'run_list',
|
|
15
|
+
'run_last_failed',
|
|
16
|
+
'heal_',
|
|
17
|
+
'data_',
|
|
18
|
+
'analyze_',
|
|
19
|
+
'proposal_list',
|
|
20
|
+
'proposal_get',
|
|
21
|
+
];
|
|
22
|
+
export const ROLES = {
|
|
23
|
+
planner: {
|
|
24
|
+
role: 'planner',
|
|
25
|
+
toolPrefixes: [...COMMON_READ, 'feature_write'],
|
|
26
|
+
smallTools: [
|
|
27
|
+
'project_get_config',
|
|
28
|
+
'feature_list',
|
|
29
|
+
'analyze_routes',
|
|
30
|
+
'analyze_coverage',
|
|
31
|
+
'step_find',
|
|
32
|
+
'feature_write',
|
|
33
|
+
],
|
|
34
|
+
needsBrowser: true,
|
|
35
|
+
defaults: { maxTurns: 30, budgetUsd: 5 },
|
|
36
|
+
buildPrompt: (i) => `Plan test coverage for project ${i.project ?? '<unknown>'}${i.env ? ` on ${i.env}` : ''}.\nGoal: ${i.goal ?? 'cover the main user journeys'}.\nWrite the plan as docs/test-plans/<name>.md through feature_write's extraFiles (path relative to the project root: ../../docs/test-plans is NOT allowed; use docs/test-plans/<name>.md under the project).`,
|
|
37
|
+
buildSmallPrompt: (i) => `Write a test plan for project ${i.project ?? '<unknown>'}.\nGoal: ${i.goal ?? 'cover the main user journeys'}.\nCall feature_write once, with the plan as an extra file named docs/test-plans/plan.md.`,
|
|
38
|
+
},
|
|
39
|
+
generator: {
|
|
40
|
+
role: 'generator',
|
|
41
|
+
toolPrefixes: [...COMMON_READ, 'feature_write', 'run_tests'],
|
|
42
|
+
// run_tests is deliberately absent: it carries the two largest schemas in the registry and a
|
|
43
|
+
// small model that calls it spends minutes on output it cannot read. A person reviews the
|
|
44
|
+
// proposal anyway.
|
|
45
|
+
smallTools: ['step_find', 'feature_list', 'feature_read', 'feature_parse', 'feature_write'],
|
|
46
|
+
needsBrowser: true,
|
|
47
|
+
defaults: { maxTurns: 40, budgetUsd: 3 },
|
|
48
|
+
buildPrompt: (i) => `Generate feature files for project ${i.project ?? '<unknown>'}${i.env ? ` on ${i.env}` : ''}.\n${i.plan ? `Plan:\n${i.plan}\n` : ''}${i.spec ? `Recorded spec to convert:\n\`\`\`ts\n${i.spec}\n\`\`\`\n` : ''}Goal: ${i.goal ?? 'implement the plan'}.\nCall step_list first, then feature_parse on your draft, then feature_write.`,
|
|
49
|
+
buildSmallPrompt: (i) => `Write one feature file for project ${i.project ?? '<unknown>'}.\nGoal: ${i.goal ?? 'implement the plan'}.\n${i.spec ? `Base it on this recording:\n\`\`\`ts\n${i.spec}\n\`\`\`\n` : ''}Call feature_write now, using only the step patterns listed above.`,
|
|
50
|
+
},
|
|
51
|
+
healer: {
|
|
52
|
+
role: 'healer',
|
|
53
|
+
toolPrefixes: [...COMMON_READ, 'feature_write', 'run_tests', 'analyze_failure'],
|
|
54
|
+
smallTools: [
|
|
55
|
+
'analyze_failure',
|
|
56
|
+
'run_get_scenario',
|
|
57
|
+
'heal_events',
|
|
58
|
+
'feature_read',
|
|
59
|
+
'feature_write',
|
|
60
|
+
],
|
|
61
|
+
needsBrowser: true,
|
|
62
|
+
defaults: { maxTurns: 30, budgetUsd: 2 },
|
|
63
|
+
buildPrompt: (i) => `Heal scenario ${i.scenario ?? '<fingerprint>'} of run ${i.runId ?? 'last'} in project ${i.project ?? '<unknown>'}.\nStart with analyze_failure and run_get_scenario. Propose the minimal fix via feature_write (extraFiles for page objects/steps).`,
|
|
64
|
+
buildSmallPrompt: (i) => `Scenario "${i.scenario ?? '<fingerprint>'}" of run ${i.runId ?? 'last'} in project ${i.project ?? '<unknown>'} is failing.\nCall analyze_failure now to find out why.`,
|
|
65
|
+
},
|
|
66
|
+
upgrader: {
|
|
67
|
+
role: 'upgrader',
|
|
68
|
+
toolPrefixes: [...COMMON_READ, 'feature_write'],
|
|
69
|
+
smallTools: [
|
|
70
|
+
'analyze_change_impact',
|
|
71
|
+
'analyze_coverage',
|
|
72
|
+
'feature_list',
|
|
73
|
+
'feature_read',
|
|
74
|
+
'feature_write',
|
|
75
|
+
],
|
|
76
|
+
needsBrowser: false,
|
|
77
|
+
defaults: { maxTurns: 30, budgetUsd: 3 },
|
|
78
|
+
buildPrompt: (i) => `Upgrade the tests of project ${i.project ?? '<unknown>'} for the change ${i.diff ?? 'HEAD~1..HEAD'}.\nStart with analyze_change_impact and analyze_coverage.${i.goal ? `\nFocus: ${i.goal}` : ''}`,
|
|
79
|
+
buildSmallPrompt: (i) => `Code changed in project ${i.project ?? '<unknown>'} (${i.diff ?? 'HEAD~1..HEAD'}).\nCall analyze_change_impact now to find the scenarios it affects.`,
|
|
80
|
+
},
|
|
81
|
+
reviewer: {
|
|
82
|
+
role: 'reviewer',
|
|
83
|
+
toolPrefixes: [...COMMON_READ, 'feature_write'],
|
|
84
|
+
smallTools: [
|
|
85
|
+
'feature_list',
|
|
86
|
+
'feature_read',
|
|
87
|
+
'analyze_best_practices',
|
|
88
|
+
'analyze_locators',
|
|
89
|
+
'feature_parse',
|
|
90
|
+
],
|
|
91
|
+
needsBrowser: false,
|
|
92
|
+
defaults: { maxTurns: 15, budgetUsd: 1 },
|
|
93
|
+
buildPrompt: (i) => `Review ${i.files?.length ? i.files.join(', ') : 'all feature files'} of project ${i.project ?? '<unknown>'}.\nUse analyze_best_practices, analyze_locators and feature_parse.${i.goal ? `\nFocus: ${i.goal}` : ''}`,
|
|
94
|
+
buildSmallPrompt: (i) => `Review the feature files of project ${i.project ?? '<unknown>'}${i.goal ? ` for: ${i.goal}` : ''}.\nCall feature_list now, then read the files it names.`,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
/** The user-facing request for a role, in the shape the profile can act on. */
|
|
98
|
+
export function rolePrompt(role, input, profile = 'full') {
|
|
99
|
+
const def = ROLES[role];
|
|
100
|
+
return profile === 'small' ? def.buildSmallPrompt(input) : def.buildPrompt(input);
|
|
101
|
+
}
|
|
102
|
+
export function roleSystemPrompt(role, input, steps, profile = 'full') {
|
|
103
|
+
return renderPrompt(role, {
|
|
104
|
+
project: input.project,
|
|
105
|
+
env: input.env,
|
|
106
|
+
goal: input.goal,
|
|
107
|
+
steps,
|
|
108
|
+
profile,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Tools visible to a role. The full profile filters by prefix; the small profile takes the
|
|
113
|
+
* role's named allowlist, in its declared order, so the model reads a short list of things it is
|
|
114
|
+
* expected to do rather than a catalogue.
|
|
115
|
+
*/
|
|
116
|
+
export function roleTools(role, registry, ctx, profile = 'full') {
|
|
117
|
+
const def = ROLES[role];
|
|
118
|
+
const all = toAgentSdkTools(registry, ctx);
|
|
119
|
+
if (profile === 'small') {
|
|
120
|
+
const byName = new Map(all.map((t) => [t.name, t]));
|
|
121
|
+
return def.smallTools.map((n) => byName.get(n)).filter((t) => !!t);
|
|
122
|
+
}
|
|
123
|
+
return all.filter((t) => def.toolPrefixes.some((p) => t.name.startsWith(p)));
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ToolContext, type ToolRegistry } from '@sdods/mcp';
|
|
2
|
+
/**
|
|
3
|
+
* The project's real step vocabulary, for the system prompt.
|
|
4
|
+
*
|
|
5
|
+
* A model that has not been shown the steps invents them, and an invented step is a scenario that
|
|
6
|
+
* cannot run. Every role prompt says "reuse existing steps", and until now nothing put the steps
|
|
7
|
+
* in front of the model on the agents path — `renderPrompt` accepted them and no caller passed
|
|
8
|
+
* any. Small models need this most, but nothing about it is small-model specific.
|
|
9
|
+
*
|
|
10
|
+
* Producing the list runs `sdods steps list`, which compiles the project and takes seconds, so it
|
|
11
|
+
* is cached against the modification time of the project's `steps/` directory.
|
|
12
|
+
*/
|
|
13
|
+
export interface StepVocabulary {
|
|
14
|
+
/** Every pattern the project knows. */
|
|
15
|
+
all: string[];
|
|
16
|
+
/** The subset put in the prompt, ranked against the goal when there is one. */
|
|
17
|
+
selected: string[];
|
|
18
|
+
fromCache: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function stepVocabulary(o: {
|
|
21
|
+
registry: ToolRegistry;
|
|
22
|
+
ctx: ToolContext;
|
|
23
|
+
project?: string;
|
|
24
|
+
/** Goal, plan or spec text: the patterns closest to it go first. */
|
|
25
|
+
focus?: string;
|
|
26
|
+
limit?: number;
|
|
27
|
+
}): Promise<StepVocabulary>;
|
|
28
|
+
//# sourceMappingURL=steps.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { similarity } from '@sdods/mcp';
|
|
4
|
+
const CACHE_DIR = join('.sdods', 'cache');
|
|
5
|
+
const TTL_MS = 10 * 60_000;
|
|
6
|
+
export async function stepVocabulary(o) {
|
|
7
|
+
const empty = { all: [], selected: [], fromCache: false };
|
|
8
|
+
if (!o.project || !o.limit)
|
|
9
|
+
return empty;
|
|
10
|
+
const cached = readCache(o.ctx.rootDir, o.project);
|
|
11
|
+
const all = cached ?? (await listSteps(o.registry, o.ctx, o.project));
|
|
12
|
+
if (!all.length)
|
|
13
|
+
return empty;
|
|
14
|
+
if (!cached)
|
|
15
|
+
writeCache(o.ctx.rootDir, o.project, all);
|
|
16
|
+
const selected = o.focus
|
|
17
|
+
? [...all]
|
|
18
|
+
.map((pattern) => ({ pattern, score: similarity(o.focus, pattern) }))
|
|
19
|
+
.sort((a, b) => b.score - a.score)
|
|
20
|
+
.slice(0, o.limit)
|
|
21
|
+
.map((s) => s.pattern)
|
|
22
|
+
: all.slice(0, o.limit);
|
|
23
|
+
return { all, selected, fromCache: Boolean(cached) };
|
|
24
|
+
}
|
|
25
|
+
async function listSteps(registry, ctx, project) {
|
|
26
|
+
try {
|
|
27
|
+
const result = await registry.call('step_list', { project }, ctx);
|
|
28
|
+
// The registry wraps a tool's data as `structuredContent.items` for a list.
|
|
29
|
+
const structured = result
|
|
30
|
+
.structuredContent;
|
|
31
|
+
const data = structured?.items ?? structured?.data;
|
|
32
|
+
if (!Array.isArray(data))
|
|
33
|
+
return [];
|
|
34
|
+
return data
|
|
35
|
+
.map((s) => {
|
|
36
|
+
const step = s;
|
|
37
|
+
return `${step.keyword ?? ''} ${step.pattern ?? ''}`.trim();
|
|
38
|
+
})
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// A project that will not compile still deserves a job; the model is simply less grounded.
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function cacheFile(rootDir, project) {
|
|
47
|
+
return join(rootDir, CACHE_DIR, `steps-${project}.json`);
|
|
48
|
+
}
|
|
49
|
+
function readCache(rootDir, project) {
|
|
50
|
+
const file = cacheFile(rootDir, project);
|
|
51
|
+
if (!existsSync(file))
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
const raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
55
|
+
if (Date.now() - raw.at > TTL_MS)
|
|
56
|
+
return undefined;
|
|
57
|
+
const stepsDir = join(rootDir, 'projects', project, 'steps');
|
|
58
|
+
if (existsSync(stepsDir) && statSync(stepsDir).mtimeMs > raw.at)
|
|
59
|
+
return undefined;
|
|
60
|
+
return raw.steps;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function writeCache(rootDir, project, steps) {
|
|
67
|
+
try {
|
|
68
|
+
mkdirSync(join(rootDir, CACHE_DIR), { recursive: true });
|
|
69
|
+
writeFileSync(cacheFile(rootDir, project), JSON.stringify({ at: Date.now(), steps }));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* a cache that cannot be written is not an error */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=steps.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sdods/agents",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "SDODS AI agents: planner, generator, healer, upgrader and reviewer over a provider-agnostic adapter.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "SDODS <admin@sdods.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./adapters": "./dist/adapter/index.js",
|
|
11
|
+
"./roles": "./dist/roles/index.js",
|
|
12
|
+
"./jobs": "./dist/jobs/runner.js",
|
|
13
|
+
"./install-claude": "./dist/claude-code/install.js"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.259",
|
|
26
|
+
"@anthropic-ai/sdk": "^0.123.0",
|
|
27
|
+
"@sdods/contracts": "0.2.0",
|
|
28
|
+
"@sdods/mcp": "0.2.0",
|
|
29
|
+
"zod": "^4.5.4",
|
|
30
|
+
"@modelcontextprotocol/client": "^2.0.0"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://sdods.com",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/siri1410/SDODS/issues"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/siri1410/SDODS.git",
|
|
39
|
+
"directory": "packages/agents"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"sdods",
|
|
43
|
+
"automation",
|
|
44
|
+
"orchestration",
|
|
45
|
+
"testing",
|
|
46
|
+
"bdd",
|
|
47
|
+
"ai-agents",
|
|
48
|
+
"mcp"
|
|
49
|
+
]
|
|
50
|
+
}
|