Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added `rlm.create_session(...)` so daemon-backed root agents can start separate top-level sessions.
69 changes: 66 additions & 3 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.j
import {
type CreateRlmSubagentRuntimeOptions,
createDefaultRlmSubagentSessionName,
createRlmCreateSessionHostHandler,
createRlmDeleteSubagentHostHandler,
createRlmFindModelsHostHandler,
createRlmListSubagentsHostHandler,
Expand All @@ -224,6 +225,7 @@ import {
normalizeRequestedRlmSubagentModel,
normalizeRequestedRlmSubagentSessionName,
normalizeRequestedRlmSubagentThinkingLevel,
type RlmCreateSessionResult,
type RlmDeleteSubagentResult,
type RlmFindModelsResult,
type RlmListSubagentsResult,
Expand Down Expand Up @@ -9211,6 +9213,9 @@ export class AgentSession {
"rlm.run": createRlmRunHostHandler(async ({ prompt, kwargs, cellSourceCode }) => ({
...(await this.runRlmChild(prompt, kwargs, cellSourceCode)),
})),
"rlm.create_session": createRlmCreateSessionHostHandler(async ({ prompt, kwargs }) => ({
...(await this.createRlmSession(prompt, kwargs)),
})),
"rlm.find_models": createRlmFindModelsHostHandler((query, limit) => this.findRlmModels(query, limit)),
"rlm.list_subagents": createRlmListSubagentsHostHandler(() => this.listRlmSubagents()),
"rlm.delete_subagent": createRlmDeleteSubagentHostHandler((target) => this.deleteRlmSubagent(target)),
Expand Down Expand Up @@ -10372,7 +10377,10 @@ export class AgentSession {
};
}

private async _resolveRlmSubagentModel(reference: string | undefined): Promise<RlmSubagentModelSelection> {
private async _resolveRlmSubagentModel(
reference: string | undefined,
target = "subagent",
): Promise<RlmSubagentModelSelection> {
const parentModel = this.model;
if (!parentModel) {
throw new Error(formatNoModelSelectedMessage());
Expand All @@ -10389,12 +10397,12 @@ export class AgentSession {
(candidate) => `${candidate.provider}/${candidate.id}`.toLowerCase() === normalizedReference,
);
if (!model) {
throw new Error(`Requested subagent model "${reference}" is unavailable, unauthenticated, or expired`);
throw new Error(`Requested ${target} model "${reference}" is unavailable, unauthenticated, or expired`);
}

const auth = await this._modelRegistry.getApiKeyAndHeaders(model);
if (!auth.ok) {
throw new Error(`Requested subagent model "${reference}" failed authentication preflight`);
throw new Error(`Requested ${target} model "${reference}" failed authentication preflight`);
}
return { model };
}
Expand Down Expand Up @@ -10785,6 +10793,61 @@ export class AgentSession {
};
}

async createRlmSession(prompt: string, kwargs: Record<string, unknown> = {}): Promise<RlmCreateSessionResult> {
const { name: rawName, model: rawModel, thinking: rawThinking, cwd: rawCwd, ...unsupported } = kwargs;
const unsupportedKeys = Object.keys(unsupported);
if (unsupportedKeys.length > 0) {
throw new Error(`Unsupported rlm.create_session kwargs: ${unsupportedKeys.sort().join(", ")}`);
}
if (!prompt.trim()) {
throw new Error("rlm.create_session prompt must not be empty");
}
if (this._rlmDepth !== 0) {
throw new Error("rlm.create_session is available only from a depth-0 session");
}
if (this._disposed || this._disposing) {
throw new Error("Cannot create a top-level session after the current session was disposed");
}
const host = this._subagentRuntimeHost;
if (!host?.createRlmRootSession) {
throw new Error("rlm.create_session requires a daemon-backed depth-0 session");
}

const operation = "rlm.create_session";
const sessionName = normalizeRequestedRlmSubagentSessionName(rawName, operation);
const requestedModel = normalizeRequestedRlmSubagentModel(rawModel, operation);
const requestedThinkingLevel = normalizeRequestedRlmSubagentThinkingLevel(rawThinking, operation);
if (sessionName) {
assertDirectAgentMessageTarget(sessionName);
const controller = this._agentMessageController;
if (controller?.assertSessionNameAvailable) {
await controller.assertSessionNameAvailable({ name: sessionName, depth: 0 });
}
}
if (rawCwd !== undefined && (typeof rawCwd !== "string" || !rawCwd.trim())) {
throw new Error("rlm.create_session cwd must be a non-empty string");
}
const cwd = rawCwd === undefined ? this._cwd : resolve(this._cwd, rawCwd.trim());
const modelSelection = await this._resolveRlmSubagentModel(requestedModel, "top-level session");
if (requestedThinkingLevel !== undefined) {
const supported = getSupportedThinkingLevels(modelSelection.model) as ThinkingLevel[];
if (!supported.includes(requestedThinkingLevel)) {
throw new Error(
`Requested thinking level "${requestedThinkingLevel}" is not supported by model "${modelSelection.model.provider}/${modelSelection.model.id}"; supported levels: ${supported.join(", ")}`,
);
}
}
const thinkingLevel =
requestedThinkingLevel ?? (clampThinkingLevel(modelSelection.model, this.thinkingLevel) as ThinkingLevel);
return host.createRlmRootSession({
prompt,
sessionName,
cwd,
model: modelSelection.model,
thinkingLevel,
});
}

async runRlmChild(
prompt: string,
kwargs: Record<string, unknown> = {},
Expand Down
4 changes: 2 additions & 2 deletions packages/coding-agent/src/core/kernel/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const REQUIRED_HARNESS_METHODS = [
"delete_prompt_note",
"record_refinement",
];
const RUNTIME_READY_CHECK = `import inspect; import rlm; from rlm import McpIntegration; import rlm.mcp as mcp; from rlm.harness import HarnessEntry; _harness_methods = ${JSON.stringify(REQUIRED_HARNESS_METHODS)}; assert callable(mcp.list_tools); assert callable(mcp.call_tool); assert hasattr(rlm, 'run'); assert callable(rlm); assert hasattr(rlm, 'rlm'); assert callable(rlm.rlm); assert callable(rlm.host_request); assert callable(rlm.find_models); assert callable(rlm.rlm.find_models); assert hasattr(rlm, 'harness'); assert hasattr(rlm, 'get_harness_state'); assert hasattr(rlm.rlm, 'harness'); assert hasattr(rlm.rlm, 'get_harness_state'); assert all(callable(getattr(_harness, _method, None)) for _harness in (rlm.harness, rlm.rlm.harness) for _method in _harness_methods); assert 'reference' in HarnessEntry.__dataclass_fields__; assert 'scope' in HarnessEntry.__dataclass_fields__; assert 'reference' in inspect.signature(rlm.harness.create_skill).parameters; assert 'reference' in inspect.signature(rlm.harness.update_skill).parameters; assert 'global_' in inspect.signature(rlm.harness.create_memory).parameters; assert 'global_' in inspect.signature(rlm.get_harness_state).parameters; assert not hasattr(rlm, 'background'); assert not hasattr(rlm.rlm, 'background'); from rlm.bash import BashHandle, BashResult; assert callable(rlm.bash); assert all(callable(getattr(BashHandle, _m, None)) for _m in ('tail', 'output', 'poll', 'kill')); assert {'exit_code', 'output', 'duration'} <= set(BashResult.__dataclass_fields__); import rlm.repl as _repl; assert callable(_repl.main); assert callable(_repl.emit); assert callable(_repl.host_request); assert callable(_repl.is_active); assert _repl.PROTOCOL_VERSION == 3; assert callable(rlm.emit); assert not hasattr(rlm, 'HOST_COMM_TARGET'); assert not hasattr(mcp, 'install_shutdown_hook')`;
const RUNTIME_READY_CHECK = `import inspect; import rlm; from rlm import McpIntegration; import rlm.mcp as mcp; from rlm.harness import HarnessEntry; _harness_methods = ${JSON.stringify(REQUIRED_HARNESS_METHODS)}; assert callable(mcp.list_tools); assert callable(mcp.call_tool); assert hasattr(rlm, 'run'); assert callable(rlm); assert hasattr(rlm, 'rlm'); assert callable(rlm.rlm); assert callable(rlm.host_request); assert callable(rlm.find_models); assert callable(rlm.rlm.find_models); assert callable(rlm.create_session); assert callable(rlm.rlm.create_session); assert hasattr(rlm, 'harness'); assert hasattr(rlm, 'get_harness_state'); assert hasattr(rlm.rlm, 'harness'); assert hasattr(rlm.rlm, 'get_harness_state'); assert all(callable(getattr(_harness, _method, None)) for _harness in (rlm.harness, rlm.rlm.harness) for _method in _harness_methods); assert 'reference' in HarnessEntry.__dataclass_fields__; assert 'scope' in HarnessEntry.__dataclass_fields__; assert 'reference' in inspect.signature(rlm.harness.create_skill).parameters; assert 'reference' in inspect.signature(rlm.harness.update_skill).parameters; assert 'global_' in inspect.signature(rlm.harness.create_memory).parameters; assert 'global_' in inspect.signature(rlm.get_harness_state).parameters; assert not hasattr(rlm, 'background'); assert not hasattr(rlm.rlm, 'background'); from rlm.bash import BashHandle, BashResult; assert callable(rlm.bash); assert all(callable(getattr(BashHandle, _m, None)) for _m in ('tail', 'output', 'poll', 'kill')); assert {'exit_code', 'output', 'duration'} <= set(BashResult.__dataclass_fields__); import rlm.repl as _repl; assert callable(_repl.main); assert callable(_repl.emit); assert callable(_repl.host_request); assert callable(_repl.is_active); assert _repl.PROTOCOL_VERSION == 3; assert callable(rlm.emit); assert not hasattr(rlm, 'HOST_COMM_TARGET'); assert not hasattr(mcp, 'install_shutdown_hook')`;
const BOOTSTRAP_VERSION_FILE = ".bootstrap-version";
const BOOTSTRAP_LOCK_NAME = ".bootstrap.lock";
const BOOTSTRAP_LOCK_RETRY_MS = 100;
Expand Down Expand Up @@ -850,7 +850,7 @@ async function ensureKernelPythonUncached(
const missing: string[] = [];
if (!(await hasPrimeAgentRuntime(python))) {
missing.push(
"a current prime-agent-runtime with callable rlm.run, rlm.host_request, and explicit harness CRUD methods",
"a current prime-agent-runtime with callable rlm.run, rlm.create_session, rlm.host_request, and explicit harness CRUD methods",
);
}
if (missing.length === 0) {
Expand Down
7 changes: 7 additions & 0 deletions packages/coding-agent/src/core/prompts/rlm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ export function buildRlmPrompt(options: RlmPromptOptions): string {
);
}

if (depth === 0 && hasIpython) {
parts.push(
"",
"From a daemon-backed depth-0 session, use `await rlm.create_session('task', name='researcher')` to start a separate top-level session. The call returns after the daemon creates the session and accepts its first prompt. Inline and nested sessions cannot use it. `rlm(...)` still creates a child.",
);
}

if (allowRecursion && hasIpython) {
parts.push(
"",
Expand Down
57 changes: 47 additions & 10 deletions packages/coding-agent/src/core/rlm-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ export interface RlmRunRequest {
cellSourceCode?: string;
}

interface RlmCreateSessionRequest {
prompt: string;
kwargs: Record<string, unknown>;
}

export interface RlmCreateSessionResult {
active_session_id: string;
session_id: string;
name: string;
session_file: string;
model: string;
}

export interface RlmSpawnHandle {
rlm_child_id: string;
name: string;
Expand Down Expand Up @@ -51,6 +64,7 @@ export interface RlmFindModelsResult {
}

export type RlmRunHandler = (request: RlmRunRequest) => Promise<Record<string, unknown>>;
type RlmCreateSessionHandler = (request: RlmCreateSessionRequest) => Promise<RlmCreateSessionResult>;
export type RlmListSubagentsHandler = () => RlmListSubagentsResult | Promise<RlmListSubagentsResult>;
export type RlmDeleteSubagentHandler = (target: string) => Promise<RlmDeleteSubagentResult>;
export type RlmFindModelsHandler = (query: string, limit: number) => RlmFindModelsResult | Promise<RlmFindModelsResult>;
Expand All @@ -59,47 +73,50 @@ const RLM_SUBAGENT_SESSION_NAME_MAX_LENGTH = 64;
export const DEFAULT_RLM_MODEL_SEARCH_LIMIT = 8;
export const MAX_RLM_MODEL_SEARCH_LIMIT = 20;

export function normalizeRequestedRlmSubagentSessionName(value: unknown): string | undefined {
export function normalizeRequestedRlmSubagentSessionName(value: unknown, operation = "rlm.run"): string | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "string") {
throw new Error("rlm.run name must be a string");
throw new Error(`${operation} name must be a string`);
}
const name = value.trim();
if (!name) {
throw new Error("rlm.run name must not be empty");
throw new Error(`${operation} name must not be empty`);
}
if (name.length > RLM_SUBAGENT_SESSION_NAME_MAX_LENGTH) {
throw new Error(`rlm.run name must be at most ${RLM_SUBAGENT_SESSION_NAME_MAX_LENGTH} characters`);
throw new Error(`${operation} name must be at most ${RLM_SUBAGENT_SESSION_NAME_MAX_LENGTH} characters`);
}
return name;
}

export function normalizeRequestedRlmSubagentThinkingLevel(value: unknown): ThinkingLevel | undefined {
export function normalizeRequestedRlmSubagentThinkingLevel(
value: unknown,
operation = "rlm.run",
): ThinkingLevel | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "string") {
throw new Error("rlm.run thinking must be a string");
throw new Error(`${operation} thinking must be a string`);
}
const level = value.trim().toLowerCase();
if (!THINKING_LEVELS.includes(level as ThinkingLevel)) {
throw new Error(`rlm.run thinking must be one of: ${THINKING_LEVELS.join(", ")}`);
throw new Error(`${operation} thinking must be one of: ${THINKING_LEVELS.join(", ")}`);
}
return level as ThinkingLevel;
}

export function normalizeRequestedRlmSubagentModel(value: unknown): string | undefined {
export function normalizeRequestedRlmSubagentModel(value: unknown, operation = "rlm.run"): string | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "string") {
throw new Error("rlm.run model must be a string");
throw new Error(`${operation} model must be a string`);
}
const model = value.trim();
if (!model) {
throw new Error("rlm.run model must not be empty");
throw new Error(`${operation} model must not be empty`);
}
return model;
}
Expand Down Expand Up @@ -161,6 +178,17 @@ export function findRlmModelMatches(query: string, models: Model<Api>[], limit:
}));
}

export function createRlmCreateSessionHostHandler(handler: RlmCreateSessionHandler): HostRequestHandler {
return async (payload) => {
if (typeof payload.prompt !== "string") {
throw new Error("rlm.create_session prompt must be a string");
}
const kwargs = isRecord(payload.kwargs) ? payload.kwargs : {};
const result = await handler({ prompt: payload.prompt, kwargs });
return result as unknown as Record<string, unknown>;
};
}

/** Adapt an RlmRunHandler into the typed `rlm.run` kernel host handler. */
export function createRlmRunHostHandler(handler: RlmRunHandler): HostRequestHandler {
return async (payload) => {
Expand Down Expand Up @@ -239,8 +267,17 @@ export interface CreateRlmSubagentRuntimeOptions {
onSessionPublished?: (session: AgentSession) => void;
}

export interface CreateRlmRootSessionOptions {
prompt: string;
sessionName?: string;
cwd: string;
model: Model<Api>;
thinkingLevel: ThinkingLevel;
}

export interface SubagentRuntimeHost {
createRlmSubagentRuntime(options: CreateRlmSubagentRuntimeOptions): Promise<RlmSubagentRuntime>;
createRlmRootSession?(options: CreateRlmRootSessionOptions): Promise<RlmCreateSessionResult>;
/** Persist host-owned completion before the child becomes passivation-eligible. */
completeRlmSubagentRuntime?(childId: string, session: AgentSession): boolean;
/** Release a host-owned child after its detached initial task settles. */
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/src/core/tools/ipython.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ except Exception as _prime_agent_rlm_error:
async def find_models(self, query="", limit=8):
self._raise_missing()

async def create_session(self, prompt, **kwargs):
self._raise_missing()

async def list_subagents(self):
self._raise_missing()

Expand Down
81 changes: 80 additions & 1 deletion packages/coding-agent/src/modes/daemon/daemon-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ import {
} from "../../core/cron-jobs.js";
import { ORPHAN_PROCESS_JOURNAL_ENV } from "../../core/orphan-process-journal.js";
import { PromptAdmissionCancelledError, waitForPromptAdmission } from "../../core/prompt-admission.js";
import type { CreateRlmSubagentRuntimeOptions, SubagentRuntimeHost } from "../../core/rlm-runtime.js";
import type {
CreateRlmRootSessionOptions,
CreateRlmSubagentRuntimeOptions,
RlmCreateSessionResult,
SubagentRuntimeHost,
} from "../../core/rlm-runtime.js";
import {
canPassivateSession,
type IdleEvictionMinutes,
Expand Down Expand Up @@ -2328,6 +2333,7 @@ export class AgentDaemon {
private createSubagentRuntimeHost(parentState: ActiveSessionState): SubagentRuntimeHost {
return {
createRlmSubagentRuntime: async (options) => this.createRlmSubagentRuntime(parentState, options),
createRlmRootSession: async (options) => this.createRlmRootSession(parentState, options),
completeRlmSubagentRuntime: (childId, session) => {
const state = [...this.sessions.values()].find(
(candidate) =>
Expand Down Expand Up @@ -2477,6 +2483,79 @@ export class AgentDaemon {
};
}

private async createRlmRootSession(
parentState: ActiveSessionState,
options: CreateRlmRootSessionOptions,
): Promise<RlmCreateSessionResult> {
const supervisorSocketPath = this.supervisorSocketPathFromEnv();
if (!this.options.worker || !supervisorSocketPath) {
throw new Error("rlm.create_session requires a daemon worker connected to its supervisor");
}

const client = new DaemonClient(supervisorSocketPath);
let activeSessionId: string | undefined;
try {
await client.connect(3000);
await client.waitForHello(3000);
const runtimeConfig = parentState.runtime.runtimeConfig;
const createResponse = await client.request(
{
type: "create",
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
lifecycle: "resident",
...(options.sessionName ? { name: options.sessionName } : {}),
config: {
cwd: options.cwd,
agentDir: parentState.runtime.services.agentDir,
...(runtimeConfig?.sessionDir ? { sessionDir: runtimeConfig.sessionDir } : {}),
provider: options.model.provider,
model: options.model.id,
thinking: options.thinkingLevel,
...(runtimeConfig?.telemetryDisabled ? { telemetryDisabled: true as const } : {}),
},
},
120_000,
);
if (!createResponse.success) throw deserializeDaemonError(createResponse);
const summary = createResponse.data as Partial<SessionSummary> | undefined;
activeSessionId = summary?.activeSessionId ?? summary?.id;
if (
!activeSessionId ||
typeof summary?.sessionId !== "string" ||
!summary.sessionId ||
typeof summary.sessionFile !== "string" ||
!summary.sessionFile ||
(summary.rlmDepth !== undefined && summary.rlmDepth !== 0)
) {
throw new Error("Daemon supervisor returned an invalid depth-0 session summary");
}

const promptResponse = await client.request(
{
type: "prompt",
activeSessionId,
message: options.prompt,
source: "rpc",
},
30_000,
);
if (!promptResponse.success) throw deserializeDaemonError(promptResponse);
return {
active_session_id: activeSessionId,
session_id: summary.sessionId,
name: summary.sessionName ?? activeSessionId,
session_file: summary.sessionFile,
model: `${options.model.provider}/${options.model.id}`,
};
} catch (error) {
if (activeSessionId) {
await client.request({ type: "kill", activeSessionId }, 30_000).catch(() => undefined);
}
throw error;
} finally {
client.close();
}
}

private async createRlmSubagentRuntime(
parentState: ActiveSessionState,
options: CreateRlmSubagentRuntimeOptions,
Expand Down
Loading
Loading