@orbit-intelligence/orbit-agent 0.3.12

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.
Files changed (80) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +23 -0
  3. package/bin/orbit +26 -0
  4. package/dist/prompts/system.js +80 -0
  5. package/dist/src/cli/args.js +145 -0
  6. package/dist/src/cli/orchestrate.js +100 -0
  7. package/dist/src/cli/run.js +393 -0
  8. package/dist/src/config/config-schema.js +151 -0
  9. package/dist/src/config/index.js +57 -0
  10. package/dist/src/core/agent/agent-loop.js +402 -0
  11. package/dist/src/core/agents/delegate.js +120 -0
  12. package/dist/src/core/agents/orchestrator.js +58 -0
  13. package/dist/src/core/agents/prompts.js +82 -0
  14. package/dist/src/core/agents/types.js +1 -0
  15. package/dist/src/core/context/context-manager.js +167 -0
  16. package/dist/src/core/events.js +23 -0
  17. package/dist/src/core/llm/http.js +207 -0
  18. package/dist/src/core/llm/index.js +93 -0
  19. package/dist/src/core/llm/models.js +228 -0
  20. package/dist/src/core/llm/providers/gemini.js +211 -0
  21. package/dist/src/core/llm/providers/openai-compat.js +31 -0
  22. package/dist/src/core/llm/router.js +125 -0
  23. package/dist/src/core/llm/secrets.js +121 -0
  24. package/dist/src/core/llm/types.js +10 -0
  25. package/dist/src/core/orchestration/dispatcher.js +74 -0
  26. package/dist/src/core/orchestration/messenger.js +139 -0
  27. package/dist/src/core/orchestration/roles.js +129 -0
  28. package/dist/src/core/orchestration/runtime.js +122 -0
  29. package/dist/src/core/orchestration/session.js +204 -0
  30. package/dist/src/core/orchestration/shared-context.js +88 -0
  31. package/dist/src/core/orchestration/tools.js +187 -0
  32. package/dist/src/core/orchestration/types.js +3 -0
  33. package/dist/src/core/permissions/index.js +58 -0
  34. package/dist/src/core/project-context.js +115 -0
  35. package/dist/src/core/skill-loader.js +31 -0
  36. package/dist/src/core/tools/edit.js +142 -0
  37. package/dist/src/core/tools/filesystem.js +203 -0
  38. package/dist/src/core/tools/git.js +138 -0
  39. package/dist/src/core/tools/registry.js +73 -0
  40. package/dist/src/core/tools/search.js +90 -0
  41. package/dist/src/core/tools/shell.js +65 -0
  42. package/dist/src/core/tools/types.js +6 -0
  43. package/dist/src/core/types.js +3 -0
  44. package/dist/src/index.js +11 -0
  45. package/dist/src/session/event-log.js +55 -0
  46. package/dist/src/session/store.js +76 -0
  47. package/dist/src/setup/wizard.js +401 -0
  48. package/dist/src/tui/InkApp.js +67 -0
  49. package/dist/src/tui/ansi.js +142 -0
  50. package/dist/src/tui/app.js +768 -0
  51. package/dist/src/tui/colors.js +13 -0
  52. package/dist/src/tui/components/AgentDock.js +46 -0
  53. package/dist/src/tui/components/Composer.js +35 -0
  54. package/dist/src/tui/components/Header.js +23 -0
  55. package/dist/src/tui/components/ModelPicker.js +23 -0
  56. package/dist/src/tui/components/PermissionModal.js +29 -0
  57. package/dist/src/tui/components/SlashMenu.js +15 -0
  58. package/dist/src/tui/components/StatusLine.js +27 -0
  59. package/dist/src/tui/components/Transcript.js +31 -0
  60. package/dist/src/tui/components/WorkingStatus.js +29 -0
  61. package/dist/src/tui/components/input.js +246 -0
  62. package/dist/src/tui/components/markdown.js +384 -0
  63. package/dist/src/tui/components/message.js +105 -0
  64. package/dist/src/tui/context.js +8 -0
  65. package/dist/src/tui/geometry.js +40 -0
  66. package/dist/src/tui/renderer.js +116 -0
  67. package/dist/src/tui/rows.js +247 -0
  68. package/dist/src/tui/scheduler.js +32 -0
  69. package/dist/src/tui/store.js +127 -0
  70. package/dist/src/tui/style.js +151 -0
  71. package/dist/src/tui/term.js +309 -0
  72. package/dist/src/tui/text.js +104 -0
  73. package/dist/src/tui/themes/index.js +15 -0
  74. package/dist/src/tui/themes/palettes.js +137 -0
  75. package/dist/src/tui/themes/types.js +1 -0
  76. package/dist/src/utils/diff.js +161 -0
  77. package/dist/src/utils/platform.js +71 -0
  78. package/dist/src/utils/signals.js +26 -0
  79. package/dist/src/version.js +4 -0
  80. package/package.json +71 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Provider credential handling.
3
+ *
4
+ * SECURITY: keys are read ONLY from environment variables or the optional,
5
+ * git-ignored, 0600 `keys.json`. They are never written to config.json,
6
+ * never logged, and never included in generated artifacts.
7
+ */
8
+ import { readFileSync, existsSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
9
+ import { keysPath, configDir } from '../../utils/platform.js';
10
+ const ENV_LABELS = {
11
+ orbitx: ['ORBITX_TOKEN', 'ORBITX_API_KEY'],
12
+ groq: ['GROQ_API_KEY'],
13
+ gemini: ['GEMINI_API_KEY'],
14
+ openrouter: ['OPENROUTER_API_KEY'],
15
+ };
16
+ // The setup wizard calls providers three times in quick succession; don't
17
+ // re-read + re-parse + re-chmod the keys file on every lookup.
18
+ const KEYS_CACHE_MS = 2000;
19
+ let keysCache = null;
20
+ function readKeysFile() {
21
+ const now = Date.now();
22
+ if (keysCache && now - keysCache.at < KEYS_CACHE_MS)
23
+ return keysCache.data;
24
+ const path = keysPath();
25
+ if (!existsSync(path)) {
26
+ keysCache = { data: {}, at: now };
27
+ return {};
28
+ }
29
+ try {
30
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
31
+ // best-effort tighten perms so other apps can't read it
32
+ try {
33
+ chmodSync(path, 0o600);
34
+ }
35
+ catch {
36
+ /* ignore */
37
+ }
38
+ keysCache = { data: raw, at: now };
39
+ return raw;
40
+ }
41
+ catch {
42
+ keysCache = { data: {}, at: now };
43
+ return {};
44
+ }
45
+ }
46
+ function invalidateKeysCache() {
47
+ keysCache = null;
48
+ }
49
+ function fromEnv(provider) {
50
+ const labels = ENV_LABELS[provider];
51
+ if (!labels)
52
+ return [];
53
+ const keys = [];
54
+ for (const label of labels) {
55
+ const direct = process.env[label];
56
+ if (direct && !keys.includes(direct))
57
+ keys.push(direct);
58
+ // allow GROQ_API_KEY_1.._9 rotation
59
+ for (let i = 1; i <= 9; i++) {
60
+ const rotated = process.env[`${label}_${i}`];
61
+ if (rotated && !keys.includes(rotated))
62
+ keys.push(rotated);
63
+ }
64
+ }
65
+ return keys;
66
+ }
67
+ export function isImportableDirectly(p) {
68
+ return p === 'groq' || p === 'gemini' || p === 'openrouter' || p === 'openai';
69
+ }
70
+ export function getProviderSecrets(provider) {
71
+ const envKeys = fromEnv(provider);
72
+ if (envKeys.length > 0)
73
+ return { provider, keys: envKeys, source: 'env' };
74
+ const file = readKeysFile();
75
+ const fileKeys = file[provider] ?? [];
76
+ if (fileKeys.length > 0)
77
+ return { provider, keys: fileKeys, source: 'file' };
78
+ return null;
79
+ }
80
+ export function hasAnySecrets() {
81
+ for (const p of ['orbitx', 'groq', 'gemini', 'openrouter']) {
82
+ if (getProviderSecrets(p))
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ export function maskSecret(key) {
88
+ if (key.length <= 8)
89
+ return '•'.repeat(Math.min(key.length, 8));
90
+ return `${key.slice(0, 4)}••••••••${key.slice(-4)}`;
91
+ }
92
+ export function describeProvidersAvailable() {
93
+ const out = [];
94
+ for (const p of ['orbitx', 'groq', 'gemini', 'openrouter']) {
95
+ const s = getProviderSecrets(p);
96
+ if (s && s.keys[0])
97
+ out.push({ provider: p, masked: maskSecret(s.keys[0]), source: s.source });
98
+ }
99
+ return out;
100
+ }
101
+ /**
102
+ * Persist provider keys to the git-ignored keys.json (0600), merging over any
103
+ * existing keys so a re-run of the wizard never wipes other providers.
104
+ */
105
+ export function writeKeys(provider, keys) {
106
+ const trimmed = keys.map((k) => k.trim()).filter(Boolean);
107
+ if (trimmed.length === 0)
108
+ return 'no keys provided';
109
+ const existing = readKeysFile();
110
+ existing[provider] = trimmed;
111
+ try {
112
+ mkdirSync(configDir(), { recursive: true });
113
+ writeFileSync(keysPath(), JSON.stringify(existing, null, 2), 'utf8');
114
+ chmodSync(keysPath(), 0o600);
115
+ invalidateKeysCache();
116
+ return null;
117
+ }
118
+ catch (err) {
119
+ return err.message;
120
+ }
121
+ }
@@ -0,0 +1,10 @@
1
+ /** Prefixed model id like "groq/llama-3.3-70b-versatile". */
2
+ export function splitModelId(id) {
3
+ const idx = id.indexOf('/');
4
+ if (idx <= 0)
5
+ return { provider: 'orbitx', model: id };
6
+ return { provider: id.slice(0, idx), model: id.slice(idx + 1) };
7
+ }
8
+ export function joinModelId(provider, model) {
9
+ return `${provider}/${model}`;
10
+ }
@@ -0,0 +1,74 @@
1
+ import { AutoRouter } from '../llm/router.js';
2
+ import { ContextManager } from '../context/context-manager.js';
3
+ import { EventBus } from '../events.js';
4
+ import { AgentLoop } from '../agent/agent-loop.js';
5
+ /**
6
+ * Roles whose prompts are READ-ONLY guards. The AgentLoop's `readOnly` flag
7
+ * force-blocks edit_file/write_file/git_add/git_commit while still allowing
8
+ * run_shell (gated by the permission policy) — matching the role prompts.
9
+ */
10
+ export const READONLY_ROLES = new Set(['architect', 'reviewer', 'security', 'researcher']);
11
+ export function createDispatcher(deps) {
12
+ let seq = 0;
13
+ return async (req) => {
14
+ const role = deps.roles.get(req.role);
15
+ if (!role) {
16
+ return { ok: false, summary: `unknown role: ${req.role} (known: ${deps.roles.list().map((r) => r.id).join(', ')})` };
17
+ }
18
+ const id = `role_${req.role}_${++seq}`;
19
+ deps.registerRole?.(id);
20
+ let ok = false;
21
+ let summary = '(role agent produced no summary)';
22
+ try {
23
+ const assignment = { role: req.role, task: req.task, files: req.files, cwd: deps.cwd };
24
+ const prompt = role.buildSystemPrompt(assignment);
25
+ const model = role.defaultModel ?? deps.defaultModel;
26
+ const candidates = model ? [model.primary, ...(model.fallback ?? [])].filter(Boolean) : [];
27
+ const router = new AutoRouter(deps.providers, candidates, deps.strategy);
28
+ deps.bus?.emit('onAgentStart', { role: req.role, task: truncate(req.task, 140) });
29
+ const subContext = new ContextManager(prompt);
30
+ const loop = new AgentLoop({
31
+ bus: deps.bus ?? new EventBus(),
32
+ router,
33
+ context: subContext,
34
+ tools: deps.registry,
35
+ permissions: deps.permissions,
36
+ systemPrompt: prompt,
37
+ maxIterations: deps.maxIterations ?? 12,
38
+ timeoutMs: deps.toolTimeoutMs ?? 30_000,
39
+ streamTimeoutMs: deps.streamTimeoutMs ?? 120_000,
40
+ contextBudgetTokens: deps.contextBudgetTokens ?? 48_000,
41
+ cwd: deps.cwd,
42
+ readOnly: READONLY_ROLES.has(req.role),
43
+ reasoning: deps.reasoning,
44
+ });
45
+ const res = await loop.run(req.task);
46
+ const last = lastAssistantMessage(subContext.history());
47
+ const hasSummary = Boolean(last?.content.trim());
48
+ summary = hasSummary ? last.content.trim() : 'role agent finished without a summary';
49
+ ok = !res.interrupted && hasSummary;
50
+ deps.bus?.emit('onAgentEnd', { role: req.role, ok, summary: truncate(summary, 200) });
51
+ return { ok, summary };
52
+ }
53
+ catch (err) {
54
+ summary = `role agent crashed: ${err.message}`;
55
+ ok = false;
56
+ deps.bus?.emit('onAgentEnd', { role: req.role, ok: false, summary });
57
+ return { ok, summary };
58
+ }
59
+ finally {
60
+ deps.unregisterRole?.(id);
61
+ }
62
+ };
63
+ }
64
+ function lastAssistantMessage(messages) {
65
+ for (let i = messages.length - 1; i >= 0; i--) {
66
+ const m = messages[i];
67
+ if (m.role === 'assistant' && m.content.trim())
68
+ return m;
69
+ }
70
+ return undefined;
71
+ }
72
+ export function truncate(s, n) {
73
+ return s.length <= n ? s : `${s.slice(0, n)}…`;
74
+ }
@@ -0,0 +1,139 @@
1
+ import { newId } from './types.js';
2
+ export class Messenger {
3
+ mailboxes = new Map();
4
+ pending = new Map();
5
+ opts;
6
+ constructor(opts) {
7
+ this.opts = opts;
8
+ }
9
+ agentIds() {
10
+ return this.opts.getAgentIds();
11
+ }
12
+ /** Deliver a message. Returns the stored message. */
13
+ send(from, to, content, kind = 'message', replyTo) {
14
+ const ids = this.agentIds();
15
+ if (!ids.includes(to)) {
16
+ throw new Error(`unknown target agent: ${to} (known: ${ids.join(', ')} or 'user')`);
17
+ }
18
+ const msg = { id: newId(), from, to, kind, content, createdAt: Date.now(), replyTo };
19
+ const box = this.mailboxes.get(to) ?? [];
20
+ box.push(msg);
21
+ this.mailboxes.set(to, box);
22
+ this.tryResolvePending(to, msg);
23
+ this.opts.onMessage?.(msg);
24
+ return msg;
25
+ }
26
+ /** Interrupt a target: if it is running it will be aborted by the caller. */
27
+ interrupt(target, reason, from = 'user') {
28
+ return this.send(from, target, reason, 'interrupt');
29
+ }
30
+ mailboxLength(target) {
31
+ return this.mailboxes.get(target)?.length ?? 0;
32
+ }
33
+ hasPending(target) {
34
+ return this.mailboxLength(target) > 0;
35
+ }
36
+ /** Drain the target's mailbox (delivered in FIFO order). */
37
+ takePending(target) {
38
+ const box = this.mailboxes.get(target) ?? [];
39
+ this.mailboxes.set(target, []);
40
+ return box;
41
+ }
42
+ /** Peek the target's mailbox without draining it. */
43
+ pendingOf(target) {
44
+ return [...(this.mailboxes.get(target) ?? [])];
45
+ }
46
+ /** Park the caller until a reply from `replyFrom` arrives or timeout. */
47
+ waitForReply(from, replyFrom, timeoutMs, signal) {
48
+ // Bookkeeping request only — NOT delivered to the target's mailbox (a bare
49
+ // 'status' ping would pollute the peer's context; the waiter is resolved by
50
+ // the peer's real reply referencing it or by anyone-newer traffic).
51
+ const requestMsg = {
52
+ id: newId(),
53
+ from,
54
+ to: replyFrom,
55
+ kind: 'status',
56
+ content: '',
57
+ createdAt: Date.now(),
58
+ };
59
+ return new Promise((resolve, reject) => {
60
+ const req = { id: requestMsg.id, from, requestMsg, resolve, reject, createdAt: Date.now() };
61
+ const list = this.pending.get(from) ?? [];
62
+ list.push(req);
63
+ this.pending.set(from, list);
64
+ this.opts.onParked?.(from);
65
+ const timer = setTimeout(() => this.settle(from, req, reject, new Error(`timeout waiting for reply from ${replyFrom}`)), timeoutMs);
66
+ const onAbort = () => {
67
+ clearTimeout(timer);
68
+ signal?.removeEventListener('abort', onAbort);
69
+ this.settle(from, req, reject, new Error('cancelled while waiting for reply'));
70
+ };
71
+ signal?.addEventListener('abort', onAbort, { once: true });
72
+ });
73
+ }
74
+ settle(from, req, reject, err) {
75
+ const list = this.pending.get(from);
76
+ if (!list)
77
+ return;
78
+ const idx = list.indexOf(req);
79
+ if (idx < 0)
80
+ return;
81
+ list.splice(idx, 1);
82
+ if (list.length === 0)
83
+ this.pending.delete(from);
84
+ reject(err);
85
+ // only resolved by us, not a real reply — still let the driver know
86
+ // we shouldn't call onMessage here (no message); driver polls instead.
87
+ this.opts.onUnparked?.(from);
88
+ }
89
+ tryResolvePending(to, incoming) {
90
+ // Waiters park under the parked agent's id (the `from` of the original
91
+ // request). A message that lands in `to`'s mailbox resolves waiters whose
92
+ // parked agent == `to`, provided the sender is not the waiter itself.
93
+ // When the waiter attached `replyTo`, only that request matches.
94
+ const entries = [...this.pending.entries()];
95
+ let reserved = false;
96
+ for (const [waiterAgent, list] of entries) {
97
+ if (list.length === 0)
98
+ continue;
99
+ const matches = list.filter((req) => {
100
+ if (req.from !== waiterAgent || waiterAgent !== to)
101
+ return false;
102
+ if (incoming.from === waiterAgent)
103
+ return false;
104
+ return incoming.replyTo ? incoming.replyTo === req.id : true;
105
+ });
106
+ if (matches.length === 0)
107
+ continue;
108
+ for (const req of matches) {
109
+ const idx = list.indexOf(req);
110
+ if (idx >= 0)
111
+ list.splice(idx, 1);
112
+ req.resolve(incoming);
113
+ this.opts.onUnparked?.(waiterAgent);
114
+ }
115
+ reserved = true;
116
+ if (list.length === 0)
117
+ this.pending.delete(waiterAgent);
118
+ }
119
+ // The reply was consumed by a waiting parker — do not redeliver it when
120
+ // the agent later drains its mailbox.
121
+ if (reserved) {
122
+ const box = this.mailboxes.get(to);
123
+ if (box) {
124
+ const ix = box.indexOf(incoming);
125
+ if (ix >= 0)
126
+ box.splice(ix, 1);
127
+ }
128
+ }
129
+ }
130
+ /** Agents currently parked waiting on a reply. */
131
+ parkedAgents() {
132
+ const out = [];
133
+ for (const [id, list] of this.pending) {
134
+ if (list.length > 0)
135
+ out.push(id);
136
+ }
137
+ return out;
138
+ }
139
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Dynamic agent role registry. Roles are definitions — id, label, prompt
3
+ * builder, optional default model — NOT hardcoded switches in the runtime.
4
+ * Built-in roles ship here; `registerRole()` adds custom ones at runtime so
5
+ * the set stays extensible without touching orchestration code.
6
+ */
7
+ function basePrompt(label, assignment) {
8
+ const scope = assignment.files && assignment.files.length > 0
9
+ ? `\nRelevant files (focus here, do not wander):\n${assignment.files.map((f) => ` - ${f}`).join('\n')}`
10
+ : '';
11
+ return `You are orbit-agent's ${label} running as a terminal agent.
12
+
13
+ # Operating environment
14
+ Working directory: ${assignment.cwd}
15
+ ${scope}
16
+
17
+ # Assignment
18
+ ${assignment.task}`;
19
+ }
20
+ function makeDef(id, label, description, body) {
21
+ return {
22
+ id,
23
+ label,
24
+ description,
25
+ buildSystemPrompt: (a) => `${basePrompt(label, a)}\n\n${body}`,
26
+ };
27
+ }
28
+ const READONLY_GUARD = `You are READ-ONLY. You cannot run shell commands or modify anything; only read, search, and inspect (including git status/diff/log).`;
29
+ const VERIFY_RULES = `- Run the project's checks (tests, typecheck, build) yourself before claiming success.
30
+ - Do not modify unrelated files. Do not commit unless the assignment says so.
31
+ - If genuinely blocked, say exactly what and why.`;
32
+ const ROLE_DEFS = [
33
+ makeDef('architect', 'Architect', 'Designs implementation plans. Explores the codebase and produces precise, minimal plans: exact files, exact changes, order, verification steps.', `# Your job
34
+ You design the implementation plan. Explore the code, identify how things fit together, and produce a precise, minimal plan: exact files, exact changes, and the order and verification steps.
35
+
36
+ # Rules
37
+ - ${READONLY_GUARD}
38
+ - Base plans on evidence you actually read, never guesses.
39
+ - Prefer the smallest changes that satisfy the assignment.
40
+ - End with a section "## Plan" listing concrete steps with file paths.
41
+ - Keep the final answer under ~60 lines.`),
42
+ makeDef('frontend', 'Frontend Developer', 'Implements frontend / UI work (React, Vue, styling, components, UX).', `# Your job
43
+ You implement frontend / UI changes. Read before you edit, preserve existing conventions, and verify builds.
44
+
45
+ # Rules
46
+ - Read before you edit. Never write content you have not verified against the current file.
47
+ - Prefer edit_file over write_file for surgical changes.
48
+ - ${VERIFY_RULES}
49
+ - End with "## Summary" listing what you changed and the verification you ran.`),
50
+ makeDef('backend', 'Backend Developer', 'Implements backend / API / data-layer work (servers, databases, contracts).', `# Your job
51
+ You implement backend / API / data-layer changes. Read before you edit, preserve existing conventions, and verify builds.
52
+
53
+ # Rules
54
+ - Read before you edit. Never write content you have not verified against the current file.
55
+ - Prefer edit_file over write_file for surgical changes.
56
+ - ${VERIFY_RULES}
57
+ - End with "## Summary" listing what you changed and the verification you ran.`),
58
+ makeDef('researcher', 'Researcher', 'Investigates code, docs, and external info. Returns findings, not edits.', `# Your job
59
+ Investigate and report back findings: how parts fit together, what a library does, current behavior, risks. Do NOT implement changes.
60
+
61
+ # Rules
62
+ - READ-ONLY for modifications; you may run shell commands to inspect (tests, git grep already available).
63
+ - Cite evidence: file:line, command output, or doc quotes. Never guess.
64
+ - Keep the final answer focused and under ~50 lines.`),
65
+ makeDef('debugger', 'Debugger', 'Reproduces bugs, narrows the root cause, and reports it with a prescribed fix.', `# Your job
66
+ Reproduce the bug, narrow the root cause, and report it precisely with a recommended fix. You may add temporary diagnostics if needed, but focus on root-cause analysis.
67
+
68
+ # Rules
69
+ - Reproduce first. Capture the failing command and output verbatim.
70
+ - Use bisecting/greps to isolate the cause. Verify your hypothesis with an experiment.
71
+ - Report root cause with file:line and a concrete recommended fix. If you are unsure, say so.
72
+ - End with "## Diagnosis" and "## Recommended fix". Keep it under ~50 lines.`),
73
+ makeDef('tester', 'Tester', 'Writes and runs tests for the given change and reports coverage gaps.', `# Your job
74
+ Write and run tests covering the given change. Report results and coverage gaps.
75
+
76
+ # Rules
77
+ - Read the project's test setup before writing tests; follow its conventions exactly.
78
+ - Run the full relevant suite and report pass/fail counts verbatim.
79
+ - Do not modify production code unless the assignment explicitly says so.
80
+ - End with "## Test results" and "## Gaps".`),
81
+ makeDef('reviewer', 'Reviewer', 'Independently reviews changes against the assignment and returns a PASS/FAIL verdict.', `# Your job
82
+ Independently review the changes against the assignment. Find real problems: bugs, regressions, architectural issues, missing tests, style drift. Do NOT rubber-stamp.
83
+
84
+ # Rules
85
+ - You are READ-ONLY for file contents (no edit_file/write_file) but MAY run shell commands to verify (tests, typecheck, diff, log).
86
+ - Verify claims: if code was changed, run the relevant checks and quote the output.
87
+ - Report concrete, actionable findings: file:line where possible, and the exact fix.
88
+ - End with "## Verdict": PASS or FAIL, and if FAIL, a short list of required fixes.`),
89
+ makeDef('security', 'Security Reviewer', 'Reviews changes for security issues (auth, injection, secrets, untrusted input).', `# Your job
90
+ Review the given code for security issues: injection, broken auth/z, secret handling, unsafe deserialization, path traversal, SSRF, denial-of-service.
91
+
92
+ # Rules
93
+ - READ-ONLY; you may run inspection commands (grep, git diff/log) to trace data flow.
94
+ - For each finding include severity (critical/high/medium/low), file:line, exploit sketch, and the concrete fix.
95
+ - End with "## Verdict": PASS or FAIL, and if FAIL a short list of required fixes.`),
96
+ ];
97
+ const DEFAULT_ROLE_MODELS = {};
98
+ export class RoleRegistry {
99
+ roles = new Map();
100
+ constructor() {
101
+ for (const def of ROLE_DEFS) {
102
+ const d = DEFAULT_ROLE_MODELS[def.id];
103
+ this.register(def.id, def.label, def.description, def.buildSystemPrompt, d);
104
+ }
105
+ }
106
+ register(id, label, description, buildSystemPrompt, defaultModel) {
107
+ this.roles.set(id, { id, label, description, buildSystemPrompt, defaultModel });
108
+ }
109
+ registerRole(def) {
110
+ const existing = this.roles.get(def.id);
111
+ this.roles.set(def.id, { ...existing, ...def });
112
+ }
113
+ get(roleId) {
114
+ return this.roles.get(roleId);
115
+ }
116
+ has(roleId) {
117
+ return this.roles.has(roleId);
118
+ }
119
+ all() {
120
+ return [...this.roles.values()];
121
+ }
122
+ buildSystemPrompt(roleId, assignment) {
123
+ return this.roles.get(roleId)?.buildSystemPrompt(assignment) ?? null;
124
+ }
125
+ list() {
126
+ return this.all().map((d) => ({ id: d.id, label: d.label, description: d.description }));
127
+ }
128
+ }
129
+ export const builtInRoleIds = ROLE_DEFS.map((d) => d.id);
@@ -0,0 +1,122 @@
1
+ import { AutoRouter } from '../llm/router.js';
2
+ import { ContextManager } from '../context/context-manager.js';
3
+ import { EventBus } from '../events.js';
4
+ import { AgentLoop } from '../agent/agent-loop.js';
5
+ const FORWARDED_EVENTS = [
6
+ 'onUserMessage',
7
+ 'onAssistantStart',
8
+ 'onThinking',
9
+ 'onToken',
10
+ 'onToolStart',
11
+ 'onToolUpdate',
12
+ 'onToolEnd',
13
+ 'onAssistantEnd',
14
+ 'onError',
15
+ 'onRoute',
16
+ 'onStatus',
17
+ 'onAgentStart',
18
+ 'onAgentEnd',
19
+ 'onContextSummary',
20
+ ];
21
+ /**
22
+ * AgentRuntime — one full agent in an orchestration session.
23
+ * Owns a per-agent router (per-agent model assignment), an isolated
24
+ * ContextManager, its own EventBus that fans out to the public session bus,
25
+ * and an AgentLoop it can (re)start and abort per turn.
26
+ */
27
+ export class AgentRuntime {
28
+ opts;
29
+ identity;
30
+ bus = new EventBus();
31
+ router;
32
+ context;
33
+ loop = null;
34
+ constructor(opts) {
35
+ this.opts = opts;
36
+ this.identity = opts.identity;
37
+ this.router = new AutoRouter(opts.providers, concatCandidates(opts.model), opts.strategy);
38
+ this.context = new ContextManager(opts.systemPrompt);
39
+ if (opts.bus) {
40
+ for (const name of FORWARDED_EVENTS) {
41
+ this.bus.on(name, (arg) => opts.bus.emit(name, arg));
42
+ }
43
+ }
44
+ }
45
+ get label() {
46
+ return this.opts.label;
47
+ }
48
+ get modelId() {
49
+ return this.opts.model.primary;
50
+ }
51
+ get candidateList() {
52
+ return this.router.candidateList;
53
+ }
54
+ /** True while a loop is running (active or parked waiting for a reply). */
55
+ get isRunning() {
56
+ return this.loop !== null;
57
+ }
58
+ get mail() {
59
+ return this.opts.messenger.mailboxLength(this.identity.id);
60
+ }
61
+ async run(input) {
62
+ this.loop = new AgentLoop(this.loopOpts());
63
+ try {
64
+ return await this.loop.run(input);
65
+ }
66
+ finally {
67
+ this.loop = null;
68
+ }
69
+ }
70
+ abort() {
71
+ this.loop?.abort();
72
+ }
73
+ loopOpts() {
74
+ const o = this.opts;
75
+ return {
76
+ bus: this.bus,
77
+ router: this.router,
78
+ context: this.context,
79
+ tools: o.tools,
80
+ permissions: o.permissions,
81
+ systemPrompt: o.systemPrompt,
82
+ maxIterations: o.maxIterations ?? 16,
83
+ maxDelegations: o.maxDelegations ?? 6,
84
+ contextBudgetTokens: o.contextBudgetTokens ?? 48_000,
85
+ timeoutMs: o.toolTimeoutMs ?? 30_000,
86
+ streamTimeoutMs: o.streamTimeoutMs ?? 120_000,
87
+ cwd: o.cwd,
88
+ readOnly: o.readOnly ?? false,
89
+ delegate: o.delegate,
90
+ maxTokensPerSecond: o.maxTokensPerSecond,
91
+ reasoning: o.reasoning,
92
+ toolContextExtras: this.toolContextExtras(),
93
+ };
94
+ }
95
+ toolContextExtras() {
96
+ const id = this.identity.id;
97
+ const { messenger, shared } = this.opts;
98
+ return {
99
+ agent: {
100
+ send: (to, content, kind = 'message') => {
101
+ const msg = messenger.send(id, to, content, kind);
102
+ return String(msg.id);
103
+ },
104
+ waitForReply: (from, timeoutMs) => messenger.waitForReply(id, from, timeoutMs, this.loop?.runSignal).then((m) => m.content),
105
+ interrupt: (target, reason) => {
106
+ messenger.interrupt(target, reason, id);
107
+ },
108
+ },
109
+ shared: {
110
+ getFacts: () => shared.getFacts(),
111
+ addFact: (fact) => {
112
+ shared.addFact(fact);
113
+ },
114
+ getJournal: (depth) => shared.getJournal(depth),
115
+ getExcerptForSelf: () => shared.excerptFor(id),
116
+ },
117
+ };
118
+ }
119
+ }
120
+ function concatCandidates(model) {
121
+ return [model.primary, ...(model.fallback ?? [])].filter(Boolean);
122
+ }