neuro-cli 4.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 +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
export interface SessionConfig {
|
|
3
|
+
model?: string;
|
|
4
|
+
systemPrompt?: string;
|
|
5
|
+
workingDirectory?: string;
|
|
6
|
+
maxTokens?: number;
|
|
7
|
+
maxIterations?: number;
|
|
8
|
+
temperature?: number;
|
|
9
|
+
autoApprove?: boolean;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
/** Optional cost rate overrides (USD per 1M tokens) */
|
|
12
|
+
inputPricePerMillion?: number;
|
|
13
|
+
outputPricePerMillion?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SessionInfo {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
status: 'active' | 'idle' | 'closed';
|
|
19
|
+
createdAt: number;
|
|
20
|
+
lastActivityAt: number;
|
|
21
|
+
messageCount: number;
|
|
22
|
+
totalInputTokens: number;
|
|
23
|
+
totalOutputTokens: number;
|
|
24
|
+
tags: string[];
|
|
25
|
+
model: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CostBreakdown {
|
|
28
|
+
inputTokens: number;
|
|
29
|
+
outputTokens: number;
|
|
30
|
+
inputCost: number;
|
|
31
|
+
outputCost: number;
|
|
32
|
+
totalCost: number;
|
|
33
|
+
currency: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SessionMessage {
|
|
36
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
37
|
+
content: string;
|
|
38
|
+
timestamp: number;
|
|
39
|
+
tokenCount?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface InterSessionMessage {
|
|
42
|
+
id: string;
|
|
43
|
+
fromSession: string;
|
|
44
|
+
toSession: string;
|
|
45
|
+
payload: unknown;
|
|
46
|
+
timestamp: number;
|
|
47
|
+
read: boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare class MultiSessionManager extends EventEmitter {
|
|
50
|
+
private sessions;
|
|
51
|
+
private currentSessionId;
|
|
52
|
+
private maxConcurrent;
|
|
53
|
+
private globalTokenBudget;
|
|
54
|
+
private totalTokensConsumed;
|
|
55
|
+
constructor(options?: {
|
|
56
|
+
maxConcurrent?: number;
|
|
57
|
+
globalTokenBudget?: number;
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Create a new independent agent session. Returns the session ID.
|
|
61
|
+
*/
|
|
62
|
+
createSession(name: string, config?: SessionConfig): Promise<string>;
|
|
63
|
+
/**
|
|
64
|
+
* Close a session by ID. Releases resources and rejects any pending replies.
|
|
65
|
+
*/
|
|
66
|
+
closeSession(id: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* List all sessions (including closed ones) with summary info.
|
|
69
|
+
*/
|
|
70
|
+
listSessions(): SessionInfo[];
|
|
71
|
+
/**
|
|
72
|
+
* Switch the active / focused session.
|
|
73
|
+
*/
|
|
74
|
+
switchSession(id: string): void;
|
|
75
|
+
/**
|
|
76
|
+
* Get the current session ID (or null if none).
|
|
77
|
+
*/
|
|
78
|
+
getCurrentSession(): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Send a prompt to a specific session and return the response.
|
|
81
|
+
* Simulates an LLM interaction: appends the prompt, generates a
|
|
82
|
+
* simulated response, and returns it.
|
|
83
|
+
*/
|
|
84
|
+
sendToSession(id: string, prompt: string): Promise<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Broadcast a prompt to all active sessions. Returns a map of
|
|
87
|
+
* session ID → response string.
|
|
88
|
+
*/
|
|
89
|
+
broadcast(prompt: string): Promise<Map<string, string>>;
|
|
90
|
+
/**
|
|
91
|
+
* Get the conversation history for a session.
|
|
92
|
+
*/
|
|
93
|
+
getSessionHistory(id: string): SessionMessage[];
|
|
94
|
+
/**
|
|
95
|
+
* Get the cost breakdown for a specific session.
|
|
96
|
+
*/
|
|
97
|
+
getSessionCost(id: string): CostBreakdown;
|
|
98
|
+
/**
|
|
99
|
+
* Post a message from one session to another's message queue.
|
|
100
|
+
* The receiving session can read it via getMessageQueue().
|
|
101
|
+
*/
|
|
102
|
+
postMessage(fromSession: string, toSession: string, message: unknown): void;
|
|
103
|
+
/**
|
|
104
|
+
* Get the pending message queue for a session.
|
|
105
|
+
*/
|
|
106
|
+
getMessageQueue(sessionId: string): InterSessionMessage[];
|
|
107
|
+
/**
|
|
108
|
+
* Drain (retrieve and clear) the message queue for a session.
|
|
109
|
+
*/
|
|
110
|
+
drainMessageQueue(sessionId: string): InterSessionMessage[];
|
|
111
|
+
/**
|
|
112
|
+
* Set the maximum number of concurrent sessions.
|
|
113
|
+
*/
|
|
114
|
+
setMaxConcurrent(max: number): void;
|
|
115
|
+
/**
|
|
116
|
+
* Get total token usage across all sessions.
|
|
117
|
+
*/
|
|
118
|
+
getTotalTokenUsage(): number;
|
|
119
|
+
/**
|
|
120
|
+
* Get aggregate cost across all sessions.
|
|
121
|
+
*/
|
|
122
|
+
getTotalCost(): CostBreakdown;
|
|
123
|
+
/**
|
|
124
|
+
* Get the number of active sessions.
|
|
125
|
+
*/
|
|
126
|
+
getActiveSessionCount(): number;
|
|
127
|
+
/**
|
|
128
|
+
* Clean up closed sessions, freeing memory.
|
|
129
|
+
*/
|
|
130
|
+
cleanup(): number;
|
|
131
|
+
private countActive;
|
|
132
|
+
private findFirstActiveSession;
|
|
133
|
+
private recordToInfo;
|
|
134
|
+
/**
|
|
135
|
+
* Rough token estimation: ~4 characters per token.
|
|
136
|
+
*/
|
|
137
|
+
private estimateTokens;
|
|
138
|
+
/**
|
|
139
|
+
* Generate a simulated LLM response for a session.
|
|
140
|
+
* In production, this would call the actual model API.
|
|
141
|
+
*/
|
|
142
|
+
private generateSimulatedResponse;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=multi-session.d.ts.map
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - Multi-Session Parallel Agents (GAP-39)
|
|
3
|
+
// Multiple agent sessions running in parallel with independent
|
|
4
|
+
// state, inter-session communication, and resource management.
|
|
5
|
+
// Uses only Node.js built-in modules.
|
|
6
|
+
// ============================================================
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import { randomUUID } from 'crypto';
|
|
9
|
+
// ---- Constants ----
|
|
10
|
+
const DEFAULT_MAX_CONCURRENT = 10;
|
|
11
|
+
const DEFAULT_MAX_TOKENS = 200_000;
|
|
12
|
+
const DEFAULT_MAX_ITERATIONS = 50;
|
|
13
|
+
const DEFAULT_INPUT_PRICE = 3.0; // USD per 1M tokens
|
|
14
|
+
const DEFAULT_OUTPUT_PRICE = 15.0; // USD per 1M tokens
|
|
15
|
+
const REPLY_TIMEOUT_MS = 120_000; // 2 minutes
|
|
16
|
+
const TOKEN_CHARS_RATIO = 4; // ~4 characters per token (heuristic)
|
|
17
|
+
// ============================================================
|
|
18
|
+
// MultiSessionManager Class
|
|
19
|
+
// ============================================================
|
|
20
|
+
export class MultiSessionManager extends EventEmitter {
|
|
21
|
+
sessions = new Map();
|
|
22
|
+
currentSessionId = null;
|
|
23
|
+
maxConcurrent;
|
|
24
|
+
globalTokenBudget;
|
|
25
|
+
totalTokensConsumed = 0;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super();
|
|
28
|
+
this.maxConcurrent = options?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
29
|
+
this.globalTokenBudget = options?.globalTokenBudget ?? 0; // 0 = unlimited
|
|
30
|
+
}
|
|
31
|
+
// ============================================================
|
|
32
|
+
// Session Lifecycle
|
|
33
|
+
// ============================================================
|
|
34
|
+
/**
|
|
35
|
+
* Create a new independent agent session. Returns the session ID.
|
|
36
|
+
*/
|
|
37
|
+
async createSession(name, config) {
|
|
38
|
+
const activeCount = this.countActive();
|
|
39
|
+
if (activeCount >= this.maxConcurrent) {
|
|
40
|
+
throw new Error(`Maximum concurrent sessions reached (${this.maxConcurrent}). ` +
|
|
41
|
+
`Close an existing session before creating a new one.`);
|
|
42
|
+
}
|
|
43
|
+
const id = randomUUID();
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const record = {
|
|
46
|
+
id,
|
|
47
|
+
name,
|
|
48
|
+
config: config ?? {},
|
|
49
|
+
history: [],
|
|
50
|
+
status: 'active',
|
|
51
|
+
createdAt: now,
|
|
52
|
+
lastActivityAt: now,
|
|
53
|
+
totalInputTokens: 0,
|
|
54
|
+
totalOutputTokens: 0,
|
|
55
|
+
messageQueue: [],
|
|
56
|
+
abortController: new AbortController(),
|
|
57
|
+
pendingReplies: new Map(),
|
|
58
|
+
};
|
|
59
|
+
// Add system message if a system prompt was provided
|
|
60
|
+
if (record.config.systemPrompt) {
|
|
61
|
+
record.history.push({
|
|
62
|
+
role: 'system',
|
|
63
|
+
content: record.config.systemPrompt,
|
|
64
|
+
timestamp: now,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
this.sessions.set(id, record);
|
|
68
|
+
// If this is the first session, auto-switch to it
|
|
69
|
+
if (this.sessions.size === 1) {
|
|
70
|
+
this.currentSessionId = id;
|
|
71
|
+
}
|
|
72
|
+
this.emit('session:created', id, name);
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Close a session by ID. Releases resources and rejects any pending replies.
|
|
77
|
+
*/
|
|
78
|
+
async closeSession(id) {
|
|
79
|
+
const record = this.sessions.get(id);
|
|
80
|
+
if (!record) {
|
|
81
|
+
throw new Error(`Session "${id}" not found`);
|
|
82
|
+
}
|
|
83
|
+
// Cancel any pending operations
|
|
84
|
+
record.abortController.abort();
|
|
85
|
+
// Reject all pending replies
|
|
86
|
+
for (const [correlationId, pending] of record.pendingReplies) {
|
|
87
|
+
clearTimeout(pending.timeout);
|
|
88
|
+
pending.reject(new Error(`Session "${id}" was closed`));
|
|
89
|
+
record.pendingReplies.delete(correlationId);
|
|
90
|
+
}
|
|
91
|
+
record.status = 'closed';
|
|
92
|
+
record.lastActivityAt = Date.now();
|
|
93
|
+
// If the closed session was current, switch to another active one
|
|
94
|
+
if (this.currentSessionId === id) {
|
|
95
|
+
this.currentSessionId = this.findFirstActiveSession();
|
|
96
|
+
}
|
|
97
|
+
this.emit('session:closed', id);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* List all sessions (including closed ones) with summary info.
|
|
101
|
+
*/
|
|
102
|
+
listSessions() {
|
|
103
|
+
const result = [];
|
|
104
|
+
const records = Array.from(this.sessions.values());
|
|
105
|
+
for (const r of records) {
|
|
106
|
+
result.push(this.recordToInfo(r));
|
|
107
|
+
}
|
|
108
|
+
return result.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Switch the active / focused session.
|
|
112
|
+
*/
|
|
113
|
+
switchSession(id) {
|
|
114
|
+
const record = this.sessions.get(id);
|
|
115
|
+
if (!record) {
|
|
116
|
+
throw new Error(`Session "${id}" not found`);
|
|
117
|
+
}
|
|
118
|
+
if (record.status === 'closed') {
|
|
119
|
+
throw new Error(`Session "${id}" is closed and cannot be switched to`);
|
|
120
|
+
}
|
|
121
|
+
this.currentSessionId = id;
|
|
122
|
+
this.emit('session:switched', id);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the current session ID (or null if none).
|
|
126
|
+
*/
|
|
127
|
+
getCurrentSession() {
|
|
128
|
+
return this.currentSessionId;
|
|
129
|
+
}
|
|
130
|
+
// ============================================================
|
|
131
|
+
// Session Communication
|
|
132
|
+
// ============================================================
|
|
133
|
+
/**
|
|
134
|
+
* Send a prompt to a specific session and return the response.
|
|
135
|
+
* Simulates an LLM interaction: appends the prompt, generates a
|
|
136
|
+
* simulated response, and returns it.
|
|
137
|
+
*/
|
|
138
|
+
async sendToSession(id, prompt) {
|
|
139
|
+
const record = this.sessions.get(id);
|
|
140
|
+
if (!record) {
|
|
141
|
+
throw new Error(`Session "${id}" not found`);
|
|
142
|
+
}
|
|
143
|
+
if (record.status === 'closed') {
|
|
144
|
+
throw new Error(`Session "${id}" is closed`);
|
|
145
|
+
}
|
|
146
|
+
// Check global token budget
|
|
147
|
+
if (this.globalTokenBudget > 0 && this.totalTokensConsumed >= this.globalTokenBudget) {
|
|
148
|
+
throw new Error('Global token budget exhausted. Cannot send more messages.');
|
|
149
|
+
}
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
// Append user message
|
|
152
|
+
const inputTokenEstimate = this.estimateTokens(prompt);
|
|
153
|
+
record.history.push({
|
|
154
|
+
role: 'user',
|
|
155
|
+
content: prompt,
|
|
156
|
+
timestamp: now,
|
|
157
|
+
tokenCount: inputTokenEstimate,
|
|
158
|
+
});
|
|
159
|
+
record.totalInputTokens += inputTokenEstimate;
|
|
160
|
+
this.totalTokensConsumed += inputTokenEstimate;
|
|
161
|
+
// Simulate assistant response (in production this would call an LLM)
|
|
162
|
+
const response = this.generateSimulatedResponse(record, prompt);
|
|
163
|
+
const outputTokenEstimate = this.estimateTokens(response);
|
|
164
|
+
record.history.push({
|
|
165
|
+
role: 'assistant',
|
|
166
|
+
content: response,
|
|
167
|
+
timestamp: Date.now(),
|
|
168
|
+
tokenCount: outputTokenEstimate,
|
|
169
|
+
});
|
|
170
|
+
record.totalOutputTokens += outputTokenEstimate;
|
|
171
|
+
this.totalTokensConsumed += outputTokenEstimate;
|
|
172
|
+
record.lastActivityAt = Date.now();
|
|
173
|
+
record.status = 'active';
|
|
174
|
+
this.emit('session:message', id, { role: 'user', content: prompt });
|
|
175
|
+
this.emit('session:message', id, { role: 'assistant', content: response });
|
|
176
|
+
return response;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Broadcast a prompt to all active sessions. Returns a map of
|
|
180
|
+
* session ID → response string.
|
|
181
|
+
*/
|
|
182
|
+
async broadcast(prompt) {
|
|
183
|
+
const results = new Map();
|
|
184
|
+
const activeIds = [];
|
|
185
|
+
const records = Array.from(this.sessions.values());
|
|
186
|
+
for (const record of records) {
|
|
187
|
+
if (record.status !== 'closed') {
|
|
188
|
+
activeIds.push(record.id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Send to all active sessions concurrently
|
|
192
|
+
const promises = activeIds.map(async (id) => {
|
|
193
|
+
try {
|
|
194
|
+
const response = await this.sendToSession(id, prompt);
|
|
195
|
+
results.set(id, response);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
results.set(id, `ERROR: ${err instanceof Error ? err.message : String(err)}`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
await Promise.allSettled(promises);
|
|
202
|
+
this.emit('broadcast', prompt, activeIds);
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
// ============================================================
|
|
206
|
+
// Session History & Cost
|
|
207
|
+
// ============================================================
|
|
208
|
+
/**
|
|
209
|
+
* Get the conversation history for a session.
|
|
210
|
+
*/
|
|
211
|
+
getSessionHistory(id) {
|
|
212
|
+
const record = this.sessions.get(id);
|
|
213
|
+
if (!record) {
|
|
214
|
+
throw new Error(`Session "${id}" not found`);
|
|
215
|
+
}
|
|
216
|
+
return [...record.history];
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get the cost breakdown for a specific session.
|
|
220
|
+
*/
|
|
221
|
+
getSessionCost(id) {
|
|
222
|
+
const record = this.sessions.get(id);
|
|
223
|
+
if (!record) {
|
|
224
|
+
throw new Error(`Session "${id}" not found`);
|
|
225
|
+
}
|
|
226
|
+
const inputPrice = record.config.inputPricePerMillion ?? DEFAULT_INPUT_PRICE;
|
|
227
|
+
const outputPrice = record.config.outputPricePerMillion ?? DEFAULT_OUTPUT_PRICE;
|
|
228
|
+
const inputCost = (record.totalInputTokens / 1_000_000) * inputPrice;
|
|
229
|
+
const outputCost = (record.totalOutputTokens / 1_000_000) * outputPrice;
|
|
230
|
+
return {
|
|
231
|
+
inputTokens: record.totalInputTokens,
|
|
232
|
+
outputTokens: record.totalOutputTokens,
|
|
233
|
+
inputCost: Math.round(inputCost * 10000) / 10000,
|
|
234
|
+
outputCost: Math.round(outputCost * 10000) / 10000,
|
|
235
|
+
totalCost: Math.round((inputCost + outputCost) * 10000) / 10000,
|
|
236
|
+
currency: 'USD',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// ============================================================
|
|
240
|
+
// Inter-Session Messaging
|
|
241
|
+
// ============================================================
|
|
242
|
+
/**
|
|
243
|
+
* Post a message from one session to another's message queue.
|
|
244
|
+
* The receiving session can read it via getMessageQueue().
|
|
245
|
+
*/
|
|
246
|
+
postMessage(fromSession, toSession, message) {
|
|
247
|
+
const fromRecord = this.sessions.get(fromSession);
|
|
248
|
+
if (!fromRecord) {
|
|
249
|
+
throw new Error(`Source session "${fromSession}" not found`);
|
|
250
|
+
}
|
|
251
|
+
const toRecord = this.sessions.get(toSession);
|
|
252
|
+
if (!toRecord) {
|
|
253
|
+
throw new Error(`Target session "${toSession}" not found`);
|
|
254
|
+
}
|
|
255
|
+
if (toRecord.status === 'closed') {
|
|
256
|
+
throw new Error(`Target session "${toSession}" is closed`);
|
|
257
|
+
}
|
|
258
|
+
const msg = {
|
|
259
|
+
id: randomUUID(),
|
|
260
|
+
fromSession,
|
|
261
|
+
toSession,
|
|
262
|
+
payload: message,
|
|
263
|
+
timestamp: Date.now(),
|
|
264
|
+
read: false,
|
|
265
|
+
};
|
|
266
|
+
toRecord.messageQueue.push(msg);
|
|
267
|
+
toRecord.lastActivityAt = Date.now();
|
|
268
|
+
this.emit('session:intermessage', fromSession, toSession, msg);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get the pending message queue for a session.
|
|
272
|
+
*/
|
|
273
|
+
getMessageQueue(sessionId) {
|
|
274
|
+
const record = this.sessions.get(sessionId);
|
|
275
|
+
if (!record) {
|
|
276
|
+
throw new Error(`Session "${sessionId}" not found`);
|
|
277
|
+
}
|
|
278
|
+
// Mark all as read
|
|
279
|
+
const messages = [...record.messageQueue];
|
|
280
|
+
for (const msg of messages) {
|
|
281
|
+
msg.read = true;
|
|
282
|
+
}
|
|
283
|
+
return messages;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Drain (retrieve and clear) the message queue for a session.
|
|
287
|
+
*/
|
|
288
|
+
drainMessageQueue(sessionId) {
|
|
289
|
+
const record = this.sessions.get(sessionId);
|
|
290
|
+
if (!record) {
|
|
291
|
+
throw new Error(`Session "${sessionId}" not found`);
|
|
292
|
+
}
|
|
293
|
+
const messages = [...record.messageQueue];
|
|
294
|
+
for (const msg of messages) {
|
|
295
|
+
msg.read = true;
|
|
296
|
+
}
|
|
297
|
+
record.messageQueue = [];
|
|
298
|
+
return messages;
|
|
299
|
+
}
|
|
300
|
+
// ============================================================
|
|
301
|
+
// Resource Management
|
|
302
|
+
// ============================================================
|
|
303
|
+
/**
|
|
304
|
+
* Set the maximum number of concurrent sessions.
|
|
305
|
+
*/
|
|
306
|
+
setMaxConcurrent(max) {
|
|
307
|
+
if (max < 1) {
|
|
308
|
+
throw new Error('Max concurrent sessions must be at least 1');
|
|
309
|
+
}
|
|
310
|
+
this.maxConcurrent = max;
|
|
311
|
+
this.emit('config:maxConcurrent', max);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get total token usage across all sessions.
|
|
315
|
+
*/
|
|
316
|
+
getTotalTokenUsage() {
|
|
317
|
+
return this.totalTokensConsumed;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get aggregate cost across all sessions.
|
|
321
|
+
*/
|
|
322
|
+
getTotalCost() {
|
|
323
|
+
let totalInputTokens = 0;
|
|
324
|
+
let totalOutputTokens = 0;
|
|
325
|
+
let totalInputCost = 0;
|
|
326
|
+
let totalOutputCost = 0;
|
|
327
|
+
const records = Array.from(this.sessions.values());
|
|
328
|
+
for (const record of records) {
|
|
329
|
+
const inputPrice = record.config.inputPricePerMillion ?? DEFAULT_INPUT_PRICE;
|
|
330
|
+
const outputPrice = record.config.outputPricePerMillion ?? DEFAULT_OUTPUT_PRICE;
|
|
331
|
+
totalInputTokens += record.totalInputTokens;
|
|
332
|
+
totalOutputTokens += record.totalOutputTokens;
|
|
333
|
+
totalInputCost += (record.totalInputTokens / 1_000_000) * inputPrice;
|
|
334
|
+
totalOutputCost += (record.totalOutputTokens / 1_000_000) * outputPrice;
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
inputTokens: totalInputTokens,
|
|
338
|
+
outputTokens: totalOutputTokens,
|
|
339
|
+
inputCost: Math.round(totalInputCost * 10000) / 10000,
|
|
340
|
+
outputCost: Math.round(totalOutputCost * 10000) / 10000,
|
|
341
|
+
totalCost: Math.round((totalInputCost + totalOutputCost) * 10000) / 10000,
|
|
342
|
+
currency: 'USD',
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get the number of active sessions.
|
|
347
|
+
*/
|
|
348
|
+
getActiveSessionCount() {
|
|
349
|
+
return this.countActive();
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Clean up closed sessions, freeing memory.
|
|
353
|
+
*/
|
|
354
|
+
cleanup() {
|
|
355
|
+
let cleaned = 0;
|
|
356
|
+
const entries = Array.from(this.sessions.entries());
|
|
357
|
+
for (const [id, record] of entries) {
|
|
358
|
+
if (record.status === 'closed') {
|
|
359
|
+
// Reject any remaining pending replies
|
|
360
|
+
for (const [, pending] of record.pendingReplies) {
|
|
361
|
+
clearTimeout(pending.timeout);
|
|
362
|
+
pending.reject(new Error('Session cleaned up'));
|
|
363
|
+
}
|
|
364
|
+
this.sessions.delete(id);
|
|
365
|
+
cleaned++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
this.emit('cleanup', cleaned);
|
|
369
|
+
return cleaned;
|
|
370
|
+
}
|
|
371
|
+
// ============================================================
|
|
372
|
+
// Private Helpers
|
|
373
|
+
// ============================================================
|
|
374
|
+
countActive() {
|
|
375
|
+
let count = 0;
|
|
376
|
+
const records = Array.from(this.sessions.values());
|
|
377
|
+
for (const record of records) {
|
|
378
|
+
if (record.status !== 'closed') {
|
|
379
|
+
count++;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return count;
|
|
383
|
+
}
|
|
384
|
+
findFirstActiveSession() {
|
|
385
|
+
const records = Array.from(this.sessions.values());
|
|
386
|
+
for (const record of records) {
|
|
387
|
+
if (record.status !== 'closed') {
|
|
388
|
+
return record.id;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
recordToInfo(record) {
|
|
394
|
+
return {
|
|
395
|
+
id: record.id,
|
|
396
|
+
name: record.name,
|
|
397
|
+
status: record.status,
|
|
398
|
+
createdAt: record.createdAt,
|
|
399
|
+
lastActivityAt: record.lastActivityAt,
|
|
400
|
+
messageCount: record.history.filter((m) => m.role !== 'system').length,
|
|
401
|
+
totalInputTokens: record.totalInputTokens,
|
|
402
|
+
totalOutputTokens: record.totalOutputTokens,
|
|
403
|
+
tags: record.config.tags ?? [],
|
|
404
|
+
model: record.config.model ?? 'default',
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Rough token estimation: ~4 characters per token.
|
|
409
|
+
*/
|
|
410
|
+
estimateTokens(text) {
|
|
411
|
+
return Math.ceil(text.length / TOKEN_CHARS_RATIO);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Generate a simulated LLM response for a session.
|
|
415
|
+
* In production, this would call the actual model API.
|
|
416
|
+
*/
|
|
417
|
+
generateSimulatedResponse(record, prompt) {
|
|
418
|
+
const sessionName = record.name;
|
|
419
|
+
const historyLength = record.history.length;
|
|
420
|
+
const maxIterations = record.config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
421
|
+
// Build context from recent history (last 5 non-system messages)
|
|
422
|
+
const recentMessages = record.history
|
|
423
|
+
.filter((m) => m.role !== 'system')
|
|
424
|
+
.slice(-5);
|
|
425
|
+
const contextSummary = recentMessages
|
|
426
|
+
.map((m) => `[${m.role}]: ${m.content.substring(0, 80)}...`)
|
|
427
|
+
.join('\n');
|
|
428
|
+
const lines = [
|
|
429
|
+
`[Session "${sessionName}" — response #${Math.floor(historyLength / 2)}]`,
|
|
430
|
+
'',
|
|
431
|
+
`Received prompt (${prompt.length} chars). Context window contains ${historyLength} messages.`,
|
|
432
|
+
'',
|
|
433
|
+
`Recent context:`,
|
|
434
|
+
contextSummary,
|
|
435
|
+
'',
|
|
436
|
+
`Remaining iterations: ${Math.max(0, maxIterations - Math.floor(historyLength / 2))}`,
|
|
437
|
+
`Session tokens: ${record.totalInputTokens + record.totalOutputTokens} (est.)`,
|
|
438
|
+
];
|
|
439
|
+
return lines.join('\n');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
//# sourceMappingURL=multi-session.js.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export type ImageFormat = 'png' | 'jpeg' | 'gif' | 'webp';
|
|
2
|
+
export interface MultimodalConfig {
|
|
3
|
+
/** Max image file size in bytes (default: 20MB) */
|
|
4
|
+
maxImageSize: number;
|
|
5
|
+
/** Supported formats */
|
|
6
|
+
supportedFormats: ImageFormat[];
|
|
7
|
+
/** Whether to auto-detect images in prompts */
|
|
8
|
+
autoDetectImages: boolean;
|
|
9
|
+
/** Whether to resize large images */
|
|
10
|
+
autoResize: boolean;
|
|
11
|
+
/** Maximum dimension for resized images (default: 2048) */
|
|
12
|
+
maxDimension: number;
|
|
13
|
+
/** Image quality for JPEG compression (0-100) */
|
|
14
|
+
jpegQuality: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ImageContent {
|
|
17
|
+
/** Base64 encoded image data */
|
|
18
|
+
base64: string;
|
|
19
|
+
/** MIME type */
|
|
20
|
+
mimeType: string;
|
|
21
|
+
/** Original file path */
|
|
22
|
+
filePath: string;
|
|
23
|
+
/** File size in bytes */
|
|
24
|
+
size: number;
|
|
25
|
+
/** Image dimensions if available */
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
/** Format */
|
|
29
|
+
format: ImageFormat;
|
|
30
|
+
}
|
|
31
|
+
export interface MultimodalMessage {
|
|
32
|
+
role: 'user' | 'assistant';
|
|
33
|
+
content: MultimodalContentPart[];
|
|
34
|
+
}
|
|
35
|
+
export type MultimodalContentPart = {
|
|
36
|
+
type: 'text';
|
|
37
|
+
text: string;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'image_url';
|
|
40
|
+
image_url: {
|
|
41
|
+
url: string;
|
|
42
|
+
detail?: 'auto' | 'low' | 'high';
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export interface VisionModelInfo {
|
|
46
|
+
modelId: string;
|
|
47
|
+
supportsVision: boolean;
|
|
48
|
+
maxImages: number;
|
|
49
|
+
}
|
|
50
|
+
export declare class MultimodalSupport {
|
|
51
|
+
private config;
|
|
52
|
+
private loadedImages;
|
|
53
|
+
constructor(config?: Partial<MultimodalConfig>);
|
|
54
|
+
/**
|
|
55
|
+
* Load an image from a file path and return base64 encoded content
|
|
56
|
+
*/
|
|
57
|
+
loadImage(filePath: string): ImageContent;
|
|
58
|
+
/**
|
|
59
|
+
* Load multiple images from file paths
|
|
60
|
+
*/
|
|
61
|
+
loadImages(filePaths: string[]): ImageContent[];
|
|
62
|
+
/**
|
|
63
|
+
* Build a multimodal message with text and images
|
|
64
|
+
*/
|
|
65
|
+
buildMessage(text: string, images: ImageContent[], detail?: 'auto' | 'low' | 'high'): MultimodalMessage;
|
|
66
|
+
/**
|
|
67
|
+
* Build a multimodal message from text and image file paths
|
|
68
|
+
*/
|
|
69
|
+
buildMessageFromPaths(text: string, imagePaths: string[], detail?: 'auto' | 'low' | 'high'): MultimodalMessage;
|
|
70
|
+
/**
|
|
71
|
+
* Detect image references in a text prompt
|
|
72
|
+
* Supports: @image:path, , file paths ending in image extensions
|
|
73
|
+
*/
|
|
74
|
+
detectImageReferences(text: string): string[];
|
|
75
|
+
/**
|
|
76
|
+
* Process a user prompt that may contain image references
|
|
77
|
+
* Returns the cleaned text and loaded images
|
|
78
|
+
*/
|
|
79
|
+
processPrompt(text: string): {
|
|
80
|
+
text: string;
|
|
81
|
+
images: ImageContent[];
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Check if a model supports vision
|
|
85
|
+
*/
|
|
86
|
+
isVisionModel(modelId: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Get vision model info
|
|
89
|
+
*/
|
|
90
|
+
getVisionModelInfo(modelId: string): VisionModelInfo | null;
|
|
91
|
+
/**
|
|
92
|
+
* Check if a file is a supported image
|
|
93
|
+
*/
|
|
94
|
+
isImageFile(filePath: string): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Get the format of an image file
|
|
97
|
+
*/
|
|
98
|
+
getImageFormat(filePath: string): ImageFormat | null;
|
|
99
|
+
/**
|
|
100
|
+
* Get base64 data URL for an image
|
|
101
|
+
*/
|
|
102
|
+
getDataUrl(image: ImageContent): string;
|
|
103
|
+
/**
|
|
104
|
+
* Get all loaded images
|
|
105
|
+
*/
|
|
106
|
+
getLoadedImages(): ImageContent[];
|
|
107
|
+
/**
|
|
108
|
+
* Clear the image cache
|
|
109
|
+
*/
|
|
110
|
+
clearCache(): void;
|
|
111
|
+
/**
|
|
112
|
+
* Get supported formats
|
|
113
|
+
*/
|
|
114
|
+
getSupportedFormats(): ImageFormat[];
|
|
115
|
+
/**
|
|
116
|
+
* Get config
|
|
117
|
+
*/
|
|
118
|
+
getConfig(): MultimodalConfig;
|
|
119
|
+
/**
|
|
120
|
+
* Print multimodal status
|
|
121
|
+
*/
|
|
122
|
+
printStatus(): void;
|
|
123
|
+
private resolvePath;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=multimodal.d.ts.map
|