You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by ku...@apache.org on 2026/09/14 09:43:54 UTC
(maka) branch main updated: feat(runtime): add plugin-backed Session executors (#5283)
This is an automated email from the ASF dual-hosted git repository.
likun666661 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new f32cf2b482 feat(runtime): add plugin-backed Session executors (#5283)
f32cf2b482 is described below
commit f32cf2b482a17f235b92e2775ed2672c2775e219
Author: Xinhao Xu <84...@users.noreply.github.com>
AuthorDate: Mon Sep 14 17:43:51 2026 +0800
feat(runtime): add plugin-backed Session executors (#5283)
* feat(runtime): add plugin-backed Session executors
Allow Host plugins to register scoped black-box executors through ctx.executors and route their output through the existing Session event stream. Persist executor selection across ordinary Sessions, WorkHub-created work, child agents, Graph operators, and Session revisions without exposing Maka tools to the external runtime.
Generated-by: Codex
* feat(runtime): harden plugin executor capabilities
---
.../runtime-host-session-catalog-ipc-main.test.ts | 20 +
.../src/main/__tests__/session-local.test.ts | 41 ++
.../main/runtime-host-session-catalog-ipc-main.ts | 11 +-
apps/desktop/src/main/session-local-service.ts | 16 +-
packages/core/package.json | 1 +
.../src/__tests__/agent-graph-schedule.test.ts | 23 +
packages/core/src/__tests__/executor-id.test.ts | 30 ++
.../__tests__/runtime-invocation-opened.test.ts | 22 +
.../src/__tests__/session-send-projection.test.ts | 17 +
packages/core/src/agent-graph-schedule.ts | 21 +-
packages/core/src/executor-id.ts | 24 +
packages/core/src/runtime-event.ts | 40 +-
packages/core/src/runtime-inputs.ts | 10 +-
packages/core/src/session-send-projection.ts | 12 +-
packages/core/src/session.ts | 14 +-
packages/eval/src/harbor-maka-subject.ts | 5 +-
.../__tests__/host-session-availability.test.ts | 41 ++
.../src/__tests__/plugin-platform.test.ts | 52 ++
.../__tests__/session-catalog-coordinator.test.ts | 82 ++++
.../src/__tests__/session-catalog-protocol.test.ts | 44 ++
.../workhub-coordination-protocol.test.ts | 24 +
.../src/client/session-catalog-summary.ts | 1 +
packages/runtime-host/src/protocol/index.ts | 3 +-
.../runtime-host/src/protocol/plugin-platform.ts | 69 ++-
.../runtime-host/src/protocol/session-catalog.ts | 43 +-
.../src/server/child-agent-composition.ts | 1 +
.../src/server/execution-composition.ts | 60 ++-
.../src/server/host-session-availability.ts | 17 +-
.../src/server/plugin-platform-coordinator.ts | 16 +-
.../runtime-host/src/server/plugin-platform.ts | 11 +
.../src/server/session-catalog-coordinator.ts | 97 +++-
.../src/server/session-revision-coordinator.ts | 3 +
packages/runtime/package.json | 2 +
.../src/__tests__/plugin-executor-backend.test.ts | 209 ++++++++
.../src/__tests__/plugin-executor-service.test.ts | 234 +++++++++
.../runtime/src/__tests__/session-manager.test.ts | 143 +++++-
.../runtime/src/__tests__/subagent-tools.test.ts | 4 +
packages/runtime/src/agent-run.ts | 18 +-
packages/runtime/src/ai-sdk-compaction.ts | 5 +-
packages/runtime/src/history-compaction.ts | 7 +-
packages/runtime/src/plugin-agent-service.ts | 1 +
packages/runtime/src/plugin-executor-backend.ts | 388 +++++++++++++++
packages/runtime/src/plugin-executor-service.ts | 529 +++++++++++++++++++++
packages/runtime/src/runtime-invocation-route.ts | 58 +++
packages/runtime/src/runtime-kernel.ts | 26 +-
packages/runtime/src/session-manager.ts | 176 ++++---
.../runtime/src/stream-graph-schedule-reconcile.ts | 1 +
.../runtime/src/stream-graph-supervisor-tools.ts | 33 +-
packages/runtime/src/subagent-tools.ts | 8 +
packages/runtime/src/tool-runtime.ts | 3 +
.../storage/src/__tests__/session-store.test.ts | 28 ++
packages/storage/src/legacy-run-header.ts | 4 +-
packages/storage/src/session-store.ts | 15 +-
.../storage/src/sqlite-session-metadata-store.ts | 3 +-
54 files changed, 2581 insertions(+), 185 deletions(-)
diff --git a/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts
index 9cb9e8b6ec..0f7cff7040 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts
@@ -62,6 +62,26 @@ test('session creation forwards the caller name for a mode that carries none', a
);
});
+test('session creation forwards a plugin executor without a model target', async () => {
+ const creates: SessionCreateInput[] = [];
+ const ipc = ipcHarness();
+ registerRuntimeHostSessionCatalogIpc(createDeps(creates), ipc as unknown as IpcMain);
+
+ await ipc.invoke('sessions:create', { executorId: 'codex.app-server' });
+
+ assert.equal(creates[0]?.executorId, 'codex.app-server');
+ assert.equal(creates[0]?.modelTarget, undefined);
+ await assert.rejects(
+ ipc.invoke('sessions:create', {
+ executorId: 'codex',
+ llmConnectionId: 'connection-1',
+ llmConnectionSlug: 'openai',
+ model: 'gpt-5',
+ }),
+ /cannot include a model target/,
+ );
+});
+
type IpcHandler = Parameters<Pick<IpcMain, 'handle'>['handle']>[1];
function ipcHarness() {
diff --git a/apps/desktop/src/main/__tests__/session-local.test.ts b/apps/desktop/src/main/__tests__/session-local.test.ts
index 3e5e33ecd2..7e05626ee1 100644
--- a/apps/desktop/src/main/__tests__/session-local.test.ts
+++ b/apps/desktop/src/main/__tests__/session-local.test.ts
@@ -638,6 +638,47 @@ test('attachment retries across restart reuse committed uploads and release stag
assert.deepEqual(db.store.stagedAttachments('authority', 'message-1'), []);
});
+test('local creation preserves a plugin executor in the pending Session projection', async (t) => {
+ const { store, beforeClose } = await database(t);
+ const target: DesktopSessionLocalTarget = {
+ partition: 'authority',
+ profileId: 'profile',
+ scope: { hostId: 'root', targetEpoch: 'target' },
+ };
+ const service = new DesktopSessionLocalService(store, {
+ targets: () => [target],
+ changed() {},
+ onError: (error) => assert.fail(String(error)),
+ });
+ beforeClose.push(() => service.close());
+ type Ipc = Parameters<typeof registerDesktopSessionLocalIpc>[0]['ipcMain'];
+ let create!: Parameters<Ipc['handle']>[1];
+ registerDesktopSessionLocalIpc({
+ ipcMain: {
+ handle: (channel, handler) => {
+ if (channel === 'session-local:create') create = handler;
+ },
+ },
+ service,
+ approvals: createAttachmentApprovalRegistry(),
+ resizeImage: async (bytes) => bytes,
+ resolveWorkspace: async () => ({ kind: 'host_path', path: '/workspace' }),
+ changed() {},
+ });
+
+ const summary = (await create(
+ {} as IpcMainInvokeEvent,
+ target.scope,
+ { executorId: 'codex.app-server' },
+ )) as DesktopSessionSummaryInput;
+ assert.equal(summary.backend, 'plugin-executor');
+ assert.equal(summary.executorId, 'codex.app-server');
+ assert.equal(summary.llmConnectionId, undefined);
+ assert.equal(summary.llmConnectionSlug, 'executor:codex.app-server');
+ assert.equal(summary.model, 'codex.app-server');
+ assert.equal(store.creation(target.partition, summary.id)?.executorId, 'codex.app-server');
+});
+
test('local submit preserves picked-file approvals until durable admission succeeds', async (t) => {
const { store, path, beforeClose } = await database(t);
const file = join(path, '..', 'picked.txt');
diff --git a/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
index a539f2aaef..a221e8fc03 100644
--- a/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
@@ -322,12 +322,21 @@ function normalizeSessionListFilter(value: unknown): SessionListFilter | undefin
export function resolveDesktopSessionCreateInput(input: CreateSessionRequestInput | undefined, sessionId: string, workspace: WorkspaceTarget): SessionCreateInput {
const request = resolveCreateSessionRequest(input);
+ const executorId = normalizeOptionalString(input?.executorId, 'executor id');
+ if (
+ executorId &&
+ (input?.llmConnectionId !== undefined ||
+ input?.llmConnectionSlug !== undefined ||
+ input?.model !== undefined)
+ ) {
+ throw new Error('Plugin executor selection cannot include a model target');
+ }
return {
sessionId, workspace,
...(request.mode === undefined ? {} : { mode: request.mode }),
name: request.name,
...(request.labels === undefined ? {} : { labels: request.labels }),
- modelTarget: normalizeModelTarget(input),
+ ...(executorId ? { executorId } : { modelTarget: normalizeModelTarget(input) }),
...normalizeCreateThinkingLevel(input?.thinkingLevel),
...(request.mode !== undefined || request.permissionMode === undefined ? {} : { permissionMode: request.permissionMode }),
collaborationMode: request.collaborationMode,
diff --git a/apps/desktop/src/main/session-local-service.ts b/apps/desktop/src/main/session-local-service.ts
index 15805d74b6..a8624a04fb 100644
--- a/apps/desktop/src/main/session-local-service.ts
+++ b/apps/desktop/src/main/session-local-service.ts
@@ -537,10 +537,18 @@ export function registerDesktopSessionLocalIpc(deps: {
labels: [...(creation.labels ?? [])],
hasUnread: false,
status: 'active',
- backend: 'ai-sdk',
- llmConnectionSlug: input.llmConnectionSlug ?? '',
- model: input.model ?? '',
- ...(input.llmConnectionId ? { llmConnectionId: input.llmConnectionId } : {}),
+ backend: creation.executorId ? 'plugin-executor' : 'ai-sdk',
+ ...(creation.executorId
+ ? {
+ executorId: creation.executorId,
+ llmConnectionSlug: `executor:${creation.executorId}`,
+ model: creation.executorId,
+ }
+ : {
+ llmConnectionSlug: input.llmConnectionSlug ?? '',
+ model: input.model ?? '',
+ ...(input.llmConnectionId ? { llmConnectionId: input.llmConnectionId } : {}),
+ }),
connectionLocked: false,
permissionMode: creation.permissionMode ?? 'ask',
collaborationMode: creation.collaborationMode,
diff --git a/packages/core/package.json b/packages/core/package.json
index 4a167a083f..331eec147c 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -7,6 +7,7 @@
"sideEffects": false,
"private": true,
"exports": {
+ "./executor-id": "./dist/executor-id.js",
"./durable-tool-result-projection": "./dist/durable-tool-result-projection.js",
"./model-projection-transition": "./dist/model-projection-transition.js",
"./canonical-runtime-event": "./dist/canonical-runtime-event.js",
diff --git a/packages/core/src/__tests__/agent-graph-schedule.test.ts b/packages/core/src/__tests__/agent-graph-schedule.test.ts
index c50b36440f..01d8d6efe2 100644
--- a/packages/core/src/__tests__/agent-graph-schedule.test.ts
+++ b/packages/core/src/__tests__/agent-graph-schedule.test.ts
@@ -34,6 +34,29 @@ describe('agent graph schedule contract', () => {
assert.equal(isAgentGraphScheduleUpdateRequest(request), true);
});
+ test('accepts a plugin executor only on newly created graph targets', () => {
+ const request = scheduleRequest();
+ request.addWork[0]!.target = {
+ kind: 'agent',
+ agentId: 'fact-checker',
+ executorId: 'codex.app-server',
+ };
+ assert.equal(isAgentGraphScheduleUpdateRequest(request), true);
+
+ request.addWork[0]!.target = {
+ kind: 'agent',
+ agentId: 'fact-checker',
+ executorId: 'invalid executor',
+ };
+ assert.equal(isAgentGraphScheduleUpdateRequest(request), false);
+ request.addWork[0]!.target = {
+ kind: 'operator',
+ operatorId: 'existing-operator',
+ executorId: 'codex',
+ } as never;
+ assert.equal(isAgentGraphScheduleUpdateRequest(request), false);
+ });
+
test('rejects ambiguous, duplicate, empty, and add-plus-finish updates', () => {
assert.equal(
isAgentGraphScheduleUpdateRequest({
diff --git a/packages/core/src/__tests__/executor-id.test.ts b/packages/core/src/__tests__/executor-id.test.ts
new file mode 100644
index 0000000000..9abbe94066
--- /dev/null
+++ b/packages/core/src/__tests__/executor-id.test.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 assert from 'node:assert/strict';
+import { test } from 'node:test';
+import { isExecutorId } from '../executor-id.js';
+
+test('executor ids use one bounded canonical grammar', () => {
+ assert.equal(isExecutorId('codex.app-server:v1'), true);
+ assert.equal(isExecutorId(`a${'b'.repeat(127)}`), true);
+ for (const value of ['', '1codex', 'codex/app', `a${'b'.repeat(128)}`, null]) {
+ assert.equal(isExecutorId(value), false);
+ }
+});
diff --git a/packages/core/src/__tests__/runtime-invocation-opened.test.ts b/packages/core/src/__tests__/runtime-invocation-opened.test.ts
index 205b345a3d..910b72ed4b 100644
--- a/packages/core/src/__tests__/runtime-invocation-opened.test.ts
+++ b/packages/core/src/__tests__/runtime-invocation-opened.test.ts
@@ -117,6 +117,28 @@ describe('invocation_opened content contract', () => {
);
});
+ test('binds a plugin executor route to its exact activation generation', () => {
+ const route = {
+ provenance: 'runtime',
+ backendKind: 'plugin-executor',
+ executorId: 'codex.app-server',
+ llmConnectionSlug: 'executor:codex.app-server',
+ modelId: 'codex.app-server',
+ providerStateIdentity: DIGEST,
+ } as const;
+ assert.deepEqual(decodeRuntimeInvocationOpened(opening({ route })).route, route);
+ assert.throws(() =>
+ decodeRuntimeInvocationOpened(
+ opening({ route: { ...route, providerStateIdentity: undefined } as never }),
+ ),
+ );
+ assert.throws(() =>
+ decodeRuntimeInvocationOpened(
+ opening({ route: { ...route, llmConnectionId: 'not-an-executor-route' } as never }),
+ ),
+ );
+ });
+
test('accepts every root authority the runtime can open, and no mixture of them', () => {
for (const root of [
{ kind: 'user' },
diff --git a/packages/core/src/__tests__/session-send-projection.test.ts b/packages/core/src/__tests__/session-send-projection.test.ts
index d450ff8d0c..45527b69b5 100644
--- a/packages/core/src/__tests__/session-send-projection.test.ts
+++ b/packages/core/src/__tests__/session-send-projection.test.ts
@@ -62,6 +62,23 @@ describe('projectSessionSendOutcome — exact Connection identity', () => {
assert.deepEqual(projectSessionSendOutcome(input()), { kind: 'ready' });
});
+ it('does not require a Maka model connection for a plugin executor Session', () => {
+ assert.deepEqual(
+ projectSessionSendOutcome(
+ input({
+ session: {
+ backend: 'plugin-executor',
+ llmConnectionSlug: 'executor:codex',
+ model: 'codex',
+ connectionLocked: false,
+ },
+ connections: [],
+ }),
+ ),
+ { kind: 'ready' },
+ );
+ });
+
it('blocks a legacy Session until the user explicitly selects an account', () => {
const current = input();
assert.deepEqual(
diff --git a/packages/core/src/agent-graph-schedule.ts b/packages/core/src/agent-graph-schedule.ts
index a41849a58f..2e60348949 100644
--- a/packages/core/src/agent-graph-schedule.ts
+++ b/packages/core/src/agent-graph-schedule.ts
@@ -24,6 +24,7 @@ import type {
} from './agent-graph-control.js';
import type { AgentGraphTopologyStore } from './agent-graph-topology.js';
import type { OrchestrationMode } from './orchestration.js';
+import { isExecutorId } from './executor-id.js';
export const AGENT_GRAPH_SCHEDULE_UPDATE_SCHEMA_VERSION = 1 as const;
@@ -48,10 +49,12 @@ export type AgentGraphWorkTarget =
| {
kind: 'agent';
agentId: string;
+ executorId?: string;
}
| {
kind: 'preset';
presetId: string;
+ executorId?: string;
}
| {
kind: 'operator';
@@ -346,16 +349,26 @@ function isSelectedResultInput(value: unknown): value is AgentGraphSelectedResul
function isWorkTarget(value: unknown): value is AgentGraphWorkTarget {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
if (
- isExactRecord(value, ['kind', 'agentId']) &&
+ isExactRecord(value, [
+ 'kind',
+ 'agentId',
+ ...(hasOwn(value, 'executorId') ? ['executorId'] : []),
+ ]) &&
value.kind === 'agent' &&
- isOpaqueIdentity(value.agentId)
+ isOpaqueIdentity(value.agentId) &&
+ (value.executorId === undefined || isExecutorId(value.executorId))
) {
return true;
}
if (
- isExactRecord(value, ['kind', 'presetId']) &&
+ isExactRecord(value, [
+ 'kind',
+ 'presetId',
+ ...(hasOwn(value, 'executorId') ? ['executorId'] : []),
+ ]) &&
value.kind === 'preset' &&
- isOpaqueIdentity(value.presetId)
+ isOpaqueIdentity(value.presetId) &&
+ (value.executorId === undefined || isExecutorId(value.executorId))
) {
return true;
}
diff --git a/packages/core/src/executor-id.ts b/packages/core/src/executor-id.ts
new file mode 100644
index 0000000000..db59c2ca3a
--- /dev/null
+++ b/packages/core/src/executor-id.ts
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+export const EXECUTOR_ID_PATTERN = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/u;
+
+export function isExecutorId(value: unknown): value is string {
+ return typeof value === 'string' && EXECUTOR_ID_PATTERN.test(value);
+}
diff --git a/packages/core/src/runtime-event.ts b/packages/core/src/runtime-event.ts
index 635b5257fe..77643ee0c4 100644
--- a/packages/core/src/runtime-event.ts
+++ b/packages/core/src/runtime-event.ts
@@ -70,6 +70,7 @@ import {
type OrchestrationMode,
} from './orchestration.js';
import { isToolMode, type ToolMode } from './tool-mode.js';
+import { isExecutorId } from './executor-id.js';
import {
isRuntimeSystemNoteKind,
type PersistedBackendKind,
@@ -267,13 +268,22 @@ export interface RuntimeEventErrorContent {
export type RuntimeInvocationRoute =
| {
provenance: 'runtime';
- backendKind: PersistedBackendKind;
+ backendKind: Exclude<PersistedBackendKind, 'plugin-executor'>;
llmConnectionId: string;
llmConnectionSlug: string;
modelId: string;
/** Frozen provider endpoint and credential ownership; absent on non-provider runs. */
providerStateIdentity?: `sha256:${string}`;
}
+ | {
+ provenance: 'runtime';
+ backendKind: 'plugin-executor';
+ executorId: string;
+ llmConnectionSlug: string;
+ modelId: string;
+ /** Frozen plugin package, entry point, and activation generation. */
+ providerStateIdentity: `sha256:${string}`;
+ }
| {
provenance: 'unknown';
backendKind: PersistedBackendKind;
@@ -760,12 +770,25 @@ const INVOCATION_OPENED_CONTENT_SHAPE = defineObjectShape<RuntimeEventInvocation
['kind', 'protocol', 'route', 'configuration', 'root', 'source'],
['lineage'],
);
-const INVOCATION_ROUTE_RUNTIME_SHAPE = defineObjectShape<
- Extract<RuntimeInvocationRoute, { provenance: 'runtime' }>
+const INVOCATION_ROUTE_RUNTIME_MODEL_SHAPE = defineObjectShape<
+ Extract<RuntimeInvocationRoute, { provenance: 'runtime'; llmConnectionId: string }>
>()(
['provenance', 'backendKind', 'llmConnectionId', 'llmConnectionSlug', 'modelId'],
['providerStateIdentity'],
);
+const INVOCATION_ROUTE_RUNTIME_EXECUTOR_SHAPE = defineObjectShape<
+ Extract<RuntimeInvocationRoute, { provenance: 'runtime'; backendKind: 'plugin-executor' }>
+>()(
+ [
+ 'provenance',
+ 'backendKind',
+ 'executorId',
+ 'llmConnectionSlug',
+ 'modelId',
+ 'providerStateIdentity',
+ ],
+ [],
+);
const INVOCATION_ROUTE_UNKNOWN_SHAPE = defineObjectShape<
Extract<RuntimeInvocationRoute, { provenance: 'unknown' }>
>()(['provenance', 'backendKind', 'llmConnectionSlug', 'modelId'], []);
@@ -1157,8 +1180,15 @@ function isRuntimeInvocationRoute(value: unknown): value is RuntimeInvocationRou
return false;
}
if (value.provenance === 'runtime') {
+ if (value.backendKind === 'plugin-executor') {
+ return (
+ hasExactShape(value, INVOCATION_ROUTE_RUNTIME_EXECUTOR_SHAPE) &&
+ isExecutorId(value.executorId) &&
+ isSha256Digest(value.providerStateIdentity)
+ );
+ }
return (
- hasExactShape(value, INVOCATION_ROUTE_RUNTIME_SHAPE) &&
+ hasExactShape(value, INVOCATION_ROUTE_RUNTIME_MODEL_SHAPE) &&
isNonEmptyString(value.llmConnectionId) &&
(value.providerStateIdentity === undefined || isSha256Digest(value.providerStateIdentity))
);
@@ -1167,7 +1197,7 @@ function isRuntimeInvocationRoute(value: unknown): value is RuntimeInvocationRou
}
function isPersistedBackendKind(value: unknown): value is PersistedBackendKind {
- return value === 'ai-sdk' || value === 'fake';
+ return value === 'ai-sdk' || value === 'plugin-executor' || value === 'fake';
}
function isRuntimeInvocationConfiguration(value: unknown): value is RuntimeInvocationConfiguration {
diff --git a/packages/core/src/runtime-inputs.ts b/packages/core/src/runtime-inputs.ts
index 42bccd337a..caecc3ae68 100644
--- a/packages/core/src/runtime-inputs.ts
+++ b/packages/core/src/runtime-inputs.ts
@@ -53,14 +53,8 @@ export interface CreateSessionInput {
projectId?: string | null;
/** If omitted, runtime auto-derives a placeholder; users may rename later. */
name?: string;
- /**
- * No `backend`: a live build has exactly one, so the field carried no choice
- * — only the chance of writing the retired `'fake'` into a new row (#3211).
- * The store stamps every new header instead. Sessions derived from an older
- * one (branch, revision, subagent) no longer inherit its backend; a copy of a
- * legacy row is a real session whose connection slug resolves to nothing,
- * which is what the readiness projection already says about it.
- */
+ /** Named plugin executor. When present, model fields are retained only as display placeholders. */
+ executorId?: string;
/** Immutable Connection entity identity. Omitted only while copying legacy state. */
llmConnectionId?: string;
llmConnectionSlug: string;
diff --git a/packages/core/src/session-send-projection.ts b/packages/core/src/session-send-projection.ts
index 83648d8270..dfe52ed341 100644
--- a/packages/core/src/session-send-projection.ts
+++ b/packages/core/src/session-send-projection.ts
@@ -27,11 +27,12 @@
* whether that exact target looks usable for presentation and readiness checks.
*
* The compatibility rules are:
- * 1. The session's own connection must pass `isConnectionReady` with
+ * 1. Plugin executor Sessions do not depend on Maka model connections.
+ * 2. The session's own connection must pass `isConnectionReady` with
* the sticky session model.
- * 2. Legacy Sessions without an immutable connection id are blocked until
+ * 3. Legacy Sessions without an immutable connection id are blocked until
* the user explicitly selects an account.
- * 3. A missing id, slug mismatch, or unusable exact connection is blocked.
+ * 4. A missing id, slug mismatch, or unusable exact connection is blocked.
*
* `lastTestStatus` deliberately plays no part here (E4): telemetry about
* a past credential test must not gate send, so it must not gate the
@@ -46,8 +47,8 @@ export interface SessionSendProjectionSession {
/**
* Session backend kind. `string` (not `PersistedBackendKind`) so legacy
* on-disk values like `'claude'` are surfaced exactly as the JSONL stored
- * them; only `'fake'` is special-cased, everything else goes through the
- * normal connection readiness gate.
+ * them; known backends are handled explicitly and unknown legacy values go
+ * through the normal connection readiness gate.
*/
backend: string;
llmConnectionId?: string;
@@ -127,6 +128,7 @@ function ownConnectionBlockReason(
| 'legacy_connection_identity'
| 'connection_identity_mismatch'
| undefined {
+ if (session.backend === 'plugin-executor') return undefined;
if (session.backend === 'fake') return 'fake_backend';
if (!session.llmConnectionId) return 'legacy_connection_identity';
const identified =
diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts
index 76bcc27a4c..1ced6886bd 100644
--- a/packages/core/src/session.ts
+++ b/packages/core/src/session.ts
@@ -18,6 +18,7 @@
*/
import { isWorkHubActionReceipt, type WorkHubActionReceipt } from './workhub-action-result.js';
+import { isExecutorId } from './executor-id.js';
import {
MODEL_FAILURE_MESSAGE_MAX_BYTES,
@@ -297,6 +298,8 @@ export interface SessionHeader {
// Backend / model config
backend: PersistedBackendKind;
+ /** Named black-box executor contributed by a plugin. Present exactly for plugin-executor. */
+ executorId?: string;
/** Immutable Connection entity identity. Optional only on legacy Session records. */
llmConnectionId?: string;
llmConnectionSlug: string;
@@ -344,7 +347,7 @@ export function isWorkHubCoordinationSessionTarget(
* shipped build may choose it, so it is not a member here. Values read back
* from durable state use {@link PersistedBackendKind} instead.
*/
-export type BackendKind = 'ai-sdk';
+export type BackendKind = 'ai-sdk' | 'plugin-executor';
/**
* The backend value a persisted record may carry.
@@ -408,6 +411,7 @@ export interface SessionSummary {
revisionIndex?: number;
revisionState?: 'preparing' | 'committed';
backend: PersistedBackendKind;
+ executorId?: string;
/** Immutable Connection entity identity. Optional only on legacy summaries. */
llmConnectionId?: string;
llmConnectionSlug: string;
@@ -945,6 +949,8 @@ export type WorkHubDelegationWorkspace =
/** User-selected creation defaults; never applied to an existing Work. */
export interface WorkHubCreateDefaults {
+ /** Named plugin executor for the new Session. Mutually exclusive with model. */
+ readonly executorId?: string;
readonly model?: {
readonly llmConnectionId: string;
readonly llmConnectionSlug: string;
@@ -956,10 +962,14 @@ export interface WorkHubCreateDefaults {
export function isWorkHubCreateDefaults(value: unknown): value is WorkHubCreateDefaults {
if (
!isRecord(value) ||
- Object.keys(value).some((key) => key !== 'model' && key !== 'permissionMode')
+ Object.keys(value).some(
+ (key) => key !== 'executorId' && key !== 'model' && key !== 'permissionMode',
+ )
)
return false;
if (value.permissionMode !== undefined && !isPermissionMode(value.permissionMode)) return false;
+ if (value.executorId !== undefined && !isExecutorId(value.executorId)) return false;
+ if (value.executorId !== undefined && value.model !== undefined) return false;
if (value.model === undefined) return true;
const model = value.model;
return (
diff --git a/packages/eval/src/harbor-maka-subject.ts b/packages/eval/src/harbor-maka-subject.ts
index fbd82b2b2c..38981900b1 100644
--- a/packages/eval/src/harbor-maka-subject.ts
+++ b/packages/eval/src/harbor-maka-subject.ts
@@ -19,8 +19,7 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
-import { runHostedExecution } from '@maka/runtime-host/client';
-import type { HostedExecutionStartInput } from '@maka/runtime-host/protocol';
+import { runHostedExecution, type RunHostedExecutionInput } from '@maka/runtime-host/client';
import { captureMakaRuntimeArtifacts, writeMakaArtifactCollectionError } from './maka-artifacts.js';
import { makaEvalRuntimePolicyDocument } from './maka-runtime-policy.js';
import { takeRelayResultToken, writeRelayResult } from './relay-result-frame.js';
@@ -32,7 +31,7 @@ const payload = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64url').toStr
artifactRoot: string;
baseUrl: string;
hostSettlementTimeoutMs: number;
- execution: HostedExecutionStartInput;
+ execution: RunHostedExecutionInput['execution'];
};
const abort = new AbortController();
let artifactCapture = Promise.resolve();
diff --git a/packages/runtime-host/src/__tests__/host-session-availability.test.ts b/packages/runtime-host/src/__tests__/host-session-availability.test.ts
index bed59d36f9..5bf3190cd0 100644
--- a/packages/runtime-host/src/__tests__/host-session-availability.test.ts
+++ b/packages/runtime-host/src/__tests__/host-session-availability.test.ts
@@ -25,8 +25,11 @@ import {
} from '@maka/core/session';
import {
runtimeHostExecutionUnavailableReason,
+ runtimeHostConversationCopyUnavailableReason,
runtimeHostSafeBoundaryContinuationUnavailableReason,
LEGACY_CONNECTION_IDENTITY_EXECUTION_UNAVAILABLE_REASON,
+ PLUGIN_EXECUTOR_CONTINUATION_UNAVAILABLE_REASON,
+ PLUGIN_EXECUTOR_COPY_UNAVAILABLE_REASON,
WORKHUB_COORDINATION_EXECUTION_UNAVAILABLE_REASON,
WORKHUB_COORDINATION_TARGET_UNAVAILABLE_REASON,
} from '../server/host-session-availability.js';
@@ -140,6 +143,44 @@ test('legacy Session identity cannot enter Host execution before explicit accoun
);
});
+test('plugin executor Sessions do not require a Maka model connection identity', () => {
+ assert.equal(
+ runtimeHostExecutionUnavailableReason(
+ {
+ ...base,
+ id: 'plugin-executor-session',
+ role: undefined,
+ llmConnectionId: undefined,
+ backend: 'plugin-executor',
+ },
+ { kind: 'external_message' },
+ ),
+ undefined,
+ );
+});
+
+test('plugin executor Sessions fail closed for safe-boundary continuation', () => {
+ assert.equal(
+ runtimeHostSafeBoundaryContinuationUnavailableReason({
+ ...base,
+ id: 'plugin-executor-session',
+ role: undefined,
+ subagentParent: undefined,
+ llmConnectionId: undefined,
+ backend: 'plugin-executor',
+ }),
+ PLUGIN_EXECUTOR_CONTINUATION_UNAVAILABLE_REASON,
+ );
+});
+
+test('plugin executor Sessions fail closed for branch and revision copies', () => {
+ assert.equal(
+ runtimeHostConversationCopyUnavailableReason({ backend: 'plugin-executor' }),
+ PLUGIN_EXECUTOR_COPY_UNAVAILABLE_REASON,
+ );
+ assert.equal(runtimeHostConversationCopyUnavailableReason({ backend: 'ai-sdk' }), undefined);
+});
+
test('legacy Session identity cannot resume a safe-boundary continuation', () => {
assert.equal(
runtimeHostSafeBoundaryContinuationUnavailableReason({
diff --git a/packages/runtime-host/src/__tests__/plugin-platform.test.ts b/packages/runtime-host/src/__tests__/plugin-platform.test.ts
index c250197926..b93bcd1a0c 100644
--- a/packages/runtime-host/src/__tests__/plugin-platform.test.ts
+++ b/packages/runtime-host/src/__tests__/plugin-platform.test.ts
@@ -93,6 +93,7 @@ function createPlatform(
...(options.tools ? { tools: options.tools } : {}),
...(options.systemPrompt ? { systemPrompt: options.systemPrompt } : {}),
...(options.commands ? { commands: options.commands } : {}),
+ ...(options.executors ? { executors: options.executors } : {}),
});
testPlatformInternals.set(platform, { composition, packages, store });
return platform;
@@ -696,6 +697,57 @@ test('Plugin Platform query projects scoped Command contributions for clients',
}
});
+test('Plugin Platform query projects scoped Executor contributions for clients', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'maka-plugin-executor-inspection-'));
+ try {
+ const platform = createPlatform(join(root, 'control'), {
+ executors: {
+ inspect: () => [
+ {
+ entryId: 'executor-entry',
+ scopeId: 'profile',
+ extensionId: 'executor-package',
+ generation: 4,
+ id: 'codex',
+ displayName: 'Codex',
+ capabilities: { thinking: true, toolActivity: false },
+ },
+ ],
+ },
+ });
+ const coordinator = new HostPluginPlatformCoordinator(platform);
+ await platform.recover();
+
+ assert.deepEqual(
+ await coordinator.handlers['plugin.platform.query'](
+ { view: 'executors', rootId: 'profile' },
+ null as never,
+ ),
+ {
+ ok: true,
+ result: {
+ view: 'executors',
+ items: [
+ {
+ entryId: 'executor-entry',
+ scopeId: 'profile',
+ extensionId: 'executor-package',
+ generation: 4,
+ id: 'codex',
+ displayName: 'Codex',
+ capabilities: { thinking: true, toolActivity: false },
+ },
+ ],
+ nextCursor: null,
+ },
+ },
+ );
+ await platform.close();
+ } finally {
+ await rm(root, { recursive: true, force: true });
+ }
+});
+
test('Plugin Platform query pages reserve a cursor even when the complete final result fits', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-plugin-query-budget-'));
try {
diff --git a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts
index 4e4dfd44b3..409d09698e 100644
--- a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts
@@ -747,6 +747,84 @@ test('creation on a relay connection honours declared levels via the catalog pro
assert.equal(persistedConnectionId, 'connection-1');
});
+test('plugin executor creation bypasses model resolution and persists the executor route', async () => {
+ let persistedInput: Parameters<CatalogStores['createStableSession']>[0]['input'] | undefined;
+ const externalHeader = (sessionId: string): SessionHeader => {
+ const { llmConnectionId: _connectionId, ...base } = sessionHeader(sessionId, ['user-label']);
+ return {
+ ...base,
+ backend: 'plugin-executor',
+ executorId: 'codex',
+ llmConnectionSlug: 'executor:codex',
+ model: 'codex',
+ };
+ };
+ const fixture = createFixture({
+ connection: {
+ onResolve: () => assert.fail('Plugin executor creation must not resolve a Maka model'),
+ },
+ stores: {
+ createStableSession: async (args) => {
+ persistedInput = args.input;
+ return {
+ kind: 'existing' as const,
+ record: headerSnapshot(externalHeader(args.sessionId), 1),
+ };
+ },
+ readCatalogRecord: async (sessionId) => catalogRecord(externalHeader(sessionId), 1),
+ },
+ });
+
+ const outcome = await fixture.coordinator.handlers['session.create'](
+ {
+ sessionId: fixture.sessionId,
+ workspace: { kind: 'host_path', path: process.cwd() },
+ executorId: 'codex',
+ },
+ context,
+ );
+
+ assert.equal(outcome.ok, true, JSON.stringify(outcome));
+ assert.equal(persistedInput?.executorId, 'codex');
+ assert.equal(persistedInput?.llmConnectionId, undefined);
+ assert.equal(persistedInput?.llmConnectionSlug, 'executor:codex');
+ assert.equal(persistedInput?.model, 'codex');
+ if (outcome.ok && !('kind' in outcome.result)) {
+ assert.equal(outcome.result.backend, 'plugin-executor');
+ assert.equal(outcome.result.executorId, 'codex');
+ }
+});
+
+test('plugin executor creation fails before persistence when the executor is unavailable', async () => {
+ let createAttempts = 0;
+ const fixture = createFixture({
+ assertExecutorAvailable: () => {
+ throw new Error('not installed');
+ },
+ stores: {
+ createStableSession: async () => {
+ createAttempts += 1;
+ throw new Error('must not persist');
+ },
+ },
+ });
+
+ const outcome = await fixture.coordinator.handlers['session.create'](
+ {
+ sessionId: fixture.sessionId,
+ workspace: { kind: 'host_path', path: process.cwd() },
+ executorId: 'missing',
+ },
+ context,
+ );
+
+ assert.deepEqual(outcome, {
+ ok: false,
+ error: { code: 'operation_unavailable', message: 'Plugin executor is unavailable: missing' },
+ });
+ assert.equal(createAttempts, 0);
+});
+
test('creation admits the enabled bootstrap DeepSeek model before discovery', async () => {
const modelId = 'deepseek-v4-flash';
let createAttempts = 0;
@@ -1895,6 +1973,7 @@ function createFixture(
readonly onProjectChanged?: () => void;
readonly legacyConnectionIdentity?: boolean;
readonly header?: Partial<SessionHeader>;
+ readonly assertExecutorAvailable?: (sessionId: string, executorId: string) => void;
} = {},
) {
const sessionId = 'session-1';
@@ -1984,6 +2063,9 @@ function createFixture(
requestDrain: () => {
drains += 1;
},
+ ...(options.assertExecutorAvailable
+ ? { assertExecutorAvailable: options.assertExecutorAvailable }
+ : {}),
});
return {
coordinator,
diff --git a/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts b/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts
index c70ebb66ce..f1c05b5e1b 100644
--- a/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts
@@ -269,6 +269,50 @@ describe('Session catalog protocol', () => {
},
);
+ assert.deepEqual(
+ decodeClientFrame({
+ requestId: 'request-executor',
+ operation: 'session.create',
+ input: {
+ sessionId: 'session-executor',
+ workspace: { kind: 'project', projectId: 'project-1' },
+ executorId: 'codex.app-server',
+ },
+ }),
+ {
+ requestId: 'request-executor',
+ operation: 'session.create',
+ input: {
+ sessionId: 'session-executor',
+ workspace: { kind: 'project', projectId: 'project-1' },
+ executorId: 'codex.app-server',
+ },
+ },
+ );
+
+ for (const input of [
+ {
+ sessionId: 'session-missing-route',
+ workspace: { kind: 'project', projectId: 'project-1' },
+ },
+ {
+ sessionId: 'session-ambiguous-route',
+ workspace: { kind: 'project', projectId: 'project-1' },
+ executorId: 'codex',
+ modelTarget: { kind: 'default' },
+ },
+ ]) {
+ assert.throws(
+ () =>
+ decodeClientFrame({
+ requestId: 'request-invalid-executor',
+ operation: 'session.create',
+ input,
+ }),
+ isProtocolError,
+ );
+ }
+
assert.deepEqual(
decodeClientFrame({
requestId: 'request-3',
diff --git a/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts b/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts
index 79863e3b81..eafaffab63 100644
--- a/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts
@@ -106,6 +106,30 @@ test('WorkHub model actions cannot supply user authority or attachment locators'
assert.equal(REMOTE_OWNER_OPERATION_GRANTS.includes('workhub.coordination.actFromTurn'), true);
});
+test('WorkHub new Sessions accept a plugin executor as their creation default', () => {
+ const input = {
+ turnId: 'active-model-turn',
+ actionId: 'tool-call-external',
+ proposal: { disposition: 'create_new', title: 'External audit' },
+ delegationText: 'Inspect the login retries',
+ create: { workspace: { kind: 'project', projectId: 'maka' } },
+ newWorkDefaults: { executorId: 'codex.app-server', permissionMode: 'ask' },
+ };
+ assert.deepEqual(decodeWorkHubCoordinationActFromTurnInput(input), input);
+ for (const newWorkDefaults of [
+ {
+ executorId: 'codex',
+ model: { llmConnectionId: 'conn', llmConnectionSlug: 'test', model: 'model' },
+ },
+ { executorId: 'invalid executor' },
+ ]) {
+ assert.throws(
+ () => decodeWorkHubCoordinationActFromTurnInput({ ...input, newWorkDefaults }),
+ RuntimeHostProtocolError,
+ );
+ }
+});
+
test('delegation content is optional, bounded, and unavailable to stop or resume', () => {
const input = {
actionId: 'delegate-content',
diff --git a/packages/runtime-host/src/client/session-catalog-summary.ts b/packages/runtime-host/src/client/session-catalog-summary.ts
index 10d50521db..49752c1394 100644
--- a/packages/runtime-host/src/client/session-catalog-summary.ts
+++ b/packages/runtime-host/src/client/session-catalog-summary.ts
@@ -60,6 +60,7 @@ export function projectSessionCatalogSummary(
...(session.revisionIndex === undefined ? {} : { revisionIndex: session.revisionIndex }),
...(session.revisionState === undefined ? {} : { revisionState: session.revisionState }),
backend: session.backend,
+ ...(session.executorId ? { executorId: session.executorId } : {}),
...(session.llmConnectionId === null ? {} : { llmConnectionId: session.llmConnectionId }),
llmConnectionSlug: session.llmConnectionSlug,
connectionLocked: session.connectionLocked,
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index 3372e7bd30..e2c277cc06 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -101,7 +101,8 @@ 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 = 152 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 153 as const;
+// 153: Sessions may select plugin executors and Plugin Platform queries expose them.
// 152: Assistant completions and transcript rows preserve interrupted responses.
// 151: WorkHub selects and delegates through a durable Host Form interaction.
// 150: Message admission accepts an empty-text Message that carries a quote or
diff --git a/packages/runtime-host/src/protocol/plugin-platform.ts b/packages/runtime-host/src/protocol/plugin-platform.ts
index cf6914ff8b..1215195fd1 100644
--- a/packages/runtime-host/src/protocol/plugin-platform.ts
+++ b/packages/runtime-host/src/protocol/plugin-platform.ts
@@ -28,6 +28,7 @@ import {
} from '@maka/runtime/plugin-runtime';
import type { PluginToolInspection } from '@maka/runtime/plugin-tool-service';
import type { PluginCommandInspection } from '@maka/runtime/plugin-command-service';
+import type { PluginExecutorInspection } from '@maka/runtime/plugin-executor-service';
import {
requireCount,
requireEncodedByteLimit,
@@ -88,7 +89,14 @@ export interface PluginPackageProjection {
}
export interface PluginPlatformQueryInput {
- readonly view: 'status' | 'packages' | 'entries' | 'tools' | 'commands' | 'failures';
+ readonly view:
+ | 'status'
+ | 'packages'
+ | 'entries'
+ | 'tools'
+ | 'commands'
+ | 'executors'
+ | 'failures';
readonly rootId?: MakaPluginRootId;
readonly cursor?: string;
readonly limit?: number;
@@ -127,6 +135,11 @@ export type PluginPlatformQueryResult =
readonly items: readonly PluginCommandInspection[];
readonly nextCursor: string | null;
}
+ | {
+ readonly view: 'executors';
+ readonly items: readonly PluginExecutorInspection[];
+ readonly nextCursor: string | null;
+ }
| {
readonly view: 'failures';
readonly items: readonly PluginPlatformFailureProjection[];
@@ -268,7 +281,7 @@ function decodePluginPlatformQueryInput(value: unknown): PluginPlatformQueryInpu
['rootId', 'cursor', 'limit'],
);
if (
- !['status', 'packages', 'entries', 'tools', 'commands', 'failures'].includes(
+ !['status', 'packages', 'entries', 'tools', 'commands', 'executors', 'failures'].includes(
input.view as string,
)
) {
@@ -292,9 +305,15 @@ function decodePluginPlatformQueryInput(value: unknown): PluginPlatformQueryInpu
) {
throw invalidProtocolFrame('Plugin Platform status query does not accept paging');
}
- if (input.rootId !== undefined && view !== 'entries' && view !== 'tools' && view !== 'commands') {
+ if (
+ input.rootId !== undefined &&
+ view !== 'entries' &&
+ view !== 'tools' &&
+ view !== 'commands' &&
+ view !== 'executors'
+ ) {
throw invalidProtocolFrame(
- 'Plugin root identity is only valid for Entry, Tool, and Command queries',
+ 'Plugin root identity is only valid for Entry, Tool, Command, and Executor queries',
);
}
let rootId: MakaPluginRootId | undefined;
@@ -367,7 +386,9 @@ function decodePluginPlatformQueryResult(value: unknown): PluginPlatformQueryRes
if (
!Array.isArray(output.items) ||
output.items.length > 64 ||
- !['packages', 'entries', 'tools', 'commands', 'failures'].includes(view as string)
+ !['packages', 'entries', 'tools', 'commands', 'executors', 'failures'].includes(
+ view as string,
+ )
) {
throw invalidProtocolFrame('Invalid Plugin Platform page');
}
@@ -384,7 +405,9 @@ function decodePluginPlatformQueryResult(value: unknown): PluginPlatformQueryRes
? { view, items: output.items.map(decodeToolInspection), nextCursor }
: view === 'commands'
? { view, items: output.items.map(decodeCommandInspection), nextCursor }
- : { view: 'failures', items: output.items.map(decodePlatformFailure), nextCursor };
+ : view === 'executors'
+ ? { view, items: output.items.map(decodeExecutorInspection), nextCursor }
+ : { view: 'failures', items: output.items.map(decodePlatformFailure), nextCursor };
}
requireEncodedByteLimit(
decoded,
@@ -418,6 +441,40 @@ function decodeCommandInspection(value: unknown): PluginCommandInspection {
};
}
+function decodeExecutorInspection(value: unknown): PluginExecutorInspection {
+ const item = requireExactRecord(value, 'Plugin Executor inspection', [
+ 'entryId',
+ 'scopeId',
+ 'extensionId',
+ 'generation',
+ 'id',
+ 'displayName',
+ 'capabilities',
+ ]);
+ const capabilities = requireExactRecord(item.capabilities, 'Plugin Executor capabilities', [
+ 'thinking',
+ 'toolActivity',
+ ]);
+ if (
+ typeof capabilities.thinking !== 'boolean' ||
+ typeof capabilities.toolActivity !== 'boolean'
+ ) {
+ throw invalidProtocolFrame('Invalid Plugin Executor capabilities');
+ }
+ return {
+ entryId: requireId(item.entryId, 'Plugin Entry identity'),
+ scopeId: requireString(item.scopeId, 'Plugin scope identity', 256),
+ extensionId: requireId(item.extensionId, 'Plugin package identity'),
+ generation: requireCount(item.generation, 'Plugin generation'),
+ id: requireString(item.id, 'Plugin Executor id', 128),
+ displayName: requireString(item.displayName, 'Plugin Executor display name', 256),
+ capabilities: {
+ thinking: capabilities.thinking,
+ toolActivity: capabilities.toolActivity,
+ },
+ };
+}
+
function decodePlatformFailure(value: unknown): PluginPlatformFailureProjection {
const failure = requireShapedRecord(
value,
diff --git a/packages/runtime-host/src/protocol/session-catalog.ts b/packages/runtime-host/src/protocol/session-catalog.ts
index 0820a6b693..eb65be7e54 100644
--- a/packages/runtime-host/src/protocol/session-catalog.ts
+++ b/packages/runtime-host/src/protocol/session-catalog.ts
@@ -31,6 +31,7 @@ import {
type SessionToolProfile,
} from '@maka/core/session';
import { isThinkingLevel, type ThinkingLevel } from '@maka/core/model-thinking';
+import { isExecutorId } from '@maka/core/executor-id';
import type { ExecutionBoundarySummary } from '@maka/core/sandbox-boundary';
export type { ExecutionBoundarySummary } from '@maka/core/sandbox-boundary';
import {
@@ -123,6 +124,7 @@ const PROJECTION_FIELDS = [
'revisionOfTurnId',
'revisionIndex',
'revisionState',
+ 'executorId',
'thinkingLevel',
'lastReadMessageId',
'liveRunState',
@@ -154,7 +156,10 @@ export interface SessionCreateInput {
readonly mode?: SessionStartMode;
readonly name?: string;
readonly labels?: readonly string[];
- readonly modelTarget: SessionModelTarget;
+ /** Required for native execution and omitted for a plugin executor. */
+ readonly modelTarget?: SessionModelTarget;
+ /** Named black-box executor contributed by a Host plugin. */
+ readonly executorId?: string;
readonly thinkingLevel?: ThinkingLevel;
readonly toolProfile?: SessionToolProfile;
readonly permissionMode?: PermissionMode;
@@ -236,6 +241,7 @@ export interface SessionCatalogProjection {
readonly revisionIndex?: number;
readonly revisionState?: 'preparing' | 'committed';
readonly backend: PersistedBackendKind;
+ readonly executorId?: string;
readonly llmConnectionId: string | null;
readonly llmConnectionSlug: string;
readonly connectionLocked: boolean;
@@ -513,11 +519,13 @@ export function decodeSessionCreateInput(value: unknown): SessionCreateInput {
const input = requireShapedRecord(
value,
'Session create input',
- ['sessionId', 'workspace', 'modelTarget'],
+ ['sessionId', 'workspace'],
[
'mode',
'name',
'labels',
+ 'modelTarget',
+ 'executorId',
'thinkingLevel',
'toolProfile',
'permissionMode',
@@ -525,13 +533,21 @@ export function decodeSessionCreateInput(value: unknown): SessionCreateInput {
'orchestrationMode',
],
);
+ const executorId = Object.hasOwn(input, 'executorId')
+ ? executorIdValue(input.executorId)
+ : undefined;
+ const target = Object.hasOwn(input, 'modelTarget') ? modelTarget(input.modelTarget) : undefined;
+ if ((executorId === undefined) === (target === undefined)) {
+ throw invalidProtocolFrame('Session creation requires exactly one model target or executor id');
+ }
return {
sessionId: requireEntityId(input.sessionId, 'sessionId'),
workspace: decodeWorkspaceTarget(input.workspace),
...(Object.hasOwn(input, 'mode') ? { mode: sessionStartMode(input.mode) } : {}),
...(Object.hasOwn(input, 'name') ? { name: sessionName(input.name) } : {}),
...(Object.hasOwn(input, 'labels') ? { labels: labels(input.labels) } : {}),
- modelTarget: modelTarget(input.modelTarget),
+ ...(target ? { modelTarget: target } : {}),
+ ...(executorId ? { executorId } : {}),
...(Object.hasOwn(input, 'thinkingLevel')
? { thinkingLevel: thinkingLevel(input.thinkingLevel) }
: {}),
@@ -760,6 +776,7 @@ export function decodeSessionCatalogProjection(value: unknown): SessionCatalogPr
...optionalRevisionIndex(record),
...optionalRevisionState(record),
backend: backend(record.backend),
+ ...optionalExecutorId(record),
llmConnectionId:
record.llmConnectionId === null
? null
@@ -776,6 +793,9 @@ export function decodeSessionCatalogProjection(value: unknown): SessionCatalogPr
collaborationMode: collaborationMode(record.collaborationMode),
orchestrationMode: orchestrationMode(record.orchestrationMode),
};
+ if ((projection.backend === 'plugin-executor') !== (projection.executorId !== undefined)) {
+ throw invalidProtocolFrame('Session executor identity does not match its backend');
+ }
requireEncodedByteLimit(
projection,
'Session catalog projection',
@@ -997,12 +1017,27 @@ function optionalThinkingLevel(
// header's durable backend, and rows written by builds that shipped
// FakeBackend still hold it (#3211).
function backend(value: unknown): SessionCatalogProjection['backend'] {
- if (value !== 'ai-sdk' && value !== 'fake') {
+ if (value !== 'ai-sdk' && value !== 'plugin-executor' && value !== 'fake') {
throw invalidProtocolFrame('Invalid Session backend');
}
return value;
}
+function executorIdValue(value: unknown): string {
+ const id = requireUtf8String(value, 'Executor id', 128);
+ if (!isExecutorId(id)) {
+ throw invalidProtocolFrame('Invalid Executor id');
+ }
+ return id;
+}
+
+function optionalExecutorId(
+ record: Record<string, unknown>,
+): Pick<SessionCatalogProjection, 'executorId'> | Record<string, never> {
+ if (record.executorId === undefined) return {};
+ return { executorId: executorIdValue(record.executorId) };
+}
+
function thinkingLevel(value: unknown): ThinkingLevel {
if (!isThinkingLevel(value)) throw invalidProtocolFrame('Invalid Session thinking level');
return value;
diff --git a/packages/runtime-host/src/server/child-agent-composition.ts b/packages/runtime-host/src/server/child-agent-composition.ts
index 461da00d2d..793b65a084 100644
--- a/packages/runtime-host/src/server/child-agent-composition.ts
+++ b/packages/runtime-host/src/server/child-agent-composition.ts
@@ -77,6 +77,7 @@ export function bindHostChildAgentBackend(
},
agentProfile: input.agentProfile,
...(input.subagentId ? { subagentId: input.subagentId } : {}),
+ ...(input.executorId ? { executorId: input.executorId } : {}),
prompt: input.prompt,
...(input.swarm ? { swarm: input.swarm } : {}),
abortSignal: input.abortSignal,
diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts
index bba8259bbf..265f1e69d3 100644
--- a/packages/runtime-host/src/server/execution-composition.ts
+++ b/packages/runtime-host/src/server/execution-composition.ts
@@ -91,6 +91,8 @@ import { PluginAttachmentService } from '@maka/runtime/plugin-attachment-service
import { PluginApprovalService } from '@maka/runtime/plugin-approval-service';
import { PluginFilesystemService } from '@maka/runtime/plugin-fs-service';
import { PluginLlmService } from '@maka/runtime/plugin-llm-service';
+import { PluginExecutorBackend } from '@maka/runtime/plugin-executor-backend';
+import { PluginExecutorService } from '@maka/runtime/plugin-executor-service';
import { PluginShellService } from '@maka/runtime/plugin-shell-service';
import { PluginUserQuestionService } from '@maka/runtime/plugin-user-question-service';
import { PluginWebService } from '@maka/runtime/plugin-web-service';
@@ -336,6 +338,7 @@ export async function createExecutionRuntimeHostComposition(
let goalExecutions: HostGoalExecutionCoordinator | undefined;
let pluginPlatform: HostPluginPlatform | undefined;
let manager: SessionManager | undefined;
+ let invalidatePluginExecutorBackends: () => void = () => undefined;
let modelMetadataRefresh: ReturnType<typeof startHostModelMetadataRefresh> | undefined;
let archiveEvidence: Awaited<ReturnType<typeof openToolResultArchiveEvidenceReader>> | undefined;
try {
@@ -346,6 +349,11 @@ export async function createExecutionRuntimeHostComposition(
new PluginUserQuestionService(pluginRoot, pluginAgents);
const pluginFilesystem = new PluginFilesystemService(pluginRoot, pluginAgents);
const pluginLlm = new PluginLlmService(pluginRoot, pluginAgents);
+ const pluginExecutors = new PluginExecutorService(pluginRoot, {
+ // Retirement aborts its generation before backend disposal begins, so the
+ // terminal event preserves executor-retired instead of looking like Stop.
+ onChanged: () => queueMicrotask(invalidatePluginExecutorBackends),
+ });
const pluginShellEnv = new PluginShellEnvService(pluginRoot);
const pluginShell = new PluginShellService(pluginRoot, pluginAgents, pluginShellEnv);
const pluginWeb = new PluginWebService(pluginRoot, pluginAgents);
@@ -369,6 +377,7 @@ export async function createExecutionRuntimeHostComposition(
tools: pluginTools,
systemPrompt: pluginSystemPrompt,
commands: pluginCommands,
+ executors: pluginExecutors,
});
const pluginPlatformCoordinator = new HostPluginPlatformCoordinator(pluginPlatform);
const openedProjectCatalog = storage.projectCatalog;
@@ -1055,6 +1064,23 @@ export async function createExecutionRuntimeHostComposition(
prepare: (backendContext) => prepareHostAiSdkBackend(hostAiSdkBackendInput(backendContext)),
},
);
+ backends.register('plugin-executor', {
+ prepare: async (backendContext) => {
+ const executorId = backendContext.header.executorId;
+ if (!executorId) throw new Error('Plugin executor Session is missing its executor id');
+ const binding = pluginExecutors.bind(backendContext.sessionId, executorId);
+ return {
+ providerStateIdentity: binding.providerStateIdentity,
+ build: (factoryContext) =>
+ new PluginExecutorBackend({
+ sessionId: factoryContext.sessionId,
+ cwd: factoryContext.header.cwd,
+ ...(factoryContext.systemPrompt ? { instructions: factoryContext.systemPrompt } : {}),
+ binding,
+ }),
+ };
+ },
+ });
const runtimeAuthority: RuntimeHostedRootAuthority = {
bindRun: (identity) => messages.bindRun(identity),
executeRoot: (input) =>
@@ -1108,6 +1134,7 @@ export async function createExecutionRuntimeHostComposition(
};
resolveAvailableToolNames = async (sessionId: string): Promise<string[]> => {
const header = await stores.sessionStore.readHeaderSnapshot(sessionId);
+ if (header.backend === 'plugin-executor') return [];
if (header.subagentRuntime) {
if (!header.subagentParent) {
throw new Error('Subagent runtime snapshot requires a linked child session');
@@ -1300,6 +1327,14 @@ export async function createExecutionRuntimeHostComposition(
toolBoundaryProtocol: stores.runtimeEventStore.toolBoundaryProtocol,
backends,
subagentCatalog,
+ assertChildExecutorAvailable: (parentSessionId, executorId) => {
+ const identity = pluginExecutors.identity(parentSessionId, executorId);
+ if (identity.scopeId !== 'profile') {
+ throw new Error(
+ `Session-scoped executor cannot be inherited by a child Session: ${executorId}`,
+ );
+ }
+ },
newId: randomUUID,
now: Date.now,
safeBoundaryResumeEnabled: process.env.MAKA_RUNTIME_SAFE_BOUNDARY_RESUME === '1',
@@ -1401,6 +1436,7 @@ export async function createExecutionRuntimeHostComposition(
const registerBackendInvalidation = (): void => {
observeBackendInvalidation(requireSessionManager(manager).refreshIdleBackends());
};
+ invalidatePluginExecutorBackends = registerBackendInvalidation;
const registerConfigurationMutation = (): void => {
hostChanges.publishConfiguration();
registerBackendInvalidation();
@@ -1684,6 +1720,7 @@ export async function createExecutionRuntimeHostComposition(
return new Promise((resolve, reject) => {
void spawn({
agentProfile: options.agentProfile ?? 'implementation',
+ ...(options.executorId ? { executorId: options.executorId } : {}),
prompt: options.prompt!,
...(options.signal ? { abortSignal: options.signal } : {}),
onReady: (ready) =>
@@ -1940,6 +1977,9 @@ export async function createExecutionRuntimeHostComposition(
continuity: continuityCoordinator,
workspaceResolver,
requestDrain: context.requestDrain,
+ assertExecutorAvailable: (sessionId, executorId) => {
+ pluginExecutors.identity(sessionId, executorId);
+ },
...(context.sessionAccessAuthority
? { sessionAccessAuthority: context.sessionAccessAuthority }
: {}),
@@ -2135,14 +2175,18 @@ export async function createExecutionRuntimeHostComposition(
sessionId: input.targetSessionId,
workspace: input.create.workspace,
name: input.create.title,
- modelTarget: input.create.defaults?.model
- ? {
- kind: 'explicit',
- connectionId: input.create.defaults.model.llmConnectionId,
- connectionSlug: input.create.defaults.model.llmConnectionSlug,
- model: input.create.defaults.model.model,
- }
- : { kind: 'default' },
+ ...(input.create.defaults?.executorId
+ ? { executorId: input.create.defaults.executorId }
+ : {
+ modelTarget: input.create.defaults?.model
+ ? {
+ kind: 'explicit' as const,
+ connectionId: input.create.defaults.model.llmConnectionId,
+ connectionSlug: input.create.defaults.model.llmConnectionSlug,
+ model: input.create.defaults.model.model,
+ }
+ : ({ kind: 'default' } as const),
+ }),
...(input.create.defaults?.permissionMode
? { permissionMode: input.create.defaults.permissionMode }
: {}),
diff --git a/packages/runtime-host/src/server/host-session-availability.ts b/packages/runtime-host/src/server/host-session-availability.ts
index 2cff50c8f0..5874aea198 100644
--- a/packages/runtime-host/src/server/host-session-availability.ts
+++ b/packages/runtime-host/src/server/host-session-availability.ts
@@ -37,6 +37,16 @@ export const WORKHUB_COORDINATION_TARGET_UNAVAILABLE_REASON =
'WorkHub Coordination execution requires the reserved Coordination Session';
export const LEGACY_CONNECTION_IDENTITY_EXECUTION_UNAVAILABLE_REASON =
'This Session requires an explicit account selection before it can run.';
+export const PLUGIN_EXECUTOR_CONTINUATION_UNAVAILABLE_REASON =
+ 'Plugin executor Sessions cannot resume without provider-owned durable continuation support.';
+export const PLUGIN_EXECUTOR_COPY_UNAVAILABLE_REASON =
+ 'Plugin executor Sessions cannot be copied without provider-owned conversation cloning';
+
+export function runtimeHostConversationCopyUnavailableReason(
+ header: Pick<SessionHeader, 'backend'>,
+): string | undefined {
+ return header.backend === 'plugin-executor' ? PLUGIN_EXECUTOR_COPY_UNAVAILABLE_REASON : undefined;
+}
export function runtimeHostExternalTurnUnavailableReason(
header: Pick<
@@ -64,9 +74,12 @@ export function runtimeHostSafeBoundaryContinuationUnavailableReason(
? WORKHUB_COORDINATION_EXECUTION_UNAVAILABLE_REASON
: undefined) ??
(header.transcriptLedgerVersion === 0 ? IMPORT_STAGING_UNAVAILABLE_REASON : undefined) ??
- (header.llmConnectionId === undefined && header.backend !== 'fake'
+ (header.llmConnectionId === undefined && header.backend === 'ai-sdk'
? LEGACY_CONNECTION_IDENTITY_EXECUTION_UNAVAILABLE_REASON
: undefined) ??
+ (header.backend === 'plugin-executor'
+ ? PLUGIN_EXECUTOR_CONTINUATION_UNAVAILABLE_REASON
+ : undefined) ??
(header.subagentParent ? CHILD_CONTINUATION_UNAVAILABLE_REASON : undefined)
);
}
@@ -111,7 +124,7 @@ export function runtimeHostExecutionUnavailableReason(
? WORKHUB_COORDINATION_EXECUTION_UNAVAILABLE_REASON
: undefined) ??
(header.transcriptLedgerVersion === 0 ? IMPORT_STAGING_UNAVAILABLE_REASON : undefined) ??
- (header.llmConnectionId === undefined && header.backend !== 'fake'
+ (header.llmConnectionId === undefined && header.backend === 'ai-sdk'
? LEGACY_CONNECTION_IDENTITY_EXECUTION_UNAVAILABLE_REASON
: undefined) ??
(header.collaborationMode === 'plan' &&
diff --git a/packages/runtime-host/src/server/plugin-platform-coordinator.ts b/packages/runtime-host/src/server/plugin-platform-coordinator.ts
index 889fd266e0..3ad2b6528d 100644
--- a/packages/runtime-host/src/server/plugin-platform-coordinator.ts
+++ b/packages/runtime-host/src/server/plugin-platform-coordinator.ts
@@ -27,6 +27,7 @@ import {
} from '@maka/runtime/plugin-runtime';
import type { PluginToolInspection } from '@maka/runtime/plugin-tool-service';
import type { PluginCommandInspection } from '@maka/runtime/plugin-command-service';
+import type { PluginExecutorInspection } from '@maka/runtime/plugin-executor-service';
import type {
OperationOutcome,
PluginPackageExportInput,
@@ -90,6 +91,12 @@ export class HostPluginPlatformCoordinator {
result: boundedPage('commands', this.platform.inspectCommands(input.rootId), input),
};
}
+ if (input.view === 'executors') {
+ return {
+ ok: true,
+ result: boundedPage('executors', this.platform.inspectExecutors(input.rootId), input),
+ };
+ }
if (input.view === 'failures') {
return { ok: true, result: boundedPage('failures', failures, input) };
}
@@ -198,13 +205,18 @@ function boundedPage(
values: readonly PluginCommandInspection[],
input: PluginPlatformQueryInput,
): Extract<PluginPlatformQueryResult, { readonly view: 'commands' }>;
+function boundedPage(
+ view: 'executors',
+ values: readonly PluginExecutorInspection[],
+ input: PluginPlatformQueryInput,
+): Extract<PluginPlatformQueryResult, { readonly view: 'executors' }>;
function boundedPage(
view: 'failures',
values: readonly PluginPlatformFailureProjection[],
input: PluginPlatformQueryInput,
): Extract<PluginPlatformQueryResult, { readonly view: 'failures' }>;
function boundedPage<T>(
- view: 'packages' | 'entries' | 'tools' | 'commands' | 'failures',
+ view: 'packages' | 'entries' | 'tools' | 'commands' | 'executors' | 'failures',
values: readonly T[],
input: PluginPlatformQueryInput,
): PluginPlatformQueryResult {
@@ -240,7 +252,7 @@ function boundedPage<T>(
interface PageCursor {
readonly version: 1;
- readonly view: 'packages' | 'entries' | 'tools' | 'commands' | 'failures';
+ readonly view: 'packages' | 'entries' | 'tools' | 'commands' | 'executors' | 'failures';
readonly rootId?: string;
readonly digest: string;
readonly offset: number;
diff --git a/packages/runtime-host/src/server/plugin-platform.ts b/packages/runtime-host/src/server/plugin-platform.ts
index 98c4a6309c..cdf59765e3 100644
--- a/packages/runtime-host/src/server/plugin-platform.ts
+++ b/packages/runtime-host/src/server/plugin-platform.ts
@@ -36,6 +36,7 @@ import type { ExtensionPackageManifest } from './extension-package-manifest.js';
import type { PluginToolInspection } from '@maka/runtime/plugin-tool-service';
import type { PluginSystemPromptInspection } from '@maka/runtime/plugin-system-prompt-service';
import type { PluginCommandInspection } from '@maka/runtime/plugin-command-service';
+import type { PluginExecutorInspection } from '@maka/runtime/plugin-executor-service';
import { validateExtensionConfiguration } from './extension-package-manifest.js';
import { recoverExtensionBundleImports } from './extension-bundle.js';
import { loadPluginCompositionPatch } from './plugin-composition-patch.js';
@@ -82,6 +83,9 @@ export interface HostPluginPlatformOptions {
inspect(rootId?: MakaPluginRootId): readonly PluginSystemPromptInspection[];
};
readonly commands?: { inspect(rootId?: MakaPluginRootId): readonly PluginCommandInspection[] };
+ readonly executors?: {
+ inspect(rootId?: MakaPluginRootId): readonly PluginExecutorInspection[];
+ };
}
export interface HostPluginPlatformFailure {
@@ -120,6 +124,7 @@ export class HostPluginPlatform {
readonly #tools?: HostPluginPlatformOptions['tools'];
readonly #systemPrompt?: HostPluginPlatformOptions['systemPrompt'];
readonly #commands?: HostPluginPlatformOptions['commands'];
+ readonly #executors?: HostPluginPlatformOptions['executors'];
#authority: PersistedPluginComposition = emptyCompositionAuthority();
#desired: MakaCompositionState = emptyCompositionState();
@@ -147,6 +152,7 @@ export class HostPluginPlatform {
this.#tools = options.tools;
this.#systemPrompt = options.systemPrompt;
this.#commands = options.commands;
+ this.#executors = options.executors;
}
async recover(): Promise<void> {
@@ -499,6 +505,11 @@ export class HostPluginPlatform {
return this.#commands?.inspect(rootId) ?? Object.freeze([]);
}
+ inspectExecutors(rootId?: MakaPluginRootId): readonly PluginExecutorInspection[] {
+ this.#assertReadable();
+ return this.#executors?.inspect(rootId) ?? Object.freeze([]);
+ }
+
async status(): Promise<{
readonly phase: PluginPlatformPhase;
readonly authorityEpoch: number;
diff --git a/packages/runtime-host/src/server/session-catalog-coordinator.ts b/packages/runtime-host/src/server/session-catalog-coordinator.ts
index face1032bd..a9a62b22c1 100644
--- a/packages/runtime-host/src/server/session-catalog-coordinator.ts
+++ b/packages/runtime-host/src/server/session-catalog-coordinator.ts
@@ -30,6 +30,7 @@ import {
type ExecutionBoundarySummary,
} from '@maka/core/sandbox-boundary';
import type { CreateSessionInput } from '@maka/core/runtime-inputs';
+import { isExecutorId } from '@maka/core/executor-id';
import type { ToolMode } from '@maka/core/tool-mode';
import type { ConnectionCatalogEntry, ConnectionCatalogSnapshot } from '@maka/core/runtime-policy';
import { DEFAULT_SESSION_NAME, normalizeUserSessionName } from '@maka/core/session-name';
@@ -138,8 +139,9 @@ type SessionConfigurationAuthority = Pick<
type SessionContinuity = Pick<SessionContinuityCoordinator, 'refreshCanonical'>;
interface ResolvedSessionConfiguration {
- readonly backend: 'ai-sdk';
- readonly llmConnectionId?: string;
+ readonly backend: 'ai-sdk' | 'plugin-executor';
+ readonly executorId: string | undefined;
+ readonly llmConnectionId: string | undefined;
readonly llmConnectionSlug: string;
readonly model: string;
readonly thinkingLevel: SessionHeader['thinkingLevel'];
@@ -188,6 +190,7 @@ export interface HostSessionCatalogCoordinatorOptions {
readonly continuity: SessionContinuity;
readonly workspaceResolver: HostWorkspaceResolver;
readonly requestDrain: () => void;
+ readonly assertExecutorAvailable?: (sessionId: string, executorId: string) => void;
readonly sessionAccessAuthority?: Pick<
RuntimeHostAccessAuthority,
'activeSessionGrantForPrincipal'
@@ -299,6 +302,7 @@ export class HostSessionCatalogCoordinator {
readonly #continuity: SessionContinuity;
readonly #workspaceResolver: HostWorkspaceResolver;
readonly #requestDrain: () => void;
+ readonly #assertExecutorAvailable: ((sessionId: string, executorId: string) => void) | undefined;
readonly #sessionAccessAuthority:
| Pick<RuntimeHostAccessAuthority, 'activeSessionGrantForPrincipal'>
| undefined;
@@ -312,6 +316,7 @@ export class HostSessionCatalogCoordinator {
this.#continuity = options.continuity;
this.#workspaceResolver = options.workspaceResolver;
this.#requestDrain = options.requestDrain;
+ this.#assertExecutorAvailable = options.assertExecutorAvailable;
this.#sessionAccessAuthority = options.sessionAccessAuthority;
}
@@ -368,7 +373,7 @@ export class HostSessionCatalogCoordinator {
const prepared = await prepareCreate(input);
return this.#workspaceResolver.runWithUsageRecorded(input.workspace, async (workspace) => {
const [model, policy] = await Promise.all([
- this.#resolveModel(input.modelTarget, input.thinkingLevel),
+ this.#resolveCreateExecution(input),
this.#readRuntimePolicy(),
]);
return {
@@ -379,7 +384,8 @@ export class HostSessionCatalogCoordinator {
...(workspace.projectId === null ? {} : { projectId: workspace.projectId }),
name: prepared.name,
labels: [...prepared.labels],
- llmConnectionId: model.connectionId,
+ ...(model.executorId ? { executorId: model.executorId } : {}),
+ ...(model.connectionId ? { llmConnectionId: model.connectionId } : {}),
llmConnectionSlug: model.connectionSlug,
model: model.model,
...(input.thinkingLevel === undefined ? {} : { thinkingLevel: input.thinkingLevel }),
@@ -607,7 +613,7 @@ export class HostSessionCatalogCoordinator {
input.workspace,
async (workspace) => {
const [model, policy] = await Promise.all([
- this.#resolveModel(input.modelTarget, input.thinkingLevel),
+ this.#resolveCreateExecution(input),
this.#readRuntimePolicy(),
]);
const createInput: CreateSessionInput = {
@@ -615,7 +621,8 @@ export class HostSessionCatalogCoordinator {
...(workspace.projectId === null ? {} : { projectId: workspace.projectId }),
name: prepared.name,
labels: [...prepared.labels],
- llmConnectionId: model.connectionId,
+ ...(model.executorId ? { executorId: model.executorId } : {}),
+ ...(model.connectionId ? { llmConnectionId: model.connectionId } : {}),
llmConnectionSlug: model.connectionSlug,
model: model.model,
...(input.thinkingLevel === undefined ? {} : { thinkingLevel: input.thinkingLevel }),
@@ -1211,7 +1218,17 @@ export class HostSessionCatalogCoordinator {
current: SessionHeader,
patch: SessionConfigurationUpdateInput['patch'],
): Promise<ResolvedSessionConfiguration> {
- if (current.llmConnectionId === undefined && patch.modelTarget === undefined) {
+ if (current.backend === 'fake' && patch.modelTarget === undefined) {
+ throw new SessionOperationFailure(
+ 'operation_conflict',
+ 'Legacy test backend configuration requires an explicit account selection',
+ );
+ }
+ if (
+ current.backend !== 'plugin-executor' &&
+ current.llmConnectionId === undefined &&
+ patch.modelTarget === undefined
+ ) {
throw new SessionOperationFailure(
'operation_conflict',
'Legacy Session configuration requires an explicit account selection',
@@ -1245,8 +1262,9 @@ export class HostSessionCatalogCoordinator {
);
}
return {
- backend: 'ai-sdk',
- ...(model.connectionId === undefined ? {} : { llmConnectionId: model.connectionId }),
+ backend: patch.modelTarget || current.backend === 'ai-sdk' ? 'ai-sdk' : 'plugin-executor',
+ executorId: patch.modelTarget ? undefined : current.executorId,
+ llmConnectionId: model.connectionId,
llmConnectionSlug: model.connectionSlug,
model: model.model,
thinkingLevel,
@@ -1257,6 +1275,36 @@ export class HostSessionCatalogCoordinator {
};
}
+ async #resolveCreateExecution(input: SessionCreateInput): Promise<{
+ readonly executorId?: string;
+ readonly connectionId?: string;
+ readonly connectionSlug: string;
+ readonly model: string;
+ }> {
+ if (input.executorId) {
+ try {
+ this.#assertExecutorAvailable?.(input.sessionId, input.executorId);
+ } catch {
+ throw new SessionOperationFailure(
+ 'operation_unavailable',
+ `Plugin executor is unavailable: ${input.executorId}`,
+ );
+ }
+ return {
+ executorId: input.executorId,
+ connectionSlug: `executor:${input.executorId}`,
+ model: input.executorId,
+ };
+ }
+ if (!input.modelTarget) {
+ throw new SessionOperationFailure(
+ 'invalid_request',
+ 'Session creation requires a model target or executor id',
+ );
+ }
+ return await this.#resolveModel(input.modelTarget, input.thinkingLevel);
+ }
+
async #readRuntimePolicy(): Promise<
Awaited<ReturnType<SessionRuntimePolicyStores['runtimePolicy']['getSnapshot']>>
> {
@@ -1273,7 +1321,8 @@ function sessionConfigurationMatches(
configuration: ResolvedSessionConfiguration,
): boolean {
return (
- header.backend === 'ai-sdk' &&
+ header.backend === configuration.backend &&
+ header.executorId === configuration.executorId &&
header.llmConnectionId === configuration.llmConnectionId &&
header.llmConnectionSlug === configuration.llmConnectionSlug &&
header.model === configuration.model &&
@@ -1302,6 +1351,15 @@ interface PreparedSessionCreate {
}
async function prepareCreate(input: SessionCreateInput): Promise<PreparedSessionCreate> {
+ if ((input.executorId === undefined) === (input.modelTarget === undefined)) {
+ throw new SessionOperationFailure(
+ 'invalid_request',
+ 'Session creation requires exactly one model target or executor id',
+ );
+ }
+ if (input.executorId !== undefined && !isExecutorId(input.executorId)) {
+ throw new SessionOperationFailure('invalid_request', 'Session executor id is invalid');
+ }
if (input.labels?.some(isExecutionSemanticLabel)) {
throw new SessionOperationFailure(
'invalid_request',
@@ -1337,14 +1395,16 @@ function createRequestFingerprint(
: ['host_path', input.workspace.path],
prepared.name,
prepared.labels,
- input.modelTarget.kind === 'default'
- ? ['default']
- : [
- 'explicit',
- input.modelTarget.connectionId,
- input.modelTarget.connectionSlug,
- input.modelTarget.model,
- ],
+ input.executorId
+ ? ['executor', input.executorId]
+ : input.modelTarget?.kind === 'default'
+ ? ['default']
+ : [
+ 'explicit',
+ input.modelTarget!.connectionId,
+ input.modelTarget!.connectionSlug,
+ input.modelTarget!.model,
+ ],
input.thinkingLevel ?? null,
input.toolProfile ?? null,
prepared.permissionMode ?? ['runtime_default'],
@@ -1417,6 +1477,7 @@ export function projectSessionCatalogRecord(
...(header.revisionIndex === undefined ? {} : { revisionIndex: header.revisionIndex }),
...(header.revisionState === undefined ? {} : { revisionState: header.revisionState }),
backend: header.backend,
+ ...(header.executorId ? { executorId: header.executorId } : {}),
llmConnectionId: header.llmConnectionId ?? null,
llmConnectionSlug: header.llmConnectionSlug,
connectionLocked: header.connectionLocked,
diff --git a/packages/runtime-host/src/server/session-revision-coordinator.ts b/packages/runtime-host/src/server/session-revision-coordinator.ts
index 74625e621f..dec9a386a5 100644
--- a/packages/runtime-host/src/server/session-revision-coordinator.ts
+++ b/packages/runtime-host/src/server/session-revision-coordinator.ts
@@ -30,6 +30,7 @@ import {
type SessionHeader,
type StoredMessage,
} from '@maka/core/session';
+import { runtimeHostConversationCopyUnavailableReason } from './host-session-availability.js';
import {
archivedToolResultContainsLinkedChildReferences,
archivedToolResultContainsConversationOwnedReferences,
@@ -345,6 +346,8 @@ export class HostSessionRevisionCoordinator {
'Deep Research Sessions cannot be copied without an exact research ledger boundary',
);
}
+ const copyUnavailableReason = runtimeHostConversationCopyUnavailableReason(sourceHeader);
+ if (copyUnavailableReason) return copyFailure('operation_unavailable', copyUnavailableReason);
if (kind !== 'side_conversation' && this.options.isSessionActive(input.sourceSessionId)) {
return copyFailure('session_busy', 'Source Session has an active Turn');
}
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 915c7c8f4d..b45bb28223 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -84,6 +84,8 @@
"./plugin-composition-loader": "./dist/plugin-composition-loader.js",
"./plugin-command-service": "./dist/plugin-command-service.js",
"./plugin-data-services": "./dist/plugin-data-services.js",
+ "./plugin-executor-backend": "./dist/plugin-executor-backend.js",
+ "./plugin-executor-service": "./dist/plugin-executor-service.js",
"./plugin-goal-service": "./dist/plugin-goal-service.js",
"./plugin-lsp-service": "./dist/plugin-lsp-service.js",
"./plugin-session-query-service": "./dist/plugin-session-query-service.js",
diff --git a/packages/runtime/src/__tests__/plugin-executor-backend.test.ts b/packages/runtime/src/__tests__/plugin-executor-backend.test.ts
new file mode 100644
index 0000000000..cb0174d007
--- /dev/null
+++ b/packages/runtime/src/__tests__/plugin-executor-backend.test.ts
@@ -0,0 +1,209 @@
+/*
+ * 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 assert from 'node:assert/strict';
+import { test } from 'node:test';
+import type { SessionEvent } from '@maka/core/events';
+import { PluginExecutorBackend } from '../plugin-executor-backend.js';
+import { Context } from '../plugin-kernel.js';
+import { PluginExecutorService } from '../plugin-executor-service.js';
+
+test('executor backend converts plugin output and result to ordinary Session events', async () => {
+ const { root, binding } = fixture(async (request, context) => {
+ assert.equal(request.instructions, 'child instructions');
+ context.emit({ type: 'output_delta', text: 'hel' });
+ return { status: 'completed', text: 'hello' };
+ });
+ const backend = new PluginExecutorBackend({
+ sessionId: 'session-a',
+ cwd: '/workspace',
+ instructions: 'child instructions',
+ binding,
+ newId: ids(),
+ now: () => 42,
+ });
+
+ const events = await collect(backend.send({ turnId: 'turn-a', runId: 'run-a', text: 'task' }));
+ assert.deepEqual(
+ events.map((event) => event.type),
+ ['text_delta', 'text_complete', 'complete'],
+ );
+ assert.equal(events[0]?.turnId, 'turn-a');
+ assert.equal(events[0]?.type === 'text_delta' ? events[0].text : undefined, 'hel');
+ assert.equal(events[1]?.type === 'text_complete' ? events[1].text : undefined, 'hello');
+ assert.equal(events[2]?.type === 'complete' ? events[2].stopReason : undefined, 'end_turn');
+ await root.fiber.dispose();
+});
+
+test('executor backend turns stop into abort and terminal events', async () => {
+ let started!: () => void;
+ const ready = new Promise<void>((resolve) => {
+ started = resolve;
+ });
+ const { root, binding } = fixture(async (_request, context) => {
+ started();
+ await new Promise<void>((resolve) => context.signal.addEventListener('abort', () => resolve()));
+ return { status: 'cancelled' };
+ });
+ const backend = new PluginExecutorBackend({
+ sessionId: 'session-a',
+ cwd: '/workspace',
+ binding,
+ });
+
+ const eventsPromise = collect(backend.send({ turnId: 'turn-a', text: 'task' }));
+ await ready;
+ await backend.stop('user_stop');
+ const events = await eventsPromise;
+ assert.deepEqual(
+ events.map((event) => event.type),
+ ['abort', 'complete'],
+ );
+ assert.equal(events[1]?.type === 'complete' ? events[1].stopReason : undefined, 'user_stop');
+ await root.fiber.dispose();
+});
+
+test('executor backend projects optional thinking and external tool activity', async () => {
+ const { root, binding } = fixture(
+ async (_request, context) => {
+ context.emit({ type: 'thinking_delta', text: 'considering' });
+ context.emit({
+ type: 'tool_start',
+ toolCallId: 'external-1',
+ name: 'search',
+ input: { query: 'maka' },
+ activityKind: 'search',
+ });
+ context.emit({ type: 'tool_progress', toolCallId: 'external-1', text: 'working' });
+ context.emit({ type: 'tool_result', toolCallId: 'external-1', text: 'found' });
+ return { status: 'completed', text: 'done' };
+ },
+ { thinking: true, toolActivity: true },
+ );
+ const backend = new PluginExecutorBackend({
+ sessionId: 'session-a',
+ cwd: '/workspace',
+ binding,
+ newId: ids(),
+ now: () => 42,
+ });
+
+ const events = await collect(backend.send({ turnId: 'turn-a', text: 'task' }));
+ assert.deepEqual(
+ events.map((event) => event.type),
+ [
+ 'thinking_delta',
+ 'tool_start',
+ 'tool_progress',
+ 'tool_result',
+ 'thinking_complete',
+ 'text_complete',
+ 'complete',
+ ],
+ );
+ assert.equal(events[1]?.type === 'tool_start' ? events[1].providerExecuted : undefined, true);
+ const stepId = events[0]?.type === 'thinking_delta' ? events[0].messageId : undefined;
+ assert.equal(events[1]?.type === 'tool_start' ? events[1].stepId : undefined, stepId);
+ assert.equal(events[4]?.type === 'thinking_complete' ? events[4].messageId : undefined, stepId);
+ assert.equal(events[5]?.type === 'text_complete' ? events[5].messageId : undefined, stepId);
+ await root.fiber.dispose();
+});
+
+test('executor retirement remains cancellation and is surfaced as a crash abort', async () => {
+ let started!: () => void;
+ const ready = new Promise<void>((resolve) => {
+ started = resolve;
+ });
+ const { root, binding, dispose } = fixture(async (_request, context) => {
+ started();
+ await new Promise<void>((_resolve, reject) =>
+ context.signal.addEventListener('abort', () => reject(context.signal.reason)),
+ );
+ return { status: 'completed', text: 'unreachable' };
+ });
+ const backend = new PluginExecutorBackend({
+ sessionId: 'session-a',
+ cwd: '/workspace',
+ binding,
+ });
+
+ const eventsPromise = collect(backend.send({ turnId: 'turn-a', text: 'task' }));
+ await ready;
+ await dispose();
+ const events = await eventsPromise;
+ assert.equal(events[0]?.type === 'abort' ? events[0].reason : undefined, 'crash');
+ assert.equal(events[1]?.type === 'complete' ? events[1].stopReason : undefined, 'user_stop');
+ await root.fiber.dispose();
+});
+
+test('executor failure closes rich output before publishing its terminal error', async () => {
+ const { root, binding } = fixture(
+ async (_request, context) => {
+ context.emit({ type: 'thinking_delta', text: 'partial thought' });
+ context.emit({ type: 'tool_start', toolCallId: 'external-1', name: 'search' });
+ throw new Error('provider crashed');
+ },
+ { thinking: true, toolActivity: true },
+ );
+ const backend = new PluginExecutorBackend({
+ sessionId: 'session-a',
+ cwd: '/workspace',
+ binding,
+ newId: ids(),
+ now: () => 42,
+ });
+
+ const events = await collect(backend.send({ turnId: 'turn-a', text: 'task' }));
+ assert.deepEqual(
+ events.map((event) => event.type),
+ ['thinking_delta', 'tool_start', 'thinking_complete', 'tool_result', 'error', 'complete'],
+ );
+ assert.equal(events[3]?.type === 'tool_result' ? events[3].isError : undefined, true);
+ assert.equal(events[4]?.type === 'error' ? events[4].message : undefined, 'provider crashed');
+ await root.fiber.dispose();
+});
+
+function fixture(
+ execute: Parameters<PluginExecutorService['register']>[0]['execute'],
+ capabilities?: Parameters<PluginExecutorService['register']>[0]['capabilities'],
+): {
+ root: Context;
+ binding: ReturnType<PluginExecutorService['bind']>;
+ dispose: ReturnType<PluginExecutorService['register']>;
+} {
+ const root = new Context();
+ const service = new PluginExecutorService(root);
+ const dispose = root
+ .extend({
+ maka: { rootId: 'profile', packageId: 'fixture', entryId: 'provider', generation: 1 },
+ })
+ .executors.register({ id: 'remote', execute, ...(capabilities ? { capabilities } : {}) });
+ return { root, binding: service.bind('session-a', 'remote'), dispose };
+}
+
+function ids(): () => string {
+ let value = 0;
+ return () => `id-${++value}`;
+}
+
+async function collect(events: AsyncIterable<SessionEvent>): Promise<SessionEvent[]> {
+ const result: SessionEvent[] = [];
+ for await (const event of events) result.push(event);
+ return result;
+}
diff --git a/packages/runtime/src/__tests__/plugin-executor-service.test.ts b/packages/runtime/src/__tests__/plugin-executor-service.test.ts
new file mode 100644
index 0000000000..bd2862e8d3
--- /dev/null
+++ b/packages/runtime/src/__tests__/plugin-executor-service.test.ts
@@ -0,0 +1,234 @@
+/*
+ * 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 assert from 'node:assert/strict';
+import { test } from 'node:test';
+import { Context } from '../plugin-kernel.js';
+import { PluginExecutorService } from '../plugin-executor-service.js';
+import { MakaPluginTransactionBuffer } from '../plugin-runtime.js';
+
+test('executors are scoped and pass black-box output without an Agent invocation', async () => {
+ const root = new Context();
+ const service = new PluginExecutorService(root);
+ const profile = plugin(root, 'profile', 'profile-provider', 1);
+ const session = plugin(root, 'session:session-a', 'session-provider', 1);
+ profile.executors.register({
+ id: 'remote',
+ displayName: 'Remote',
+ execute: async (request, context) => {
+ context.emit({ type: 'output_delta', text: 'working' });
+ return { status: 'completed', text: `${request.sessionId}:${request.text}` };
+ },
+ });
+ session.executors.register({
+ id: 'private',
+ execute: async () => ({ status: 'completed', text: 'private' }),
+ });
+
+ const output: string[] = [];
+ assert.deepEqual(
+ await service.execute('remote', request('session-b'), {
+ onEvent: (event) => {
+ if (event.type === 'output_delta') output.push(event.text);
+ },
+ }),
+ { status: 'completed', text: 'session-b:hello' },
+ );
+ assert.deepEqual(output, ['working']);
+ assert.deepEqual(
+ service.list('session-a').map((item) => item.id),
+ ['private', 'remote'],
+ );
+ assert.deepEqual(
+ service.list('session-b').map((item) => item.id),
+ ['remote'],
+ );
+ await assert.rejects(() => service.execute('private', request('session-b')), /unavailable/u);
+ await root.fiber.dispose();
+});
+
+test('executor registration is transactional across hot reload', async () => {
+ const root = new Context();
+ const service = new PluginExecutorService(root);
+ const previous = plugin(root, 'profile', 'provider', 1);
+ const disposePrevious = previous.executors.register(provider('previous'));
+ const candidateOwner = plugin(root, 'profile', 'provider', 2);
+ const transaction = new MakaPluginTransactionBuffer(candidateOwner);
+ const candidate = candidateOwner.extend({ makaTransaction: transaction });
+ const disposeCandidate = candidate.executors.register(provider('candidate'));
+
+ assert.equal(await executeText(service), 'previous');
+ await transaction.commit();
+ assert.equal(await executeText(service), 'candidate');
+ await disposePrevious();
+ assert.equal(await executeText(service), 'candidate');
+ await disposeCandidate();
+ await assert.rejects(() => executeText(service), /unavailable/u);
+ await root.fiber.dispose();
+});
+
+test('retiring an executor aborts and drains its active calls', async () => {
+ const root = new Context();
+ const service = new PluginExecutorService(root);
+ const owner = plugin(root, 'profile', 'provider', 1);
+ let observed: AbortSignal | undefined;
+ let started!: () => void;
+ const startedPromise = new Promise<void>((resolve) => {
+ started = resolve;
+ });
+ const dispose = owner.executors.register({
+ id: 'remote',
+ execute: async (_request, context) => {
+ observed = context.signal;
+ started();
+ await new Promise<void>((resolve) =>
+ context.signal.addEventListener('abort', () => resolve()),
+ );
+ return { status: 'cancelled' };
+ },
+ });
+ const execution = service.execute('remote', request('session-a'));
+ await startedPromise;
+ await dispose();
+ assert.equal(observed?.aborted, true);
+ assert.deepEqual(await execution, {
+ status: 'cancelled',
+ source: 'executor_retired',
+ reason: 'Executor was retired: remote',
+ });
+ await root.fiber.dispose();
+});
+
+test('executor registration binds prototype methods to the provider instance', async () => {
+ class ClassExecutor {
+ readonly id = 'remote';
+ readonly prefix = 'class';
+
+ async execute() {
+ return { status: 'completed' as const, text: `${this.prefix}:ok` };
+ }
+ }
+
+ const root = new Context();
+ const service = new PluginExecutorService(root);
+ plugin(root, 'profile', 'provider', 1).executors.register(new ClassExecutor());
+ assert.equal(await executeText(service), 'class:ok');
+ await root.fiber.dispose();
+});
+
+test('executor bindings pin one provider generation', async () => {
+ const root = new Context();
+ const service = new PluginExecutorService(root);
+ const previous = plugin(root, 'profile', 'provider', 1);
+ const disposePrevious = previous.executors.register(provider('previous'));
+ const previousBinding = service.bind('session-a', 'remote');
+ const candidateOwner = plugin(root, 'profile', 'provider', 2);
+ const transaction = new MakaPluginTransactionBuffer(candidateOwner);
+ const candidate = candidateOwner.extend({ makaTransaction: transaction });
+ candidate.executors.register(provider('candidate'));
+ await transaction.commit();
+
+ assert.equal((await previousBinding.execute(request('session-a'))).status, 'completed');
+ assert.equal(await executeText(service), 'candidate');
+ await disposePrevious();
+ assert.deepEqual(await previousBinding.execute(request('session-a')), {
+ status: 'cancelled',
+ source: 'executor_retired',
+ reason: 'Executor was retired: remote',
+ });
+ await root.fiber.dispose();
+});
+
+test('executor completion after caller cancellation is normalized to cancelled', async () => {
+ const root = new Context();
+ const service = new PluginExecutorService(root);
+ const owner = plugin(root, 'profile', 'provider', 1);
+ let started!: () => void;
+ const ready = new Promise<void>((resolve) => {
+ started = resolve;
+ });
+ owner.executors.register({
+ id: 'remote',
+ execute: async (_request, context) => {
+ started();
+ await new Promise<void>((resolve) =>
+ context.signal.addEventListener('abort', () => resolve()),
+ );
+ return { status: 'completed', text: 'late success' };
+ },
+ });
+ const abort = new AbortController();
+ const execution = service.execute('remote', request('session-a'), { signal: abort.signal });
+ await ready;
+ abort.abort(new Error('redirect'));
+ assert.deepEqual(await execution, { status: 'cancelled', source: 'caller', reason: 'redirect' });
+ await root.fiber.dispose();
+});
+
+test('executor rich events require an explicitly declared capability', async () => {
+ const root = new Context();
+ const service = new PluginExecutorService(root);
+ plugin(root, 'profile', 'provider', 1).executors.register({
+ id: 'remote',
+ execute: async (_request, context) => {
+ context.emit({ type: 'thinking_delta', text: 'undeclared' });
+ return { status: 'completed', text: 'unreachable' };
+ },
+ });
+
+ await assert.rejects(
+ () => service.execute('remote', request('session-a')),
+ /invalid or undeclared/u,
+ );
+ await root.fiber.dispose();
+});
+
+function plugin(
+ root: Context,
+ rootId: 'profile' | `session:${string}`,
+ entryId: string,
+ generation: number,
+) {
+ return root.extend({
+ maka: { rootId, packageId: 'fixture', entryId, generation },
+ });
+}
+
+function provider(text: string) {
+ return {
+ id: 'remote',
+ execute: async () => ({ status: 'completed' as const, text }),
+ };
+}
+
+function request(sessionId: string) {
+ return {
+ sessionId,
+ turnId: 'turn-a',
+ conversationKey: sessionId,
+ text: 'hello',
+ cwd: '/workspace',
+ };
+}
+
+async function executeText(service: PluginExecutorService): Promise<string> {
+ const result = await service.execute('remote', request('session-a'));
+ assert.equal(result.status, 'completed');
+ return result.text;
+}
diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts
index 7c525616ec..9204c0994a 100644
--- a/packages/runtime/src/__tests__/session-manager.test.ts
+++ b/packages/runtime/src/__tests__/session-manager.test.ts
@@ -747,6 +747,56 @@ describe('SessionManager graph operator provisioning', () => {
assert.deepStrictEqual(await runStore.listSessionInvocations(result.header.id), []);
});
+ test('provisions a graph child on an explicit plugin executor without Maka tools', async () => {
+ const store = new MemorySessionStore();
+ const runStore = new MemoryAgentRunStore();
+ const checkedExecutors: string[] = [];
+ const manager = new SessionManager({
+ store,
+ runStore,
+ runtimeEventStore: runStore,
+ backends: new BackendRegistry(),
+ childTools: [],
+ assertChildExecutorAvailable: (parentSessionId, executorId) => {
+ checkedExecutors.push(`${parentSessionId}:${executorId}`);
+ },
+ newId: nextId(),
+ now: nextNow(40),
+ });
+ const parent = await manager.createSession(makeInput());
+ await seedInvocationFromHeader(
+ runStore,
+ makeRunHeader({
+ sessionId: parent.id,
+ runId: 'supervisor-run',
+ turnId: 'supervisor-turn',
+ }),
+ );
+
+ const result = await manager.provisionAgentGraphOperator({
+ graphId: 'graph-external',
+ workId: `graph_work_${'4'.repeat(32)}`,
+ agentId: LOCAL_READ_AGENT_ID,
+ executorId: 'codex',
+ operatorId: `graph_operator_${'5'.repeat(32)}`,
+ source: {
+ sessionId: parent.id,
+ runId: 'supervisor-run',
+ turnId: 'supervisor-turn',
+ toolCallId: 'schedule-tool',
+ },
+ edges: [],
+ expectedScheduleRevision: 1,
+ });
+
+ assert.strictEqual(result.header.backend, 'plugin-executor');
+ assert.strictEqual(result.header.executorId, 'codex');
+ assert.strictEqual(result.header.llmConnectionId, undefined);
+ assert.strictEqual(result.header.llmConnectionSlug, 'executor:codex');
+ assert.deepStrictEqual(result.header.subagentRuntime?.toolNames, []);
+ assert.deepStrictEqual(checkedExecutors, [`${parent.id}:codex`]);
+ });
+
test('keeps four large graph branches and a replacement off the supervisor data plane', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
@@ -2273,6 +2323,87 @@ describe('SessionManager claimed graph intent execution', () => {
});
describe('SessionManager child-session runtime primitive', () => {
+ test('runs an explicitly delegated child through a plugin executor with no Maka tools', async () => {
+ const store = new MemorySessionStore();
+ const runStore = new MemoryAgentRunStore();
+ const backends = new BackendRegistry();
+ const parentGate = makeGate();
+ let childContext: BackendFactoryContext | undefined;
+ const checkedExecutors: string[] = [];
+ backends.register('ai-sdk', (ctx) => new TestBackend(ctx, parentGate));
+ backends.register('plugin-executor', (ctx) => {
+ childContext = ctx;
+ return {
+ kind: 'plugin-executor' as const,
+ sessionId: ctx.sessionId,
+ async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
+ yield {
+ type: 'text_complete',
+ id: 'external-complete-text',
+ turnId: input.turnId,
+ ts: 1,
+ messageId: 'external-message',
+ text: 'external result',
+ };
+ yield {
+ type: 'complete',
+ id: 'external-complete',
+ turnId: input.turnId,
+ ts: 2,
+ stopReason: 'end_turn',
+ };
+ },
+ async stop() {},
+ async respondToSandboxBoundary() {},
+ async dispose() {},
+ };
+ });
+ const manager = new SessionManager({
+ store,
+ runStore,
+ runtimeEventStore: runStore,
+ backends,
+ childTools: [],
+ assertChildExecutorAvailable: (parentSessionId, executorId) => {
+ checkedExecutors.push(`${parentSessionId}:${executorId}`);
+ },
+ newId: nextId(),
+ now: nextNow(80),
+ });
+ const parent = await manager.createSession(makeInput());
+ const parentTurn = manager
+ .sendMessage(parent.id, { turnId: 'parent-turn', text: 'delegate externally' })
+ [Symbol.asyncIterator]();
+ await parentTurn.next();
+ const [parentRun] = await runStore.listSessionInvocations(parent.id);
+ if (!parentRun) throw new Error('parent run was not recorded');
+
+ const result = await manager.spawnChildSession(parent.id, {
+ spawnedBy: {
+ parentRunId: parentRun.runId,
+ parentTurnId: parentRun.turnId,
+ toolCallId: 'tool-call-external',
+ },
+ agentProfile: LOCAL_READ_AGENT_PROFILE,
+ executorId: 'codex',
+ prompt: 'inspect through codex',
+ });
+ const child = await store.readHeader(result.childSessionId);
+
+ assert.strictEqual(result.status, 'completed');
+ assert.strictEqual(result.summary, 'external result');
+ assert.strictEqual(child.backend, 'plugin-executor');
+ assert.strictEqual(child.executorId, 'codex');
+ assert.strictEqual(child.llmConnectionId, undefined);
+ assert.deepStrictEqual(child.subagentRuntime?.toolNames, []);
+ assert.deepStrictEqual(childContext?.tools, []);
+ assert.strictEqual(childContext?.systemPrompt, LOCAL_READ_AGENT_DEFINITION.systemPrompt);
+ assert.deepStrictEqual(checkedExecutors, [`${parent.id}:codex`]);
+
+ parentGate.release();
+ while (!(await parentTurn.next()).done) {}
+ });
+
test('creates a fresh read-only child with a session-inline first run and no parent history', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
@@ -3516,7 +3647,9 @@ describe('SessionManager manual compaction and quiescent session changes', () =>
{
runId: sourceRun.runId,
connectionId:
- sourceRoute.provenance === 'runtime' ? sourceRoute.llmConnectionId : undefined,
+ sourceRoute.provenance === 'runtime' && sourceRoute.backendKind !== 'plugin-executor'
+ ? sourceRoute.llmConnectionId
+ : undefined,
modelId: sourceRoute.modelId,
},
],
@@ -12940,7 +13073,8 @@ class CompactingTestBackend extends TestBackend {
runtimeContextCount: input.runtimeContext.length,
sourceRoutes: (input.runtimeContextInvocations ?? []).map((run) => ({
runId: run.runId,
- ...(run.opening.route.provenance === 'runtime'
+ ...(run.opening.route.provenance === 'runtime' &&
+ run.opening.route.backendKind !== 'plugin-executor'
? { connectionId: run.opening.route.llmConnectionId }
: {}),
modelId: run.opening.route.modelId,
@@ -13756,7 +13890,8 @@ class MemorySessionStore implements SessionStore {
...(input.revisionIndex !== undefined ? { revisionIndex: input.revisionIndex } : {}),
...(input.revisionState ? { revisionState: input.revisionState } : {}),
hasUnread: false,
- backend: 'ai-sdk',
+ backend: input.executorId ? 'plugin-executor' : 'ai-sdk',
+ ...(input.executorId ? { executorId: input.executorId } : {}),
...(input.llmConnectionId === undefined ? {} : { llmConnectionId: input.llmConnectionId }),
llmConnectionSlug: input.llmConnectionSlug,
connectionLocked: input.subagentParent !== undefined,
@@ -15093,7 +15228,7 @@ function testInvocationOpening(header: TestRunHeader): RuntimeEventInvocationOpe
kind: 'invocation_opened',
protocol: 'invocation_opened_v1',
route:
- header.llmConnectionId === undefined
+ header.llmConnectionId === undefined || header.backendKind === 'plugin-executor'
? {
provenance: 'unknown',
backendKind: header.backendKind,
diff --git a/packages/runtime/src/__tests__/subagent-tools.test.ts b/packages/runtime/src/__tests__/subagent-tools.test.ts
index 0a26942d44..08434de0f7 100644
--- a/packages/runtime/src/__tests__/subagent-tools.test.ts
+++ b/packages/runtime/src/__tests__/subagent-tools.test.ts
@@ -111,6 +111,7 @@ describe('subagent tools', () => {
assert.deepStrictEqual(Object.keys(await advertisedProperties(buildSubagentSpawnTool())), [
'profile',
'subagent_id',
+ 'executor_id',
'task',
'write_back',
'isolation',
@@ -412,6 +413,7 @@ describe('subagent tools', () => {
const result = await tool.impl(
{
profile: LOCAL_READ_AGENT_PROFILE,
+ executor_id: 'codex',
task: 'Inspect the runtime tests.',
},
{
@@ -464,10 +466,12 @@ describe('subagent tools', () => {
assert.strictEqual(calls.length, 1);
const call = calls[0] as {
agentProfile: string;
+ executorId?: string;
prompt: string;
onEvent?: (event: SessionEvent) => void;
};
assert.strictEqual(call.agentProfile, LOCAL_READ_AGENT_PROFILE);
+ assert.strictEqual(call.executorId, 'codex');
assert.strictEqual(call.prompt, 'Inspect the runtime tests.');
assert.strictEqual(typeof call.onEvent, 'function');
assert.deepStrictEqual(output, [
diff --git a/packages/runtime/src/agent-run.ts b/packages/runtime/src/agent-run.ts
index e693903960..eac4fbaed6 100644
--- a/packages/runtime/src/agent-run.ts
+++ b/packages/runtime/src/agent-run.ts
@@ -29,6 +29,7 @@ import type { RuntimeEventStore } from '@maka/core/runtime-event-store';
import { isRuntimeHandoffPause, type RuntimeHandoffIntent } from '@maka/core/runtime-handoff';
import { RunHandoffGate, type RunHandoffRequest } from './run-handoff-gate.js';
import { preserveHandoffOpening } from './runtime-resume.js';
+import { runtimeInvocationRouteForHeader } from './runtime-invocation-route.js';
import type {
RequestCompositionSnapshot,
RequestCompositionSnapshotInput,
@@ -1444,22 +1445,7 @@ export class AgentRun {
const opening: RuntimeEventInvocationOpenedContent = {
kind: 'invocation_opened',
protocol: 'invocation_opened_v1',
- route:
- this.header.llmConnectionId === undefined
- ? {
- provenance: 'unknown',
- backendKind: this.header.backend,
- llmConnectionSlug: this.header.llmConnectionSlug,
- modelId: this.header.model,
- }
- : {
- provenance: 'runtime',
- backendKind: this.header.backend,
- llmConnectionId: this.header.llmConnectionId,
- llmConnectionSlug: this.header.llmConnectionSlug,
- modelId: this.header.model,
- ...(providerStateIdentity ? { providerStateIdentity } : {}),
- },
+ route: runtimeInvocationRouteForHeader(this.header, providerStateIdentity),
configuration: {
cwd: this.header.cwd,
permissionMode: this.header.permissionMode,
diff --git a/packages/runtime/src/ai-sdk-compaction.ts b/packages/runtime/src/ai-sdk-compaction.ts
index a515fe02bd..6c5004ee37 100644
--- a/packages/runtime/src/ai-sdk-compaction.ts
+++ b/packages/runtime/src/ai-sdk-compaction.ts
@@ -492,7 +492,9 @@ export class AiSdkCompaction {
const route = invocation.opening.route;
return {
runId: invocation.runId,
- ...(route.provenance === 'runtime' ? { connectionId: route.llmConnectionId } : {}),
+ ...(route.provenance === 'runtime' && route.backendKind !== 'plugin-executor'
+ ? { connectionId: route.llmConnectionId }
+ : {}),
modelId: route.modelId,
};
})
@@ -1498,6 +1500,7 @@ function persistedRequestAnchor(
const route = invocations.find((candidate) => candidate.runId === event?.runId)?.opening.route;
if (
route?.provenance !== 'runtime' ||
+ route.backendKind === 'plugin-executor' ||
route.modelId !== modelId ||
route.llmConnectionId !== connectionId
) {
diff --git a/packages/runtime/src/history-compaction.ts b/packages/runtime/src/history-compaction.ts
index 69210d4893..6c4a15edc1 100644
--- a/packages/runtime/src/history-compaction.ts
+++ b/packages/runtime/src/history-compaction.ts
@@ -255,7 +255,12 @@ function acceptedInputBoundary(
const onRoute = (event: RuntimeEvent | undefined): boolean => {
if (event?.role !== 'model') return false;
const opened = invocations.find((candidate) => candidate.runId === event.runId)?.opening.route;
- if (opened?.provenance !== 'runtime' || opened.modelId !== route.modelId) return false;
+ if (
+ opened?.provenance !== 'runtime' ||
+ opened.backendKind === 'plugin-executor' ||
+ opened.modelId !== route.modelId
+ )
+ return false;
return opened.llmConnectionId === route.connectionId;
};
let index = -1;
diff --git a/packages/runtime/src/plugin-agent-service.ts b/packages/runtime/src/plugin-agent-service.ts
index 727ee4639b..ea298d1da1 100644
--- a/packages/runtime/src/plugin-agent-service.ts
+++ b/packages/runtime/src/plugin-agent-service.ts
@@ -57,6 +57,7 @@ export interface PluginAgentCreateOptions {
readonly cwd?: string;
readonly prompt?: string;
readonly agentProfile?: AgentProfile;
+ readonly executorId?: string;
readonly model?: string;
readonly permissionMode?: PermissionMode;
readonly signal?: AbortSignal;
diff --git a/packages/runtime/src/plugin-executor-backend.ts b/packages/runtime/src/plugin-executor-backend.ts
new file mode 100644
index 0000000000..4bedece094
--- /dev/null
+++ b/packages/runtime/src/plugin-executor-backend.ts
@@ -0,0 +1,388 @@
+/*
+ * 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 { randomUUID } from 'node:crypto';
+import type { SessionEvent } from '@maka/core/events';
+import type { AgentBackend, BackendSendInput } from '@maka/core/backend-types';
+import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary';
+import type { UserQuestionResponse } from '@maka/core/user-question';
+import { AsyncEventQueue } from './async-queue.js';
+import type {
+ PluginExecutorBinding,
+ PluginExecutorOutputEvent,
+ PluginExecutorResult,
+} from './plugin-executor-service.js';
+
+interface ActiveExecution {
+ readonly abort: AbortController;
+ readonly settled: Promise<void>;
+}
+
+export interface PluginExecutorBackendInput {
+ readonly sessionId: string;
+ readonly cwd: string;
+ readonly instructions?: string;
+ readonly binding: PluginExecutorBinding;
+ readonly newId?: () => string;
+ readonly now?: () => number;
+}
+
+/** Converts a small plugin executor contract into Maka's existing Run event stream. */
+export class PluginExecutorBackend implements AgentBackend {
+ readonly kind = 'plugin-executor' as const;
+ readonly sessionId: string;
+ readonly #cwd: string;
+ readonly #instructions?: string;
+ readonly #binding: PluginExecutorBinding;
+ readonly #newId: () => string;
+ readonly #now: () => number;
+ readonly #active = new Set<ActiveExecution>();
+ #disposed = false;
+
+ constructor(input: PluginExecutorBackendInput) {
+ this.sessionId = input.sessionId;
+ this.#cwd = input.cwd;
+ this.#instructions = input.instructions;
+ this.#binding = input.binding;
+ this.#newId = input.newId ?? randomUUID;
+ this.#now = input.now ?? Date.now;
+ }
+
+ async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
+ if (this.#disposed) throw new Error('Plugin executor backend is disposed');
+ const abort = new AbortController();
+ const queue = new AsyncEventQueue<SessionEvent>();
+ const messageId = this.#newId();
+ const producer = this.#produce(input, messageId, abort.signal, queue).finally(() =>
+ queue.close(),
+ );
+ const active: ActiveExecution = { abort, settled: producer };
+ this.#active.add(active);
+ try {
+ for await (const event of queue) {
+ yield event;
+ queue.ackConsumed();
+ }
+ await producer;
+ } finally {
+ queue.noteConsumerDetached();
+ abort.abort(new Error('Plugin executor event consumer detached'));
+ await producer.catch(() => undefined);
+ this.#active.delete(active);
+ }
+ }
+
+ async stop(reason: 'user_stop' | 'redirect'): Promise<void> {
+ const active = [...this.#active];
+ for (const execution of active) execution.abort.abort(new Error(reason));
+ await Promise.allSettled(active.map((execution) => execution.settled));
+ }
+
+ async respondToSandboxBoundary(_response: SandboxBoundaryResponse): Promise<void> {
+ throw new Error('Plugin executor does not expose Maka sandbox-boundary requests');
+ }
+
+ async respondToUserQuestion(_response: UserQuestionResponse): Promise<void> {
+ throw new Error('Plugin executor does not expose Maka user-question requests');
+ }
+
+ async dispose(): Promise<void> {
+ if (this.#disposed) return;
+ this.#disposed = true;
+ await this.stop('user_stop');
+ }
+
+ async #produce(
+ input: BackendSendInput,
+ messageId: string,
+ signal: AbortSignal,
+ queue: AsyncEventQueue<SessionEvent>,
+ ): Promise<void> {
+ const turnId = input.turnId;
+ let thinkingText = '';
+ const toolUseIds = new Map<string, string>();
+ let result: PluginExecutorResult | undefined;
+ let failure: unknown;
+ let failed = false;
+ try {
+ result = await this.#binding.execute(
+ {
+ sessionId: this.sessionId,
+ turnId,
+ ...(input.runId ? { runId: input.runId } : {}),
+ conversationKey: this.sessionId,
+ text: input.text,
+ cwd: this.#cwd,
+ ...(this.#instructions ? { instructions: this.#instructions } : {}),
+ ...(input.attachments ? { attachments: input.attachments } : {}),
+ ...(input.directoryReferences ? { directoryReferences: input.directoryReferences } : {}),
+ ...(input.quotes ? { quotes: input.quotes } : {}),
+ },
+ {
+ signal,
+ onEvent: (event) => {
+ if (event.type === 'thinking_delta') thinkingText += event.text;
+ this.#publishOutputEvent(turnId, messageId, event, toolUseIds, queue);
+ },
+ },
+ );
+ } catch (error) {
+ failed = true;
+ failure = error;
+ }
+
+ this.#closeOptionalOutput(turnId, messageId, thinkingText, toolUseIds, queue);
+ if (failed) {
+ if (signal.aborted)
+ this.#publishCancellation(turnId, { status: 'cancelled', source: 'caller' }, queue);
+ else
+ this.#publishFailure(
+ turnId,
+ failure instanceof Error ? failure.message : 'External executor failed',
+ undefined,
+ false,
+ queue,
+ );
+ return;
+ }
+ if (result === undefined) {
+ this.#publishFailure(
+ turnId,
+ 'External executor returned no terminal result',
+ undefined,
+ false,
+ queue,
+ );
+ return;
+ }
+ this.#publishResult(turnId, messageId, result, queue);
+ }
+
+ #closeOptionalOutput(
+ turnId: string,
+ messageId: string,
+ thinkingText: string,
+ toolUseIds: Map<string, string>,
+ queue: AsyncEventQueue<SessionEvent>,
+ ): void {
+ if (thinkingText) {
+ queue.push({
+ type: 'thinking_complete',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ messageId,
+ text: thinkingText,
+ });
+ }
+ for (const toolUseId of toolUseIds.values()) {
+ queue.push({
+ type: 'tool_result',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ toolUseId,
+ providerExecuted: true,
+ isError: true,
+ content: { kind: 'text', text: 'External executor ended before reporting a tool result' },
+ });
+ }
+ }
+
+ #publishResult(
+ turnId: string,
+ messageId: string,
+ result: PluginExecutorResult,
+ queue: AsyncEventQueue<SessionEvent>,
+ ): void {
+ if (result.status === 'completed') {
+ queue.push({
+ type: 'text_complete',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ messageId,
+ text: result.text,
+ });
+ queue.push({
+ type: 'complete',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ stopReason: 'end_turn',
+ });
+ return;
+ }
+ if (result.status === 'cancelled') {
+ this.#publishCancellation(turnId, result, queue);
+ return;
+ }
+ this.#publishFailure(turnId, result.message, result.code, result.recoverable ?? false, queue);
+ }
+
+ #publishCancellation(
+ turnId: string,
+ result: Extract<PluginExecutorResult, { status: 'cancelled' }>,
+ queue: AsyncEventQueue<SessionEvent>,
+ ): void {
+ const reason = cancellationEventReason(result);
+ queue.push({
+ type: 'abort',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ reason,
+ });
+ queue.push({
+ type: 'complete',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ stopReason: 'user_stop',
+ });
+ }
+
+ #publishOutputEvent(
+ turnId: string,
+ messageId: string,
+ event: PluginExecutorOutputEvent,
+ toolUseIds: Map<string, string>,
+ queue: AsyncEventQueue<SessionEvent>,
+ ): void {
+ if (event.type === 'output_delta') {
+ if (!event.text) return;
+ queue.push({
+ type: 'text_delta',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ messageId,
+ text: event.text,
+ });
+ return;
+ }
+ if (event.type === 'thinking_delta') {
+ if (!event.text) return;
+ queue.push({
+ type: 'thinking_delta',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ messageId,
+ text: event.text,
+ });
+ return;
+ }
+ if (event.type === 'tool_start') {
+ const previousToolUseId = toolUseIds.get(event.toolCallId);
+ if (previousToolUseId) {
+ queue.push({
+ type: 'tool_result',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ toolUseId: previousToolUseId,
+ providerExecuted: true,
+ isError: true,
+ content: { kind: 'text', text: 'External executor reused an active tool call id' },
+ });
+ }
+ const toolUseId = this.#newId();
+ toolUseIds.set(event.toolCallId, toolUseId);
+ queue.push({
+ type: 'tool_start',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ toolUseId,
+ toolName: event.name,
+ args: event.input ?? {},
+ providerExecuted: true,
+ stepId: messageId,
+ ...(event.displayName === undefined ? {} : { displayName: event.displayName }),
+ ...(event.activityKind === undefined ? {} : { activityKind: event.activityKind }),
+ });
+ return;
+ }
+ const toolUseId = toolUseIds.get(event.toolCallId);
+ if (!toolUseId) return;
+ if (event.type === 'tool_progress') {
+ if (!event.text) return;
+ queue.push({
+ type: 'tool_progress',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ toolUseId,
+ chunk: event.text,
+ });
+ return;
+ }
+ toolUseIds.delete(event.toolCallId);
+ queue.push({
+ type: 'tool_result',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ toolUseId,
+ providerExecuted: true,
+ isError: event.isError ?? false,
+ content: { kind: 'text', text: event.text },
+ });
+ }
+
+ #publishFailure(
+ turnId: string,
+ message: string,
+ code: string | undefined,
+ recoverable: boolean,
+ queue: AsyncEventQueue<SessionEvent>,
+ ): void {
+ queue.push({
+ type: 'error',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ recoverable,
+ ...(code ? { code, reason: code } : {}),
+ message: boundedMessage(message),
+ });
+ queue.push({
+ type: 'complete',
+ id: this.#newId(),
+ turnId,
+ ts: this.#now(),
+ stopReason: 'error',
+ });
+ }
+}
+
+function boundedMessage(value: string): string {
+ if (value.length <= 8_192) return value;
+ return `${value.slice(0, 8_191)}…`;
+}
+
+function cancellationEventReason(
+ result: Extract<PluginExecutorResult, { status: 'cancelled' }>,
+): 'user_stop' | 'redirect' | 'timeout' | 'crash' {
+ if (result.reason === 'redirect') return 'redirect';
+ if (result.reason === 'timeout') return 'timeout';
+ if (result.source === 'executor_retired') return 'crash';
+ return 'user_stop';
+}
diff --git a/packages/runtime/src/plugin-executor-service.ts b/packages/runtime/src/plugin-executor-service.ts
new file mode 100644
index 0000000000..26eb19fe39
--- /dev/null
+++ b/packages/runtime/src/plugin-executor-service.ts
@@ -0,0 +1,529 @@
+/*
+ * 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 { createHash } from 'node:crypto';
+import type {
+ AttachmentRef,
+ DirectoryReference,
+ QuoteRef,
+ ToolActivityKind,
+} from '@maka/core/events';
+import { TOOL_ACTIVITY_KINDS } from '@maka/core/events';
+import { isExecutorId } from '@maka/core/executor-id';
+import { Service, type Context, type Disposable } from './plugin-kernel.js';
+import {
+ MakaPluginRuntimeError,
+ pluginIdentity,
+ registerPluginContribution,
+ type MakaContributionIdentity,
+ type MakaPluginRootId,
+} from './plugin-runtime.js';
+import { PluginScopeRegistry } from './plugin-scope-registry.js';
+
+declare module './plugin-kernel.js' {
+ interface Context {
+ readonly executors: PluginExecutorService;
+ }
+}
+
+export interface PluginExecutorRequest {
+ readonly sessionId: string;
+ readonly turnId: string;
+ readonly runId?: string;
+ /** Stable key a provider may use to retain its own external conversation. */
+ readonly conversationKey: string;
+ readonly text: string;
+ readonly cwd: string;
+ /** Child-agent instruction when this request belongs to a linked child Session. */
+ readonly instructions?: string;
+ readonly attachments?: readonly AttachmentRef[];
+ readonly directoryReferences?: readonly DirectoryReference[];
+ readonly quotes?: readonly QuoteRef[];
+}
+
+/** Optional presentation capabilities. Text output and terminal results are always supported. */
+export interface PluginExecutorCapabilities {
+ readonly thinking?: boolean;
+ readonly toolActivity?: boolean;
+}
+
+export type PluginExecutorOutputEvent =
+ | { readonly type: 'output_delta'; readonly text: string }
+ | { readonly type: 'thinking_delta'; readonly text: string }
+ | {
+ readonly type: 'tool_start';
+ readonly toolCallId: string;
+ readonly name: string;
+ readonly input?: unknown;
+ readonly displayName?: string;
+ readonly activityKind?: ToolActivityKind;
+ }
+ | { readonly type: 'tool_progress'; readonly toolCallId: string; readonly text: string }
+ | {
+ readonly type: 'tool_result';
+ readonly toolCallId: string;
+ readonly text: string;
+ readonly isError?: boolean;
+ };
+
+export type PluginExecutorCancellationSource = 'provider' | 'caller' | 'executor_retired';
+
+export type PluginExecutorResult =
+ | { readonly status: 'completed'; readonly text: string }
+ | {
+ readonly status: 'cancelled';
+ readonly reason?: string;
+ /** Service-owned provenance; provider-supplied values are ignored. */
+ readonly source?: PluginExecutorCancellationSource;
+ }
+ | {
+ readonly status: 'failed';
+ readonly message: string;
+ readonly code?: string;
+ readonly recoverable?: boolean;
+ };
+
+export interface PluginExecutorContext {
+ readonly signal: AbortSignal;
+ emit(event: PluginExecutorOutputEvent): void;
+}
+
+/** A black-box executor contributed by one Host plugin. */
+export interface PluginExecutorProvider {
+ readonly id: string;
+ readonly displayName?: string;
+ readonly capabilities?: PluginExecutorCapabilities;
+ execute(
+ request: Readonly<PluginExecutorRequest>,
+ context: PluginExecutorContext,
+ ): Promise<PluginExecutorResult>;
+}
+
+export interface PluginExecutorExecutionOptions {
+ readonly signal?: AbortSignal;
+ readonly onEvent?: (event: PluginExecutorOutputEvent) => void;
+}
+
+export interface PluginExecutorInspection extends MakaContributionIdentity {
+ readonly id: string;
+ readonly displayName: string;
+ readonly capabilities: Readonly<Required<PluginExecutorCapabilities>>;
+}
+
+/** Generation-pinned handle used by one prepared backend instance. */
+export interface PluginExecutorBinding {
+ readonly identity: PluginExecutorInspection;
+ readonly providerStateIdentity: `sha256:${string}`;
+ execute(
+ request: PluginExecutorRequest,
+ options?: PluginExecutorExecutionOptions,
+ ): Promise<PluginExecutorResult>;
+}
+
+export interface PluginExecutorServiceOptions {
+ readonly onChanged?: (rootId: MakaPluginRootId) => void;
+}
+
+interface RegisteredExecutor extends MakaContributionIdentity {
+ readonly provider: PluginExecutorProvider;
+ readonly token: symbol;
+ readonly active: Set<ActiveExecution>;
+ retired: boolean;
+}
+
+interface ActiveExecution {
+ readonly abort: AbortController;
+ readonly settled: Promise<void>;
+}
+
+class ExecutorRetiredAbort extends Error {
+ constructor(executorId: string) {
+ super(`Executor was retired: ${executorId}`);
+ this.name = 'ExecutorRetiredAbort';
+ }
+}
+
+/**
+ * Scoped black-box execution registry.
+ *
+ * The service owns only registration, visibility, cancellation, and result
+ * validation. Protocol processes, credentials, external conversation ids, and
+ * tools remain private to the contributing plugin.
+ */
+export class PluginExecutorService extends Service {
+ private readonly registry = new PluginScopeRegistry<RegisteredExecutor>();
+ private readonly onChanged: ((rootId: MakaPluginRootId) => void) | undefined;
+
+ constructor(ctx: Context, options: PluginExecutorServiceOptions = {}) {
+ super(ctx, 'executors');
+ this.onChanged = options.onChanged;
+ }
+
+ register(provider: PluginExecutorProvider): Disposable<Promise<void>> {
+ validateProvider(provider);
+ const identity = pluginIdentity(this.ctx);
+ return registerPluginContribution(this.ctx, `executors.register(${provider.id})`, () => {
+ const rootId = identity.scopeId as MakaPluginRootId;
+ const existing = this.registry.get(rootId, provider.id);
+ if (existing && existing.entryId !== identity.entryId) {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ `Executor is already registered in this scope: ${provider.id}`,
+ );
+ }
+ const capabilities = normalizeCapabilities(provider.capabilities);
+ const registeredProvider: PluginExecutorProvider = Object.freeze({
+ id: provider.id,
+ ...(provider.displayName === undefined ? {} : { displayName: provider.displayName }),
+ capabilities,
+ execute: provider.execute.bind(provider),
+ });
+ const entry: RegisteredExecutor = {
+ ...identity,
+ provider: registeredProvider,
+ token: Symbol(provider.id),
+ active: new Set(),
+ retired: false,
+ };
+ return this.registry.publish(rootId, provider.id, entry, {
+ ...(this.onChanged ? { onChanged: this.onChanged } : {}),
+ onRetired: async (retired) => {
+ for (const execution of retired.active) {
+ execution.abort.abort(new ExecutorRetiredAbort(retired.provider.id));
+ }
+ await Promise.allSettled([...retired.active].map((execution) => execution.settled));
+ },
+ });
+ });
+ }
+
+ list(sessionId: string): readonly PluginExecutorInspection[] {
+ assertSessionId(sessionId);
+ return Object.freeze(
+ [...this.registry.visible(sessionId).values()]
+ .sort((left, right) => left.provider.id.localeCompare(right.provider.id))
+ .map(({ provider, token: _token, active: _active, retired: _retired, ...identity }) =>
+ Object.freeze({
+ ...identity,
+ id: provider.id,
+ displayName: provider.displayName?.trim() || provider.id,
+ capabilities: normalizeCapabilities(provider.capabilities),
+ }),
+ ),
+ );
+ }
+
+ inspect(rootId?: MakaPluginRootId): readonly PluginExecutorInspection[] {
+ const seen = new Set<RegisteredExecutor>();
+ return Object.freeze(
+ [...this.registry.entries(rootId)]
+ .filter((entry) => !seen.has(entry) && Boolean(seen.add(entry)))
+ .sort((left, right) => left.provider.id.localeCompare(right.provider.id))
+ .map(({ provider, token: _token, active: _active, retired: _retired, ...identity }) =>
+ Object.freeze({
+ ...identity,
+ id: provider.id,
+ displayName: provider.displayName?.trim() || provider.id,
+ capabilities: normalizeCapabilities(provider.capabilities),
+ }),
+ ),
+ );
+ }
+
+ identity(sessionId: string, executorId: string): PluginExecutorInspection {
+ return this.identityForEntry(this.entry(sessionId, executorId));
+ }
+
+ bind(sessionId: string, executorId: string): PluginExecutorBinding {
+ const entry = this.entry(sessionId, executorId);
+ const identity = this.identityForEntry(entry);
+ return Object.freeze({
+ identity,
+ providerStateIdentity: providerStateIdentity(identity),
+ execute: (request: PluginExecutorRequest, options: PluginExecutorExecutionOptions = {}) => {
+ if (request.sessionId !== sessionId) {
+ throw new Error('Executor binding cannot cross Session scope');
+ }
+ return this.executeEntry(entry, request, options);
+ },
+ });
+ }
+
+ async execute(
+ executorId: string,
+ request: PluginExecutorRequest,
+ options: PluginExecutorExecutionOptions = {},
+ ): Promise<PluginExecutorResult> {
+ const normalizedRequest = normalizeRequest(request);
+ return this.executeEntry(
+ this.entry(normalizedRequest.sessionId, executorId),
+ normalizedRequest,
+ options,
+ );
+ }
+
+ private async executeEntry(
+ entry: RegisteredExecutor,
+ request: PluginExecutorRequest,
+ options: PluginExecutorExecutionOptions,
+ ): Promise<PluginExecutorResult> {
+ const normalizedRequest = normalizeRequest(request);
+ const abort = new AbortController();
+ const signal = options.signal ? AbortSignal.any([options.signal, abort.signal]) : abort.signal;
+ let settle!: () => void;
+ const settled = new Promise<void>((resolve) => {
+ settle = resolve;
+ });
+ const active: ActiveExecution = { abort, settled };
+ entry.active.add(active);
+ try {
+ if (entry.retired) return cancelledResult(new ExecutorRetiredAbort(entry.provider.id));
+ try {
+ const result = await entry.provider.execute(normalizedRequest, {
+ signal,
+ emit: (event) => {
+ if (signal.aborted || entry.retired) return;
+ const normalized = normalizeOutputEvent(event, entry.provider.capabilities);
+ try {
+ options.onEvent?.(normalized);
+ } catch {
+ // A presentation observer must not change external execution.
+ }
+ },
+ });
+ if (signal.aborted) return cancelledResult(signal.reason);
+ return normalizeResult(result);
+ } catch (error) {
+ if (signal.aborted) return cancelledResult(signal.reason);
+ throw error;
+ }
+ } finally {
+ entry.active.delete(active);
+ settle();
+ }
+ }
+
+ private entry(sessionId: string, executorId: string): RegisteredExecutor {
+ assertSessionId(sessionId);
+ if (!isExecutorId(executorId)) throw new TypeError('Executor id is invalid');
+ const entry = this.registry.visible(sessionId).get(executorId);
+ if (!entry || entry.retired) throw new Error(`Executor is unavailable: ${executorId}`);
+ return entry;
+ }
+
+ private identityForEntry(entry: RegisteredExecutor): PluginExecutorInspection {
+ return Object.freeze({
+ entryId: entry.entryId,
+ scopeId: entry.scopeId,
+ extensionId: entry.extensionId,
+ generation: entry.generation,
+ id: entry.provider.id,
+ displayName: entry.provider.displayName?.trim() || entry.provider.id,
+ capabilities: normalizeCapabilities(entry.provider.capabilities),
+ });
+ }
+}
+
+function validateProvider(provider: PluginExecutorProvider): void {
+ if (!provider || typeof provider !== 'object')
+ throw new TypeError('Executor provider is required');
+ if (!isExecutorId(provider.id)) throw new TypeError('Executor id is invalid');
+ if (typeof provider.execute !== 'function') {
+ throw new TypeError(`Executor implementation is invalid: ${provider.id}`);
+ }
+ if (
+ provider.displayName !== undefined &&
+ (typeof provider.displayName !== 'string' || !provider.displayName.trim())
+ ) {
+ throw new TypeError(`Executor display name is invalid: ${provider.id}`);
+ }
+}
+
+function assertSessionId(value: string): void {
+ if (!value || /[\0\r\n]/u.test(value)) throw new TypeError('Session id is invalid');
+}
+
+function normalizeRequest(request: PluginExecutorRequest): Readonly<PluginExecutorRequest> {
+ if (!request || typeof request !== 'object') throw new TypeError('Executor request is required');
+ assertSessionId(request.sessionId);
+ for (const [label, value] of [
+ ['turnId', request.turnId],
+ ['conversationKey', request.conversationKey],
+ ['cwd', request.cwd],
+ ] as const) {
+ if (!value || /[\0\r\n]/u.test(value)) throw new TypeError(`Executor ${label} is invalid`);
+ }
+ if (typeof request.text !== 'string') throw new TypeError('Executor request text is invalid');
+ if (request.runId !== undefined && (!request.runId || /[\0\r\n]/u.test(request.runId))) {
+ throw new TypeError('Executor runId is invalid');
+ }
+ if (request.instructions !== undefined && typeof request.instructions !== 'string') {
+ throw new TypeError('Executor instructions are invalid');
+ }
+ return Object.freeze({
+ ...request,
+ ...(request.attachments ? { attachments: Object.freeze([...request.attachments]) } : {}),
+ ...(request.directoryReferences
+ ? { directoryReferences: Object.freeze([...request.directoryReferences]) }
+ : {}),
+ ...(request.quotes ? { quotes: Object.freeze([...request.quotes]) } : {}),
+ });
+}
+
+function normalizeCapabilities(
+ value: PluginExecutorCapabilities | undefined,
+): Readonly<Required<PluginExecutorCapabilities>> {
+ if (
+ value !== undefined &&
+ (!value ||
+ typeof value !== 'object' ||
+ (value.thinking !== undefined && typeof value.thinking !== 'boolean') ||
+ (value.toolActivity !== undefined && typeof value.toolActivity !== 'boolean'))
+ ) {
+ throw new TypeError('Executor capabilities are invalid');
+ }
+ return Object.freeze({
+ thinking: value?.thinking === true,
+ toolActivity: value?.toolActivity === true,
+ });
+}
+
+function normalizeOutputEvent(
+ event: PluginExecutorOutputEvent,
+ capabilities: PluginExecutorCapabilities | undefined,
+): PluginExecutorOutputEvent {
+ if (!event || typeof event !== 'object') throw new TypeError('Executor output event is invalid');
+ if (event.type === 'output_delta' && typeof event.text === 'string') {
+ return Object.freeze({ type: event.type, text: event.text });
+ }
+ if (
+ event.type === 'thinking_delta' &&
+ capabilities?.thinking === true &&
+ isSafeEventText(event.text)
+ ) {
+ return Object.freeze({ type: event.type, text: event.text });
+ }
+ if (
+ event.type === 'tool_start' &&
+ capabilities?.toolActivity === true &&
+ isSafeEventId(event.toolCallId) &&
+ isSafeEventId(event.name) &&
+ (event.displayName === undefined || isSafeEventText(event.displayName)) &&
+ (event.activityKind === undefined || TOOL_ACTIVITY_KINDS.includes(event.activityKind))
+ ) {
+ return Object.freeze({
+ type: event.type,
+ toolCallId: event.toolCallId,
+ name: event.name,
+ ...(event.input === undefined ? {} : { input: structuredClone(event.input) }),
+ ...(event.displayName === undefined ? {} : { displayName: event.displayName }),
+ ...(event.activityKind === undefined ? {} : { activityKind: event.activityKind }),
+ });
+ }
+ if (
+ event.type === 'tool_progress' &&
+ capabilities?.toolActivity === true &&
+ isSafeEventId(event.toolCallId) &&
+ isSafeEventText(event.text)
+ ) {
+ return Object.freeze({ type: event.type, toolCallId: event.toolCallId, text: event.text });
+ }
+ if (
+ event.type === 'tool_result' &&
+ capabilities?.toolActivity === true &&
+ isSafeEventId(event.toolCallId) &&
+ isSafeEventText(event.text) &&
+ (event.isError === undefined || typeof event.isError === 'boolean')
+ ) {
+ return Object.freeze({
+ type: event.type,
+ toolCallId: event.toolCallId,
+ text: event.text,
+ ...(event.isError === undefined ? {} : { isError: event.isError }),
+ });
+ }
+ throw new TypeError('Executor output event is invalid or undeclared');
+}
+
+function normalizeResult(result: PluginExecutorResult): PluginExecutorResult {
+ if (!result || typeof result !== 'object') throw new TypeError('Executor result is invalid');
+ if (result.status === 'completed' && typeof result.text === 'string') {
+ return Object.freeze({ status: result.status, text: result.text });
+ }
+ if (
+ result.status === 'cancelled' &&
+ (result.reason === undefined || typeof result.reason === 'string')
+ ) {
+ return Object.freeze({
+ status: result.status,
+ ...(result.reason === undefined ? {} : { reason: result.reason }),
+ source: 'provider',
+ });
+ }
+ if (
+ result.status === 'failed' &&
+ typeof result.message === 'string' &&
+ (result.code === undefined || typeof result.code === 'string') &&
+ (result.recoverable === undefined || typeof result.recoverable === 'boolean')
+ ) {
+ return Object.freeze({
+ status: result.status,
+ message: result.message,
+ ...(result.code === undefined ? {} : { code: result.code }),
+ ...(result.recoverable === undefined ? {} : { recoverable: result.recoverable }),
+ });
+ }
+ throw new TypeError('Executor result is invalid');
+}
+
+function providerStateIdentity(identity: PluginExecutorInspection): `sha256:${string}` {
+ return `sha256:${createHash('sha256')
+ .update(
+ JSON.stringify([
+ 'plugin-executor.v1',
+ identity.id,
+ identity.extensionId,
+ identity.entryId,
+ identity.generation,
+ ]),
+ )
+ .digest('hex')}`;
+}
+
+function cancelledResult(reason: unknown): PluginExecutorResult {
+ const source: PluginExecutorCancellationSource =
+ reason instanceof ExecutorRetiredAbort ? 'executor_retired' : 'caller';
+ const message =
+ reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : undefined;
+ return Object.freeze({
+ status: 'cancelled',
+ source,
+ ...(message ? { reason: message } : {}),
+ });
+}
+
+function isSafeEventId(value: unknown): value is string {
+ return (
+ typeof value === 'string' && value.length > 0 && value.length <= 256 && !/[\0\r\n]/u.test(value)
+ );
+}
+
+function isSafeEventText(value: unknown): value is string {
+ return typeof value === 'string' && value.length <= 8_192 && !/[\0\r]/u.test(value);
+}
diff --git a/packages/runtime/src/runtime-invocation-route.ts b/packages/runtime/src/runtime-invocation-route.ts
new file mode 100644
index 0000000000..87e53cff8a
--- /dev/null
+++ b/packages/runtime/src/runtime-invocation-route.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { isExecutorId } from '@maka/core/executor-id';
+import type { RuntimeInvocationRoute } from '@maka/core/runtime-event';
+import type { SessionHeader } from '@maka/core/session';
+
+export function runtimeInvocationRouteForHeader(
+ header: SessionHeader,
+ providerStateIdentity: `sha256:${string}` | undefined,
+): RuntimeInvocationRoute {
+ if (
+ header.backend === 'plugin-executor' &&
+ isExecutorId(header.executorId) &&
+ providerStateIdentity
+ ) {
+ return {
+ provenance: 'runtime',
+ backendKind: header.backend,
+ executorId: header.executorId,
+ llmConnectionSlug: header.llmConnectionSlug,
+ modelId: header.model,
+ providerStateIdentity,
+ };
+ }
+ if (header.backend !== 'plugin-executor' && header.llmConnectionId !== undefined) {
+ return {
+ provenance: 'runtime',
+ backendKind: header.backend,
+ llmConnectionId: header.llmConnectionId,
+ llmConnectionSlug: header.llmConnectionSlug,
+ modelId: header.model,
+ ...(providerStateIdentity ? { providerStateIdentity } : {}),
+ };
+ }
+ return {
+ provenance: 'unknown',
+ backendKind: header.backend,
+ llmConnectionSlug: header.llmConnectionSlug,
+ modelId: header.model,
+ };
+}
diff --git a/packages/runtime/src/runtime-kernel.ts b/packages/runtime/src/runtime-kernel.ts
index 83217db13c..efb5327a28 100644
--- a/packages/runtime/src/runtime-kernel.ts
+++ b/packages/runtime/src/runtime-kernel.ts
@@ -151,6 +151,7 @@ import {
import { DeliveryAckQueue, isDeliveryAckQueueClosed } from './delivery-ack-queue.js';
import { runtimeHandoffPause, type RuntimeHandoffIntent } from '@maka/core/runtime-handoff';
import { preserveHandoffOpening } from './runtime-resume.js';
+import { runtimeInvocationRouteForHeader } from './runtime-invocation-route.js';
import type { AgentRunHandoffRequest } from './agent-run.js';
export interface RuntimeKernelLike {
@@ -2679,6 +2680,12 @@ export class RuntimeKernel implements RuntimeKernelLike {
if (!header.subagentParent) {
throw new Error('Subagent runtime snapshot requires a linked child session');
}
+ if (header.backend === 'plugin-executor') {
+ return {
+ systemPrompt: snapshot.systemPrompt,
+ tools: [],
+ };
+ }
const snapshotDefinition = {
id: snapshot.agentId,
permissionMode: header.permissionMode,
@@ -3146,24 +3153,7 @@ function continuationTargetOpeningForExecution(input: {
const opening: RuntimeEventInvocationOpenedContent = {
kind: 'invocation_opened',
protocol: 'invocation_opened_v1',
- route:
- sessionHeader.llmConnectionId === undefined
- ? {
- provenance: 'unknown',
- backendKind: sessionHeader.backend,
- llmConnectionSlug: sessionHeader.llmConnectionSlug,
- modelId: sessionHeader.model,
- }
- : {
- provenance: 'runtime',
- backendKind: sessionHeader.backend,
- llmConnectionId: sessionHeader.llmConnectionId,
- llmConnectionSlug: sessionHeader.llmConnectionSlug,
- modelId: sessionHeader.model,
- ...(input.targetProviderStateIdentity
- ? { providerStateIdentity: input.targetProviderStateIdentity }
- : {}),
- },
+ route: runtimeInvocationRouteForHeader(sessionHeader, input.targetProviderStateIdentity),
configuration: {
cwd: sessionHeader.cwd,
permissionMode: sessionHeader.permissionMode,
diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts
index f6635e2a3c..214c6efc2c 100644
--- a/packages/runtime/src/session-manager.ts
+++ b/packages/runtime/src/session-manager.ts
@@ -308,6 +308,8 @@ export interface SpawnChildSessionInput {
agentProfile: AgentProfile;
/** User-approved catalog selector. The runtime resolves its frozen model target. */
subagentId?: string;
+ /** Optional plugin executor. Non-preset children inherit the parent's executor. */
+ executorId?: string;
prompt: string;
name?: string;
turnId?: string;
@@ -353,6 +355,7 @@ export interface ProvisionAgentGraphOperatorInput {
workId: string;
agentId?: string;
subagentId?: string;
+ executorId?: string;
operatorId: string;
source: AgentGraphScheduleUpdateSource;
edges: AgentGraphProvisionedEdge[];
@@ -524,6 +527,7 @@ export interface SessionConfigurationStoreUpdate {
readonly expectedVersion: number;
readonly configuration: {
readonly backend: SessionHeader['backend'];
+ readonly executorId?: string;
readonly llmConnectionId?: string;
readonly llmConnectionSlug: string;
readonly connectionLocked: boolean;
@@ -820,6 +824,8 @@ interface SessionManagerBaseDeps {
list(): Promise<SubagentPresetListItem[]>;
resolve(id: string): Promise<ResolvedSubagentPreset>;
};
+ /** Host gate for an executor that must remain visible in a newly created child Session. */
+ assertChildExecutorAvailable?: (parentSessionId: string, executorId: string) => void;
/** Host-owned filesystem isolation for worktree-backed child Sessions. */
worktreeChildExecutor?: SubagentWorktreeExecutor;
listArtifactsForTurn?: (sessionId: string, turnId: string) => Promise<ArtifactRecord[]>;
@@ -1035,6 +1041,20 @@ export class SessionManager {
});
}
+ private async resolveChildToolNames(
+ parentSessionId: string,
+ parentHeader: SessionHeader,
+ definition: AgentDefinition,
+ ): Promise<string[]> {
+ const availableChildTools = await this.childToolsForSession(parentSessionId);
+ assertAgentDefinitionRunnable({
+ definition,
+ tools: availableChildTools,
+ worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(parentHeader),
+ });
+ return buildToolsForAgentDefinition(availableChildTools, definition).map((tool) => tool.name);
+ }
+
private async finalizeAndListChildTurnArtifacts(
sessionId: string,
turnId: string,
@@ -2610,15 +2630,13 @@ export class SessionManager {
const definition = resolvedPreset
? requireBuiltinAgentDefinitionByProfile(resolvedPreset.profile)
: requireBuiltinAgentDefinition(input.agentId!);
- const availableChildTools = await this.childToolsForSession(input.source.sessionId);
- assertAgentDefinitionRunnable({
- definition,
- tools: availableChildTools,
- worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(parentHeader),
- });
- const resolvedToolNames = buildToolsForAgentDefinition(availableChildTools, definition).map(
- (tool) => tool.name,
- );
+ const executorId = input.executorId ?? (resolvedPreset ? undefined : parentHeader.executorId);
+ if (executorId) {
+ this.deps.assertChildExecutorAvailable?.(input.source.sessionId, executorId);
+ }
+ const resolvedToolNames = executorId
+ ? []
+ : await this.resolveChildToolNames(input.source.sessionId, parentHeader, definition);
const childPermissionMode =
parentHeader.permissionMode === 'bypass' ? 'bypass' : definition.permissionMode;
@@ -2646,6 +2664,7 @@ export class SessionManager {
toolNames: resolvedToolNames,
categoryPolicy: {},
systemPrompt: definition.systemPrompt,
+ executorId: executorId ?? null,
...(resolvedPreset
? {
preset: {
@@ -2682,20 +2701,28 @@ export class SessionManager {
cwd: workspace?.worktreePath ?? parentHeader.cwd,
...(parentHeader.projectId !== undefined ? { projectId: parentHeader.projectId } : {}),
name: resolvedPreset?.name ?? definition.name,
- ...(resolvedPreset
- ? { llmConnectionId: resolvedPreset.connectionId }
- : parentHeader.llmConnectionId === undefined
- ? {}
- : { llmConnectionId: parentHeader.llmConnectionId }),
- llmConnectionSlug: resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
- model: resolvedPreset?.model ?? parentHeader.model,
- ...(resolvedPreset
- ? resolvedPreset.thinkingLevel !== undefined
- ? { thinkingLevel: resolvedPreset.thinkingLevel }
- : {}
- : parentHeader.thinkingLevel !== undefined
- ? { thinkingLevel: parentHeader.thinkingLevel }
- : {}),
+ ...(executorId
+ ? {
+ executorId,
+ llmConnectionSlug: `executor:${executorId}`,
+ model: executorId,
+ }
+ : {
+ ...(resolvedPreset
+ ? { llmConnectionId: resolvedPreset.connectionId }
+ : parentHeader.llmConnectionId === undefined
+ ? {}
+ : { llmConnectionId: parentHeader.llmConnectionId }),
+ llmConnectionSlug: resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
+ model: resolvedPreset?.model ?? parentHeader.model,
+ ...(resolvedPreset
+ ? resolvedPreset.thinkingLevel !== undefined
+ ? { thinkingLevel: resolvedPreset.thinkingLevel }
+ : {}
+ : parentHeader.thinkingLevel !== undefined
+ ? { thinkingLevel: parentHeader.thinkingLevel }
+ : {}),
+ }),
permissionMode: childPermissionMode,
collaborationMode: 'agent',
orchestrationMode: 'default',
@@ -3203,15 +3230,12 @@ export class SessionManager {
this.assertActiveParentRun(parentSessionId, parentRun, input.spawnedBy.parentTurnId);
const definition = requireBuiltinAgentDefinitionByProfile(input.agentProfile);
- const availableChildTools = await this.childToolsForSession(parentSessionId);
- assertAgentDefinitionRunnable({
- definition,
- tools: availableChildTools,
- worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(parentHeader),
- });
- const resolvedToolNames = buildToolsForAgentDefinition(availableChildTools, definition).map(
- (tool) => tool.name,
- );
+ const executorId =
+ input.executorId ?? (input.resolvedPreset ? undefined : parentHeader.executorId);
+ if (executorId) this.deps.assertChildExecutorAvailable?.(parentSessionId, executorId);
+ const resolvedToolNames = executorId
+ ? []
+ : await this.resolveChildToolNames(parentSessionId, parentHeader, definition);
const proposedTurnId = input.turnId ?? this.deps.newId();
const proposedRunId = input.runId ?? this.deps.newId();
@@ -3225,20 +3249,29 @@ export class SessionManager {
cwd: workspace?.worktreePath ?? parentHeader.cwd,
...(parentHeader.projectId !== undefined ? { projectId: parentHeader.projectId } : {}),
name: input.name ?? input.resolvedPreset?.name ?? definition.name,
- ...(input.resolvedPreset
- ? { llmConnectionId: input.resolvedPreset.connectionId }
- : parentHeader.llmConnectionId === undefined
- ? {}
- : { llmConnectionId: parentHeader.llmConnectionId }),
- llmConnectionSlug: input.resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
- model: input.resolvedPreset?.model ?? parentHeader.model,
- ...(input.resolvedPreset
- ? input.resolvedPreset.thinkingLevel !== undefined
- ? { thinkingLevel: input.resolvedPreset.thinkingLevel }
- : {}
- : parentHeader.thinkingLevel !== undefined
- ? { thinkingLevel: parentHeader.thinkingLevel }
- : {}),
+ ...(executorId
+ ? {
+ executorId,
+ llmConnectionSlug: `executor:${executorId}`,
+ model: executorId,
+ }
+ : {
+ ...(input.resolvedPreset
+ ? { llmConnectionId: input.resolvedPreset.connectionId }
+ : parentHeader.llmConnectionId === undefined
+ ? {}
+ : { llmConnectionId: parentHeader.llmConnectionId }),
+ llmConnectionSlug:
+ input.resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
+ model: input.resolvedPreset?.model ?? parentHeader.model,
+ ...(input.resolvedPreset
+ ? input.resolvedPreset.thinkingLevel !== undefined
+ ? { thinkingLevel: input.resolvedPreset.thinkingLevel }
+ : {}
+ : parentHeader.thinkingLevel !== undefined
+ ? { thinkingLevel: parentHeader.thinkingLevel }
+ : {}),
+ }),
permissionMode: definition.permissionMode,
collaborationMode: 'agent',
orchestrationMode: 'default',
@@ -3956,7 +3989,7 @@ export class SessionManager {
kind: 'invocation_opened',
protocol: 'invocation_opened_v1',
route:
- session.llmConnectionId === undefined
+ session.llmConnectionId === undefined || session.backend === 'plugin-executor'
? {
provenance: 'unknown',
backendKind: session.backend,
@@ -5029,6 +5062,7 @@ export function headerToSummary(h: SessionHeader): SessionSummary {
...(h.revisionIndex !== undefined ? { revisionIndex: h.revisionIndex } : {}),
...(h.revisionState ? { revisionState: h.revisionState } : {}),
backend: h.backend,
+ ...(h.executorId ? { executorId: h.executorId } : {}),
...(h.llmConnectionId === undefined ? {} : { llmConnectionId: h.llmConnectionId }),
llmConnectionSlug: h.llmConnectionSlug,
connectionLocked: h.connectionLocked,
@@ -5124,33 +5158,47 @@ function childSessionRequestFingerprint(
parentSessionId: string,
input: Pick<
ResolvedSpawnChildSessionInput,
- 'spawnedBy' | 'agentProfile' | 'prompt' | 'swarm' | 'resolvedPreset'
+ 'spawnedBy' | 'agentProfile' | 'executorId' | 'prompt' | 'swarm' | 'resolvedPreset'
>,
): string {
- const payload = input.resolvedPreset
+ const payload = input.executorId
? [
- 2,
+ 3,
parentSessionId,
input.spawnedBy.parentRunId,
input.spawnedBy.parentTurnId,
input.spawnedBy.toolCallId,
input.agentProfile,
- input.resolvedPreset,
+ input.executorId,
+ input.resolvedPreset ?? null,
input.prompt,
input.swarm?.swarmId ?? null,
input.swarm?.itemId ?? null,
]
- : [
- 1,
- parentSessionId,
- input.spawnedBy.parentRunId,
- input.spawnedBy.parentTurnId,
- input.spawnedBy.toolCallId,
- input.agentProfile,
- input.prompt,
- input.swarm?.swarmId ?? null,
- input.swarm?.itemId ?? null,
- ];
+ : input.resolvedPreset
+ ? [
+ 2,
+ parentSessionId,
+ input.spawnedBy.parentRunId,
+ input.spawnedBy.parentTurnId,
+ input.spawnedBy.toolCallId,
+ input.agentProfile,
+ input.resolvedPreset,
+ input.prompt,
+ input.swarm?.swarmId ?? null,
+ input.swarm?.itemId ?? null,
+ ]
+ : [
+ 1,
+ parentSessionId,
+ input.spawnedBy.parentRunId,
+ input.spawnedBy.parentTurnId,
+ input.spawnedBy.toolCallId,
+ input.agentProfile,
+ input.prompt,
+ input.swarm?.swarmId ?? null,
+ input.swarm?.itemId ?? null,
+ ];
return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
}
@@ -5201,7 +5249,8 @@ function sessionConfigurationWithPermissionMode(
): SessionConfigurationTransitionRequest['configuration'] {
return {
backend: header.backend,
- ...(header.llmConnectionId === undefined ? {} : { llmConnectionId: header.llmConnectionId }),
+ executorId: header.executorId,
+ llmConnectionId: header.llmConnectionId,
llmConnectionSlug: header.llmConnectionSlug,
connectionLocked: header.connectionLocked,
model: header.model,
@@ -5218,6 +5267,7 @@ function sessionConfigurationMatchesExceptPermissionMode(
): boolean {
return (
header.backend === configuration.backend &&
+ header.executorId === configuration.executorId &&
header.llmConnectionId === configuration.llmConnectionId &&
header.llmConnectionSlug === configuration.llmConnectionSlug &&
header.connectionLocked === configuration.connectionLocked &&
diff --git a/packages/runtime/src/stream-graph-schedule-reconcile.ts b/packages/runtime/src/stream-graph-schedule-reconcile.ts
index f282f2fb55..78e30b63e9 100644
--- a/packages/runtime/src/stream-graph-schedule-reconcile.ts
+++ b/packages/runtime/src/stream-graph-schedule-reconcile.ts
@@ -958,6 +958,7 @@ function buildOperatorProvisionInput(
...(work.target.kind === 'preset'
? { subagentId: work.target.presetId }
: { agentId: work.target.agentId }),
+ ...(work.target.executorId ? { executorId: work.target.executorId } : {}),
operatorId,
source,
edges,
diff --git a/packages/runtime/src/stream-graph-supervisor-tools.ts b/packages/runtime/src/stream-graph-supervisor-tools.ts
index fef66a8d9c..b93fba2ece 100644
--- a/packages/runtime/src/stream-graph-supervisor-tools.ts
+++ b/packages/runtime/src/stream-graph-supervisor-tools.ts
@@ -18,6 +18,7 @@
*/
import { z } from 'zod';
+import { isExecutorId } from '@maka/core/executor-id';
import {
AGENT_GRAPH_SCHEDULE_MAX_ADD_WORK,
AGENT_GRAPH_SCHEDULE_MAX_INPUT_IDS,
@@ -65,6 +66,8 @@ const identitySchema = z
.max(256)
.refine((value) => !/[\u0000-\u001f\u007f]/.test(value), 'Identity contains control characters');
+const executorIdSchema = z.string().refine(isExecutorId, 'Invalid plugin executor id');
+
const cursorSchema = z
.string()
.trim()
@@ -95,6 +98,9 @@ const addWorkSchema = z.preprocess(
.describe(
'Runtime id of an EXISTING graph operator returned by view_agent_graph. Use only for follow-up work; set operator_id OR agent_id, never both.',
),
+ executor_id: executorIdSchema
+ .optional()
+ .describe('Plugin executor id for a newly created agent or preset target.'),
instruction: z.string().trim().min(1).max(AGENT_GRAPH_SCHEDULE_MAX_INSTRUCTION_CHARS),
input_ids: z
.array(identitySchema)
@@ -332,6 +338,7 @@ function cleanAddWorkInput(input: unknown): unknown {
if (cleaned.target_kind === 'existing_operator') {
delete cleaned.agent_id;
delete cleaned.subagent_id;
+ delete cleaned.executor_id;
}
if (cleaned.replacement_mode === 'none') delete cleaned.replaces;
return cleaned;
@@ -373,6 +380,7 @@ export interface UpdateAgentGraphToolInput {
agent_id?: string;
subagent_id?: string;
operator_id?: string;
+ executor_id?: string;
instruction: string;
input_ids?: string[];
selected_result_inputs?: Array<{
@@ -1101,12 +1109,21 @@ function normalizeWorkTarget(input: {
agent_id?: string;
subagent_id?: string;
operator_id?: string;
+ executor_id?: string;
}): AgentGraphWorkTarget {
if (input.target_kind === 'new_agent') {
- return { kind: 'agent', agentId: requireIdentity(input.agent_id, 'agent id') };
+ return {
+ kind: 'agent',
+ agentId: requireIdentity(input.agent_id, 'agent id'),
+ ...(input.executor_id ? { executorId: input.executor_id } : {}),
+ };
}
if (input.target_kind === 'new_preset') {
- return { kind: 'preset', presetId: requireIdentity(input.subagent_id, 'subagent preset id') };
+ return {
+ kind: 'preset',
+ presetId: requireIdentity(input.subagent_id, 'subagent preset id'),
+ ...(input.executor_id ? { executorId: input.executor_id } : {}),
+ };
}
if (input.target_kind === 'existing_operator') {
return {
@@ -1120,10 +1137,18 @@ function normalizeWorkTarget(input: {
throw new Error('Exactly one of subagent_id, agent_id, or operator_id is required');
}
if (input.subagent_id) {
- return { kind: 'preset', presetId: requireIdentity(input.subagent_id, 'subagent preset id') };
+ return {
+ kind: 'preset',
+ presetId: requireIdentity(input.subagent_id, 'subagent preset id'),
+ ...(input.executor_id ? { executorId: input.executor_id } : {}),
+ };
}
return input.agent_id
- ? { kind: 'agent', agentId: requireIdentity(input.agent_id, 'agent id') }
+ ? {
+ kind: 'agent',
+ agentId: requireIdentity(input.agent_id, 'agent id'),
+ ...(input.executor_id ? { executorId: input.executor_id } : {}),
+ }
: { kind: 'operator', operatorId: requireIdentity(input.operator_id, 'operator id') };
}
diff --git a/packages/runtime/src/subagent-tools.ts b/packages/runtime/src/subagent-tools.ts
index ade65bd679..29982e8dba 100644
--- a/packages/runtime/src/subagent-tools.ts
+++ b/packages/runtime/src/subagent-tools.ts
@@ -18,6 +18,7 @@
*/
import { z } from 'zod';
+import { isExecutorId } from '@maka/core/executor-id';
import { decodeCanonicalToolResultContent } from '@maka/core/tool-result-record-schema';
import { isSafeSubagentPresetId } from '@maka/core/subagent-settings';
import { type ToolResultContent } from '@maka/core/events';
@@ -93,6 +94,7 @@ export function buildSubagentSpawnTool(
{
profile?: string;
subagent_id?: string;
+ executor_id?: string;
task: string;
write_back?: string;
isolation?: string;
@@ -118,6 +120,11 @@ export function buildSubagentSpawnTool(
.refine(isSafeSubagentPresetId)
.optional()
.describe('User-approved subagent preset id from agent_list.'),
+ executor_id: z
+ .string()
+ .refine(isExecutorId)
+ .optional()
+ .describe('Plugin executor id for this child task.'),
task: z
.string()
.min(1)
@@ -202,6 +209,7 @@ export function buildSubagentSpawnTool(
await ctx.spawnChildSession({
agentProfile: definition.profile,
...(input.subagent_id ? { subagentId: input.subagent_id } : {}),
+ ...(input.executor_id ? { executorId: input.executor_id } : {}),
prompt: input.task,
onEvent: (event) => progress.observe(event),
}),
diff --git a/packages/runtime/src/tool-runtime.ts b/packages/runtime/src/tool-runtime.ts
index fa8b628c89..b282185fe5 100644
--- a/packages/runtime/src/tool-runtime.ts
+++ b/packages/runtime/src/tool-runtime.ts
@@ -282,6 +282,7 @@ export interface MakaToolContext {
spawnChildSession?: (input: {
agentProfile: AgentProfile;
subagentId?: string;
+ executorId?: string;
prompt: string;
/** Optional swarm identity, scoped to the owning tool call. */
swarm?: {
@@ -419,6 +420,7 @@ export interface ToolRuntimeInput {
toolCallId: string;
agentProfile: AgentProfile;
subagentId?: string;
+ executorId?: string;
prompt: string;
swarm?: {
swarmId: string;
@@ -2572,6 +2574,7 @@ export class ToolRuntime {
toolCallId: input.toolUseId,
agentProfile: spawnInput.agentProfile,
...(spawnInput.subagentId ? { subagentId: spawnInput.subagentId } : {}),
+ ...(spawnInput.executorId ? { executorId: spawnInput.executorId } : {}),
prompt: spawnInput.prompt,
...(spawnInput.swarm ? { swarm: spawnInput.swarm } : {}),
abortSignal,
diff --git a/packages/storage/src/__tests__/session-store.test.ts b/packages/storage/src/__tests__/session-store.test.ts
index 582590be52..dbad1e6bd0 100644
--- a/packages/storage/src/__tests__/session-store.test.ts
+++ b/packages/storage/src/__tests__/session-store.test.ts
@@ -58,6 +58,34 @@ describe('SQLite SessionStore', () => {
}
});
+ test('persists a plugin executor route across reloads and catalog projection', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'maka-plugin-executor-route-'));
+ let store = createSessionStore(root);
+ try {
+ const created = await store.create(
+ makeInput({
+ cwd: root,
+ executorId: 'codex',
+ llmConnectionSlug: 'executor:codex',
+ model: 'codex',
+ }),
+ );
+ assert.equal(created.backend, 'plugin-executor');
+ assert.equal(created.executorId, 'codex');
+ assert.equal(created.llmConnectionId, undefined);
+
+ await store.close?.();
+ store = createSessionStore(root);
+ const reloaded = await store.readHeader(created.id);
+ assert.equal(reloaded.backend, 'plugin-executor');
+ assert.equal(reloaded.executorId, 'codex');
+ assert.equal((await store.list())[0]?.executorId, 'codex');
+ } finally {
+ await store.close?.();
+ await rm(root, { recursive: true, force: true });
+ }
+ });
+
test('requires the reserved WorkHub Coordination identity and role together', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-workhub-coordination-identity-role-'));
const store = createSessionStore(root);
diff --git a/packages/storage/src/legacy-run-header.ts b/packages/storage/src/legacy-run-header.ts
index 7136b7df95..38e4bf3403 100644
--- a/packages/storage/src/legacy-run-header.ts
+++ b/packages/storage/src/legacy-run-header.ts
@@ -353,7 +353,7 @@ export function invocationOpeningFromLegacyRunHeader(
}
function invocationRouteFromLegacyRunHeader(header: LegacyRunHeader): RuntimeInvocationRoute {
- if (header.llmConnectionId === undefined) {
+ if (header.llmConnectionId === undefined || header.backendKind === 'plugin-executor') {
return {
provenance: 'unknown',
backendKind: header.backendKind,
@@ -446,7 +446,7 @@ function isLegacyContinuationSource(value: unknown): value is LegacyContinuation
/** `'fake'` stays accepted: runs written by builds that shipped FakeBackend must keep decoding (#3211). */
function isPersistedBackendKind(value: unknown): value is PersistedBackendKind {
- return value === 'ai-sdk' || value === 'fake';
+ return value === 'ai-sdk' || value === 'plugin-executor' || value === 'fake';
}
function isSha256Digest(value: unknown): value is `sha256:${string}` {
diff --git a/packages/storage/src/session-store.ts b/packages/storage/src/session-store.ts
index 2679e6b21e..9ddf142933 100644
--- a/packages/storage/src/session-store.ts
+++ b/packages/storage/src/session-store.ts
@@ -36,6 +36,7 @@ import {
OPERATIONAL_STATE_DATABASE_NAME,
} from './operational-state-store.js';
import { DEFAULT_SESSION_NAME, normalizeUserSessionName } from '@maka/core/session-name';
+import { isExecutorId } from '@maka/core/executor-id';
import {
decodeCanonicalMessage,
deriveTurnRecords,
@@ -1340,7 +1341,8 @@ function buildSessionHeader(
...(input.revisionIndex !== undefined ? { revisionIndex: input.revisionIndex } : {}),
...(input.revisionState ? { revisionState: input.revisionState } : {}),
hasUnread: false,
- backend: 'ai-sdk',
+ backend: input.executorId ? 'plugin-executor' : 'ai-sdk',
+ ...(input.executorId ? { executorId: input.executorId } : {}),
...(input.llmConnectionId === undefined ? {} : { llmConnectionId: input.llmConnectionId }),
llmConnectionSlug: input.llmConnectionSlug,
// A subagent Session's route is chosen by the spawn that created it and is
@@ -1405,6 +1407,7 @@ export function normalizeSessionHeader(
(header.lastReadMessageId === undefined || typeof header.lastReadMessageId === 'string') &&
typeof header.hasUnread === 'boolean' &&
isPersistedBackendKind(header.backend) &&
+ isValidExecutorSelection(header) &&
(header.llmConnectionId === undefined ||
(typeof header.llmConnectionId === 'string' && header.llmConnectionId.length > 0)) &&
typeof header.llmConnectionSlug === 'string' &&
@@ -1566,7 +1569,14 @@ function isValidSubagentSessionLineage(header: SessionHeader): boolean {
* FakeBackend fail `normalizeSessionHeader` and read back as malformed (#3211).
*/
function isPersistedBackendKind(value: unknown): value is SessionHeader['backend'] {
- return value === 'ai-sdk' || value === 'fake';
+ return value === 'ai-sdk' || value === 'plugin-executor' || value === 'fake';
+}
+
+function isValidExecutorSelection(header: SessionHeader): boolean {
+ if (header.backend === 'plugin-executor') {
+ return isExecutorId(header.executorId);
+ }
+ return header.executorId === undefined;
}
function isFiniteNumber(value: unknown): value is number {
@@ -1641,6 +1651,7 @@ function toSummary(header: SessionHeader): SessionSummary {
...(header.revisionIndex !== undefined ? { revisionIndex: header.revisionIndex } : {}),
...(header.revisionState ? { revisionState: header.revisionState } : {}),
backend: header.backend,
+ ...(header.executorId ? { executorId: header.executorId } : {}),
...(header.llmConnectionId === undefined ? {} : { llmConnectionId: header.llmConnectionId }),
llmConnectionSlug: header.llmConnectionSlug,
connectionLocked: header.connectionLocked,
diff --git a/packages/storage/src/sqlite-session-metadata-store.ts b/packages/storage/src/sqlite-session-metadata-store.ts
index 73d1194aa6..fe7ca77ff3 100644
--- a/packages/storage/src/sqlite-session-metadata-store.ts
+++ b/packages/storage/src/sqlite-session-metadata-store.ts
@@ -391,7 +391,8 @@ export interface SessionConfigurationMetadataUpdate {
readonly expectedVersion: number;
readonly configuration: {
readonly backend: SessionHeader['backend'];
- readonly llmConnectionId: string;
+ readonly executorId?: string;
+ readonly llmConnectionId?: string;
readonly llmConnectionSlug: string;
readonly connectionLocked: boolean;
readonly model: string;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org