@stackbilt/aegis-core 0.6.5 → 0.8.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/cli/aegis.mjs +356 -0
- package/package.json +21 -3
- package/public/assets/index-CQHn03rW.css +1 -0
- package/public/assets/index-CTKpNJEr.js +74 -0
- package/public/index.html +14 -0
- package/schema.sql +4 -0
- package/src/adapters/voice/cloudflare-agent.ts +0 -0
- package/src/agent-routing.ts +38 -0
- package/src/assets.ts +6 -0
- package/src/auth.ts +14 -5
- package/src/bluesky.ts +0 -0
- package/src/claude-tools/content.ts +0 -0
- package/src/claude-tools/email.ts +0 -0
- package/src/claude.ts +133 -268
- package/src/codebeast.ts +0 -0
- package/src/composite.ts +49 -79
- package/src/content/column.ts +0 -0
- package/src/content/hero-image.ts +0 -0
- package/src/content/index.ts +0 -0
- package/src/content/journal.ts +0 -0
- package/src/content/roundtable.ts +0 -0
- package/src/contracts/agenda-item.contract.ts +0 -0
- package/src/contracts/cc-task.contract.ts +0 -0
- package/src/contracts/goal.contract.ts +0 -0
- package/src/contracts/memory-entry.contract.ts +0 -0
- package/src/core.ts +5 -0
- package/src/dashboard.ts +0 -0
- package/src/decision-docs.ts +0 -0
- package/src/dispatch.ts +0 -0
- package/src/durable-objects/chat-session-auth.ts +20 -0
- package/src/durable-objects/chat-session.ts +251 -0
- package/src/edge-env.ts +0 -0
- package/src/exports.ts +0 -0
- package/src/github-projects.ts +0 -0
- package/src/groq.ts +61 -113
- package/src/index.ts +11 -1
- package/src/kernel/argus-actions.ts +0 -0
- package/src/kernel/argus-correlation.ts +0 -0
- package/src/kernel/board.ts +0 -0
- package/src/kernel/classify-memory-topic.ts +0 -0
- package/src/kernel/disambiguation.ts +55 -0
- package/src/kernel/dispatch.ts +59 -44
- package/src/kernel/dynamic-tools.ts +30 -52
- package/src/kernel/executor-port.ts +0 -0
- package/src/kernel/executor-router.ts +0 -0
- package/src/kernel/executors/claude.ts +1 -0
- package/src/kernel/executors/direct.ts +14 -0
- package/src/kernel/executors/workers-ai.ts +5 -0
- package/src/kernel/grounding/fabrication-detector.ts +0 -0
- package/src/kernel/grounding/fanout.ts +0 -0
- package/src/kernel/grounding/semantic-sanhedrin.ts +0 -0
- package/src/kernel/grounding/verify.ts +0 -0
- package/src/kernel/grounding-layer.ts +0 -0
- package/src/kernel/insight-cache.ts +0 -0
- package/src/kernel/memory/episodic.ts +3 -1
- package/src/kernel/memory/insights.ts +0 -0
- package/src/kernel/memory-guardrails.ts +0 -0
- package/src/kernel/memory-service.ts +0 -0
- package/src/kernel/patterns.ts +0 -0
- package/src/kernel/port.ts +0 -0
- package/src/kernel/provider-factory.ts +0 -0
- package/src/kernel/resilience.ts +0 -0
- package/src/kernel/router.ts +33 -11
- package/src/kernel/scheduled/agent-dispatch.ts +0 -0
- package/src/kernel/scheduled/argus-analytics.ts +0 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +0 -0
- package/src/kernel/scheduled/argus-notify.ts +0 -0
- package/src/kernel/scheduled/board-sync.ts +0 -0
- package/src/kernel/scheduled/ci-watcher.ts +0 -0
- package/src/kernel/scheduled/content-drip.ts +0 -0
- package/src/kernel/scheduled/content.ts +0 -0
- package/src/kernel/scheduled/conversation-facts.ts +9 -7
- package/src/kernel/scheduled/cost-report.ts +0 -0
- package/src/kernel/scheduled/dev-activity.ts +0 -0
- package/src/kernel/scheduled/digest.ts +30 -3
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +0 -0
- package/src/kernel/scheduled/dreaming/facts.ts +0 -0
- package/src/kernel/scheduled/dreaming/index.ts +0 -0
- package/src/kernel/scheduled/dreaming/llm.ts +9 -5
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +0 -0
- package/src/kernel/scheduled/dreaming/persona.ts +0 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +0 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +0 -0
- package/src/kernel/scheduled/entropy.ts +0 -0
- package/src/kernel/scheduled/feed-watcher.ts +0 -0
- package/src/kernel/scheduled/inbox-processor.ts +0 -0
- package/src/kernel/scheduled/issue-proposer.ts +0 -0
- package/src/kernel/scheduled/issue-watcher.ts +0 -0
- package/src/kernel/scheduled/pr-automerge.ts +0 -0
- package/src/kernel/scheduled/product-health.ts +0 -0
- package/src/kernel/scheduled/self-improvement.ts +0 -0
- package/src/kernel/scheduled/social-engage.ts +12 -8
- package/src/kernel/scheduled/task-audit.ts +0 -0
- package/src/kernel/types.ts +6 -0
- package/src/landing.ts +0 -0
- package/src/lib/audit-chain/chain.ts +0 -0
- package/src/lib/audit-chain/types.ts +0 -0
- package/src/lib/observability/errors.ts +0 -0
- package/src/operator/config.ts +0 -0
- package/src/operator/persona.ts +0 -0
- package/src/operator/prompt-builder.ts +3 -0
- package/src/pulse.ts +0 -0
- package/src/routes/bluesky.ts +0 -0
- package/src/routes/chat-ws.ts +17 -0
- package/src/routes/codebeast.ts +0 -0
- package/src/routes/content.ts +0 -0
- package/src/routes/dynamic-tools.ts +0 -0
- package/src/routes/messages.ts +11 -6
- package/src/routes/observability.ts +0 -0
- package/src/routes/operator-logs.ts +0 -0
- package/src/routes/pages.ts +12 -1
- package/src/schema-enums.ts +0 -0
- package/src/task-intelligence.ts +0 -0
- package/src/types.ts +8 -0
- package/src/ui/index.html +13 -0
- package/src/ui/main.tsx +356 -0
- package/src/ui/styles.css +391 -0
- package/src/ui.ts +594 -2
- package/src/version.ts +3 -3
- package/src/wiki/client.ts +0 -0
- package/src/wiki/types.ts +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="theme-color" content="#101317" />
|
|
7
|
+
<title>AEGIS</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CTKpNJEr.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CQHn03rW.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/schema.sql
CHANGED
|
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS conversations (
|
|
|
7
7
|
id TEXT PRIMARY KEY,
|
|
8
8
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
9
9
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
|
+
user_id TEXT NOT NULL DEFAULT 'operator',
|
|
10
11
|
title TEXT
|
|
11
12
|
);
|
|
12
13
|
|
|
@@ -67,6 +68,7 @@ CREATE TABLE IF NOT EXISTS episodic_memory (
|
|
|
67
68
|
executor TEXT, -- which executor handled this
|
|
68
69
|
complexity_tier TEXT, -- aegis#563: procedureKey complement (low|mid|high); NULL for non-dispatcher producers
|
|
69
70
|
executor_config TEXT, -- aegis#563: config snapshot at emit time (evaluator-replay fidelity)
|
|
71
|
+
grounding_gap INTEGER NOT NULL DEFAULT 0, -- aegis#497/#34: unresolved grounding gap observed on this episode
|
|
70
72
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
71
73
|
);
|
|
72
74
|
|
|
@@ -199,6 +201,7 @@ CREATE TABLE IF NOT EXISTS operator_log (
|
|
|
199
201
|
|
|
200
202
|
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
|
|
201
203
|
CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_user_updated ON conversations(user_id, updated_at);
|
|
202
205
|
CREATE INDEX IF NOT EXISTS idx_memory_topic ON memory_entries(topic);
|
|
203
206
|
CREATE INDEX IF NOT EXISTS idx_memory_dedup ON memory_entries(topic, fact_hash);
|
|
204
207
|
CREATE INDEX IF NOT EXISTS idx_memory_expires ON memory_entries(expires_at);
|
|
@@ -208,6 +211,7 @@ CREATE INDEX IF NOT EXISTS idx_episodic_class ON episodic_memory(intent_class);
|
|
|
208
211
|
CREATE INDEX IF NOT EXISTS idx_episodic_created ON episodic_memory(created_at);
|
|
209
212
|
CREATE INDEX IF NOT EXISTS idx_episodic_thread ON episodic_memory(thread_id);
|
|
210
213
|
CREATE INDEX IF NOT EXISTS idx_episodic_class_complexity ON episodic_memory(intent_class, complexity_tier);
|
|
214
|
+
CREATE INDEX IF NOT EXISTS idx_episodic_grounding_gap ON episodic_memory(grounding_gap, created_at);
|
|
211
215
|
CREATE INDEX IF NOT EXISTS idx_procedural_pattern ON procedural_memory(task_pattern);
|
|
212
216
|
CREATE INDEX IF NOT EXISTS idx_procedural_status ON procedural_memory(status);
|
|
213
217
|
CREATE INDEX IF NOT EXISTS idx_heartbeat_created ON heartbeat_results(created_at);
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { routeAgentRequest } from 'agents';
|
|
2
|
+
import type { Env } from './types.js';
|
|
3
|
+
|
|
4
|
+
export async function routeAegisAgentRequest(
|
|
5
|
+
request: Request,
|
|
6
|
+
env: Env,
|
|
7
|
+
): Promise<Response | undefined> {
|
|
8
|
+
const { pathname } = new URL(request.url);
|
|
9
|
+
if (!pathname.startsWith('/agents/')) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const token = extractBearer(request.headers.get('Authorization'))
|
|
14
|
+
?? getCookie(request.headers.get('Cookie') ?? '', 'aegis_token')
|
|
15
|
+
?? new URL(request.url).searchParams.get('token');
|
|
16
|
+
|
|
17
|
+
if (!token || token !== env.AEGIS_TOKEN) {
|
|
18
|
+
return new Response('Unauthorized', { status: 401 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const response = await routeAgentRequest(request, env);
|
|
23
|
+
return response ?? new Response('Agent route not found', { status: 404 });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26
|
+
return new Response(`Agent route failed: ${message}`, { status: 503 });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractBearer(header: string | null): string | null {
|
|
31
|
+
if (!header?.startsWith('Bearer ')) return null;
|
|
32
|
+
return header.slice(7);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getCookie(cookieHeader: string, name: string): string | null {
|
|
36
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
37
|
+
return match?.[1] ?? null;
|
|
38
|
+
}
|
package/src/assets.ts
ADDED
package/src/auth.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { Context, Next } from 'hono';
|
|
2
2
|
import type { Env } from './types.js';
|
|
3
3
|
|
|
4
|
-
export async function bearerAuth(c: Context<{ Bindings: Env }>, next: Next): Promise<Response | void> {
|
|
5
|
-
// Public routes — no auth required
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
export async function bearerAuth(c: Context<{ Bindings: Env }>, next: Next): Promise<Response | void> {
|
|
5
|
+
// Public routes — no auth required
|
|
6
|
+
if (
|
|
7
|
+
c.req.path === '/health' ||
|
|
8
|
+
c.req.path === '/pulse' ||
|
|
9
|
+
((c.req.path === '/' || c.req.path === '/chat' || c.req.path === '/manifest.json' || c.req.path === '/sw.js') && c.req.method === 'GET') ||
|
|
10
|
+
(c.req.method === 'GET' && (c.req.path.startsWith('/assets/') || c.req.path === '/favicon.svg')) ||
|
|
11
|
+
c.req.path.startsWith('/tech') ||
|
|
12
|
+
c.req.path === '/api/feedback' ||
|
|
13
|
+
c.req.path === '/observe' ||
|
|
14
|
+
c.req.path.startsWith('/api/overworld/public')
|
|
15
|
+
) {
|
|
16
|
+
return next();
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
// Webhook routes — auth handled by per-route HMAC verification
|
|
11
20
|
if (c.req.path.startsWith('/webhooks/') || c.req.path === '/api/webhook') {
|
package/src/bluesky.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/claude.ts
CHANGED
|
@@ -1,160 +1,20 @@
|
|
|
1
|
-
// Edge-native Claude executor —
|
|
2
|
-
// Replaces
|
|
1
|
+
// Edge-native Claude executor — provider-backed tool loop
|
|
2
|
+
// Replaces direct Anthropic transport with @stackbilt/llm-providers.
|
|
3
3
|
|
|
4
|
+
import { createLLMProviderFactory, type LLMMessage, type Tool, type ToolCall, type ToolResult as LLMToolResult } from '@stackbilt/llm-providers';
|
|
4
5
|
import { McpClient } from './mcp-client.js';
|
|
5
6
|
import { budgetConversationHistory } from './kernel/memory/index.js';
|
|
7
|
+
import { buildLLMProviderFactory } from './kernel/provider-factory.js';
|
|
6
8
|
import {
|
|
7
9
|
buildContext,
|
|
8
10
|
handleInProcessTool,
|
|
9
11
|
resolveMcpTool,
|
|
10
|
-
getModelCostRates,
|
|
11
12
|
type ClaudeConfig,
|
|
12
|
-
type ContentBlock,
|
|
13
|
-
type Message,
|
|
14
|
-
type ApiResponse,
|
|
15
13
|
} from './claude-tools/index.js';
|
|
16
14
|
|
|
17
15
|
// Re-export for external consumers
|
|
18
16
|
export { buildContext, handleInProcessTool, resolveMcpTool, type ClaudeConfig } from './claude-tools/index.js';
|
|
19
17
|
|
|
20
|
-
// ─── Anthropic SSE streaming ─────────────────────────────────
|
|
21
|
-
|
|
22
|
-
type StreamBlockState = {
|
|
23
|
-
type: 'text' | 'tool_use';
|
|
24
|
-
text: string;
|
|
25
|
-
id: string;
|
|
26
|
-
name: string;
|
|
27
|
-
inputJson: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
async function* parseAnthropicSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<Record<string, unknown>> {
|
|
31
|
-
const reader = body.getReader();
|
|
32
|
-
const decoder = new TextDecoder();
|
|
33
|
-
let buffer = '';
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
while (true) {
|
|
37
|
-
const { done, value } = await reader.read();
|
|
38
|
-
if (done) break;
|
|
39
|
-
buffer += decoder.decode(value, { stream: true });
|
|
40
|
-
|
|
41
|
-
let newlineIdx: number;
|
|
42
|
-
while ((newlineIdx = buffer.indexOf('\n')) >= 0) {
|
|
43
|
-
const line = buffer.slice(0, newlineIdx).trim();
|
|
44
|
-
buffer = buffer.slice(newlineIdx + 1);
|
|
45
|
-
|
|
46
|
-
if (!line.startsWith('data: ')) continue;
|
|
47
|
-
const payload = line.slice(6).trim();
|
|
48
|
-
if (payload === '[DONE]') return;
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
yield JSON.parse(payload);
|
|
52
|
-
} catch {
|
|
53
|
-
// Malformed chunk — skip
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} finally {
|
|
58
|
-
reader.releaseLock();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function callAnthropicStream(
|
|
63
|
-
apiKey: string,
|
|
64
|
-
model: string,
|
|
65
|
-
system: string,
|
|
66
|
-
messages: Message[],
|
|
67
|
-
tools: unknown[],
|
|
68
|
-
onDelta: (text: string) => void,
|
|
69
|
-
baseUrl?: string,
|
|
70
|
-
): Promise<{ content: ContentBlock[]; stopReason: string; inputTokens: number; outputTokens: number }> {
|
|
71
|
-
const url = `${baseUrl || 'https://api.anthropic.com'}/v1/messages`;
|
|
72
|
-
const response = await fetch(url, {
|
|
73
|
-
method: 'POST',
|
|
74
|
-
headers: {
|
|
75
|
-
'Content-Type': 'application/json',
|
|
76
|
-
'x-api-key': apiKey,
|
|
77
|
-
'anthropic-version': '2023-06-01',
|
|
78
|
-
},
|
|
79
|
-
body: JSON.stringify({
|
|
80
|
-
model,
|
|
81
|
-
max_tokens: 4096,
|
|
82
|
-
system,
|
|
83
|
-
tools,
|
|
84
|
-
messages,
|
|
85
|
-
stream: true,
|
|
86
|
-
}),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (!response.ok) {
|
|
90
|
-
const errText = await response.text();
|
|
91
|
-
throw new Error(`Anthropic streaming error ${response.status}: ${errText}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!response.body) throw new Error('No response body for streaming');
|
|
95
|
-
|
|
96
|
-
const content: ContentBlock[] = [];
|
|
97
|
-
let currentBlock: StreamBlockState | null = null;
|
|
98
|
-
let stopReason = 'end_turn';
|
|
99
|
-
let inputTokens = 0;
|
|
100
|
-
let outputTokens = 0;
|
|
101
|
-
|
|
102
|
-
for await (const event of parseAnthropicSSE(response.body)) {
|
|
103
|
-
const type = event.type as string;
|
|
104
|
-
|
|
105
|
-
if (type === 'content_block_start') {
|
|
106
|
-
const cb = event.content_block as Record<string, unknown>;
|
|
107
|
-
currentBlock = {
|
|
108
|
-
type: ((cb.type as string) ?? 'text') as 'text' | 'tool_use',
|
|
109
|
-
text: (cb.text as string) ?? '',
|
|
110
|
-
id: (cb.id as string) ?? '',
|
|
111
|
-
name: (cb.name as string) ?? '',
|
|
112
|
-
inputJson: '',
|
|
113
|
-
};
|
|
114
|
-
} else if (type === 'content_block_delta') {
|
|
115
|
-
const delta = event.delta as Record<string, unknown>;
|
|
116
|
-
if (delta.type === 'text_delta' && currentBlock) {
|
|
117
|
-
currentBlock.text += (delta.text as string) ?? '';
|
|
118
|
-
onDelta((delta.text as string) ?? '');
|
|
119
|
-
} else if (delta.type === 'input_json_delta' && currentBlock) {
|
|
120
|
-
currentBlock.inputJson += (delta.partial_json as string) ?? '';
|
|
121
|
-
}
|
|
122
|
-
} else if (type === 'content_block_stop' && currentBlock) {
|
|
123
|
-
if (currentBlock.type === 'text') {
|
|
124
|
-
content.push({ type: 'text', text: currentBlock.text });
|
|
125
|
-
} else {
|
|
126
|
-
let input: Record<string, unknown> = {};
|
|
127
|
-
try { input = JSON.parse(currentBlock.inputJson || '{}'); } catch { /* empty input */ }
|
|
128
|
-
content.push({ type: 'tool_use', id: currentBlock.id, name: currentBlock.name, input });
|
|
129
|
-
}
|
|
130
|
-
currentBlock = null;
|
|
131
|
-
} else if (type === 'message_delta') {
|
|
132
|
-
const md = event.delta as Record<string, unknown> | undefined;
|
|
133
|
-
if (md?.stop_reason) stopReason = md.stop_reason as string;
|
|
134
|
-
} else if (type === 'message_start') {
|
|
135
|
-
const msg = event.message as Record<string, unknown> | undefined;
|
|
136
|
-
const usage = msg?.usage as Record<string, number> | undefined;
|
|
137
|
-
if (usage) inputTokens = usage.input_tokens ?? 0;
|
|
138
|
-
} else if (type === 'message_delta') {
|
|
139
|
-
const usage = event.usage as Record<string, number> | undefined;
|
|
140
|
-
if (usage) outputTokens = usage.output_tokens ?? 0;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Flush any remaining block
|
|
145
|
-
if (currentBlock) {
|
|
146
|
-
if (currentBlock.type === 'text') {
|
|
147
|
-
content.push({ type: 'text', text: currentBlock.text });
|
|
148
|
-
} else {
|
|
149
|
-
let input: Record<string, unknown> = {};
|
|
150
|
-
try { input = JSON.parse(currentBlock.inputJson || '{}'); } catch { /* empty input */ }
|
|
151
|
-
content.push({ type: 'tool_use', id: currentBlock.id, name: currentBlock.name, input });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return { content, stopReason, inputTokens, outputTokens };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
18
|
// ─── MCP Tool Health Tracker ─────────────────────────────────
|
|
159
19
|
// Tracks per-tool call outcomes so the heartbeat can surface degradation.
|
|
160
20
|
|
|
@@ -237,101 +97,138 @@ export async function callMcpWithRetry(
|
|
|
237
97
|
return `Tool unavailable: ${name}`;
|
|
238
98
|
}
|
|
239
99
|
|
|
100
|
+
type AnthropicToolDef = {
|
|
101
|
+
name?: unknown;
|
|
102
|
+
description?: unknown;
|
|
103
|
+
input_schema?: unknown;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function toolParameters(inputSchema: unknown): Tool['function']['parameters'] {
|
|
107
|
+
if (inputSchema && typeof inputSchema === 'object' && !Array.isArray(inputSchema)) {
|
|
108
|
+
const schema = inputSchema as Record<string, unknown>;
|
|
109
|
+
if (schema.type === 'object' && schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties)) {
|
|
110
|
+
return {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: schema.properties as Record<string, unknown>,
|
|
113
|
+
required: Array.isArray(schema.required) ? schema.required.filter((v): v is string => typeof v === 'string') : undefined,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { type: 'object', properties: {} };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toProviderTools(tools: unknown[]): Tool[] {
|
|
122
|
+
return (tools as AnthropicToolDef[])
|
|
123
|
+
.filter(tool => typeof tool.name === 'string')
|
|
124
|
+
.map(tool => ({
|
|
125
|
+
type: 'function' as const,
|
|
126
|
+
function: {
|
|
127
|
+
name: tool.name as string,
|
|
128
|
+
description: typeof tool.description === 'string' ? tool.description : '',
|
|
129
|
+
parameters: toolParameters(tool.input_schema),
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildClaudeProviderFactory(config: ClaudeConfig) {
|
|
135
|
+
if (config.edgeEnv) return buildLLMProviderFactory(config.edgeEnv);
|
|
136
|
+
return createLLMProviderFactory({
|
|
137
|
+
anthropic: {
|
|
138
|
+
apiKey: config.apiKey,
|
|
139
|
+
baseUrl: config.baseUrl,
|
|
140
|
+
},
|
|
141
|
+
fallbackRules: [],
|
|
142
|
+
enableCircuitBreaker: true,
|
|
143
|
+
enableRetries: true,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function initialMessages(conversationHistory: Array<{ role: 'user' | 'assistant'; content: string }>, userText: string): LLMMessage[] {
|
|
148
|
+
return [
|
|
149
|
+
...budgetConversationHistory(conversationHistory).map(message => ({
|
|
150
|
+
role: message.role,
|
|
151
|
+
content: message.content,
|
|
152
|
+
})),
|
|
153
|
+
{ role: 'user', content: userText },
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseToolArgs(call: ToolCall): Record<string, unknown> {
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(call.function.arguments || '{}');
|
|
160
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
161
|
+
} catch {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function executeToolCall(config: ClaudeConfig, anthropicConfig: { apiKey: string; model: string; baseUrl: string }, call: ToolCall): Promise<LLMToolResult> {
|
|
167
|
+
const args = parseToolArgs(call);
|
|
168
|
+
const inProcess = await handleInProcessTool(
|
|
169
|
+
config.db,
|
|
170
|
+
call.function.name,
|
|
171
|
+
args,
|
|
172
|
+
config.githubToken,
|
|
173
|
+
config.githubRepo,
|
|
174
|
+
config.braveApiKey,
|
|
175
|
+
config.roundtableDb,
|
|
176
|
+
anthropicConfig,
|
|
177
|
+
config.memoryBinding,
|
|
178
|
+
config.resendApiKeys,
|
|
179
|
+
config.edgeEnv,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (inProcess !== null) return { id: call.id, output: inProcess };
|
|
183
|
+
|
|
184
|
+
const resolved = resolveMcpTool(call.function.name, config.mcpClient, config.mcpRegistry);
|
|
185
|
+
if (resolved) {
|
|
186
|
+
return { id: call.id, output: await callMcpWithRetry(resolved.client, resolved.mcpName, args) };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { id: call.id, output: `Unknown tool: ${call.function.name}` };
|
|
190
|
+
}
|
|
191
|
+
|
|
240
192
|
export async function executeClaudeChat(
|
|
241
193
|
config: ClaudeConfig,
|
|
242
194
|
userText: string,
|
|
243
195
|
): Promise<{ text: string; cost: number }> {
|
|
244
196
|
config.userQuery = userText;
|
|
245
197
|
const { systemPrompt, tools, conversationHistory } = await buildContext(config, config.roundtableDb);
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
const messages
|
|
250
|
-
...budgetConversationHistory(conversationHistory),
|
|
251
|
-
{ role: 'user', content: userText },
|
|
252
|
-
];
|
|
198
|
+
const anthropicConfig = { apiKey: config.apiKey, model: config.model, baseUrl: config.baseUrl || 'https://api.anthropic.com' };
|
|
199
|
+
const factory = buildClaudeProviderFactory(config);
|
|
200
|
+
const providerTools = toProviderTools(tools);
|
|
201
|
+
const messages = initialMessages(conversationHistory, userText);
|
|
253
202
|
|
|
254
203
|
let totalCost = 0;
|
|
255
204
|
const MAX_TOOL_ROUNDS = 10;
|
|
256
205
|
|
|
257
206
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
},
|
|
265
|
-
body: JSON.stringify({
|
|
266
|
-
model: config.model,
|
|
267
|
-
max_tokens: 4096,
|
|
268
|
-
system: systemPrompt,
|
|
269
|
-
tools,
|
|
270
|
-
messages,
|
|
271
|
-
}),
|
|
207
|
+
const result = await factory.generateResponse({
|
|
208
|
+
messages: [...messages],
|
|
209
|
+
model: config.model,
|
|
210
|
+
systemPrompt,
|
|
211
|
+
tools: providerTools,
|
|
212
|
+
maxTokens: 4096,
|
|
272
213
|
});
|
|
273
214
|
|
|
274
|
-
|
|
275
|
-
const errText = await response.text();
|
|
276
|
-
throw new Error(`Anthropic API error ${response.status}: ${errText}`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const data = await response.json<ApiResponse>();
|
|
280
|
-
|
|
281
|
-
const rates = getModelCostRates(config.model);
|
|
282
|
-
totalCost += (data.usage.input_tokens * rates.input + data.usage.output_tokens * rates.output) / 1_000_000;
|
|
283
|
-
|
|
284
|
-
// Check if we're done (no tool use)
|
|
285
|
-
if (data.stop_reason === 'end_turn' || data.stop_reason === 'max_tokens') {
|
|
286
|
-
const textBlocks = data.content.filter(b => b.type === 'text');
|
|
287
|
-
return {
|
|
288
|
-
text: textBlocks.map(b => b.text ?? '').join('') || '(no response)',
|
|
289
|
-
cost: totalCost,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Handle tool use
|
|
294
|
-
if (data.stop_reason === 'tool_use') {
|
|
295
|
-
// Add assistant message with all content blocks
|
|
296
|
-
messages.push({ role: 'assistant', content: data.content });
|
|
297
|
-
|
|
298
|
-
// Process each tool use
|
|
299
|
-
const toolResults: ContentBlock[] = [];
|
|
300
|
-
|
|
301
|
-
for (const block of data.content) {
|
|
302
|
-
if (block.type !== 'tool_use' || !block.id || !block.name) continue;
|
|
303
|
-
|
|
304
|
-
let result: string;
|
|
305
|
-
const inProcess = await handleInProcessTool(config.db, block.name, block.input ?? {}, config.githubToken, config.githubRepo, config.braveApiKey, config.roundtableDb, anthropicConfig, config.memoryBinding, config.resendApiKeys);
|
|
306
|
-
if (inProcess !== null) {
|
|
307
|
-
result = inProcess;
|
|
308
|
-
} else {
|
|
309
|
-
const resolved = resolveMcpTool(block.name, config.mcpClient, config.mcpRegistry);
|
|
310
|
-
if (resolved) {
|
|
311
|
-
result = await callMcpWithRetry(resolved.client, resolved.mcpName, block.input ?? {});
|
|
312
|
-
} else {
|
|
313
|
-
result = `Unknown tool: ${block.name}`;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result } as unknown as ContentBlock);
|
|
318
|
-
}
|
|
215
|
+
totalCost += result.usage.cost;
|
|
319
216
|
|
|
320
|
-
|
|
321
|
-
|
|
217
|
+
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
218
|
+
return { text: result.message || '(no response)', cost: totalCost };
|
|
322
219
|
}
|
|
323
220
|
|
|
324
|
-
const
|
|
325
|
-
|
|
221
|
+
const toolResults = await Promise.all(result.toolCalls.map(call => executeToolCall(config, anthropicConfig, call)));
|
|
222
|
+
messages.push({ role: 'assistant', content: result.message, toolCalls: result.toolCalls });
|
|
223
|
+
messages.push({ role: 'user', content: '', toolResults });
|
|
326
224
|
}
|
|
327
225
|
|
|
328
226
|
return { text: '(reached maximum tool rounds)', cost: totalCost };
|
|
329
227
|
}
|
|
330
228
|
|
|
331
229
|
// ─── Streaming variant ───────────────────────────────────────
|
|
332
|
-
// Tool-use rounds
|
|
333
|
-
//
|
|
334
|
-
// "I'll check the dashboard..." text never leaks to the UI.
|
|
230
|
+
// Tool-use rounds use the provider factory. The final answer is emitted as one
|
|
231
|
+
// delta so intermediate tool-planning text stays invisible to the UI.
|
|
335
232
|
|
|
336
233
|
export async function executeClaudeChatStream(
|
|
337
234
|
config: ClaudeConfig,
|
|
@@ -340,66 +237,34 @@ export async function executeClaudeChatStream(
|
|
|
340
237
|
): Promise<{ text: string; cost: number }> {
|
|
341
238
|
config.userQuery = userText;
|
|
342
239
|
const { systemPrompt, tools, conversationHistory } = await buildContext(config, config.roundtableDb);
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
const messages
|
|
240
|
+
const anthropicConfig = { apiKey: config.apiKey, model: config.model, baseUrl: config.baseUrl || 'https://api.anthropic.com' };
|
|
241
|
+
const factory = buildClaudeProviderFactory(config);
|
|
242
|
+
const providerTools = toProviderTools(tools);
|
|
243
|
+
const messages = initialMessages(conversationHistory, userText);
|
|
347
244
|
|
|
348
245
|
let totalCost = 0;
|
|
349
246
|
const MAX_TOOL_ROUNDS = 10;
|
|
350
247
|
|
|
351
248
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
const rates = getModelCostRates(config.model);
|
|
362
|
-
totalCost += (inputTokens * rates.input + outputTokens * rates.output) / 1_000_000;
|
|
363
|
-
|
|
364
|
-
if (stopReason === 'end_turn' || stopReason === 'max_tokens') {
|
|
365
|
-
// Flush buffered deltas to caller now that we know it's the final round
|
|
366
|
-
for (const delta of roundBuffer) onDelta(delta);
|
|
367
|
-
const text = content.filter(b => b.type === 'text').map(b => b.text ?? '').join('') || '(no response)';
|
|
368
|
-
return { text, cost: totalCost };
|
|
369
|
-
}
|
|
249
|
+
const result = await factory.generateResponse({
|
|
250
|
+
messages: [...messages],
|
|
251
|
+
model: config.model,
|
|
252
|
+
systemPrompt,
|
|
253
|
+
tools: providerTools,
|
|
254
|
+
maxTokens: 4096,
|
|
255
|
+
});
|
|
370
256
|
|
|
371
|
-
|
|
372
|
-
// Discard roundBuffer — tool-use intermediate text stays invisible
|
|
373
|
-
messages.push({ role: 'assistant', content });
|
|
374
|
-
const toolResults: ContentBlock[] = [];
|
|
375
|
-
|
|
376
|
-
for (const block of content) {
|
|
377
|
-
if (block.type !== 'tool_use' || !block.id || !block.name) continue;
|
|
378
|
-
let result: string;
|
|
379
|
-
|
|
380
|
-
const inProcess = await handleInProcessTool(config.db, block.name, block.input ?? {}, config.githubToken, config.githubRepo, config.braveApiKey, config.roundtableDb, anthropicConfigStream, config.memoryBinding);
|
|
381
|
-
if (inProcess !== null) {
|
|
382
|
-
result = inProcess;
|
|
383
|
-
} else {
|
|
384
|
-
const resolved = resolveMcpTool(block.name, config.mcpClient, config.mcpRegistry);
|
|
385
|
-
if (resolved) {
|
|
386
|
-
result = await callMcpWithRetry(resolved.client, resolved.mcpName, block.input ?? {});
|
|
387
|
-
} else {
|
|
388
|
-
result = `Unknown tool: ${block.name}`;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result } as unknown as ContentBlock);
|
|
393
|
-
}
|
|
257
|
+
totalCost += result.usage.cost;
|
|
394
258
|
|
|
395
|
-
|
|
396
|
-
|
|
259
|
+
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
260
|
+
const text = result.message || '(no response)';
|
|
261
|
+
onDelta(text);
|
|
262
|
+
return { text, cost: totalCost };
|
|
397
263
|
}
|
|
398
264
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return { text, cost: totalCost };
|
|
265
|
+
const toolResults = await Promise.all(result.toolCalls.map(call => executeToolCall(config, anthropicConfig, call)));
|
|
266
|
+
messages.push({ role: 'assistant', content: result.message, toolCalls: result.toolCalls });
|
|
267
|
+
messages.push({ role: 'user', content: '', toolResults });
|
|
403
268
|
}
|
|
404
269
|
|
|
405
270
|
return { text: '(reached maximum tool rounds)', cost: totalCost };
|
package/src/codebeast.ts
CHANGED
|
File without changes
|