@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onmars/lunar-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -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', () => {
@@ -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
  }