brainiverse 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +228 -2
- package/assets/brainiverse-intro.svg +120 -0
- package/assets/brainiverse.svg +85 -0
- package/bin/brainiverse.mjs +102 -0
- package/package.json +30 -4
- package/skill/brainiverse/SKILL.md +105 -0
- package/skill/brainiverse/agents/analyst.md +22 -0
- package/skill/brainiverse/agents/challenger.md +23 -0
- package/skill/brainiverse/agents/communicator.md +18 -0
- package/skill/brainiverse/agents/creative-thinker.md +20 -0
- package/skill/brainiverse/agents/executor.md +23 -0
- package/skill/brainiverse/agents/leader.md +39 -0
- package/skill/brainiverse/agents/organizer.md +18 -0
- package/skill/brainiverse/agents/quality-reviewer.md +24 -0
- package/skill/brainiverse/agents/researcher.md +25 -0
- package/skill/brainiverse/agents/specialist.md +19 -0
- package/skill/brainiverse/bin/brainiverse.mjs +5 -0
- package/skill/brainiverse/docs/architecture.md +73 -0
- package/skill/brainiverse/docs/cli.md +628 -0
- package/skill/brainiverse/docs/customization.md +86 -0
- package/skill/brainiverse/docs/examples.md +96 -0
- package/skill/brainiverse/docs/limitations.md +38 -0
- package/skill/brainiverse/meetings/meeting-protocol.md +90 -0
- package/skill/brainiverse/runtime/args.mjs +87 -0
- package/skill/brainiverse/runtime/audit.mjs +26 -0
- package/skill/brainiverse/runtime/briefs.mjs +139 -0
- package/skill/brainiverse/runtime/cli.mjs +859 -0
- package/skill/brainiverse/runtime/data/archetypes.json +165 -0
- package/skill/brainiverse/runtime/data/domains.json +628 -0
- package/skill/brainiverse/runtime/data/templates/app-builders.json +25 -0
- package/skill/brainiverse/runtime/data/templates/discovery-crew.json +24 -0
- package/skill/brainiverse/runtime/data/templates/travel-squad.json +25 -0
- package/skill/brainiverse/runtime/data/terminology.json +18 -0
- package/skill/brainiverse/runtime/data/themes.json +50 -0
- package/skill/brainiverse/runtime/evaluation.mjs +58 -0
- package/skill/brainiverse/runtime/meetings.mjs +224 -0
- package/skill/brainiverse/runtime/memory.mjs +115 -0
- package/skill/brainiverse/runtime/orchestrator.mjs +385 -0
- package/skill/brainiverse/runtime/permissions.mjs +67 -0
- package/skill/brainiverse/runtime/registry.mjs +230 -0
- package/skill/brainiverse/runtime/skills.mjs +204 -0
- package/skill/brainiverse/runtime/store.mjs +216 -0
- package/skill/brainiverse/runtime/teams.mjs +272 -0
- package/skill/brainiverse/runtime/tools.mjs +74 -0
- package/skill/brainiverse/teams/app-builders.md +24 -0
- package/skill/brainiverse/teams/discovery-crew.md +22 -0
- package/skill/brainiverse/teams/travel-squad.md +26 -0
- package/skill/brainiverse/workflows/collaboration.md +26 -0
- package/skill/brainiverse/workflows/mission-intake.md +35 -0
- package/skill/brainiverse/workflows/orchestration.md +59 -0
- package/skill/brainiverse/workflows/skill-discovery.md +61 -0
- package/skill/brainiverse/workflows/team-creation.md +41 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"themed": {
|
|
3
|
+
"team": "Squad",
|
|
4
|
+
"project": "Mission",
|
|
5
|
+
"meeting": "War Room",
|
|
6
|
+
"invite": "Call in a specialist",
|
|
7
|
+
"task": "Mission assignment",
|
|
8
|
+
"final_review": "Mission debrief"
|
|
9
|
+
},
|
|
10
|
+
"professional": {
|
|
11
|
+
"team": "Team",
|
|
12
|
+
"project": "Project",
|
|
13
|
+
"meeting": "Meeting",
|
|
14
|
+
"invite": "Invite an agent",
|
|
15
|
+
"task": "Task",
|
|
16
|
+
"final_review": "Final review"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"heroes": {
|
|
3
|
+
"description": "Default: original superhero-style crew names, safe to share publicly.",
|
|
4
|
+
"terminology": "themed",
|
|
5
|
+
"identities": {
|
|
6
|
+
"leader": {"name": "Captain", "personality": "Confident, strategic, decisive", "avatar": "🛡️"},
|
|
7
|
+
"researcher": {"name": "Lumen", "personality": "Curious, insightful, investigative", "avatar": "🔭"},
|
|
8
|
+
"creative_thinker": {"name": "Spark", "personality": "Inventive, witty, unconventional", "avatar": "⚡"},
|
|
9
|
+
"analyst": {"name": "Quant", "personality": "Thoughtful, analytical, evidence-driven", "avatar": "📊"},
|
|
10
|
+
"executor": {"name": "Forge", "personality": "Practical, resourceful, action-oriented", "avatar": "🔨"},
|
|
11
|
+
"challenger": {"name": "Riddle", "personality": "Questioning, unpredictable, skeptical", "avatar": "🃏"},
|
|
12
|
+
"specialist": {"name": "Sage", "personality": "Knowledgeable, precise, exploratory", "avatar": "🧭"},
|
|
13
|
+
"communicator": {"name": "Echo", "personality": "Diplomatic, trustworthy, persuasive", "avatar": "📣"},
|
|
14
|
+
"organizer": {"name": "Atlas", "personality": "Organized, discreet, coordinated", "avatar": "🗂️"},
|
|
15
|
+
"quality_reviewer": {"name": "Sentinel", "personality": "Observant, precise, detail-oriented", "avatar": "🎯"}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"classic": {
|
|
19
|
+
"description": "The nicknames from the original spec. They echo comic-book characters, so keep this theme for personal use and don't ship it in anything public.",
|
|
20
|
+
"terminology": "themed",
|
|
21
|
+
"identities": {
|
|
22
|
+
"leader": {"name": "Captain", "personality": "Confident, strategic, decisive"},
|
|
23
|
+
"researcher": {"name": "Vision", "personality": "Curious, insightful, investigative"},
|
|
24
|
+
"creative_thinker": {"name": "Stark", "personality": "Inventive, witty, unconventional"},
|
|
25
|
+
"analyst": {"name": "Banner", "personality": "Thoughtful, analytical, evidence-driven"},
|
|
26
|
+
"executor": {"name": "Ironclad", "personality": "Practical, resourceful, action-oriented"},
|
|
27
|
+
"challenger": {"name": "Loki", "personality": "Questioning, unpredictable, skeptical"},
|
|
28
|
+
"specialist": {"name": "Strange", "personality": "Knowledgeable, precise, exploratory"},
|
|
29
|
+
"communicator": {"name": "Cap", "personality": "Diplomatic, trustworthy, persuasive"},
|
|
30
|
+
"organizer": {"name": "Widow", "personality": "Organized, discreet, coordinated"},
|
|
31
|
+
"quality_reviewer": {"name": "Hawkeye", "personality": "Observant, precise, detail-oriented"}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"professional": {
|
|
35
|
+
"description": "Plain role titles and business terminology.",
|
|
36
|
+
"terminology": "professional",
|
|
37
|
+
"identities": {
|
|
38
|
+
"leader": {"name": "Lead", "personality": "Clear, structured, decisive"},
|
|
39
|
+
"researcher": {"name": "Researcher", "personality": "Thorough, neutral, well-sourced"},
|
|
40
|
+
"creative_thinker": {"name": "Ideation Lead", "personality": "Open, generative, practical"},
|
|
41
|
+
"analyst": {"name": "Analyst", "personality": "Rigorous, quantitative, cautious"},
|
|
42
|
+
"executor": {"name": "Builder", "personality": "Pragmatic, reliable, concise"},
|
|
43
|
+
"challenger": {"name": "Red Team", "personality": "Skeptical, constructive, direct"},
|
|
44
|
+
"specialist": {"name": "Domain Expert", "personality": "Precise, authoritative, careful"},
|
|
45
|
+
"communicator": {"name": "Communications", "personality": "Clear, audience-aware, tactful"},
|
|
46
|
+
"organizer": {"name": "Coordinator", "personality": "Methodical, punctual, structured"},
|
|
47
|
+
"quality_reviewer": {"name": "QA Reviewer", "personality": "Meticulous, evidence-based, impartial"}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Evaluation Engine: evidence-backed reviews and mission metrics.
|
|
2
|
+
import * as orchestrator from "./orchestrator.mjs";
|
|
3
|
+
import { BrainiverseError } from "./store.mjs";
|
|
4
|
+
|
|
5
|
+
export const VERDICTS = ["pass", "pass_with_issues", "fail"];
|
|
6
|
+
|
|
7
|
+
/** A review must cite evidence. A pass without evidence is refused, as is reviewing nothing. */
|
|
8
|
+
export function addReview(state, target, verdict, evidence = [], issues = [], reviewer = "leader") {
|
|
9
|
+
if (!VERDICTS.includes(verdict)) throw new BrainiverseError(`verdict must be one of ${VERDICTS.join(", ")}`);
|
|
10
|
+
if (!evidence.some((e) => e && e.trim())) throw new BrainiverseError("a review needs evidence: what was checked and how");
|
|
11
|
+
if (verdict === "fail" && !issues.length) throw new BrainiverseError("a failing review must list the issues");
|
|
12
|
+
if (target !== "mission") {
|
|
13
|
+
const task = orchestrator.getTask(state, target);
|
|
14
|
+
if (!["submitted", "done"].includes(task.status)) {
|
|
15
|
+
throw new BrainiverseError(`${target} is ${task.status}; there is nothing to review yet`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const review = { target, verdict, evidence: [...evidence], issues: [...issues], reviewer, at: orchestrator.now() };
|
|
19
|
+
state.reviews.push(review);
|
|
20
|
+
return review;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const round = (n, digits) => Math.round(n * 10 ** digits) / 10 ** digits;
|
|
24
|
+
|
|
25
|
+
export function metrics(state) {
|
|
26
|
+
const tasks = Object.values(state.tasks);
|
|
27
|
+
const invocations = Object.values(state.invocations);
|
|
28
|
+
const latencies = invocations.filter((i) => i.finished).map((i) => i.finished - i.started).sort((a, b) => a - b);
|
|
29
|
+
const byAgent = {};
|
|
30
|
+
for (const i of invocations) byAgent[i.agent] = (byAgent[i.agent] || 0) + 1;
|
|
31
|
+
const enabled = state.team.agents.filter((a) => a.enabled && a.id !== state.team.leader).map((a) => a.id);
|
|
32
|
+
const deliverables = tasks.filter((t) => t.kind === "task");
|
|
33
|
+
const firstTime = deliverables.filter((t) => t.status === "done" && t.iterations === 0 && t.attempts === 1);
|
|
34
|
+
// Invocations whose output was never used: failed, timed out, cancelled, or superseded by a revision.
|
|
35
|
+
const unused = invocations.filter((i) => ["failed", "timed_out", "cancelled"].includes(i.status)).length +
|
|
36
|
+
tasks.reduce((n, t) => n + t.iterations, 0);
|
|
37
|
+
return {
|
|
38
|
+
tasks_total: tasks.length,
|
|
39
|
+
tasks_done: tasks.filter((t) => t.status === "done").length,
|
|
40
|
+
tasks_failed: tasks.filter((t) => t.status === "failed").length,
|
|
41
|
+
completion_rate: deliverables.length ? round(deliverables.filter((t) => t.status === "done").length / deliverables.length, 3) : null,
|
|
42
|
+
first_pass_acceptance: deliverables.length ? round(firstTime.length / deliverables.length, 3) : null,
|
|
43
|
+
invocations: invocations.length,
|
|
44
|
+
invocations_by_agent: byAgent,
|
|
45
|
+
unused_invocations: unused,
|
|
46
|
+
retries: tasks.reduce((n, t) => n + Math.max(t.attempts - 1, 0), 0),
|
|
47
|
+
revisions: tasks.reduce((n, t) => n + t.iterations, 0),
|
|
48
|
+
idle_team_members: enabled.filter((a) => !(a in byAgent)),
|
|
49
|
+
latency_s: {
|
|
50
|
+
median: latencies.length ? round(latencies[Math.floor(latencies.length / 2)], 1) : null,
|
|
51
|
+
max: latencies.length ? round(latencies.at(-1), 1) : null,
|
|
52
|
+
},
|
|
53
|
+
tokens: state.usage.tokens,
|
|
54
|
+
reviews: Object.fromEntries(VERDICTS.map((v) => [v, state.reviews.filter((r) => r.verdict === v).length])),
|
|
55
|
+
results_with_sources: tasks.filter((t) => t.result && t.result.evidence?.length).length,
|
|
56
|
+
results_total: tasks.filter((t) => t.result).length,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Meeting Engine: structured discussions between the human, the Leader, and invited agents,
|
|
2
|
+
// plus direct lines (the human talking one-on-one with a single agent).
|
|
3
|
+
//
|
|
4
|
+
// There is deliberately no command for posting a message *as* an agent. Agent contributions
|
|
5
|
+
// enter the transcript only when the agent was actually invoked for a meeting task and
|
|
6
|
+
// submitted a result, so participation can never be fabricated.
|
|
7
|
+
import * as orchestrator from "./orchestrator.mjs";
|
|
8
|
+
import * as teams from "./teams.mjs";
|
|
9
|
+
import { BrainiverseError, nextId } from "./store.mjs";
|
|
10
|
+
|
|
11
|
+
const { now } = orchestrator;
|
|
12
|
+
|
|
13
|
+
export function getMeeting(state, meetingId) {
|
|
14
|
+
if (!(meetingId in state.meetings)) throw new BrainiverseError(`no meeting ${meetingId}`);
|
|
15
|
+
return state.meetings[meetingId];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requireOpen(meeting, allowPaused = false) {
|
|
19
|
+
const allowed = allowPaused ? ["active", "paused"] : ["active"];
|
|
20
|
+
if (!allowed.includes(meeting.status)) throw new BrainiverseError(`meeting ${meeting.id} is ${meeting.status}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function post(meeting, from, kind, text, extra = {}) {
|
|
24
|
+
const message = { seq: meeting.messages.length + 1, from, kind, text, at: now(), ...extra };
|
|
25
|
+
meeting.messages.push(message);
|
|
26
|
+
return message;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function newMeeting(state, fields) {
|
|
30
|
+
const id = nextId("M", state.meetings);
|
|
31
|
+
const meeting = {
|
|
32
|
+
id, mode: "group", status: "active", started: now(), ended: null, messages: [], decisions: [],
|
|
33
|
+
assumptions: [], questions: [], actions: [], summary: "", ...fields,
|
|
34
|
+
};
|
|
35
|
+
state.meetings[id] = meeting;
|
|
36
|
+
return meeting;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function start(state, objective, participants) {
|
|
40
|
+
orchestrator.requireActive(state);
|
|
41
|
+
if (!objective || !objective.trim()) throw new BrainiverseError("a meeting needs an objective");
|
|
42
|
+
const leader = state.team.leader;
|
|
43
|
+
const invited = [...new Set(participants)].filter((p) => p !== leader);
|
|
44
|
+
if (!invited.length) throw new BrainiverseError("invite at least one agent besides the leader");
|
|
45
|
+
const limit = state.limits.max_meeting_participants;
|
|
46
|
+
if (invited.length > limit) {
|
|
47
|
+
throw new BrainiverseError(`${invited.length} participants exceeds max_meeting_participants (${limit}); ` +
|
|
48
|
+
"invite only agents who add value");
|
|
49
|
+
}
|
|
50
|
+
for (const id of invited) if (!teams.findAgent(state.team, id).enabled) throw new BrainiverseError(`${id} is disabled`);
|
|
51
|
+
const meeting = newMeeting(state, { objective: objective.trim(), host: leader, participants: invited });
|
|
52
|
+
const names = invited.map((a) => teams.findAgent(state.team, a).name).join(", ");
|
|
53
|
+
post(meeting, leader, "system", `Meeting opened: ${meeting.objective}. Participants: ${names}.`);
|
|
54
|
+
return meeting;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A one-on-one conversation between the human and a single agent, without the Leader. */
|
|
58
|
+
export function startDirect(state, agentId, topic = "") {
|
|
59
|
+
orchestrator.requireActive(state);
|
|
60
|
+
const profile = teams.findAgent(state.team, agentId);
|
|
61
|
+
if (!profile.enabled) throw new BrainiverseError(`${profile.name} is disabled`);
|
|
62
|
+
const meeting = newMeeting(state, {
|
|
63
|
+
mode: "direct", objective: topic.trim() || `Direct conversation with ${profile.name}`, host: "human",
|
|
64
|
+
participants: [agentId],
|
|
65
|
+
});
|
|
66
|
+
post(meeting, "human", "system", `Direct line opened with ${profile.name}.`);
|
|
67
|
+
return meeting;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The human's message to the agent; creates the task whose real invocation produces the reply. */
|
|
71
|
+
export function directMessage(state, meetingId, text) {
|
|
72
|
+
const meeting = getMeeting(state, meetingId);
|
|
73
|
+
if (meeting.mode !== "direct") throw new BrainiverseError(`${meetingId} is a group meeting; use \`meeting human\` there`);
|
|
74
|
+
requireOpen(meeting);
|
|
75
|
+
if (!text || !text.trim()) throw new BrainiverseError("message is empty");
|
|
76
|
+
post(meeting, "human", "human", text);
|
|
77
|
+
return orchestrator.addTask(state, {
|
|
78
|
+
objective: `Reply to the human: ${text.trim()}`, agent: meeting.participants[0], kind: "meeting",
|
|
79
|
+
meeting: meetingId, expected_output: "Your reply to the human, in findings, written as you would say it",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Create a contribution task. The Leader then runs it like any task (start, invoke, submit). */
|
|
84
|
+
export function ask(state, meetingId, agentId, prompt) {
|
|
85
|
+
const meeting = getMeeting(state, meetingId);
|
|
86
|
+
requireOpen(meeting);
|
|
87
|
+
if (meeting.mode === "direct") throw new BrainiverseError(`${meetingId} is a direct line; the human's messages go through \`chat say\``);
|
|
88
|
+
if (!meeting.participants.includes(agentId)) throw new BrainiverseError(`${agentId} is not in meeting ${meetingId}; add them first`);
|
|
89
|
+
const task = orchestrator.addTask(state, {
|
|
90
|
+
objective: prompt, agent: agentId, kind: "meeting", meeting: meetingId,
|
|
91
|
+
expected_output: "A contribution to the discussion in the result protocol, responding to the transcript",
|
|
92
|
+
});
|
|
93
|
+
post(meeting, meeting.host, "request", `@${agentId}: ${prompt}`, { task: task.id });
|
|
94
|
+
return task;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Called when a meeting task is submitted: the real result becomes the agent's message. */
|
|
98
|
+
export function recordContribution(state, task) {
|
|
99
|
+
const meeting = getMeeting(state, task.meeting);
|
|
100
|
+
const { findings, recommendations = [], uncertainty = "" } = task.result;
|
|
101
|
+
const text = typeof findings === "string" ? findings : findings.map((f) => `- ${f}`).join("\n");
|
|
102
|
+
post(meeting, task.agent, "contribution", text, {
|
|
103
|
+
task: task.id, invocation: task.invocations.at(-1), recommendations, uncertainty,
|
|
104
|
+
});
|
|
105
|
+
task.status = "done"; // a contribution is discussion, not a deliverable awaiting acceptance
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function say(state, meetingId, text) {
|
|
109
|
+
const meeting = getMeeting(state, meetingId);
|
|
110
|
+
requireOpen(meeting);
|
|
111
|
+
if (meeting.mode === "direct") throw new BrainiverseError("the Leader does not speak in a direct line");
|
|
112
|
+
return post(meeting, meeting.host, "host", text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const findItem = (items, id) => {
|
|
116
|
+
const item = items.find((i) => i.id === id);
|
|
117
|
+
if (!item) throw new BrainiverseError(`no item ${id}`);
|
|
118
|
+
return item;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export function human(state, meetingId, text, questionId) {
|
|
122
|
+
const meeting = getMeeting(state, meetingId);
|
|
123
|
+
requireOpen(meeting, true);
|
|
124
|
+
const message = post(meeting, "human", "human", text);
|
|
125
|
+
if (questionId) findItem(meeting.questions, questionId).answer = text;
|
|
126
|
+
return message;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function question(state, meetingId, text, to = "human") {
|
|
130
|
+
const meeting = getMeeting(state, meetingId);
|
|
131
|
+
requireOpen(meeting);
|
|
132
|
+
const item = { id: nextId("Q", meeting.questions.map((q) => q.id)), text, to, answer: null };
|
|
133
|
+
meeting.questions.push(item);
|
|
134
|
+
post(meeting, meeting.host, "question", text, { question: item.id });
|
|
135
|
+
return item;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function decide(state, meetingId, text, approvedByHuman = false) {
|
|
139
|
+
const meeting = getMeeting(state, meetingId);
|
|
140
|
+
requireOpen(meeting, true);
|
|
141
|
+
const decision = {
|
|
142
|
+
id: nextId("D", state.decisions.map((d) => d.id)), text, meeting: meetingId,
|
|
143
|
+
approved_by_human: approvedByHuman, at: now(),
|
|
144
|
+
};
|
|
145
|
+
meeting.decisions.push(decision.id);
|
|
146
|
+
state.decisions.push(decision);
|
|
147
|
+
post(meeting, meeting.host, "decision", text, { decision: decision.id });
|
|
148
|
+
return decision;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function assume(state, meetingId, text) {
|
|
152
|
+
const meeting = getMeeting(state, meetingId);
|
|
153
|
+
meeting.assumptions.push(text);
|
|
154
|
+
post(meeting, meeting.host, "assumption", text);
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Convert a conclusion into a real task on the board. */
|
|
159
|
+
export function action(state, meetingId, agentId, objective, { acceptance = [], depends_on = [] } = {}) {
|
|
160
|
+
const meeting = getMeeting(state, meetingId);
|
|
161
|
+
const task = orchestrator.addTask(state, {
|
|
162
|
+
objective, agent: agentId, acceptance, depends_on,
|
|
163
|
+
context: `Action item from meeting ${meetingId}: ${meeting.objective}`,
|
|
164
|
+
});
|
|
165
|
+
meeting.actions.push(task.id);
|
|
166
|
+
post(meeting, meeting.host, "action", `${task.id} → ${agentId}: ${objective}`, { task: task.id });
|
|
167
|
+
return task;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function addParticipant(state, meetingId, agentId) {
|
|
171
|
+
const meeting = getMeeting(state, meetingId);
|
|
172
|
+
requireOpen(meeting, true);
|
|
173
|
+
if (meeting.mode === "direct") throw new BrainiverseError("a direct line has exactly one agent; start a meeting instead");
|
|
174
|
+
if (!teams.findAgent(state.team, agentId).enabled) throw new BrainiverseError(`${agentId} is disabled`);
|
|
175
|
+
if (meeting.participants.includes(agentId)) throw new BrainiverseError(`${agentId} is already in the meeting`);
|
|
176
|
+
if (meeting.participants.length >= state.limits.max_meeting_participants) {
|
|
177
|
+
throw new BrainiverseError("meeting is at max_meeting_participants");
|
|
178
|
+
}
|
|
179
|
+
meeting.participants.push(agentId);
|
|
180
|
+
post(meeting, meeting.host, "system", `${agentId} joined.`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function removeParticipant(state, meetingId, agentId) {
|
|
184
|
+
const meeting = getMeeting(state, meetingId);
|
|
185
|
+
if (!meeting.participants.includes(agentId)) throw new BrainiverseError(`${agentId} is not in the meeting`);
|
|
186
|
+
meeting.participants = meeting.participants.filter((p) => p !== agentId);
|
|
187
|
+
for (const task of Object.values(state.tasks)) {
|
|
188
|
+
if (task.meeting === meetingId && task.agent === agentId && task.status === "pending") {
|
|
189
|
+
orchestrator.cancel(state, task.id, "participant removed from meeting");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
post(meeting, meeting.host, "system", `${agentId} left.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function setStatus(state, meetingId, status, summary = "") {
|
|
196
|
+
const meeting = getMeeting(state, meetingId);
|
|
197
|
+
const transitions = { paused: ["active"], active: ["paused"], ended: ["active", "paused"] };
|
|
198
|
+
if (!transitions[status].includes(meeting.status)) {
|
|
199
|
+
throw new BrainiverseError(`cannot move meeting from ${meeting.status} to ${status}`);
|
|
200
|
+
}
|
|
201
|
+
if (status === "ended") {
|
|
202
|
+
if (!summary.trim() && meeting.mode === "direct") summary = "Direct conversation ended.";
|
|
203
|
+
if (!summary.trim()) throw new BrainiverseError("ending a meeting needs a summary of decisions and open issues");
|
|
204
|
+
meeting.summary = summary;
|
|
205
|
+
meeting.ended = now();
|
|
206
|
+
for (const task of Object.values(state.tasks)) {
|
|
207
|
+
if (task.meeting === meetingId && task.kind === "meeting" && task.status === "pending") {
|
|
208
|
+
orchestrator.cancel(state, task.id, "meeting ended");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
meeting.status = status;
|
|
213
|
+
post(meeting, meeting.host, "system", `Meeting ${status}.` + (summary ? ` Summary: ${summary}` : ""));
|
|
214
|
+
return meeting;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function transcript(state, meetingId, limit = 40) {
|
|
218
|
+
const meeting = getMeeting(state, meetingId);
|
|
219
|
+
const names = Object.fromEntries(state.team.agents.map((a) => [a.id, a.name]));
|
|
220
|
+
names.human = "Human";
|
|
221
|
+
return meeting.messages.slice(-limit)
|
|
222
|
+
.map((m) => `[${m.kind}] ${names[m.from] ?? m.from}: ${m.text}`)
|
|
223
|
+
.join("\n");
|
|
224
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Memory Manager: agent, team, and project memory, plus secret redaction.
|
|
2
|
+
//
|
|
3
|
+
// Scopes:
|
|
4
|
+
// agent:<id> role-specific knowledge an agent keeps across missions
|
|
5
|
+
// team:<id> shared conventions and team-level decisions
|
|
6
|
+
// project goals, findings and decisions for the active mission (stored in mission state)
|
|
7
|
+
//
|
|
8
|
+
// Nothing is remembered automatically. Agents may suggest memories in their results; only the
|
|
9
|
+
// Leader writes them, and records that look like secrets are refused.
|
|
10
|
+
import { now } from "./orchestrator.mjs";
|
|
11
|
+
import { BrainiverseError, checkSlug, nextId, readJson, writeJson } from "./store.mjs";
|
|
12
|
+
|
|
13
|
+
// The generic "key: value" rule runs first so it consumes the whole value, and never matches a
|
|
14
|
+
// "[redacted ...]" marker left by an earlier rule.
|
|
15
|
+
const SECRET_PATTERNS = [
|
|
16
|
+
[/\b(?:password|passwd|secret|api[_-]?key|token)\s*[:=]\s*[^\s[]\S{5,}/gi, "credential"],
|
|
17
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, "API key"],
|
|
18
|
+
[/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, "AWS access key"],
|
|
19
|
+
[/\bgh[pousr]_[A-Za-z0-9]{20,}/g, "GitHub token"],
|
|
20
|
+
[/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, "Slack token"],
|
|
21
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/g, "private key"],
|
|
22
|
+
[/\b(?:\d[ -]?){13,16}\b/g, "card number"],
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function findSecrets(text) {
|
|
26
|
+
const found = new Set();
|
|
27
|
+
for (const [pattern, label] of SECRET_PATTERNS) {
|
|
28
|
+
pattern.lastIndex = 0;
|
|
29
|
+
if (pattern.test(text || "")) found.add(label);
|
|
30
|
+
}
|
|
31
|
+
return [...found].sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function redact(text) {
|
|
35
|
+
if (typeof text !== "string") return text;
|
|
36
|
+
for (const [pattern, label] of SECRET_PATTERNS) text = text.replace(pattern, `[redacted ${label}]`);
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseScope(scope) {
|
|
41
|
+
if (scope === "project") return ["project", null];
|
|
42
|
+
const [kind, ...rest] = String(scope).split(":");
|
|
43
|
+
const ref = rest.join(":");
|
|
44
|
+
if (!["agent", "team"].includes(kind) || !ref) throw new BrainiverseError("scope must be 'project', 'agent:<id>' or 'team:<id>'");
|
|
45
|
+
return [kind, checkSlug(ref, `${kind} id`)];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const storePath = (ws, kind, ref) => ws.path("memory", `${kind}-${ref}.json`);
|
|
49
|
+
|
|
50
|
+
function loadScope(ws, scope, state) {
|
|
51
|
+
const [kind, ref] = parseScope(scope);
|
|
52
|
+
if (kind === "project") {
|
|
53
|
+
if (!state) throw new BrainiverseError("project memory needs an active mission");
|
|
54
|
+
return state.memory;
|
|
55
|
+
}
|
|
56
|
+
return readJson(storePath(ws, kind, ref), []);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function saveScope(ws, scope, records, state) {
|
|
60
|
+
const [kind, ref] = parseScope(scope);
|
|
61
|
+
if (kind === "project") state.memory = records;
|
|
62
|
+
else writeJson(storePath(ws, kind, ref), records);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function refuseSecrets(text) {
|
|
66
|
+
const secrets = findSecrets(text);
|
|
67
|
+
if (secrets.length) {
|
|
68
|
+
throw new BrainiverseError(`refusing to store what looks like a ${secrets.join(", ")}; ` +
|
|
69
|
+
"secrets belong in the environment or a secret manager, not in memory");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function add(ws, scope, text, { tags = [], source = "leader", state = null } = {}) {
|
|
74
|
+
if (!text || !text.trim()) throw new BrainiverseError("memory text is empty");
|
|
75
|
+
refuseSecrets(text);
|
|
76
|
+
const records = loadScope(ws, scope, state);
|
|
77
|
+
const record = { id: nextId("R", records.map((r) => r.id)), text: text.trim(), tags: [...tags], source, created: now() };
|
|
78
|
+
records.push(record);
|
|
79
|
+
saveScope(ws, scope, records, state);
|
|
80
|
+
return record;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function list(ws, scope, { state = null, tags = [] } = {}) {
|
|
84
|
+
const records = loadScope(ws, scope, state);
|
|
85
|
+
return tags.length ? records.filter((r) => r.tags.some((t) => tags.includes(t))) : records;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function edit(ws, scope, recordId, text, { state = null } = {}) {
|
|
89
|
+
refuseSecrets(text);
|
|
90
|
+
const records = loadScope(ws, scope, state);
|
|
91
|
+
const record = records.find((r) => r.id === recordId);
|
|
92
|
+
if (!record) throw new BrainiverseError(`no memory record ${recordId} in ${scope}`);
|
|
93
|
+
record.text = text.trim();
|
|
94
|
+
record.edited = now();
|
|
95
|
+
saveScope(ws, scope, records, state);
|
|
96
|
+
return record;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function remove(ws, scope, recordId, { state = null } = {}) {
|
|
100
|
+
const records = loadScope(ws, scope, state);
|
|
101
|
+
const kept = records.filter((r) => r.id !== recordId);
|
|
102
|
+
if (kept.length === records.length) throw new BrainiverseError(`no memory record ${recordId} in ${scope}`);
|
|
103
|
+
saveScope(ws, scope, kept, state);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Memory shared with one agent: its own, its team's, and the project's. Never other agents'. */
|
|
107
|
+
export function contextFor(ws, state, agentId, limit = 12) {
|
|
108
|
+
const agentMemory = agentId ? readJson(storePath(ws, "agent", agentId), []) : [];
|
|
109
|
+
const teamMemory = readJson(storePath(ws, "team", state.team_ref), []);
|
|
110
|
+
return {
|
|
111
|
+
agent: agentMemory.slice(-limit).map((r) => r.text),
|
|
112
|
+
team: teamMemory.slice(-limit).map((r) => r.text),
|
|
113
|
+
project: state.memory.slice(-limit).map((r) => r.text),
|
|
114
|
+
};
|
|
115
|
+
}
|