@onmars/lunar-core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/config-loader.test.ts +136 -0
- package/src/__tests__/router.test.ts +124 -0
- package/src/lib/config-loader.ts +10 -0
- package/src/lib/daemon.ts +4 -1
- package/src/lib/router.ts +15 -2
- package/src/types/moon.ts +3 -0
package/package.json
CHANGED
|
@@ -1510,3 +1510,139 @@ channels:
|
|
|
1510
1510
|
expect(notifications.agentId).toBe('hermes')
|
|
1511
1511
|
})
|
|
1512
1512
|
})
|
|
1513
|
+
|
|
1514
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1515
|
+
// Thinking config (agent-level + channel-level)
|
|
1516
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1517
|
+
|
|
1518
|
+
describe('thinking config', () => {
|
|
1519
|
+
it('parses thinking field from agent entry', () => {
|
|
1520
|
+
const config = loadWorkspaceConfig(
|
|
1521
|
+
setup(`
|
|
1522
|
+
agents:
|
|
1523
|
+
deimos:
|
|
1524
|
+
workspace: "."
|
|
1525
|
+
thinking: high
|
|
1526
|
+
default: true
|
|
1527
|
+
hermes:
|
|
1528
|
+
workspace: "moons/hermes"
|
|
1529
|
+
thinking: off
|
|
1530
|
+
`),
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
expect(config.agents[0].thinking).toBe('high')
|
|
1534
|
+
expect(config.agents[1].thinking).toBe('off')
|
|
1535
|
+
})
|
|
1536
|
+
|
|
1537
|
+
it('agent without thinking field has undefined thinking', () => {
|
|
1538
|
+
const config = loadWorkspaceConfig(
|
|
1539
|
+
setup(`
|
|
1540
|
+
agents:
|
|
1541
|
+
deimos:
|
|
1542
|
+
workspace: "."
|
|
1543
|
+
default: true
|
|
1544
|
+
`),
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
expect(config.agents[0].thinking).toBeUndefined()
|
|
1548
|
+
})
|
|
1549
|
+
|
|
1550
|
+
it('ignores invalid thinking values on agents', () => {
|
|
1551
|
+
const config = loadWorkspaceConfig(
|
|
1552
|
+
setup(`
|
|
1553
|
+
agents:
|
|
1554
|
+
deimos:
|
|
1555
|
+
workspace: "."
|
|
1556
|
+
thinking: extreme
|
|
1557
|
+
default: true
|
|
1558
|
+
`),
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
expect(config.agents[0].thinking).toBeUndefined()
|
|
1562
|
+
})
|
|
1563
|
+
|
|
1564
|
+
it('parses thinking field from channel persona', () => {
|
|
1565
|
+
process.env.T_THINK = 'token'
|
|
1566
|
+
|
|
1567
|
+
const config = loadWorkspaceConfig(
|
|
1568
|
+
setup(`
|
|
1569
|
+
platforms:
|
|
1570
|
+
discord:
|
|
1571
|
+
token: "\${T_THINK}"
|
|
1572
|
+
channels:
|
|
1573
|
+
orbit:
|
|
1574
|
+
id: "111"
|
|
1575
|
+
thinking: medium
|
|
1576
|
+
research:
|
|
1577
|
+
id: "222"
|
|
1578
|
+
thinking: high
|
|
1579
|
+
general:
|
|
1580
|
+
id: "333"
|
|
1581
|
+
`),
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
const orbit = config.channels.find((c) => c.name === 'orbit')!
|
|
1585
|
+
expect(orbit.thinking).toBe('medium')
|
|
1586
|
+
|
|
1587
|
+
const research = config.channels.find((c) => c.name === 'research')!
|
|
1588
|
+
expect(research.thinking).toBe('high')
|
|
1589
|
+
|
|
1590
|
+
const general = config.channels.find((c) => c.name === 'general')!
|
|
1591
|
+
expect(general.thinking).toBeUndefined()
|
|
1592
|
+
|
|
1593
|
+
delete process.env.T_THINK
|
|
1594
|
+
})
|
|
1595
|
+
|
|
1596
|
+
it('ignores invalid thinking values on channels', () => {
|
|
1597
|
+
process.env.T_THINK2 = 'token'
|
|
1598
|
+
|
|
1599
|
+
const config = loadWorkspaceConfig(
|
|
1600
|
+
setup(`
|
|
1601
|
+
platforms:
|
|
1602
|
+
discord:
|
|
1603
|
+
token: "\${T_THINK2}"
|
|
1604
|
+
channels:
|
|
1605
|
+
orbit:
|
|
1606
|
+
id: "111"
|
|
1607
|
+
thinking: turbo
|
|
1608
|
+
`),
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1611
|
+
const orbit = config.channels.find((c) => c.name === 'orbit')!
|
|
1612
|
+
expect(orbit.thinking).toBeUndefined()
|
|
1613
|
+
|
|
1614
|
+
delete process.env.T_THINK2
|
|
1615
|
+
})
|
|
1616
|
+
|
|
1617
|
+
it('accepts all valid thinking levels', () => {
|
|
1618
|
+
process.env.T_THINK3 = 'token'
|
|
1619
|
+
|
|
1620
|
+
const config = loadWorkspaceConfig(
|
|
1621
|
+
setup(`
|
|
1622
|
+
platforms:
|
|
1623
|
+
discord:
|
|
1624
|
+
token: "\${T_THINK3}"
|
|
1625
|
+
channels:
|
|
1626
|
+
a:
|
|
1627
|
+
id: "1"
|
|
1628
|
+
thinking: "off"
|
|
1629
|
+
b:
|
|
1630
|
+
id: "2"
|
|
1631
|
+
thinking: "low"
|
|
1632
|
+
c:
|
|
1633
|
+
id: "3"
|
|
1634
|
+
thinking: "medium"
|
|
1635
|
+
d:
|
|
1636
|
+
id: "4"
|
|
1637
|
+
thinking: "high"
|
|
1638
|
+
`),
|
|
1639
|
+
)
|
|
1640
|
+
|
|
1641
|
+
expect(config.channels.find((c) => c.name === 'a')!.thinking).toBe('off')
|
|
1642
|
+
expect(config.channels.find((c) => c.name === 'b')!.thinking).toBe('low')
|
|
1643
|
+
expect(config.channels.find((c) => c.name === 'c')!.thinking).toBe('medium')
|
|
1644
|
+
expect(config.channels.find((c) => c.name === 'd')!.thinking).toBe('high')
|
|
1645
|
+
|
|
1646
|
+
delete process.env.T_THINK3
|
|
1647
|
+
})
|
|
1648
|
+
})
|
|
@@ -1066,6 +1066,130 @@ describe('Router', () => {
|
|
|
1066
1066
|
})
|
|
1067
1067
|
})
|
|
1068
1068
|
|
|
1069
|
+
// ─── Thinking resolution chain ──────────────────────────────
|
|
1070
|
+
|
|
1071
|
+
describe('thinking resolution chain — session > channel > agent > undefined', () => {
|
|
1072
|
+
it('uses channel-level thinking when no session override', async () => {
|
|
1073
|
+
const spy = createSpyAgent()
|
|
1074
|
+
registry.registerAgent(spy, true)
|
|
1075
|
+
|
|
1076
|
+
const channels: import('../types/moon').ChannelPersona[] = [
|
|
1077
|
+
{ id: 'chan-1', name: 'orbit', platform: 'discord', thinking: 'high' },
|
|
1078
|
+
]
|
|
1079
|
+
const router = new Router(registry, sessions, mockConfig, moonLoader, channels)
|
|
1080
|
+
|
|
1081
|
+
await router.route('discord', msg('Hello'))
|
|
1082
|
+
|
|
1083
|
+
expect(spy.capturedInput.thinking).toBe('high')
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
it('uses agent-level thinking when no channel or session override', async () => {
|
|
1087
|
+
const spy = createSpyAgent()
|
|
1088
|
+
registry.registerAgent(spy, true)
|
|
1089
|
+
|
|
1090
|
+
const channels: import('../types/moon').ChannelPersona[] = [
|
|
1091
|
+
{ id: 'chan-1', name: 'orbit', platform: 'discord' },
|
|
1092
|
+
]
|
|
1093
|
+
const agents = [{ id: 'claude', thinking: 'medium', default: true }]
|
|
1094
|
+
const router = new Router(
|
|
1095
|
+
registry,
|
|
1096
|
+
sessions,
|
|
1097
|
+
mockConfig,
|
|
1098
|
+
moonLoader,
|
|
1099
|
+
channels,
|
|
1100
|
+
undefined,
|
|
1101
|
+
undefined,
|
|
1102
|
+
undefined,
|
|
1103
|
+
undefined,
|
|
1104
|
+
undefined,
|
|
1105
|
+
undefined,
|
|
1106
|
+
undefined,
|
|
1107
|
+
agents as any,
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
await router.route('discord', msg('Hello'))
|
|
1111
|
+
|
|
1112
|
+
expect(spy.capturedInput.thinking).toBe('medium')
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
it('channel thinking takes priority over agent thinking', async () => {
|
|
1116
|
+
const spy = createSpyAgent()
|
|
1117
|
+
registry.registerAgent(spy, true)
|
|
1118
|
+
|
|
1119
|
+
const channels: import('../types/moon').ChannelPersona[] = [
|
|
1120
|
+
{ id: 'chan-1', name: 'orbit', platform: 'discord', thinking: 'low' },
|
|
1121
|
+
]
|
|
1122
|
+
const agents = [{ id: 'claude', thinking: 'high', default: true }]
|
|
1123
|
+
const router = new Router(
|
|
1124
|
+
registry,
|
|
1125
|
+
sessions,
|
|
1126
|
+
mockConfig,
|
|
1127
|
+
moonLoader,
|
|
1128
|
+
channels,
|
|
1129
|
+
undefined,
|
|
1130
|
+
undefined,
|
|
1131
|
+
undefined,
|
|
1132
|
+
undefined,
|
|
1133
|
+
undefined,
|
|
1134
|
+
undefined,
|
|
1135
|
+
undefined,
|
|
1136
|
+
agents as any,
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
await router.route('discord', msg('Hello'))
|
|
1140
|
+
|
|
1141
|
+
expect(spy.capturedInput.thinking).toBe('low')
|
|
1142
|
+
})
|
|
1143
|
+
|
|
1144
|
+
it('session thinking override takes priority over all', async () => {
|
|
1145
|
+
const spy = createSpyAgent()
|
|
1146
|
+
registry.registerAgent(spy, true)
|
|
1147
|
+
|
|
1148
|
+
const channels: import('../types/moon').ChannelPersona[] = [
|
|
1149
|
+
{ id: 'chan-1', name: 'orbit', platform: 'discord', thinking: 'low' },
|
|
1150
|
+
]
|
|
1151
|
+
const agents = [{ id: 'claude', thinking: 'medium', default: true }]
|
|
1152
|
+
const router = new Router(
|
|
1153
|
+
registry,
|
|
1154
|
+
sessions,
|
|
1155
|
+
mockConfig,
|
|
1156
|
+
moonLoader,
|
|
1157
|
+
channels,
|
|
1158
|
+
undefined,
|
|
1159
|
+
undefined,
|
|
1160
|
+
undefined,
|
|
1161
|
+
undefined,
|
|
1162
|
+
undefined,
|
|
1163
|
+
undefined,
|
|
1164
|
+
undefined,
|
|
1165
|
+
agents as any,
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
// Set session-level thinking override (simulates /think command)
|
|
1169
|
+
const sessionKey = 'claude:discord:chan-1'
|
|
1170
|
+
sessions.get(sessionKey) // ensure session exists
|
|
1171
|
+
sessions.updateMetadata(sessionKey, JSON.stringify({ thinkingLevel: 'off' }))
|
|
1172
|
+
|
|
1173
|
+
await router.route('discord', msg('Hello'))
|
|
1174
|
+
|
|
1175
|
+
expect(spy.capturedInput.thinking).toBe('off')
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
it('thinking is undefined when no level set anywhere', async () => {
|
|
1179
|
+
const spy = createSpyAgent()
|
|
1180
|
+
registry.registerAgent(spy, true)
|
|
1181
|
+
|
|
1182
|
+
const channels: import('../types/moon').ChannelPersona[] = [
|
|
1183
|
+
{ id: 'chan-1', name: 'orbit', platform: 'discord' },
|
|
1184
|
+
]
|
|
1185
|
+
const router = new Router(registry, sessions, mockConfig, moonLoader, channels)
|
|
1186
|
+
|
|
1187
|
+
await router.route('discord', msg('Hello'))
|
|
1188
|
+
|
|
1189
|
+
expect(spy.capturedInput.thinking).toBeUndefined()
|
|
1190
|
+
})
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1069
1193
|
// ─── Multi-agent routing (Phase B) ────────────────────────
|
|
1070
1194
|
|
|
1071
1195
|
describe('multi-agent routing — channelPersona.agentId directs to correct agent', () => {
|
package/src/lib/config-loader.ts
CHANGED
|
@@ -260,6 +260,8 @@ export interface AgentEntry {
|
|
|
260
260
|
binary?: string
|
|
261
261
|
/** Whether to mark as default agent (first = default if not specified) */
|
|
262
262
|
default?: boolean
|
|
263
|
+
/** Default thinking level for this agent (off, low, medium, high) */
|
|
264
|
+
thinking?: string
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
/**
|
|
@@ -648,6 +650,10 @@ function parsePlatformChannels(
|
|
|
648
650
|
model: typeof ch.model === 'string' ? ch.model : undefined,
|
|
649
651
|
skills: Array.isArray(ch.skills) ? (ch.skills as unknown[]).map(String) : undefined,
|
|
650
652
|
agentId: typeof ch.agent === 'string' ? ch.agent : undefined,
|
|
653
|
+
thinking:
|
|
654
|
+
typeof ch.thinking === 'string' && ['off', 'low', 'medium', 'high'].includes(ch.thinking)
|
|
655
|
+
? ch.thinking
|
|
656
|
+
: undefined,
|
|
651
657
|
})
|
|
652
658
|
}
|
|
653
659
|
|
|
@@ -1187,6 +1193,9 @@ function parseAgents(raw: unknown): AgentEntry[] {
|
|
|
1187
1193
|
const isDefault = obj.default === true
|
|
1188
1194
|
if (isDefault) hasExplicitDefault = true
|
|
1189
1195
|
|
|
1196
|
+
const thinking = typeof obj.thinking === 'string' ? obj.thinking : undefined
|
|
1197
|
+
const validThinking = thinking && ['off', 'low', 'medium', 'high'].includes(thinking) ? thinking : undefined
|
|
1198
|
+
|
|
1190
1199
|
const entry: AgentEntry = {
|
|
1191
1200
|
id,
|
|
1192
1201
|
workspace: typeof obj.workspace === 'string' ? obj.workspace : undefined,
|
|
@@ -1194,6 +1203,7 @@ function parseAgents(raw: unknown): AgentEntry[] {
|
|
|
1194
1203
|
context1m: obj.context1m === true ? true : undefined,
|
|
1195
1204
|
binary: typeof obj.binary === 'string' ? obj.binary : undefined,
|
|
1196
1205
|
default: isFirst && !hasExplicitDefault ? true : isDefault,
|
|
1206
|
+
thinking: validThinking,
|
|
1197
1207
|
}
|
|
1198
1208
|
|
|
1199
1209
|
agents.push(entry)
|
package/src/lib/daemon.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, resolve } from 'node:path'
|
|
|
3
3
|
import type { MemoryProvider } from '../types/memory'
|
|
4
4
|
import type { ChannelPersona } from '../types/moon'
|
|
5
5
|
import type { LunarConfig } from './config'
|
|
6
|
-
import type { MemoryConfig, RecallConfig, SecurityConfig, VoiceConfig } from './config-loader'
|
|
6
|
+
import type { AgentEntry, MemoryConfig, RecallConfig, SecurityConfig, VoiceConfig } from './config-loader'
|
|
7
7
|
import { log } from './logger'
|
|
8
8
|
import type { MoonLoader } from './moon-loader'
|
|
9
9
|
import type { PluginRegistry } from './plugin'
|
|
@@ -31,6 +31,8 @@ export interface DaemonOptions {
|
|
|
31
31
|
scheduler?: Scheduler
|
|
32
32
|
/** Voice configuration (TTS mode, STT mode) */
|
|
33
33
|
voiceConfig?: VoiceConfig
|
|
34
|
+
/** Agent entries (for agent-level defaults like thinking) */
|
|
35
|
+
agents?: AgentEntry[]
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -61,6 +63,7 @@ export class Daemon {
|
|
|
61
63
|
options.memoryConfig,
|
|
62
64
|
options.scheduler,
|
|
63
65
|
options.voiceConfig,
|
|
66
|
+
options.agents,
|
|
64
67
|
)
|
|
65
68
|
}
|
|
66
69
|
|
package/src/lib/router.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { thinkCommand } from './commands/think'
|
|
|
19
19
|
import { usageCommand } from './commands/usage'
|
|
20
20
|
import { whoamiCommand } from './commands/whoami'
|
|
21
21
|
import type { LunarConfig } from './config'
|
|
22
|
-
import type { MemoryConfig, RecallConfig, SecurityConfig, VoiceConfig } from './config-loader'
|
|
22
|
+
import type { AgentEntry, MemoryConfig, RecallConfig, SecurityConfig, VoiceConfig } from './config-loader'
|
|
23
23
|
import { log } from './logger'
|
|
24
24
|
import type { MemoryOrchestrator } from './memory-orchestrator'
|
|
25
25
|
import type { MoonLoader } from './moon-loader'
|
|
@@ -67,6 +67,9 @@ export class Router {
|
|
|
67
67
|
|
|
68
68
|
private voiceConfig?: VoiceConfig
|
|
69
69
|
|
|
70
|
+
/** Agent-level thinking defaults: agentId → thinking level */
|
|
71
|
+
private agentThinkingMap = new Map<string, string>()
|
|
72
|
+
|
|
70
73
|
/** Slash command handler */
|
|
71
74
|
private commandHandler: CommandHandler
|
|
72
75
|
|
|
@@ -86,6 +89,7 @@ export class Router {
|
|
|
86
89
|
private memoryConfig?: MemoryConfig,
|
|
87
90
|
private scheduler?: Scheduler,
|
|
88
91
|
voiceConfig?: VoiceConfig,
|
|
92
|
+
agents?: AgentEntry[],
|
|
89
93
|
) {
|
|
90
94
|
this.redact = security ? createRedactor(security) : (t: string) => t
|
|
91
95
|
this.sanitize = security
|
|
@@ -101,6 +105,15 @@ export class Router {
|
|
|
101
105
|
this.memoryInstructions = this.loadMemoryInstructions()
|
|
102
106
|
this.voiceConfig = voiceConfig
|
|
103
107
|
|
|
108
|
+
// Build agent-level thinking defaults map
|
|
109
|
+
if (agents) {
|
|
110
|
+
for (const a of agents) {
|
|
111
|
+
if (a.thinking) {
|
|
112
|
+
this.agentThinkingMap.set(a.id, a.thinking)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
// Build index: platform:channelId → ChannelPersona
|
|
105
118
|
// Also index by bare ID for backward compat (single-platform setups)
|
|
106
119
|
this.channelIndex = new Map()
|
|
@@ -333,7 +346,7 @@ export class Router {
|
|
|
333
346
|
systemPrompt,
|
|
334
347
|
model: sessionModelOverride ?? channelPersona?.model ?? moon?.model,
|
|
335
348
|
context1m: moon?.context1m,
|
|
336
|
-
thinking: sessionThinking,
|
|
349
|
+
thinking: sessionThinking ?? channelPersona?.thinking ?? this.agentThinkingMap.get(agent.id),
|
|
337
350
|
metadata: {
|
|
338
351
|
channelId: message.channelId,
|
|
339
352
|
channelName: channelPersona?.name,
|
package/src/types/moon.ts
CHANGED
|
@@ -53,4 +53,7 @@ export interface ChannelPersona {
|
|
|
53
53
|
/** Which agent handles this channel. Must match a registered agent ID.
|
|
54
54
|
* Omit = use default agent (first registered or marked as default). */
|
|
55
55
|
agentId?: string
|
|
56
|
+
/** Per-channel thinking level override (off, low, medium, high).
|
|
57
|
+
* Takes priority over agent-level thinking. */
|
|
58
|
+
thinking?: string
|
|
56
59
|
}
|