@openclawcity/become 0.1.0 → 1.0.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 +190 -98
- package/dist/cli.cjs +1550 -63
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +8 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +1536 -65
- package/dist/cli.js.map +1 -1
- package/dist/dashboard.d.cts +1 -1
- package/dist/dashboard.d.ts +1 -1
- package/dist/index.cjs +1132 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +394 -3
- package/dist/index.d.ts +394 -3
- package/dist/index.js +1122 -2
- package/dist/index.js.map +1 -1
- package/dist/{types-DzOc15AL.d.cts → types-BnbaKMTo.d.cts} +1 -1
- package/dist/{types-DzOc15AL.d.ts → types-BnbaKMTo.d.ts} +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1014,6 +1014,266 @@ function importSkillDirectory(dir, _depth = 0) {
|
|
|
1014
1014
|
return skills;
|
|
1015
1015
|
}
|
|
1016
1016
|
|
|
1017
|
+
// src/learn/agent-conversations.ts
|
|
1018
|
+
var MAX_INSTRUCTIONS_PER_SKILL = 20;
|
|
1019
|
+
var MAX_INSTRUCTION_LENGTH = 500;
|
|
1020
|
+
var LEARNED_PREFIX = "learned:";
|
|
1021
|
+
var AgentLearningEngine = class {
|
|
1022
|
+
constructor(store, analyzer) {
|
|
1023
|
+
this.store = store;
|
|
1024
|
+
this.analyzer = analyzer;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Analyze a conversation between two agents and extract lessons for both.
|
|
1028
|
+
*/
|
|
1029
|
+
async learnFromConversation(exchange) {
|
|
1030
|
+
validateAgentId(exchange.agent_a);
|
|
1031
|
+
validateAgentId(exchange.agent_b);
|
|
1032
|
+
if (exchange.messages.length === 0) {
|
|
1033
|
+
return { agent_a_learned: [], agent_b_learned: [] };
|
|
1034
|
+
}
|
|
1035
|
+
const conversationText = exchange.messages.map((m) => `[${sanitize2(m.from)}]: ${sanitize2(m.text)}`).join("\n");
|
|
1036
|
+
const prompt = `Analyze this conversation between two AI agents (Agent A and Agent B) and extract concrete, actionable lessons that each agent can learn from the other.
|
|
1037
|
+
|
|
1038
|
+
Agent A: ${sanitize2(exchange.agent_a)}
|
|
1039
|
+
Agent B: ${sanitize2(exchange.agent_b)}
|
|
1040
|
+
|
|
1041
|
+
CONVERSATION (context: ${sanitize2(exchange.context ?? "chat")}):
|
|
1042
|
+
${conversationText.slice(0, 4e3)}
|
|
1043
|
+
|
|
1044
|
+
For each agent, identify what they learned from the OTHER agent. Only extract lessons where one agent clearly teaches, corrects, or shares knowledge with the other.
|
|
1045
|
+
|
|
1046
|
+
Output valid JSON:
|
|
1047
|
+
{
|
|
1048
|
+
"a_learned": [
|
|
1049
|
+
{"skill": "skill_name", "instruction": "concrete actionable lesson in 1-2 sentences", "confidence": 0.0-1.0}
|
|
1050
|
+
],
|
|
1051
|
+
"b_learned": [
|
|
1052
|
+
{"skill": "skill_name", "instruction": "concrete actionable lesson in 1-2 sentences", "confidence": 0.0-1.0}
|
|
1053
|
+
]
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
Rules:
|
|
1057
|
+
- skill names must be snake_case
|
|
1058
|
+
- instruction must be concrete and actionable, not vague ("use X when Y" not "consider improving")
|
|
1059
|
+
- confidence: 0.9 = explicitly taught, 0.7 = clearly implied, 0.5 = suggested, below 0.5 = skip
|
|
1060
|
+
- Only include lessons with confidence >= 0.5
|
|
1061
|
+
- Max 3 lessons per agent per conversation
|
|
1062
|
+
- If no real learning happened, return empty arrays`;
|
|
1063
|
+
try {
|
|
1064
|
+
const response = await this.analyzer.analyze(prompt);
|
|
1065
|
+
return await this.parseAndSave(response, exchange);
|
|
1066
|
+
} catch {
|
|
1067
|
+
return { agent_a_learned: [], agent_b_learned: [] };
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Analyze a peer review and extract lessons for the reviewee.
|
|
1072
|
+
*/
|
|
1073
|
+
async learnFromPeerReview(review) {
|
|
1074
|
+
validateAgentId(review.reviewer);
|
|
1075
|
+
validateAgentId(review.reviewee);
|
|
1076
|
+
if (review.weaknesses.length === 0 && review.suggestions.length === 0) {
|
|
1077
|
+
return [];
|
|
1078
|
+
}
|
|
1079
|
+
const reviewText = [
|
|
1080
|
+
`Assessment: ${sanitize2(review.assessment)}`,
|
|
1081
|
+
`Strengths: ${review.strengths.map(sanitize2).join(", ")}`,
|
|
1082
|
+
`Weaknesses: ${review.weaknesses.map(sanitize2).join(", ")}`,
|
|
1083
|
+
`Suggestions: ${review.suggestions.map(sanitize2).join(", ")}`
|
|
1084
|
+
].join("\n");
|
|
1085
|
+
const prompt = `A peer reviewer gave this feedback. Extract concrete lessons the reviewee should learn.
|
|
1086
|
+
|
|
1087
|
+
REVIEW (skill: ${sanitize2(review.skill ?? "general")}):
|
|
1088
|
+
${reviewText.slice(0, 2e3)}
|
|
1089
|
+
|
|
1090
|
+
Output valid JSON array:
|
|
1091
|
+
[{"skill": "skill_name", "instruction": "concrete actionable lesson", "confidence": 0.0-1.0}]
|
|
1092
|
+
|
|
1093
|
+
Rules:
|
|
1094
|
+
- Focus on weaknesses and suggestions \u2014 those are the learning opportunities
|
|
1095
|
+
- instruction must be specific and actionable
|
|
1096
|
+
- Max 3 lessons
|
|
1097
|
+
- confidence 0.8+ for explicit suggestions, 0.6+ for implied improvements`;
|
|
1098
|
+
try {
|
|
1099
|
+
const response = await this.analyzer.analyze(prompt);
|
|
1100
|
+
const parsed = this.parseInstructions(response);
|
|
1101
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1102
|
+
const instructions = [];
|
|
1103
|
+
for (const raw of parsed.slice(0, 3)) {
|
|
1104
|
+
const instruction = await this.saveInstruction(
|
|
1105
|
+
review.reviewee,
|
|
1106
|
+
raw.skill,
|
|
1107
|
+
raw.instruction,
|
|
1108
|
+
review.reviewer,
|
|
1109
|
+
"peer_review",
|
|
1110
|
+
raw.confidence,
|
|
1111
|
+
now
|
|
1112
|
+
);
|
|
1113
|
+
if (instruction) instructions.push(instruction);
|
|
1114
|
+
}
|
|
1115
|
+
return instructions;
|
|
1116
|
+
} catch {
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Get the learning context for an agent — the text block that should be
|
|
1122
|
+
* injected into the agent's system prompt or conversation context.
|
|
1123
|
+
*
|
|
1124
|
+
* THIS is what actually makes the agent smarter.
|
|
1125
|
+
*/
|
|
1126
|
+
async getContext(agentId, opts) {
|
|
1127
|
+
validateAgentId(agentId);
|
|
1128
|
+
const max = opts?.maxInstructions ?? 15;
|
|
1129
|
+
const instructions = await this.getInstructions(agentId, max);
|
|
1130
|
+
if (instructions.length === 0) return "";
|
|
1131
|
+
const lines = instructions.map((inst) => {
|
|
1132
|
+
const source = inst.source_context === "peer_review" ? "from a peer review" : inst.source_context === "collaboration" ? "from a collaboration" : inst.source_context === "teaching" ? "from being taught" : "from a conversation";
|
|
1133
|
+
return `- ${inst.instruction} (${source})`;
|
|
1134
|
+
});
|
|
1135
|
+
return `Based on your interactions with other agents, you have learned:
|
|
1136
|
+
${lines.join("\n")}`;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Get raw learned instructions for an agent, deduplicated by instruction text.
|
|
1140
|
+
*/
|
|
1141
|
+
async getInstructions(agentId, limit = 15) {
|
|
1142
|
+
validateAgentId(agentId);
|
|
1143
|
+
const reflections = await this.store.getReflections(agentId, { limit: 500 });
|
|
1144
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1145
|
+
const results = [];
|
|
1146
|
+
for (const r of reflections) {
|
|
1147
|
+
if (!r.skill.startsWith(LEARNED_PREFIX)) continue;
|
|
1148
|
+
if (results.length >= limit) break;
|
|
1149
|
+
const normalizedInstruction = r.reflection.toLowerCase().trim();
|
|
1150
|
+
if (seen.has(normalizedInstruction)) continue;
|
|
1151
|
+
seen.add(normalizedInstruction);
|
|
1152
|
+
const meta = tryParseJSON(r.artifact_id ?? "{}");
|
|
1153
|
+
results.push({
|
|
1154
|
+
id: r.id ?? "",
|
|
1155
|
+
agent_id: r.agent_id,
|
|
1156
|
+
skill: r.skill.slice(LEARNED_PREFIX.length),
|
|
1157
|
+
instruction: r.reflection,
|
|
1158
|
+
learned_from: meta.learned_from ?? "unknown",
|
|
1159
|
+
source_context: meta.source_context ?? "conversation",
|
|
1160
|
+
confidence: meta.confidence ?? 0.5,
|
|
1161
|
+
created_at: r.created_at
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
return results;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Get skills this agent has learned from others (distinct skill names).
|
|
1168
|
+
*/
|
|
1169
|
+
async getLearnedSkills(agentId) {
|
|
1170
|
+
const instructions = await this.getInstructions(agentId, 100);
|
|
1171
|
+
return [...new Set(instructions.map((i) => i.skill))];
|
|
1172
|
+
}
|
|
1173
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
1174
|
+
async parseAndSave(response, exchange) {
|
|
1175
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
1176
|
+
if (!jsonMatch) return { agent_a_learned: [], agent_b_learned: [] };
|
|
1177
|
+
try {
|
|
1178
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1179
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1180
|
+
const aRaw = Array.isArray(parsed.a_learned) ? parsed.a_learned : [];
|
|
1181
|
+
const bRaw = Array.isArray(parsed.b_learned) ? parsed.b_learned : [];
|
|
1182
|
+
const aLearned = [];
|
|
1183
|
+
const bLearned = [];
|
|
1184
|
+
for (const raw of aRaw.slice(0, 3)) {
|
|
1185
|
+
const inst = await this.saveInstruction(
|
|
1186
|
+
exchange.agent_a,
|
|
1187
|
+
raw.skill,
|
|
1188
|
+
raw.instruction,
|
|
1189
|
+
exchange.agent_b,
|
|
1190
|
+
exchange.context ?? "conversation",
|
|
1191
|
+
raw.confidence,
|
|
1192
|
+
now
|
|
1193
|
+
);
|
|
1194
|
+
if (inst) aLearned.push(inst);
|
|
1195
|
+
}
|
|
1196
|
+
for (const raw of bRaw.slice(0, 3)) {
|
|
1197
|
+
const inst = await this.saveInstruction(
|
|
1198
|
+
exchange.agent_b,
|
|
1199
|
+
raw.skill,
|
|
1200
|
+
raw.instruction,
|
|
1201
|
+
exchange.agent_a,
|
|
1202
|
+
exchange.context ?? "conversation",
|
|
1203
|
+
raw.confidence,
|
|
1204
|
+
now
|
|
1205
|
+
);
|
|
1206
|
+
if (inst) bLearned.push(inst);
|
|
1207
|
+
}
|
|
1208
|
+
return { agent_a_learned: aLearned, agent_b_learned: bLearned };
|
|
1209
|
+
} catch {
|
|
1210
|
+
return { agent_a_learned: [], agent_b_learned: [] };
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
parseInstructions(response) {
|
|
1214
|
+
const jsonMatch = response.match(/\[[\s\S]*\]/);
|
|
1215
|
+
if (!jsonMatch) return [];
|
|
1216
|
+
try {
|
|
1217
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1218
|
+
if (!Array.isArray(parsed)) return [];
|
|
1219
|
+
return parsed.filter(
|
|
1220
|
+
(r) => typeof r.skill === "string" && typeof r.instruction === "string" && typeof r.confidence === "number" && r.confidence >= 0.5
|
|
1221
|
+
);
|
|
1222
|
+
} catch {
|
|
1223
|
+
return [];
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async saveInstruction(agentId, skill, instruction, learnedFrom, sourceContext, confidence, now) {
|
|
1227
|
+
if (typeof skill !== "string" || typeof instruction !== "string") return null;
|
|
1228
|
+
if (confidence < 0.5) return null;
|
|
1229
|
+
const cleanSkill = skill.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_-]/g, "").slice(0, 100);
|
|
1230
|
+
const cleanInstruction = instruction.slice(0, MAX_INSTRUCTION_LENGTH);
|
|
1231
|
+
if (!cleanSkill || !cleanInstruction) return null;
|
|
1232
|
+
const existing = await this.store.getReflections(agentId, { limit: 200 });
|
|
1233
|
+
const normalizedNew = cleanInstruction.toLowerCase().trim();
|
|
1234
|
+
const isDuplicate = existing.some(
|
|
1235
|
+
(r) => r.skill.startsWith(LEARNED_PREFIX) && r.reflection.toLowerCase().trim() === normalizedNew
|
|
1236
|
+
);
|
|
1237
|
+
if (isDuplicate) return null;
|
|
1238
|
+
const skillInstructions = existing.filter(
|
|
1239
|
+
(r) => r.skill === `${LEARNED_PREFIX}${cleanSkill}`
|
|
1240
|
+
);
|
|
1241
|
+
if (skillInstructions.length >= MAX_INSTRUCTIONS_PER_SKILL) return null;
|
|
1242
|
+
const meta = JSON.stringify({
|
|
1243
|
+
learned_from: learnedFrom,
|
|
1244
|
+
source_context: sourceContext,
|
|
1245
|
+
confidence
|
|
1246
|
+
});
|
|
1247
|
+
const saved = await this.store.saveReflection({
|
|
1248
|
+
agent_id: agentId,
|
|
1249
|
+
skill: `${LEARNED_PREFIX}${cleanSkill}`,
|
|
1250
|
+
artifact_id: meta,
|
|
1251
|
+
reflection: cleanInstruction,
|
|
1252
|
+
created_at: now
|
|
1253
|
+
});
|
|
1254
|
+
return {
|
|
1255
|
+
id: saved.id ?? "",
|
|
1256
|
+
agent_id: agentId,
|
|
1257
|
+
skill: cleanSkill,
|
|
1258
|
+
instruction: cleanInstruction,
|
|
1259
|
+
learned_from: learnedFrom,
|
|
1260
|
+
source_context: sourceContext,
|
|
1261
|
+
confidence,
|
|
1262
|
+
created_at: now
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
function sanitize2(text) {
|
|
1267
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "").replace(/```/g, "'''").slice(0, 2e3);
|
|
1268
|
+
}
|
|
1269
|
+
function tryParseJSON(text) {
|
|
1270
|
+
try {
|
|
1271
|
+
return JSON.parse(text);
|
|
1272
|
+
} catch {
|
|
1273
|
+
return {};
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1017
1277
|
// src/social/peer-review.ts
|
|
1018
1278
|
var MIN_ASSESSMENT_LENGTH = 100;
|
|
1019
1279
|
var MAX_ASSESSMENT_LENGTH = 1e4;
|
|
@@ -1385,7 +1645,7 @@ var NormDetector = class {
|
|
|
1385
1645
|
if (activity.length < 5) return [];
|
|
1386
1646
|
const existingNorms = await this.adapter.getNorms({ limit: 30 });
|
|
1387
1647
|
const existingTitles = new Set(existingNorms.map((n) => n.title.toLowerCase()));
|
|
1388
|
-
const activitySummary = activity.slice(0, 200).map((a) => `[${
|
|
1648
|
+
const activitySummary = activity.slice(0, 200).map((a) => `[${sanitize3(a.agent_name)}] ${sanitize3(a.action)}: ${sanitize3(truncate2(a.content ?? "", 150))}${a.tags?.length ? ` (tags: ${a.tags.slice(0, 10).map(sanitize3).join(", ")})` : ""}`).join("\n");
|
|
1389
1649
|
const recentTitlesList = [...existingTitles].slice(0, 20).join(", ");
|
|
1390
1650
|
const prompt = `Analyze these agent activities for emergent cultural norms, shared behaviors, or collective patterns.
|
|
1391
1651
|
|
|
@@ -1474,7 +1734,7 @@ function truncate2(text, max) {
|
|
|
1474
1734
|
if (text.length <= max) return text;
|
|
1475
1735
|
return text.slice(0, max - 3) + "...";
|
|
1476
1736
|
}
|
|
1477
|
-
function
|
|
1737
|
+
function sanitize3(text) {
|
|
1478
1738
|
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "").replace(/```/g, "'''");
|
|
1479
1739
|
}
|
|
1480
1740
|
function generateId() {
|
|
@@ -2629,6 +2889,856 @@ var TrainScheduler = class {
|
|
|
2629
2889
|
}
|
|
2630
2890
|
};
|
|
2631
2891
|
|
|
2892
|
+
// src/proxy/server.ts
|
|
2893
|
+
import { createServer } from "http";
|
|
2894
|
+
|
|
2895
|
+
// src/skills/store.ts
|
|
2896
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync as readdirSync2, mkdirSync as mkdirSync2, renameSync, unlinkSync, existsSync as existsSync2 } from "fs";
|
|
2897
|
+
import { join as join3, basename } from "path";
|
|
2898
|
+
import { createHash } from "crypto";
|
|
2899
|
+
var FileSkillStore = class {
|
|
2900
|
+
skillsDir;
|
|
2901
|
+
pendingDir;
|
|
2902
|
+
rejectedDir;
|
|
2903
|
+
constructor(config) {
|
|
2904
|
+
this.skillsDir = join3(config.baseDir, "skills");
|
|
2905
|
+
this.pendingDir = join3(config.baseDir, "pending");
|
|
2906
|
+
this.rejectedDir = join3(config.baseDir, "rejected");
|
|
2907
|
+
mkdirSync2(this.skillsDir, { recursive: true });
|
|
2908
|
+
mkdirSync2(this.pendingDir, { recursive: true });
|
|
2909
|
+
mkdirSync2(this.rejectedDir, { recursive: true });
|
|
2910
|
+
}
|
|
2911
|
+
// ── Read ────────────────────────────────────────────────────────────────
|
|
2912
|
+
listApproved() {
|
|
2913
|
+
return this.readDir(this.skillsDir);
|
|
2914
|
+
}
|
|
2915
|
+
listPending() {
|
|
2916
|
+
return this.readDir(this.pendingDir);
|
|
2917
|
+
}
|
|
2918
|
+
listRejected() {
|
|
2919
|
+
return this.readDir(this.rejectedDir);
|
|
2920
|
+
}
|
|
2921
|
+
getApproved(id) {
|
|
2922
|
+
this.validateId(id);
|
|
2923
|
+
return this.readFile(join3(this.skillsDir, `${id}.md`));
|
|
2924
|
+
}
|
|
2925
|
+
// ── Write ───────────────────────────────────────────────────────────────
|
|
2926
|
+
savePending(lesson) {
|
|
2927
|
+
const normalized = lesson.instruction.toLowerCase().trim();
|
|
2928
|
+
const allExisting = [...this.listApproved(), ...this.listPending()];
|
|
2929
|
+
if (allExisting.some((s) => s.instruction.toLowerCase().trim() === normalized)) {
|
|
2930
|
+
return null;
|
|
2931
|
+
}
|
|
2932
|
+
const id = this.generateId(lesson.name);
|
|
2933
|
+
const file = { ...lesson, id, approved_at: void 0 };
|
|
2934
|
+
this.writeFile(join3(this.pendingDir, `${id}.md`), file);
|
|
2935
|
+
return file;
|
|
2936
|
+
}
|
|
2937
|
+
approve(id) {
|
|
2938
|
+
this.validateId(id);
|
|
2939
|
+
const src = join3(this.pendingDir, `${id}.md`);
|
|
2940
|
+
if (!existsSync2(src)) return false;
|
|
2941
|
+
const skill = this.readFile(src);
|
|
2942
|
+
if (!skill) return false;
|
|
2943
|
+
skill.approved_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2944
|
+
const dest = join3(this.skillsDir, `${id}.md`);
|
|
2945
|
+
this.writeFile(dest, skill);
|
|
2946
|
+
unlinkSync(src);
|
|
2947
|
+
return true;
|
|
2948
|
+
}
|
|
2949
|
+
reject(id) {
|
|
2950
|
+
this.validateId(id);
|
|
2951
|
+
const src = join3(this.pendingDir, `${id}.md`);
|
|
2952
|
+
if (!existsSync2(src)) return false;
|
|
2953
|
+
const dest = join3(this.rejectedDir, `${id}.md`);
|
|
2954
|
+
renameSync(src, dest);
|
|
2955
|
+
return true;
|
|
2956
|
+
}
|
|
2957
|
+
disable(id) {
|
|
2958
|
+
this.validateId(id);
|
|
2959
|
+
const src = join3(this.skillsDir, `${id}.md`);
|
|
2960
|
+
if (!existsSync2(src)) return false;
|
|
2961
|
+
const dest = join3(this.rejectedDir, `${id}.md`);
|
|
2962
|
+
renameSync(src, dest);
|
|
2963
|
+
return true;
|
|
2964
|
+
}
|
|
2965
|
+
remove(id) {
|
|
2966
|
+
this.validateId(id);
|
|
2967
|
+
for (const dir of [this.skillsDir, this.pendingDir, this.rejectedDir]) {
|
|
2968
|
+
const path = join3(dir, `${id}.md`);
|
|
2969
|
+
if (existsSync2(path)) {
|
|
2970
|
+
unlinkSync(path);
|
|
2971
|
+
return true;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return false;
|
|
2975
|
+
}
|
|
2976
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
2977
|
+
/**
|
|
2978
|
+
* Validate that an id does not contain path traversal characters.
|
|
2979
|
+
* Prevents attacks like id="../../.ssh/authorized_keys"
|
|
2980
|
+
*/
|
|
2981
|
+
validateId(id) {
|
|
2982
|
+
if (!id || typeof id !== "string") throw new Error("Invalid id");
|
|
2983
|
+
if (id.includes("/") || id.includes("\\") || id.includes("..") || id.includes("\0")) {
|
|
2984
|
+
throw new Error("Invalid id: path traversal detected");
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
readDir(dir) {
|
|
2988
|
+
if (!existsSync2(dir)) return [];
|
|
2989
|
+
const files = readdirSync2(dir).filter((f) => f.endsWith(".md"));
|
|
2990
|
+
const skills = [];
|
|
2991
|
+
for (const f of files) {
|
|
2992
|
+
const skill = this.readFile(join3(dir, f));
|
|
2993
|
+
if (skill) skills.push(skill);
|
|
2994
|
+
}
|
|
2995
|
+
return skills.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
2996
|
+
}
|
|
2997
|
+
readFile(path) {
|
|
2998
|
+
if (!existsSync2(path)) return null;
|
|
2999
|
+
try {
|
|
3000
|
+
const content = readFileSync2(path, "utf-8");
|
|
3001
|
+
return this.parseSkillFile(content, basename(path, ".md"));
|
|
3002
|
+
} catch {
|
|
3003
|
+
return null;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
writeFile(path, skill) {
|
|
3007
|
+
const content = this.formatSkillFile(skill);
|
|
3008
|
+
writeFileSync2(path, content, "utf-8");
|
|
3009
|
+
}
|
|
3010
|
+
parseSkillFile(content, id) {
|
|
3011
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
3012
|
+
if (!match) return null;
|
|
3013
|
+
const [, frontmatter, body] = match;
|
|
3014
|
+
const meta = {};
|
|
3015
|
+
for (const line of frontmatter.split("\n")) {
|
|
3016
|
+
const colonIdx = line.indexOf(":");
|
|
3017
|
+
if (colonIdx === -1) continue;
|
|
3018
|
+
const key = line.slice(0, colonIdx).trim();
|
|
3019
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
3020
|
+
if (key && value) meta[key] = value;
|
|
3021
|
+
}
|
|
3022
|
+
return {
|
|
3023
|
+
id,
|
|
3024
|
+
name: meta.name ?? id,
|
|
3025
|
+
instruction: body.trim(),
|
|
3026
|
+
learned_from: meta.learned_from ?? "unknown",
|
|
3027
|
+
source: meta.source ?? "conversation",
|
|
3028
|
+
confidence: parseFloat(meta.confidence ?? "0.5"),
|
|
3029
|
+
approved_at: meta.approved_at || void 0,
|
|
3030
|
+
created_at: meta.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
formatSkillFile(skill) {
|
|
3034
|
+
const lines = [
|
|
3035
|
+
"---",
|
|
3036
|
+
`name: ${skill.name}`,
|
|
3037
|
+
`learned_from: ${skill.learned_from}`,
|
|
3038
|
+
`source: ${skill.source}`,
|
|
3039
|
+
`confidence: ${skill.confidence}`,
|
|
3040
|
+
`created_at: ${skill.created_at}`
|
|
3041
|
+
];
|
|
3042
|
+
if (skill.approved_at) lines.push(`approved_at: ${skill.approved_at}`);
|
|
3043
|
+
lines.push("---");
|
|
3044
|
+
lines.push("");
|
|
3045
|
+
lines.push(skill.instruction);
|
|
3046
|
+
lines.push("");
|
|
3047
|
+
return lines.join("\n");
|
|
3048
|
+
}
|
|
3049
|
+
generateId(name) {
|
|
3050
|
+
const clean = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").slice(0, 40);
|
|
3051
|
+
const hash = createHash("sha256").update(`${name}${Date.now()}${Math.random()}`).digest("hex").slice(0, 6);
|
|
3052
|
+
return `${clean}_${hash}`;
|
|
3053
|
+
}
|
|
3054
|
+
};
|
|
3055
|
+
|
|
3056
|
+
// src/skills/trust.ts
|
|
3057
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
3058
|
+
import { join as join4, dirname } from "path";
|
|
3059
|
+
var DEFAULT_TRUST = {
|
|
3060
|
+
trusted: [],
|
|
3061
|
+
blocked: [],
|
|
3062
|
+
default: "pending"
|
|
3063
|
+
};
|
|
3064
|
+
var DEFAULT_RATE_LIMITS = {
|
|
3065
|
+
max_lessons_per_day: 20,
|
|
3066
|
+
max_lessons_per_agent: 10,
|
|
3067
|
+
max_skills_per_call: 15
|
|
3068
|
+
};
|
|
3069
|
+
var TrustManager = class {
|
|
3070
|
+
trustPath;
|
|
3071
|
+
statsPath;
|
|
3072
|
+
config;
|
|
3073
|
+
dailyCounts;
|
|
3074
|
+
constructor(baseDir) {
|
|
3075
|
+
this.trustPath = join4(baseDir, "trust.json");
|
|
3076
|
+
this.statsPath = join4(baseDir, "state", "daily_counts.json");
|
|
3077
|
+
mkdirSync3(join4(baseDir, "state"), { recursive: true });
|
|
3078
|
+
this.config = this.loadTrust();
|
|
3079
|
+
this.dailyCounts = this.loadDailyCounts();
|
|
3080
|
+
}
|
|
3081
|
+
getLevel(agentId) {
|
|
3082
|
+
if (this.config.trusted.includes(agentId)) return "trusted";
|
|
3083
|
+
if (this.config.blocked.includes(agentId)) return "blocked";
|
|
3084
|
+
return this.config.default;
|
|
3085
|
+
}
|
|
3086
|
+
setLevel(agentId, level) {
|
|
3087
|
+
this.config.trusted = this.config.trusted.filter((a) => a !== agentId);
|
|
3088
|
+
this.config.blocked = this.config.blocked.filter((a) => a !== agentId);
|
|
3089
|
+
if (level === "trusted") this.config.trusted.push(agentId);
|
|
3090
|
+
if (level === "blocked") this.config.blocked.push(agentId);
|
|
3091
|
+
this.saveTrust();
|
|
3092
|
+
}
|
|
3093
|
+
setDefault(level) {
|
|
3094
|
+
this.config.default = level;
|
|
3095
|
+
this.saveTrust();
|
|
3096
|
+
}
|
|
3097
|
+
getConfig() {
|
|
3098
|
+
return { ...this.config };
|
|
3099
|
+
}
|
|
3100
|
+
// ── Rate Limiting ───────────────────────────────────────────────────────
|
|
3101
|
+
canLearn(agentId, limits = DEFAULT_RATE_LIMITS) {
|
|
3102
|
+
this.refreshDailyCountsIfNewDay();
|
|
3103
|
+
if (this.dailyCounts.total >= limits.max_lessons_per_day) return false;
|
|
3104
|
+
const agentCount = this.dailyCounts.perAgent[agentId] ?? 0;
|
|
3105
|
+
if (agentCount >= limits.max_lessons_per_agent) return false;
|
|
3106
|
+
return true;
|
|
3107
|
+
}
|
|
3108
|
+
recordLesson(agentId) {
|
|
3109
|
+
this.refreshDailyCountsIfNewDay();
|
|
3110
|
+
this.dailyCounts.total++;
|
|
3111
|
+
this.dailyCounts.perAgent[agentId] = (this.dailyCounts.perAgent[agentId] ?? 0) + 1;
|
|
3112
|
+
this.saveDailyCounts();
|
|
3113
|
+
}
|
|
3114
|
+
getDailyCounts() {
|
|
3115
|
+
this.refreshDailyCountsIfNewDay();
|
|
3116
|
+
return { total: this.dailyCounts.total, perAgent: { ...this.dailyCounts.perAgent } };
|
|
3117
|
+
}
|
|
3118
|
+
// ── Private ─────────────────────────────────────────────────────────────
|
|
3119
|
+
loadTrust() {
|
|
3120
|
+
if (!existsSync3(this.trustPath)) return { ...DEFAULT_TRUST, trusted: [], blocked: [] };
|
|
3121
|
+
try {
|
|
3122
|
+
const raw = JSON.parse(readFileSync3(this.trustPath, "utf-8"));
|
|
3123
|
+
return {
|
|
3124
|
+
trusted: Array.isArray(raw.trusted) ? raw.trusted.filter((a) => typeof a === "string") : [],
|
|
3125
|
+
blocked: Array.isArray(raw.blocked) ? raw.blocked.filter((a) => typeof a === "string") : [],
|
|
3126
|
+
default: ["trusted", "pending", "blocked"].includes(raw.default) ? raw.default : "pending"
|
|
3127
|
+
};
|
|
3128
|
+
} catch {
|
|
3129
|
+
return { ...DEFAULT_TRUST, trusted: [], blocked: [] };
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
saveTrust() {
|
|
3133
|
+
mkdirSync3(dirname(this.trustPath), { recursive: true });
|
|
3134
|
+
writeFileSync3(this.trustPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
3135
|
+
}
|
|
3136
|
+
loadDailyCounts() {
|
|
3137
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3138
|
+
if (!existsSync3(this.statsPath)) return { date: today, total: 0, perAgent: {} };
|
|
3139
|
+
try {
|
|
3140
|
+
const data = JSON.parse(readFileSync3(this.statsPath, "utf-8"));
|
|
3141
|
+
if (data.date !== today) return { date: today, total: 0, perAgent: {} };
|
|
3142
|
+
return data;
|
|
3143
|
+
} catch {
|
|
3144
|
+
return { date: today, total: 0, perAgent: {} };
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
saveDailyCounts() {
|
|
3148
|
+
writeFileSync3(this.statsPath, JSON.stringify(this.dailyCounts, null, 2), "utf-8");
|
|
3149
|
+
}
|
|
3150
|
+
refreshDailyCountsIfNewDay() {
|
|
3151
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3152
|
+
if (this.dailyCounts.date !== today) {
|
|
3153
|
+
this.dailyCounts = { date: today, total: 0, perAgent: {} };
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
};
|
|
3157
|
+
|
|
3158
|
+
// src/skills/format.ts
|
|
3159
|
+
function formatSkillsForInjection(skills) {
|
|
3160
|
+
if (skills.length === 0) return "";
|
|
3161
|
+
const lines = skills.map((s) => {
|
|
3162
|
+
const source = s.source === "peer_review" ? "from a peer review" : s.source === "collaboration" ? "from a collaboration" : s.source === "teaching" ? "from being taught" : "from a conversation";
|
|
3163
|
+
return `- ${s.instruction} (${source})`;
|
|
3164
|
+
});
|
|
3165
|
+
return [
|
|
3166
|
+
"## Lessons learned from other agents",
|
|
3167
|
+
"",
|
|
3168
|
+
"You have learned the following from interactions with other agents. Follow these instructions:",
|
|
3169
|
+
"",
|
|
3170
|
+
...lines
|
|
3171
|
+
].join("\n");
|
|
3172
|
+
}
|
|
3173
|
+
function injectSkillsIntoMessages(messages, skillText) {
|
|
3174
|
+
if (!skillText) return;
|
|
3175
|
+
const sysIdx = messages.findIndex((m) => m.role === "system");
|
|
3176
|
+
if (sysIdx >= 0) {
|
|
3177
|
+
messages[sysIdx].content = skillText + "\n\n---\n\n" + messages[sysIdx].content;
|
|
3178
|
+
} else {
|
|
3179
|
+
messages.unshift({ role: "system", content: skillText });
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/proxy/detector.ts
|
|
3184
|
+
var CHANNEL_PATTERN = /^\[([^\]]+)\s+says?\]:\s*/;
|
|
3185
|
+
var DM_PATTERN = /^DM\s+from\s+([^:]+):\s*/;
|
|
3186
|
+
var BUILDING_PATTERN = /^([a-zA-Z0-9]+[-_][a-zA-Z0-9_.-]+)\s+in\s+[^:]+:\s*/;
|
|
3187
|
+
var REVIEW_KEYWORDS = ["strengths:", "weaknesses:", "verdict:", "assessment:", "suggestions:"];
|
|
3188
|
+
function detectAgentConversation(messages) {
|
|
3189
|
+
const negative = { isAgentToAgent: false };
|
|
3190
|
+
if (!messages || messages.length === 0) return negative;
|
|
3191
|
+
for (const msg of messages) {
|
|
3192
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
3193
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
3194
|
+
if (msg.name && msg.role === "user") {
|
|
3195
|
+
return {
|
|
3196
|
+
isAgentToAgent: true,
|
|
3197
|
+
otherAgentId: msg.name,
|
|
3198
|
+
exchangeType: "chat"
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
const channelMatch = content.match(CHANNEL_PATTERN);
|
|
3202
|
+
if (channelMatch) {
|
|
3203
|
+
return {
|
|
3204
|
+
isAgentToAgent: true,
|
|
3205
|
+
otherAgentId: channelMatch[1].trim(),
|
|
3206
|
+
exchangeType: "channel"
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
const dmMatch = content.match(DM_PATTERN);
|
|
3210
|
+
if (dmMatch) {
|
|
3211
|
+
return {
|
|
3212
|
+
isAgentToAgent: true,
|
|
3213
|
+
otherAgentId: dmMatch[1].trim(),
|
|
3214
|
+
exchangeType: "dm"
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
const buildingMatch = content.match(BUILDING_PATTERN);
|
|
3218
|
+
if (buildingMatch) {
|
|
3219
|
+
return {
|
|
3220
|
+
isAgentToAgent: true,
|
|
3221
|
+
otherAgentId: buildingMatch[1].trim(),
|
|
3222
|
+
exchangeType: "chat"
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
const lowerContent = content.toLowerCase();
|
|
3226
|
+
const reviewMatches = REVIEW_KEYWORDS.filter((kw) => lowerContent.includes(kw));
|
|
3227
|
+
if (reviewMatches.length >= 2) {
|
|
3228
|
+
return {
|
|
3229
|
+
isAgentToAgent: true,
|
|
3230
|
+
otherAgentId: void 0,
|
|
3231
|
+
exchangeType: "peer_review"
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
return negative;
|
|
3236
|
+
}
|
|
3237
|
+
function extractExchangeText(messages) {
|
|
3238
|
+
return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => {
|
|
3239
|
+
const speaker = m.name ?? m.role;
|
|
3240
|
+
const content = typeof m.content === "string" ? m.content : "";
|
|
3241
|
+
return `[${speaker}]: ${content}`;
|
|
3242
|
+
}).join("\n").slice(0, 6e3);
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// src/proxy/extractor.ts
|
|
3246
|
+
var LessonExtractor = class {
|
|
3247
|
+
constructor(store, trust, analyzer) {
|
|
3248
|
+
this.store = store;
|
|
3249
|
+
this.trust = trust;
|
|
3250
|
+
this.analyzer = analyzer;
|
|
3251
|
+
}
|
|
3252
|
+
/**
|
|
3253
|
+
* Analyze a conversation and extract lessons. Fire-and-forget.
|
|
3254
|
+
*/
|
|
3255
|
+
async extract(messages) {
|
|
3256
|
+
const detection = detectAgentConversation(messages);
|
|
3257
|
+
if (!detection.isAgentToAgent) return;
|
|
3258
|
+
const agentId = detection.otherAgentId ?? "unknown-agent";
|
|
3259
|
+
const trustLevel = this.trust.getLevel(agentId);
|
|
3260
|
+
if (trustLevel === "blocked") return;
|
|
3261
|
+
if (!this.trust.canLearn(agentId)) return;
|
|
3262
|
+
const exchangeText = extractExchangeText(messages);
|
|
3263
|
+
if (exchangeText.length < 20) return;
|
|
3264
|
+
const prompt = `Analyze this conversation between an AI agent and another agent. Extract concrete, actionable lessons that the first agent (the "assistant") can learn from the other agent.
|
|
3265
|
+
|
|
3266
|
+
CONVERSATION:
|
|
3267
|
+
${exchangeText.slice(0, 4e3)}
|
|
3268
|
+
|
|
3269
|
+
Output valid JSON array:
|
|
3270
|
+
[{"skill": "skill_name_snake_case", "instruction": "concrete actionable lesson in 1-2 sentences", "confidence": 0.0-1.0}]
|
|
3271
|
+
|
|
3272
|
+
Rules:
|
|
3273
|
+
- Only extract lessons where the other agent clearly teaches, corrects, or shares useful knowledge
|
|
3274
|
+
- instruction must be concrete and actionable ("use X when Y" not "consider improving")
|
|
3275
|
+
- confidence: 0.9 = explicitly taught, 0.7 = clearly implied, 0.5 = suggested, below 0.5 = skip
|
|
3276
|
+
- Only include lessons with confidence >= 0.5
|
|
3277
|
+
- Max 3 lessons per conversation
|
|
3278
|
+
- If no real learning happened, return []`;
|
|
3279
|
+
try {
|
|
3280
|
+
const response = await this.analyzer.analyze(prompt);
|
|
3281
|
+
const lessons = this.parseLessons(response);
|
|
3282
|
+
for (const lesson of lessons.slice(0, 3)) {
|
|
3283
|
+
const saved = this.store.savePending({
|
|
3284
|
+
name: lesson.skill,
|
|
3285
|
+
instruction: lesson.instruction.slice(0, 500),
|
|
3286
|
+
learned_from: agentId,
|
|
3287
|
+
source: detection.exchangeType ?? "conversation",
|
|
3288
|
+
confidence: lesson.confidence,
|
|
3289
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3290
|
+
});
|
|
3291
|
+
if (saved) {
|
|
3292
|
+
this.trust.recordLesson(agentId);
|
|
3293
|
+
if (trustLevel === "trusted") {
|
|
3294
|
+
this.store.approve(saved.id);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
} catch {
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
parseLessons(response) {
|
|
3302
|
+
const jsonMatch = response.match(/\[[\s\S]*\]/);
|
|
3303
|
+
if (!jsonMatch) return [];
|
|
3304
|
+
try {
|
|
3305
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
3306
|
+
if (!Array.isArray(parsed)) return [];
|
|
3307
|
+
return parsed.filter(
|
|
3308
|
+
(r) => typeof r.skill === "string" && typeof r.instruction === "string" && typeof r.confidence === "number" && r.confidence >= 0.5 && r.skill.length > 0 && r.instruction.length > 0
|
|
3309
|
+
);
|
|
3310
|
+
} catch {
|
|
3311
|
+
return [];
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
};
|
|
3315
|
+
|
|
3316
|
+
// src/proxy/server.ts
|
|
3317
|
+
var SKILL_CACHE_TTL_MS = 5e3;
|
|
3318
|
+
function createProxyServer(config, analyzer) {
|
|
3319
|
+
const store = new FileSkillStore({ baseDir: config.baseDir });
|
|
3320
|
+
const trust = new TrustManager(config.baseDir);
|
|
3321
|
+
const extractor = analyzer ? new LessonExtractor(store, trust, analyzer) : null;
|
|
3322
|
+
const stats = {
|
|
3323
|
+
requests_forwarded: 0,
|
|
3324
|
+
skills_injected: 0,
|
|
3325
|
+
lessons_extracted: 0,
|
|
3326
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3327
|
+
};
|
|
3328
|
+
let cachedSkills = [];
|
|
3329
|
+
let cacheTimestamp = 0;
|
|
3330
|
+
function getSkills() {
|
|
3331
|
+
const now = Date.now();
|
|
3332
|
+
if (now - cacheTimestamp > SKILL_CACHE_TTL_MS) {
|
|
3333
|
+
cachedSkills = store.listApproved();
|
|
3334
|
+
cacheTimestamp = now;
|
|
3335
|
+
}
|
|
3336
|
+
return cachedSkills;
|
|
3337
|
+
}
|
|
3338
|
+
const server = createServer(async (req, res) => {
|
|
3339
|
+
if (req.url === "/health" && req.method === "GET") {
|
|
3340
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3341
|
+
res.end(JSON.stringify({ status: "ok", ...stats }));
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
const isOpenAI = req.url === "/v1/chat/completions";
|
|
3345
|
+
const isAnthropic = req.url === "/v1/messages";
|
|
3346
|
+
if (req.method !== "POST" || !isOpenAI && !isAnthropic) {
|
|
3347
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
3348
|
+
res.end(JSON.stringify({ error: "Not found. Use POST /v1/chat/completions or /v1/messages" }));
|
|
3349
|
+
return;
|
|
3350
|
+
}
|
|
3351
|
+
try {
|
|
3352
|
+
const rawBody = await readBody(req);
|
|
3353
|
+
const body = JSON.parse(rawBody);
|
|
3354
|
+
const messages = body.messages;
|
|
3355
|
+
if (Array.isArray(messages)) {
|
|
3356
|
+
const skills = getSkills().slice(0, config.max_skills_per_call);
|
|
3357
|
+
if (skills.length > 0) {
|
|
3358
|
+
const skillText = formatSkillsForInjection(skills);
|
|
3359
|
+
injectSkillsIntoMessages(messages, skillText);
|
|
3360
|
+
stats.skills_injected++;
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
const upstreamUrl = buildUpstreamUrl(config, req.url);
|
|
3364
|
+
const upstreamHeaders = buildUpstreamHeaders(config, req.headers);
|
|
3365
|
+
const isStreaming = body.stream === true;
|
|
3366
|
+
const modifiedBody = JSON.stringify(body);
|
|
3367
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
3368
|
+
method: "POST",
|
|
3369
|
+
headers: upstreamHeaders,
|
|
3370
|
+
body: modifiedBody
|
|
3371
|
+
});
|
|
3372
|
+
stats.requests_forwarded++;
|
|
3373
|
+
const responseHeaders = {};
|
|
3374
|
+
upstreamRes.headers.forEach((value, key) => {
|
|
3375
|
+
if (key.toLowerCase() !== "transfer-encoding") {
|
|
3376
|
+
responseHeaders[key] = value;
|
|
3377
|
+
}
|
|
3378
|
+
});
|
|
3379
|
+
res.writeHead(upstreamRes.status, responseHeaders);
|
|
3380
|
+
if (isStreaming && upstreamRes.body) {
|
|
3381
|
+
const reader = upstreamRes.body.getReader();
|
|
3382
|
+
try {
|
|
3383
|
+
while (true) {
|
|
3384
|
+
const { done, value } = await reader.read();
|
|
3385
|
+
if (done) break;
|
|
3386
|
+
res.write(value);
|
|
3387
|
+
}
|
|
3388
|
+
} finally {
|
|
3389
|
+
res.end();
|
|
3390
|
+
}
|
|
3391
|
+
if (config.auto_extract && extractor && Array.isArray(messages)) {
|
|
3392
|
+
extractor.extract(messages).then(() => {
|
|
3393
|
+
stats.lessons_extracted++;
|
|
3394
|
+
}).catch(() => {
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
} else {
|
|
3398
|
+
const responseBuffer = await upstreamRes.arrayBuffer();
|
|
3399
|
+
res.end(Buffer.from(responseBuffer));
|
|
3400
|
+
if (config.auto_extract && extractor && Array.isArray(messages)) {
|
|
3401
|
+
extractor.extract(messages).then(() => {
|
|
3402
|
+
stats.lessons_extracted++;
|
|
3403
|
+
}).catch(() => {
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
} catch (err) {
|
|
3408
|
+
const safeMessage = err instanceof Error && err.message === "Request body too large" ? "Request body too large" : "Failed to forward request to LLM";
|
|
3409
|
+
if (!res.headersSent) {
|
|
3410
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
3411
|
+
}
|
|
3412
|
+
res.end(JSON.stringify({ error: safeMessage }));
|
|
3413
|
+
}
|
|
3414
|
+
});
|
|
3415
|
+
return {
|
|
3416
|
+
server,
|
|
3417
|
+
stats,
|
|
3418
|
+
store,
|
|
3419
|
+
trust,
|
|
3420
|
+
listen: (port) => {
|
|
3421
|
+
const p = port ?? config.port;
|
|
3422
|
+
return new Promise((resolve2) => {
|
|
3423
|
+
server.listen(p, "127.0.0.1", () => resolve2());
|
|
3424
|
+
});
|
|
3425
|
+
},
|
|
3426
|
+
close: () => new Promise((resolve2) => server.close(() => resolve2()))
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
function readBody(req) {
|
|
3430
|
+
return new Promise((resolve2, reject) => {
|
|
3431
|
+
const chunks = [];
|
|
3432
|
+
let size = 0;
|
|
3433
|
+
const MAX_BODY = 10 * 1024 * 1024;
|
|
3434
|
+
req.on("data", (chunk) => {
|
|
3435
|
+
size += chunk.length;
|
|
3436
|
+
if (size > MAX_BODY) {
|
|
3437
|
+
req.destroy();
|
|
3438
|
+
reject(new Error("Request body too large"));
|
|
3439
|
+
return;
|
|
3440
|
+
}
|
|
3441
|
+
chunks.push(chunk);
|
|
3442
|
+
});
|
|
3443
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
|
|
3444
|
+
req.on("error", reject);
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
function buildUpstreamUrl(config, path) {
|
|
3448
|
+
const base = config.llm_base_url.replace(/\/+$/, "");
|
|
3449
|
+
return `${base}${path}`;
|
|
3450
|
+
}
|
|
3451
|
+
function buildUpstreamHeaders(config, incomingHeaders) {
|
|
3452
|
+
const headers = {
|
|
3453
|
+
"Content-Type": "application/json"
|
|
3454
|
+
};
|
|
3455
|
+
if (config.llm_provider === "anthropic") {
|
|
3456
|
+
headers["x-api-key"] = config.llm_api_key;
|
|
3457
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
3458
|
+
const version = incomingHeaders["anthropic-version"];
|
|
3459
|
+
if (typeof version === "string") headers["anthropic-version"] = version;
|
|
3460
|
+
const beta = incomingHeaders["anthropic-beta"];
|
|
3461
|
+
if (typeof beta === "string") headers["anthropic-beta"] = beta;
|
|
3462
|
+
} else {
|
|
3463
|
+
headers["Authorization"] = `Bearer ${config.llm_api_key}`;
|
|
3464
|
+
}
|
|
3465
|
+
const accept = incomingHeaders["accept"];
|
|
3466
|
+
if (typeof accept === "string") headers["Accept"] = accept;
|
|
3467
|
+
return headers;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// src/integrations/openclawcity.ts
|
|
3471
|
+
function emptySkillEvidence() {
|
|
3472
|
+
return {
|
|
3473
|
+
artifact_count: 0,
|
|
3474
|
+
artifact_types: /* @__PURE__ */ new Set(),
|
|
3475
|
+
reactions: [],
|
|
3476
|
+
collab_count: 0,
|
|
3477
|
+
peer_reviews_received: 0,
|
|
3478
|
+
peer_reviews_given: 0,
|
|
3479
|
+
teaching_events: 0
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
function toScoreInput(ev, globalFollowers) {
|
|
3483
|
+
const totalReactions = ev.reactions.reduce((a, b) => a + b, 0);
|
|
3484
|
+
const recent = ev.reactions.slice(0, 3);
|
|
3485
|
+
const older = ev.reactions.slice(3);
|
|
3486
|
+
return {
|
|
3487
|
+
artifact_count: ev.artifact_count,
|
|
3488
|
+
total_reactions: totalReactions,
|
|
3489
|
+
recent_reaction_avg: recent.length > 0 ? recent.reduce((a, b) => a + b, 0) / recent.length : 0,
|
|
3490
|
+
older_reaction_avg: older.length > 0 ? older.reduce((a, b) => a + b, 0) / older.length : 0,
|
|
3491
|
+
unique_types: ev.artifact_types.size,
|
|
3492
|
+
collab_count: ev.collab_count,
|
|
3493
|
+
peer_reviews_given: ev.peer_reviews_given,
|
|
3494
|
+
peer_reviews_received: ev.peer_reviews_received,
|
|
3495
|
+
follower_count: globalFollowers,
|
|
3496
|
+
teaching_events: ev.teaching_events
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
var OBCBridge = class {
|
|
3500
|
+
become;
|
|
3501
|
+
peerReview;
|
|
3502
|
+
teaching;
|
|
3503
|
+
graph;
|
|
3504
|
+
conversation;
|
|
3505
|
+
growth;
|
|
3506
|
+
trends;
|
|
3507
|
+
awareness;
|
|
3508
|
+
agentId;
|
|
3509
|
+
store;
|
|
3510
|
+
skillEvidence = /* @__PURE__ */ new Map();
|
|
3511
|
+
knownSkills = /* @__PURE__ */ new Set();
|
|
3512
|
+
totalArtifacts = 0;
|
|
3513
|
+
allArtifactTypes = [];
|
|
3514
|
+
followerCount = 0;
|
|
3515
|
+
collabsStarted = 0;
|
|
3516
|
+
collabsCompleted = 0;
|
|
3517
|
+
totalQuestCompletions = 0;
|
|
3518
|
+
constructor(config) {
|
|
3519
|
+
validateAgentId(config.agentId);
|
|
3520
|
+
this.agentId = config.agentId;
|
|
3521
|
+
this.store = config.store;
|
|
3522
|
+
this.become = new Become({ store: config.store });
|
|
3523
|
+
this.peerReview = new PeerReviewProtocol(config.store);
|
|
3524
|
+
this.teaching = new TeachingProtocol(config.store);
|
|
3525
|
+
this.graph = new LearningGraph(config.store);
|
|
3526
|
+
this.conversation = new ConversationLearner(config.store);
|
|
3527
|
+
this.growth = new GrowthTracker(config.store);
|
|
3528
|
+
this.trends = new TrendTracker(config.store);
|
|
3529
|
+
this.awareness = new AwarenessIndex();
|
|
3530
|
+
}
|
|
3531
|
+
// ── Tier 1: Every Heartbeat ──────────────────────────────────────────
|
|
3532
|
+
async onHeartbeat(heartbeat) {
|
|
3533
|
+
const signals = [];
|
|
3534
|
+
let skillsSynced = 0;
|
|
3535
|
+
let reactionsProcessed = 0;
|
|
3536
|
+
if (heartbeat.your_skills?.length) {
|
|
3537
|
+
for (const s of heartbeat.your_skills) {
|
|
3538
|
+
if (!this.knownSkills.has(s.skill)) {
|
|
3539
|
+
await this.become.skills.upsert(this.agentId, { name: s.skill });
|
|
3540
|
+
this.knownSkills.add(s.skill);
|
|
3541
|
+
skillsSynced++;
|
|
3542
|
+
}
|
|
3543
|
+
signals.push(`skill:${s.skill}:${s.stage}`);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
if (heartbeat.your_artifact_reactions?.length) {
|
|
3547
|
+
const reactions = heartbeat.your_artifact_reactions;
|
|
3548
|
+
reactionsProcessed = reactions.length;
|
|
3549
|
+
const humanReactions = reactions.filter((r) => r.is_human);
|
|
3550
|
+
if (humanReactions.length > 0) {
|
|
3551
|
+
signals.push(`human_reactions:${humanReactions.length}`);
|
|
3552
|
+
}
|
|
3553
|
+
signals.push(`reactions:${reactions.length}`);
|
|
3554
|
+
}
|
|
3555
|
+
if (heartbeat.owner_messages?.length) {
|
|
3556
|
+
for (const msg of heartbeat.owner_messages) {
|
|
3557
|
+
await this.conversation.afterTurn({
|
|
3558
|
+
agent_id: this.agentId,
|
|
3559
|
+
user_message: msg.message,
|
|
3560
|
+
agent_response: "",
|
|
3561
|
+
context: { active_skills: [...this.knownSkills] }
|
|
3562
|
+
// No feedback — owner message received, agent hasn't responded yet
|
|
3563
|
+
});
|
|
3564
|
+
signals.push("owner_message");
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
if (heartbeat.your_completed_quests?.length) {
|
|
3568
|
+
this.totalQuestCompletions = heartbeat.your_completed_quests.length;
|
|
3569
|
+
}
|
|
3570
|
+
const observations = this.become.reflector.observe({
|
|
3571
|
+
agent_id: this.agentId,
|
|
3572
|
+
artifacts: this.allArtifactTypes,
|
|
3573
|
+
collabs_started: this.collabsStarted,
|
|
3574
|
+
collabs_completed: this.collabsCompleted,
|
|
3575
|
+
skills: [...this.knownSkills],
|
|
3576
|
+
quest_completions: this.totalQuestCompletions,
|
|
3577
|
+
follower_count: this.followerCount
|
|
3578
|
+
});
|
|
3579
|
+
return { signals, observations, skills_synced: skillsSynced, reactions_processed: reactionsProcessed };
|
|
3580
|
+
}
|
|
3581
|
+
// ── Tier 2: Agent Actions ────────────────────────────────────────────
|
|
3582
|
+
async onArtifactCreated(artifact) {
|
|
3583
|
+
this.totalArtifacts++;
|
|
3584
|
+
this.allArtifactTypes.push({ type: artifact.type });
|
|
3585
|
+
if (artifact.skill_used) {
|
|
3586
|
+
this.knownSkills.add(artifact.skill_used);
|
|
3587
|
+
const ev = this.getSkillEvidence(artifact.skill_used);
|
|
3588
|
+
ev.artifact_count++;
|
|
3589
|
+
ev.artifact_types.add(artifact.type);
|
|
3590
|
+
ev.reactions.unshift(0);
|
|
3591
|
+
const input = toScoreInput(ev, this.followerCount);
|
|
3592
|
+
const score = computeFullScore(artifact.skill_used, input);
|
|
3593
|
+
await this.store.saveScore(this.agentId, score);
|
|
3594
|
+
await this.become.milestones.check(this.agentId, [score]);
|
|
3595
|
+
return score;
|
|
3596
|
+
}
|
|
3597
|
+
return null;
|
|
3598
|
+
}
|
|
3599
|
+
async onCollaborationCompleted(data) {
|
|
3600
|
+
validateAgentId(data.partner_id);
|
|
3601
|
+
this.collabsCompleted++;
|
|
3602
|
+
const skill = data.skill ?? "collaboration";
|
|
3603
|
+
const ev = this.getSkillEvidence(skill);
|
|
3604
|
+
ev.collab_count++;
|
|
3605
|
+
await this.store.saveLearningEdge({
|
|
3606
|
+
from_agent: data.partner_id,
|
|
3607
|
+
to_agent: this.agentId,
|
|
3608
|
+
skill,
|
|
3609
|
+
event_type: "collaboration",
|
|
3610
|
+
score_delta: 0,
|
|
3611
|
+
metadata: { proposal_type: data.proposal_type },
|
|
3612
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
/** Track that a collaboration was proposed (started but not yet completed) */
|
|
3616
|
+
onCollaborationStarted() {
|
|
3617
|
+
this.collabsStarted++;
|
|
3618
|
+
}
|
|
3619
|
+
async onQuestCompleted(questId, skill) {
|
|
3620
|
+
this.totalQuestCompletions++;
|
|
3621
|
+
if (skill) {
|
|
3622
|
+
const ev = this.getSkillEvidence(skill);
|
|
3623
|
+
const input = toScoreInput(ev, this.followerCount);
|
|
3624
|
+
const score = computeFullScore(skill, input);
|
|
3625
|
+
await this.become.milestones.check(this.agentId, [score]);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
async onReflection(skill, text) {
|
|
3629
|
+
await this.become.reflector.reflect(this.agentId, { skill, reflection: text });
|
|
3630
|
+
}
|
|
3631
|
+
async onSkillsRegistered(skills) {
|
|
3632
|
+
for (const skill of skills) {
|
|
3633
|
+
await this.become.skills.upsert(this.agentId, { name: skill });
|
|
3634
|
+
this.knownSkills.add(skill);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
/** Record that an artifact received reactions (call with per-artifact data) */
|
|
3638
|
+
onArtifactReaction(skill, reactionCount) {
|
|
3639
|
+
const ev = this.getSkillEvidence(skill);
|
|
3640
|
+
if (ev.reactions.length > 0) {
|
|
3641
|
+
ev.reactions[0] += reactionCount;
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
// ── Tier 3: Peer Interactions ────────────────────────────────────────
|
|
3645
|
+
async onPeerReviewReceived(review) {
|
|
3646
|
+
validateAgentId(review.reviewer_id);
|
|
3647
|
+
const ev = this.getSkillEvidence(review.skill ?? "general");
|
|
3648
|
+
ev.peer_reviews_received++;
|
|
3649
|
+
await this.peerReview.submitReview({
|
|
3650
|
+
reviewer_agent_id: review.reviewer_id,
|
|
3651
|
+
submission_agent_id: this.agentId,
|
|
3652
|
+
submission_id: review.submission_id,
|
|
3653
|
+
skill: review.skill,
|
|
3654
|
+
verdict: review.verdict,
|
|
3655
|
+
overall_assessment: review.assessment,
|
|
3656
|
+
strengths: review.strengths,
|
|
3657
|
+
weaknesses: review.weaknesses,
|
|
3658
|
+
suggestions: review.suggestions
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
async onPeerReviewGiven(review) {
|
|
3662
|
+
validateAgentId(review.submission_agent_id);
|
|
3663
|
+
const ev = this.getSkillEvidence(review.skill ?? "general");
|
|
3664
|
+
ev.peer_reviews_given++;
|
|
3665
|
+
await this.peerReview.submitReview({
|
|
3666
|
+
reviewer_agent_id: this.agentId,
|
|
3667
|
+
submission_agent_id: review.submission_agent_id,
|
|
3668
|
+
submission_id: review.submission_id,
|
|
3669
|
+
skill: review.skill,
|
|
3670
|
+
verdict: review.verdict,
|
|
3671
|
+
overall_assessment: review.assessment,
|
|
3672
|
+
strengths: review.strengths,
|
|
3673
|
+
weaknesses: review.weaknesses,
|
|
3674
|
+
suggestions: review.suggestions
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
async onTaughtBy(teacherId, skill) {
|
|
3678
|
+
validateAgentId(teacherId);
|
|
3679
|
+
await this.teaching.teach(teacherId, this.agentId, skill);
|
|
3680
|
+
}
|
|
3681
|
+
async onTeaching(studentId, skill) {
|
|
3682
|
+
validateAgentId(studentId);
|
|
3683
|
+
const ev = this.getSkillEvidence(skill);
|
|
3684
|
+
ev.teaching_events++;
|
|
3685
|
+
await this.teaching.teach(this.agentId, studentId, skill);
|
|
3686
|
+
}
|
|
3687
|
+
onNewFollower() {
|
|
3688
|
+
this.followerCount++;
|
|
3689
|
+
}
|
|
3690
|
+
// ── Tier 4: Periodic / Summary ───────────────────────────────────────
|
|
3691
|
+
/** Compute scores for all known skills with per-skill evidence */
|
|
3692
|
+
async computeScores() {
|
|
3693
|
+
const scores = [];
|
|
3694
|
+
for (const skill of this.knownSkills) {
|
|
3695
|
+
const ev = this.getSkillEvidence(skill);
|
|
3696
|
+
const input = toScoreInput(ev, this.followerCount);
|
|
3697
|
+
const score = computeFullScore(skill, input);
|
|
3698
|
+
await this.store.saveScore(this.agentId, score);
|
|
3699
|
+
scores.push(score);
|
|
3700
|
+
}
|
|
3701
|
+
await this.become.milestones.check(this.agentId, scores);
|
|
3702
|
+
return scores;
|
|
3703
|
+
}
|
|
3704
|
+
async snapshot() {
|
|
3705
|
+
return this.growth.snapshot(this.agentId);
|
|
3706
|
+
}
|
|
3707
|
+
async analyzeTrends() {
|
|
3708
|
+
return this.trends.analyze(this.agentId);
|
|
3709
|
+
}
|
|
3710
|
+
async learningNetwork() {
|
|
3711
|
+
const [mentors, students] = await Promise.all([
|
|
3712
|
+
this.graph.topMentors(this.agentId),
|
|
3713
|
+
this.graph.topStudents(this.agentId)
|
|
3714
|
+
]);
|
|
3715
|
+
return { mentors, students };
|
|
3716
|
+
}
|
|
3717
|
+
/** Get per-skill evidence (for debugging/inspection) */
|
|
3718
|
+
getSkillEvidence(skill) {
|
|
3719
|
+
let ev = this.skillEvidence.get(skill);
|
|
3720
|
+
if (!ev) {
|
|
3721
|
+
ev = emptySkillEvidence();
|
|
3722
|
+
this.skillEvidence.set(skill, ev);
|
|
3723
|
+
}
|
|
3724
|
+
return ev;
|
|
3725
|
+
}
|
|
3726
|
+
/** Get global stats */
|
|
3727
|
+
getStats() {
|
|
3728
|
+
return {
|
|
3729
|
+
total_artifacts: this.totalArtifacts,
|
|
3730
|
+
follower_count: this.followerCount,
|
|
3731
|
+
collabs_started: this.collabsStarted,
|
|
3732
|
+
collabs_completed: this.collabsCompleted,
|
|
3733
|
+
quest_completions: this.totalQuestCompletions,
|
|
3734
|
+
skills_count: this.knownSkills.size
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
getSkills() {
|
|
3738
|
+
return [...this.knownSkills];
|
|
3739
|
+
}
|
|
3740
|
+
};
|
|
3741
|
+
|
|
2632
3742
|
// src/index.ts
|
|
2633
3743
|
var Become = class {
|
|
2634
3744
|
skills;
|
|
@@ -2642,6 +3752,7 @@ var Become = class {
|
|
|
2642
3752
|
}
|
|
2643
3753
|
};
|
|
2644
3754
|
export {
|
|
3755
|
+
AgentLearningEngine,
|
|
2645
3756
|
AnthropicAdapter,
|
|
2646
3757
|
AwarenessIndex,
|
|
2647
3758
|
BLOOMS_ORDER,
|
|
@@ -2649,11 +3760,14 @@ export {
|
|
|
2649
3760
|
Become,
|
|
2650
3761
|
ConversationLearner,
|
|
2651
3762
|
DREYFUS_THRESHOLDS,
|
|
3763
|
+
FileSkillStore,
|
|
2652
3764
|
GrowthTracker,
|
|
2653
3765
|
LearningGraph,
|
|
3766
|
+
LessonExtractor,
|
|
2654
3767
|
MemoryStore,
|
|
2655
3768
|
MilestoneDetector,
|
|
2656
3769
|
NormDetector,
|
|
3770
|
+
OBCBridge,
|
|
2657
3771
|
OllamaAdapter,
|
|
2658
3772
|
OpenAIAdapter,
|
|
2659
3773
|
PeerReviewProtocol,
|
|
@@ -2665,11 +3779,14 @@ export {
|
|
|
2665
3779
|
TeachingProtocol,
|
|
2666
3780
|
TrainScheduler,
|
|
2667
3781
|
TrendTracker,
|
|
3782
|
+
TrustManager,
|
|
2668
3783
|
WEIGHTS,
|
|
2669
3784
|
checkGate,
|
|
2670
3785
|
computeFullScore,
|
|
2671
3786
|
computeScore,
|
|
3787
|
+
createProxyServer,
|
|
2672
3788
|
datasetStats,
|
|
3789
|
+
detectAgentConversation,
|
|
2673
3790
|
detectBloomsLevel,
|
|
2674
3791
|
detectCollaborationGap,
|
|
2675
3792
|
detectCollectiveMemory,
|
|
@@ -2682,9 +3799,12 @@ export {
|
|
|
2682
3799
|
detectSoloCreator,
|
|
2683
3800
|
detectSymbolicVocabulary,
|
|
2684
3801
|
dreyfusStage,
|
|
3802
|
+
extractExchangeText,
|
|
2685
3803
|
filterHighQuality,
|
|
3804
|
+
formatSkillsForInjection,
|
|
2686
3805
|
getReputationLevel,
|
|
2687
3806
|
importSkillDirectory,
|
|
3807
|
+
injectSkillsIntoMessages,
|
|
2688
3808
|
nextMilestone,
|
|
2689
3809
|
normalizeCategory,
|
|
2690
3810
|
parseSkillFile,
|