@roj-ai/sdk 0.1.33 → 0.1.35
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/bootstrap.d.ts +39 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +5 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/core/agents/agent.d.ts +11 -0
- package/dist/core/agents/agent.d.ts.map +1 -1
- package/dist/core/agents/agent.js +19 -2
- package/dist/core/agents/agent.js.map +1 -1
- package/dist/core/preset/index.d.ts +1 -0
- package/dist/core/preset/index.d.ts.map +1 -1
- package/dist/core/preset/index.js +2 -0
- package/dist/core/preset/index.js.map +1 -1
- package/dist/core/preset/overrides.d.ts +14 -0
- package/dist/core/preset/overrides.d.ts.map +1 -0
- package/dist/core/preset/overrides.js +20 -0
- package/dist/core/preset/overrides.js.map +1 -0
- package/dist/core/sessions/session-manager.d.ts +5 -0
- package/dist/core/sessions/session-manager.d.ts.map +1 -1
- package/dist/core/sessions/session-manager.js +20 -6
- package/dist/core/sessions/session-manager.js.map +1 -1
- package/dist/core/sessions/session-overrides.integration.test.d.ts +2 -0
- package/dist/core/sessions/session-overrides.integration.test.d.ts.map +1 -0
- package/dist/core/sessions/session-overrides.integration.test.js +154 -0
- package/dist/core/sessions/session-overrides.integration.test.js.map +1 -0
- package/dist/core/sessions/session.d.ts +10 -1
- package/dist/core/sessions/session.d.ts.map +1 -1
- package/dist/core/sessions/session.js +15 -0
- package/dist/core/sessions/session.js.map +1 -1
- package/dist/core/sessions/state.d.ts +57 -0
- package/dist/core/sessions/state.d.ts.map +1 -1
- package/dist/core/sessions/state.js +48 -0
- package/dist/core/sessions/state.js.map +1 -1
- package/dist/core/system.d.ts +2 -0
- package/dist/core/system.d.ts.map +1 -1
- package/dist/core/system.js +2 -1
- package/dist/core/system.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/services/pid-registry.d.ts +72 -0
- package/dist/plugins/services/pid-registry.d.ts.map +1 -0
- package/dist/plugins/services/pid-registry.js +171 -0
- package/dist/plugins/services/pid-registry.js.map +1 -0
- package/dist/plugins/services/pid-registry.test.d.ts +2 -0
- package/dist/plugins/services/pid-registry.test.d.ts.map +1 -0
- package/dist/plugins/services/pid-registry.test.js +161 -0
- package/dist/plugins/services/pid-registry.test.js.map +1 -0
- package/dist/plugins/services/plugin.d.ts +6 -0
- package/dist/plugins/services/plugin.d.ts.map +1 -1
- package/dist/plugins/services/plugin.js +55 -15
- package/dist/plugins/services/plugin.js.map +1 -1
- package/dist/plugins/services/prompt.d.ts.map +1 -1
- package/dist/plugins/services/prompt.js +8 -0
- package/dist/plugins/services/prompt.js.map +1 -1
- package/dist/plugins/services/schema.d.ts +33 -0
- package/dist/plugins/services/schema.d.ts.map +1 -1
- package/dist/plugins/services/service.d.ts +24 -0
- package/dist/plugins/services/service.d.ts.map +1 -1
- package/dist/plugins/services/service.js +96 -1
- package/dist/plugins/services/service.js.map +1 -1
- package/dist/plugins/services/services.integration.test.js +254 -0
- package/dist/plugins/services/services.integration.test.js.map +1 -1
- package/dist/plugins/session-lifecycle/plugin.d.ts +36 -0
- package/dist/plugins/session-lifecycle/plugin.d.ts.map +1 -1
- package/dist/plugins/session-lifecycle/plugin.js +53 -1
- package/dist/plugins/session-lifecycle/plugin.js.map +1 -1
- package/dist/testing/test-harness.d.ts +12 -2
- package/dist/testing/test-harness.d.ts.map +1 -1
- package/dist/testing/test-harness.js +13 -2
- package/dist/testing/test-harness.js.map +1 -1
- package/dist/transport/http/middleware/error-handler.d.ts +1 -1
- package/package.json +2 -2
- package/src/bootstrap.ts +7 -0
- package/src/core/agents/agent.ts +19 -2
- package/src/core/preset/index.ts +3 -0
- package/src/core/preset/overrides.ts +24 -0
- package/src/core/sessions/session-manager.ts +27 -4
- package/src/core/sessions/session-overrides.integration.test.ts +210 -0
- package/src/core/sessions/session.ts +19 -1
- package/src/core/sessions/state.ts +77 -0
- package/src/core/system.ts +4 -0
- package/src/index.ts +8 -1
- package/src/plugins/services/pid-registry.test.ts +180 -0
- package/src/plugins/services/pid-registry.ts +191 -0
- package/src/plugins/services/plugin.ts +58 -15
- package/src/plugins/services/prompt.ts +8 -0
- package/src/plugins/services/schema.ts +33 -0
- package/src/plugins/services/service.ts +125 -1
- package/src/plugins/services/services.integration.test.ts +270 -0
- package/src/plugins/session-lifecycle/plugin.ts +59 -1
- package/src/testing/test-harness.ts +22 -3
package/src/core/agents/agent.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type { Logger } from '../../lib/logger/logger.js'
|
|
|
51
51
|
import type { SessionContext } from '../sessions/context.js'
|
|
52
52
|
import type { SessionStore } from '../sessions/session-store.js'
|
|
53
53
|
import type { SessionState } from '../sessions/state.js'
|
|
54
|
+
import { resolveAgentOverrides } from '../sessions/state.js'
|
|
54
55
|
import type { SessionEnvironment, ToolExecutor } from '../tools/index.js'
|
|
55
56
|
import type { AgentContext } from './context.js'
|
|
56
57
|
import { sanitizeLLMResponse } from './response-sanitizer.js'
|
|
@@ -200,6 +201,22 @@ export class Agent {
|
|
|
200
201
|
return this.store.getAgentState(this.id)
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Model for the next inference: the session override if one is set, else the
|
|
206
|
+
* model from the preset definition.
|
|
207
|
+
*
|
|
208
|
+
* Resolved per inference rather than frozen at construction so that changing
|
|
209
|
+
* the override applies to agents that are already running. Applying it in
|
|
210
|
+
* `Session.getAgentConfig()` instead would only reach agents created after the
|
|
211
|
+
* change, and catching up the rest would mean rebuilding live Agent instances
|
|
212
|
+
* mid-turn — abort controllers, debounce timers and mailbox state included.
|
|
213
|
+
*/
|
|
214
|
+
private resolveModel(): ModelId {
|
|
215
|
+
const definitionName = this.state?.definitionName
|
|
216
|
+
if (definitionName === undefined) return this.config.model
|
|
217
|
+
return resolveAgentOverrides(this.store.getState(), definitionName).model ?? this.config.model
|
|
218
|
+
}
|
|
219
|
+
|
|
203
220
|
// ============================================================================
|
|
204
221
|
// Public API
|
|
205
222
|
// ============================================================================
|
|
@@ -449,7 +466,7 @@ export class Agent {
|
|
|
449
466
|
const cachedMessages = applyCacheBreakpoint(messages, extraMessages.length, this.config.cacheTtl, agentState.preamble.length)
|
|
450
467
|
|
|
451
468
|
const request: InferenceRequest = {
|
|
452
|
-
model: this.
|
|
469
|
+
model: this.resolveModel(),
|
|
453
470
|
systemPrompt: this.buildSystemPrompt(),
|
|
454
471
|
messages: cachedMessages,
|
|
455
472
|
tools: this.tools.size > 0 ? [...this.tools.values()] : undefined,
|
|
@@ -628,7 +645,7 @@ export class Agent {
|
|
|
628
645
|
|
|
629
646
|
// 5. LLM inference (with retry)
|
|
630
647
|
const request: InferenceRequest = {
|
|
631
|
-
model: this.
|
|
648
|
+
model: this.resolveModel(),
|
|
632
649
|
systemPrompt: this.buildSystemPrompt(),
|
|
633
650
|
messages: cachedMessages,
|
|
634
651
|
tools: this.tools.size > 0 ? [...this.tools.values()] : undefined,
|
package/src/core/preset/index.ts
CHANGED
|
@@ -9,3 +9,6 @@ export { createPreset, validatePreset } from './config.js'
|
|
|
9
9
|
// Re-export from preset-builder
|
|
10
10
|
export type { CreateOrchestratorInput, DefineAgentInput } from './preset-builder.js'
|
|
11
11
|
export { createOrchestrator, defineAgent } from './preset-builder.js'
|
|
12
|
+
|
|
13
|
+
// Re-export from overrides
|
|
14
|
+
export { knownDefinitionNames, unknownOverrideTargets } from './overrides.js'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation of session overrides against the preset they target.
|
|
3
|
+
*
|
|
4
|
+
* Overrides are keyed by agent definition name, which makes a typo silent: the
|
|
5
|
+
* entry simply never matches and the agent quietly keeps the preset's model.
|
|
6
|
+
* Every entry point that accepts an override patch rejects unknown names instead.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { COMMUNICATOR_ROLE, ORCHESTRATOR_ROLE } from '~/core/agents/agent-roles.js'
|
|
10
|
+
import type { SessionOverridesPatch } from '~/core/sessions/state.js'
|
|
11
|
+
import type { PresetDefinition } from './config.js'
|
|
12
|
+
|
|
13
|
+
/** Every definition name an override may target, in preset order. */
|
|
14
|
+
export const knownDefinitionNames = (preset: PresetDefinition): string[] => [
|
|
15
|
+
ORCHESTRATOR_ROLE,
|
|
16
|
+
...(preset.communicator ? [COMMUNICATOR_ROLE] : []),
|
|
17
|
+
...preset.agents.map(agent => agent.name),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/** Names in the patch that the preset does not define. Empty when the patch is valid. */
|
|
21
|
+
export const unknownOverrideTargets = (preset: PresetDefinition, patch: SessionOverridesPatch): string[] => {
|
|
22
|
+
const known = new Set(knownDefinitionNames(preset))
|
|
23
|
+
return Object.keys(patch.agents ?? {}).filter(name => !known.has(name))
|
|
24
|
+
}
|
|
@@ -21,9 +21,10 @@ import type { LLMLogger } from '~/core/llm/logger.js'
|
|
|
21
21
|
import type { LLMProvider } from '~/core/llm/provider.js'
|
|
22
22
|
import type { CallerContext, ConfiguredPlugin, ManagerMethodContext, PluginDefinition } from '~/core/plugins/plugin-builder.js'
|
|
23
23
|
import type { Preset } from '~/core/preset/index.js'
|
|
24
|
+
import { knownDefinitionNames, unknownOverrideTargets } from '~/core/preset/overrides.js'
|
|
24
25
|
import type { SessionId } from '~/core/sessions/schema.js'
|
|
25
26
|
import { generateSessionId } from '~/core/sessions/schema.js'
|
|
26
|
-
import type { SessionCreatedEvent } from '~/core/sessions/state.js'
|
|
27
|
+
import type { SessionCreatedEvent, SessionOverridesPatch } from '~/core/sessions/state.js'
|
|
27
28
|
import { checkRecoveryNeeded, isSessionCreatedEvent, reconstructSessionState, sessionEvents } from '~/core/sessions/state.js'
|
|
28
29
|
import type { ToolExecutor } from '~/core/tools'
|
|
29
30
|
import { FileLogger } from '~/lib/logger/file.js'
|
|
@@ -34,6 +35,7 @@ import type { Result } from '~/lib/utils/result.js'
|
|
|
34
35
|
import { Err, Ok } from '~/lib/utils/result.js'
|
|
35
36
|
import type { SpawnableAgentInfo } from '~/plugins/agents/index.js'
|
|
36
37
|
import { getAgentUnconsumedMailbox, selectMailboxState } from '~/plugins/mailbox/query.js'
|
|
38
|
+
import type { ServicePidRegistry } from '~/plugins/services/pid-registry.js'
|
|
37
39
|
import type { PortPool } from '~/plugins/services/port-pool.js'
|
|
38
40
|
import type { ServiceConfig } from '~/plugins/services/schema.js'
|
|
39
41
|
import { selectSessionStats, type SessionStatsState } from '~/plugins/session-stats/index.js'
|
|
@@ -77,6 +79,7 @@ export interface SessionManagerOptions {
|
|
|
77
79
|
preprocessorRegistry?: PreprocessorRegistry
|
|
78
80
|
llmLogger?: LLMLogger
|
|
79
81
|
portPool?: PortPool
|
|
82
|
+
pidRegistry?: ServicePidRegistry
|
|
80
83
|
/** Host-environment adapters (filesystem, process). */
|
|
81
84
|
platform: Platform
|
|
82
85
|
systemPlugins?: readonly PluginDefinition<string, any, any, any, any>[]
|
|
@@ -108,6 +111,7 @@ export class SessionManager {
|
|
|
108
111
|
private readonly preprocessorRegistry?: PreprocessorRegistry
|
|
109
112
|
private readonly llmLogger?: LLMLogger
|
|
110
113
|
private readonly portPool?: PortPool
|
|
114
|
+
private readonly pidRegistry?: ServicePidRegistry
|
|
111
115
|
private readonly platform: Platform
|
|
112
116
|
private readonly systemPlugins: readonly PluginDefinition<string, any, any, any, any>[]
|
|
113
117
|
|
|
@@ -124,6 +128,7 @@ export class SessionManager {
|
|
|
124
128
|
this.preprocessorRegistry = options.preprocessorRegistry
|
|
125
129
|
this.llmLogger = options.llmLogger
|
|
126
130
|
this.portPool = options.portPool
|
|
131
|
+
this.pidRegistry = options.pidRegistry
|
|
127
132
|
this.platform = options.platform
|
|
128
133
|
this.systemPlugins = options.systemPlugins ?? []
|
|
129
134
|
this.managerMethods = this.collectManagerMethods()
|
|
@@ -139,13 +144,22 @@ export class SessionManager {
|
|
|
139
144
|
*/
|
|
140
145
|
async createSession(
|
|
141
146
|
presetId: string,
|
|
142
|
-
options?: { workspaceDir?: string; sessionId?: string },
|
|
147
|
+
options?: { workspaceDir?: string; sessionId?: string; overrides?: SessionOverridesPatch },
|
|
143
148
|
): Promise<Result<Session, DomainError>> {
|
|
144
149
|
const preset = this.presets.get(presetId)
|
|
145
150
|
if (!preset) {
|
|
146
151
|
return Err(PresetErrors.notFound(presetId))
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
if (options?.overrides) {
|
|
155
|
+
const unknown = unknownOverrideTargets(preset, options.overrides)
|
|
156
|
+
if (unknown.length > 0) {
|
|
157
|
+
return Err(ValidationErrors.invalid(
|
|
158
|
+
`Unknown agent definitions in overrides: ${unknown.join(', ')}. Preset '${presetId}' defines: ${knownDefinitionNames(preset).join(', ')}`,
|
|
159
|
+
))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
149
163
|
const sessionId = options?.sessionId ? (options.sessionId as any) : generateSessionId()
|
|
150
164
|
// First orchestrator gets seq 1
|
|
151
165
|
const orchestratorId = generateAgentId(ORCHESTRATOR_ROLE, 1)
|
|
@@ -171,6 +185,15 @@ export class SessionManager {
|
|
|
171
185
|
...(workspaceDir ? { workspaceDir } : {}),
|
|
172
186
|
}),
|
|
173
187
|
),
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
// Seeded through the same event a runtime change uses, so there is one code
|
|
191
|
+
// path and a forked/replayed session reconstructs the overrides for free.
|
|
192
|
+
if (options?.overrides) {
|
|
193
|
+
events.push(withSessionId(sessionId, sessionEvents.create('session_overrides_set', options.overrides)))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
events.push(
|
|
174
197
|
withSessionId(
|
|
175
198
|
sessionId,
|
|
176
199
|
agentEvents.create('agent_spawned', {
|
|
@@ -179,7 +202,7 @@ export class SessionManager {
|
|
|
179
202
|
parentId: null,
|
|
180
203
|
}),
|
|
181
204
|
),
|
|
182
|
-
|
|
205
|
+
)
|
|
183
206
|
|
|
184
207
|
// Spawn communicator if configured
|
|
185
208
|
const hasCommunicator = !!preset.communicator
|
|
@@ -774,7 +797,7 @@ export class SessionManager {
|
|
|
774
797
|
}
|
|
775
798
|
}
|
|
776
799
|
if (servicesByType.size > 0 && this.portPool) {
|
|
777
|
-
configs.set('services', { services: [...servicesByType.values()], portPool: this.portPool })
|
|
800
|
+
configs.set('services', { services: [...servicesByType.values()], portPool: this.portPool, pidRegistry: this.pidRegistry })
|
|
778
801
|
}
|
|
779
802
|
|
|
780
803
|
return configs
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { ORCHESTRATOR_ROLE } from '~/core/agents/agent-roles.js'
|
|
3
|
+
import { MockLLMProvider } from '~/core/llm/mock.js'
|
|
4
|
+
import { ModelId } from '~/core/llm/schema.js'
|
|
5
|
+
import { createTestPreset, TestHarness } from '~/testing/index.js'
|
|
6
|
+
|
|
7
|
+
const LUNA = ModelId('openai/gpt-5.6-luna')
|
|
8
|
+
const KIMI = ModelId('moonshotai/kimi-k3')
|
|
9
|
+
|
|
10
|
+
/** Models the mock provider was actually asked for, in call order. */
|
|
11
|
+
const modelsUsed = (llmProvider: MockLLMProvider): string[] => llmProvider.getCallHistory().map(request => String(request.model))
|
|
12
|
+
|
|
13
|
+
const newHarness = () =>
|
|
14
|
+
new TestHarness({
|
|
15
|
+
presets: [createTestPreset({ agents: [{ name: 'worker', system: 'worker' }] })],
|
|
16
|
+
llmProvider: MockLLMProvider.withFixedResponse({ content: 'ok', toolCalls: [] }),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('session overrides', () => {
|
|
20
|
+
it('uses the preset model when no override is set', async () => {
|
|
21
|
+
const harness = newHarness()
|
|
22
|
+
const session = await harness.createSession('test')
|
|
23
|
+
|
|
24
|
+
await session.sendAndWaitForIdle('hello')
|
|
25
|
+
|
|
26
|
+
expect(modelsUsed(harness.llmProvider)).toEqual(['mock'])
|
|
27
|
+
|
|
28
|
+
await harness.shutdown()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('applies an override seeded at session creation', async () => {
|
|
32
|
+
const harness = newHarness()
|
|
33
|
+
const session = await harness.createSession('test', {
|
|
34
|
+
overrides: { agents: { [ORCHESTRATOR_ROLE]: { model: LUNA } } },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await session.sendAndWaitForIdle('hello')
|
|
38
|
+
|
|
39
|
+
expect(modelsUsed(harness.llmProvider)).toEqual([String(LUNA)])
|
|
40
|
+
|
|
41
|
+
await harness.shutdown()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// The point of resolving the model per inference instead of freezing it on the
|
|
45
|
+
// Agent at construction: this agent is already built and has already run a turn.
|
|
46
|
+
it('applies an override set at runtime to an already-running agent', async () => {
|
|
47
|
+
const harness = newHarness()
|
|
48
|
+
const session = await harness.createSession('test')
|
|
49
|
+
|
|
50
|
+
await session.sendAndWaitForIdle('before')
|
|
51
|
+
await session.setOverrides({ agents: { [ORCHESTRATOR_ROLE]: { model: LUNA } } })
|
|
52
|
+
await session.sendAndWaitForIdle('after')
|
|
53
|
+
|
|
54
|
+
expect(modelsUsed(harness.llmProvider)).toEqual(['mock', String(LUNA)])
|
|
55
|
+
|
|
56
|
+
await harness.shutdown()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('reverts to the preset model when an override is cleared with null', async () => {
|
|
60
|
+
const harness = newHarness()
|
|
61
|
+
const session = await harness.createSession('test', {
|
|
62
|
+
overrides: { agents: { [ORCHESTRATOR_ROLE]: { model: LUNA } } },
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
await session.sendAndWaitForIdle('before')
|
|
66
|
+
await session.setOverrides({ agents: { [ORCHESTRATOR_ROLE]: null } })
|
|
67
|
+
await session.sendAndWaitForIdle('after')
|
|
68
|
+
|
|
69
|
+
expect(modelsUsed(harness.llmProvider)).toEqual([String(LUNA), 'mock'])
|
|
70
|
+
|
|
71
|
+
await harness.shutdown()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('falls back to defaults for an agent with no entry of its own', async () => {
|
|
75
|
+
const harness = newHarness()
|
|
76
|
+
const session = await harness.createSession('test', { overrides: { defaults: { model: KIMI } } })
|
|
77
|
+
|
|
78
|
+
await session.sendAndWaitForIdle('hello')
|
|
79
|
+
|
|
80
|
+
expect(modelsUsed(harness.llmProvider)).toEqual([String(KIMI)])
|
|
81
|
+
|
|
82
|
+
await harness.shutdown()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('lets a per-agent entry win over defaults', async () => {
|
|
86
|
+
const harness = newHarness()
|
|
87
|
+
const session = await harness.createSession('test', {
|
|
88
|
+
overrides: { defaults: { model: KIMI }, agents: { [ORCHESTRATOR_ROLE]: { model: LUNA } } },
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
await session.sendAndWaitForIdle('hello')
|
|
92
|
+
|
|
93
|
+
expect(modelsUsed(harness.llmProvider)).toEqual([String(LUNA)])
|
|
94
|
+
|
|
95
|
+
await harness.shutdown()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('leaves defaults alone when a patch only touches agents', async () => {
|
|
99
|
+
const harness = newHarness()
|
|
100
|
+
const session = await harness.createSession('test', { overrides: { defaults: { model: KIMI } } })
|
|
101
|
+
|
|
102
|
+
await session.setOverrides({ agents: { worker: { model: LUNA } } })
|
|
103
|
+
await session.sendAndWaitForIdle('hello')
|
|
104
|
+
|
|
105
|
+
// The orchestrator still resolves through the untouched defaults.
|
|
106
|
+
expect(modelsUsed(harness.llmProvider)).toEqual([String(KIMI)])
|
|
107
|
+
|
|
108
|
+
await harness.shutdown()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('rejects an override targeting an agent the preset does not define', async () => {
|
|
112
|
+
const harness = newHarness()
|
|
113
|
+
|
|
114
|
+
const result = await harness.createSessionResult('test', {
|
|
115
|
+
overrides: { agents: { 'no-such-agent': { model: LUNA } } },
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
expect(result.ok).toBe(false)
|
|
119
|
+
if (!result.ok) {
|
|
120
|
+
expect(result.error.message).toContain('no-such-agent')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await harness.shutdown()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// The RPC surface the worker actually calls.
|
|
127
|
+
describe('sessions.setOverrides', () => {
|
|
128
|
+
it('sets the override and echoes back the resolved state', async () => {
|
|
129
|
+
const harness = newHarness()
|
|
130
|
+
const session = await harness.createSession('test')
|
|
131
|
+
|
|
132
|
+
const result = await harness.sessionManager.callManagerMethod('sessions.setOverrides', {
|
|
133
|
+
sessionId: String(session.sessionId),
|
|
134
|
+
overrides: { agents: { worker: { model: LUNA } }, defaults: { model: KIMI } },
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(result.ok).toBe(true)
|
|
138
|
+
if (result.ok) {
|
|
139
|
+
expect(result.value).toEqual({
|
|
140
|
+
agents: { worker: { model: LUNA } },
|
|
141
|
+
defaults: { model: KIMI },
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await harness.shutdown()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('rejects an unknown agent name and names what the preset defines', async () => {
|
|
149
|
+
const harness = newHarness()
|
|
150
|
+
const session = await harness.createSession('test')
|
|
151
|
+
|
|
152
|
+
const result = await harness.sessionManager.callManagerMethod('sessions.setOverrides', {
|
|
153
|
+
sessionId: String(session.sessionId),
|
|
154
|
+
overrides: { agents: { wrker: { model: LUNA } } },
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
expect(result.ok).toBe(false)
|
|
158
|
+
if (!result.ok) {
|
|
159
|
+
expect(result.error.message).toContain('wrker')
|
|
160
|
+
expect(result.error.message).toContain('worker')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await harness.shutdown()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('reports the effective overrides back through sessions.get', async () => {
|
|
167
|
+
const harness = newHarness()
|
|
168
|
+
const session = await harness.createSession('test', {
|
|
169
|
+
overrides: { agents: { [ORCHESTRATOR_ROLE]: { model: LUNA } } },
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const result = await harness.sessionManager.callPluginMethod(
|
|
173
|
+
session.sessionId,
|
|
174
|
+
'sessions.get',
|
|
175
|
+
{ sessionId: String(session.sessionId) },
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
expect(result.ok).toBe(true)
|
|
179
|
+
if (result.ok) {
|
|
180
|
+
expect(result.value).toMatchObject({
|
|
181
|
+
overrides: { agents: { [ORCHESTRATOR_ROLE]: { model: LUNA } } },
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await harness.shutdown()
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Overrides are state like any other, so they must survive a replay rather than
|
|
190
|
+
// living only on the in-memory Session that happened to accept the patch.
|
|
191
|
+
it('reconstructs overrides when the session is reopened from the event store', async () => {
|
|
192
|
+
const harness = newHarness()
|
|
193
|
+
const session = await harness.createSession('test')
|
|
194
|
+
await session.setOverrides({ agents: { [ORCHESTRATOR_ROLE]: { model: LUNA } } })
|
|
195
|
+
await harness.shutdown()
|
|
196
|
+
|
|
197
|
+
const restarted = new TestHarness({
|
|
198
|
+
presets: [createTestPreset({ agents: [{ name: 'worker', system: 'worker' }] })],
|
|
199
|
+
llmProvider: MockLLMProvider.withFixedResponse({ content: 'ok', toolCalls: [] }),
|
|
200
|
+
eventStore: harness.eventStore,
|
|
201
|
+
})
|
|
202
|
+
const reopened = await restarted.openSession(session.sessionId)
|
|
203
|
+
|
|
204
|
+
await reopened.sendAndWaitForIdle('hello')
|
|
205
|
+
|
|
206
|
+
expect(modelsUsed(restarted.llmProvider)).toEqual([String(LUNA)])
|
|
207
|
+
|
|
208
|
+
await restarted.shutdown()
|
|
209
|
+
})
|
|
210
|
+
})
|
|
@@ -22,7 +22,7 @@ import type { AgentPluginConfig, BaseSessionHookContext, CallerContext, Configur
|
|
|
22
22
|
import { AGENT_CALLER, DEFAULT_CALLER, buildPluginDeps, type PluginNotification } from '~/core/plugins/plugin-builder.js'
|
|
23
23
|
import type { Preset } from '~/core/preset/index.js'
|
|
24
24
|
import type { SessionId } from '~/core/sessions/schema.js'
|
|
25
|
-
import type { SessionState } from '~/core/sessions/state.js'
|
|
25
|
+
import type { SessionOverridesPatch, SessionState } from '~/core/sessions/state.js'
|
|
26
26
|
import { getEntryAgentId, getNextAgentSeq, sessionEvents } from '~/core/sessions/state.js'
|
|
27
27
|
import type { Logger } from '~/lib/logger/logger.js'
|
|
28
28
|
import type { Platform } from '~/platform/index.js'
|
|
@@ -214,6 +214,24 @@ export class Session {
|
|
|
214
214
|
return Ok(undefined)
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Patch the session's overrides (see {@link SessionOverrides}).
|
|
219
|
+
*
|
|
220
|
+
* Takes effect on the next inference each affected agent runs — running agents
|
|
221
|
+
* are not rebuilt and a request already in flight completes on the model it
|
|
222
|
+
* started with. Callers are expected to have validated the patch against the
|
|
223
|
+
* preset; see `unknownOverrideTargets`.
|
|
224
|
+
*/
|
|
225
|
+
async setOverrides(patch: SessionOverridesPatch): Promise<Result<void, DomainError>> {
|
|
226
|
+
if (this.store.isClosed()) {
|
|
227
|
+
return Err(SessionErrors.closed(String(this.id)))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await this.store.emit(withSessionId(this.id, sessionEvents.create('session_overrides_set', patch)))
|
|
231
|
+
|
|
232
|
+
return Ok(undefined)
|
|
233
|
+
}
|
|
234
|
+
|
|
217
235
|
/**
|
|
218
236
|
* Get an agent by ID.
|
|
219
237
|
*/
|
|
@@ -6,6 +6,7 @@ import { agentEvents } from '~/core/agents/state.js'
|
|
|
6
6
|
import { contextEvents } from '~/core/context/state.js'
|
|
7
7
|
import { createEventsFactory } from '~/core/events/types'
|
|
8
8
|
import type { DomainEvent } from '~/core/events/types.js'
|
|
9
|
+
import { modelIdSchema } from '~/core/llm/schema.js'
|
|
9
10
|
import { llmEvents } from '~/core/llm/state.js'
|
|
10
11
|
import { SessionId, sessionIdSchema } from '~/core/sessions/schema.js'
|
|
11
12
|
import type { PendingToolResult } from '~/core/tools/schema.js'
|
|
@@ -24,6 +25,65 @@ import { createTypedReducer } from './reducer.js'
|
|
|
24
25
|
*/
|
|
25
26
|
export type SessionHandlerName = 'onSessionReady' | 'onSessionClose'
|
|
26
27
|
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Session overrides
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-agent settings layered over the preset definition.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately an object rather than a bare model id: the same layering is where
|
|
36
|
+
* `cacheTtl`, token caps or reasoning effort belong once they need to be
|
|
37
|
+
* session-scoped, and adding a field here stays backwards compatible.
|
|
38
|
+
*/
|
|
39
|
+
export const agentOverridesSchema = z4.object({
|
|
40
|
+
/** Replaces the model declared on the agent definition. */
|
|
41
|
+
model: modelIdSchema.optional(),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export type AgentOverrides = z4.infer<typeof agentOverridesSchema>
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A patch against {@link SessionOverrides}. An absent key is left alone, `null`
|
|
48
|
+
* clears it, and an object replaces that entry wholesale (it is not merged into
|
|
49
|
+
* the previous value — the entry *is* the override).
|
|
50
|
+
*/
|
|
51
|
+
export const sessionOverridesPatchSchema = z4.object({
|
|
52
|
+
defaults: agentOverridesSchema.nullable().optional(),
|
|
53
|
+
agents: z4.record(z4.string(), agentOverridesSchema.nullable()).optional(),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export type SessionOverridesPatch = z4.infer<typeof sessionOverridesPatchSchema>
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Session-scoped configuration layered over the preset, patchable at any time.
|
|
60
|
+
*
|
|
61
|
+
* Two levels because they answer different questions. `defaults` is "run this
|
|
62
|
+
* whole session on X"; `agents` is "…except this one". A preset that pins a
|
|
63
|
+
* cheap model to specific sub-agents (redo's `fastModel`) would have that intent
|
|
64
|
+
* flattened by `defaults` alone, so the per-definition level is what makes the
|
|
65
|
+
* override surgical.
|
|
66
|
+
*/
|
|
67
|
+
export interface SessionOverrides {
|
|
68
|
+
/** Applied to every agent that has no `agents` entry. */
|
|
69
|
+
defaults?: AgentOverrides
|
|
70
|
+
/** Keyed by definitionName, including ORCHESTRATOR_ROLE / COMMUNICATOR_ROLE. */
|
|
71
|
+
agents: Map<string, AgentOverrides>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Effective overrides for one agent definition: `agents` layered over `defaults`
|
|
76
|
+
* field by field, so `defaults: { model }` plus a per-agent entry that only sets
|
|
77
|
+
* some other field keeps the default model.
|
|
78
|
+
*/
|
|
79
|
+
export const resolveAgentOverrides = (
|
|
80
|
+
state: SessionState,
|
|
81
|
+
definitionName: string,
|
|
82
|
+
): AgentOverrides => ({
|
|
83
|
+
...state.overrides.defaults,
|
|
84
|
+
...state.overrides.agents.get(definitionName),
|
|
85
|
+
})
|
|
86
|
+
|
|
27
87
|
export const sessionEvents = createEventsFactory({
|
|
28
88
|
events: {
|
|
29
89
|
session_created: z4.object({
|
|
@@ -44,6 +104,7 @@ export const sessionEvents = createEventsFactory({
|
|
|
44
104
|
key: z4.string(),
|
|
45
105
|
value: z4.unknown(),
|
|
46
106
|
}),
|
|
107
|
+
session_overrides_set: sessionOverridesPatchSchema,
|
|
47
108
|
session_handler_started: z4.object({
|
|
48
109
|
handlerName: z4.enum(['onSessionReady', 'onSessionClose']),
|
|
49
110
|
pluginName: z4.string(),
|
|
@@ -62,6 +123,7 @@ export type SessionClosedEvent = (typeof sessionEvents)['Events']['session_close
|
|
|
62
123
|
export type SessionReopenedEvent = (typeof sessionEvents)['Events']['session_reopened']
|
|
63
124
|
export type SessionRestartedEvent = (typeof sessionEvents)['Events']['session_restarted']
|
|
64
125
|
export type SessionMetadataSetEvent = (typeof sessionEvents)['Events']['session_metadata_set']
|
|
126
|
+
export type SessionOverridesSetEvent = (typeof sessionEvents)['Events']['session_overrides_set']
|
|
65
127
|
export type SessionHandlerStartedEvent = (typeof sessionEvents)['Events']['session_handler_started']
|
|
66
128
|
export type SessionHandlerCompletedEvent = (typeof sessionEvents)['Events']['session_handler_completed']
|
|
67
129
|
|
|
@@ -89,6 +151,8 @@ export interface SessionState {
|
|
|
89
151
|
workspaceDir?: string
|
|
90
152
|
/** Generic key-value metadata store (tool sets use their own keys) */
|
|
91
153
|
metadata: Map<string, unknown>
|
|
154
|
+
/** Session-scoped settings layered over the preset. See {@link SessionOverrides}. */
|
|
155
|
+
overrides: SessionOverrides
|
|
92
156
|
/** If this session was forked from another session */
|
|
93
157
|
forkedFrom?: { sessionId: SessionId; eventIndex: number }
|
|
94
158
|
}
|
|
@@ -109,6 +173,7 @@ export const createSessionState = (
|
|
|
109
173
|
createdAt: timestamp,
|
|
110
174
|
agentCounters: new Map(),
|
|
111
175
|
metadata: new Map(),
|
|
176
|
+
overrides: { agents: new Map() },
|
|
112
177
|
})
|
|
113
178
|
|
|
114
179
|
// ============================================================================
|
|
@@ -429,6 +494,18 @@ export const coreReducer = createTypedReducer(
|
|
|
429
494
|
return { ...state, metadata: newMetadata }
|
|
430
495
|
}
|
|
431
496
|
|
|
497
|
+
case 'session_overrides_set': {
|
|
498
|
+
const agents = new Map(state.overrides.agents)
|
|
499
|
+
for (const [definitionName, override] of Object.entries(event.agents ?? {})) {
|
|
500
|
+
if (override === null) agents.delete(definitionName)
|
|
501
|
+
else agents.set(definitionName, override)
|
|
502
|
+
}
|
|
503
|
+
// undefined = untouched, null = cleared. `?? undefined` collapses the
|
|
504
|
+
// explicit null, which the state shape does not carry.
|
|
505
|
+
const defaults = event.defaults === undefined ? state.overrides.defaults : (event.defaults ?? undefined)
|
|
506
|
+
return { ...state, overrides: { defaults, agents } }
|
|
507
|
+
}
|
|
508
|
+
|
|
432
509
|
case 'agent_paused':
|
|
433
510
|
return updateAgent(state, event.agentId, (agent) => ({
|
|
434
511
|
...agent,
|
package/src/core/system.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type z4 from 'zod/v4'
|
|
11
11
|
import type { Logger } from '~/lib/logger/logger.js'
|
|
12
12
|
import type { Platform } from '~/platform/index.js'
|
|
13
|
+
import type { ServicePidRegistry } from '~/plugins/services/pid-registry.js'
|
|
13
14
|
import type { PortPool } from '~/plugins/services/port-pool.js'
|
|
14
15
|
import type { PreprocessorRegistry } from '~/plugins/uploads/preprocessor.js'
|
|
15
16
|
import type { EventStore } from './events/event-store.js'
|
|
@@ -120,6 +121,7 @@ export interface CreateSystemOptions<TPlugins extends readonly PluginDefinition<
|
|
|
120
121
|
preprocessorRegistry?: PreprocessorRegistry
|
|
121
122
|
llmLogger?: LLMLogger
|
|
122
123
|
portPool?: PortPool
|
|
124
|
+
pidRegistry?: ServicePidRegistry
|
|
123
125
|
/** Host-environment adapters (filesystem, process). */
|
|
124
126
|
platform: Platform
|
|
125
127
|
}
|
|
@@ -145,6 +147,7 @@ export function createSystem<const TPlugins extends readonly PluginDefinition<st
|
|
|
145
147
|
preprocessorRegistry,
|
|
146
148
|
llmLogger,
|
|
147
149
|
portPool,
|
|
150
|
+
pidRegistry,
|
|
148
151
|
platform,
|
|
149
152
|
} = options
|
|
150
153
|
|
|
@@ -167,6 +170,7 @@ export function createSystem<const TPlugins extends readonly PluginDefinition<st
|
|
|
167
170
|
preprocessorRegistry,
|
|
168
171
|
llmLogger,
|
|
169
172
|
portPool,
|
|
173
|
+
pidRegistry,
|
|
170
174
|
platform,
|
|
171
175
|
systemPlugins: [...plugins],
|
|
172
176
|
})
|
package/src/index.ts
CHANGED
|
@@ -66,7 +66,14 @@ export { applyEvent } from '~/core/sessions/apply-event.js'
|
|
|
66
66
|
export { selectPluginState } from '~/core/sessions/reducer.js'
|
|
67
67
|
export { SessionId } from '~/core/sessions/schema.js'
|
|
68
68
|
export type { SessionMetadata } from '~/core/sessions/schema.js'
|
|
69
|
-
export {
|
|
69
|
+
export type { AgentOverrides, SessionOverrides, SessionOverridesPatch } from '~/core/sessions/state.js'
|
|
70
|
+
export {
|
|
71
|
+
agentOverridesSchema,
|
|
72
|
+
createSessionState,
|
|
73
|
+
reconstructSessionState,
|
|
74
|
+
resolveAgentOverrides,
|
|
75
|
+
sessionOverridesPatchSchema,
|
|
76
|
+
} from '~/core/sessions/state.js'
|
|
70
77
|
export type { SessionState } from '~/core/sessions/state.js'
|
|
71
78
|
export type { ToolCallId } from '~/core/tools/schema.js'
|
|
72
79
|
export type { ToolStartedEvent } from '~/core/tools/state.js'
|