@sheepbun/yips 0.1.1
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/dist/agent/commands/command-catalog.js +243 -0
- package/dist/agent/commands/commands.js +418 -0
- package/dist/agent/conductor.js +118 -0
- package/dist/agent/context/code-context.js +68 -0
- package/dist/agent/context/memory-store.js +159 -0
- package/dist/agent/context/session-store.js +211 -0
- package/dist/agent/protocol/tool-protocol.js +160 -0
- package/dist/agent/skills/skills.js +327 -0
- package/dist/agent/tools/tool-executor.js +415 -0
- package/dist/agent/tools/tool-safety.js +52 -0
- package/dist/app/index.js +35 -0
- package/dist/app/repl.js +105 -0
- package/dist/app/update-check.js +132 -0
- package/dist/app/version.js +51 -0
- package/dist/code-context.js +68 -0
- package/dist/colors.js +204 -0
- package/dist/command-catalog.js +242 -0
- package/dist/commands.js +350 -0
- package/dist/conductor.js +94 -0
- package/dist/config/config.js +335 -0
- package/dist/config/hooks.js +187 -0
- package/dist/config.js +335 -0
- package/dist/downloader-state.js +302 -0
- package/dist/downloader-ui.js +289 -0
- package/dist/gateway/adapters/discord.js +108 -0
- package/dist/gateway/adapters/formatting.js +96 -0
- package/dist/gateway/adapters/telegram.js +106 -0
- package/dist/gateway/adapters/types.js +2 -0
- package/dist/gateway/adapters/whatsapp.js +124 -0
- package/dist/gateway/auth-policy.js +66 -0
- package/dist/gateway/core.js +87 -0
- package/dist/gateway/headless-conductor.js +328 -0
- package/dist/gateway/message-router.js +23 -0
- package/dist/gateway/rate-limiter.js +48 -0
- package/dist/gateway/runtime/backend-policy.js +18 -0
- package/dist/gateway/runtime/discord-bot.js +104 -0
- package/dist/gateway/runtime/discord-main.js +69 -0
- package/dist/gateway/session-manager.js +77 -0
- package/dist/gateway/types.js +2 -0
- package/dist/hardware.js +92 -0
- package/dist/hooks.js +187 -0
- package/dist/index.js +34 -0
- package/dist/input-engine.js +250 -0
- package/dist/llama-client.js +227 -0
- package/dist/llama-server.js +620 -0
- package/dist/llm/llama-client.js +227 -0
- package/dist/llm/llama-server.js +620 -0
- package/dist/llm/token-counter.js +47 -0
- package/dist/memory-store.js +159 -0
- package/dist/messages.js +59 -0
- package/dist/model-downloader.js +382 -0
- package/dist/model-manager-state.js +118 -0
- package/dist/model-manager-ui.js +194 -0
- package/dist/model-manager.js +190 -0
- package/dist/models/hardware.js +92 -0
- package/dist/models/model-downloader.js +382 -0
- package/dist/models/model-manager.js +190 -0
- package/dist/prompt-box.js +78 -0
- package/dist/prompt-composer.js +498 -0
- package/dist/repl.js +105 -0
- package/dist/session-store.js +211 -0
- package/dist/spinner.js +76 -0
- package/dist/title-box.js +388 -0
- package/dist/token-counter.js +47 -0
- package/dist/tool-executor.js +415 -0
- package/dist/tool-protocol.js +121 -0
- package/dist/tool-safety.js +52 -0
- package/dist/tui/app.js +2553 -0
- package/dist/tui/startup.js +56 -0
- package/dist/tui-input-routing.js +53 -0
- package/dist/tui.js +51 -0
- package/dist/types/app-types.js +2 -0
- package/dist/types.js +2 -0
- package/dist/ui/colors.js +204 -0
- package/dist/ui/downloader/downloader-state.js +302 -0
- package/dist/ui/downloader/downloader-ui.js +289 -0
- package/dist/ui/input/input-engine.js +250 -0
- package/dist/ui/input/tui-input-routing.js +53 -0
- package/dist/ui/input/vt-session.js +168 -0
- package/dist/ui/messages.js +59 -0
- package/dist/ui/model-manager/model-manager-state.js +118 -0
- package/dist/ui/model-manager/model-manager-ui.js +194 -0
- package/dist/ui/prompt/prompt-box.js +78 -0
- package/dist/ui/prompt/prompt-composer.js +498 -0
- package/dist/ui/spinner.js +76 -0
- package/dist/ui/title-box.js +388 -0
- package/dist/ui/tui/app.js +6 -0
- package/dist/ui/tui/autocomplete.js +85 -0
- package/dist/ui/tui/constants.js +18 -0
- package/dist/ui/tui/history.js +29 -0
- package/dist/ui/tui/layout.js +341 -0
- package/dist/ui/tui/runtime-core.js +2584 -0
- package/dist/ui/tui/runtime-utils.js +53 -0
- package/dist/ui/tui/start-tui.js +54 -0
- package/dist/ui/tui/startup.js +56 -0
- package/dist/version.js +51 -0
- package/dist/vt-session.js +168 -0
- package/install.sh +457 -0
- package/package.json +128 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WhatsAppAdapter = void 0;
|
|
4
|
+
const formatting_1 = require("#gateway/adapters/formatting");
|
|
5
|
+
const DEFAULT_API_BASE_URL = "https://graph.facebook.com";
|
|
6
|
+
const DEFAULT_API_VERSION = "v21.0";
|
|
7
|
+
const DEFAULT_MAX_MESSAGE_LENGTH = 4000;
|
|
8
|
+
function isObject(value) {
|
|
9
|
+
return typeof value === "object" && value !== null;
|
|
10
|
+
}
|
|
11
|
+
function parseTimestamp(value) {
|
|
12
|
+
if (!value) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const seconds = Number.parseInt(value, 10);
|
|
16
|
+
if (!Number.isFinite(seconds)) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return new Date(seconds * 1000);
|
|
20
|
+
}
|
|
21
|
+
function findContactByWaId(contacts, waId) {
|
|
22
|
+
if (!contacts || contacts.length === 0) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return contacts.find((contact) => contact.wa_id === waId);
|
|
26
|
+
}
|
|
27
|
+
function toInboundMessage(message, value, entryId) {
|
|
28
|
+
if (message.type !== "text") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const senderId = message.from;
|
|
32
|
+
const text = message.text?.body;
|
|
33
|
+
if (typeof senderId !== "string" || typeof text !== "string") {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const contact = findContactByWaId(value.contacts, senderId);
|
|
37
|
+
return {
|
|
38
|
+
platform: "whatsapp",
|
|
39
|
+
senderId,
|
|
40
|
+
channelId: value.metadata?.phone_number_id,
|
|
41
|
+
text,
|
|
42
|
+
messageId: message.id,
|
|
43
|
+
timestamp: parseTimestamp(message.timestamp),
|
|
44
|
+
metadata: {
|
|
45
|
+
waId: contact?.wa_id ?? senderId,
|
|
46
|
+
profileName: contact?.profile?.name,
|
|
47
|
+
displayPhoneNumber: value.metadata?.display_phone_number,
|
|
48
|
+
phoneNumberId: value.metadata?.phone_number_id,
|
|
49
|
+
entryId
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function parseInboundMessages(payload) {
|
|
54
|
+
if (!isObject(payload)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const envelope = payload;
|
|
58
|
+
if (!Array.isArray(envelope.entry)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const messages = [];
|
|
62
|
+
for (const entry of envelope.entry) {
|
|
63
|
+
if (!Array.isArray(entry.changes)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const change of entry.changes) {
|
|
67
|
+
const value = change.value;
|
|
68
|
+
if (!value || !Array.isArray(value.messages)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const message of value.messages) {
|
|
72
|
+
const inbound = toInboundMessage(message, value, entry.id);
|
|
73
|
+
if (inbound) {
|
|
74
|
+
messages.push(inbound);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return messages;
|
|
80
|
+
}
|
|
81
|
+
class WhatsAppAdapter {
|
|
82
|
+
platform = "whatsapp";
|
|
83
|
+
accessToken;
|
|
84
|
+
phoneNumberId;
|
|
85
|
+
apiBaseUrl;
|
|
86
|
+
apiVersion;
|
|
87
|
+
maxMessageLength;
|
|
88
|
+
constructor(options) {
|
|
89
|
+
this.accessToken = options.accessToken.trim();
|
|
90
|
+
this.phoneNumberId = options.phoneNumberId.trim();
|
|
91
|
+
this.apiBaseUrl = (options.apiBaseUrl ?? DEFAULT_API_BASE_URL).trim().replace(/\/+$/, "");
|
|
92
|
+
this.apiVersion = (options.apiVersion ?? DEFAULT_API_VERSION).trim();
|
|
93
|
+
this.maxMessageLength = Math.max(1, Math.trunc(options.maxMessageLength ?? DEFAULT_MAX_MESSAGE_LENGTH));
|
|
94
|
+
}
|
|
95
|
+
parseInbound(payload) {
|
|
96
|
+
return parseInboundMessages(payload);
|
|
97
|
+
}
|
|
98
|
+
formatOutbound(context, response) {
|
|
99
|
+
const normalizedText = (0, formatting_1.normalizeOutboundText)(response.text);
|
|
100
|
+
const chunks = (0, formatting_1.chunkOutboundText)(normalizedText, this.maxMessageLength);
|
|
101
|
+
if (chunks.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const requests = chunks.map((chunk) => ({
|
|
105
|
+
method: "POST",
|
|
106
|
+
endpoint: `${this.apiBaseUrl}/${this.apiVersion}/${this.phoneNumberId}/messages`,
|
|
107
|
+
headers: {
|
|
108
|
+
authorization: `Bearer ${this.accessToken}`,
|
|
109
|
+
"content-type": "application/json"
|
|
110
|
+
},
|
|
111
|
+
body: {
|
|
112
|
+
messaging_product: "whatsapp",
|
|
113
|
+
recipient_type: "individual",
|
|
114
|
+
to: context.message.senderId,
|
|
115
|
+
type: "text",
|
|
116
|
+
text: {
|
|
117
|
+
body: chunk
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}));
|
|
121
|
+
return requests.length === 1 ? (requests[0] ?? null) : requests;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.WhatsAppAdapter = WhatsAppAdapter;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GatewayAuthPolicy = void 0;
|
|
4
|
+
function toSenderKey(message) {
|
|
5
|
+
return `${message.platform}:${message.senderId}`;
|
|
6
|
+
}
|
|
7
|
+
function parseAuthCommand(text) {
|
|
8
|
+
const trimmed = text.trim();
|
|
9
|
+
if (!trimmed.startsWith("/auth")) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const parts = trimmed.split(/\s+/);
|
|
13
|
+
if (parts[0] !== "/auth") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return parts[1] ?? "";
|
|
17
|
+
}
|
|
18
|
+
class GatewayAuthPolicy {
|
|
19
|
+
allowedSenderIds;
|
|
20
|
+
passphrase;
|
|
21
|
+
authenticatedSenders = new Set();
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.allowedSenderIds =
|
|
24
|
+
options.allowedSenderIds && options.allowedSenderIds.length > 0
|
|
25
|
+
? new Set(options.allowedSenderIds)
|
|
26
|
+
: null;
|
|
27
|
+
this.passphrase = options.passphrase?.trim() ? options.passphrase.trim() : null;
|
|
28
|
+
}
|
|
29
|
+
evaluate(message) {
|
|
30
|
+
if (this.allowedSenderIds && !this.allowedSenderIds.has(message.senderId)) {
|
|
31
|
+
return {
|
|
32
|
+
type: "unauthorized",
|
|
33
|
+
reason: "sender_not_allowed"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!this.passphrase) {
|
|
37
|
+
return {
|
|
38
|
+
type: "authorized"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const senderKey = toSenderKey(message);
|
|
42
|
+
if (this.authenticatedSenders.has(senderKey)) {
|
|
43
|
+
return {
|
|
44
|
+
type: "authorized"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const authAttempt = parseAuthCommand(message.text);
|
|
48
|
+
if (authAttempt === null) {
|
|
49
|
+
return {
|
|
50
|
+
type: "unauthorized",
|
|
51
|
+
reason: "passphrase_required"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (authAttempt !== this.passphrase) {
|
|
55
|
+
return {
|
|
56
|
+
type: "unauthorized",
|
|
57
|
+
reason: "passphrase_invalid"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
this.authenticatedSenders.add(senderKey);
|
|
61
|
+
return {
|
|
62
|
+
type: "authenticated"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.GatewayAuthPolicy = GatewayAuthPolicy;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GatewayCore = void 0;
|
|
4
|
+
const auth_policy_1 = require("#gateway/auth-policy");
|
|
5
|
+
const message_router_1 = require("#gateway/message-router");
|
|
6
|
+
const rate_limiter_1 = require("#gateway/rate-limiter");
|
|
7
|
+
const session_manager_1 = require("#gateway/session-manager");
|
|
8
|
+
class GatewayCore {
|
|
9
|
+
authPolicy;
|
|
10
|
+
unauthorizedMessage;
|
|
11
|
+
rateLimiter;
|
|
12
|
+
sessionManager;
|
|
13
|
+
handleMessageFn;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.authPolicy = new auth_policy_1.GatewayAuthPolicy({
|
|
16
|
+
allowedSenderIds: options.allowedSenderIds,
|
|
17
|
+
passphrase: options.passphrase
|
|
18
|
+
});
|
|
19
|
+
this.unauthorizedMessage =
|
|
20
|
+
options.unauthorizedMessage ??
|
|
21
|
+
"Access denied. Authenticate with /auth <passphrase> or contact the administrator.";
|
|
22
|
+
this.rateLimiter =
|
|
23
|
+
options.rateLimiter ??
|
|
24
|
+
new rate_limiter_1.GatewayRateLimiter({
|
|
25
|
+
maxMessages: 20,
|
|
26
|
+
windowMs: 60_000
|
|
27
|
+
});
|
|
28
|
+
this.sessionManager = options.sessionManager ?? new session_manager_1.GatewaySessionManager();
|
|
29
|
+
this.handleMessageFn = options.handleMessage;
|
|
30
|
+
}
|
|
31
|
+
async dispatch(message) {
|
|
32
|
+
const normalizedMessage = (0, message_router_1.normalizeIncomingMessage)(message);
|
|
33
|
+
const validationError = (0, message_router_1.validateIncomingMessage)(normalizedMessage);
|
|
34
|
+
if (validationError) {
|
|
35
|
+
return {
|
|
36
|
+
status: "invalid",
|
|
37
|
+
reason: validationError
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const authDecision = this.authPolicy.evaluate(normalizedMessage);
|
|
41
|
+
if (authDecision.type === "authenticated") {
|
|
42
|
+
return {
|
|
43
|
+
status: "authenticated",
|
|
44
|
+
response: {
|
|
45
|
+
text: "Authentication successful. You can now send messages."
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (authDecision.type === "unauthorized") {
|
|
50
|
+
return {
|
|
51
|
+
status: "unauthorized",
|
|
52
|
+
reason: authDecision.reason,
|
|
53
|
+
response: {
|
|
54
|
+
text: this.unauthorizedMessage
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const rate = this.rateLimiter.check(normalizedMessage.senderId);
|
|
59
|
+
if (!rate.allowed) {
|
|
60
|
+
return {
|
|
61
|
+
status: "rate_limited",
|
|
62
|
+
reason: "rate_limit_exceeded",
|
|
63
|
+
retryAfterMs: rate.retryAfterMs
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const session = this.sessionManager.getOrCreateSession(normalizedMessage);
|
|
67
|
+
const response = await this.handleMessageFn({
|
|
68
|
+
message: normalizedMessage,
|
|
69
|
+
session
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
status: "ok",
|
|
73
|
+
sessionId: session.id,
|
|
74
|
+
response
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
listSessions() {
|
|
78
|
+
return this.sessionManager.listSessions();
|
|
79
|
+
}
|
|
80
|
+
pruneIdleSessions(maxIdleMs) {
|
|
81
|
+
return this.sessionManager.pruneIdleSessions(maxIdleMs);
|
|
82
|
+
}
|
|
83
|
+
pruneRateLimiterState() {
|
|
84
|
+
return this.rateLimiter.pruneStaleCounters();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.GatewayCore = GatewayCore;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GatewayHeadlessConductor = void 0;
|
|
4
|
+
exports.createGatewayHeadlessMessageHandler = createGatewayHeadlessMessageHandler;
|
|
5
|
+
const conductor_1 = require("#agent/conductor");
|
|
6
|
+
const code_context_1 = require("#agent/context/code-context");
|
|
7
|
+
const session_store_1 = require("#agent/context/session-store");
|
|
8
|
+
const skills_1 = require("#agent/skills/skills");
|
|
9
|
+
const tool_executor_1 = require("#agent/tools/tool-executor");
|
|
10
|
+
const tool_safety_1 = require("#agent/tools/tool-safety");
|
|
11
|
+
const llama_server_1 = require("#llm/llama-server");
|
|
12
|
+
const llama_client_1 = require("#llm/llama-client");
|
|
13
|
+
const token_counter_1 = require("#llm/token-counter");
|
|
14
|
+
const vt_session_1 = require("#ui/input/vt-session");
|
|
15
|
+
const UNSUPPORTED_BACKEND_RESPONSE = "Gateway headless mode currently supports backend 'llamacpp' only.";
|
|
16
|
+
const AUTO_DENY_OUTPUT = "Action denied by gateway safety policy.";
|
|
17
|
+
function composeChatRequestMessages(history, codeContextMessage) {
|
|
18
|
+
if (!codeContextMessage) {
|
|
19
|
+
return history;
|
|
20
|
+
}
|
|
21
|
+
return [{ role: "system", content: codeContextMessage }, ...history];
|
|
22
|
+
}
|
|
23
|
+
function buildSubagentScopeMessage(call) {
|
|
24
|
+
const lines = [
|
|
25
|
+
"Subagent scope:",
|
|
26
|
+
`Task: ${call.task}`,
|
|
27
|
+
call.context ? `Context: ${call.context}` : null,
|
|
28
|
+
call.allowedTools
|
|
29
|
+
? `Allowed tools: ${call.allowedTools.length > 0 ? call.allowedTools.join(", ") : "(none)"}`
|
|
30
|
+
: "Allowed tools: all available tools",
|
|
31
|
+
"Stay focused on the delegated scope and return concise findings."
|
|
32
|
+
].filter((line) => line !== null);
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
}
|
|
35
|
+
function assessToolCallRisk(call, workingZone) {
|
|
36
|
+
if (call.name === "run_command") {
|
|
37
|
+
const command = typeof call.arguments["command"] === "string" ? call.arguments["command"] : "";
|
|
38
|
+
const cwdArg = typeof call.arguments["cwd"] === "string" ? call.arguments["cwd"] : ".";
|
|
39
|
+
const resolvedCwd = (0, tool_safety_1.resolveToolPath)(cwdArg, workingZone);
|
|
40
|
+
return (0, tool_safety_1.assessCommandRisk)(command, resolvedCwd, workingZone);
|
|
41
|
+
}
|
|
42
|
+
const pathArg = typeof call.arguments["path"] === "string" ? call.arguments["path"] : ".";
|
|
43
|
+
return (0, tool_safety_1.assessPathRisk)(pathArg, workingZone);
|
|
44
|
+
}
|
|
45
|
+
function toErrorMessage(error) {
|
|
46
|
+
return error instanceof Error ? error.message : String(error);
|
|
47
|
+
}
|
|
48
|
+
const DEFAULT_DEPS = {
|
|
49
|
+
createLlamaClient: (config) => new llama_client_1.LlamaClient({
|
|
50
|
+
baseUrl: config.llamaBaseUrl,
|
|
51
|
+
model: config.model
|
|
52
|
+
}),
|
|
53
|
+
ensureReady: llama_server_1.ensureLlamaReady,
|
|
54
|
+
formatStartupFailure: llama_server_1.formatLlamaStartupFailure,
|
|
55
|
+
runConductor: conductor_1.runConductorTurn,
|
|
56
|
+
executeTool: tool_executor_1.executeToolCall,
|
|
57
|
+
executeSkill: skills_1.executeSkillCall,
|
|
58
|
+
createVtSession: () => new vt_session_1.VirtualTerminalSession(),
|
|
59
|
+
loadCodeContext: code_context_1.loadCodeContext,
|
|
60
|
+
toCodeContextSystemMessage: code_context_1.toCodeContextSystemMessage,
|
|
61
|
+
createSessionFile: session_store_1.createSessionFileFromHistory,
|
|
62
|
+
writeSessionFile: session_store_1.writeSessionFile,
|
|
63
|
+
estimateCompletionTokens: (text) => (0, token_counter_1.estimateConversationTokens)([{ content: text }]),
|
|
64
|
+
estimateHistoryTokens: (history) => (0, token_counter_1.estimateConversationTokens)(history)
|
|
65
|
+
};
|
|
66
|
+
class GatewayHeadlessConductor {
|
|
67
|
+
config;
|
|
68
|
+
gatewayBackend;
|
|
69
|
+
username;
|
|
70
|
+
workingDirectory;
|
|
71
|
+
maxRounds;
|
|
72
|
+
deps;
|
|
73
|
+
sessionStates = new Map();
|
|
74
|
+
llamaClient;
|
|
75
|
+
vtSession;
|
|
76
|
+
codeContextMessage = null;
|
|
77
|
+
constructor(options, deps = {}) {
|
|
78
|
+
this.config = options.config;
|
|
79
|
+
this.gatewayBackend = options.gatewayBackend ?? this.config.backend;
|
|
80
|
+
this.username = options.username ?? "Gateway User";
|
|
81
|
+
this.workingDirectory = options.workingDirectory ?? process.cwd();
|
|
82
|
+
this.maxRounds = options.maxRounds;
|
|
83
|
+
this.deps = { ...DEFAULT_DEPS, ...deps };
|
|
84
|
+
this.llamaClient = this.deps.createLlamaClient(this.config);
|
|
85
|
+
this.vtSession = this.deps.createVtSession();
|
|
86
|
+
}
|
|
87
|
+
async initialize() {
|
|
88
|
+
const loaded = await this.deps.loadCodeContext(this.workingDirectory);
|
|
89
|
+
if (!loaded) {
|
|
90
|
+
this.codeContextMessage = null;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.codeContextMessage = this.deps.toCodeContextSystemMessage(loaded);
|
|
94
|
+
}
|
|
95
|
+
async handleMessage(context) {
|
|
96
|
+
if (this.gatewayBackend !== "llamacpp") {
|
|
97
|
+
return {
|
|
98
|
+
text: `${UNSUPPORTED_BACKEND_RESPONSE} Configured backend: '${this.gatewayBackend}'.`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const state = this.getOrCreateSessionState(context);
|
|
102
|
+
state.history.push({ role: "user", content: context.message.text });
|
|
103
|
+
let latestAssistantText = "";
|
|
104
|
+
try {
|
|
105
|
+
await this.deps.runConductor({
|
|
106
|
+
history: state.history,
|
|
107
|
+
requestAssistant: async () => {
|
|
108
|
+
const readiness = await this.deps.ensureReady(this.config);
|
|
109
|
+
if (!readiness.ready) {
|
|
110
|
+
throw new Error(readiness.failure
|
|
111
|
+
? this.deps.formatStartupFailure(readiness.failure, this.config)
|
|
112
|
+
: "llama.cpp is unavailable.");
|
|
113
|
+
}
|
|
114
|
+
this.llamaClient.setModel(this.config.model);
|
|
115
|
+
const requestMessages = composeChatRequestMessages(state.history, this.codeContextMessage);
|
|
116
|
+
const result = await this.llamaClient.chat(requestMessages, this.config.model);
|
|
117
|
+
return {
|
|
118
|
+
text: result.text,
|
|
119
|
+
rendered: false,
|
|
120
|
+
totalTokens: result.usage?.totalTokens,
|
|
121
|
+
completionTokens: result.usage?.completionTokens ?? this.deps.estimateCompletionTokens(result.text)
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
executeToolCalls: async (toolCalls) => {
|
|
125
|
+
const results = [];
|
|
126
|
+
for (const call of toolCalls) {
|
|
127
|
+
const risk = assessToolCallRisk(call, this.workingDirectory);
|
|
128
|
+
if (risk.requiresConfirmation) {
|
|
129
|
+
results.push({
|
|
130
|
+
callId: call.id,
|
|
131
|
+
tool: call.name,
|
|
132
|
+
status: "denied",
|
|
133
|
+
output: AUTO_DENY_OUTPUT
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const result = await this.deps.executeTool(call, {
|
|
138
|
+
workingDirectory: this.workingDirectory,
|
|
139
|
+
vtSession: this.vtSession
|
|
140
|
+
});
|
|
141
|
+
results.push(result);
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
},
|
|
145
|
+
executeSkillCalls: async (skillCalls) => {
|
|
146
|
+
const results = [];
|
|
147
|
+
for (const call of skillCalls) {
|
|
148
|
+
const result = await this.deps.executeSkill(call, {
|
|
149
|
+
workingDirectory: this.workingDirectory,
|
|
150
|
+
vtSession: this.vtSession
|
|
151
|
+
});
|
|
152
|
+
results.push(result);
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
},
|
|
156
|
+
executeSubagentCalls: async (subagentCalls) => {
|
|
157
|
+
const results = [];
|
|
158
|
+
for (const subagentCall of subagentCalls) {
|
|
159
|
+
const scopedHistory = [
|
|
160
|
+
{ role: "system", content: buildSubagentScopeMessage(subagentCall) },
|
|
161
|
+
{ role: "user", content: subagentCall.task }
|
|
162
|
+
];
|
|
163
|
+
const allowedTools = subagentCall.allowedTools !== undefined ? new Set(subagentCall.allowedTools) : null;
|
|
164
|
+
try {
|
|
165
|
+
const turn = await this.deps.runConductor({
|
|
166
|
+
history: scopedHistory,
|
|
167
|
+
requestAssistant: async () => {
|
|
168
|
+
const readiness = await this.deps.ensureReady(this.config);
|
|
169
|
+
if (!readiness.ready) {
|
|
170
|
+
throw new Error(readiness.failure
|
|
171
|
+
? this.deps.formatStartupFailure(readiness.failure, this.config)
|
|
172
|
+
: "llama.cpp is unavailable.");
|
|
173
|
+
}
|
|
174
|
+
this.llamaClient.setModel(this.config.model);
|
|
175
|
+
const requestMessages = composeChatRequestMessages(scopedHistory, null);
|
|
176
|
+
const result = await this.llamaClient.chat(requestMessages, this.config.model);
|
|
177
|
+
return {
|
|
178
|
+
text: result.text,
|
|
179
|
+
rendered: false,
|
|
180
|
+
totalTokens: result.usage?.totalTokens,
|
|
181
|
+
completionTokens: result.usage?.completionTokens ??
|
|
182
|
+
this.deps.estimateCompletionTokens(result.text)
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
executeToolCalls: async (toolCalls) => {
|
|
186
|
+
const denied = [];
|
|
187
|
+
const permitted = [];
|
|
188
|
+
for (const call of toolCalls) {
|
|
189
|
+
if (allowedTools && !allowedTools.has(call.name)) {
|
|
190
|
+
denied.push({
|
|
191
|
+
callId: call.id,
|
|
192
|
+
tool: call.name,
|
|
193
|
+
status: "denied",
|
|
194
|
+
output: `Tool '${call.name}' is not allowed for subagent ${subagentCall.id}.`
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const risk = assessToolCallRisk(call, this.workingDirectory);
|
|
199
|
+
if (risk.requiresConfirmation) {
|
|
200
|
+
denied.push({
|
|
201
|
+
callId: call.id,
|
|
202
|
+
tool: call.name,
|
|
203
|
+
status: "denied",
|
|
204
|
+
output: AUTO_DENY_OUTPUT
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
permitted.push(call);
|
|
209
|
+
}
|
|
210
|
+
const executed = [];
|
|
211
|
+
for (const call of permitted) {
|
|
212
|
+
const result = await this.deps.executeTool(call, {
|
|
213
|
+
workingDirectory: this.workingDirectory,
|
|
214
|
+
vtSession: this.vtSession
|
|
215
|
+
});
|
|
216
|
+
executed.push(result);
|
|
217
|
+
}
|
|
218
|
+
return [...denied, ...executed];
|
|
219
|
+
},
|
|
220
|
+
executeSkillCalls: async (skillCalls) => {
|
|
221
|
+
const skillResults = [];
|
|
222
|
+
for (const call of skillCalls) {
|
|
223
|
+
const result = await this.deps.executeSkill(call, {
|
|
224
|
+
workingDirectory: this.workingDirectory,
|
|
225
|
+
vtSession: this.vtSession
|
|
226
|
+
});
|
|
227
|
+
skillResults.push(result);
|
|
228
|
+
}
|
|
229
|
+
return skillResults;
|
|
230
|
+
},
|
|
231
|
+
onAssistantText: () => {
|
|
232
|
+
// Subagent responses are returned in metadata only.
|
|
233
|
+
},
|
|
234
|
+
onWarning: () => {
|
|
235
|
+
// Warnings are intentionally omitted from outbound gateway text.
|
|
236
|
+
},
|
|
237
|
+
estimateCompletionTokens: this.deps.estimateCompletionTokens,
|
|
238
|
+
estimateHistoryTokens: this.deps.estimateHistoryTokens,
|
|
239
|
+
computeTokensPerSecond: () => null,
|
|
240
|
+
maxRounds: subagentCall.maxRounds ?? 4
|
|
241
|
+
});
|
|
242
|
+
const latest = [...scopedHistory]
|
|
243
|
+
.reverse()
|
|
244
|
+
.find((entry) => entry.role === "assistant")?.content;
|
|
245
|
+
results.push({
|
|
246
|
+
callId: subagentCall.id,
|
|
247
|
+
status: turn.finished ? "ok" : "timeout",
|
|
248
|
+
output: latest ?? "Subagent completed without assistant output.",
|
|
249
|
+
metadata: {
|
|
250
|
+
rounds: turn.rounds
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
results.push({
|
|
256
|
+
callId: subagentCall.id,
|
|
257
|
+
status: "error",
|
|
258
|
+
output: `Subagent failed: ${toErrorMessage(error)}`
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return results;
|
|
263
|
+
},
|
|
264
|
+
onAssistantText: (assistantText) => {
|
|
265
|
+
latestAssistantText = assistantText;
|
|
266
|
+
},
|
|
267
|
+
onWarning: () => {
|
|
268
|
+
// Warnings are intentionally omitted from outbound gateway text.
|
|
269
|
+
},
|
|
270
|
+
estimateCompletionTokens: this.deps.estimateCompletionTokens,
|
|
271
|
+
estimateHistoryTokens: this.deps.estimateHistoryTokens,
|
|
272
|
+
computeTokensPerSecond: () => null,
|
|
273
|
+
maxRounds: this.maxRounds
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
const message = `Request failed: ${toErrorMessage(error)}`;
|
|
278
|
+
state.history.push({ role: "assistant", content: message });
|
|
279
|
+
await this.persistSession(state);
|
|
280
|
+
return { text: message };
|
|
281
|
+
}
|
|
282
|
+
const finalText = latestAssistantText.trim().length > 0 ? latestAssistantText : "(no response)";
|
|
283
|
+
await this.persistSession(state);
|
|
284
|
+
return { text: finalText };
|
|
285
|
+
}
|
|
286
|
+
dispose() {
|
|
287
|
+
this.vtSession.dispose();
|
|
288
|
+
this.sessionStates.clear();
|
|
289
|
+
}
|
|
290
|
+
getOrCreateSessionState(context) {
|
|
291
|
+
const key = `${context.session.id}:${context.session.createdAt.toISOString()}`;
|
|
292
|
+
const existing = this.sessionStates.get(key);
|
|
293
|
+
if (existing) {
|
|
294
|
+
return existing;
|
|
295
|
+
}
|
|
296
|
+
const created = {
|
|
297
|
+
history: [],
|
|
298
|
+
sessionFilePath: null
|
|
299
|
+
};
|
|
300
|
+
this.sessionStates.set(key, created);
|
|
301
|
+
return created;
|
|
302
|
+
}
|
|
303
|
+
async persistSession(state) {
|
|
304
|
+
if (state.history.length === 0) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (!state.sessionFilePath) {
|
|
308
|
+
const created = await this.deps.createSessionFile(state.history);
|
|
309
|
+
state.sessionFilePath = created.path;
|
|
310
|
+
}
|
|
311
|
+
await this.deps.writeSessionFile({
|
|
312
|
+
path: state.sessionFilePath,
|
|
313
|
+
username: this.username,
|
|
314
|
+
history: state.history
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
exports.GatewayHeadlessConductor = GatewayHeadlessConductor;
|
|
319
|
+
async function createGatewayHeadlessMessageHandler(options, deps = {}) {
|
|
320
|
+
const runtime = new GatewayHeadlessConductor(options, deps);
|
|
321
|
+
await runtime.initialize();
|
|
322
|
+
return {
|
|
323
|
+
handleMessage: async (context) => await runtime.handleMessage(context),
|
|
324
|
+
dispose: () => {
|
|
325
|
+
runtime.dispose();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeIncomingMessage = normalizeIncomingMessage;
|
|
4
|
+
exports.validateIncomingMessage = validateIncomingMessage;
|
|
5
|
+
const MAX_TEXT_LENGTH = 4000;
|
|
6
|
+
function normalizeIncomingMessage(message) {
|
|
7
|
+
return {
|
|
8
|
+
...message,
|
|
9
|
+
senderId: message.senderId.trim(),
|
|
10
|
+
channelId: message.channelId?.trim(),
|
|
11
|
+
text: message.text.trim().slice(0, MAX_TEXT_LENGTH),
|
|
12
|
+
timestamp: message.timestamp ?? new Date()
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function validateIncomingMessage(message) {
|
|
16
|
+
if (message.senderId.trim().length === 0) {
|
|
17
|
+
return "sender_id_required";
|
|
18
|
+
}
|
|
19
|
+
if (message.text.trim().length === 0) {
|
|
20
|
+
return "message_text_required";
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GatewayRateLimiter = void 0;
|
|
4
|
+
class GatewayRateLimiter {
|
|
5
|
+
maxMessages;
|
|
6
|
+
windowMs;
|
|
7
|
+
counters = new Map();
|
|
8
|
+
now;
|
|
9
|
+
constructor(options, now = () => new Date()) {
|
|
10
|
+
this.maxMessages = Math.max(1, Math.trunc(options.maxMessages));
|
|
11
|
+
this.windowMs = Math.max(1000, Math.trunc(options.windowMs));
|
|
12
|
+
this.now = now;
|
|
13
|
+
}
|
|
14
|
+
check(senderId) {
|
|
15
|
+
const now = this.now().valueOf();
|
|
16
|
+
const existing = this.counters.get(senderId);
|
|
17
|
+
const active = existing && now - existing.startedAt < this.windowMs ? existing : { count: 0, startedAt: now };
|
|
18
|
+
active.count += 1;
|
|
19
|
+
this.counters.set(senderId, active);
|
|
20
|
+
const resetAt = new Date(active.startedAt + this.windowMs);
|
|
21
|
+
if (active.count <= this.maxMessages) {
|
|
22
|
+
return {
|
|
23
|
+
allowed: true,
|
|
24
|
+
remaining: this.maxMessages - active.count,
|
|
25
|
+
retryAfterMs: 0,
|
|
26
|
+
resetAt
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
allowed: false,
|
|
31
|
+
remaining: 0,
|
|
32
|
+
retryAfterMs: Math.max(1, active.startedAt + this.windowMs - now),
|
|
33
|
+
resetAt
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
pruneStaleCounters() {
|
|
37
|
+
const now = this.now().valueOf();
|
|
38
|
+
let removed = 0;
|
|
39
|
+
for (const [senderId, window] of this.counters) {
|
|
40
|
+
if (now - window.startedAt >= this.windowMs) {
|
|
41
|
+
this.counters.delete(senderId);
|
|
42
|
+
removed += 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return removed;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.GatewayRateLimiter = GatewayRateLimiter;
|