Skip to content
Open
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
Expand Up @@ -36,7 +36,7 @@ import { runtimeHostSessionFixture } from './runtime-host-session-test-fixture.j

type BotClient = RuntimeHostBotSessionAdapterDeps['client'];

test('creates an explore Session through the Host-owned default model route', async () => {
test('creates a bot-mode Session through the Host-owned default model route', async () => {
const creates: unknown[] = [];
const changes: unknown[] = [];
const client = botClient({
Expand Down Expand Up @@ -68,7 +68,7 @@ test('creates an explore Session through the Host-owned default model route', as
name: 'Telegram conversation',
labels: ['bot', 'telegram'],
modelTarget: { kind: 'default' },
permissionMode: 'explore',
mode: 'bot',
},
]);
assert.deepEqual(changes, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@

import assert from 'node:assert/strict';
import test from 'node:test';
import type { SessionCatalogProjection } from '@maka/runtime-host/protocol';
import { toDesktopHostSessionSummary } from '../runtime-host-session-catalog-ipc-main.js';
import type { IpcMain } from 'electron';
import type { SessionCatalogProjection, SessionCreateInput } from '@maka/runtime-host/protocol';
import {
registerRuntimeHostSessionCatalogIpc,
toDesktopHostSessionSummary,
type RuntimeHostSessionCatalogIpcDeps,
} from '../runtime-host-session-catalog-ipc-main.js';

test('maps Runtime Host live run state without collapsing unknown and known-empty', () => {
const unknown = toDesktopHostSessionSummary(projection());
Expand All @@ -36,6 +41,58 @@ test('maps Runtime Host live run state without collapsing unknown and known-empt
assert.deepEqual(running.runningTurnIds, ['turn-live']);
});

test('session creation forwards the caller name for a mode that carries none', async () => {
const creates: SessionCreateInput[] = [];
const ipc = ipcHarness();
registerRuntimeHostSessionCatalogIpc(createDeps(creates), ipc as unknown as IpcMain);

await ipc.invoke('sessions:create', { mode: 'bot', name: '飞书 任务' });
await ipc.invoke('sessions:create', { mode: 'deep_research', name: '飞书 任务' });

assert.deepEqual(
creates.map((input) => [input.mode, input.name]),
[
['bot', '飞书 任务'],
['deep_research', '飞书 任务'],
],
);
});

type IpcHandler = Parameters<Pick<IpcMain, 'handle'>['handle']>[1];

function ipcHarness() {
const handlers = new Map<string, IpcHandler>();
return {
handle(channel: string, handler: IpcHandler) {
handlers.set(channel, handler);
},
async invoke(channel: string, ...args: unknown[]): Promise<unknown> {
const handler = handlers.get(channel);
assert.ok(handler, `missing handler: ${channel}`);
return handler({} as never, ...args);
},
};
}

function createDeps(creates: SessionCreateInput[]): RuntimeHostSessionCatalogIpcDeps {
return {
client: {
createSession: async (input: SessionCreateInput) => {
creates.push(input);
return projection({ id: input.sessionId });
},
} as unknown as RuntimeHostSessionCatalogIpcDeps['client'],
runningTurnIds: () => [],
resolveCreateProject: async () => ({ kind: 'host_path', path: '/workspace' }),
emitSessionsChanged: () => {},
releaseSessionResources: () => {},
sessionCopyCleanup: {
ownCreation: async <T>(_creation: unknown, operation: () => Promise<T>) => operation(),
recover: async () => ({ removed: [], failed: [] }),
} as unknown as RuntimeHostSessionCatalogIpcDeps['sessionCopyCleanup'],
};
}

function projection(overrides: Partial<SessionCatalogProjection> = {}): SessionCatalogProjection {
return {
id: 'session-1',
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/main/create-session-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import type { OrchestrationMode } from '@maka/core/orchestration';

import type { PermissionMode } from '@maka/core/permission';

import type { SessionStartMode } from '@maka/core/deep-research';
import type { SessionStartMode } from '@maka/core/session-start-mode';
import { DEFAULT_SESSION_NAME } from '@maka/core/session-name';

import { isChatDefaultPermissionMode } from '@maka/core/settings';
Expand All @@ -52,7 +52,7 @@ import { isCollaborationMode } from '@maka/core/collaboration';

import { isOrchestrationMode } from '@maka/core/orchestration';

import { isSessionStartMode } from '@maka/core/deep-research';
import { isSessionStartMode } from '@maka/core/session-start-mode';

/**
* `unknown`, because this is an IPC boundary and the renderer's type is a
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/runtime-host-bot-session-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function createRuntimeHostBotSessionAdapter(
name: input.name,
labels: [...input.labels],
modelTarget: { kind: 'default' },
permissionMode: 'explore',
mode: 'bot',
});
} catch (error) {
if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ export function registerRuntimeHostSessionCatalogIpc(
sessionId: newId(),
workspace,
...(request.mode === undefined ? {} : { mode: request.mode }),
...(request.mode === undefined ? { name: request.name } : {}),
// A nameless mode (`bot`) keeps the caller's name, so always forward it.
name: request.name,
...(request.labels === undefined ? {} : { labels: request.labels }),
modelTarget: normalizeModelTarget(input),
...normalizeCreateThinkingLevel(input?.thinkingLevel),
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-command-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { useMemo, useRef } from "react";
import type { LlmConnection } from '@maka/core/llm-connections';
import type { PermissionMode } from '@maka/core/permission';
import type { SessionStartMode } from '@maka/core/deep-research';
import type { SessionStartMode } from '@maka/core/session-start-mode';
import type { SessionSummary, StoredMessage } from '@maka/core/session';
import type { SettingsSection, ThemePreference } from '@maka/core/settings';
import type { UiLocale } from '@maka/core/ui-locale';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import type { SessionStartMode } from '@maka/core/deep-research';
import type { SessionStartMode } from '@maka/core/session-start-mode';
import type { UiLocale } from '@maka/core/ui-locale';
import type { NavSelection } from '@maka/ui';
import type { DesktopNewTaskTarget } from '../preload/bridge-contract.js';
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"./daily-review": "./dist/daily-review.js",
"./work-board": "./dist/work-board.js",
"./deep-research": "./dist/deep-research.js",
"./session-start-mode": "./dist/session-start-mode.js",
"./long-term-memory": "./dist/long-term-memory.js",
"./local-memory": "./dist/local-memory.js",
"./web-search": "./dist/web-search.js",
Expand Down
42 changes: 1 addition & 41 deletions packages/core/src/deep-research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,47 +20,7 @@
/** Read-only Deep Research session semantics and prompt contract. */

import type { DeepResearchRun } from './deep-research-run.js';
import type { PermissionMode } from './permission.js';

/**
* A product intent a caller can open a new session at, distinct from the
* ordinary chat that needs no intent at all. Absence is spelled `undefined`,
* not a `'chat'` member: a second spelling of "no mode" is a second thing to
* keep in agreement.
*/
export interface SessionStartModeSpec {
readonly name: string;
readonly labels: readonly string[];
readonly permissionMode: PermissionMode;
}

export const SESSION_START_MODE_SPECS = {
deep_research: {
name: 'Deep Research',
labels: ['mode:deep_research'],
permissionMode: 'explore',
},
} as const satisfies Record<string, SessionStartModeSpec>;

export type SessionStartMode = keyof typeof SESSION_START_MODE_SPECS;
export const SESSION_START_MODES: readonly SessionStartMode[] = Object.keys(
SESSION_START_MODE_SPECS,
) as SessionStartMode[];
export const SESSION_START_MODE_LABELS: readonly string[] = [
...new Set(Object.values(SESSION_START_MODE_SPECS).flatMap((spec) => spec.labels)),
];

export function isSessionStartMode(value: unknown): value is SessionStartMode {
return typeof value === 'string' && Object.hasOwn(SESSION_START_MODE_SPECS, value);
}

export function sessionStartModeSpec(mode: SessionStartMode): SessionStartModeSpec {
return SESSION_START_MODE_SPECS[mode];
}

export function isSessionStartModeLabel(value: unknown): value is string {
return typeof value === 'string' && SESSION_START_MODE_LABELS.includes(value);
}
import { SESSION_START_MODE_SPECS } from './session-start-mode.js';

export const DEEP_RESEARCH_SESSION_NAME = SESSION_START_MODE_SPECS.deep_research.name;
export const DEEP_RESEARCH_SESSION_LABEL = SESSION_START_MODE_SPECS.deep_research.labels[0];
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/runtime-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import type { PermissionMode } from './permission.js';
import type { ThinkingLevel } from './model-thinking.js';
import type { CollaborationMode } from './collaboration.js';
import type { OrchestrationMode, TurnOrchestration } from './orchestration.js';
import type { SessionStartMode } from './deep-research.js';
import type { SessionStartMode } from './session-start-mode.js';
import type { SubagentWorkspaceBinding } from './subagent-workspace.js';
import type { ToolMode } from './tool-mode.js';
import type { TurnOrigin } from './turn-origin.js';
Expand Down
65 changes: 65 additions & 0 deletions packages/core/src/session-start-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import type { PermissionMode } from './permission.js';

/**
* A product intent a caller can open a new session at, distinct from the
* ordinary chat that needs no intent at all. Absence is spelled `undefined`,
* not a `'chat'` member: a second spelling of "no mode" is a second thing to
* keep in agreement.
*/
export interface SessionStartModeSpec {
/** Omitted where the mode keeps the caller's name, as `bot` does. */
readonly name?: string;
readonly labels: readonly string[];
readonly permissionMode: PermissionMode;
}

export const SESSION_START_MODE_SPECS = {
deep_research: {
name: 'Deep Research',
labels: ['mode:deep_research'],
permissionMode: 'explore',
},
bot: {
labels: ['mode:bot'],
permissionMode: 'explore',
},
} as const satisfies Record<string, SessionStartModeSpec>;

export type SessionStartMode = keyof typeof SESSION_START_MODE_SPECS;
export const SESSION_START_MODES: readonly SessionStartMode[] = Object.keys(
SESSION_START_MODE_SPECS,
) as SessionStartMode[];
export const SESSION_START_MODE_LABELS: readonly string[] = [
...new Set(Object.values(SESSION_START_MODE_SPECS).flatMap((spec) => spec.labels)),
];

export function isSessionStartMode(value: unknown): value is SessionStartMode {
return typeof value === 'string' && Object.hasOwn(SESSION_START_MODE_SPECS, value);
}

export function sessionStartModeSpec(mode: SessionStartMode): SessionStartModeSpec {
return SESSION_START_MODE_SPECS[mode];
}

export function isSessionStartModeLabel(value: unknown): value is string {
return typeof value === 'string' && SESSION_START_MODE_LABELS.includes(value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,40 @@ test('creation materializes Deep Research semantics inside the Host transaction'
assert.equal(fixture.drainRequests(), 0);
});

test('bot mode grants explore while keeping the Bot-supplied Session name', async () => {
let created: Parameters<CatalogStores['createStableSession']>[0] | undefined;
const fixture = createFixture({
stores: {
createStableSession: async (request) => {
created = request;
return {
kind: 'existing',
record: headerSnapshot(sessionHeader(request.sessionId, request.input.labels ?? []), 3),
};
},
},
});

const outcome = await fixture.coordinator.handlers['session.create'](
{
sessionId: fixture.sessionId,
workspace: { kind: 'host_path', path: process.cwd() },
mode: 'bot',
name: '飞书 任务',
labels: ['bot', 'feishu'],
modelTarget: { kind: 'default' },
},
context,
);

assert.equal(outcome.ok, true);
assert.ok(created);
assert.equal(created.input.name, '飞书 任务');
assert.deepEqual(created.input.labels, ['bot', 'feishu', 'mode:bot']);
assert.equal(created.input.permissionMode, 'explore');
assert.equal(fixture.drainRequests(), 0);
});

test('configuration update admits Plan mode through Runtime authority', async () => {
const fixture = createFixture();
const input = configurationInput(fixture.sessionId, fixture.revision());
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-host/src/protocol/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 68 as const;
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 69 as const;
// 68: Connection onboarding replaces nullable canonical-slug targeting with
// explicit create/existing identity and returns the committed Connection.
// Older peers reject the closed target and saved-result shapes.
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-host/src/protocol/session-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { isCollaborationMode, type CollaborationMode } from '@maka/core/collaboration';
import { isOrchestrationMode, type OrchestrationMode } from '@maka/core/orchestration';
import { isPermissionMode, type PermissionMode } from '@maka/core/permission';
import { isSessionStartMode, type SessionStartMode } from '@maka/core/deep-research';
import { isSessionStartMode, type SessionStartMode } from '@maka/core/session-start-mode';
import {
isSessionBlockedReason,
isSessionToolProfile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { DEFAULT_SESSION_NAME, normalizeUserSessionName } from '@maka/core/sessi
import {
isSessionStartModeLabel as isExecutionSemanticLabel,
sessionStartModeSpec,
} from '@maka/core/deep-research';
} from '@maka/core/session-start-mode';
import {
isWorkHubCoordinationSessionId,
isWorkHubCoordinationSessionTarget,
Expand Down