blob: ea298d1da1c2a928c5020aa6f5d396c7d443ff1c [file]
/*
* 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 { AsyncLocalStorage } from 'node:async_hooks';
import type { PermissionMode } from '@maka/core/permission';
import type { ExecutionBoundary } from '@maka/core/sandbox-boundary';
import type { AgentProfile } from './agent-catalog.js';
import { Service, type Context, type Disposable } from './plugin-kernel.js';
import { pluginInvocationSignal } from './plugin-invocation-signal.js';
import type { MakaToolContext } from './tool-runtime.js';
declare module './plugin-kernel.js' {
interface Context {
readonly agents: PluginAgentService;
readonly agent?: PluginAgent;
}
}
export interface PluginAgentInvocation {
readonly sessionId: string;
readonly runId?: string;
readonly turnId: string;
readonly cwd: string;
readonly permissionMode?: PermissionMode;
readonly executionBoundary?: ExecutionBoundary;
readonly toolCallId?: string;
readonly abortSignal: AbortSignal;
readonly toolContext?: MakaToolContext;
}
export interface PluginAgentDescriptor {
readonly id: string;
readonly sessionId: string;
readonly root: boolean;
readonly status?: string;
readonly ownerId?: string;
}
export interface PluginAgentCreateOptions {
readonly sessionId?: string;
readonly cwd?: string;
readonly prompt?: string;
readonly agentProfile?: AgentProfile;
readonly executorId?: string;
readonly model?: string;
readonly permissionMode?: PermissionMode;
readonly signal?: AbortSignal;
}
export interface PluginAgentResumeOptions {
readonly sessionId: string;
readonly prompt?: string;
readonly signal?: AbortSignal;
}
export interface PluginAgentRuntime {
create(
options: PluginAgentCreateOptions,
initiator: PluginAgentInvocation,
): Promise<PluginAgentDescriptor>;
resume(
options: PluginAgentResumeOptions,
initiator: PluginAgentInvocation,
): Promise<PluginAgentDescriptor>;
get(id: string, initiator: PluginAgentInvocation): Promise<PluginAgentDescriptor | undefined>;
list(initiator: PluginAgentInvocation): Promise<readonly PluginAgentDescriptor[]>;
roots(initiator: PluginAgentInvocation): Promise<readonly PluginAgentDescriptor[]>;
followup(id: string, message: unknown, initiator: PluginAgentInvocation): Promise<unknown>;
steer(id: string, message: unknown, initiator: PluginAgentInvocation): Promise<unknown>;
inject(id: string, message: unknown, initiator: PluginAgentInvocation): Promise<unknown>;
cancel(id: string, initiator: PluginAgentInvocation): Promise<unknown>;
whenIdle(
id: string,
signal: AbortSignal | undefined,
initiator: PluginAgentInvocation,
): Promise<void>;
snapshot(id: string, initiator: PluginAgentInvocation): Promise<unknown>;
inbox(id: string, initiator: PluginAgentInvocation): Promise<unknown>;
result(id: string, initiator: PluginAgentInvocation): Promise<unknown>;
artifacts(id: string, initiator: PluginAgentInvocation): Promise<unknown>;
transcript(id: string, initiator: PluginAgentInvocation): Promise<unknown>;
dispose(id: string, initiator: PluginAgentInvocation): Promise<void>;
}
export interface PluginAgent {
readonly id: string;
readonly sessionId: string;
readonly root: boolean;
readonly status?: string;
readonly ownerId?: string;
followup(message: unknown): Promise<unknown>;
steer(message: unknown): Promise<unknown>;
inject(message: unknown): Promise<unknown>;
cancel(): Promise<unknown>;
whenIdle(signal?: AbortSignal): Promise<void>;
snapshot(): Promise<unknown>;
inbox(): Promise<unknown>;
result(): Promise<unknown>;
artifacts(): Promise<unknown>;
transcript(): Promise<unknown>;
dispose(): Promise<void>;
}
/** Agent registry and invocation carrier exposed to trusted Host plugins. */
export class PluginAgentService extends Service {
private readonly invocations = new AsyncLocalStorage<PluginAgentInvocation>();
private agentRuntime: PluginAgentRuntime | undefined;
constructor(ctx: Context) {
super(ctx, 'agents');
ctx.accessor('agent', {
get: () => this.current(),
});
}
bindRuntime(runtime: PluginAgentRuntime): Disposable<Promise<void>> {
if (this.ctx.maka) throw new Error('Only the Host may bind the Agent Runtime');
if (this.agentRuntime) throw new Error('Plugin Agent Runtime is already bound');
this.agentRuntime = runtime;
return this.ctx.effect(
() => () => {
if (this.agentRuntime === runtime) this.agentRuntime = undefined;
},
'agents.bindRuntime()',
);
}
currentInvocation(): PluginAgentInvocation | undefined {
return this.invocations.getStore();
}
requireInvocation(): PluginAgentInvocation {
const invocation = this.currentInvocation();
if (!invocation) throw new Error('This capability requires an active Agent invocation');
return invocation;
}
withInvocation<T>(toolContext: MakaToolContext, operation: () => T): T {
const invocation: PluginAgentInvocation = Object.freeze({
sessionId: toolContext.sessionId,
...(toolContext.runId ? { runId: toolContext.runId } : {}),
turnId: toolContext.turnId,
cwd: toolContext.cwd,
...(toolContext.permissionMode ? { permissionMode: toolContext.permissionMode } : {}),
...(toolContext.executionBoundary
? { executionBoundary: toolContext.executionBoundary }
: {}),
toolCallId: toolContext.toolCallId,
abortSignal: toolContext.abortSignal,
toolContext,
});
return this.invocations.run(invocation, operation);
}
current(): PluginAgent | undefined {
const invocation = this.currentInvocation();
if (!invocation) return undefined;
return this.handle({
id: invocation.sessionId,
sessionId: invocation.sessionId,
root: true,
status: 'running',
});
}
async create(options: PluginAgentCreateOptions = {}): Promise<PluginAgent> {
const invocation = this.requireInvocation();
return this.handle(
await this.runtime().create(
{ ...options, signal: pluginInvocationSignal(invocation.abortSignal, options.signal) },
invocation,
),
invocation,
);
}
async resume(options: PluginAgentResumeOptions): Promise<PluginAgent> {
const invocation = this.requireInvocation();
return this.handle(
await this.runtime().resume(
{ ...options, signal: pluginInvocationSignal(invocation.abortSignal, options.signal) },
invocation,
),
invocation,
);
}
async get(id: string): Promise<PluginAgent | undefined> {
const invocation = this.requireInvocation();
const descriptor = await this.runtime().get(assertId(id), invocation);
return descriptor ? this.handle(descriptor, invocation) : undefined;
}
async list(): Promise<readonly PluginAgent[]> {
const invocation = this.requireInvocation();
return Object.freeze(
(await this.runtime().list(invocation)).map((descriptor) =>
this.handle(descriptor, invocation),
),
);
}
async roots(): Promise<readonly PluginAgent[]> {
const invocation = this.requireInvocation();
return Object.freeze(
(await this.runtime().roots(invocation)).map((descriptor) =>
this.handle(descriptor, invocation),
),
);
}
private runtime(): PluginAgentRuntime {
if (!this.agentRuntime) throw new Error('Plugin Agent Runtime is unavailable');
return this.agentRuntime;
}
private handle(
descriptor: PluginAgentDescriptor,
invocation: PluginAgentInvocation = this.requireInvocation(),
): PluginAgent {
const service = this;
const id = assertId(descriptor.id);
return Object.freeze({
...descriptor,
id,
followup: (message: unknown) => service.runtime().followup(id, message, invocation),
steer: (message: unknown) => service.runtime().steer(id, message, invocation),
inject: (message: unknown) => service.runtime().inject(id, message, invocation),
cancel: () => service.runtime().cancel(id, invocation),
whenIdle: (signal?: AbortSignal) =>
service
.runtime()
.whenIdle(id, pluginInvocationSignal(invocation.abortSignal, signal), invocation),
snapshot: () => service.runtime().snapshot(id, invocation),
inbox: () => service.runtime().inbox(id, invocation),
result: () => service.runtime().result(id, invocation),
artifacts: () => service.runtime().artifacts(id, invocation),
transcript: () => service.runtime().transcript(id, invocation),
dispose: () => service.runtime().dispose(id, invocation),
});
}
}
function assertId(id: string): string {
if (!id || /[\0\r\n]/u.test(id)) throw new TypeError('Agent id is invalid');
return id;
}