@sdods/agents 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/adapter/claude-code.d.ts +29 -0
- package/dist/adapter/claude-code.js +195 -0
- package/dist/adapter/claude.d.ts +42 -0
- package/dist/adapter/claude.js +216 -0
- package/dist/adapter/cli-common.d.ts +46 -0
- package/dist/adapter/cli-common.js +145 -0
- package/dist/adapter/codex.d.ts +31 -0
- package/dist/adapter/codex.js +252 -0
- package/dist/adapter/factory.d.ts +41 -0
- package/dist/adapter/factory.js +102 -0
- package/dist/adapter/fake.d.ts +39 -0
- package/dist/adapter/fake.js +91 -0
- package/dist/adapter/index.d.ts +11 -0
- package/dist/adapter/index.js +11 -0
- package/dist/adapter/json-schema.d.ts +11 -0
- package/dist/adapter/json-schema.js +17 -0
- package/dist/adapter/mcp-bridge.d.ts +35 -0
- package/dist/adapter/mcp-bridge.js +122 -0
- package/dist/adapter/ollama.d.ts +62 -0
- package/dist/adapter/ollama.js +272 -0
- package/dist/adapter/openai-compat.d.ts +44 -0
- package/dist/adapter/openai-compat.js +200 -0
- package/dist/adapter/profile.d.ts +54 -0
- package/dist/adapter/profile.js +42 -0
- package/dist/adapter/tool-loop.d.ts +64 -0
- package/dist/adapter/tool-loop.js +192 -0
- package/dist/adapter/types.d.ts +109 -0
- package/dist/adapter/types.js +20 -0
- package/dist/claude-code/install.d.ts +36 -0
- package/dist/claude-code/install.js +183 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jobs/runner.d.ts +80 -0
- package/dist/jobs/runner.js +197 -0
- package/dist/roles/index.d.ts +50 -0
- package/dist/roles/index.js +125 -0
- package/dist/roles/steps.d.ts +28 -0
- package/dist/roles/steps.js +75 -0
- package/package.json +50 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { sdodsMcpServerSpec, playwrightMcpSpec, requireCli, runJsonl, runQuiet, } from './cli-common.js';
|
|
2
|
+
import { cliFailure } from './claude-code.js';
|
|
3
|
+
import { AgentsConfigError, DEFAULT_MODELS, } from './types.js';
|
|
4
|
+
const INSTALL_HINT = 'Install Codex (`npm i -g @openai/codex`) and run `codex login`, or use --adapter claude-code, --adapter claude with ANTHROPIC_API_KEY, or --dry-run.';
|
|
5
|
+
/** Codex `exec --json` event stream → SDODS events. */
|
|
6
|
+
export function normalizeCodexEvent(ev) {
|
|
7
|
+
const out = [];
|
|
8
|
+
const item = ev.item;
|
|
9
|
+
switch (ev.type) {
|
|
10
|
+
case 'item.started':
|
|
11
|
+
if (item && isToolItem(item.type))
|
|
12
|
+
out.push({ type: 'tool_call', id: item.id, name: toolName(item), input: toolInput(item) });
|
|
13
|
+
break;
|
|
14
|
+
case 'item.completed':
|
|
15
|
+
if (!item)
|
|
16
|
+
break;
|
|
17
|
+
if (item.type === 'agent_message' && typeof item.text === 'string')
|
|
18
|
+
out.push({ type: 'text', text: item.text });
|
|
19
|
+
else if (item.type === 'error')
|
|
20
|
+
out.push({ type: 'status', message: String(item.message ?? 'error') });
|
|
21
|
+
else if (item.type === 'reasoning') {
|
|
22
|
+
/* keep reasoning out of the transcript */
|
|
23
|
+
}
|
|
24
|
+
else if (isToolItem(item.type)) {
|
|
25
|
+
// Codex does not always emit item.started for tools; emit the call if we have not seen it.
|
|
26
|
+
out.push({
|
|
27
|
+
type: 'tool_result',
|
|
28
|
+
id: item.id,
|
|
29
|
+
output: item.output ?? item.result ?? item.aggregated_output ?? item.changes ?? null,
|
|
30
|
+
isError: item.status === 'failed' || item.status === 'error',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
case 'turn.failed':
|
|
35
|
+
case 'error':
|
|
36
|
+
out.push({
|
|
37
|
+
type: 'status',
|
|
38
|
+
message: String(ev.error?.message ?? ev.message ?? 'turn failed'),
|
|
39
|
+
});
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function isToolItem(type) {
|
|
47
|
+
return (type === 'mcp_tool_call' ||
|
|
48
|
+
type === 'command_execution' ||
|
|
49
|
+
type === 'file_change' ||
|
|
50
|
+
type === 'web_search' ||
|
|
51
|
+
type === 'tool_call');
|
|
52
|
+
}
|
|
53
|
+
function toolName(item) {
|
|
54
|
+
if (item.type === 'mcp_tool_call')
|
|
55
|
+
return `mcp__${item.server ?? 'mcp'}__${item.tool ?? item.name ?? 'tool'}`;
|
|
56
|
+
if (item.type === 'command_execution')
|
|
57
|
+
return 'shell';
|
|
58
|
+
if (item.type === 'file_change')
|
|
59
|
+
return 'file_change';
|
|
60
|
+
if (item.type === 'web_search')
|
|
61
|
+
return 'web_search';
|
|
62
|
+
return String(item.name ?? item.type);
|
|
63
|
+
}
|
|
64
|
+
function toolInput(item) {
|
|
65
|
+
return item.arguments ?? item.input ?? item.command ?? item.query ?? null;
|
|
66
|
+
}
|
|
67
|
+
export function codexUsage(u) {
|
|
68
|
+
if (!u)
|
|
69
|
+
return undefined;
|
|
70
|
+
return {
|
|
71
|
+
inputTokens: typeof u.input_tokens === 'number' ? u.input_tokens : undefined,
|
|
72
|
+
outputTokens: typeof u.output_tokens === 'number' ? u.output_tokens : undefined,
|
|
73
|
+
cacheReadTokens: typeof u.cached_input_tokens === 'number' ? u.cached_input_tokens : undefined,
|
|
74
|
+
cacheWriteTokens: typeof u.cache_write_input_tokens === 'number' ? u.cache_write_input_tokens : undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Runs SDODS agent roles through the user's logged-in OpenAI Codex CLI (`codex exec --json`).
|
|
79
|
+
* No OPENAI_API_KEY is needed when Codex is logged in with a ChatGPT account. The sdods MCP
|
|
80
|
+
* server and the bundled Playwright MCP are injected with `-c mcp_servers.*` overrides; the run
|
|
81
|
+
* is ephemeral, read-only sandboxed, and the system prompt is prepended to the task.
|
|
82
|
+
*/
|
|
83
|
+
export class CodexCliAdapter {
|
|
84
|
+
ctx;
|
|
85
|
+
provider = 'codex';
|
|
86
|
+
defaultModel;
|
|
87
|
+
constructor(ctx = {}) {
|
|
88
|
+
this.ctx = ctx;
|
|
89
|
+
this.defaultModel = ctx.model ?? process.env.SDODS_CODEX_MODEL ?? DEFAULT_MODELS.codex;
|
|
90
|
+
}
|
|
91
|
+
bin() {
|
|
92
|
+
const bin = requireCli(this.ctx.bin ?? 'codex', INSTALL_HINT);
|
|
93
|
+
if (process.env.SDODS_SKIP_CLI_AUTH_CHECK !== '1') {
|
|
94
|
+
const r = runQuiet(bin, ['login', 'status']);
|
|
95
|
+
if (r.ok && /not logged in/i.test(r.stdout)) {
|
|
96
|
+
throw new AgentsConfigError('Codex is installed but not logged in.', 'Run `codex login` (ChatGPT account) or set OPENAI_API_KEY.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return bin;
|
|
100
|
+
}
|
|
101
|
+
static loginStatus(bin = 'codex') {
|
|
102
|
+
const found = (() => {
|
|
103
|
+
try {
|
|
104
|
+
return requireCli(bin, '');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
if (!found)
|
|
111
|
+
return { installed: false, loggedIn: null, detail: 'not installed' };
|
|
112
|
+
const v = runQuiet(found, ['--version']);
|
|
113
|
+
const s = runQuiet(found, ['login', 'status']);
|
|
114
|
+
const loggedIn = s.ok ? !/not logged in/i.test(s.stdout) : null;
|
|
115
|
+
return {
|
|
116
|
+
installed: true,
|
|
117
|
+
loggedIn,
|
|
118
|
+
detail: `${v.stdout.trim().split('\n')[0] || 'installed'}${loggedIn ? ` · ${s.stdout.trim().split('\n')[0]}` : loggedIn === false ? ' · NOT logged in' : ''}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
baseArgs(cwd, model) {
|
|
122
|
+
const m = model ?? this.defaultModel;
|
|
123
|
+
return [
|
|
124
|
+
'exec',
|
|
125
|
+
'--json',
|
|
126
|
+
'--skip-git-repo-check',
|
|
127
|
+
'--ephemeral',
|
|
128
|
+
'-s',
|
|
129
|
+
'read-only',
|
|
130
|
+
...(cwd ? ['-C', cwd] : []),
|
|
131
|
+
...(m && m !== 'default' ? ['-m', m] : []),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
mcpArgs(o) {
|
|
135
|
+
const sdods = sdodsMcpServerSpec({ ...this.ctx, rootDir: this.ctx.rootDir ?? o.cwd });
|
|
136
|
+
const args = [
|
|
137
|
+
'-c',
|
|
138
|
+
`mcp_servers.sdods.command=${tomlString(sdods.command)}`,
|
|
139
|
+
'-c',
|
|
140
|
+
`mcp_servers.sdods.args=${tomlArray(sdods.args)}`,
|
|
141
|
+
];
|
|
142
|
+
if (this.ctx.withPlaywrightMcp !== false) {
|
|
143
|
+
const pw = playwrightMcpSpec();
|
|
144
|
+
args.push('-c', `mcp_servers.playwright.command=${tomlString(pw.command)}`, '-c', `mcp_servers.playwright.args=${tomlArray(pw.args)}`);
|
|
145
|
+
}
|
|
146
|
+
for (const [name, cfg] of Object.entries(o.mcpServers ?? {})) {
|
|
147
|
+
if (cfg.transport === 'http' && cfg.url) {
|
|
148
|
+
args.push('-c', `mcp_servers.${name}.url=${tomlString(cfg.url)}`);
|
|
149
|
+
}
|
|
150
|
+
else if (cfg.command) {
|
|
151
|
+
args.push('-c', `mcp_servers.${name}.command=${tomlString(cfg.command)}`, '-c', `mcp_servers.${name}.args=${tomlArray(cfg.args ?? [])}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return args;
|
|
155
|
+
}
|
|
156
|
+
async complete(req) {
|
|
157
|
+
const bin = this.bin();
|
|
158
|
+
let text = '';
|
|
159
|
+
let usage;
|
|
160
|
+
const prompt = req.system ? `${req.system}\n\n---\n\n${req.prompt}` : req.prompt;
|
|
161
|
+
const r = await runJsonl(bin, [...this.baseArgs(undefined, req.model), prompt], {
|
|
162
|
+
signal: req.signal,
|
|
163
|
+
onLine: (line) => {
|
|
164
|
+
if (typeof line !== 'object' || !line)
|
|
165
|
+
return;
|
|
166
|
+
const ev = line;
|
|
167
|
+
for (const e of normalizeCodexEvent(ev))
|
|
168
|
+
if (e.type === 'text')
|
|
169
|
+
text += e.text;
|
|
170
|
+
if (ev.type === 'turn.completed')
|
|
171
|
+
usage = codexUsage(ev.usage);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
if (r.exitCode !== 0 && !text)
|
|
175
|
+
throw cliFailure('codex', r.exitCode, r.stderr);
|
|
176
|
+
return { text: text.trim(), usage };
|
|
177
|
+
}
|
|
178
|
+
async *stream(req) {
|
|
179
|
+
const r = await this.complete(req);
|
|
180
|
+
yield r.text;
|
|
181
|
+
}
|
|
182
|
+
async runAgent(o) {
|
|
183
|
+
const bin = this.bin();
|
|
184
|
+
const prompt = `${o.system}\n\n---\n\n${o.prompt}`;
|
|
185
|
+
const args = [...this.baseArgs(o.cwd, o.model), ...this.mcpArgs(o), prompt];
|
|
186
|
+
let text = '';
|
|
187
|
+
let turns = 0;
|
|
188
|
+
let toolCalls = 0;
|
|
189
|
+
let usage;
|
|
190
|
+
let failed = false;
|
|
191
|
+
const seenCalls = new Set();
|
|
192
|
+
const r = await runJsonl(bin, args, {
|
|
193
|
+
cwd: o.cwd,
|
|
194
|
+
signal: o.signal,
|
|
195
|
+
onLine: (line) => {
|
|
196
|
+
if (typeof line !== 'object' || !line)
|
|
197
|
+
return;
|
|
198
|
+
const ev = line;
|
|
199
|
+
if (ev.type === 'turn.completed') {
|
|
200
|
+
turns++;
|
|
201
|
+
usage = codexUsage(ev.usage) ?? usage;
|
|
202
|
+
}
|
|
203
|
+
if (ev.type === 'turn.failed')
|
|
204
|
+
failed = true;
|
|
205
|
+
for (const e of normalizeCodexEvent(ev)) {
|
|
206
|
+
if (e.type === 'tool_call') {
|
|
207
|
+
seenCalls.add(e.id);
|
|
208
|
+
toolCalls++;
|
|
209
|
+
}
|
|
210
|
+
if (e.type === 'tool_result' && !seenCalls.has(e.id)) {
|
|
211
|
+
// no item.started was emitted for this tool: count the call now
|
|
212
|
+
seenCalls.add(e.id);
|
|
213
|
+
toolCalls++;
|
|
214
|
+
o.onEvent?.({ type: 'tool_call', id: e.id, name: 'tool', input: null });
|
|
215
|
+
}
|
|
216
|
+
o.onEvent?.(e);
|
|
217
|
+
if (e.type === 'text')
|
|
218
|
+
text += e.text;
|
|
219
|
+
}
|
|
220
|
+
if (o.maxTurns && turns >= o.maxTurns) {
|
|
221
|
+
o.onEvent?.({ type: 'status', message: `max turns (${o.maxTurns}) reached` });
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
const isError = failed || (r.exitCode !== 0 && !text);
|
|
226
|
+
if (isError && !text)
|
|
227
|
+
throw cliFailure('codex', r.exitCode, r.stderr);
|
|
228
|
+
const result = {
|
|
229
|
+
text,
|
|
230
|
+
usage,
|
|
231
|
+
turns: Math.max(turns, 1),
|
|
232
|
+
stopReason: isError ? 'error' : 'end',
|
|
233
|
+
isError,
|
|
234
|
+
toolCalls,
|
|
235
|
+
};
|
|
236
|
+
o.onEvent?.({
|
|
237
|
+
type: 'result',
|
|
238
|
+
usage,
|
|
239
|
+
turns: result.turns,
|
|
240
|
+
stopReason: result.stopReason,
|
|
241
|
+
isError,
|
|
242
|
+
});
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function tomlString(s) {
|
|
247
|
+
return JSON.stringify(s);
|
|
248
|
+
}
|
|
249
|
+
function tomlArray(items) {
|
|
250
|
+
return `[${items.map(tomlString).join(',')}]`;
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=codex.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type FakeAdapterOptions } from './fake.js';
|
|
2
|
+
import { type LlmAdapter, type Provider } from './types.js';
|
|
3
|
+
export interface CreateAdapterOptions {
|
|
4
|
+
provider?: Provider | string;
|
|
5
|
+
model?: string;
|
|
6
|
+
/** Endpoint for the model server (ollama, vLLM, LM Studio, Azure …). */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** `options.num_ctx` for local models, and the ceiling their prompts are measured against. */
|
|
9
|
+
contextTokens?: number;
|
|
10
|
+
temperature?: number;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
fake?: FakeAdapterOptions;
|
|
13
|
+
/** repo root, project and env let the CLI adapters start the sdods MCP server */
|
|
14
|
+
rootDir?: string;
|
|
15
|
+
project?: string;
|
|
16
|
+
env?: string;
|
|
17
|
+
/** called when the provider was auto-detected (not set explicitly) */
|
|
18
|
+
onAutoDetect?: (provider: Provider, reason: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function normalizeProvider(raw: string): Provider;
|
|
21
|
+
/**
|
|
22
|
+
* Detection order when nothing is set explicitly:
|
|
23
|
+
* ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → OPENAI_API_KEY → Ollama → fake.
|
|
24
|
+
*
|
|
25
|
+
* A key or a CLI login is a deliberate statement of intent, so those come first; an installed
|
|
26
|
+
* Ollama still beats `fake`, which produces nothing. Detection is synchronous and must not touch
|
|
27
|
+
* the network, so it looks for `OLLAMA_HOST` or the binary; `detectProviderAsync` additionally
|
|
28
|
+
* probes the default host for callers that can await.
|
|
29
|
+
*/
|
|
30
|
+
export declare function detectProvider(env?: NodeJS.ProcessEnv): {
|
|
31
|
+
provider: Provider;
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
34
|
+
/** Detection plus a short probe of the default Ollama host, for callers that can await. */
|
|
35
|
+
export declare function detectProviderAsync(env?: NodeJS.ProcessEnv): Promise<{
|
|
36
|
+
provider: Provider;
|
|
37
|
+
reason: string;
|
|
38
|
+
}>;
|
|
39
|
+
export declare function resolveProvider(explicit?: string, projectDefault?: string, onAutoDetect?: (provider: Provider, reason: string) => void): Provider;
|
|
40
|
+
export declare function createAdapter(opts?: CreateAdapterOptions, projectDefault?: string): LlmAdapter;
|
|
41
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ClaudeAdapter } from './claude.js';
|
|
2
|
+
import { ClaudeCodeCliAdapter } from './claude-code.js';
|
|
3
|
+
import { findOnPath } from './cli-common.js';
|
|
4
|
+
import { CodexCliAdapter } from './codex.js';
|
|
5
|
+
import { FakeAdapter } from './fake.js';
|
|
6
|
+
import { OllamaAdapter } from './ollama.js';
|
|
7
|
+
import { OpenAiCompatibleAdapter } from './openai-compat.js';
|
|
8
|
+
import { AgentsConfigError } from './types.js';
|
|
9
|
+
export function normalizeProvider(raw) {
|
|
10
|
+
const p = raw.toLowerCase();
|
|
11
|
+
if (p === 'claude' || p === 'anthropic')
|
|
12
|
+
return 'claude';
|
|
13
|
+
if (p === 'claude-code' || p === 'claudecode' || p === 'claude-cli')
|
|
14
|
+
return 'claude-code';
|
|
15
|
+
if (p === 'codex' || p === 'codex-cli' || p === 'openai-codex')
|
|
16
|
+
return 'codex';
|
|
17
|
+
if (p === 'ollama' || p === 'local')
|
|
18
|
+
return 'ollama';
|
|
19
|
+
if (p === 'openai' ||
|
|
20
|
+
p === 'openai-compatible' ||
|
|
21
|
+
p === 'azure' ||
|
|
22
|
+
p === 'vllm' ||
|
|
23
|
+
p === 'lmstudio')
|
|
24
|
+
return 'openai-compatible';
|
|
25
|
+
if (p === 'fake' || p === 'dry-run')
|
|
26
|
+
return 'fake';
|
|
27
|
+
throw new AgentsConfigError(`Unknown LLM provider "${raw}".`, 'Use claude, claude-code, codex, openai-compatible, ollama or fake.');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Detection order when nothing is set explicitly:
|
|
31
|
+
* ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → OPENAI_API_KEY → Ollama → fake.
|
|
32
|
+
*
|
|
33
|
+
* A key or a CLI login is a deliberate statement of intent, so those come first; an installed
|
|
34
|
+
* Ollama still beats `fake`, which produces nothing. Detection is synchronous and must not touch
|
|
35
|
+
* the network, so it looks for `OLLAMA_HOST` or the binary; `detectProviderAsync` additionally
|
|
36
|
+
* probes the default host for callers that can await.
|
|
37
|
+
*/
|
|
38
|
+
export function detectProvider(env = process.env) {
|
|
39
|
+
if (env.ANTHROPIC_API_KEY)
|
|
40
|
+
return { provider: 'claude', reason: 'ANTHROPIC_API_KEY is set' };
|
|
41
|
+
if (findOnPath('claude'))
|
|
42
|
+
return { provider: 'claude-code', reason: 'claude CLI found on PATH' };
|
|
43
|
+
if (findOnPath('codex'))
|
|
44
|
+
return { provider: 'codex', reason: 'codex CLI found on PATH' };
|
|
45
|
+
if (env.OPENAI_API_KEY)
|
|
46
|
+
return { provider: 'openai-compatible', reason: 'OPENAI_API_KEY is set' };
|
|
47
|
+
if (env.OLLAMA_HOST)
|
|
48
|
+
return { provider: 'ollama', reason: 'OLLAMA_HOST is set' };
|
|
49
|
+
if (findOnPath('ollama'))
|
|
50
|
+
return { provider: 'ollama', reason: 'ollama found on PATH' };
|
|
51
|
+
return {
|
|
52
|
+
provider: 'fake',
|
|
53
|
+
reason: 'no API key and no claude/codex CLI found; using the fake adapter',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Detection plus a short probe of the default Ollama host, for callers that can await. */
|
|
57
|
+
export async function detectProviderAsync(env = process.env) {
|
|
58
|
+
const sync = detectProvider(env);
|
|
59
|
+
if (sync.provider !== 'fake')
|
|
60
|
+
return sync;
|
|
61
|
+
if (await OllamaAdapter.reachable())
|
|
62
|
+
return { provider: 'ollama', reason: 'a local Ollama server answered' };
|
|
63
|
+
return sync;
|
|
64
|
+
}
|
|
65
|
+
export function resolveProvider(explicit, projectDefault, onAutoDetect) {
|
|
66
|
+
const chosen = explicit ?? process.env.SDODS_LLM_PROVIDER ?? projectDefault;
|
|
67
|
+
if (chosen)
|
|
68
|
+
return normalizeProvider(chosen);
|
|
69
|
+
const d = detectProvider();
|
|
70
|
+
onAutoDetect?.(d.provider, d.reason);
|
|
71
|
+
return d.provider;
|
|
72
|
+
}
|
|
73
|
+
export function createAdapter(opts = {}, projectDefault) {
|
|
74
|
+
const provider = resolveProvider(opts.provider, projectDefault, opts.onAutoDetect);
|
|
75
|
+
const cliCtx = { rootDir: opts.rootDir, project: opts.project, env: opts.env, model: opts.model };
|
|
76
|
+
switch (provider) {
|
|
77
|
+
case 'claude':
|
|
78
|
+
return new ClaudeAdapter({ model: opts.model });
|
|
79
|
+
case 'claude-code':
|
|
80
|
+
return new ClaudeCodeCliAdapter(cliCtx);
|
|
81
|
+
case 'codex':
|
|
82
|
+
return new CodexCliAdapter(cliCtx);
|
|
83
|
+
case 'openai-compatible':
|
|
84
|
+
return new OpenAiCompatibleAdapter({
|
|
85
|
+
model: opts.model,
|
|
86
|
+
baseUrl: opts.baseUrl,
|
|
87
|
+
temperature: opts.temperature,
|
|
88
|
+
timeoutMs: opts.timeoutMs,
|
|
89
|
+
});
|
|
90
|
+
case 'ollama':
|
|
91
|
+
return new OllamaAdapter({
|
|
92
|
+
model: opts.model,
|
|
93
|
+
baseUrl: opts.baseUrl,
|
|
94
|
+
contextTokens: opts.contextTokens,
|
|
95
|
+
temperature: opts.temperature,
|
|
96
|
+
timeoutMs: opts.timeoutMs,
|
|
97
|
+
});
|
|
98
|
+
case 'fake':
|
|
99
|
+
return new FakeAdapter(opts.fake);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CompleteRequest, type CompleteResult, type LlmAdapter, type RunAgentOptions, type RunAgentResult } from './types.js';
|
|
2
|
+
export type FakeStep = {
|
|
3
|
+
text: string;
|
|
4
|
+
} | {
|
|
5
|
+
toolCall: {
|
|
6
|
+
name: string;
|
|
7
|
+
input: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
} | {
|
|
10
|
+
fn: (ctx: {
|
|
11
|
+
prompt: string;
|
|
12
|
+
system: string;
|
|
13
|
+
lastToolOutput?: unknown;
|
|
14
|
+
}) => FakeStep | FakeStep[] | undefined;
|
|
15
|
+
};
|
|
16
|
+
export interface FakeAdapterOptions {
|
|
17
|
+
/** scripted sequence; tool calls execute the REAL registry handlers */
|
|
18
|
+
script?: FakeStep[];
|
|
19
|
+
completion?: string | ((req: CompleteRequest) => string);
|
|
20
|
+
costPerTurnUsd?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Deterministic adapter for tests and `--dry-run`: no network, real tool handlers. */
|
|
23
|
+
export declare class FakeAdapter implements LlmAdapter {
|
|
24
|
+
private readonly opts;
|
|
25
|
+
readonly provider: "fake";
|
|
26
|
+
readonly defaultModel: string;
|
|
27
|
+
readonly calls: Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
input: unknown;
|
|
30
|
+
output: unknown;
|
|
31
|
+
}>;
|
|
32
|
+
constructor(opts?: FakeAdapterOptions);
|
|
33
|
+
complete(req: CompreteOrCompleteRequest): Promise<CompleteResult>;
|
|
34
|
+
stream(req: CompleteRequest): AsyncIterable<string>;
|
|
35
|
+
runAgent(o: RunAgentOptions): Promise<RunAgentResult>;
|
|
36
|
+
}
|
|
37
|
+
type CompreteOrCompleteRequest = CompleteRequest;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=fake.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { DEFAULT_MODELS, } from './types.js';
|
|
2
|
+
/** Deterministic adapter for tests and `--dry-run`: no network, real tool handlers. */
|
|
3
|
+
export class FakeAdapter {
|
|
4
|
+
opts;
|
|
5
|
+
provider = 'fake';
|
|
6
|
+
defaultModel = DEFAULT_MODELS.fake;
|
|
7
|
+
calls = [];
|
|
8
|
+
constructor(opts = {}) {
|
|
9
|
+
this.opts = opts;
|
|
10
|
+
}
|
|
11
|
+
async complete(req) {
|
|
12
|
+
const c = this.opts.completion;
|
|
13
|
+
const text = typeof c === 'function' ? c(req) : (c ?? `[fake completion for: ${req.prompt.slice(0, 80)}]`);
|
|
14
|
+
return {
|
|
15
|
+
text,
|
|
16
|
+
usage: { inputTokens: req.prompt.length, outputTokens: text.length },
|
|
17
|
+
costUsd: 0,
|
|
18
|
+
model: this.defaultModel,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async *stream(req) {
|
|
22
|
+
yield (await this.complete(req)).text;
|
|
23
|
+
}
|
|
24
|
+
async runAgent(o) {
|
|
25
|
+
const byName = new Map(o.tools.map((t) => [t.name, t]));
|
|
26
|
+
const queue = [
|
|
27
|
+
...(this.opts.script ?? [
|
|
28
|
+
{ text: `[fake agent] tools available: ${o.tools.map((t) => t.name).join(', ')}` },
|
|
29
|
+
]),
|
|
30
|
+
];
|
|
31
|
+
let text = '';
|
|
32
|
+
let turns = 0;
|
|
33
|
+
let toolCalls = 0;
|
|
34
|
+
let lastToolOutput;
|
|
35
|
+
const cost = () => turns * (this.opts.costPerTurnUsd ?? 0);
|
|
36
|
+
while (queue.length) {
|
|
37
|
+
if (o.maxTurns && turns >= o.maxTurns)
|
|
38
|
+
return finish('error_max_turns', true);
|
|
39
|
+
if (o.maxBudgetUsd !== undefined && cost() > o.maxBudgetUsd)
|
|
40
|
+
return finish('error_max_budget_usd', true);
|
|
41
|
+
const step = queue.shift();
|
|
42
|
+
turns++;
|
|
43
|
+
if ('fn' in step) {
|
|
44
|
+
const next = step.fn({ prompt: o.prompt, system: o.system, lastToolOutput });
|
|
45
|
+
if (next)
|
|
46
|
+
queue.unshift(...(Array.isArray(next) ? next : [next]));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if ('text' in step) {
|
|
50
|
+
text += step.text;
|
|
51
|
+
o.onEvent?.({ type: 'text', text: step.text });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const id = `call_${toolCalls + 1}`;
|
|
55
|
+
toolCalls++;
|
|
56
|
+
o.onEvent?.({ type: 'tool_call', id, name: step.toolCall.name, input: step.toolCall.input });
|
|
57
|
+
const tool = byName.get(step.toolCall.name);
|
|
58
|
+
const result = tool
|
|
59
|
+
? await tool.handler(step.toolCall.input)
|
|
60
|
+
: {
|
|
61
|
+
content: [{ type: 'text', text: `Unknown tool ${step.toolCall.name}` }],
|
|
62
|
+
isError: true,
|
|
63
|
+
};
|
|
64
|
+
lastToolOutput = result;
|
|
65
|
+
this.calls.push({ name: step.toolCall.name, input: step.toolCall.input, output: result });
|
|
66
|
+
o.onEvent?.({ type: 'tool_result', id, output: result.content, isError: result.isError });
|
|
67
|
+
}
|
|
68
|
+
return finish('end_turn', false);
|
|
69
|
+
function finish(stopReason, isError) {
|
|
70
|
+
const r = {
|
|
71
|
+
text,
|
|
72
|
+
costUsd: cost(),
|
|
73
|
+
usage: { inputTokens: 0, outputTokens: text.length },
|
|
74
|
+
turns,
|
|
75
|
+
stopReason,
|
|
76
|
+
isError,
|
|
77
|
+
toolCalls,
|
|
78
|
+
};
|
|
79
|
+
o.onEvent?.({
|
|
80
|
+
type: 'result',
|
|
81
|
+
costUsd: r.costUsd,
|
|
82
|
+
usage: r.usage,
|
|
83
|
+
turns,
|
|
84
|
+
stopReason,
|
|
85
|
+
isError,
|
|
86
|
+
});
|
|
87
|
+
return r;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=fake.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './claude.js';
|
|
3
|
+
export * from './claude-code.js';
|
|
4
|
+
export * from './codex.js';
|
|
5
|
+
export * from './cli-common.js';
|
|
6
|
+
export * from './mcp-bridge.js';
|
|
7
|
+
export * from './ollama.js';
|
|
8
|
+
export * from './openai-compat.js';
|
|
9
|
+
export * from './fake.js';
|
|
10
|
+
export * from './factory.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './claude.js';
|
|
3
|
+
export * from './claude-code.js';
|
|
4
|
+
export * from './codex.js';
|
|
5
|
+
export * from './cli-common.js';
|
|
6
|
+
export * from './mcp-bridge.js';
|
|
7
|
+
export * from './ollama.js';
|
|
8
|
+
export * from './openai-compat.js';
|
|
9
|
+
export * from './fake.js';
|
|
10
|
+
export * from './factory.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Tool parameters as a JSON schema the widest range of servers accepts.
|
|
4
|
+
*
|
|
5
|
+
* Two details matter and both bite. Zod's `toJSONSchema` defaults to `io: 'output'`, which marks
|
|
6
|
+
* every field carrying a `.default()` as **required** — telling the model it must invent a value
|
|
7
|
+
* it could have omitted. And the `$schema` key it adds is rejected by some grammar-constrained
|
|
8
|
+
* local servers. Neither belongs in a function definition.
|
|
9
|
+
*/
|
|
10
|
+
export declare function toolParameters(shape: Parameters<typeof z.object>[0], jsonSchema?: Record<string, unknown>): Record<string, unknown>;
|
|
11
|
+
//# sourceMappingURL=json-schema.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Tool parameters as a JSON schema the widest range of servers accepts.
|
|
4
|
+
*
|
|
5
|
+
* Two details matter and both bite. Zod's `toJSONSchema` defaults to `io: 'output'`, which marks
|
|
6
|
+
* every field carrying a `.default()` as **required** — telling the model it must invent a value
|
|
7
|
+
* it could have omitted. And the `$schema` key it adds is rejected by some grammar-constrained
|
|
8
|
+
* local servers. Neither belongs in a function definition.
|
|
9
|
+
*/
|
|
10
|
+
export function toolParameters(shape, jsonSchema) {
|
|
11
|
+
const schema = jsonSchema
|
|
12
|
+
? { ...jsonSchema }
|
|
13
|
+
: z.toJSONSchema(z.object(shape), { io: 'input' });
|
|
14
|
+
delete schema.$schema;
|
|
15
|
+
return schema;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=json-schema.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentSdkToolDef } from '@sdods/mcp';
|
|
2
|
+
import type { AgentEvent, ExternalMcpServerConfig } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Lets an adapter that drives a chat endpoint itself use MCP servers.
|
|
5
|
+
*
|
|
6
|
+
* The Claude and CLI adapters hand `mcpServers` to something that already speaks MCP — the Agent
|
|
7
|
+
* SDK, or the `claude`/`codex` binary. The chat-completions adapters had nowhere to hand them, so
|
|
8
|
+
* they dropped them with a note, which quietly meant the planner, generator and healer ran without
|
|
9
|
+
* a browser: they are the roles whose whole job is to look at the application. That was true of
|
|
10
|
+
* every OpenAI-compatible provider, hosted or local, and it is the reason a local model could not
|
|
11
|
+
* do the interesting half of the work.
|
|
12
|
+
*
|
|
13
|
+
* This connects the servers as a client, lists their tools, and presents them as ordinary function
|
|
14
|
+
* definitions named `mcp__<server>__<tool>` — the same convention Claude Code uses, so prompts and
|
|
15
|
+
* allowlists read the same everywhere.
|
|
16
|
+
*/
|
|
17
|
+
export interface BridgedTools {
|
|
18
|
+
tools: AgentSdkToolDef[];
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface BridgeOptions {
|
|
22
|
+
servers?: Record<string, ExternalMcpServerConfig>;
|
|
23
|
+
/** Adds `npx playwright mcp --headless`, the way the Claude adapter does for browser roles. */
|
|
24
|
+
withPlaywright?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Ceiling on the tools taken from each server. The browser server alone offers 24, which is
|
|
27
|
+
* more than a small model can choose between, so the small profile keeps the ones a test author
|
|
28
|
+
* actually uses and drops the rest.
|
|
29
|
+
*/
|
|
30
|
+
maxToolsPerServer?: number;
|
|
31
|
+
cwd?: string;
|
|
32
|
+
onEvent?: (e: AgentEvent) => void;
|
|
33
|
+
}
|
|
34
|
+
export declare function bridgeMcpServers(opts: BridgeOptions): Promise<BridgedTools>;
|
|
35
|
+
//# sourceMappingURL=mcp-bridge.d.ts.map
|