zyndo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentLoop.d.ts +14 -0
- package/dist/agentLoop.js +76 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +25 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +109 -0
- package/dist/connection.d.ts +58 -0
- package/dist/connection.js +114 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +68 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +178 -0
- package/dist/mcp/mcpCore.d.ts +22 -0
- package/dist/mcp/mcpCore.js +448 -0
- package/dist/mcp/mcpServer.d.ts +1 -0
- package/dist/mcp/mcpServer.js +61 -0
- package/dist/providers/anthropic.d.ts +2 -0
- package/dist/providers/anthropic.js +52 -0
- package/dist/providers/claudeCode.d.ts +5 -0
- package/dist/providers/claudeCode.js +174 -0
- package/dist/providers/ollama.d.ts +2 -0
- package/dist/providers/ollama.js +90 -0
- package/dist/providers/openai.d.ts +2 -0
- package/dist/providers/openai.js +103 -0
- package/dist/providers/types.d.ts +33 -0
- package/dist/providers/types.js +4 -0
- package/dist/sellerDaemon.d.ts +9 -0
- package/dist/sellerDaemon.js +336 -0
- package/dist/state.d.ts +21 -0
- package/dist/state.js +63 -0
- package/dist/tools/askBuyer.d.ts +2 -0
- package/dist/tools/askBuyer.js +19 -0
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.js +35 -0
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +28 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +36 -0
- package/dist/tools/pathSafety.d.ts +1 -0
- package/dist/tools/pathSafety.js +9 -0
- package/dist/tools/readFile.d.ts +2 -0
- package/dist/tools/readFile.js +35 -0
- package/dist/tools/types.d.ts +9 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/writeFile.d.ts +2 -0
- package/dist/tools/writeFile.js +28 -0
- package/package.json +36 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// MCP Server — stdio transport adapter
|
|
3
|
+
//
|
|
4
|
+
// Thin wrapper over mcpCore.ts. Reads JSON-RPC from stdin, writes to stdout.
|
|
5
|
+
// Manages a single McpSessionState and heartbeat timer for the stdio session.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
import { heartbeat } from '../connection.js';
|
|
9
|
+
import { handleMcpMethod, mcpError } from './mcpCore.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Heartbeat with auto-reconnect
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function startHeartbeat(state) {
|
|
14
|
+
return setInterval(async () => {
|
|
15
|
+
if (state.agentSession === undefined)
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
await heartbeat(state.agentSession);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Session expired — the next tool call will trigger auto-reconnect via mcpCore
|
|
22
|
+
}
|
|
23
|
+
}, 30_000);
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Start MCP server (stdio transport)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
export async function startMcpServer() {
|
|
29
|
+
const state = {
|
|
30
|
+
agentSession: undefined,
|
|
31
|
+
lastConnectName: 'Claude Code Buyer',
|
|
32
|
+
lastEventId: 0,
|
|
33
|
+
bridgeUrl: process.env.ZYNDO_BRIDGE_URL ?? 'https://bridge.zyndo.ai',
|
|
34
|
+
apiKey: process.env.ZYNDO_API_KEY ?? ''
|
|
35
|
+
};
|
|
36
|
+
let heartbeatTimer;
|
|
37
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
38
|
+
rl.on('line', async (line) => {
|
|
39
|
+
if (line.trim().length === 0)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
const request = JSON.parse(line);
|
|
43
|
+
const response = await handleMcpMethod(state, request.method, request.id, request.params);
|
|
44
|
+
// Start heartbeat after a successful connect
|
|
45
|
+
if (request.method === 'tools/call' && state.agentSession !== undefined && heartbeatTimer === undefined) {
|
|
46
|
+
heartbeatTimer = startHeartbeat(state);
|
|
47
|
+
}
|
|
48
|
+
if (response.length > 0) {
|
|
49
|
+
process.stdout.write(response + '\n');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
process.stdout.write(mcpError(null, -32700, 'Parse error') + '\n');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
rl.on('close', () => {
|
|
57
|
+
if (heartbeatTimer !== undefined)
|
|
58
|
+
clearInterval(heartbeatTimer);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Anthropic Messages API adapter
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export function createAnthropicProvider(apiKey, model) {
|
|
5
|
+
return {
|
|
6
|
+
async chat(messages, tools, systemPrompt) {
|
|
7
|
+
const anthropicMessages = messages.map((m) => ({
|
|
8
|
+
role: m.role,
|
|
9
|
+
content: m.content
|
|
10
|
+
}));
|
|
11
|
+
const anthropicTools = tools.map((t) => ({
|
|
12
|
+
name: t.name,
|
|
13
|
+
description: t.description,
|
|
14
|
+
input_schema: t.inputSchema
|
|
15
|
+
}));
|
|
16
|
+
const body = {
|
|
17
|
+
model,
|
|
18
|
+
max_tokens: 8192,
|
|
19
|
+
system: systemPrompt,
|
|
20
|
+
messages: anthropicMessages
|
|
21
|
+
};
|
|
22
|
+
if (anthropicTools.length > 0) {
|
|
23
|
+
body.tools = anthropicTools;
|
|
24
|
+
}
|
|
25
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'content-type': 'application/json',
|
|
29
|
+
'x-api-key': apiKey,
|
|
30
|
+
'anthropic-version': '2023-06-01'
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body)
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
throw new Error(`Anthropic API error (${res.status}): ${text}`);
|
|
37
|
+
}
|
|
38
|
+
const data = (await res.json());
|
|
39
|
+
return {
|
|
40
|
+
content: data.content,
|
|
41
|
+
stopReason: mapStopReason(data.stop_reason)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function mapStopReason(reason) {
|
|
47
|
+
if (reason === 'tool_use')
|
|
48
|
+
return 'tool_use';
|
|
49
|
+
if (reason === 'max_tokens')
|
|
50
|
+
return 'max_tokens';
|
|
51
|
+
return 'end_turn';
|
|
52
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SellerConfig, HarnessName } from '../config.js';
|
|
2
|
+
import type { AgentLoopResult } from '../agentLoop.js';
|
|
3
|
+
import type { DaemonLogger } from '../sellerDaemon.js';
|
|
4
|
+
export declare function detectHarness(binary: string): HarnessName;
|
|
5
|
+
export declare function runClaudeCodeTask(taskContext: string, config: SellerConfig, logger: DaemonLogger): Promise<AgentLoopResult>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Claude Code provider — spawns a CLI harness (claude, codex, or generic)
|
|
3
|
+
// instead of making raw LLM API calls.
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { basename } from 'node:path';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Harness detection and args
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export function detectHarness(binary) {
|
|
11
|
+
const name = basename(binary).toLowerCase();
|
|
12
|
+
if (name === 'codex' || name.startsWith('codex-'))
|
|
13
|
+
return 'codex';
|
|
14
|
+
if (name === 'claude' || name.startsWith('claude-'))
|
|
15
|
+
return 'claude';
|
|
16
|
+
return 'generic';
|
|
17
|
+
}
|
|
18
|
+
function buildHarnessArgs(harness, config, systemPrompt) {
|
|
19
|
+
switch (harness) {
|
|
20
|
+
case 'claude': {
|
|
21
|
+
const args = [
|
|
22
|
+
'--print',
|
|
23
|
+
'--output-format', 'json',
|
|
24
|
+
'--system-prompt', systemPrompt,
|
|
25
|
+
'--model', config.model,
|
|
26
|
+
'--add-dir', config.workingDirectory,
|
|
27
|
+
'--permission-mode', 'bypassPermissions',
|
|
28
|
+
'--no-session-persistence'
|
|
29
|
+
];
|
|
30
|
+
if (config.claudeCodeMaxBudgetUsd !== undefined) {
|
|
31
|
+
args.push('--max-budget-usd', String(config.claudeCodeMaxBudgetUsd));
|
|
32
|
+
}
|
|
33
|
+
return args;
|
|
34
|
+
}
|
|
35
|
+
case 'codex':
|
|
36
|
+
return [
|
|
37
|
+
'--quiet',
|
|
38
|
+
'--model', config.model,
|
|
39
|
+
'--approval-mode', 'full-auto'
|
|
40
|
+
];
|
|
41
|
+
case 'generic':
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Prompt construction
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
function buildPrompt(taskContext, config) {
|
|
49
|
+
const skillLines = config.skills
|
|
50
|
+
.map((s) => `- ${s.id}: ${s.name} — ${s.description}`)
|
|
51
|
+
.join('\n');
|
|
52
|
+
return [
|
|
53
|
+
'You are a seller agent on the Zyndo marketplace.',
|
|
54
|
+
'',
|
|
55
|
+
'**Your identity:**',
|
|
56
|
+
`- Name: ${config.name}`,
|
|
57
|
+
`- Description: ${config.description}`,
|
|
58
|
+
'',
|
|
59
|
+
'**Your skills:**',
|
|
60
|
+
skillLines,
|
|
61
|
+
'',
|
|
62
|
+
`**Working directory:** ${config.workingDirectory}`,
|
|
63
|
+
'',
|
|
64
|
+
'**Task from buyer:**',
|
|
65
|
+
taskContext,
|
|
66
|
+
'',
|
|
67
|
+
'**Instructions:**',
|
|
68
|
+
'Do the work described above. Use the tools available to you (read files, write files, run commands, search code).',
|
|
69
|
+
'',
|
|
70
|
+
'**Delivery format — CRITICAL:**',
|
|
71
|
+
'When finished, output the COMPLETE content of every file you created or modified.',
|
|
72
|
+
'Each file MUST appear in a fenced code block with the filename as the info string:',
|
|
73
|
+
'',
|
|
74
|
+
'```filename.html',
|
|
75
|
+
'<full file content here>',
|
|
76
|
+
'```',
|
|
77
|
+
'',
|
|
78
|
+
'Include ALL files. Do not summarize, abbreviate, or describe what you built.',
|
|
79
|
+
'The text you output IS the deliverable — the buyer CANNOT access your filesystem.',
|
|
80
|
+
'If multiple files, output each in its own code block.',
|
|
81
|
+
'After all file blocks, add a 2-3 line summary of what was built.',
|
|
82
|
+
'',
|
|
83
|
+
'If you need clarification from the buyer before you can proceed, output ONLY this JSON on a line by itself and stop:',
|
|
84
|
+
'{"__zyndo_ask_buyer__": "your question here"}',
|
|
85
|
+
'Do not output anything else after this sentinel. Do not attempt to answer the question yourself.'
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Output parsing
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
const ASK_BUYER_RE = /\{"__zyndo_ask_buyer__":\s*"[^"]*"\}\s*$/;
|
|
92
|
+
function parseClaudeJsonOutput(raw) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
return parsed.result ?? raw;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return raw;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function extractAskBuyerQuestion(text) {
|
|
102
|
+
const match = text.match(ASK_BUYER_RE);
|
|
103
|
+
if (match === null)
|
|
104
|
+
return undefined;
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(match[0]);
|
|
107
|
+
return parsed.__zyndo_ask_buyer__;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function parseOutput(raw, harness) {
|
|
114
|
+
const text = harness === 'claude' ? parseClaudeJsonOutput(raw) : raw;
|
|
115
|
+
const trimmed = text.trim();
|
|
116
|
+
const question = extractAskBuyerQuestion(trimmed);
|
|
117
|
+
if (question !== undefined) {
|
|
118
|
+
return { output: '', paused: true, pendingQuestion: question };
|
|
119
|
+
}
|
|
120
|
+
return { output: trimmed, paused: false };
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Main entry point
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
|
|
126
|
+
export async function runClaudeCodeTask(taskContext, config, logger) {
|
|
127
|
+
const binary = config.claudeCodeBinary ?? 'claude';
|
|
128
|
+
const harness = config.harness ?? detectHarness(binary);
|
|
129
|
+
const timeoutMs = config.claudeCodeTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
130
|
+
const prompt = buildPrompt(taskContext, config);
|
|
131
|
+
const systemPrompt = config.systemPrompt;
|
|
132
|
+
const args = buildHarnessArgs(harness, config, systemPrompt);
|
|
133
|
+
logger.info(`Spawning ${harness} harness: ${binary} ${args.join(' ')}`);
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
137
|
+
const proc = spawn(binary, [...args], {
|
|
138
|
+
cwd: config.workingDirectory,
|
|
139
|
+
signal: controller.signal,
|
|
140
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
141
|
+
env: { ...process.env }
|
|
142
|
+
});
|
|
143
|
+
const stdoutChunks = [];
|
|
144
|
+
const stderrChunks = [];
|
|
145
|
+
proc.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
146
|
+
proc.stderr.on('data', (chunk) => stderrChunks.push(chunk));
|
|
147
|
+
proc.on('error', (err) => {
|
|
148
|
+
clearTimeout(timeoutHandle);
|
|
149
|
+
if (err.name === 'AbortError') {
|
|
150
|
+
logger.error(`Task timed out after ${timeoutMs / 1000}s`);
|
|
151
|
+
resolve({ output: `Task timed out after ${timeoutMs / 1000} seconds.`, paused: false, timedOut: true });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
logger.error(`Spawn error: ${err.message}`);
|
|
155
|
+
resolve({ output: `Harness error: ${err.message}`, paused: false, timedOut: true });
|
|
156
|
+
});
|
|
157
|
+
proc.on('close', (code) => {
|
|
158
|
+
clearTimeout(timeoutHandle);
|
|
159
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
|
|
160
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf-8');
|
|
161
|
+
if (stderr.length > 0) {
|
|
162
|
+
logger.info(`Harness stderr: ${stderr.slice(0, 500)}`);
|
|
163
|
+
}
|
|
164
|
+
if (code !== 0 && stdout.length === 0) {
|
|
165
|
+
logger.error(`Harness exited with code ${code}`);
|
|
166
|
+
resolve({ output: `Harness failed (exit ${code}): ${stderr.slice(0, 1000)}`, paused: false });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
resolve(parseOutput(stdout, harness));
|
|
170
|
+
});
|
|
171
|
+
proc.stdin.write(prompt);
|
|
172
|
+
proc.stdin.end();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Ollama adapter (local models)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
export function createOllamaProvider(model, baseUrl = 'http://localhost:11434') {
|
|
6
|
+
return {
|
|
7
|
+
async chat(messages, tools, systemPrompt) {
|
|
8
|
+
const ollamaMessages = [
|
|
9
|
+
{ role: 'system', content: systemPrompt }
|
|
10
|
+
];
|
|
11
|
+
for (const msg of messages) {
|
|
12
|
+
if (typeof msg.content === 'string') {
|
|
13
|
+
ollamaMessages.push({ role: msg.role, content: msg.content });
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
const textParts = msg.content.filter((b) => b.type === 'text');
|
|
17
|
+
const toolUseParts = msg.content.filter((b) => b.type === 'tool_use');
|
|
18
|
+
if (msg.role === 'assistant') {
|
|
19
|
+
const text = textParts.map((t) => t.text).join('\n');
|
|
20
|
+
const toolCalls = toolUseParts.map((t) => ({
|
|
21
|
+
function: { name: t.name, arguments: t.input }
|
|
22
|
+
}));
|
|
23
|
+
ollamaMessages.push({
|
|
24
|
+
role: 'assistant',
|
|
25
|
+
content: text,
|
|
26
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const toolResults = msg.content.filter((b) => b.type === 'tool_result');
|
|
31
|
+
for (const tr of toolResults) {
|
|
32
|
+
ollamaMessages.push({
|
|
33
|
+
role: 'tool',
|
|
34
|
+
content: 'content' in tr ? tr.content : ''
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (textParts.length > 0) {
|
|
38
|
+
ollamaMessages.push({ role: 'user', content: textParts.map((t) => t.text).join('\n') });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const ollamaTools = tools.map((t) => ({
|
|
44
|
+
type: 'function',
|
|
45
|
+
function: {
|
|
46
|
+
name: t.name,
|
|
47
|
+
description: t.description,
|
|
48
|
+
parameters: t.inputSchema
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
const body = {
|
|
52
|
+
model,
|
|
53
|
+
messages: ollamaMessages,
|
|
54
|
+
stream: false
|
|
55
|
+
};
|
|
56
|
+
if (ollamaTools.length > 0) {
|
|
57
|
+
body.tools = ollamaTools;
|
|
58
|
+
}
|
|
59
|
+
const res = await fetch(`${baseUrl}/api/chat`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify(body)
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
throw new Error(`Ollama API error (${res.status}): ${text}`);
|
|
67
|
+
}
|
|
68
|
+
const data = (await res.json());
|
|
69
|
+
const content = [];
|
|
70
|
+
if (data.message.content.length > 0) {
|
|
71
|
+
content.push({ type: 'text', text: data.message.content });
|
|
72
|
+
}
|
|
73
|
+
if (data.message.tool_calls !== undefined) {
|
|
74
|
+
for (const tc of data.message.tool_calls) {
|
|
75
|
+
content.push({
|
|
76
|
+
type: 'tool_use',
|
|
77
|
+
id: randomUUID(),
|
|
78
|
+
name: tc.function.name,
|
|
79
|
+
input: tc.function.arguments
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const hasToolUse = content.some((b) => b.type === 'tool_use');
|
|
84
|
+
return {
|
|
85
|
+
content,
|
|
86
|
+
stopReason: hasToolUse ? 'tool_use' : 'end_turn'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// OpenAI Chat Completions adapter
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export function createOpenAIProvider(apiKey, model) {
|
|
5
|
+
return {
|
|
6
|
+
async chat(messages, tools, systemPrompt) {
|
|
7
|
+
const openaiMessages = [
|
|
8
|
+
{ role: 'system', content: systemPrompt }
|
|
9
|
+
];
|
|
10
|
+
for (const msg of messages) {
|
|
11
|
+
if (typeof msg.content === 'string') {
|
|
12
|
+
openaiMessages.push({ role: msg.role, content: msg.content });
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// Map content blocks
|
|
16
|
+
const textParts = msg.content.filter((b) => b.type === 'text');
|
|
17
|
+
const toolUseParts = msg.content.filter((b) => b.type === 'tool_use');
|
|
18
|
+
const toolResultParts = msg.content.filter((b) => b.type === 'tool_result');
|
|
19
|
+
if (msg.role === 'assistant') {
|
|
20
|
+
const text = textParts.map((t) => t.text).join('\n') || null;
|
|
21
|
+
const toolCalls = toolUseParts.map((t) => ({
|
|
22
|
+
id: t.id,
|
|
23
|
+
type: 'function',
|
|
24
|
+
function: { name: t.name, arguments: JSON.stringify(t.input) }
|
|
25
|
+
}));
|
|
26
|
+
openaiMessages.push({
|
|
27
|
+
role: 'assistant',
|
|
28
|
+
content: text,
|
|
29
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// User message with tool results
|
|
34
|
+
for (const tr of toolResultParts) {
|
|
35
|
+
if (tr.type === 'tool_result') {
|
|
36
|
+
openaiMessages.push({
|
|
37
|
+
role: 'tool',
|
|
38
|
+
content: 'content' in tr ? tr.content : '',
|
|
39
|
+
tool_call_id: 'tool_use_id' in tr ? tr.tool_use_id : ''
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (textParts.length > 0) {
|
|
44
|
+
openaiMessages.push({ role: 'user', content: textParts.map((t) => t.text).join('\n') });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const openaiTools = tools.map((t) => ({
|
|
50
|
+
type: 'function',
|
|
51
|
+
function: {
|
|
52
|
+
name: t.name,
|
|
53
|
+
description: t.description,
|
|
54
|
+
parameters: t.inputSchema
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
const body = {
|
|
58
|
+
model,
|
|
59
|
+
messages: openaiMessages,
|
|
60
|
+
max_tokens: 8192
|
|
61
|
+
};
|
|
62
|
+
if (openaiTools.length > 0) {
|
|
63
|
+
body.tools = openaiTools;
|
|
64
|
+
}
|
|
65
|
+
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'content-type': 'application/json',
|
|
69
|
+
'authorization': `Bearer ${apiKey}`
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify(body)
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const text = await res.text();
|
|
75
|
+
throw new Error(`OpenAI API error (${res.status}): ${text}`);
|
|
76
|
+
}
|
|
77
|
+
const data = (await res.json());
|
|
78
|
+
const choice = data.choices[0];
|
|
79
|
+
const content = [];
|
|
80
|
+
if (choice.message.content !== null) {
|
|
81
|
+
content.push({ type: 'text', text: choice.message.content });
|
|
82
|
+
}
|
|
83
|
+
if (choice.message.tool_calls !== undefined) {
|
|
84
|
+
for (const tc of choice.message.tool_calls) {
|
|
85
|
+
content.push({
|
|
86
|
+
type: 'tool_use',
|
|
87
|
+
id: tc.id,
|
|
88
|
+
name: tc.function.name,
|
|
89
|
+
input: JSON.parse(tc.function.arguments)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
content,
|
|
95
|
+
stopReason: choice.finish_reason === 'tool_calls'
|
|
96
|
+
? 'tool_use'
|
|
97
|
+
: choice.finish_reason === 'length'
|
|
98
|
+
? 'max_tokens'
|
|
99
|
+
: 'end_turn'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ToolDefinition = Readonly<{
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: Record<string, unknown>;
|
|
5
|
+
}>;
|
|
6
|
+
export type TextBlock = Readonly<{
|
|
7
|
+
type: 'text';
|
|
8
|
+
text: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type ToolUseBlock = Readonly<{
|
|
11
|
+
type: 'tool_use';
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
input: Record<string, unknown>;
|
|
15
|
+
}>;
|
|
16
|
+
export type ToolResultBlock = Readonly<{
|
|
17
|
+
type: 'tool_result';
|
|
18
|
+
tool_use_id: string;
|
|
19
|
+
content: string;
|
|
20
|
+
is_error?: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
export type ContentBlock = TextBlock | ToolUseBlock;
|
|
23
|
+
export type Message = Readonly<{
|
|
24
|
+
role: 'user' | 'assistant';
|
|
25
|
+
content: string | ReadonlyArray<ContentBlock | ToolResultBlock>;
|
|
26
|
+
}>;
|
|
27
|
+
export type ChatResponse = Readonly<{
|
|
28
|
+
content: ReadonlyArray<ContentBlock>;
|
|
29
|
+
stopReason: 'end_turn' | 'tool_use' | 'max_tokens';
|
|
30
|
+
}>;
|
|
31
|
+
export type LLMProvider = Readonly<{
|
|
32
|
+
chat: (messages: ReadonlyArray<Message>, tools: ReadonlyArray<ToolDefinition>, systemPrompt: string) => Promise<ChatResponse>;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SellerConfig } from './config.js';
|
|
2
|
+
export type DaemonLogger = Readonly<{
|
|
3
|
+
info: (msg: string) => void;
|
|
4
|
+
error: (msg: string) => void;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function startSellerDaemon(config: SellerConfig, opts?: {
|
|
7
|
+
logger?: DaemonLogger;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}): Promise<void>;
|