You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by xx...@apache.org on 2026/09/08 08:46:41 UTC
(maka) branch feat/plugin-system-prompt-service created (now 8fbc26f5db)
This is an automated email from the ASF dual-hosted git repository.
xxhZs pushed a change to branch feat/plugin-system-prompt-service
in repository https://gitbox.apache.org/repos/asf/maka.git
at 8fbc26f5db feat(plugins): add scoped system prompt service
This branch includes the following new commits:
new 8fbc26f5db feat(plugins): add scoped system prompt service
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 01/01: feat(plugins): add scoped system prompt service
Posted by xx...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
xxhZs pushed a commit to branch feat/plugin-system-prompt-service
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 8fbc26f5dbdbfda484eb9e409954c7dcd509ebb1
Author: xxhZs <84...@users.noreply.github.com>
AuthorDate: Tue Sep 8 16:46:19 2026 +0800
feat(plugins): add scoped system prompt service
---
.../src/__tests__/interactive-run-composer.test.ts | 35 +-
.../src/__tests__/plugin-platform.test.ts | 53 +++
.../src/server/execution-composition.ts | 17 +
.../src/server/interactive-run-composer.ts | 52 +-
.../runtime-host/src/server/plugin-platform.ts | 11 +
packages/runtime/package.json | 1 +
.../__tests__/plugin-system-prompt-service.test.ts | 218 +++++++++
.../runtime/src/plugin-system-prompt-service.ts | 521 +++++++++++++++++++++
8 files changed, 899 insertions(+), 9 deletions(-)
diff --git a/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts b/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts
index 2befc4173f..80bf678f51 100644
--- a/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts
+++ b/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts
@@ -127,6 +127,33 @@ test('an explicit tool profile remains an exact ceiling over scoped Tool additio
assert.equal(composer.resolveTools?.().filter(({ name }) => name === 'Read').length, 1);
});
+test('the composer caches the Host base but reassembles scoped Plugin prompts each step', async () => {
+ let pluginText = 'FIRST_PLUGIN_PROMPT';
+ let assemblies = 0;
+ const composer = createFixtureComposer({
+ resolveAdditionalSystemPrompt: async (_context, baseText) => {
+ assemblies += 1;
+ return {
+ text: `${baseText}\n\n${pluginText}`,
+ sourceRevisions: [{ id: 'plugin.system-prompt', revision: `revision-${assemblies}` }],
+ };
+ },
+ });
+ const context = { sessionId: 'session', turnId: 'turn', cwd: '/workspace' };
+
+ const first = await composer.resolveSystemPrompt(context);
+ pluginText = 'SECOND_PLUGIN_PROMPT';
+ const second = await composer.resolveSystemPrompt(context);
+
+ assert.match(first.text ?? '', /FIRST_PLUGIN_PROMPT/u);
+ assert.match(second.text ?? '', /SECOND_PLUGIN_PROMPT/u);
+ assert.equal(assemblies, 2);
+ assert.deepEqual(
+ second.sourceRevisions.find(({ id }) => id === 'plugin.system-prompt'),
+ { id: 'plugin.system-prompt', revision: 'revision-2' },
+ );
+});
+
function tool(name: string): MakaTool {
return {
name,
@@ -144,7 +171,13 @@ function createFixtureComposer(
skills: {
readCanonicalModelInventory: async () => ({ inventory: [] }),
} as unknown as HostSkillCatalogCoordinator,
- memory: {} as HostMemoryCoordinator,
+ memory: {
+ readPromptProjection: async () => ({
+ bundleRevision: null,
+ memoryRevision: null,
+ body: undefined,
+ }),
+ } as unknown as HostMemoryCoordinator,
sessionTodo: {} as SessionTodoToolStore,
builtinTools: {},
...overrides,
diff --git a/packages/runtime-host/src/__tests__/plugin-platform.test.ts b/packages/runtime-host/src/__tests__/plugin-platform.test.ts
index c07b137cf6..df5aead6fc 100644
--- a/packages/runtime-host/src/__tests__/plugin-platform.test.ts
+++ b/packages/runtime-host/src/__tests__/plugin-platform.test.ts
@@ -25,6 +25,7 @@ import { test } from 'node:test';
import { waitFor } from '@maka/core/test-only/async-primitives';
import { MakaCompositionLoader } from '@maka/runtime/plugin-composition-loader';
import { Context } from '@maka/runtime/plugin-kernel';
+import { PluginSystemPromptService } from '@maka/runtime/plugin-system-prompt-service';
import { PluginToolService } from '@maka/runtime/plugin-tool-service';
import {
decodePluginCompositionApplyInput,
@@ -66,6 +67,7 @@ function createPlatform(
packageLoader,
store,
...(options.tools ? { tools: options.tools } : {}),
+ ...(options.systemPrompt ? { systemPrompt: options.systemPrompt } : {}),
});
testPlatformInternals.set(platform, { composition, packages, store });
return platform;
@@ -482,6 +484,51 @@ test('Package replacement releases a single-provider Service before activating i
}
});
+test('Package lifecycle publishes and retires scoped System Prompt contributions', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'maka-plugin-system-prompt-'));
+ try {
+ const pluginRoot = new Context();
+ const systemPrompt = new PluginSystemPromptService(pluginRoot);
+ const composition = new MakaCompositionLoader({ root: pluginRoot });
+ const platform = createPlatform(join(root, 'control'), { composition, systemPrompt });
+ await platform.recover();
+ await platform.installPackage(
+ await writeFixturePackage(root, 'prompt-package', 'prompt', {
+ systemPrompt: { name: 'plugin:fixture', order: 10, text: 'fixture prompt' },
+ composition: [
+ { type: 'insert', entry: { id: 'prompt-entry', packageId: 'prompt-package' } },
+ ],
+ }),
+ );
+
+ assert.equal(
+ (
+ await systemPrompt.assemble(
+ { sessionId: 'alpha', turnId: 'turn-1', cwd: '/workspace' },
+ 'base',
+ )
+ ).text,
+ 'base\n\nfixture prompt',
+ );
+ assert.equal(platform.inspectSystemPrompt('profile')[0]?.name, 'plugin:fixture');
+
+ await platform.uninstallPackage('prompt-package');
+ assert.equal(
+ (
+ await systemPrompt.assemble(
+ { sessionId: 'alpha', turnId: 'turn-2', cwd: '/workspace' },
+ 'base',
+ )
+ ).text,
+ 'base',
+ );
+ assert.deepEqual(platform.inspectSystemPrompt('profile'), []);
+ await platform.close();
+ } finally {
+ await rm(root, { recursive: true, force: true });
+ }
+});
+
test('package Composition layers override in install order and unwind on uninstall', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-plugin-layers-'));
try {
@@ -1595,6 +1642,11 @@ async function writeFixturePackage(
readonly manifest?: Readonly<Record<string, unknown>>;
readonly composition?: readonly unknown[];
readonly tool?: { readonly name: string; readonly result: unknown };
+ readonly systemPrompt?: {
+ readonly name: string;
+ readonly order: number;
+ readonly text: string;
+ };
} = {},
): Promise<string> {
const source = join(
@@ -1631,6 +1683,7 @@ async function writeFixturePackage(
${options.throwOnApply ? "throw new Error('fixture activation failed');" : ''}
${options.provideService ? `ctx.provide(${JSON.stringify(options.provideService)}, { source: ${JSON.stringify(contributionId)} });` : ''}
${options.tool ? `ctx.tools.register(Object.freeze({ name: ${JSON.stringify(options.tool.name)}, description: 'fixture tool', parameters: {}, impl: async () => (${JSON.stringify(options.tool.result)}) }));` : ''}
+ ${options.systemPrompt ? `ctx.systemPrompt.section(${JSON.stringify(options.systemPrompt)});` : ''}
ctx.effect(() => () => undefined, 'fixture');
} }),
});\n`,
diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts
index f098c3ab81..f8b97f5728 100644
--- a/packages/runtime-host/src/server/execution-composition.ts
+++ b/packages/runtime-host/src/server/execution-composition.ts
@@ -82,6 +82,7 @@ import { type MakaTool } from '@maka/runtime/tool-runtime';
import { Context } from '@maka/runtime/plugin-kernel';
import { MakaCompositionLoader } from '@maka/runtime/plugin-composition-loader';
import { PluginToolService } from '@maka/runtime/plugin-tool-service';
+import { PluginSystemPromptService } from '@maka/runtime/plugin-system-prompt-service';
import { type RuntimeHostedRootAuthority } from '@maka/runtime/message-authority';
import { isHostedExecutionTerminal } from './hosted-execution-authority.js';
import { createAgentGraphControlStore } from '@maka/storage/agent-graph-control-store';
@@ -296,9 +297,11 @@ export async function createExecutionRuntimeHostComposition(
try {
const pluginRoot = new Context();
const pluginTools = new PluginToolService(pluginRoot);
+ const pluginSystemPrompt = new PluginSystemPromptService(pluginRoot);
pluginPlatform = new HostPluginPlatform(context.owner.controlDirectory, {
composition: new MakaCompositionLoader({ root: pluginRoot }),
tools: pluginTools,
+ systemPrompt: pluginSystemPrompt,
});
const pluginPlatformCoordinator = new HostPluginPlatformCoordinator(pluginPlatform);
const openedProjectCatalog = storage.projectCatalog;
@@ -781,6 +784,20 @@ export async function createExecutionRuntimeHostComposition(
requireGraphCoordinator(graphCoordinator).toolsForSession(sessionId),
resolvePluginTools: (sessionId, coreTools) =>
pluginTools.resolveContributions(sessionId, coreTools),
+ resolvePluginSystemPrompt: async (sessionId, promptContext, baseText) => {
+ const assembly = await pluginSystemPrompt.assemble(
+ {
+ sessionId,
+ turnId: promptContext.turnId,
+ cwd: promptContext.cwd,
+ },
+ baseText,
+ );
+ return {
+ text: assembly.text,
+ sourceRevisions: assembly.sourceRevision ? [assembly.sourceRevision] : [],
+ };
+ },
parentAgentTools: childAgentTools.parentTools,
childTools: childAgentTools.childTools,
worktreePatchWriteBackAvailable: true,
diff --git a/packages/runtime-host/src/server/interactive-run-composer.ts b/packages/runtime-host/src/server/interactive-run-composer.ts
index fad8b91a18..213ebb3e65 100644
--- a/packages/runtime-host/src/server/interactive-run-composer.ts
+++ b/packages/runtime-host/src/server/interactive-run-composer.ts
@@ -110,6 +110,11 @@ export interface InteractiveRunComposerInput {
readonly builtinTools?: BuildBuiltinToolsOptions;
readonly hostTools?: readonly MakaTool[];
readonly resolveAdditionalTools?: (hostTools: readonly MakaTool[]) => readonly MakaTool[];
+ /** Reassembles the scoped Plugin prompt surface before each logical model step. */
+ readonly resolveAdditionalSystemPrompt?: (
+ context: HostModelPromptContext,
+ baseText: string | undefined,
+ ) => Promise<ResolvedRunPrompt>;
readonly scheduledTaskTool?: MakaTool;
readonly goalTools?: readonly MakaTool[];
readonly parentAgentTools?: readonly MakaTool[];
@@ -189,8 +194,8 @@ export function createInteractiveRunComposer(input: InteractiveRunComposerInput)
};
const childInstruction = input.childInstruction?.trim();
const runProfile = hostedExecutionRunProfile(input.toolProfile);
- const resolvedSystemPrompts = new Map<string, Promise<ResolvedRunPrompt>>();
- const resolveSystemPrompt = (context: HostModelPromptContext): Promise<ResolvedRunPrompt> => {
+ const resolvedBaseSystemPrompts = new Map<string, Promise<ResolvedRunPrompt>>();
+ const resolveBaseSystemPrompt = (context: HostModelPromptContext): Promise<ResolvedRunPrompt> => {
if (runProfile) {
return Promise.resolve(
Object.freeze({
@@ -200,7 +205,7 @@ export function createInteractiveRunComposer(input: InteractiveRunComposerInput)
);
}
const key = `${context.sessionId}\u0000${context.turnId}`;
- const cached = resolvedSystemPrompts.get(key);
+ const cached = resolvedBaseSystemPrompts.get(key);
if (cached) return cached;
const pending = Promise.all([
readPromptState(input, context.sessionId, Boolean(childInstruction)),
@@ -253,16 +258,27 @@ export function createInteractiveRunComposer(input: InteractiveRunComposerInput)
});
})
.catch((error: unknown) => {
- if (resolvedSystemPrompts.get(key) === pending) resolvedSystemPrompts.delete(key);
+ if (resolvedBaseSystemPrompts.get(key) === pending) resolvedBaseSystemPrompts.delete(key);
throw error;
});
- resolvedSystemPrompts.set(key, pending);
- if (resolvedSystemPrompts.size > 100) {
- const oldest = resolvedSystemPrompts.keys().next().value;
- if (typeof oldest === 'string' && oldest !== key) resolvedSystemPrompts.delete(oldest);
+ resolvedBaseSystemPrompts.set(key, pending);
+ if (resolvedBaseSystemPrompts.size > 100) {
+ const oldest = resolvedBaseSystemPrompts.keys().next().value;
+ if (typeof oldest === 'string' && oldest !== key) resolvedBaseSystemPrompts.delete(oldest);
}
return pending;
};
+ const resolveSystemPrompt = async (
+ context: HostModelPromptContext,
+ ): Promise<ResolvedRunPrompt> => {
+ const base = await resolveBaseSystemPrompt(context);
+ if (!input.resolveAdditionalSystemPrompt || runProfile) return base;
+ const plugin = await input.resolveAdditionalSystemPrompt(context, base.text);
+ return Object.freeze({
+ text: plugin.text,
+ sourceRevisions: mergeSourceRevisions(base.sourceRevisions, plugin.sourceRevisions),
+ });
+ };
return Object.freeze({
composerId: INTERACTIVE_RUN_COMPOSER_ID,
@@ -288,6 +304,11 @@ export interface InteractiveRunComposerFactoryInput
) => {
readonly tools: readonly MakaTool[];
};
+ readonly resolvePluginSystemPrompt?: (
+ sessionId: string,
+ context: HostModelPromptContext,
+ baseText: string | undefined,
+ ) => Promise<ResolvedRunPrompt>;
readonly childTools?: readonly MakaTool[];
readonly worktreePatchWriteBackAvailable?: boolean;
readonly planStore?: PlanStore;
@@ -426,6 +447,12 @@ export function createInteractiveRunComposerFactory(
},
}
: {}),
+ ...(input.resolvePluginSystemPrompt && !backendContext.tools
+ ? {
+ resolveAdditionalSystemPrompt: (context, baseText) =>
+ input.resolvePluginSystemPrompt!(backendContext.sessionId, context, baseText),
+ }
+ : {}),
...(input.scheduledTaskTool ? { scheduledTaskTool: input.scheduledTaskTool } : {}),
...(input.goalTools ? { goalTools: input.goalTools } : {}),
...(parentAgentTools ? { parentAgentTools } : {}),
@@ -617,6 +644,15 @@ function interactiveSourceRevisions(input: {
]);
}
+function mergeSourceRevisions(
+ base: readonly RunCompositionSourceRevision[],
+ additions: readonly RunCompositionSourceRevision[],
+): readonly RunCompositionSourceRevision[] {
+ const merged = new Map(base.map((revision) => [revision.id, revision]));
+ for (const revision of additions) merged.set(revision.id, revision);
+ return Object.freeze([...merged.values()].sort((left, right) => left.id.localeCompare(right.id)));
+}
+
async function readPromptState(
input: Pick<InteractiveRunComposerInput, 'runtimePolicy' | 'memory'>,
sessionId: string,
diff --git a/packages/runtime-host/src/server/plugin-platform.ts b/packages/runtime-host/src/server/plugin-platform.ts
index b2d9e20271..5136cfdfe2 100644
--- a/packages/runtime-host/src/server/plugin-platform.ts
+++ b/packages/runtime-host/src/server/plugin-platform.ts
@@ -34,6 +34,7 @@ import {
} from '@maka/runtime/plugin-runtime';
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 { validateExtensionConfiguration } from './extension-package-manifest.js';
import { recoverExtensionBundleImports } from './extension-bundle.js';
import { loadPluginCompositionPatch } from './plugin-composition-patch.js';
@@ -76,6 +77,9 @@ export interface HostPluginPlatformOptions {
readonly packageLoader?: TrustedPluginPackageLoader;
readonly store?: HostPluginCompositionStore;
readonly tools?: { inspect(rootId?: MakaPluginRootId): readonly PluginToolInspection[] };
+ readonly systemPrompt?: {
+ inspect(rootId?: MakaPluginRootId): readonly PluginSystemPromptInspection[];
+ };
}
export interface HostPluginPlatformFailure {
@@ -112,6 +116,7 @@ export class HostPluginPlatform {
readonly #packageLoader: TrustedPluginPackageLoader;
readonly #store: HostPluginCompositionStore;
readonly #tools?: HostPluginPlatformOptions['tools'];
+ readonly #systemPrompt?: HostPluginPlatformOptions['systemPrompt'];
#authority: PersistedPluginComposition = emptyCompositionAuthority();
#desired: MakaCompositionState = emptyCompositionState();
@@ -137,6 +142,7 @@ export class HostPluginPlatform {
options.packageLoader ?? new TrustedPluginPackageLoader(controlDirectory, this.#packages);
this.#store = options.store ?? new HostPluginCompositionStore(controlDirectory);
this.#tools = options.tools;
+ this.#systemPrompt = options.systemPrompt;
}
async recover(): Promise<void> {
@@ -479,6 +485,11 @@ export class HostPluginPlatform {
return this.#tools?.inspect(rootId) ?? Object.freeze([]);
}
+ inspectSystemPrompt(rootId?: MakaPluginRootId): readonly PluginSystemPromptInspection[] {
+ this.#assertReadable();
+ return this.#systemPrompt?.inspect(rootId) ?? Object.freeze([]);
+ }
+
async status(): Promise<{
readonly phase: PluginPlatformPhase;
readonly authorityEpoch: number;
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 9e7405eafa..eb0da23799 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -82,6 +82,7 @@
"./plugin-composition-loader": "./dist/plugin-composition-loader.js",
"./plugin-kernel": "./dist/plugin-kernel.js",
"./plugin-runtime": "./dist/plugin-runtime.js",
+ "./plugin-system-prompt-service": "./dist/plugin-system-prompt-service.js",
"./plugin-tool-service": "./dist/plugin-tool-service.js",
"./process-tree-terminator": "./dist/process-tree-terminator.js",
"./provider-request-telemetry": "./dist/provider-request-telemetry.js",
diff --git a/packages/runtime/src/__tests__/plugin-system-prompt-service.test.ts b/packages/runtime/src/__tests__/plugin-system-prompt-service.test.ts
new file mode 100644
index 0000000000..7d792d7e67
--- /dev/null
+++ b/packages/runtime/src/__tests__/plugin-system-prompt-service.test.ts
@@ -0,0 +1,218 @@
+/*
+ * 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 { MakaCompositionLoader } from '../plugin-composition-loader.js';
+import {
+ PLUGIN_SYSTEM_PROMPT_SOURCE_ID,
+ PluginSystemPromptService,
+ type PluginSystemPromptContext,
+} from '../plugin-system-prompt-service.js';
+
+const assemblyContext: PluginSystemPromptContext = {
+ sessionId: 'alpha',
+ turnId: 'turn-1',
+ cwd: '/workspace',
+};
+
+test('Profile sections are inherited and exact Session sections shadow them', async () => {
+ const { loader, prompts } = setup();
+ await loader.install({
+ packageId: 'profile-package',
+ host: (ctx) => ctx.systemPrompt.section({ name: 'plugin:policy', order: 10, text: 'profile' }),
+ });
+ await loader.install({
+ packageId: 'session-package',
+ host: (ctx) => ctx.systemPrompt.section({ name: 'plugin:policy', order: 10, text: 'session' }),
+ });
+ await loader.create('profile', { id: 'profile-entry', packageId: 'profile-package' });
+ await loader.create('session:alpha', { id: 'session-entry', packageId: 'session-package' });
+
+ assert.equal((await prompts.assemble(assemblyContext, 'base')).text, 'base\n\nsession');
+ assert.equal(
+ (await prompts.assemble({ ...assemblyContext, sessionId: 'beta' }, 'base')).text,
+ 'base\n\nprofile',
+ );
+
+ await loader.remove('session-entry');
+ assert.equal((await prompts.assemble(assemblyContext, 'base')).text, 'base\n\nprofile');
+ await loader.close();
+});
+
+test('sections and variables resolve for every assembly from a stable membership snapshot', async () => {
+ const { loader, prompts } = setup();
+ let value = 'first';
+ let late = false;
+ await loader.install({
+ packageId: 'dynamic-package',
+ host: (ctx) => {
+ ctx.systemPrompt.variable('mode', () => value);
+ ctx.systemPrompt.section({
+ name: 'plugin:dynamic',
+ order: 20,
+ text: () => {
+ if (!late) {
+ late = true;
+ ctx.systemPrompt.section({ name: 'plugin:late', order: 30, text: 'late' });
+ }
+ return 'mode={{mode}}';
+ },
+ });
+ },
+ });
+ await loader.create('profile', { id: 'dynamic-entry', packageId: 'dynamic-package' });
+
+ const first = await prompts.assemble(assemblyContext, 'base');
+ value = 'second';
+ const second = await prompts.assemble(assemblyContext, 'base');
+ assert.equal(first.text, 'base\n\nmode=first');
+ assert.equal(second.text, 'base\n\nmode=second\n\nlate');
+ assert.equal(first.sourceRevision?.id, PLUGIN_SYSTEM_PROMPT_SOURCE_ID);
+ assert.notEqual(first.sourceRevision?.revision, second.sourceRevision?.revision);
+ await loader.close();
+});
+
+test('complete sections replace the Host base and multiple complete sections fail closed', async () => {
+ const { loader, prompts } = setup();
+ await loader.install({
+ packageId: 'complete-package',
+ host: (ctx) => {
+ ctx.systemPrompt.section({ name: 'plugin:extra', order: 1, text: 'extra' });
+ ctx.systemPrompt.section({
+ name: 'plugin:complete',
+ order: 2,
+ text: 'replacement',
+ complete: true,
+ });
+ },
+ });
+ await loader.create('profile', { id: 'complete-entry', packageId: 'complete-package' });
+ assert.equal((await prompts.assemble(assemblyContext, 'base')).text, 'replacement');
+
+ await loader.install({
+ packageId: 'second-complete-package',
+ host: (ctx) =>
+ ctx.systemPrompt.section({
+ name: 'plugin:second-complete',
+ order: 3,
+ text: 'second',
+ complete: true,
+ }),
+ });
+ await loader.create('profile', {
+ id: 'second-complete-entry',
+ packageId: 'second-complete-package',
+ });
+ await assert.rejects(() => prompts.assemble(assemblyContext, 'base'), /Multiple complete/u);
+ await loader.close();
+});
+
+test('failed activation publishes no partial System Prompt contribution', async () => {
+ const { loader, prompts } = setup();
+ await loader.install({
+ packageId: 'broken-package',
+ host: (ctx) => {
+ ctx.systemPrompt.section({ name: 'plugin:partial', order: 1, text: 'partial' });
+ throw new Error('activation failed');
+ },
+ });
+
+ await assert.rejects(
+ () => loader.create('profile', { id: 'broken-entry', packageId: 'broken-package' }),
+ /activation failed/u,
+ );
+ assert.deepEqual(prompts.inspect(), []);
+ assert.equal((await prompts.assemble(assemblyContext, 'base')).text, 'base');
+ await loader.close();
+});
+
+test('replacement restores the prior generation when the candidate rolls back', async () => {
+ const { loader, prompts } = setup();
+ await loader.install({
+ packageId: 'replaceable-package',
+ host: (ctx) =>
+ ctx.systemPrompt.section({ name: 'plugin:replaceable', order: 1, text: 'current' }),
+ });
+ await loader.create('profile', { id: 'replaceable-entry', packageId: 'replaceable-package' });
+
+ await assert.rejects(
+ () =>
+ loader.reload({
+ packageId: 'replaceable-package',
+ host: (ctx) => {
+ ctx.systemPrompt.section({ name: 'plugin:replaceable', order: 1, text: 'candidate' });
+ throw new Error('candidate failed');
+ },
+ }),
+ /candidate failed/u,
+ );
+ assert.equal((await prompts.assemble(assemblyContext, 'base')).text, 'base\n\ncurrent');
+ await loader.close();
+});
+
+test('successful replacement does not resurrect its retired predecessor', async () => {
+ const { loader, prompts } = setup();
+ await loader.install({
+ packageId: 'replaceable-package',
+ host: (ctx) => ctx.systemPrompt.section({ name: 'plugin:replaceable', order: 1, text: 'old' }),
+ });
+ await loader.create('profile', { id: 'replaceable-entry', packageId: 'replaceable-package' });
+ await loader.reload({
+ packageId: 'replaceable-package',
+ host: (ctx) => ctx.systemPrompt.section({ name: 'plugin:replaceable', order: 1, text: 'new' }),
+ });
+ assert.equal((await prompts.assemble(assemblyContext, 'base')).text, 'base\n\nnew');
+
+ await loader.remove('replaceable-entry');
+ assert.equal((await prompts.assemble(assemblyContext, 'base')).text, 'base');
+ await loader.close();
+});
+
+test('desktop-ui registration and unresolved variables fail closed', async () => {
+ const { loader, prompts } = setup();
+ await loader.install({
+ packageId: 'ui-package',
+ host: (ctx) => ctx.systemPrompt.section({ name: 'plugin:ui', order: 1, text: 'ui' }),
+ });
+ await assert.rejects(
+ () => loader.create('desktop-ui', { id: 'ui-entry', packageId: 'ui-package' }),
+ /desktop-ui plugins cannot contribute Host System Prompt/u,
+ );
+
+ await loader.install({
+ packageId: 'variable-package',
+ host: (ctx) =>
+ ctx.systemPrompt.section({ name: 'plugin:missing', order: 1, text: '{{missing}}' }),
+ });
+ await loader.create('profile', { id: 'variable-entry', packageId: 'variable-package' });
+ await assert.rejects(
+ () => prompts.assemble(assemblyContext, 'base'),
+ /Unknown System Prompt variable/u,
+ );
+ await loader.close();
+});
+
+function setup(): { loader: MakaCompositionLoader; prompts: PluginSystemPromptService } {
+ const root = new Context();
+ const prompts = new PluginSystemPromptService(root);
+ const loader = new MakaCompositionLoader({ root });
+ return { loader, prompts };
+}
diff --git a/packages/runtime/src/plugin-system-prompt-service.ts b/packages/runtime/src/plugin-system-prompt-service.ts
new file mode 100644
index 0000000000..2260a41852
--- /dev/null
+++ b/packages/runtime/src/plugin-system-prompt-service.ts
@@ -0,0 +1,521 @@
+/*
+ * 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 { Service, type Awaitable, type Context, type Disposable } from './plugin-kernel.js';
+import {
+ MakaPluginRuntimeError,
+ pluginIdentity,
+ registerPluginContribution,
+ type MakaContributionIdentity,
+ type MakaPluginRootId,
+} from './plugin-runtime.js';
+
+declare module './plugin-kernel.js' {
+ interface Context {
+ readonly systemPrompt: PluginSystemPromptService;
+ }
+}
+
+const PROMPT_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u;
+const VARIABLE_NAME_PATTERN = /^[a-z][a-z0-9_]*$/u;
+const VARIABLE_REFERENCE_PATTERN = /\{\{([^{}]*)\}\}/gu;
+const HOST_BASE_SECTION = 'maka:base';
+
+export const PLUGIN_SYSTEM_PROMPT_SOURCE_ID = 'plugin.system-prompt';
+
+export interface PluginSystemPromptContext {
+ readonly sessionId: string;
+ readonly turnId: string;
+ readonly cwd: string;
+}
+
+export type PluginSystemPromptText =
+ | string
+ | ((context: PluginSystemPromptContext) => Awaitable<string | undefined>);
+
+export interface PluginSystemPromptSection {
+ readonly name: string;
+ readonly order: number;
+ readonly text: PluginSystemPromptText;
+ /** Replaces the Host base and every non-complete section for this scope. */
+ readonly complete?: boolean;
+}
+
+export type PluginSystemPromptVariableProvider = (
+ context: PluginSystemPromptContext,
+) => Awaitable<string | undefined>;
+
+export interface PluginSystemPromptAssembly {
+ readonly text: string | undefined;
+ readonly sourceRevision?: { readonly id: string; readonly revision: string };
+}
+
+export interface PluginSystemPromptInspection extends MakaContributionIdentity {
+ readonly kind: 'section' | 'variable';
+ readonly name: string;
+ readonly order?: number;
+ readonly complete?: boolean;
+}
+
+interface RegisteredSection extends MakaContributionIdentity {
+ readonly definition: PluginSystemPromptSection;
+ readonly token: symbol;
+ retired: boolean;
+}
+
+interface RegisteredVariable extends MakaContributionIdentity {
+ readonly name: string;
+ readonly provider: PluginSystemPromptVariableProvider;
+ readonly token: symbol;
+ retired: boolean;
+}
+
+interface PromptLayer {
+ readonly sections: Map<string, RegisteredSection>;
+ readonly variables: Map<string, RegisteredVariable>;
+}
+
+interface ResolvedSection extends MakaContributionIdentity {
+ readonly name: string;
+ readonly order: number;
+ readonly text: string;
+ readonly complete: boolean;
+}
+
+/**
+ * Context-scoped, Fiber-owned System Prompt registry for trusted Host plugins.
+ *
+ * Profile contributions are inherited by Session roots. A Session contribution
+ * with the same name shadows its Profile counterpart before either provider is
+ * evaluated. Every assembly snapshots membership and resolves providers anew,
+ * so changes committed by one tool call appear at the next logical model step.
+ */
+export class PluginSystemPromptService extends Service {
+ private readonly layers = new Map<MakaPluginRootId, PromptLayer>();
+
+ constructor(ctx: Context) {
+ super(ctx, 'systemPrompt');
+ }
+
+ section(definition: PluginSystemPromptSection): Disposable<Promise<void>> {
+ const identity = pluginIdentity(this.ctx);
+ assertHostPromptScope(identity.scopeId);
+ validateSection(definition);
+ return registerPluginContribution(
+ this.ctx,
+ `systemPrompt.section(${JSON.stringify(definition.name)})`,
+ () => this.publishSection(identity, definition),
+ );
+ }
+
+ variable(name: string, provider: PluginSystemPromptVariableProvider): Disposable<Promise<void>> {
+ const identity = pluginIdentity(this.ctx);
+ assertHostPromptScope(identity.scopeId);
+ validateVariable(name, provider);
+ return registerPluginContribution(
+ this.ctx,
+ `systemPrompt.variable(${JSON.stringify(name)})`,
+ () => this.publishVariable(identity, name, provider),
+ );
+ }
+
+ async assemble(
+ context: PluginSystemPromptContext,
+ baseText: string | undefined,
+ ): Promise<PluginSystemPromptAssembly> {
+ validateAssemblyContext(context);
+ const visibleSections = this.visible(context.sessionId, (layer) => layer.sections);
+ const visibleVariables = this.visible(context.sessionId, (layer) => layer.variables);
+ const sectionSnapshot = [...visibleSections.values()];
+ const variableSnapshot = [...visibleVariables.values()];
+
+ if (sectionSnapshot.length === 0 && variableSnapshot.length === 0) {
+ return Object.freeze({ text: baseText });
+ }
+
+ const variables: Record<string, string | undefined> = {};
+ for (const variable of variableSnapshot.sort(compareRegistration)) {
+ const value = await variable.provider(context);
+ if (value !== undefined && typeof value !== 'string') {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ `System Prompt variable ${JSON.stringify(variable.name)} returned a non-string value`,
+ );
+ }
+ variables[variable.name] = value;
+ }
+ const resolved: ResolvedSection[] = [];
+ for (const section of sectionSnapshot) {
+ const value =
+ typeof section.definition.text === 'string'
+ ? section.definition.text
+ : await section.definition.text(context);
+ if (value !== undefined && typeof value !== 'string') {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ `System Prompt section ${JSON.stringify(section.definition.name)} returned a non-string value`,
+ );
+ }
+ resolved.push({
+ ...section,
+ name: section.definition.name,
+ order: section.definition.order,
+ text: value ?? '',
+ complete: section.definition.complete === true,
+ });
+ }
+ const complete = resolved.filter((section) => section.complete);
+ if (complete.length > 1) {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ `Multiple complete System Prompt sections are active: ${complete
+ .map(({ name }) => JSON.stringify(name))
+ .sort()
+ .join(', ')}`,
+ );
+ }
+ const effective = complete.length
+ ? complete
+ : [
+ ...(baseText === undefined
+ ? []
+ : [
+ {
+ entryId: HOST_BASE_SECTION,
+ scopeId: 'profile',
+ extensionId: 'maka',
+ generation: 0,
+ name: HOST_BASE_SECTION,
+ order: 0,
+ text: baseText,
+ complete: false,
+ } satisfies ResolvedSection,
+ ]),
+ ...resolved,
+ ];
+ const rendered = effective
+ .sort(compareSections)
+ .map((section) => interpolate(section.name, section.text, variables))
+ .filter(Boolean)
+ .join('\n\n');
+ const revision = promptRevision(resolved, variableSnapshot, variables);
+ return Object.freeze({
+ text: rendered || undefined,
+ sourceRevision: Object.freeze({
+ id: PLUGIN_SYSTEM_PROMPT_SOURCE_ID,
+ revision,
+ }),
+ });
+ }
+
+ inspect(rootId?: MakaPluginRootId): readonly PluginSystemPromptInspection[] {
+ const layers = rootId ? [[rootId, this.layers.get(rootId)] as const] : [...this.layers];
+ return Object.freeze(
+ layers
+ .flatMap(([, layer]) => [
+ ...[...(layer?.sections.values() ?? [])].map((entry) => ({
+ entryId: entry.entryId,
+ scopeId: entry.scopeId,
+ extensionId: entry.extensionId,
+ generation: entry.generation,
+ kind: 'section' as const,
+ name: entry.definition.name,
+ order: entry.definition.order,
+ complete: entry.definition.complete === true,
+ })),
+ ...[...(layer?.variables.values() ?? [])].map((entry) => ({
+ entryId: entry.entryId,
+ scopeId: entry.scopeId,
+ extensionId: entry.extensionId,
+ generation: entry.generation,
+ kind: 'variable' as const,
+ name: entry.name,
+ })),
+ ])
+ .sort(compareInspection),
+ );
+ }
+
+ private visible<T>(
+ sessionId: string,
+ select: (layer: PromptLayer) => Map<string, T>,
+ ): Map<string, T> {
+ const visible = new Map<string, T>();
+ for (const [name, entry] of select(this.layers.get('profile') ?? emptyLayer())) {
+ visible.set(name, entry);
+ }
+ const session = this.layers.get(`session:${sessionId}`);
+ if (session) {
+ for (const [name, entry] of select(session)) visible.set(name, entry);
+ }
+ return visible;
+ }
+
+ private publishSection(
+ identity: MakaContributionIdentity,
+ definition: PluginSystemPromptSection,
+ ): Disposable<Promise<void>> {
+ const layer = this.layer(identity.scopeId as MakaPluginRootId);
+ const existing = layer.sections.get(definition.name);
+ assertOwner(existing, identity, 'section', definition.name);
+ const entry: RegisteredSection = {
+ ...identity,
+ definition: Object.freeze({ ...definition }),
+ token: Symbol(definition.name),
+ retired: false,
+ };
+ layer.sections.set(definition.name, entry);
+ return this.retire(
+ identity.scopeId as MakaPluginRootId,
+ layer.sections,
+ definition.name,
+ entry,
+ existing,
+ );
+ }
+
+ private publishVariable(
+ identity: MakaContributionIdentity,
+ name: string,
+ provider: PluginSystemPromptVariableProvider,
+ ): Disposable<Promise<void>> {
+ const layer = this.layer(identity.scopeId as MakaPluginRootId);
+ const existing = layer.variables.get(name);
+ assertOwner(existing, identity, 'variable', name);
+ const entry: RegisteredVariable = {
+ ...identity,
+ name,
+ provider,
+ token: Symbol(name),
+ retired: false,
+ };
+ layer.variables.set(name, entry);
+ return this.retire(
+ identity.scopeId as MakaPluginRootId,
+ layer.variables,
+ name,
+ entry,
+ existing,
+ );
+ }
+
+ private retire<T extends { readonly token: symbol; retired: boolean }>(
+ rootId: MakaPluginRootId,
+ registry: Map<string, T>,
+ name: string,
+ entry: T,
+ previous: T | undefined,
+ ): Disposable<Promise<void>> {
+ let retired = false;
+ return async () => {
+ if (retired) return;
+ retired = true;
+ entry.retired = true;
+ if (registry.get(name)?.token !== entry.token) return;
+ if (previous && !previous.retired) registry.set(name, previous);
+ else registry.delete(name);
+ this.prune(rootId);
+ };
+ }
+
+ private layer(rootId: MakaPluginRootId): PromptLayer {
+ let layer = this.layers.get(rootId);
+ if (!layer) {
+ layer = emptyLayer();
+ this.layers.set(rootId, layer);
+ }
+ return layer;
+ }
+
+ private prune(rootId: MakaPluginRootId): void {
+ const layer = this.layers.get(rootId);
+ if (layer && layer.sections.size === 0 && layer.variables.size === 0) {
+ this.layers.delete(rootId);
+ }
+ }
+}
+
+function emptyLayer(): PromptLayer {
+ return { sections: new Map(), variables: new Map() };
+}
+
+function assertHostPromptScope(scopeId: string): void {
+ if (scopeId === 'desktop-ui') {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ 'desktop-ui plugins cannot contribute Host System Prompt sections',
+ );
+ }
+}
+
+function validateSection(section: PluginSystemPromptSection): void {
+ validateName(section.name, 'System Prompt section');
+ if (section.name === HOST_BASE_SECTION) {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ 'The Host base System Prompt is reserved',
+ );
+ }
+ if (!Number.isFinite(section.order)) {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ 'System Prompt section order must be finite',
+ );
+ }
+ if (typeof section.text !== 'string' && typeof section.text !== 'function') {
+ throw new MakaPluginRuntimeError('activation_failed', 'System Prompt section text is invalid');
+ }
+}
+
+function validateVariable(name: string, provider: PluginSystemPromptVariableProvider): void {
+ if (!VARIABLE_NAME_PATTERN.test(name) || Buffer.byteLength(name, 'utf8') > 128) {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ `Invalid System Prompt variable: ${name}`,
+ );
+ }
+ if (typeof provider !== 'function') {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ 'System Prompt variable provider is invalid',
+ );
+ }
+}
+
+function validateName(name: string, label: string): void {
+ if (!PROMPT_NAME_PATTERN.test(name) || Buffer.byteLength(name, 'utf8') > 128) {
+ throw new MakaPluginRuntimeError('activation_failed', `Invalid ${label} name: ${name}`);
+ }
+}
+
+function validateAssemblyContext(context: PluginSystemPromptContext): void {
+ if (
+ !context.sessionId ||
+ !context.turnId ||
+ !context.cwd ||
+ /[\0\r\n]/u.test(context.sessionId) ||
+ /[\0\r\n]/u.test(context.turnId)
+ ) {
+ throw new Error('Invalid System Prompt assembly context');
+ }
+}
+
+function assertOwner(
+ current: MakaContributionIdentity | undefined,
+ identity: MakaContributionIdentity,
+ kind: string,
+ name: string,
+): void {
+ if (current && current.entryId !== identity.entryId) {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ `System Prompt ${kind} ${JSON.stringify(name)} is already registered by ${current.entryId}`,
+ );
+ }
+}
+
+function interpolate(
+ sectionName: string,
+ text: string,
+ variables: Readonly<Record<string, string | undefined>>,
+): string {
+ VARIABLE_REFERENCE_PATTERN.lastIndex = 0;
+ return text.replace(VARIABLE_REFERENCE_PATTERN, (reference, rawName: string) => {
+ if (!VARIABLE_NAME_PATTERN.test(rawName)) {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ `Malformed System Prompt variable ${JSON.stringify(reference)} in section ${JSON.stringify(sectionName)}`,
+ );
+ }
+ if (!Object.hasOwn(variables, rawName)) {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ `Unknown System Prompt variable ${JSON.stringify(rawName)} in section ${JSON.stringify(sectionName)}`,
+ );
+ }
+ const value = variables[rawName];
+ if (value === undefined) {
+ throw new MakaPluginRuntimeError(
+ 'activation_failed',
+ `System Prompt variable ${JSON.stringify(rawName)} has no value in section ${JSON.stringify(sectionName)}`,
+ );
+ }
+ return value;
+ });
+}
+
+function promptRevision(
+ sections: readonly ResolvedSection[],
+ variables: readonly RegisteredVariable[],
+ values: Readonly<Record<string, string | undefined>>,
+): string {
+ const canonical = {
+ sections: [...sections].sort(compareSections).map((section) => ({
+ scopeId: section.scopeId,
+ entryId: section.entryId,
+ extensionId: section.extensionId,
+ generation: section.generation,
+ name: section.name,
+ order: section.order,
+ complete: section.complete,
+ text: section.text,
+ })),
+ variables: [...variables].sort(compareRegistration).map((variable) => ({
+ scopeId: variable.scopeId,
+ entryId: variable.entryId,
+ extensionId: variable.extensionId,
+ generation: variable.generation,
+ name: variable.name,
+ value: values[variable.name] ?? null,
+ })),
+ };
+ return `sha256:${createHash('sha256').update(JSON.stringify(canonical)).digest('hex')}`;
+}
+
+function compareSections(left: ResolvedSection, right: ResolvedSection): number {
+ return left.order - right.order || compareCodeUnits(left.name, right.name);
+}
+
+function compareRegistration(
+ left: MakaContributionIdentity,
+ right: MakaContributionIdentity,
+): number {
+ return (
+ compareCodeUnits(left.scopeId, right.scopeId) ||
+ compareCodeUnits(left.entryId, right.entryId) ||
+ left.generation - right.generation
+ );
+}
+
+function compareInspection(
+ left: PluginSystemPromptInspection,
+ right: PluginSystemPromptInspection,
+): number {
+ return (
+ compareCodeUnits(left.scopeId, right.scopeId) ||
+ compareCodeUnits(left.kind, right.kind) ||
+ compareCodeUnits(left.name, right.name) ||
+ compareCodeUnits(left.entryId, right.entryId)
+ );
+}
+
+function compareCodeUnits(left: string, right: string): number {
+ return left < right ? -1 : left > right ? 1 : 0;
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org