Skip to content

Commit 5e0e782

Browse files
Aegisclaude
authored andcommitted
fix: setOperatorConfig + migrate reflection/operator-log off raw Anthropic (#412)
- Add setOperatorConfig() to operator module so consumers can override core defaults (fixes example.com email domain in digest) - createAegisApp() now calls setOperatorConfig(config.operator) at startup - Export setOperatorConfig from public API - Migrate operator log and memory reflection from raw Anthropic fetch() to askWorkersAiOrGroq() (Workers AI free → Groq fallback) - Errors now propagate to task runner instead of being silently swallowed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cee6e4b commit 5e0e782

4 files changed

Lines changed: 100 additions & 145 deletions

File tree

web/src/core.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { Env } from './types.js';
1313
import type { EdgeEnv } from './kernel/dispatch.js';
1414
import type { KernelIntent, DispatchResult, Executor, CognitiveState } from './kernel/types.js';
1515
import type { OperatorConfig, Product, SelfModel } from './operator/types.js';
16+
import { setOperatorConfig } from './operator/index.js';
1617
import { bearerAuth } from './auth.js';
1718
import { runScheduledTasks } from './kernel/scheduled/index.js';
1819

@@ -186,6 +187,11 @@ export interface AegisApp {
186187
* ```
187188
*/
188189
export function createAegisApp(config: AegisAppConfig): AegisApp {
190+
// ── Operator config override ──
191+
// Must happen before anything else — core modules (email, dispatch, MCP tools)
192+
// import operatorConfig at the module level, so this sets the live binding.
193+
setOperatorConfig(config.operator);
194+
189195
// ── Version override ──
190196
if (config.version) setAppVersion(config.version);
191197

web/src/exports.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export {
135135
export { runScheduledTasks } from './kernel/scheduled/index.js';
136136

137137
// ─── Operator ───────────────────────────────────────────────
138-
export { operatorConfig, renderTemplate } from './operator/index.js';
138+
export { operatorConfig, setOperatorConfig, renderTemplate } from './operator/index.js';
139139

140140
// ─── Auth ───────────────────────────────────────────────────
141141
export { bearerAuth } from './auth.js';

web/src/kernel/scheduled/reflection.ts

Lines changed: 47 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type EdgeEnv } from '../dispatch.js';
22
import { getAllMemoryForReflection } from '../memory-adapter.js';
3+
import { askWorkersAiOrGroq } from './dreaming/llm.js';
34
// Standalone emails removed — all content routes through daily digest
45

56
// ─── Memory Reflection (#introspection) ───────────────────────
@@ -84,67 +85,34 @@ export async function runMemoryReflectionCycle(env: EdgeEnv): Promise<void> {
8485

8586
const userPrompt = `Here is my complete active memory — ${entries.length} entries across ${byTopic.size} topics:\n\n${topicSections}\n\nReflect.`;
8687

87-
// Call Claude directly (no tools needed, pure generation)
88-
const anthropicBase = env.anthropicBaseUrl || 'https://api.anthropic.com';
89-
90-
try {
91-
const response = await fetch(`${anthropicBase}/v1/messages`, {
92-
method: 'POST',
93-
headers: {
94-
'Content-Type': 'application/json',
95-
'x-api-key': env.anthropicApiKey,
96-
'anthropic-version': '2023-06-01',
97-
},
98-
body: JSON.stringify({
99-
model: env.claudeModel,
100-
max_tokens: 2048,
101-
system: REFLECTION_SYSTEM,
102-
messages: [{ role: 'user', content: userPrompt }],
103-
}),
104-
});
105-
106-
if (!response.ok) {
107-
const errText = await response.text();
108-
throw new Error(`Anthropic API error ${response.status}: ${errText}`);
109-
}
110-
111-
const data = await response.json<{
112-
content: Array<{ type: string; text?: string }>;
113-
usage: { input_tokens: number; output_tokens: number };
114-
}>();
115-
116-
const reflection = data.content.filter(b => b.type === 'text').map(b => b.text ?? '').join('');
117-
if (!reflection) throw new Error('Empty reflection response');
118-
119-
// Cost calculation (Sonnet rates)
120-
const cost = (data.usage.input_tokens * 3 + data.usage.output_tokens * 15) / 1_000_000;
121-
const topics = [...byTopic.keys()];
122-
123-
// Store in D1
124-
await env.db.prepare(
125-
'INSERT INTO reflections (content, memory_count, topics_covered, cost) VALUES (?, ?, ?, ?)'
126-
).bind(reflection, entries.length, JSON.stringify(topics), cost).run();
127-
128-
// Queue reflection for daily digest instead of standalone email
129-
const reflectionPayload = JSON.stringify({
130-
reflection: reflection.slice(0, 2000),
131-
memoryCount: entries.length,
132-
topics,
133-
timestamp: new Date().toISOString(),
134-
});
135-
await env.db.prepare(
136-
"INSERT INTO digest_sections (section, payload) VALUES ('memory_reflection', ?)"
137-
).bind(reflectionPayload).run();
138-
139-
// Record timestamp
140-
await env.db.prepare(
141-
"INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_memory_reflection', datetime('now'))"
142-
).run();
143-
144-
console.log(`[reflection] Weekly memory reflection complete — ${entries.length} memories, ${byTopic.size} topics, $${cost.toFixed(4)}`);
145-
} catch (err) {
146-
console.error('[reflection] Memory reflection failed:', err instanceof Error ? err.message : String(err));
147-
}
88+
// Route through Workers AI (free) ��� Groq fallback — no raw Anthropic calls (#412)
89+
const reflection = await askWorkersAiOrGroq(env, REFLECTION_SYSTEM, userPrompt);
90+
if (!reflection) throw new Error('Empty reflection response');
91+
92+
const topics = [...byTopic.keys()];
93+
94+
// Store in D1 (cost ≈ 0 for Workers AI, minimal for Groq fallback)
95+
await env.db.prepare(
96+
'INSERT INTO reflections (content, memory_count, topics_covered, cost) VALUES (?, ?, ?, ?)'
97+
).bind(reflection, entries.length, JSON.stringify(topics), 0).run();
98+
99+
// Queue reflection for daily digest instead of standalone email
100+
const reflectionPayload = JSON.stringify({
101+
reflection: reflection.slice(0, 2000),
102+
memoryCount: entries.length,
103+
topics,
104+
timestamp: new Date().toISOString(),
105+
});
106+
await env.db.prepare(
107+
"INSERT INTO digest_sections (section, payload) VALUES ('memory_reflection', ?)"
108+
).bind(reflectionPayload).run();
109+
110+
// Record timestamp
111+
await env.db.prepare(
112+
"INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_memory_reflection', datetime('now'))"
113+
).run();
114+
115+
console.log(`[reflection] Weekly memory reflection complete — ${entries.length} memories, ${byTopic.size} topics`);
148116
}
149117

150118
// ─── Operator's Log (#introspection) ──────────────────────────
@@ -384,54 +352,22 @@ ${goalSummary}
384352
385353
Write your worklog entry.`;
386354

387-
const anthropicBase = env.anthropicBaseUrl || 'https://api.anthropic.com';
388-
389-
try {
390-
const response = await fetch(`${anthropicBase}/v1/messages`, {
391-
method: 'POST',
392-
headers: {
393-
'Content-Type': 'application/json',
394-
'x-api-key': env.anthropicApiKey,
395-
'anthropic-version': '2023-06-01',
396-
},
397-
body: JSON.stringify({
398-
model: env.claudeModel,
399-
max_tokens: 1024,
400-
system: OPERATOR_LOG_SYSTEM,
401-
messages: [{ role: 'user', content: userPrompt }],
402-
}),
403-
});
404-
405-
if (!response.ok) {
406-
const errText = await response.text();
407-
throw new Error(`Anthropic API error ${response.status}: ${errText}`);
408-
}
409-
410-
const data = await response.json<{
411-
content: Array<{ type: string; text?: string }>;
412-
usage: { input_tokens: number; output_tokens: number };
413-
}>();
414-
415-
const logEntry = data.content.filter(b => b.type === 'text').map(b => b.text ?? '').join('');
416-
if (!logEntry) throw new Error('Empty log response');
417-
418-
const cost = (data.usage.input_tokens * 3 + data.usage.output_tokens * 15) / 1_000_000;
419-
420-
// Store in D1
421-
await env.db.prepare(
422-
'INSERT INTO operator_log (content, episodes_count, goals_run, tasks_completed, tasks_failed, prs_created, total_cost, cost) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
423-
).bind(logEntry, activity.episodes.length, activity.goalActions.length, activity.tasksCompleted.length, activity.tasksFailed.length, prsCreated, activity.totalCost, cost).run();
424-
425-
// Operator log is consumed by the daily digest (reads operator_log table).
426-
// No standalone email — consolidated into the single daily digest.
427-
428-
// Record timestamp
429-
await env.db.prepare(
430-
"INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_operator_log', datetime('now'))"
431-
).run();
432-
433-
console.log(`[operator-log] Nightly log complete — ${activity.episodes.length} episodes, ${activity.goalActions.length} goals, $${cost.toFixed(4)}`);
434-
} catch (err) {
435-
console.error('[operator-log] Failed:', err instanceof Error ? err.message : String(err));
436-
}
355+
// Route through Workers AI (free) → Groq fallback — no raw Anthropic calls (#412)
356+
const logEntry = await askWorkersAiOrGroq(env, OPERATOR_LOG_SYSTEM, userPrompt);
357+
if (!logEntry) throw new Error('Empty log response');
358+
359+
// Store in D1 (cost ≈ 0 for Workers AI, minimal for Groq fallback)
360+
await env.db.prepare(
361+
'INSERT INTO operator_log (content, episodes_count, goals_run, tasks_completed, tasks_failed, prs_created, total_cost, cost) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
362+
).bind(logEntry, activity.episodes.length, activity.goalActions.length, activity.tasksCompleted.length, activity.tasksFailed.length, prsCreated, activity.totalCost, 0).run();
363+
364+
// Operator log is consumed by the daily digest (reads operator_log table).
365+
// No standalone email — consolidated into the single daily digest.
366+
367+
// Record timestamp
368+
await env.db.prepare(
369+
"INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_operator_log', datetime('now'))"
370+
).run();
371+
372+
console.log(`[operator-log] Nightly log complete — ${activity.episodes.length} episodes, ${activity.goalActions.length} goals`);
437373
}

web/src/operator/index.ts

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,46 @@
1-
import type { OperatorConfig } from './types.js';
2-
import raw from './config.js';
3-
4-
// ─── Validation ───────────────────────────────────────────────
5-
6-
function validate(cfg: OperatorConfig): OperatorConfig {
7-
if (!cfg.identity?.name) throw new Error('operator config: identity.name is required');
8-
if (!cfg.persona?.tagline) throw new Error('operator config: persona.tagline is required');
9-
if (!cfg.persona?.traits?.length) throw new Error('operator config: persona.traits must have at least one entry');
10-
11-
// Auto-derive possessive if not provided
12-
if (!cfg.identity.possessive) {
13-
cfg.identity.possessive = cfg.identity.name.endsWith('s')
14-
? `${cfg.identity.name}'`
15-
: `${cfg.identity.name}'s`;
16-
}
17-
18-
return cfg;
19-
}
20-
21-
// ─── Frozen export ────────────────────────────────────────────
22-
23-
export const operatorConfig: Readonly<OperatorConfig> = Object.freeze(validate({ ...raw }));
24-
25-
// ─── Template helper ──────────────────────────────────────────
26-
27-
export function renderTemplate(template: string): string {
28-
return template
29-
.replace(/\{name\}/g, operatorConfig.identity.name)
30-
.replace(/\{possessive\}/g, operatorConfig.identity.possessive!);
31-
}
32-
33-
export type { OperatorConfig };
1+
import type { OperatorConfig } from './types.js';
2+
import raw from './config.js';
3+
4+
// ─── Validation ───────────────────────────────────────────────
5+
6+
function validate(cfg: OperatorConfig): OperatorConfig {
7+
if (!cfg.identity?.name) throw new Error('operator config: identity.name is required');
8+
if (!cfg.persona?.tagline) throw new Error('operator config: persona.tagline is required');
9+
if (!cfg.persona?.traits?.length) throw new Error('operator config: persona.traits must have at least one entry');
10+
11+
// Auto-derive possessive if not provided
12+
if (!cfg.identity.possessive) {
13+
cfg.identity.possessive = cfg.identity.name.endsWith('s')
14+
? `${cfg.identity.name}'`
15+
: `${cfg.identity.name}'s`;
16+
}
17+
18+
return cfg;
19+
}
20+
21+
// ─── Mutable config singleton ─────────────────────────────────
22+
// Starts with the core default config. Consumers override via
23+
// setOperatorConfig() — called by createAegisApp() — so that all
24+
// internal modules (email, dispatch, MCP tools) pick up the
25+
// consumer's real addresses instead of the core's example.com defaults.
26+
27+
// eslint-disable-next-line import/no-mutable-exports
28+
export let operatorConfig: Readonly<OperatorConfig> = Object.freeze(validate({ ...raw }));
29+
30+
/**
31+
* Override the operator config at app startup.
32+
* Must be called before any scheduled tasks or route handlers run.
33+
*/
34+
export function setOperatorConfig(cfg: OperatorConfig): void {
35+
operatorConfig = Object.freeze(validate({ ...cfg }));
36+
}
37+
38+
// ─── Template helper ──────────────────────────────────────────
39+
40+
export function renderTemplate(template: string): string {
41+
return template
42+
.replace(/\{name\}/g, operatorConfig.identity.name)
43+
.replace(/\{possessive\}/g, operatorConfig.identity.possessive!);
44+
}
45+
46+
export type { OperatorConfig };

0 commit comments

Comments
 (0)