whale-code 6.5.3 → 6.5.5
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/cli/services/agent-config.d.ts +25 -0
- package/dist/cli/services/agent-config.js +61 -0
- package/dist/cli/services/agent-loop.js +30 -9
- package/dist/cli/services/error-logger.d.ts +2 -3
- package/dist/cli/services/error-logger.js +43 -52
- package/dist/cli/services/subagent.js +11 -7
- package/dist/cli/services/teammate.js +28 -14
- package/dist/index.js +16 -5
- package/dist/server/handlers/api-docs.d.ts +6 -0
- package/dist/server/handlers/api-docs.js +1478 -0
- package/dist/server/handlers/api-keys.js +16 -2
- package/dist/server/handlers/comms.d.ts +0 -53
- package/dist/server/handlers/comms.js +45 -27
- package/dist/server/handlers/voice.js +22 -0
- package/dist/server/index.js +57 -26
- package/dist/server/lib/clickhouse-client.js +2 -2
- package/dist/server/lib/pdf-renderer.d.ts +1 -1
- package/dist/server/lib/pdf-renderer.js +18 -4
- package/dist/server/lib/server-agent-loop.d.ts +6 -0
- package/dist/server/lib/server-agent-loop.js +20 -10
- package/dist/server/lib/server-subagent.d.ts +2 -0
- package/dist/server/lib/server-subagent.js +4 -2
- package/dist/server/providers/anthropic.js +4 -4
- package/dist/server/providers/bedrock.js +4 -4
- package/dist/server/tool-router.d.ts +13 -0
- package/dist/server/tool-router.js +3 -1
- package/dist/shared/agent-core.d.ts +86 -8
- package/dist/shared/agent-core.js +94 -19
- package/dist/shared/api-client.d.ts +1 -0
- package/dist/shared/api-client.js +2 -2
- package/dist/shared/tool-dispatch.d.ts +0 -2
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Config Loader — fetches agent settings from ai_agent_config (Supabase).
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth: the DB record IS the config.
|
|
5
|
+
* Nothing is hardcoded — all behavioral knobs come from context_config JSONB.
|
|
6
|
+
* Cached for the session lifetime (5 min TTL) to avoid hammering the DB.
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentContextConfig } from "../../shared/agent-core.js";
|
|
9
|
+
export interface CLIAgentConfig {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
model: string;
|
|
13
|
+
max_tokens: number;
|
|
14
|
+
max_tool_calls: number;
|
|
15
|
+
temperature: number;
|
|
16
|
+
context_config: AgentContextConfig | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fetch the Whale Code agent's config from ai_agent_config.
|
|
20
|
+
* Uses the defaultAgentId from CLI config, or falls back to the known Whale Code UUID.
|
|
21
|
+
* Cached for 5 minutes — call freely from hot paths.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadCLIAgentConfig(): Promise<CLIAgentConfig | null>;
|
|
24
|
+
/** Clear the cache (e.g., on /clear or session reset) */
|
|
25
|
+
export declare function resetAgentConfigCache(): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Config Loader — fetches agent settings from ai_agent_config (Supabase).
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth: the DB record IS the config.
|
|
5
|
+
* Nothing is hardcoded — all behavioral knobs come from context_config JSONB.
|
|
6
|
+
* Cached for the session lifetime (5 min TTL) to avoid hammering the DB.
|
|
7
|
+
*/
|
|
8
|
+
import { createClient } from "@supabase/supabase-js";
|
|
9
|
+
import { resolveConfig } from "./config-store.js";
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// CACHE
|
|
12
|
+
// ============================================================================
|
|
13
|
+
let cachedConfig = null;
|
|
14
|
+
let cacheTimestamp = 0;
|
|
15
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
/**
|
|
17
|
+
* Fetch the Whale Code agent's config from ai_agent_config.
|
|
18
|
+
* Uses the defaultAgentId from CLI config, or falls back to the known Whale Code UUID.
|
|
19
|
+
* Cached for 5 minutes — call freely from hot paths.
|
|
20
|
+
*/
|
|
21
|
+
export async function loadCLIAgentConfig() {
|
|
22
|
+
if (cachedConfig && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
|
|
23
|
+
return cachedConfig;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const { supabaseUrl, supabaseKey, defaultAgentId } = resolveConfig();
|
|
27
|
+
if (!supabaseUrl || !supabaseKey)
|
|
28
|
+
return cachedConfig;
|
|
29
|
+
const agentId = defaultAgentId || "72d7aaa8-2e8b-48a0-a38d-f332f69600bb";
|
|
30
|
+
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
31
|
+
const { data, error } = await supabase
|
|
32
|
+
.from("ai_agent_config")
|
|
33
|
+
.select("id, name, model, max_tokens, max_tool_calls, temperature, context_config")
|
|
34
|
+
.eq("id", agentId)
|
|
35
|
+
.single();
|
|
36
|
+
if (error || !data) {
|
|
37
|
+
// Silently fall back to cached config on error — never block the CLI
|
|
38
|
+
return cachedConfig;
|
|
39
|
+
}
|
|
40
|
+
cachedConfig = {
|
|
41
|
+
id: data.id,
|
|
42
|
+
name: data.name,
|
|
43
|
+
model: data.model,
|
|
44
|
+
max_tokens: data.max_tokens,
|
|
45
|
+
max_tool_calls: data.max_tool_calls,
|
|
46
|
+
temperature: data.temperature,
|
|
47
|
+
context_config: data.context_config,
|
|
48
|
+
};
|
|
49
|
+
cacheTimestamp = Date.now();
|
|
50
|
+
return cachedConfig;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Network failure, auth expired, etc. — never crash the CLI
|
|
54
|
+
return cachedConfig;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Clear the cache (e.g., on /clear or session reset) */
|
|
58
|
+
export function resetAgentConfigCache() {
|
|
59
|
+
cachedConfig = null;
|
|
60
|
+
cacheTimestamp = 0;
|
|
61
|
+
}
|
|
@@ -26,7 +26,8 @@ import { captureError, addBreadcrumb } from "./error-logger.js";
|
|
|
26
26
|
import { setGlobalEmitter, clearGlobalEmitter, } from "./agent-events.js";
|
|
27
27
|
import { mcpClientManager } from "./mcp-client.js";
|
|
28
28
|
import { loadHooks, runBeforeToolHook, runAfterToolHook, runSessionHook } from "./hooks.js";
|
|
29
|
-
import { LoopDetector,
|
|
29
|
+
import { LoopDetector, COMPACTION_TOTAL_BUDGET, getCompactionConfig, getContextManagement, DEFAULT_SESSION_COST_BUDGET_USD, emitCostWarningIfNeeded, resolveAgentLoopConfig, AGENT_DEFAULTS } from "../../shared/agent-core.js";
|
|
30
|
+
import { loadCLIAgentConfig, resetAgentConfigCache } from "./agent-config.js";
|
|
30
31
|
import { parseSSEStream, processStreamWithCallbacks, collectStreamResult } from "../../shared/sse-parser.js";
|
|
31
32
|
import { callServerProxy, callTranscribe, buildAPIRequest, buildSystemBlocks, prepareWithCaching, trimGeminiContext, trimOpenAIContext, requestProviderCompaction } from "../../shared/api-client.js";
|
|
32
33
|
import { getProvider, MODELS } from "../../shared/constants.js";
|
|
@@ -88,10 +89,13 @@ export function resetSessionState() {
|
|
|
88
89
|
resetGitContext();
|
|
89
90
|
resetClaudeMdCache();
|
|
90
91
|
clearReadCache();
|
|
92
|
+
resetAgentConfigCache();
|
|
91
93
|
}
|
|
92
94
|
/** CLI-only: loop detector — persists session error state across turns (reset by resetSessionState) */
|
|
93
95
|
let sessionLoopDetector = null;
|
|
94
|
-
|
|
96
|
+
// CLI max turns — high ceiling since user controls via Ctrl+C.
|
|
97
|
+
// DB agent config maxTurns is used when available, this is the absolute safety cap.
|
|
98
|
+
const CLI_MAX_TURNS_CAP = 200;
|
|
95
99
|
// ============================================================================
|
|
96
100
|
// SHELL OUTPUT SUMMARIZATION
|
|
97
101
|
// ============================================================================
|
|
@@ -231,7 +235,8 @@ export async function runAgentLoop(opts) {
|
|
|
231
235
|
if (emitter) {
|
|
232
236
|
setGlobalEmitter(emitter);
|
|
233
237
|
}
|
|
234
|
-
|
|
238
|
+
// effectiveMaxTurns resolved after DB config fetch below
|
|
239
|
+
let effectiveMaxTurns = opts.maxTurns || CLI_MAX_TURNS_CAP;
|
|
235
240
|
// Load hooks from project and user config
|
|
236
241
|
const hooksCwd = opts.cwd || process.cwd();
|
|
237
242
|
const hooks = loadHooks(hooksCwd);
|
|
@@ -308,8 +313,27 @@ export async function runAgentLoop(opts) {
|
|
|
308
313
|
let sessionCostUsd = 0;
|
|
309
314
|
let compactionCount = 0;
|
|
310
315
|
const costWarningsEmitted = new Set();
|
|
311
|
-
const effectiveBudget = opts.maxBudgetUsd ?? DEFAULT_SESSION_COST_BUDGET_USD;
|
|
312
316
|
const activeModel = getModel();
|
|
317
|
+
// Fetch agent config from DB — single source of truth, nothing hardcoded.
|
|
318
|
+
// Falls back to AGENT_DEFAULTS only if DB is unreachable (never blocks).
|
|
319
|
+
const dbAgent = await loadCLIAgentConfig();
|
|
320
|
+
const resolved = dbAgent
|
|
321
|
+
? resolveAgentLoopConfig(dbAgent, "sse")
|
|
322
|
+
: null;
|
|
323
|
+
const effectiveBudget = opts.maxBudgetUsd
|
|
324
|
+
?? resolved?.contextOverrides.session_cost_budget_usd
|
|
325
|
+
?? DEFAULT_SESSION_COST_BUDGET_USD;
|
|
326
|
+
// Compaction settings from DB via getContextManagement — pass DB overrides so
|
|
327
|
+
// the trigger/budget values match what the Anthropic API actually uses.
|
|
328
|
+
const ctxOverrides = resolved?.contextOverrides;
|
|
329
|
+
const ctxMgmt = getContextManagement(activeModel, ctxOverrides);
|
|
330
|
+
const compactEdit = ctxMgmt.config.edits.find((e) => e.type === "compact_20260112");
|
|
331
|
+
const effectiveCompactionTrigger = compactEdit?.trigger?.value ?? AGENT_DEFAULTS.compactionTriggerTokens;
|
|
332
|
+
const effectiveCompactionBudget = ctxOverrides?.compaction_total_budget ?? COMPACTION_TOTAL_BUDGET;
|
|
333
|
+
// Override maxTurns from DB config (if not explicitly set by caller)
|
|
334
|
+
if (!opts.maxTurns && resolved) {
|
|
335
|
+
effectiveMaxTurns = resolved.maxTurns;
|
|
336
|
+
}
|
|
313
337
|
// Tool executor — routes to interactive, local, server, or MCP tools.
|
|
314
338
|
// Wraps execution with before/after hooks when hooks are loaded.
|
|
315
339
|
const INTERACTIVE_TOOL_NAMES = new Set(INTERACTIVE_TOOL_DEFINITIONS.map(t => t.name));
|
|
@@ -375,9 +399,6 @@ export async function runAgentLoop(opts) {
|
|
|
375
399
|
}
|
|
376
400
|
return result;
|
|
377
401
|
};
|
|
378
|
-
// Deprecated: Anthropic context_management handles limits via clear_tool_uses + compaction.
|
|
379
|
-
// tool-dispatch.ts uses SAFETY_MAX_CHARS (500K) — this value is ignored.
|
|
380
|
-
const maxResultChars = undefined;
|
|
381
402
|
try {
|
|
382
403
|
for (let iteration = 0; iteration < effectiveMaxTurns; iteration++) {
|
|
383
404
|
if (abortSignal?.aborted) {
|
|
@@ -401,6 +422,7 @@ export async function runAgentLoop(opts) {
|
|
|
401
422
|
model: currentModel,
|
|
402
423
|
contextProfile: "main",
|
|
403
424
|
thinkingEnabled: opts.thinking,
|
|
425
|
+
contextOverrides: ctxOverrides,
|
|
404
426
|
});
|
|
405
427
|
// Prepare with prompt caching
|
|
406
428
|
let { tools: cachedTools, messages: cachedMessages } = prepareWithCaching(tools, messages);
|
|
@@ -557,7 +579,7 @@ export async function runAgentLoop(opts) {
|
|
|
557
579
|
compactionCount++;
|
|
558
580
|
logSpan({ action: "chat.compaction_pause", durationMs: Date.now() - apiStart, context: turnCtx, storeId: storeId || undefined, details: { compaction_count: compactionCount, messages_before: messages.length } });
|
|
559
581
|
// Budget enforcement
|
|
560
|
-
if (compactionCount *
|
|
582
|
+
if (compactionCount * effectiveCompactionTrigger >= effectiveCompactionBudget) {
|
|
561
583
|
const budgetMsg = "\n[Context budget reached — wrapping up.]";
|
|
562
584
|
if (emitter) {
|
|
563
585
|
emitter.emitText(budgetMsg);
|
|
@@ -605,7 +627,6 @@ export async function runAgentLoop(opts) {
|
|
|
605
627
|
const { results: toolResults, bailOut, bailMessage } = await dispatchTools(result.toolUseBlocks, toolExecutor, {
|
|
606
628
|
loopDetector,
|
|
607
629
|
maxConcurrent: 7,
|
|
608
|
-
maxResultChars,
|
|
609
630
|
onStart: (name, input) => {
|
|
610
631
|
callbacks.onToolStart(name, input);
|
|
611
632
|
},
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Error Logger — fire-and-forget error capture to error_events table
|
|
2
|
+
* Error Logger — fire-and-forget error capture to ClickHouse error_events table
|
|
3
3
|
*
|
|
4
|
-
* Sentry replacement built on our own
|
|
4
|
+
* Sentry replacement built on our own ClickHouse infrastructure.
|
|
5
5
|
* Mirrors the Swift WhaleErrorLogger: same fingerprinting, same table, same schema.
|
|
6
|
-
* Reuses the telemetry.ts Supabase client pattern.
|
|
7
6
|
*/
|
|
8
7
|
export interface ErrorLoggerConfig {
|
|
9
8
|
serviceName: string;
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Error Logger — fire-and-forget error capture to error_events table
|
|
2
|
+
* Error Logger — fire-and-forget error capture to ClickHouse error_events table
|
|
3
3
|
*
|
|
4
|
-
* Sentry replacement built on our own
|
|
4
|
+
* Sentry replacement built on our own ClickHouse infrastructure.
|
|
5
5
|
* Mirrors the Swift WhaleErrorLogger: same fingerprinting, same table, same schema.
|
|
6
|
-
* Reuses the telemetry.ts Supabase client pattern.
|
|
7
6
|
*/
|
|
8
|
-
import { createClient } from "@supabase/supabase-js";
|
|
9
7
|
import { createRequire } from "module";
|
|
10
8
|
import { createHash } from "crypto";
|
|
11
9
|
import os from "os";
|
|
12
|
-
import { resolveConfig } from "./config-store.js";
|
|
13
|
-
import { getValidToken, createAuthenticatedClient } from "./auth-service.js";
|
|
14
10
|
const require = createRequire(import.meta.url);
|
|
15
11
|
const PKG_VERSION = require("../../../package.json").version;
|
|
16
12
|
// ============================================================================
|
|
@@ -24,7 +20,6 @@ let config = {
|
|
|
24
20
|
maxBreadcrumbs: 25,
|
|
25
21
|
};
|
|
26
22
|
let breadcrumbs = [];
|
|
27
|
-
let supabaseClient = null;
|
|
28
23
|
// User context (set externally)
|
|
29
24
|
let currentUserId;
|
|
30
25
|
let currentUserEmail;
|
|
@@ -98,10 +93,12 @@ export function captureMessage(message, severity = "info", extra) {
|
|
|
98
93
|
// INTERNAL
|
|
99
94
|
// ============================================================================
|
|
100
95
|
async function _captureError(opts) {
|
|
101
|
-
const
|
|
102
|
-
|
|
96
|
+
const host = process.env.CLICKHOUSE_HOST || "jmp0o2f6so.us-east-1.aws.clickhouse.cloud";
|
|
97
|
+
const user = process.env.CLICKHOUSE_USER || "default";
|
|
98
|
+
const password = process.env.CLICKHOUSE_PASSWORD;
|
|
99
|
+
if (!host || !password) {
|
|
103
100
|
if (process.env.DEBUG_TELEMETRY) {
|
|
104
|
-
process.stderr.write(`[error-logger]
|
|
101
|
+
process.stderr.write(`[error-logger] ClickHouse not configured\n`);
|
|
105
102
|
}
|
|
106
103
|
return;
|
|
107
104
|
}
|
|
@@ -114,61 +111,55 @@ async function _captureError(opts) {
|
|
|
114
111
|
? `${opts.file}:${opts.function}`
|
|
115
112
|
: extractSourceFromStack(stackTrace);
|
|
116
113
|
const fingerprint = computeFingerprint(errorType, normalizedMessage, sourceLocation);
|
|
117
|
-
// Parse source from stack if not provided
|
|
118
114
|
const parsedSource = parseSourceFromStack(stackTrace);
|
|
119
115
|
const event = {
|
|
120
116
|
fingerprint,
|
|
121
117
|
severity: opts.severity || "error",
|
|
122
118
|
error_type: errorType,
|
|
123
|
-
error_message: errorMessage,
|
|
124
|
-
error_code: opts.errorCode,
|
|
125
|
-
source_file: opts.file || parsedSource.file,
|
|
126
|
-
source_line: opts.line || parsedSource.line,
|
|
127
|
-
source_function: opts.function || parsedSource.function,
|
|
128
|
-
stack_trace: stackTrace,
|
|
129
|
-
stack_frames: stackTrace ? parseStackFrames(stackTrace) : undefined,
|
|
130
|
-
breadcrumbs: [...breadcrumbs],
|
|
119
|
+
error_message: errorMessage.slice(0, 4096),
|
|
120
|
+
error_code: opts.errorCode || "",
|
|
121
|
+
source_file: opts.file || parsedSource.file || "",
|
|
122
|
+
source_line: opts.line || parsedSource.line || 0,
|
|
123
|
+
source_function: opts.function || parsedSource.function || "",
|
|
124
|
+
stack_trace: stackTrace || "",
|
|
131
125
|
platform: config.platform || "typescript",
|
|
132
126
|
service_name: config.serviceName,
|
|
133
|
-
service_version: config.serviceVersion,
|
|
127
|
+
service_version: config.serviceVersion || "",
|
|
134
128
|
environment: config.environment || "production",
|
|
135
|
-
device_info: collectDeviceInfo(),
|
|
136
|
-
runtime_info: collectRuntimeInfo(),
|
|
137
|
-
store_id: opts.storeId || currentStoreId,
|
|
138
|
-
user_id: opts.userId || currentUserId,
|
|
139
|
-
user_email: opts.userEmail || currentUserEmail,
|
|
140
|
-
trace_id: opts.traceId,
|
|
141
|
-
span_id: opts.spanId,
|
|
142
|
-
|
|
143
|
-
|
|
129
|
+
device_info: JSON.stringify(collectDeviceInfo()),
|
|
130
|
+
runtime_info: JSON.stringify(collectRuntimeInfo()),
|
|
131
|
+
store_id: opts.storeId || currentStoreId || "",
|
|
132
|
+
user_id: opts.userId || currentUserId || "",
|
|
133
|
+
user_email: opts.userEmail || currentUserEmail || "",
|
|
134
|
+
trace_id: opts.traceId || "",
|
|
135
|
+
span_id: opts.spanId || "",
|
|
136
|
+
request_id: "",
|
|
137
|
+
tags: JSON.stringify(opts.tags || {}),
|
|
138
|
+
extra: JSON.stringify(opts.extra || {}),
|
|
139
|
+
breadcrumbs: JSON.stringify(breadcrumbs),
|
|
144
140
|
occurred_at: new Date().toISOString(),
|
|
145
141
|
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return supabaseClient;
|
|
157
|
-
const cfg = resolveConfig();
|
|
158
|
-
// Prefer service role key
|
|
159
|
-
if (cfg.supabaseUrl && cfg.supabaseKey) {
|
|
160
|
-
supabaseClient = createClient(cfg.supabaseUrl, cfg.supabaseKey, {
|
|
161
|
-
auth: { persistSession: false, autoRefreshToken: false },
|
|
142
|
+
try {
|
|
143
|
+
const credentials = Buffer.from(`${user}:${password}`).toString("base64");
|
|
144
|
+
const url = `https://${host}:8443/?query=${encodeURIComponent("INSERT INTO error_events FORMAT JSONEachRow")}`;
|
|
145
|
+
const response = await fetch(url, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Basic ${credentials}`,
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify(event),
|
|
162
152
|
});
|
|
163
|
-
|
|
153
|
+
if (!response.ok && process.env.DEBUG_TELEMETRY) {
|
|
154
|
+
const text = await response.text();
|
|
155
|
+
process.stderr.write(`[error-logger] ClickHouse ${response.status}: ${text.slice(0, 200)}\n`);
|
|
156
|
+
}
|
|
164
157
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return supabaseClient;
|
|
158
|
+
catch (err) {
|
|
159
|
+
if (process.env.DEBUG_TELEMETRY) {
|
|
160
|
+
process.stderr.write(`[error-logger] insert failed: ${err.message}\n`);
|
|
161
|
+
}
|
|
170
162
|
}
|
|
171
|
-
return null;
|
|
172
163
|
}
|
|
173
164
|
// ============================================================================
|
|
174
165
|
// FINGERPRINTING (identical algorithm to Swift)
|
|
@@ -11,7 +11,8 @@ import { readFileSync, existsSync, writeFileSync, mkdirSync, appendFileSync } fr
|
|
|
11
11
|
import { join } from "path";
|
|
12
12
|
import { homedir, tmpdir } from "os";
|
|
13
13
|
import { LOCAL_TOOL_DEFINITIONS, } from "./local-tools.js";
|
|
14
|
-
import { LoopDetector, estimateCostUsd, demoteSubagentModel } from "../../shared/agent-core.js";
|
|
14
|
+
import { LoopDetector, estimateCostUsd, demoteSubagentModel, AGENT_DEFAULTS } from "../../shared/agent-core.js";
|
|
15
|
+
import { loadCLIAgentConfig } from "./agent-config.js";
|
|
15
16
|
import { MODEL_MAP } from "../../shared/constants.js";
|
|
16
17
|
import { loadServerToolDefinitions, } from "./server-tools.js";
|
|
17
18
|
import { logSpan, generateSpanId, generateTraceId } from "./telemetry.js";
|
|
@@ -23,10 +24,9 @@ import { callAgentAPI, executeToolBlocks, extractTextBlocks, extractToolUseBlock
|
|
|
23
24
|
// CONSTANTS
|
|
24
25
|
// ============================================================================
|
|
25
26
|
const AGENTS_DIR = join(homedir(), ".swagmanager", "agents");
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
const
|
|
29
|
-
const SUBAGENT_MAX_OUTPUT_TOKENS = 32_000; // Claude Code parity — subagents get 32K output
|
|
27
|
+
// Subagent defaults — overridden by DB config (ai_agent_config.context_config) when available.
|
|
28
|
+
// These are safety caps only; actual values come from AGENT_DEFAULTS or the resolved config.
|
|
29
|
+
const SUBAGENT_MAX_TURNS_CAP = 50; // Absolute safety cap
|
|
30
30
|
// ============================================================================
|
|
31
31
|
// AGENT PROMPTS — specialized per type
|
|
32
32
|
// ============================================================================
|
|
@@ -289,7 +289,11 @@ export async function runSubagent(options) {
|
|
|
289
289
|
const cwd = process.cwd();
|
|
290
290
|
const systemPrompt = buildAgentPrompt(subagent_type, cwd);
|
|
291
291
|
const startTime = Date.now();
|
|
292
|
-
|
|
292
|
+
// DB config → caller override → AGENT_DEFAULTS fallback
|
|
293
|
+
const dbAgent = await loadCLIAgentConfig();
|
|
294
|
+
const dbMaxTurns = dbAgent?.context_config?.subagent_max_turns ?? AGENT_DEFAULTS.subagentMaxTurns;
|
|
295
|
+
const dbMaxTokens = dbAgent?.context_config?.subagent_max_tokens ?? AGENT_DEFAULTS.subagentMaxTokens;
|
|
296
|
+
const effectiveMaxTurns = max_turns ? Math.max(1, Math.min(SUBAGENT_MAX_TURNS_CAP, max_turns)) : dbMaxTurns;
|
|
293
297
|
// Extract short description from prompt (first sentence or 60 chars)
|
|
294
298
|
const descMatch = prompt.match(/^[^.!?\n]+/);
|
|
295
299
|
const shortDescription = name || (descMatch
|
|
@@ -369,7 +373,7 @@ export async function runSubagent(options) {
|
|
|
369
373
|
messages: state.messages,
|
|
370
374
|
tools,
|
|
371
375
|
thinkingEnabled: false,
|
|
372
|
-
maxOutputTokens:
|
|
376
|
+
maxOutputTokens: dbMaxTokens,
|
|
373
377
|
cacheLastTool: true,
|
|
374
378
|
});
|
|
375
379
|
lastError = null;
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { Worker, parentPort, workerData, isMainThread } from "worker_threads";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
import { loadTeam, claimTask, completeTask, failTask, getAvailableTasks, sendMessage, getUnreadMessages, markMessagesRead, updateTeammate, } from "./team-state.js";
|
|
13
|
-
import { LoopDetector, estimateCostUsd } from "../../shared/agent-core.js";
|
|
13
|
+
import { LoopDetector, estimateCostUsd, AGENT_DEFAULTS } from "../../shared/agent-core.js";
|
|
14
|
+
import { loadCLIAgentConfig } from "./agent-config.js";
|
|
14
15
|
import { MODEL_MAP } from "../../shared/constants.js";
|
|
15
16
|
import { LOCAL_TOOL_DEFINITIONS, } from "./local-tools.js";
|
|
16
17
|
import { loadServerToolDefinitions, } from "./server-tools.js";
|
|
@@ -20,12 +21,23 @@ import { callAgentAPI, executeToolBlocks, extractTextBlocks, extractToolUseBlock
|
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// CONSTANTS
|
|
22
23
|
// ============================================================================
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
24
|
+
// Teammate constants — behavioral defaults from DB, infrastructure timeouts hardcoded (acceptable)
|
|
25
|
+
let TEAMMATE_MAX_TURNS_PER_TASK = 12;
|
|
26
|
+
let TEAMMATE_MAX_OUTPUT_TOKENS = 16384;
|
|
27
|
+
let TEAMMATE_MAX_TOTAL_TURNS = 50;
|
|
28
|
+
let TEAMMATE_MAX_DURATION_MS = AGENT_DEFAULTS.maxDurationMs;
|
|
29
|
+
const API_TIMEOUT_MS = 90_000; // 90s timeout on proxy/API fetch calls (infrastructure)
|
|
30
|
+
const TOOL_TIMEOUT_MS = 60_000; // 60s timeout on individual tool execution (infrastructure)
|
|
31
|
+
// Lazy init from DB — called once per teammate spawn
|
|
32
|
+
async function resolveTeammateConfig() {
|
|
33
|
+
const dbAgent = await loadCLIAgentConfig();
|
|
34
|
+
if (!dbAgent?.context_config)
|
|
35
|
+
return;
|
|
36
|
+
const cc = dbAgent.context_config;
|
|
37
|
+
TEAMMATE_MAX_TURNS_PER_TASK = cc.subagent_max_turns ?? TEAMMATE_MAX_TURNS_PER_TASK;
|
|
38
|
+
TEAMMATE_MAX_OUTPUT_TOKENS = cc.subagent_max_tokens ?? TEAMMATE_MAX_OUTPUT_TOKENS;
|
|
39
|
+
TEAMMATE_MAX_DURATION_MS = cc.max_duration_ms ?? TEAMMATE_MAX_DURATION_MS;
|
|
40
|
+
}
|
|
29
41
|
// ============================================================================
|
|
30
42
|
// TEAMMATE SYSTEM PROMPT
|
|
31
43
|
// ============================================================================
|
|
@@ -221,7 +233,7 @@ async function callAPI(modelId, systemPrompt, messages, tools, thinkingEnabled =
|
|
|
221
233
|
messages,
|
|
222
234
|
tools,
|
|
223
235
|
thinkingEnabled,
|
|
224
|
-
maxOutputTokens:
|
|
236
|
+
maxOutputTokens: TEAMMATE_MAX_OUTPUT_TOKENS,
|
|
225
237
|
timeoutMs: API_TIMEOUT_MS,
|
|
226
238
|
});
|
|
227
239
|
}
|
|
@@ -232,6 +244,8 @@ async function runTeammateLoop(data) {
|
|
|
232
244
|
const { teamId, teammateId, teammateName, model, cwd, parentConversationId, teamName, authToken } = data;
|
|
233
245
|
const modelId = MODEL_MAP[model] || MODEL_MAP.opus; // Inherit parent default
|
|
234
246
|
const startTime = Date.now();
|
|
247
|
+
// Load behavioral config from DB — no hardcoded values
|
|
248
|
+
await resolveTeammateConfig();
|
|
235
249
|
// Initialize telemetry client with auth token if provided
|
|
236
250
|
if (authToken) {
|
|
237
251
|
initializeTelemetryClient(authToken);
|
|
@@ -343,15 +357,15 @@ async function runTeammateLoop(data) {
|
|
|
343
357
|
let outerTurn = 0;
|
|
344
358
|
while (true) {
|
|
345
359
|
// Safety: max turns guard
|
|
346
|
-
if (++outerTurn >
|
|
360
|
+
if (++outerTurn > TEAMMATE_MAX_TOTAL_TURNS) {
|
|
347
361
|
logTeammateComplete("max_turns_reached");
|
|
348
|
-
report({ type: "done", teammateId, content: `${teammateName} reached max turns (${
|
|
362
|
+
report({ type: "done", teammateId, content: `${teammateName} reached max turns (${TEAMMATE_MAX_TOTAL_TURNS})`, tokensUsed: { input: totalIn, output: totalOut } });
|
|
349
363
|
break;
|
|
350
364
|
}
|
|
351
365
|
// Safety: elapsed time guard
|
|
352
|
-
if (Date.now() - startTime >
|
|
366
|
+
if (Date.now() - startTime > TEAMMATE_MAX_DURATION_MS) {
|
|
353
367
|
logTeammateComplete("timeout");
|
|
354
|
-
report({ type: "done", teammateId, content: `${teammateName} timed out after ${Math.round(
|
|
368
|
+
report({ type: "done", teammateId, content: `${teammateName} timed out after ${Math.round(TEAMMATE_MAX_DURATION_MS / 60_000)}min`, tokensUsed: { input: totalIn, output: totalOut } });
|
|
355
369
|
break;
|
|
356
370
|
}
|
|
357
371
|
const team = loadTeam(teamId);
|
|
@@ -389,7 +403,7 @@ async function runTeammateLoop(data) {
|
|
|
389
403
|
// Resolve per-task model: task-level → team default
|
|
390
404
|
const taskModel = claimed.model || model;
|
|
391
405
|
currentTaskModelId = MODEL_MAP[taskModel] || MODEL_MAP[model] || MODEL_MAP.opus;
|
|
392
|
-
// Disable thinking for teammates — with
|
|
406
|
+
// Disable thinking for teammates — with TEAMMATE_MAX_OUTPUT_TOKENS=16K, adaptive thinking
|
|
393
407
|
// burns 8-12K tokens on reasoning per turn, leaving only 4-8K for actual code output.
|
|
394
408
|
// This caused catastrophic token bloat (359K input tokens per task) and truncated responses.
|
|
395
409
|
currentTaskThinking = false;
|
|
@@ -407,7 +421,7 @@ async function runTeammateLoop(data) {
|
|
|
407
421
|
const systemPrompt = buildTeammatePrompt(teammateName, freshTeam, freshTask, cwd);
|
|
408
422
|
// Agent loop for current task
|
|
409
423
|
let apiError = null;
|
|
410
|
-
for (let turn = 0; turn <
|
|
424
|
+
for (let turn = 0; turn < TEAMMATE_MAX_TURNS_PER_TASK; turn++) {
|
|
411
425
|
const apiStart = Date.now();
|
|
412
426
|
let response;
|
|
413
427
|
try {
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createClient } from "@supabase/supabase-js";
|
|
|
19
19
|
import { createRequire } from "module";
|
|
20
20
|
import { startUpdateLoop } from "./updater.js";
|
|
21
21
|
import { resolveConfig, loadConfig, updateConfig } from "./cli/services/config-store.js";
|
|
22
|
-
import { getValidToken, getStoresForUser, selectStore } from "./cli/services/auth-service.js";
|
|
22
|
+
import { getValidToken, getStoresForUser, selectStore, SUPABASE_ANON_KEY } from "./cli/services/auth-service.js";
|
|
23
23
|
import { signInWithBrowser } from "./cli/services/browser-auth.js";
|
|
24
24
|
import { LocalAgentConnection } from "./local-agent/connection.js";
|
|
25
25
|
import { initErrorLogger, setErrorLoggerUser, captureError } from "./cli/services/error-logger.js";
|
|
@@ -68,10 +68,21 @@ if (!IS_SERVICE_ROLE && rawConfig.access_token && rawConfig.refresh_token) {
|
|
|
68
68
|
let isAuthenticated = !!(SUPABASE_URL && INITIAL_SUPABASE_KEY);
|
|
69
69
|
let supabase = null;
|
|
70
70
|
if (isAuthenticated) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
if (IS_SERVICE_ROLE) {
|
|
72
|
+
// Service role key — use directly as apiKey (full access)
|
|
73
|
+
supabase = createClient(SUPABASE_URL, INITIAL_SUPABASE_KEY, {
|
|
74
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
75
|
+
});
|
|
76
|
+
if (!USER_ID) {
|
|
77
|
+
console.error("[MCP] WARNING: Using service role key without user login. Tools will work but store scoping is disabled.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// User JWT — use anon key as apiKey, pass JWT via Authorization header
|
|
82
|
+
supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
83
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
84
|
+
global: { headers: { Authorization: `Bearer ${INITIAL_SUPABASE_KEY}` } },
|
|
85
|
+
});
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
else {
|