@mcoda/agents 0.1.4
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/AgentService/AgentService.d.ts +22 -0
- package/dist/AgentService/AgentService.d.ts.map +1 -0
- package/dist/AgentService/AgentService.js +291 -0
- package/dist/adapters/AdapterTypes.d.ts +29 -0
- package/dist/adapters/AdapterTypes.d.ts.map +1 -0
- package/dist/adapters/AdapterTypes.js +1 -0
- package/dist/adapters/codex/CodexAdapter.d.ts +11 -0
- package/dist/adapters/codex/CodexAdapter.d.ts.map +1 -0
- package/dist/adapters/codex/CodexAdapter.js +58 -0
- package/dist/adapters/codex/CodexCliRunner.d.ts +13 -0
- package/dist/adapters/codex/CodexCliRunner.d.ts.map +1 -0
- package/dist/adapters/codex/CodexCliRunner.js +143 -0
- package/dist/adapters/gemini/GeminiAdapter.d.ts +11 -0
- package/dist/adapters/gemini/GeminiAdapter.d.ts.map +1 -0
- package/dist/adapters/gemini/GeminiAdapter.js +53 -0
- package/dist/adapters/gemini/GeminiCliRunner.d.ts +13 -0
- package/dist/adapters/gemini/GeminiCliRunner.d.ts.map +1 -0
- package/dist/adapters/gemini/GeminiCliRunner.js +68 -0
- package/dist/adapters/local/LocalAdapter.d.ts +11 -0
- package/dist/adapters/local/LocalAdapter.d.ts.map +1 -0
- package/dist/adapters/local/LocalAdapter.js +38 -0
- package/dist/adapters/ollama/OllamaCliAdapter.d.ts +11 -0
- package/dist/adapters/ollama/OllamaCliAdapter.d.ts.map +1 -0
- package/dist/adapters/ollama/OllamaCliAdapter.js +53 -0
- package/dist/adapters/ollama/OllamaCliRunner.d.ts +13 -0
- package/dist/adapters/ollama/OllamaCliRunner.d.ts.map +1 -0
- package/dist/adapters/ollama/OllamaCliRunner.js +61 -0
- package/dist/adapters/ollama/OllamaRemoteAdapter.d.ts +23 -0
- package/dist/adapters/ollama/OllamaRemoteAdapter.d.ts.map +1 -0
- package/dist/adapters/ollama/OllamaRemoteAdapter.js +199 -0
- package/dist/adapters/openai/OpenAiAdapter.d.ts +11 -0
- package/dist/adapters/openai/OpenAiAdapter.d.ts.map +1 -0
- package/dist/adapters/openai/OpenAiAdapter.js +51 -0
- package/dist/adapters/openai/OpenAiCliAdapter.d.ts +11 -0
- package/dist/adapters/openai/OpenAiCliAdapter.d.ts.map +1 -0
- package/dist/adapters/openai/OpenAiCliAdapter.js +57 -0
- package/dist/adapters/qa/QaAdapter.d.ts +11 -0
- package/dist/adapters/qa/QaAdapter.d.ts.map +1 -0
- package/dist/adapters/qa/QaAdapter.js +37 -0
- package/dist/adapters/zhipu/ZhipuApiAdapter.d.ts +30 -0
- package/dist/adapters/zhipu/ZhipuApiAdapter.d.ts.map +1 -0
- package/dist/adapters/zhipu/ZhipuApiAdapter.js +255 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/package.json +41 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 bekir dag
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Agent, AgentAuthMetadata, AgentHealth, AgentPromptManifest } from "@mcoda/shared";
|
|
2
|
+
import { GlobalRepository } from "@mcoda/db";
|
|
3
|
+
import { AgentAdapter, InvocationRequest, InvocationResult } from "../adapters/AdapterTypes.js";
|
|
4
|
+
export declare class AgentService {
|
|
5
|
+
private repo;
|
|
6
|
+
constructor(repo: GlobalRepository);
|
|
7
|
+
static create(): Promise<AgentService>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
resolveAgent(identifier: string): Promise<Agent>;
|
|
10
|
+
getPrompts(agentId: string): Promise<AgentPromptManifest | undefined>;
|
|
11
|
+
getCapabilities(agentId: string): Promise<string[]>;
|
|
12
|
+
getAuthMetadata(agentId: string): Promise<AgentAuthMetadata>;
|
|
13
|
+
private getDecryptedSecret;
|
|
14
|
+
private buildAdapterConfig;
|
|
15
|
+
private resolveAdapterType;
|
|
16
|
+
getAdapter(agent: Agent): Promise<AgentAdapter>;
|
|
17
|
+
healthCheck(agentId: string): Promise<AgentHealth>;
|
|
18
|
+
invoke(agentId: string, request: InvocationRequest): Promise<InvocationResult>;
|
|
19
|
+
invokeStream(agentId: string, request: InvocationRequest): Promise<AsyncGenerator<InvocationResult>>;
|
|
20
|
+
private applyGatewayHandoff;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=AgentService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AgentService.d.ts","sourceRoot":"","sources":["../../src/AgentService/AgentService.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAU7C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAoFhG,qBAAa,YAAY;IACX,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,gBAAgB;WAE7B,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAKtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAUhD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAIrE,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAInD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;YAIpD,kBAAkB;YAMlB,kBAAkB;IA6BhC,OAAO,CAAC,kBAAkB;IA+BpB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,YAAY,CAAC;IAsC/C,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAmBlD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAmB9E,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;YA0B5F,mBAAmB;CASlC"}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { CryptoHelper } from "@mcoda/shared";
|
|
3
|
+
import { GlobalRepository } from "@mcoda/db";
|
|
4
|
+
import { CodexAdapter } from "../adapters/codex/CodexAdapter.js";
|
|
5
|
+
import { GeminiAdapter } from "../adapters/gemini/GeminiAdapter.js";
|
|
6
|
+
import { LocalAdapter } from "../adapters/local/LocalAdapter.js";
|
|
7
|
+
import { OllamaRemoteAdapter } from "../adapters/ollama/OllamaRemoteAdapter.js";
|
|
8
|
+
import { OllamaCliAdapter } from "../adapters/ollama/OllamaCliAdapter.js";
|
|
9
|
+
import { OpenAiAdapter } from "../adapters/openai/OpenAiAdapter.js";
|
|
10
|
+
import { OpenAiCliAdapter } from "../adapters/openai/OpenAiCliAdapter.js";
|
|
11
|
+
import { ZhipuApiAdapter } from "../adapters/zhipu/ZhipuApiAdapter.js";
|
|
12
|
+
import { QaAdapter } from "../adapters/qa/QaAdapter.js";
|
|
13
|
+
const CLI_BASED_ADAPTERS = new Set(["codex-cli", "gemini-cli", "openai-cli", "ollama-cli"]);
|
|
14
|
+
const LOCAL_ADAPTERS = new Set(["local-model"]);
|
|
15
|
+
const SUPPORTED_ADAPTERS = new Set([
|
|
16
|
+
"openai-api",
|
|
17
|
+
"codex-cli",
|
|
18
|
+
"gemini-cli",
|
|
19
|
+
"openai-cli",
|
|
20
|
+
"zhipu-api",
|
|
21
|
+
"local-model",
|
|
22
|
+
"qa-cli",
|
|
23
|
+
"ollama-remote",
|
|
24
|
+
"ollama-cli",
|
|
25
|
+
]);
|
|
26
|
+
const DEFAULT_JOB_PROMPT = "You are an mcoda agent that follows workspace runbooks and responds with actionable, concise output.";
|
|
27
|
+
const DEFAULT_CHARACTER_PROMPT = "Write clearly, avoid hallucinations, cite assumptions, and prioritize risk mitigation for the user.";
|
|
28
|
+
const HANDOFF_ENV_INLINE = "MCODA_GATEWAY_HANDOFF";
|
|
29
|
+
const HANDOFF_ENV_PATH = "MCODA_GATEWAY_HANDOFF_PATH";
|
|
30
|
+
const HANDOFF_HEADER = "[Gateway handoff]";
|
|
31
|
+
const MAX_HANDOFF_CHARS = 8000;
|
|
32
|
+
const IO_ENV = "MCODA_STREAM_IO";
|
|
33
|
+
const IO_PROMPT_ENV = "MCODA_STREAM_IO_PROMPT";
|
|
34
|
+
const IO_PREFIX = "[agent-io]";
|
|
35
|
+
const isIoEnabled = () => {
|
|
36
|
+
const raw = process.env[IO_ENV];
|
|
37
|
+
if (!raw)
|
|
38
|
+
return false;
|
|
39
|
+
const normalized = raw.trim().toLowerCase();
|
|
40
|
+
return !["0", "false", "off", "no"].includes(normalized);
|
|
41
|
+
};
|
|
42
|
+
const isIoPromptEnabled = () => {
|
|
43
|
+
const raw = process.env[IO_PROMPT_ENV];
|
|
44
|
+
if (!raw)
|
|
45
|
+
return true;
|
|
46
|
+
const normalized = raw.trim().toLowerCase();
|
|
47
|
+
return !["0", "false", "off", "no"].includes(normalized);
|
|
48
|
+
};
|
|
49
|
+
const emitIoLine = (line) => {
|
|
50
|
+
const normalized = line.endsWith("\n") ? line : `${line}\n`;
|
|
51
|
+
process.stderr.write(normalized);
|
|
52
|
+
};
|
|
53
|
+
const renderIoHeader = (agent, request, mode) => {
|
|
54
|
+
emitIoLine(`${IO_PREFIX} begin agent=${agent.slug ?? agent.id} adapter=${agent.adapter} model=${agent.defaultModel ?? "default"} mode=${mode}`);
|
|
55
|
+
const command = request.metadata?.command ? ` command=${String(request.metadata.command)}` : "";
|
|
56
|
+
if (command)
|
|
57
|
+
emitIoLine(`${IO_PREFIX} meta${command}`);
|
|
58
|
+
if (isIoPromptEnabled()) {
|
|
59
|
+
emitIoLine(`${IO_PREFIX} input`);
|
|
60
|
+
emitIoLine(request.input ?? "");
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const renderIoChunk = (chunk) => {
|
|
64
|
+
if (chunk.output) {
|
|
65
|
+
emitIoLine(`${IO_PREFIX} output ${chunk.output}`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const renderIoEnd = () => {
|
|
69
|
+
emitIoLine(`${IO_PREFIX} end`);
|
|
70
|
+
};
|
|
71
|
+
const readGatewayHandoff = async () => {
|
|
72
|
+
const inline = process.env[HANDOFF_ENV_INLINE];
|
|
73
|
+
if (inline && inline.trim()) {
|
|
74
|
+
return inline.trim().slice(0, MAX_HANDOFF_CHARS);
|
|
75
|
+
}
|
|
76
|
+
const filePath = process.env[HANDOFF_ENV_PATH];
|
|
77
|
+
if (!filePath)
|
|
78
|
+
return undefined;
|
|
79
|
+
try {
|
|
80
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
81
|
+
const trimmed = content.trim();
|
|
82
|
+
if (!trimmed)
|
|
83
|
+
return undefined;
|
|
84
|
+
return trimmed.slice(0, MAX_HANDOFF_CHARS);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
export class AgentService {
|
|
91
|
+
constructor(repo) {
|
|
92
|
+
this.repo = repo;
|
|
93
|
+
}
|
|
94
|
+
static async create() {
|
|
95
|
+
const repo = await GlobalRepository.create();
|
|
96
|
+
return new AgentService(repo);
|
|
97
|
+
}
|
|
98
|
+
async close() {
|
|
99
|
+
await this.repo.close();
|
|
100
|
+
}
|
|
101
|
+
async resolveAgent(identifier) {
|
|
102
|
+
const byId = await this.repo.getAgentById(identifier);
|
|
103
|
+
if (byId)
|
|
104
|
+
return byId;
|
|
105
|
+
const bySlug = await this.repo.getAgentBySlug(identifier);
|
|
106
|
+
if (!bySlug) {
|
|
107
|
+
throw new Error(`Agent ${identifier} not found`);
|
|
108
|
+
}
|
|
109
|
+
return bySlug;
|
|
110
|
+
}
|
|
111
|
+
async getPrompts(agentId) {
|
|
112
|
+
return this.repo.getAgentPrompts(agentId);
|
|
113
|
+
}
|
|
114
|
+
async getCapabilities(agentId) {
|
|
115
|
+
return this.repo.getAgentCapabilities(agentId);
|
|
116
|
+
}
|
|
117
|
+
async getAuthMetadata(agentId) {
|
|
118
|
+
return this.repo.getAgentAuthMetadata(agentId);
|
|
119
|
+
}
|
|
120
|
+
async getDecryptedSecret(agentId) {
|
|
121
|
+
const secret = await this.repo.getAgentAuthSecret(agentId);
|
|
122
|
+
if (!secret?.encryptedSecret)
|
|
123
|
+
return undefined;
|
|
124
|
+
return CryptoHelper.decryptSecret(secret.encryptedSecret);
|
|
125
|
+
}
|
|
126
|
+
async buildAdapterConfig(agent) {
|
|
127
|
+
const [capabilities, prompts, authMetadata] = await Promise.all([
|
|
128
|
+
this.getCapabilities(agent.id),
|
|
129
|
+
this.getPrompts(agent.id),
|
|
130
|
+
this.getAuthMetadata(agent.id),
|
|
131
|
+
]);
|
|
132
|
+
const secret = await this.getDecryptedSecret(agent.id);
|
|
133
|
+
const adapterConfig = (agent.config ?? {});
|
|
134
|
+
const mergedPrompts = {
|
|
135
|
+
agentId: agent.id,
|
|
136
|
+
jobPrompt: prompts?.jobPrompt ?? DEFAULT_JOB_PROMPT,
|
|
137
|
+
characterPrompt: prompts?.characterPrompt ?? DEFAULT_CHARACTER_PROMPT,
|
|
138
|
+
commandPrompts: prompts?.commandPrompts,
|
|
139
|
+
jobPath: prompts?.jobPath,
|
|
140
|
+
characterPath: prompts?.characterPath,
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
...adapterConfig,
|
|
144
|
+
agent,
|
|
145
|
+
capabilities,
|
|
146
|
+
model: agent.defaultModel,
|
|
147
|
+
apiKey: secret,
|
|
148
|
+
prompts: mergedPrompts,
|
|
149
|
+
authMetadata,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
resolveAdapterType(agent, apiKey) {
|
|
153
|
+
const hasSecret = Boolean(apiKey);
|
|
154
|
+
const config = agent.config;
|
|
155
|
+
const cliAdapter = config?.cliAdapter;
|
|
156
|
+
const localAdapter = config?.localAdapter;
|
|
157
|
+
let adapterType = agent.adapter;
|
|
158
|
+
if (!SUPPORTED_ADAPTERS.has(adapterType)) {
|
|
159
|
+
throw new Error(`Unsupported adapter type: ${adapterType}`);
|
|
160
|
+
}
|
|
161
|
+
if (adapterType.endsWith("-api")) {
|
|
162
|
+
if (hasSecret)
|
|
163
|
+
return adapterType;
|
|
164
|
+
if (adapterType === "codex-api" || adapterType === "openai-api") {
|
|
165
|
+
// Default to the codex CLI when API creds are missing.
|
|
166
|
+
adapterType = "codex-cli";
|
|
167
|
+
}
|
|
168
|
+
else if (adapterType === "gemini-api") {
|
|
169
|
+
adapterType = "gemini-cli";
|
|
170
|
+
}
|
|
171
|
+
else if (cliAdapter && CLI_BASED_ADAPTERS.has(cliAdapter)) {
|
|
172
|
+
adapterType = cliAdapter;
|
|
173
|
+
}
|
|
174
|
+
else if (localAdapter) {
|
|
175
|
+
throw new Error(`AUTH_REQUIRED: API credentials missing for adapter ${adapterType}; configure cliAdapter (${localAdapter}) or provide credentials.`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
throw new Error(`AUTH_REQUIRED: API credentials missing for adapter ${adapterType}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return adapterType;
|
|
182
|
+
}
|
|
183
|
+
async getAdapter(agent) {
|
|
184
|
+
const config = await this.buildAdapterConfig(agent);
|
|
185
|
+
const adapterType = this.resolveAdapterType(agent, config.apiKey);
|
|
186
|
+
const configWithAdapter = { ...config, adapter: adapterType };
|
|
187
|
+
if (adapterType === "openai-api") {
|
|
188
|
+
return new OpenAiAdapter(configWithAdapter);
|
|
189
|
+
}
|
|
190
|
+
if (adapterType === "zhipu-api") {
|
|
191
|
+
return new ZhipuApiAdapter(configWithAdapter);
|
|
192
|
+
}
|
|
193
|
+
if (adapterType === "codex-cli") {
|
|
194
|
+
return new CodexAdapter(configWithAdapter);
|
|
195
|
+
}
|
|
196
|
+
if (adapterType === "gemini-cli") {
|
|
197
|
+
return new GeminiAdapter(configWithAdapter);
|
|
198
|
+
}
|
|
199
|
+
if (adapterType === "openai-cli") {
|
|
200
|
+
return new OpenAiCliAdapter(configWithAdapter);
|
|
201
|
+
}
|
|
202
|
+
if (adapterType === "local-model" || LOCAL_ADAPTERS.has(adapterType)) {
|
|
203
|
+
return new LocalAdapter(configWithAdapter);
|
|
204
|
+
}
|
|
205
|
+
if (adapterType === "ollama-remote") {
|
|
206
|
+
return new OllamaRemoteAdapter(configWithAdapter);
|
|
207
|
+
}
|
|
208
|
+
if (adapterType === "ollama-cli") {
|
|
209
|
+
return new OllamaCliAdapter(configWithAdapter);
|
|
210
|
+
}
|
|
211
|
+
if (adapterType === "gemini-cli") {
|
|
212
|
+
return new GeminiAdapter(configWithAdapter);
|
|
213
|
+
}
|
|
214
|
+
if (adapterType === "qa-cli") {
|
|
215
|
+
return new QaAdapter(configWithAdapter);
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`Unsupported adapter type: ${adapterType}`);
|
|
218
|
+
}
|
|
219
|
+
async healthCheck(agentId) {
|
|
220
|
+
const agent = await this.resolveAgent(agentId);
|
|
221
|
+
try {
|
|
222
|
+
const adapter = await this.getAdapter(agent);
|
|
223
|
+
const result = await adapter.healthCheck();
|
|
224
|
+
await this.repo.setAgentHealth(result);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const failure = {
|
|
229
|
+
agentId: agent.id,
|
|
230
|
+
status: "unreachable",
|
|
231
|
+
lastCheckedAt: new Date().toISOString(),
|
|
232
|
+
details: { error: error.message },
|
|
233
|
+
};
|
|
234
|
+
await this.repo.setAgentHealth(failure);
|
|
235
|
+
return failure;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async invoke(agentId, request) {
|
|
239
|
+
const agent = await this.resolveAgent(agentId);
|
|
240
|
+
const adapter = await this.getAdapter(agent);
|
|
241
|
+
if (!adapter.invoke) {
|
|
242
|
+
throw new Error("Adapter does not support invoke");
|
|
243
|
+
}
|
|
244
|
+
const enriched = await this.applyGatewayHandoff(request);
|
|
245
|
+
const ioEnabled = isIoEnabled();
|
|
246
|
+
if (ioEnabled) {
|
|
247
|
+
renderIoHeader(agent, enriched, "invoke");
|
|
248
|
+
}
|
|
249
|
+
const result = await adapter.invoke(enriched);
|
|
250
|
+
if (ioEnabled) {
|
|
251
|
+
renderIoChunk(result);
|
|
252
|
+
renderIoEnd();
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
async invokeStream(agentId, request) {
|
|
257
|
+
const agent = await this.resolveAgent(agentId);
|
|
258
|
+
const adapter = await this.getAdapter(agent);
|
|
259
|
+
if (!adapter.invokeStream) {
|
|
260
|
+
throw new Error("Adapter does not support streaming");
|
|
261
|
+
}
|
|
262
|
+
const enriched = await this.applyGatewayHandoff(request);
|
|
263
|
+
const ioEnabled = isIoEnabled();
|
|
264
|
+
const generator = await adapter.invokeStream(enriched);
|
|
265
|
+
async function* wrap() {
|
|
266
|
+
if (ioEnabled) {
|
|
267
|
+
renderIoHeader(agent, enriched, "stream");
|
|
268
|
+
}
|
|
269
|
+
for await (const chunk of generator) {
|
|
270
|
+
if (ioEnabled) {
|
|
271
|
+
renderIoChunk(chunk);
|
|
272
|
+
}
|
|
273
|
+
yield chunk;
|
|
274
|
+
}
|
|
275
|
+
if (ioEnabled) {
|
|
276
|
+
renderIoEnd();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return wrap();
|
|
280
|
+
}
|
|
281
|
+
async applyGatewayHandoff(request) {
|
|
282
|
+
if (request.metadata?.command === "gateway-agent") {
|
|
283
|
+
return request;
|
|
284
|
+
}
|
|
285
|
+
const handoff = await readGatewayHandoff();
|
|
286
|
+
if (!handoff)
|
|
287
|
+
return request;
|
|
288
|
+
const suffix = `\n\n${HANDOFF_HEADER}\n${handoff}`;
|
|
289
|
+
return { ...request, input: `${request.input ?? ""}${suffix}` };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Agent, AgentAuthMetadata, AgentHealth, AgentPromptManifest } from "@mcoda/shared";
|
|
2
|
+
export interface AdapterConfig {
|
|
3
|
+
agent: Agent;
|
|
4
|
+
capabilities: string[];
|
|
5
|
+
model?: string;
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
prompts?: AgentPromptManifest;
|
|
8
|
+
authMetadata?: AgentAuthMetadata;
|
|
9
|
+
adapter?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AgentAdapter {
|
|
12
|
+
getCapabilities(): Promise<string[]>;
|
|
13
|
+
healthCheck(): Promise<AgentHealth>;
|
|
14
|
+
invoke?(request: InvocationRequest): Promise<InvocationResult>;
|
|
15
|
+
invokeStream?(_input: InvocationRequest): AsyncGenerator<InvocationResult, void, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface InvocationRequest {
|
|
18
|
+
input: string;
|
|
19
|
+
adapterType?: string;
|
|
20
|
+
authMode?: "api" | "cli" | "local" | "none";
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface InvocationResult {
|
|
24
|
+
output: string;
|
|
25
|
+
adapter: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=AdapterTypes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdapterTypes.d.ts","sourceRoot":"","sources":["../../src/adapters/AdapterTypes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE3F,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/D,YAAY,CAAC,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc,CAAC,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;CAC3F;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AgentHealth } from "@mcoda/shared";
|
|
2
|
+
import { AdapterConfig, AgentAdapter, InvocationRequest, InvocationResult } from "../AdapterTypes.js";
|
|
3
|
+
export declare class CodexAdapter implements AgentAdapter {
|
|
4
|
+
private config;
|
|
5
|
+
constructor(config: AdapterConfig);
|
|
6
|
+
getCapabilities(): Promise<string[]>;
|
|
7
|
+
healthCheck(): Promise<AgentHealth>;
|
|
8
|
+
invoke(request: InvocationRequest): Promise<InvocationResult>;
|
|
9
|
+
invokeStream(request: InvocationRequest): AsyncGenerator<InvocationResult, void, unknown>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=CodexAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodexAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/codex/CodexAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtG,qBAAa,YAAa,YAAW,YAAY;IACnC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAEnC,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAIpC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC;IAYnC,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAmB5D,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAAC,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC;CAoBjG"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { cliHealthy, runCodexExec, runCodexExecStream } from "./CodexCliRunner.js";
|
|
2
|
+
export class CodexAdapter {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.config = config;
|
|
5
|
+
}
|
|
6
|
+
async getCapabilities() {
|
|
7
|
+
return this.config.capabilities;
|
|
8
|
+
}
|
|
9
|
+
async healthCheck() {
|
|
10
|
+
const started = Date.now();
|
|
11
|
+
const result = cliHealthy();
|
|
12
|
+
return {
|
|
13
|
+
agentId: this.config.agent.id,
|
|
14
|
+
status: result.ok ? "healthy" : "unreachable",
|
|
15
|
+
lastCheckedAt: new Date().toISOString(),
|
|
16
|
+
latencyMs: Date.now() - started,
|
|
17
|
+
details: { adapter: "codex-cli", ...result.details },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async invoke(request) {
|
|
21
|
+
const health = cliHealthy(true);
|
|
22
|
+
const cliDetails = health.details;
|
|
23
|
+
const result = runCodexExec(request.input, this.config.model);
|
|
24
|
+
return {
|
|
25
|
+
output: result.output,
|
|
26
|
+
adapter: this.config.adapter ?? "codex-cli",
|
|
27
|
+
model: this.config.model,
|
|
28
|
+
metadata: {
|
|
29
|
+
mode: "cli",
|
|
30
|
+
capabilities: this.config.capabilities,
|
|
31
|
+
adapterType: this.config.adapter ?? "codex-cli",
|
|
32
|
+
authMode: "cli",
|
|
33
|
+
cli: cliDetails,
|
|
34
|
+
raw: result.raw,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async *invokeStream(request) {
|
|
39
|
+
const health = cliHealthy(true);
|
|
40
|
+
const cliDetails = health.details;
|
|
41
|
+
for await (const chunk of runCodexExecStream(request.input, this.config.model)) {
|
|
42
|
+
yield {
|
|
43
|
+
output: chunk.output,
|
|
44
|
+
adapter: this.config.adapter ?? "codex-cli",
|
|
45
|
+
model: this.config.model,
|
|
46
|
+
metadata: {
|
|
47
|
+
mode: "cli",
|
|
48
|
+
capabilities: this.config.capabilities,
|
|
49
|
+
adapterType: this.config.adapter ?? "codex-cli",
|
|
50
|
+
authMode: "cli",
|
|
51
|
+
cli: cliDetails,
|
|
52
|
+
raw: chunk.raw,
|
|
53
|
+
streaming: true,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const cliHealthy: (throwOnError?: boolean) => {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
details?: Record<string, unknown>;
|
|
4
|
+
};
|
|
5
|
+
export declare const runCodexExec: (prompt: string, model?: string) => {
|
|
6
|
+
output: string;
|
|
7
|
+
raw: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function runCodexExecStream(prompt: string, model?: string): AsyncGenerator<{
|
|
10
|
+
output: string;
|
|
11
|
+
raw: string;
|
|
12
|
+
}, void, unknown>;
|
|
13
|
+
//# sourceMappingURL=CodexCliRunner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodexCliRunner.d.ts","sourceRoot":"","sources":["../../../src/adapters/codex/CodexCliRunner.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,GAAI,sBAAoB,KAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CA2BjG,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,EAAE,QAAQ,MAAM,KAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAyC1F,CAAC;AAEF,wBAAuB,kBAAkB,CACvC,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,cAAc,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CA0EhE"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
const CODEX_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
3
|
+
export const cliHealthy = (throwOnError = false) => {
|
|
4
|
+
if (process.env.MCODA_CLI_STUB === "1") {
|
|
5
|
+
return { ok: true, details: { stub: true } };
|
|
6
|
+
}
|
|
7
|
+
if (process.env.MCODA_SKIP_CLI_CHECKS === "1") {
|
|
8
|
+
return { ok: true, details: { skipped: true } };
|
|
9
|
+
}
|
|
10
|
+
const result = spawnSync("codex", ["--version"], { encoding: "utf8", maxBuffer: CODEX_MAX_BUFFER_BYTES });
|
|
11
|
+
if (result.error) {
|
|
12
|
+
const details = { reason: "missing_cli", error: result.error.message };
|
|
13
|
+
if (throwOnError) {
|
|
14
|
+
const error = new Error(`AUTH_ERROR: codex CLI unavailable (${details.reason})`);
|
|
15
|
+
error.details = details;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
return { ok: false, details };
|
|
19
|
+
}
|
|
20
|
+
if (result.status !== 0) {
|
|
21
|
+
const details = { reason: "cli_error", exitCode: result.status, stderr: result.stderr?.toString() };
|
|
22
|
+
if (throwOnError) {
|
|
23
|
+
const error = new Error(`AUTH_ERROR: codex CLI unavailable (${details.reason})`);
|
|
24
|
+
error.details = details;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return { ok: false, details };
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, details: { version: result.stdout?.toString().trim() } };
|
|
30
|
+
};
|
|
31
|
+
export const runCodexExec = (prompt, model) => {
|
|
32
|
+
if (process.env.MCODA_CLI_STUB === "1") {
|
|
33
|
+
const output = `qa-stub:${prompt}`;
|
|
34
|
+
const raw = JSON.stringify({ type: "item.completed", item: { type: "agent_message", text: output } });
|
|
35
|
+
return { output, raw };
|
|
36
|
+
}
|
|
37
|
+
const health = cliHealthy(true);
|
|
38
|
+
const args = ["exec", "--model", model ?? "gpt-5.1-codex-max", "--full-auto", "--json"];
|
|
39
|
+
const result = spawnSync("codex", args, {
|
|
40
|
+
input: prompt,
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
maxBuffer: CODEX_MAX_BUFFER_BYTES,
|
|
43
|
+
});
|
|
44
|
+
if (result.error) {
|
|
45
|
+
const error = new Error(`AUTH_ERROR: codex CLI failed (${result.error.message})`);
|
|
46
|
+
error.details = { reason: "cli_error", cli: health.details };
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
if (result.status !== 0) {
|
|
50
|
+
const error = new Error(`AUTH_ERROR: codex CLI failed (exit ${result.status}): ${result.stderr ?? result.stdout ?? ""}`);
|
|
51
|
+
error.details = { reason: "cli_error", exitCode: result.status, stderr: result.stderr };
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const raw = result.stdout?.toString() ?? "";
|
|
55
|
+
const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
56
|
+
let message;
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(line);
|
|
60
|
+
if (parsed?.type === "item.completed" && parsed?.item?.type === "agent_message" && typeof parsed?.item?.text === "string") {
|
|
61
|
+
message = parsed.item.text;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* ignore parse errors */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!message) {
|
|
69
|
+
message = lines[lines.length - 1] ?? "";
|
|
70
|
+
}
|
|
71
|
+
return { output: message.trim(), raw };
|
|
72
|
+
};
|
|
73
|
+
export async function* runCodexExecStream(prompt, model) {
|
|
74
|
+
if (process.env.MCODA_CLI_STUB === "1") {
|
|
75
|
+
const output = `qa-stub:${prompt}\n`;
|
|
76
|
+
const raw = JSON.stringify({ type: "item.delta", item: { type: "agent_message", text: output } });
|
|
77
|
+
yield { output, raw };
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
cliHealthy(true);
|
|
81
|
+
const args = ["exec", "--model", model ?? "gpt-5.1-codex-max", "--full-auto", "--json"];
|
|
82
|
+
const child = spawn("codex", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
83
|
+
child.stdin.write(prompt);
|
|
84
|
+
child.stdin.end();
|
|
85
|
+
let stderr = "";
|
|
86
|
+
child.stderr?.setEncoding("utf8");
|
|
87
|
+
child.stderr?.on("data", (chunk) => {
|
|
88
|
+
stderr += chunk.toString();
|
|
89
|
+
});
|
|
90
|
+
const closePromise = new Promise((resolve, reject) => {
|
|
91
|
+
child.on("error", (err) => reject(err));
|
|
92
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
93
|
+
});
|
|
94
|
+
const parseLine = (line) => {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(line);
|
|
97
|
+
const item = parsed?.item;
|
|
98
|
+
if (item?.type === "agent_message" && typeof item.text === "string") {
|
|
99
|
+
return item.text;
|
|
100
|
+
}
|
|
101
|
+
// The codex CLI emits many JSONL event types (thread/turn/task/tool events).
|
|
102
|
+
// We only want the agent's textual output here.
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// `codex exec --json` is expected to emit JSONL, but it can still print non-JSON
|
|
107
|
+
// preamble lines (e.g., "Reading prompt from stdin..."). Treat those as noise.
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const normalizeOutput = (value) => (value.endsWith("\n") ? value : `${value}\n`);
|
|
112
|
+
const stream = child.stdout;
|
|
113
|
+
stream?.setEncoding("utf8");
|
|
114
|
+
let buffer = "";
|
|
115
|
+
for await (const chunk of stream ?? []) {
|
|
116
|
+
buffer += chunk;
|
|
117
|
+
let idx;
|
|
118
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
119
|
+
const line = buffer.slice(0, idx);
|
|
120
|
+
buffer = buffer.slice(idx + 1);
|
|
121
|
+
const normalized = line.replace(/\r$/, "");
|
|
122
|
+
const parsed = parseLine(normalized);
|
|
123
|
+
if (!parsed)
|
|
124
|
+
continue;
|
|
125
|
+
const output = normalizeOutput(parsed);
|
|
126
|
+
yield { output, raw: normalized };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const trailing = buffer.replace(/\r$/, "");
|
|
130
|
+
if (trailing) {
|
|
131
|
+
const parsed = parseLine(trailing);
|
|
132
|
+
if (parsed) {
|
|
133
|
+
const output = normalizeOutput(parsed);
|
|
134
|
+
yield { output, raw: trailing };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const exitCode = await closePromise;
|
|
138
|
+
if (exitCode !== 0) {
|
|
139
|
+
const error = new Error(`AUTH_ERROR: codex CLI failed (exit ${exitCode}): ${stderr || "no output"}`);
|
|
140
|
+
error.details = { reason: "cli_error", exitCode, stderr };
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AgentHealth } from "@mcoda/shared";
|
|
2
|
+
import { AdapterConfig, AgentAdapter, InvocationRequest, InvocationResult } from "../AdapterTypes.js";
|
|
3
|
+
export declare class GeminiAdapter implements AgentAdapter {
|
|
4
|
+
private config;
|
|
5
|
+
constructor(config: AdapterConfig);
|
|
6
|
+
getCapabilities(): Promise<string[]>;
|
|
7
|
+
healthCheck(): Promise<AgentHealth>;
|
|
8
|
+
invoke(request: InvocationRequest): Promise<InvocationResult>;
|
|
9
|
+
invokeStream(request: InvocationRequest): AsyncGenerator<InvocationResult, void, unknown>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=GeminiAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GeminiAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/gemini/GeminiAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtG,qBAAa,aAAc,YAAW,YAAY;IACpC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAEnC,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAIpC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC;IAYnC,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAkB5D,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAAC,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC;CAgBjG"}
|