scalix 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +29 -0
- package/PARITY-PLAN.md +116 -0
- package/README.md +1 -1
- package/bin/scalix.js +1 -0
- package/dist/agent-definitions.js +100 -0
- package/dist/agent-registry.js +226 -0
- package/dist/agent.js +363 -74
- package/dist/api-client.js +77 -0
- package/dist/cli.js +724 -76
- package/dist/commands.js +53 -2
- package/dist/constants.js +43 -9
- package/dist/context-builder.js +8 -1
- package/dist/context-retriever.js +257 -0
- package/dist/git-context.js +32 -29
- package/dist/grounding-checker.js +154 -0
- package/dist/headless.js +149 -20
- package/dist/hooks.js +148 -10
- package/dist/injection-detector.js +86 -0
- package/dist/ipc-server.js +6 -2
- package/dist/keychain.js +109 -0
- package/dist/markdown.js +46 -3
- package/dist/plugins.js +132 -0
- package/dist/project-index.js +22 -1
- package/dist/sandbox.js +201 -11
- package/dist/sessions.js +96 -2
- package/dist/settings.js +194 -8
- package/dist/skills.js +269 -0
- package/dist/sub-agent.js +83 -8
- package/dist/task-board.js +105 -0
- package/dist/terminal-notify.js +73 -0
- package/dist/text-utils.js +146 -0
- package/dist/tools-advanced.js +304 -49
- package/dist/tools-browser.js +280 -19
- package/dist/tools-extra.js +9 -8
- package/dist/tools-vision.js +4 -3
- package/dist/tools.js +13 -3
- package/dist/update-checker.js +111 -0
- package/dist/worktree.js +172 -0
- package/package.json +8 -3
- package/app.py +0 -55
package/.env.example
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Required — your Scalix API key
|
|
2
|
+
SCALIX_API_KEY=sk-...
|
|
3
|
+
|
|
4
|
+
# Optional — custom API base URL (defaults to Scalix cloud)
|
|
5
|
+
# SCALIX_BASE_URL=https://api.scalix.world/v1
|
|
6
|
+
|
|
7
|
+
# Optional — override the default model
|
|
8
|
+
# SCALIX_MODEL=
|
|
9
|
+
|
|
10
|
+
# Optional — permission mode: plan | default | auto | yolo
|
|
11
|
+
# SCALIX_PERMISSION_MODE=default
|
|
12
|
+
|
|
13
|
+
# Optional — speech-to-text model for voice input
|
|
14
|
+
# SCALIX_STT_MODEL=
|
|
15
|
+
|
|
16
|
+
# Optional — log level: debug | info | warn | error
|
|
17
|
+
# LOG_LEVEL=info
|
|
18
|
+
|
|
19
|
+
# Optional — shell override (defaults to $SHELL)
|
|
20
|
+
# SCALIX_SHELL=/bin/zsh
|
|
21
|
+
|
|
22
|
+
# Optional — web search API keys (enable web_search tool)
|
|
23
|
+
# BRAVE_API_KEY=
|
|
24
|
+
# TAVILY_API_KEY=
|
|
25
|
+
|
|
26
|
+
# Optional — custom search endpoint overrides
|
|
27
|
+
# BRAVE_SEARCH_URL=
|
|
28
|
+
# TAVILY_SEARCH_URL=
|
|
29
|
+
# DDG_SEARCH_URL=
|
package/PARITY-PLAN.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Scalix Coder — Claude Code Parity Implementation Plan
|
|
2
|
+
|
|
3
|
+
**Status:** In Progress
|
|
4
|
+
**Target:** Full feature and UX parity with Claude Code
|
|
5
|
+
**Estimated Total:** 12-15 days
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Phase 1: Security & Reliability (1-2 days) ✓ COMPLETE
|
|
10
|
+
|
|
11
|
+
- [x] **1.1 Secure credential storage** — macOS Keychain / Linux libsecret via `keychain.ts`, auto-strips key from config file
|
|
12
|
+
- [x] **1.2 Auto-retry with exponential backoff** — Already existed in agent.ts (backoffDelay + circuit breaker)
|
|
13
|
+
- [x] **1.3 Non-streaming fallback** — `forceNonStreaming` flag auto-retries with `stream: false` on stream errors
|
|
14
|
+
|
|
15
|
+
## Phase 2: Chat Experience Parity (3-4 days) ✓ COMPLETE
|
|
16
|
+
|
|
17
|
+
- [x] **2.1 Syntax highlighting in code blocks** — `cli-highlight` integration for 100+ languages + render cache (128 entries)
|
|
18
|
+
- [x] **2.2 Collapsible tool groups** — Auto-groups 3+ consecutive tool items, Ctrl+O toggles expansion
|
|
19
|
+
- [x] **2.3 Multiline input** — Alt+Enter inserts newline (terminal limitation prevents Shift+Enter)
|
|
20
|
+
- [x] **2.4 "Always Allow" in permission prompts** — [A] key auto-allows tool for session, shown in prompt UI
|
|
21
|
+
- [x] **2.5 @-mention typeahead in terminal** — `@` triggers fuzzy file search from project index, Tab/Enter selects
|
|
22
|
+
- [x] **2.6 Ctrl+R reverse history search** — Incremental reverse search with visual indicator, Enter/Esc to accept/cancel
|
|
23
|
+
- [x] **2.7 External editor support** — Ctrl+X Ctrl+E opens $EDITOR with input buffer, loads edited content back
|
|
24
|
+
- [x] **2.8 Markdown render cache** — Content-hash cache (128 entries) prevents re-rendering identical content
|
|
25
|
+
- [x] **2.9 Rate limit warning banner** — Context % in status bar when >50%, rate limit warning on 429 errors, 2-min auto-clear
|
|
26
|
+
- [x] **2.10 Image paste from clipboard** — Ctrl+V detects clipboard images (macOS pngpaste/osascript, Linux xclip), shows `[Image #N]` chips, sends to vision model
|
|
27
|
+
|
|
28
|
+
## Phase 3: Permission & Hook System (2-3 days) ✓ COMPLETE
|
|
29
|
+
|
|
30
|
+
- [x] **3.1 Scoped permissions** — `run_command(git:*)` prefix matching per tool, configurable allow/deny rules in `scopedPermissions` config
|
|
31
|
+
- [x] **3.2 Enhanced hooks — 9 events** — Added Stop, SessionEnd, Notification events (now 9 total)
|
|
32
|
+
- [x] **3.3 Prompt-type hooks** — `type: 'prompt'` hooks with `getPromptHooks()` API, separate from command hooks
|
|
33
|
+
- [x] **3.4 Structured hook I/O** — JSON stdin with event/tool/args, JSON stdout parsing, exit code 2 = block with message
|
|
34
|
+
- [x] **3.5 Matcher patterns** — Pipe-separated `run_command|edit_file`, wildcard `*`, regex `/pattern/` in hook match field
|
|
35
|
+
- [x] **3.6 Permission memory** — "Always Allow" ([A] key) persisted per session, scoped rules in config, shown in `/permissions`
|
|
36
|
+
- [x] **3.7 Recent denials view** — `/permissions` shows last 10 denied tools with timestamps and args
|
|
37
|
+
|
|
38
|
+
## Phase 4: Command & Skills System (2-3 days) ✓ COMPLETE
|
|
39
|
+
|
|
40
|
+
- [x] **4.1 Rich command frontmatter** — Parse `allowed-tools`, `model`, `argument-hint`, `disable-model-invocation` from .md commands
|
|
41
|
+
- [x] **4.2 @file references in commands** — `@path/to/file` includes file content inline in command prompt
|
|
42
|
+
- [x] **4.3 Inline bash execution** — `` !`command` `` syntax executes shell and injects stdout into command prompt
|
|
43
|
+
- [x] **4.4 Positional arguments** — `$1`, `$2`, etc. in addition to existing `$ARGS`
|
|
44
|
+
- [x] **4.5 Skills framework** — `SKILL.md` with frontmatter (name, description, version, paths), progressive 3-level disclosure
|
|
45
|
+
- [x] **4.6 Skill discovery** — `/skills` command lists available skills with descriptions
|
|
46
|
+
- [x] **4.7 Path-triggered skills** — `paths:` frontmatter auto-loads skill when working in matching directories
|
|
47
|
+
- [x] **4.8 Skill resources** — `scripts/`, `references/`, `examples/`, `assets/` directories per skill
|
|
48
|
+
- [x] **4.9 Built-in skills** — feature-dev (7-phase workflow), code-review, commit-push-pr
|
|
49
|
+
|
|
50
|
+
## Phase 5: Session & Conversation (1-2 days) ✓ COMPLETE
|
|
51
|
+
|
|
52
|
+
- [x] **5.1 Auto-save sessions** — Auto-saves crash state after each assistant turn via `saveCrashState()`
|
|
53
|
+
- [x] **5.2 Session resume picker** — `/resume` tries crash recovery first, falls back to interactive session picker
|
|
54
|
+
- [x] **5.3 Session forking** — `/fork [N]` branches conversation from message N in history
|
|
55
|
+
- [x] **5.4 Session diff** — `getSessionDiff()` tracks file modifications (edit_file/write_file) per session
|
|
56
|
+
- [x] **5.5 Crash recovery** — `.crash-recovery.json` auto-saved, detected on startup, resumable via `/resume`
|
|
57
|
+
|
|
58
|
+
## Phase 6: Configuration & Settings (1 day) ✓ COMPLETE
|
|
59
|
+
|
|
60
|
+
- [x] **6.1 Local settings overrides** — `.scalix/settings.local.json` (gitignored) for per-project model/permission/limits overrides
|
|
61
|
+
- [x] **6.2 Managed/organization settings** — `managed-settings.json` for enterprise policy enforcement (permission mode, security, scoped permissions)
|
|
62
|
+
- [x] **6.3 Keybinding customization** — Covered by existing keyboard shortcuts system + Ctrl+X Ctrl+E editor support
|
|
63
|
+
- [x] **6.4 Theme support** — Covered by existing marked-terminal + cli-highlight integration (auto-adapts to terminal colors)
|
|
64
|
+
- [x] **6.5 .scalixignore file** — `.scalixignore` and `.scalix/.ignore` files for excluding paths from project indexing
|
|
65
|
+
|
|
66
|
+
## Phase 7: Advanced Features (1-2 days) ✓ COMPLETE
|
|
67
|
+
|
|
68
|
+
- [x] **7.1 Worktree support** — `--worktree` CLI flag + `/worktree` command (list/create/remove/cleanup), backed by existing `worktree.ts`
|
|
69
|
+
- [x] **7.2 CJK/RTL text support** — `text-utils.ts` with CJK/RTL detection, double-width char handling, `wrapText()`, `applyRTLMarks()`
|
|
70
|
+
- [x] **7.3 IME composition support** — `stringWidth()` for accurate cursor positioning with CJK chars, combining mark handling
|
|
71
|
+
- [x] **7.4 Idle session nudge** — 75-minute idle timer suggests `/clear` or `/compact` when conversation has 10+ messages
|
|
72
|
+
- [x] **7.5 Terminal notification passthrough** — `terminal-notify.ts` with iTerm2/Kitty/Ghostty/tmux OSC support, auto-notify on task completion
|
|
73
|
+
- [x] **7.6 Plugin management commands** — `/plugin install/uninstall/enable/disable/list` with git clone + local symlink support
|
|
74
|
+
- [x] **7.7 Installation channels** — Homebrew formula, curl install script, PowerShell installer (all in `scripts/`)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Summary
|
|
79
|
+
|
|
80
|
+
| Phase | Items | Est. Days | Priority |
|
|
81
|
+
|-------|-------|-----------|----------|
|
|
82
|
+
| 1. Security & Reliability | 3 | 1-2 | Critical |
|
|
83
|
+
| 2. Chat Experience | 10 | 3-4 | Critical |
|
|
84
|
+
| 3. Permission & Hooks | 7 | 2-3 | Critical |
|
|
85
|
+
| 4. Command & Skills | 9 | 2-3 | Important |
|
|
86
|
+
| 5. Session & Conversation | 5 | 1-2 | Important |
|
|
87
|
+
| 6. Configuration | 5 | 1 | Important |
|
|
88
|
+
| 7. Advanced Features | 7 | 1-2 | Nice to have |
|
|
89
|
+
| **Total** | **46** | **12-15** | |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Already Ahead (Preserve These Advantages)
|
|
94
|
+
|
|
95
|
+
These features already exceed Claude Code — protect them during implementation:
|
|
96
|
+
|
|
97
|
+
- 40+ built-in tools (browser automation, multi-provider search, vision, voice)
|
|
98
|
+
- Model-agnostic with multi-model complexity routing
|
|
99
|
+
- Dynamic model discovery from API
|
|
100
|
+
- Auto-convention detection and project memory
|
|
101
|
+
- Injection detection with configurable sensitivity
|
|
102
|
+
- Checkpoint/undo system (`/checkpoint`, `/undo`)
|
|
103
|
+
- VS Code: inline chat, tab completion, context menu, 6 @-mention types
|
|
104
|
+
- Session replay (`/replay`)
|
|
105
|
+
- Message queueing during processing
|
|
106
|
+
- Shell escape (`!cmd`) prefix
|
|
107
|
+
- Input stashing (Ctrl+S)
|
|
108
|
+
- Auto-verification after file edits (build/lint check)
|
|
109
|
+
- Multiple output formats (text, json, json-schema, patch, markdown)
|
|
110
|
+
- Gradient banner with ink-big-text
|
|
111
|
+
- Informative status bar (model + tokens + tools + mode + elapsed time + version)
|
|
112
|
+
- Box-drawn diff frames with addition/deletion summary
|
|
113
|
+
- Smart error hints (pattern-matched suggestions)
|
|
114
|
+
- Scroll indicators (↑ more above / ↓ more below)
|
|
115
|
+
- Distinctive ❯ prompt with spinner animation
|
|
116
|
+
- Dynamic placeholder text
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
AI-powered coding agent for the terminal and VS Code — built by [Scalix World](https://scalix.world).
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
package/bin/scalix.js
CHANGED
|
@@ -13,6 +13,7 @@ const needsHeadless =
|
|
|
13
13
|
args.includes('--pipe') || args.includes('-p') ||
|
|
14
14
|
args.includes('--ipc') ||
|
|
15
15
|
args.includes('--help') || args.includes('-h') ||
|
|
16
|
+
args.includes('--worktree') || args.includes('-w') ||
|
|
16
17
|
!process.stdin.isTTY;
|
|
17
18
|
|
|
18
19
|
const entry = resolve(__dirname, '..', 'dist', needsHeadless ? 'headless.js' : 'cli.js');
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { USER_CONFIG_DIR, PROJECT_LOOKUP_DIRS } from './constants.js';
|
|
5
|
+
const VALID_TOOL_LEVELS = new Set(['read-only', 'standard', 'all']);
|
|
6
|
+
const BUILT_IN_TYPES = new Set(['general', 'explore', 'plan']);
|
|
7
|
+
export function parseAgentDefinition(content, filename) {
|
|
8
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
9
|
+
if (!frontmatterMatch)
|
|
10
|
+
return null;
|
|
11
|
+
const [, frontmatter, body] = frontmatterMatch;
|
|
12
|
+
const instructions = body.trim();
|
|
13
|
+
if (!instructions)
|
|
14
|
+
return null;
|
|
15
|
+
const fields = {};
|
|
16
|
+
for (const line of frontmatter.split('\n')) {
|
|
17
|
+
const colonIdx = line.indexOf(':');
|
|
18
|
+
if (colonIdx === -1)
|
|
19
|
+
continue;
|
|
20
|
+
const key = line.slice(0, colonIdx).trim();
|
|
21
|
+
const val = line.slice(colonIdx + 1).trim();
|
|
22
|
+
fields[key] = val;
|
|
23
|
+
}
|
|
24
|
+
const name = fields.name || path.basename(filename, path.extname(filename));
|
|
25
|
+
if (BUILT_IN_TYPES.has(name))
|
|
26
|
+
return null;
|
|
27
|
+
const description = fields.description || `Custom agent: ${name}`;
|
|
28
|
+
const tools = VALID_TOOL_LEVELS.has(fields.tools) ? fields.tools : 'standard';
|
|
29
|
+
const maxSteps = fields.maxSteps ? parseInt(fields.maxSteps, 10) : undefined;
|
|
30
|
+
const model = fields.model && fields.model !== '(optional override)' ? fields.model : undefined;
|
|
31
|
+
const allowSpawning = fields.allowSpawning === 'true' ? true : fields.allowSpawning === 'false' ? false : undefined;
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
description,
|
|
35
|
+
tools,
|
|
36
|
+
maxSteps: maxSteps && Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : undefined,
|
|
37
|
+
model,
|
|
38
|
+
allowSpawning,
|
|
39
|
+
instructions,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function loadDefinitionsFromDir(dir) {
|
|
43
|
+
const defs = new Map();
|
|
44
|
+
try {
|
|
45
|
+
const entries = await fs.readdir(dir);
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.endsWith('.md'))
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const content = await fs.readFile(path.join(dir, entry), 'utf-8');
|
|
51
|
+
const def = parseAgentDefinition(content, entry);
|
|
52
|
+
if (def)
|
|
53
|
+
defs.set(def.name, def);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// skip unreadable files
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// directory doesn't exist — that's fine
|
|
62
|
+
}
|
|
63
|
+
return defs;
|
|
64
|
+
}
|
|
65
|
+
export async function loadAgentDefinitions(projectDir) {
|
|
66
|
+
const globalDir = path.join(os.homedir(), USER_CONFIG_DIR, 'agents');
|
|
67
|
+
const globalDefs = await loadDefinitionsFromDir(globalDir);
|
|
68
|
+
const projectDefs = new Map();
|
|
69
|
+
if (projectDir) {
|
|
70
|
+
for (const lookupDir of PROJECT_LOOKUP_DIRS) {
|
|
71
|
+
const dir = path.join(projectDir, lookupDir, 'agents');
|
|
72
|
+
const defs = await loadDefinitionsFromDir(dir);
|
|
73
|
+
for (const [name, def] of defs) {
|
|
74
|
+
projectDefs.set(name, def);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Project definitions override global
|
|
79
|
+
const merged = new Map(globalDefs);
|
|
80
|
+
for (const [name, def] of projectDefs) {
|
|
81
|
+
merged.set(name, def);
|
|
82
|
+
}
|
|
83
|
+
return merged;
|
|
84
|
+
}
|
|
85
|
+
let _cachedDefinitions = null;
|
|
86
|
+
let _cacheProjectDir;
|
|
87
|
+
export async function getAgentDefinitions(projectDir) {
|
|
88
|
+
if (_cachedDefinitions && _cacheProjectDir === projectDir)
|
|
89
|
+
return _cachedDefinitions;
|
|
90
|
+
_cachedDefinitions = await loadAgentDefinitions(projectDir);
|
|
91
|
+
_cacheProjectDir = projectDir;
|
|
92
|
+
return _cachedDefinitions;
|
|
93
|
+
}
|
|
94
|
+
export function invalidateAgentDefinitionCache() {
|
|
95
|
+
_cachedDefinitions = null;
|
|
96
|
+
_cacheProjectDir = undefined;
|
|
97
|
+
}
|
|
98
|
+
export function isBuiltInAgentType(name) {
|
|
99
|
+
return BUILT_IN_TYPES.has(name);
|
|
100
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
import { MULTI_AGENT_DEFAULTS } from './constants.js';
|
|
5
|
+
export class AgentRegistry extends EventEmitter {
|
|
6
|
+
static instance = null;
|
|
7
|
+
agents = new Map();
|
|
8
|
+
agentRefs = new Map();
|
|
9
|
+
messages = [];
|
|
10
|
+
limits;
|
|
11
|
+
constructor(limits) {
|
|
12
|
+
super();
|
|
13
|
+
this.limits = {
|
|
14
|
+
maxDepth: limits?.maxDepth ?? MULTI_AGENT_DEFAULTS.maxDepth,
|
|
15
|
+
maxConcurrentAgents: limits?.maxConcurrentAgents ?? MULTI_AGENT_DEFAULTS.maxConcurrentAgents,
|
|
16
|
+
globalTokenBudget: limits?.globalTokenBudget ?? MULTI_AGENT_DEFAULTS.globalTokenBudget,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
static getInstance(limits) {
|
|
20
|
+
if (!AgentRegistry.instance) {
|
|
21
|
+
AgentRegistry.instance = new AgentRegistry(limits);
|
|
22
|
+
}
|
|
23
|
+
return AgentRegistry.instance;
|
|
24
|
+
}
|
|
25
|
+
static resetInstance() {
|
|
26
|
+
if (AgentRegistry.instance) {
|
|
27
|
+
AgentRegistry.instance.removeAllListeners();
|
|
28
|
+
AgentRegistry.instance.agents.clear();
|
|
29
|
+
AgentRegistry.instance.agentRefs.clear();
|
|
30
|
+
AgentRegistry.instance.messages = [];
|
|
31
|
+
}
|
|
32
|
+
AgentRegistry.instance = null;
|
|
33
|
+
}
|
|
34
|
+
register(id, agentRef, parentId, metadata) {
|
|
35
|
+
const depth = parentId ? this.getDepth(parentId) + 1 : 0;
|
|
36
|
+
const entry = {
|
|
37
|
+
id,
|
|
38
|
+
parentId,
|
|
39
|
+
depth,
|
|
40
|
+
description: metadata.description,
|
|
41
|
+
model: metadata.model,
|
|
42
|
+
status: 'running',
|
|
43
|
+
tokenUsage: 0,
|
|
44
|
+
stepCount: 0,
|
|
45
|
+
spawnedAt: Date.now(),
|
|
46
|
+
completedAt: null,
|
|
47
|
+
};
|
|
48
|
+
this.agents.set(id, entry);
|
|
49
|
+
this.agentRefs.set(id, agentRef);
|
|
50
|
+
this.emit('agent:spawned', entry);
|
|
51
|
+
logger.info('Agent registered', { id, parentId, depth, description: metadata.description });
|
|
52
|
+
}
|
|
53
|
+
deregister(id, status) {
|
|
54
|
+
const entry = this.agents.get(id);
|
|
55
|
+
if (!entry)
|
|
56
|
+
return;
|
|
57
|
+
entry.status = status;
|
|
58
|
+
entry.completedAt = Date.now();
|
|
59
|
+
this.agentRefs.delete(id);
|
|
60
|
+
this.emit('agent:completed', entry);
|
|
61
|
+
logger.info('Agent deregistered', { id, status, tokens: entry.tokenUsage, steps: entry.stepCount });
|
|
62
|
+
}
|
|
63
|
+
getAgent(id) {
|
|
64
|
+
return this.agentRefs.get(id);
|
|
65
|
+
}
|
|
66
|
+
getMeta(id) {
|
|
67
|
+
return this.agents.get(id);
|
|
68
|
+
}
|
|
69
|
+
listActive() {
|
|
70
|
+
return [...this.agents.values()].filter(a => a.status === 'running');
|
|
71
|
+
}
|
|
72
|
+
listAll() {
|
|
73
|
+
return [...this.agents.values()];
|
|
74
|
+
}
|
|
75
|
+
getChildren(parentId) {
|
|
76
|
+
return [...this.agents.values()].filter(a => a.parentId === parentId);
|
|
77
|
+
}
|
|
78
|
+
getDepth(id) {
|
|
79
|
+
const entry = this.agents.get(id);
|
|
80
|
+
if (!entry)
|
|
81
|
+
return 0;
|
|
82
|
+
if (!entry.parentId)
|
|
83
|
+
return 0;
|
|
84
|
+
return 1 + this.getDepth(entry.parentId);
|
|
85
|
+
}
|
|
86
|
+
canSpawn(parentId) {
|
|
87
|
+
const activeCount = this.listActive().length;
|
|
88
|
+
if (activeCount >= this.limits.maxConcurrentAgents) {
|
|
89
|
+
return { allowed: false, reason: `Concurrent agent limit reached (${activeCount}/${this.limits.maxConcurrentAgents})` };
|
|
90
|
+
}
|
|
91
|
+
if (parentId) {
|
|
92
|
+
const depth = this.getDepth(parentId) + 1;
|
|
93
|
+
if (depth >= this.limits.maxDepth) {
|
|
94
|
+
return { allowed: false, reason: `Maximum agent depth reached (${depth}/${this.limits.maxDepth})` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const totalTokens = this.getTotalTokens();
|
|
98
|
+
if (totalTokens >= this.limits.globalTokenBudget) {
|
|
99
|
+
return { allowed: false, reason: `Global token budget exhausted (${totalTokens.toLocaleString()}/${this.limits.globalTokenBudget.toLocaleString()})` };
|
|
100
|
+
}
|
|
101
|
+
return { allowed: true };
|
|
102
|
+
}
|
|
103
|
+
// --- Messaging ---
|
|
104
|
+
sendMessage(from, to, content) {
|
|
105
|
+
if (from === to) {
|
|
106
|
+
return { error: 'Cannot send a message to self' };
|
|
107
|
+
}
|
|
108
|
+
const recipient = this.agents.get(to);
|
|
109
|
+
if (!recipient) {
|
|
110
|
+
return { error: `Agent "${to}" not found` };
|
|
111
|
+
}
|
|
112
|
+
if (recipient.status !== 'running') {
|
|
113
|
+
return { error: `Agent "${to}" is not running (status: ${recipient.status})` };
|
|
114
|
+
}
|
|
115
|
+
const msg = {
|
|
116
|
+
id: randomUUID(),
|
|
117
|
+
from,
|
|
118
|
+
to,
|
|
119
|
+
content,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
read: false,
|
|
122
|
+
};
|
|
123
|
+
this.messages.push(msg);
|
|
124
|
+
this.emit('agent:message', msg);
|
|
125
|
+
return msg;
|
|
126
|
+
}
|
|
127
|
+
broadcast(from, content) {
|
|
128
|
+
const sent = [];
|
|
129
|
+
for (const agent of this.listActive()) {
|
|
130
|
+
if (agent.id === from)
|
|
131
|
+
continue;
|
|
132
|
+
const result = this.sendMessage(from, agent.id, content);
|
|
133
|
+
if ('id' in result)
|
|
134
|
+
sent.push(result);
|
|
135
|
+
}
|
|
136
|
+
return sent;
|
|
137
|
+
}
|
|
138
|
+
getUnread(agentId) {
|
|
139
|
+
return this.messages.filter(m => m.to === agentId && !m.read);
|
|
140
|
+
}
|
|
141
|
+
markRead(agentId) {
|
|
142
|
+
for (const msg of this.messages) {
|
|
143
|
+
if (msg.to === agentId && !msg.read) {
|
|
144
|
+
msg.read = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// --- Resource Tracking ---
|
|
149
|
+
recordTokens(id, tokens) {
|
|
150
|
+
const entry = this.agents.get(id);
|
|
151
|
+
if (entry) {
|
|
152
|
+
entry.tokenUsage += tokens;
|
|
153
|
+
}
|
|
154
|
+
const totalTokens = this.getTotalTokens();
|
|
155
|
+
const budgetPct = totalTokens / this.limits.globalTokenBudget;
|
|
156
|
+
if (budgetPct >= 0.9) {
|
|
157
|
+
this.emit('budget:warning', {
|
|
158
|
+
totalTokens,
|
|
159
|
+
budget: this.limits.globalTokenBudget,
|
|
160
|
+
percentage: Math.round(budgetPct * 100),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
recordStep(id) {
|
|
165
|
+
const entry = this.agents.get(id);
|
|
166
|
+
if (entry) {
|
|
167
|
+
entry.stepCount++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
getTotalTokens() {
|
|
171
|
+
let total = 0;
|
|
172
|
+
for (const entry of this.agents.values()) {
|
|
173
|
+
total += entry.tokenUsage;
|
|
174
|
+
}
|
|
175
|
+
return total;
|
|
176
|
+
}
|
|
177
|
+
getUsageSummary() {
|
|
178
|
+
const all = [...this.agents.values()];
|
|
179
|
+
return {
|
|
180
|
+
totalTokens: this.getTotalTokens(),
|
|
181
|
+
activeCount: all.filter(a => a.status === 'running').length,
|
|
182
|
+
completedCount: all.filter(a => a.status === 'completed').length,
|
|
183
|
+
failedCount: all.filter(a => a.status === 'failed' || a.status === 'aborted').length,
|
|
184
|
+
byAgent: all.map(a => ({
|
|
185
|
+
id: a.id,
|
|
186
|
+
description: a.description,
|
|
187
|
+
model: a.model,
|
|
188
|
+
tokens: a.tokenUsage,
|
|
189
|
+
steps: a.stepCount,
|
|
190
|
+
status: a.status,
|
|
191
|
+
})),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
getAgentTree(rootId) {
|
|
195
|
+
const lines = [];
|
|
196
|
+
const roots = rootId
|
|
197
|
+
? [this.agents.get(rootId)].filter(Boolean)
|
|
198
|
+
: [...this.agents.values()].filter(a => !a.parentId);
|
|
199
|
+
const render = (entry, indent) => {
|
|
200
|
+
const statusIcon = entry.status === 'running' ? '*' : entry.status === 'completed' ? '+' : 'x';
|
|
201
|
+
lines.push(`${indent}[${statusIcon}] ${entry.id.slice(0, 8)} ${entry.description} (${entry.model}, ${entry.tokenUsage.toLocaleString()} tok)`);
|
|
202
|
+
const children = this.getChildren(entry.id);
|
|
203
|
+
for (const child of children) {
|
|
204
|
+
render(child, indent + ' ');
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
for (const root of roots) {
|
|
208
|
+
render(root, '');
|
|
209
|
+
}
|
|
210
|
+
return lines.join('\n') || '(no agents)';
|
|
211
|
+
}
|
|
212
|
+
getLimits() {
|
|
213
|
+
return { ...this.limits };
|
|
214
|
+
}
|
|
215
|
+
setLimits(limits) {
|
|
216
|
+
if (limits.maxDepth !== undefined)
|
|
217
|
+
this.limits.maxDepth = limits.maxDepth;
|
|
218
|
+
if (limits.maxConcurrentAgents !== undefined)
|
|
219
|
+
this.limits.maxConcurrentAgents = limits.maxConcurrentAgents;
|
|
220
|
+
if (limits.globalTokenBudget !== undefined)
|
|
221
|
+
this.limits.globalTokenBudget = limits.globalTokenBudget;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export function generateAgentId() {
|
|
225
|
+
return randomUUID();
|
|
226
|
+
}
|