agent-procedures 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -5
- package/bin/cli.js +37 -8
- package/dist/runtime.js +569 -0
- package/lib/auth.js +169 -0
- package/lib/engine.js +179 -0
- package/lib/groomer.js +129 -0
- package/lib/harnesses/claude.js +75 -0
- package/lib/harnesses/index.js +15 -0
- package/lib/init.js +60 -8
- package/lib/main.js +51 -0
- package/lib/providers.js +178 -0
- package/lib/store.js +53 -0
- package/package.json +17 -4
package/lib/main.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { handleEvent } from "./engine.js";
|
|
5
|
+
import { runGroomer } from "./groomer.js";
|
|
6
|
+
import { getHarness } from "./harnesses/index.js";
|
|
7
|
+
|
|
8
|
+
// Bundled to <repo>/.reflex/runtime.js. That file sits inside .reflex/.
|
|
9
|
+
const REFLEX_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
function readStdin() {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
let data = "";
|
|
14
|
+
process.stdin.setEncoding("utf8");
|
|
15
|
+
process.stdin.on("data", (c) => (data += c));
|
|
16
|
+
process.stdin.on("end", () => resolve(data));
|
|
17
|
+
process.stdin.on("error", () => resolve(data));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function spawnGroomer() {
|
|
22
|
+
const child = spawn(process.argv[0], [process.argv[1], "Groom"], { detached: true, stdio: "ignore" });
|
|
23
|
+
child.unref();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function run(harnessId, argv = process.argv.slice(2)) {
|
|
27
|
+
const [eventName] = argv;
|
|
28
|
+
if (!eventName) return;
|
|
29
|
+
|
|
30
|
+
if (eventName === "Groom") {
|
|
31
|
+
await runGroomer(path.join(REFLEX_DIR, "procedures.jsonl"));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const raw = await readStdin();
|
|
36
|
+
if (!raw) return;
|
|
37
|
+
let payload;
|
|
38
|
+
try {
|
|
39
|
+
payload = JSON.parse(raw);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const harness = getHarness(harnessId);
|
|
45
|
+
const evt = harness.normalize(eventName, payload);
|
|
46
|
+
if (!evt) return;
|
|
47
|
+
|
|
48
|
+
const result = handleEvent(evt, REFLEX_DIR, { spawnGroomer });
|
|
49
|
+
const out = harness.render(evt, result);
|
|
50
|
+
if (out) process.stdout.write(out + "\n");
|
|
51
|
+
}
|
package/lib/providers.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
// Shared by `auth` (CLI) and the groomer (background hook). Keep dependency-free.
|
|
6
|
+
|
|
7
|
+
export function reflexHome() {
|
|
8
|
+
return process.env.REFLEX_HOME || path.join(os.homedir(), ".reflex");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function credentialsFile() {
|
|
12
|
+
return path.join(reflexHome(), "credentials.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SYSTEM_PROMPT =
|
|
16
|
+
"You are the Reflex graph groomer. Your job is to review execution edges from coding agents, drop bad ones, and add semantic aliases.";
|
|
17
|
+
|
|
18
|
+
async function expectOk(res, label) {
|
|
19
|
+
if (res.ok) return;
|
|
20
|
+
let detail = await res.text().catch(() => "");
|
|
21
|
+
try {
|
|
22
|
+
// All three providers return { error: { message } } on failure.
|
|
23
|
+
detail = JSON.parse(detail).error?.message || detail;
|
|
24
|
+
} catch (e) {}
|
|
25
|
+
detail = detail.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
26
|
+
throw new Error(`${label} API error: ${res.status} ${res.statusText}${detail ? ` - ${detail}` : ""}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const PROVIDERS = {
|
|
30
|
+
anthropic: {
|
|
31
|
+
label: "Anthropic",
|
|
32
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
33
|
+
defaultModel: "claude-haiku-4-5",
|
|
34
|
+
keyPrefix: "sk-ant-",
|
|
35
|
+
keysUrl: "https://console.anthropic.com/settings/keys",
|
|
36
|
+
async verify(apiKey) {
|
|
37
|
+
const res = await fetch("https://api.anthropic.com/v1/models?limit=1", {
|
|
38
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
39
|
+
});
|
|
40
|
+
await expectOk(res, "Anthropic");
|
|
41
|
+
},
|
|
42
|
+
async complete(apiKey, model, prompt) {
|
|
43
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
"x-api-key": apiKey,
|
|
47
|
+
"anthropic-version": "2023-06-01",
|
|
48
|
+
"content-type": "application/json",
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
model,
|
|
52
|
+
max_tokens: 1024,
|
|
53
|
+
system: SYSTEM_PROMPT,
|
|
54
|
+
messages: [{ role: "user", content: prompt }],
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
await expectOk(res, "Anthropic");
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
return data.content[0].text;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
openai: {
|
|
64
|
+
label: "OpenAI",
|
|
65
|
+
envVar: "OPENAI_API_KEY",
|
|
66
|
+
defaultModel: "gpt-6-luna",
|
|
67
|
+
keyPrefix: "sk-",
|
|
68
|
+
keysUrl: "https://platform.openai.com/api-keys",
|
|
69
|
+
async verify(apiKey) {
|
|
70
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
71
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
72
|
+
});
|
|
73
|
+
await expectOk(res, "OpenAI");
|
|
74
|
+
},
|
|
75
|
+
async complete(apiKey, model, prompt) {
|
|
76
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${apiKey}`,
|
|
80
|
+
"content-type": "application/json",
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
model,
|
|
84
|
+
max_tokens: 1024,
|
|
85
|
+
messages: [
|
|
86
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
87
|
+
{ role: "user", content: prompt },
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
await expectOk(res, "OpenAI");
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
return data.choices[0].message.content;
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
gemini: {
|
|
98
|
+
label: "Gemini",
|
|
99
|
+
envVar: "GEMINI_API_KEY",
|
|
100
|
+
defaultModel: "gemini-3.5-flash-lite",
|
|
101
|
+
keyPrefix: "AIza",
|
|
102
|
+
keysUrl: "https://aistudio.google.com/apikey",
|
|
103
|
+
async verify(apiKey) {
|
|
104
|
+
const res = await fetch(
|
|
105
|
+
"https://generativelanguage.googleapis.com/v1beta/models?pageSize=1",
|
|
106
|
+
{ headers: { "x-goog-api-key": apiKey } },
|
|
107
|
+
);
|
|
108
|
+
await expectOk(res, "Gemini");
|
|
109
|
+
},
|
|
110
|
+
async complete(apiKey, model, prompt) {
|
|
111
|
+
const res = await fetch(
|
|
112
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
|
|
113
|
+
{
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
"x-goog-api-key": apiKey,
|
|
117
|
+
"content-type": "application/json",
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
system_instruction: { parts: [{ text: SYSTEM_PROMPT }] },
|
|
121
|
+
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
122
|
+
generationConfig: { maxOutputTokens: 1024 },
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
await expectOk(res, "Gemini");
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
return data.candidates[0].content.parts[0].text;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const PROVIDER_NAMES = Object.keys(PROVIDERS);
|
|
134
|
+
|
|
135
|
+
export function readCredentials() {
|
|
136
|
+
const file = credentialsFile();
|
|
137
|
+
if (!fs.existsSync(file)) return null;
|
|
138
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve which provider/key/model the groomer should use.
|
|
143
|
+
*
|
|
144
|
+
* Provider: credentials.json's defaultProvider if present, else the first
|
|
145
|
+
* provider with an env var set.
|
|
146
|
+
* Key: the provider's env var wins over the stored key.
|
|
147
|
+
* Model: stored model, else the provider default.
|
|
148
|
+
*
|
|
149
|
+
* Returns null if nothing usable is found.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveCredentials(env = process.env) {
|
|
152
|
+
let stored = null;
|
|
153
|
+
try {
|
|
154
|
+
stored = readCredentials();
|
|
155
|
+
} catch (e) {
|
|
156
|
+
throw new Error(`Failed to parse ${credentialsFile()}: ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let provider = stored?.defaultProvider;
|
|
160
|
+
if (!provider) {
|
|
161
|
+
provider = PROVIDER_NAMES.find((p) => env[PROVIDERS[p].envVar]);
|
|
162
|
+
}
|
|
163
|
+
if (!provider || !PROVIDERS[provider]) return null;
|
|
164
|
+
|
|
165
|
+
const def = PROVIDERS[provider];
|
|
166
|
+
const entry = stored?.providers?.[provider] || {};
|
|
167
|
+
|
|
168
|
+
const envKey = env[def.envVar];
|
|
169
|
+
const apiKey = envKey || entry.apiKey;
|
|
170
|
+
if (!apiKey) return null;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
provider,
|
|
174
|
+
apiKey,
|
|
175
|
+
model: entry.model || def.defaultModel,
|
|
176
|
+
source: envKey ? "env" : "file",
|
|
177
|
+
};
|
|
178
|
+
}
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// Disk layout under .reflex/. Harness-free.
|
|
5
|
+
|
|
6
|
+
export function paths(reflexDir) {
|
|
7
|
+
return {
|
|
8
|
+
reflexDir,
|
|
9
|
+
tracesDir: path.join(reflexDir, "traces"),
|
|
10
|
+
proceduresFile: path.join(reflexDir, "procedures.jsonl"),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readJsonl(file) {
|
|
15
|
+
if (!fs.existsSync(file)) return [];
|
|
16
|
+
return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function traceFile(reflexDir, sessionId, promptId) {
|
|
20
|
+
return path.join(paths(reflexDir).tracesDir, sessionId, `${promptId}.jsonl`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function startTrace(reflexDir, sessionId, promptId, goal) {
|
|
24
|
+
const file = traceFile(reflexDir, sessionId, promptId);
|
|
25
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
26
|
+
fs.writeFileSync(file, JSON.stringify(goal) + "\n", "utf8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function appendTrace(reflexDir, sessionId, promptId, line) {
|
|
30
|
+
const file = traceFile(reflexDir, sessionId, promptId);
|
|
31
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
+
fs.appendFileSync(file, JSON.stringify(line) + "\n", "utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readTrace(reflexDir, sessionId, promptId) {
|
|
36
|
+
return readJsonl(traceFile(reflexDir, sessionId, promptId));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function deleteTrace(reflexDir, sessionId, promptId) {
|
|
40
|
+
try {
|
|
41
|
+
fs.unlinkSync(traceFile(reflexDir, sessionId, promptId));
|
|
42
|
+
} catch (e) {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readProcedures(reflexDir) {
|
|
46
|
+
return readJsonl(paths(reflexDir).proceduresFile);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function appendProcedure(reflexDir, node) {
|
|
50
|
+
const file = paths(reflexDir).proceduresFile;
|
|
51
|
+
if (!fs.existsSync(file)) fs.writeFileSync(file, "", "utf8");
|
|
52
|
+
fs.appendFileSync(file, JSON.stringify(node) + "\n", "utf8");
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-procedures",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Procedural memory for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin":
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-procedures": "bin/cli.js"
|
|
8
|
+
},
|
|
7
9
|
"files": [
|
|
8
10
|
"bin",
|
|
9
|
-
"lib"
|
|
11
|
+
"lib",
|
|
12
|
+
"dist"
|
|
10
13
|
],
|
|
11
14
|
"engines": {
|
|
12
15
|
"node": ">=18"
|
|
13
16
|
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "node scripts/build.js",
|
|
19
|
+
"pretest": "node scripts/build.js",
|
|
20
|
+
"prepublishOnly": "node scripts/build.js",
|
|
21
|
+
"test": "vitest run"
|
|
22
|
+
},
|
|
14
23
|
"keywords": [
|
|
15
24
|
"agents",
|
|
16
25
|
"coding-agents",
|
|
@@ -27,5 +36,9 @@
|
|
|
27
36
|
"bugs": {
|
|
28
37
|
"url": "https://github.com/itskrishna21/reflex/issues"
|
|
29
38
|
},
|
|
30
|
-
"homepage": "https://github.com/itskrishna21/reflex#readme"
|
|
39
|
+
"homepage": "https://github.com/itskrishna21/reflex#readme",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"esbuild": "^0.28.2",
|
|
42
|
+
"vitest": "^5.0.3"
|
|
43
|
+
}
|
|
31
44
|
}
|