blob: 45f6aa088059ec94ca3f96087d3bbe6c1ff421a3 [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 type { MakaTool } from '@maka/runtime/tool-runtime';
const TASKS = 'mcp__desktop_workhub__tasks';
const REMINDER =
'Before answering or performing other work, record this turn’s topic with tasks associate. Use fresh candidates and conversation context to resolve omitted subjects. Use targetSessionId: null and a reason only for unrelated, overview, or genuinely ambiguous requests. A prior turn’s association does not label this turn. Do not explain this internal step to the user.';
/** Per-composer, per-turn prerequisite. Reconstructed backends safely re-associate. */
export function createWorkHubTopicGate() {
const associated = new Set<string>();
const pending = (turnId: string) => !associated.has(turnId);
const check = (name: string, args: any, turnId: string) => {
if (!pending(turnId)) return;
if (name === 'WorkHubInspect' || name === 'WorkHubResult') return;
if (name === TASKS && ['candidates', 'associate'].includes(args?.request?.operation)) return;
throw new Error(`WorkHub topic association required. ${REMINDER}`);
};
const observe = (name: string, args: any, turnId: string, output: unknown) => {
if (name !== TASKS || args?.request?.operation !== 'associate') return;
if (!output || typeof output !== 'object' || ('isError' in output && output.isError)) return;
const result = 'structuredContent' in output ? output.structuredContent : output;
if (
!result ||
typeof result !== 'object' ||
!('operation' in result) ||
result.operation !== 'associate' ||
!('targetSessionId' in result) ||
result.targetSessionId !== args.request.targetSessionId
)
return;
if (
result.targetSessionId !== null &&
(!('targetSessionKey' in result) || typeof result.targetSessionKey !== 'string')
)
return;
associated.add(turnId);
if (associated.size > 100) associated.delete(associated.values().next().value!);
};
return {
prompt(turnId: string) {
return pending(turnId)
? {
requireToolCall: true as const,
contexts: [{ name: 'workhub-topic-required', text: REMINDER }],
}
: {};
},
wrap(tool: MakaTool): MakaTool {
return {
...tool,
...(tool.prepareExecution
? {
prepareExecution: async (args, context) => {
check(tool.name, args, context.turnId);
const prepared = await tool.prepareExecution!(args, context);
return {
...prepared,
execute: async (executionContext) => {
check(tool.name, args, context.turnId);
const output = await prepared.execute(executionContext);
observe(tool.name, args, context.turnId, output);
return output;
},
};
},
}
: {}),
impl: async (args, context) => {
check(tool.name, args, context.turnId);
const output = await tool.impl(args, context);
observe(tool.name, args, context.turnId, output);
return output;
},
};
},
};
}